pax_global_header00006660000000000000000000000064117460015600014512gustar00rootroot0000000000000052 comment=df2fa81d8c661d6494be47675cfba0f18bdd87bf mcollective-plugins-0.0.0~git20120507.df2fa81/000077500000000000000000000000001174600156000203515ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/README.markdown000066400000000000000000000002621174600156000230520ustar00rootroot00000000000000A number of plugins for The Marionette Collective. For documentation about these plugins please see [the wiki](http://projects.puppetlabs.com/projects/mcollective-plugins/wiki) mcollective-plugins-0.0.0~git20120507.df2fa81/Rakefile000066400000000000000000000005321174600156000220160ustar00rootroot00000000000000specdir = File.join([File.dirname(__FILE__), "spec"]) require "#{specdir}/spec_helper.rb" require 'rake' require 'rspec/core/rake_task' desc "Run agent and application tests" RSpec::Core::RakeTask.new(:test) do |t| t.pattern = 'agent/**/spec/*_spec.rb' t.rspec_opts = File.read("#{specdir}/spec.opts").chomp end task :default => :test mcollective-plugins-0.0.0~git20120507.df2fa81/agent/000077500000000000000000000000001174600156000214475ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/dsh/000077500000000000000000000000001174600156000222255ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/dsh/README.md000066400000000000000000000006731174600156000235120ustar00rootroot00000000000000DSH Application =============== This application provides an example of using mcollective discovery to populate the machines list of Dancer's Shell. This is so you can execute remote commands using SSH, but with the power of MCollective discovery. Examples -------- To do a concurrent ls -la on all hosts. mco dsh -- -c ls -la To run df -h sequentially on all Debian hosts: mco dsh --wf='operatingsystem=Debian' -- -c -- ls -la mcollective-plugins-0.0.0~git20120507.df2fa81/agent/dsh/application/000077500000000000000000000000001174600156000245305ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/dsh/application/dsh.rb000066400000000000000000000005621174600156000256360ustar00rootroot00000000000000# Dancers Shell Application class MCollective::Application::Dsh "filemgr", :description => "File Manager", :author => "Mike Pountney ", :license => "Apache 2", :version => "0.3", :url => "http://www.puppetlabs.com/mcollective", :timeout => 5 action "touch", :description => "Creates an empty file or touch it's timestamp" do input :file, :prompt => "File", :description => "File to touch", :type => :string, :validation => '^.+$', :optional => true, :maxlength => 256 end action "remove", :description => "Removes a file" do input :file, :prompt => "File", :description => "File to remove", :type => :string, :validation => '^.+$', :optional => true, :maxlength => 256 end action "status", :description => "Basic information about a file" do display :always input :file, :prompt => "File", :description => "File to get information for", :type => :string, :validation => '^.+$', :optional => true, :maxlength => 256 output :name, :description => "File name", :display_as => "Name" output :output, :description => "Human readable information about the file", :display_as => "Status" output :present, :description => "Indicates if the file exist using 0 or 1", :display_as => "Present" output :size, :description => "File size", :display_as => "Size" output :mode, :description => "File mode", :display_as => "Mode" output :md5, :description => "File MD5 digest", :display_as => "MD5" output :mtime, :description => "File modification time", :display_as => "Modification time" output :ctime, :description => "File change time", :display_as => "Change time" output :atime, :description => "File access time", :display_as => "Access time" output :mtime_seconds, :description => "File modification time in seconds", :display_as => "Modification time" output :ctime_seconds, :description => "File change time in seconds", :display_as => "Change time" output :atime_seconds, :description => "File access time in seconds", :display_as => "Access time" output :uid, :description => "File owner", :display_as => "Owner" output :gid, :description => "File group", :display_as => "Group" output :type, :description => "File type", :display_as => "Type" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/filemgr/agent/filemgr.rb000066400000000000000000000074131174600156000261510ustar00rootroot00000000000000require 'fileutils' require 'digest/md5' module MCollective module Agent # A basic file management agent, you can touch, remove or inspec files. # # A common use case for this plugin is to test your mcollective setup # as such if you just call the touch/info/remove actions with no arguments # it will default to the file /var/run/mcollective.plugin.filemgr.touch # or whatever is specified in the plugin.filemgr.touch_file setting class Filemgr "filemgr", :description => "File Manager", :author => "Mike Pountney ", :license => "Apache 2", :version => "1.0", :url => "http://www.puppetlabs.com/mcollective", :timeout => 5 # Basic file touch action - create (empty) file if it doesn't exist, # update last mod time otherwise. # useful for checking if mcollective is operational, via NRPE or similar. action "touch" do touch end # Basic file removal action action "remove" do remove end # Basic status of a file action "status" do status end private def get_filename request[:file] || config.pluginconf["filemgr.touch_file"] || "/var/run/mcollective.plugin.filemgr.touch" end def status file = get_filename reply[:name] = file reply[:output] = "not present" reply[:type] = "unknown" reply[:mode] = "0000" reply[:present] = 0 reply[:size] = 0 reply[:mtime] = 0 reply[:ctime] = 0 reply[:atime] = 0 reply[:mtime_seconds] = 0 reply[:ctime_seconds] = 0 reply[:atime_seconds] = 0 reply[:md5] = 0 reply[:uid] = 0 reply[:gid] = 0 if File.exists?(file) logger.debug("Asked for status of '#{file}' - it is present") reply[:output] = "present" reply[:present] = 1 if File.symlink?(file) stat = File.lstat(file) else stat = File.stat(file) end [:size, :mtime, :ctime, :atime, :uid, :gid].each do |item| reply[item] = stat.send(item) end [:mtime, :ctime, :atime].each do |item| reply["#{item}_seconds".to_sym] = stat.send(item).to_i end reply[:mode] = "%o" % [stat.mode] reply[:md5] = Digest::MD5.hexdigest(File.read(file)) if stat.file? reply[:type] = "directory" if stat.directory? reply[:type] = "file" if stat.file? reply[:type] = "symlink" if stat.symlink? reply[:type] = "socket" if stat.socket? reply[:type] = "chardev" if stat.chardev? reply[:type] = "blockdev" if stat.blockdev? else logger.debug("Asked for status of '#{file}' - it is not present") reply.fail! "#{file} does not exist" end end def remove file = get_filename if ! File.exists?(file) logger.debug("Asked to remove file '#{file}', but it does not exist") reply.statusmsg = "OK" end begin FileUtils.rm(file) logger.debug("Removed file '#{file}'") reply.statusmsg = "OK" rescue logger.warn("Could not remove file '#{file}'") reply.fail! "Could not remove file '#{file}'" end end def touch file = get_filename begin FileUtils.touch(file) logger.debug("Touched file '#{file}'") rescue logger.warn("Could not touch file '#{file}'") reply.fail! "Could not touch file '#{file}'" end end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/filemgr/application/000077500000000000000000000000001174600156000253775ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/filemgr/application/filemgr.rb000077500000000000000000000026131174600156000273560ustar00rootroot00000000000000class MCollective::Application::Filemgr "File to manage", :arguments => ["--file FILE", "-f FILE"], :required => true option :details, :description => "Show full file details", :arguments => ["--details", "-d"], :type => :bool def post_option_parser(configuration) configuration[:command] = ARGV.shift if ARGV.size > 0 end def validate_configuration(configuration) configuration[:command] = "touch" unless configuration.include?(:command) end def main mc = rpcclient("filemgr", :options => options) case configuration[:command] when "remove" printrpc mc.remove(:file => configuration[:file]) when "touch" printrpc mc.touch(:file => configuration[:file]) when "status" if configuration[:details] printrpc mc.status(:file => configuration[:file]) else mc.status(:file => configuration[:file]).each do |resp| printf("%-40s: %s\n", resp[:sender], resp[:data][:output] || resp[:statusmsg] ) end end else mc.disconnect puts "Valid commands are 'touch', 'status', and 'remove'" exit 1 end mc.disconnect printrpcstats end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/000077500000000000000000000000001174600156000254255ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/agent/000077500000000000000000000000001174600156000265235ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/agent/iptables.ddl000066400000000000000000000031411174600156000310120ustar00rootroot00000000000000metadata :name => "SimpleRPC IP Tables Agent", :description => "An agent that manipulates a chain called 'junkfilter' with iptables", :author => "R.I.Pienaar", :license => "Apache 2", :version => "1.3", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 2 ["block", "unblock"].each do |act| action act, :description => "#{act.capitalize} an IP" do input :ipaddr, :prompt => "IP address", :description => "The IP address to #{act}", :type => :string, :validation => '^\d+\.\d+\.\d+\.\d+$', :optional => false, :maxlength => 15 output :output, :description => "Output from iptables or a human readable status", :display_as => "Result" end end action "listblocked", :description => "Returns list of blocked ips" do display :always output :blocked, :description => "Blocked IPs", :display_as => "Blocked" end action "isblocked", :description => "Check if an IP is blocked" do display :always input :ipaddr, :prompt => "IP address", :description => "The IP address to check", :type => :string, :validation => '^\d+\.\d+\.\d+\.\d+$', :optional => false, :maxlength => 15 output :output, :description => "Human readable indication if the IP is blocked or not", :display_as => "Result" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/agent/iptables.rb000066400000000000000000000077341174600156000306660ustar00rootroot00000000000000require 'socket' module MCollective module Agent # An agent that manipulates a chain called 'junkfilter' with iptables # # See http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/AgentIptablesJunkfilter # # Released under the terms of the GPL class Iptables "SimpleRPC IP Tables Agent", :description => "An agent that manipulates a chain called 'junkfilter' with iptables", :author => "R.I.Pienaar", :license => "Apache 2", :version => "2.0", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 2 action "block" do validate :ipaddr, :ipv4address blockip(request[:ipaddr]) end action "unblock" do validate :ipaddr, :ipv4address unblockip(request[:ipaddr]) end action "isblocked" do validate :ipaddr, :ipv4address isblocked(request[:ipaddr]) end action "listblocked" do reply[:blocked] = listblocked end private # Deals with requests to block an ip def blockip(ip) logger.debug("Blocking #{ip} with target #{target}") out = "" # if he's already blocked we just dont bother doing it again unless isblocked?(ip) run("/sbin/iptables -A junk_filter -s #{ip} -j #{target} 2>&1", :stdout => out, :chomp => true) run("/usr/bin/logger -i -t mcollective 'Attempted to add #{ip} to iptables junk_filter chain on #{Socket.gethostname}'") else reply.fail! "#{ip} was already blocked" return end if isblocked?(ip) unless out == "" reply[:output] = out else reply[:output] = "#{ip} was blocked" end else reply.fail! "Failed to add #{ip}: #{out}" end end # Deals with requests to unblock an ip def unblockip(ip) logger.debug("Unblocking #{ip} with target #{target}") out = "" # remove it if it's blocked if isblocked?(ip) run("/sbin/iptables -D junk_filter -s #{ip} -j #{target} 2>&1", :stdout => out, :chomp => true) run("/usr/bin/logger -i -t mcollective 'Attempted to remove #{ip} from iptables junk_filter chain on #{Socket.gethostname}'") else reply.fail! "#{ip} was already unblocked" return end # check it was removed if isblocked?(ip) reply.fail! "IP left blocked, iptables says: #{out}" else unless out == "" reply[:output] = out else reply[:output] = "#{ip} was unblocked" end end end # Deals with requests for status of a ip def isblocked(ip) if isblocked?(ip) reply[:output] = "#{ip} is blocked" else reply[:output] = "#{ip} is not blocked" end end # Utility to figure out if a ip is blocked or not, just return true or false def isblocked?(ip) logger.debug("Checking if #{ip} is blocked with target #{target}") prematches = "" run("/sbin/iptables -L junk_filter -n 2>&1", :stdout => prematches, :chomp => true) matches = prematches.split("\n").grep(/^#{target}.+#{ip}/).size matches >= 1 end # Returns a list of blocked ips def listblocked preout = "" run("/sbin/iptables -L junk_filter -n 2>&1", :stdout => preout, :chomp => true) out = preout.split("\n").grep(/^#{target}/) out.map {|l| l.split(/\s+/)[3]} end # Returns the target to use for rules def target target = "DROP" if @config.pluginconf.include?("iptables.target") target = @config.pluginconf["iptables.target"] end target end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/application/000077500000000000000000000000001174600156000277305ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/iptables-junkfilter/application/iptables.rb000077500000000000000000000031641174600156000320670ustar00rootroot00000000000000class MCollective::Application::Iptables "Do not wait for results", :arguments => "-s", :type => :bool def post_option_parser(configuration) if ARGV.size == 2 configuration[:command] = ARGV.shift configuration[:ipaddress] = ARGV.shift end end def validate_configuration(configuration) raise "Command should be one of block, unblock or isblocked" unless configuration[:command] =~ /^block|unblock|isblocked$/ require 'ipaddr' ip = IPAddr.new(configuration[:ipaddress]) raise "#{configuration[:ipaddress]} should be an ipv4 address" unless ip.ipv4? end def main iptables = rpcclient("iptables") if configuration[:silent] puts "Sent request " << iptables.send(configuration[:command], {:ipaddr => configuration[:ipaddress], :process_results => false}) else iptables.send(configuration[:command], {:ipaddr => configuration[:ipaddress]}).each do |node| if iptables.verbose printf("%-40s %s\n", node[:sender], node[:statusmsg]) puts "\t\t#{node[:data][:output]}" if node[:data][:output] else case configuration[:command] when /^block|unblock/ printf("%-40s %s\n", node[:sender], node[:statusmsg]) unless node[:statuscode] == 0 when "isblocked" printf("%-40s %s\n", node[:sender], node[:data][:output]) end end end printrpcstats end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/000077500000000000000000000000001174600156000231355ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/agent/000077500000000000000000000000001174600156000242335ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/agent/nettest.ddl000066400000000000000000000026251174600156000264130ustar00rootroot00000000000000metadata :name => "Ping", :description => "Agent to do network tests from a mcollective host", :author => "Dean Smith ", :license => "BSD", :version => "2.1", :url => "http://github.com/deasmi", :timeout => 60 action "ping", :description => "Returns rrt of ping to host" do display :always input :fqdn, :prompt => "FQDN", :description => "The fully qualified domain name to ping", :type => :string, :validation => '^.+$', :optional => false, :maxlength => 80 output :rtt, :description => "The round trip time in ms", :display_as=>"RTT" end action "connect", :description => "Check connectivity of remote server on port" do display :always input :fqdn, :prompt => "FQDN", :description => "The fully qualified domain name to ping", :validation => '^.+$', :type => :string, :optional => false, :maxlength => 80 input :port, :prompt => "Port", :description => "The port to connect on", :validation => '^[0-9]+$', :type => :string, :maxlength => 4, :optional => false output :connect, :description => "Can we connect", :display_as=>"connected" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/agent/nettest.rb000066400000000000000000000024761174600156000262570ustar00rootroot00000000000000require 'rubygems' require 'net/ping' require 'socket' require 'timeout' module MCollective module Agent class Nettest "Ping", :description => "Agent to do network tests from a mcollective host", :author => "Dean Smith", :license => "BSD", :version => "2.1", :url => "http://github.com/deasmi", :timeout => 60 action "ping" do validate :fqdn, String fqdn = request[:fqdn] icmp = Net::Ping::ICMP.new(fqdn) if icmp.ping? then reply[:rtt] = (icmp.duration * 1000).to_s else reply[:rtt] = "Host did not respond" end end action "connect" do validate :fqdn, String validate :port, String fqdn = request[:fqdn] port = Integer(request[:port]) begin Timeout::timeout(2) do begin t = TCPSocket.new(fqdn, port) t.close reply[:connect] = "Connected" rescue reply[:connect] = "Connection Refused" end end rescue Timeout::Error reply[:connect] = "Connection timeout" end end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/application/000077500000000000000000000000001174600156000254405ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/application/nettest.rb000066400000000000000000000115411174600156000274550ustar00rootroot00000000000000module MCollective class Application::Nettest < Application description "Network tests from a mcollective host" usage <<-END_OF_USAGE mco nettest [OPTIONS] [FILTERS] [PORT] The ACTION can be one of the following: ping - return round-trip time between this and remote host connect - check connectivity of remote host on specific port END_OF_USAGE def post_option_parser(configuration) if ARGV.size < 2 raise "Please specify an action and optional arguments" else # We trust that validation will be handled correctly # as per accompanying DDL file ... action = ARGV.shift host_name = ARGV.shift remote_port = ARGV.shift unless action.match(/^(ping|connect)$/) raise "Action can only to be ping or connect" end case action when /^ping$/ arguments = {:fqdn => host_name} when /^connect$/ arguments = {:fqdn => host_name, :port => remote_port} end configuration[:action] = action configuration[:arguments] = arguments end end def validate_configuration(configuration) # We have to ask this question because you do NOT want # your entire network of bazillion machines to simply # go and hammer some poor remote host ... You may get # your network blocked or whatnot, and that would be # quite an unpleasant thing to have place so better to # be sorry and safe ... if MCollective::Util.empty_filter?(options[:filter]) print "Do you really want to perform network tests unfiltered? (y/n): " STDOUT.flush # Only match letter "y" or complete word "yes" ... exit! unless STDIN.gets.strip.match(/^(?:y|yes)$/i) end end def print_statistics(statistics, action_statistics, type) puts "\n---- nettest summary ----" puts " Nodes: %d / %d" % [statistics[:responses] + statistics[:noresponsefrom].size, statistics[:responses]] if action_statistics.size > 0 case type when "ping" times = action_statistics[:ping] sum = times.inject(0) { |v, i| v + i } average = sum / times.size.to_f puts " Results: replies=%d, maximum=%.3f ms, minimum=%.3f ms, average=%.3f ms" % [times.size, times.max, times.min, average] when "connect" puts " Results: connected=%d, connection refused=%d, timed out=%d" % [action_statistics[:connect][0], action_statistics[:connect][1], action_statistics[:connect][2]] end else puts " Results: No responses received" end puts " Elapsed Time: %.2f s\n" % [ statistics[:blocktime] ] end def process_connect_result(result, node, action_statistics, verbose) action_statistics[:connect] ||= [ 0, 0, 0 ] # This is to be in line with the usual format of output ... result = result.tr("A-Z", "a-z") case result when "connected" action_statistics[:connect][0] += 1 when "refused" action_statistics[:connect][1] += 1 when "timeout" action_statistics[:connect][2] += 1 end if verbose puts "%-40s status=%s\n\t\t%s" % [node[:sender], result, node[:statusmsg]] else puts "%-40s status=%s" % [node[:sender], result] end end def process_ping_result(result, node, action_statistics, verbose) action_statistics[:ping] ||= [] result = Float(result) rescue 0.0 if verbose puts "%-40s time=%.3f\n\t\t%s" % [node[:sender], result, node[:statusmsg]] else puts "%-40s time=%.3f" % [node[:sender], result] end action_statistics[:ping] << result end def main action_statistics = {} action = configuration[:action] arguments = configuration[:arguments] rpc_nettest = rpcclient("nettest", {:options => options}) rpc_nettest.send(action, arguments).each do |node| # We want new line here ... puts if action_statistics.size.zero? and not rpc_nettest.progress data = node[:data] # If the status code is non-zero and data is empty then we # assume that something out of an ordinary had place and # therefore assume that there was some sort of error ... unless node[:statuscode].zero? and data result = "error" else result = data[:rtt] || data[:connect] end case action when "ping" process_ping_result(result, node, action_statistics, rpc_nettest.verbose) when "connect" process_connect_result(result, node, action_statistics, rpc_nettest.verbose) end end print_statistics(rpc_nettest.stats, action_statistics, action) end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/spec/000077500000000000000000000000001174600156000240675ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/spec/nettest_agent_spec.rb000066400000000000000000000052261174600156000302770ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "nettest agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/nettest.rb"]) @agent = MCollective::Test::LocalAgentTest.new("nettest", :agent_file => agent_file).plugin @agent.instance_variable_set("@lockfile", "spec_test_lock_file") @agent.instance_variable_set("@pidfile", "spec_test_pid_file") end describe "#meta" do it "should have valid metadata" do @agent.should have_valid_metadata end end describe "#ping" do it "should fail for non string fqdns" do result = @agent.call(:ping, :fqdn => nil) result.should be_invalid_data_error result = @agent.call(:ping) result.should be_missing_data_error end it "should set correct rtt if it can ping the host" do icmp = mock icmp.expects("ping?").returns(true) icmp.expects(:duration).returns(0.001) Net::Ping::ICMP.expects(:new).with("rspec").returns(icmp) result = @agent.call(:ping, :fqdn => "rspec") result.should be_successful result.should have_data_items(:rtt => "1.0") end it "should return failure when it cannot ping" do icmp = mock icmp.expects("ping?").returns(false) Net::Ping::ICMP.expects(:new).with("rspec").returns(icmp) result = @agent.call(:ping, :fqdn => "rspec") result.should be_successful result.should have_data_items(:rtt => "Host did not respond") end end describe "#connect" do it "should fail for invalid fqdn and port" do @agent.call(:connect).should be_missing_data_error @agent.call(:connect, :fqdn => "rspec").should be_missing_data_error @agent.call(:connect, :port => "rspec").should be_missing_data_error end it "should handle timeout errors on connection" do TCPSocket.expects(:new).raises(Timeout::Error) result = @agent.call(:connect, :fqdn => "rspec", :port => "80") result.should be_successful result.should have_data_items(:connect => "Connection timeout") end it "should report connection refused errors" do TCPSocket.expects(:new).raises(Errno::ECONNREFUSED) result = @agent.call(:connect, :fqdn => "rspec", :port => "80") result.should be_successful result.should have_data_items(:connect => "Connection Refused") end it "should report connected status correctly" do socket = mock socket.expects(:close) TCPSocket.expects(:new).with("rspec", 80).returns(socket) result = @agent.call(:connect, :fqdn => "rspec", :port => "80") result.should be_successful result.should have_data_items(:connect => "Connected") end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nettest/spec/nettest_application_spec.rb000066400000000000000000000157171174600156000315120ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' module Mcollective describe "nettest application" do before do application_file = File.join([File.dirname(__FILE__), "../application/nettest.rb"]) @util = MCollective::Test::ApplicationTest.new("nettest", :application_file => application_file) @app = @util.plugin end describe "#application_description" do it "should have a description" do @app.should have_a_description end end describe "#post_option_parser" do it "should raise an exception for no arguments" do expect { @app.post_option_parser({}) }.to raise_error("Please specify an action and optional arguments") end it "should raise an exception for unknown actions" do ARGV << "action" ARGV << "rspec" expect { @app.post_option_parser({}) }.to raise_error("Action can only to be ping or connect") end it "should set fqdn and port correctly in the configuration" do ARGV << "ping" ARGV << "rspec" configuration = {} @app.post_option_parser(configuration) configuration.should == {:action=>"ping", :arguments=>{:fqdn=>"rspec"}} ARGV << "connect" ARGV << "rspec" ARGV << "80" configuration = {} @app.post_option_parser(configuration) configuration.should == {:action=>"connect", :arguments=>{:port=>"80", :fqdn=>"rspec"}} end end describe "#validate_configuration" do it "should check if no filter is supplied and ask confirmation expecting y or yes" do MCollective::Util.expects("empty_filter?").returns(true).twice @app.expects(:print).with("Do you really want to perform network tests unfiltered? (y/n): ").twice @app.expects(:options).returns({}).twice STDIN.expects(:gets).returns("y") @app.expects("exit!").never @app.validate_configuration({}) STDIN.expects(:gets).returns("yes") @app.validate_configuration({}) end it "should exit unless y or yes is supplied" do MCollective::Util.expects("empty_filter?").returns(true) @app.expects(:print).with("Do you really want to perform network tests unfiltered? (y/n): ") @app.expects(:options).returns({}) @app.expects("exit!") STDIN.expects(:gets).returns("n") @app.validate_configuration({}) end end describe "#process_connect_result" do it "should correctly handle connected results" do @app.expects(:puts).with(regexp_matches(/rspec.+status=connected/)) @app.process_connect_result("connected", {:sender => "rspec", :statusmsg => "OK"}, (stats = {}), false) stats.should == {:connect => [1, 0, 0]} end it "should correctly handle refused results" do @app.expects(:puts).with(regexp_matches(/rspec.+status=refused/)) @app.process_connect_result("refused", {:sender => "rspec", :statusmsg => "OK"}, (stats = {}), false) stats.should == {:connect => [0, 1, 0]} end it "should correctly handle timeout results" do @app.expects(:puts).with(regexp_matches(/rspec.+status=timeout/)) @app.process_connect_result("timeout", {:sender => "rspec", :statusmsg => "OK"}, (stats = {}), false) stats.should == {:connect => [0, 0, 1]} end it "should support verbose output" do @app.expects(:puts).with(regexp_matches(/rspec.+status=timeout.+OK/m)) @app.process_connect_result("timeout", {:sender => "rspec", :statusmsg => "OK"}, (stats = {}), true) stats.should == {:connect => [0, 0, 1]} end end describe "#process_ping_result" do it "should track ping stats correctly" do @app.expects(:puts).with(regexp_matches(/rspec.+time=1.100/)) @app.expects(:puts).with(regexp_matches(/rspec.+time=1.200/)) @app.process_ping_result("1.1", {:sender => "rspec", :statusmsg => "OK"}, (stats = {}), false) @app.process_ping_result("1.2", {:sender => "rspec", :statusmsg => "OK"}, stats, false) stats.should == {:ping => [1.1, 1.2]} end it "should support verbose output" do @app.expects(:puts).with(regexp_matches(/rspec.+time=1.100.+OK/m)) @app.process_ping_result("1.1", {:sender => "rspec", :statusmsg => "OK"}, {}, true) end end describe "#print_statistics" do it "should support ping statistics" do @app.expects(:puts).with(regexp_matches(/Nodes: 2 \/ 2/)) @app.expects(:puts).with(regexp_matches(/Results: replies=2, maximum=2.000 ms, minimum=1.000 ms, average=1.500 ms/)) @app.expects(:puts).with(regexp_matches(/Elapsed.+2.00 s/)) @app.print_statistics({:responses => 2, :noresponsefrom => [], :blocktime => 2}, {:ping => [1.0, 2.0]}, "ping") end it "should support connect statistics" do @app.expects(:puts).with(regexp_matches(/Nodes: 2 \/ 2/)) @app.expects(:puts).with(regexp_matches(/Results: connected=1, connection refused=0, timed out=1/)) @app.expects(:puts).with(regexp_matches(/Elapsed.+2.00 s/)) @app.print_statistics({:responses => 2, :noresponsefrom => [], :blocktime => 2}, {:connect => [1, 0, 1]}, "connect") end it "should support empty results" do @app.expects(:puts).with(regexp_matches(/Results: No responses received/)) @app.print_statistics({:responses => 2, :noresponsefrom => [], :blocktime => 2}, {}, "ping") end end describe "#main" do before do @rpc_client = mock @rpc_client.expects(:stats).returns(:stats) @rpc_client.expects(:progress).returns(false) @rpc_client.expects(:verbose).returns(false) @app.expects(:rpcclient).returns(@rpc_client) end it "should handle errors from nodes" do @app.expects(:configuration).returns({:action => "ping", :arguments => {:fqdn => "rspec"}}).twice @rpc_client.expects(:send).with("ping", {:fqdn => "rspec"}).returns([{:statuscode => 1}]) @app.expects(:process_ping_result).with("error", {:statuscode => 1}, {}, false) @app.expects(:print_statistics).with(:stats, {}, "ping") @app.main end it "should handle ping actions correctly" do @app.expects(:configuration).returns({:action => "ping", :arguments => {:fqdn => "rspec"}}).twice @rpc_client.expects(:send).with("ping", {:fqdn => "rspec"}).returns([{:statuscode => 0, :data => {}}]) @app.expects(:print_statistics).with(:stats, {:ping => [0.0]}, "ping") @app.main end it "should handle connect actions correctly" do @app.expects(:configuration).returns({:action => "connect", :arguments => {:fqdn => "rspec", :port => 80}}).twice @rpc_client.expects(:send).with("connect", {:fqdn => "rspec", :port => 80}).returns([{:statuscode => 0, :data => {:connect => "rspec"}}]) @app.expects(:print_statistics).with(:stats, {:connect => [0, 0, 0]}, "connect") @app.main end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/000077500000000000000000000000001174600156000224135ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/agent/000077500000000000000000000000001174600156000235115ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/agent/nrpe.ddl000066400000000000000000000020561174600156000251450ustar00rootroot00000000000000metadata :name => "SimpleRPC Agent For NRPE Commands", :description => "Agent to query NRPE commands via MCollective", :author => "R.I.Pienaar", :license => "Apache 2", :version => "1.3", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 5 action "runcommand", :description => "Run a NRPE command" do input :command, :prompt => "Command", :description => "NRPE command to run", :type => :string, :validation => '^[a-zA-Z0-9_-]+$', :optional => false, :maxlength => 50 output :output, :description => "Output from the Nagios plugin", :display_as => "Output" output :exitcode, :description => "Exit Code from the Nagios plugin", :display_as => "Exit Code" output :perfdata, :description => "Performance Data from the Nagios plugin", :display_as => "Performance Data" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/agent/nrpe.rb000066400000000000000000000036711174600156000250110ustar00rootroot00000000000000module MCollective module Agent class Nrpe "SimpleRPC Agent For NRPE Commands", :description => "Agent to query NRPE commands via MCollective", :author => "R.I.Pienaar", :license => "Apache 2", :version => "2.0", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 5 action "runcommand" do validate :command, :shellsafe command = plugin_for_command(request[:command]) if command == nil reply[:output] = "No such command: #{request[:command]}" if command == nil reply[:exitcode] = 3 reply.fail "UNKNOWN" return end reply[:exitcode] = run(command[:cmd], :stdout => :output, :chomp => true) case reply[:exitcode] when 0 reply.statusmsg = "OK" when 1 reply.fail "WARNING" when 2 reply.fail "CRITICAL" else reply.fail "UNKNOWN" end if reply[:output] =~ /^(.+)\|(.+)$/ reply[:output] = $1 reply[:perfdata] = $2 else reply[:perfdata] = "" end end private def plugin_for_command(req) ret = nil fname = nil fdir = config.pluginconf["nrpe.conf_dir"] || "/etc/nagios/nrpe.d" if config.pluginconf["nrpe.conf_file"] fname = "#{fdir}/#{config.pluginconf['nrpe.conf_file']}" else fname = "#{fdir}/#{req}.cfg" end if File.exist?(fname) t = File.readlines(fname) t.each do |check| check.chomp! if check =~ /command\[#{request[:command]}\]=(.+)$/ ret = {:cmd => $1} end end end ret end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/application/000077500000000000000000000000001174600156000247165ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/application/nrpe.rb000077500000000000000000000031721174600156000262150ustar00rootroot00000000000000class MCollective::Application::Nrpe" def post_option_parser(configuration) configuration[:command] = ARGV.shift if ARGV.size > 0 end def validate_configuration(configuration) raise "Please specify a check name" unless configuration.include?(:command) end def main nrpe = rpcclient("nrpe") stats = [0, 0, 0, 0] statuscodes = [0] nrpe_results = nrpe.runcommand(:command => configuration[:command]) puts nrpe_results.each do |result| exitcode = result[:data][:exitcode].to_i statuscodes << exitcode if exitcode >=0 and exitcode < 4 stats[exitcode] += 1 end if nrpe.verbose printf("%-40s status=%s\n", result[:sender], result[:statusmsg]) printf(" %-40s\n\n", result[:data][:output]) else if [1,2,3].include?(exitcode) printf("%-40s status=%s\n", result[:sender], result[:statusmsg]) printf(" %-40s\n\n", result[:data][:output]) if result[:data][:output] end end end puts # Nodes that don't respond are UNKNOWNs if nrpe.stats[:noresponsefrom].size > 0 stats[3] += nrpe.stats[:noresponsefrom].size statuscodes << 3 end printrpcstats :caption => "#{configuration[:command]} NRPE results" printf("\nNagios Statusses:\n") if nrpe.verbose printf(" OK: %d\n", stats[0]) printf(" WARNING: %d\n", stats[1]) printf(" CRITICAL: %d\n", stats[2]) printf(" UNKNOWN: %d\n", stats[3]) end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/puppet/000077500000000000000000000000001174600156000237305ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/puppet/nrpe-config.erb000066400000000000000000000001051174600156000266250ustar00rootroot00000000000000command[<%= name %>]=<%= plugdir %>/<%= command %> <%= parameters %> mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/puppet/nrpe_command.pp000066400000000000000000000021351174600156000267340ustar00rootroot00000000000000define nrpe_command ($command, $parameters="", $cplugdir="auto", $ensure="present") { # find out the default nagios paths for plugis $defaultdir = $architecture ? { "x86_64" => "/usr/lib64/nagios/plugins", default => "/usr/lib/nagios/plugins" } # if we overrode cplugdir then use that, else go with the nagios default # for this architecture case $cplugdir { auto: { $plugdir = $defaultdir } default: { $plugdir = $cplugdir } } case $ensure { "absent": { file{"/etc/nagios/nrpe.d/${name}.cfg": ensure => absent } } default: { file {"/etc/nagios/nrpe.d/${name}.cfg": owner => root, group => root, mode => 644, content => template("nagios/nrpe-config.erb"), require => File["/etc/nagios/nrpe.d"], } } } } mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/puppet/sample.pp000066400000000000000000000007451174600156000255600ustar00rootroot00000000000000 nrpe_command{"check_mysqldump": command => "check_file_age", parameters => "-f /srv/backup/mysql/mysql.dump -w 86400 -c 90000 -W 8000 -C 8000" } nrpe_command{"check_mysqldump": command => "check_file.pl", parameters => "--file /srv/backup/mysql/latest --warnage=89999 --critage=90000 --critsize=300000000 --warnsize=60000000 --larger", cplugdir => "/usr/local/bin", } mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/sbin/000077500000000000000000000000001174600156000233465ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/sbin/check-mc-nrpe000077500000000000000000000035321174600156000257130ustar00rootroot00000000000000#!/usr/bin/env ruby # Nagios check for the mcollective nrpe agent found at https://github.com/puppetlabs/mcollective-plugins # # Returns std Nagios exit codes and performance data. Lists of hosts in the given status is printed # on subsequent lines of output, Nagios 3 will read these and display them up to 4KB require 'mcollective' include MCollective::RPC nrpe = rpcclient("nrpe") nrpe.progress = false if ARGV.length > 0 command = ARGV.shift else puts("UNKNOWN: NRPE command not specified") exit 3 end labels = ["OK", "WARNING", "CRITICAL", "UNKNOWN"] stats = [[], [], [], []] statuscodes = [0] if nrpe.discover.size == 0 puts("#{command}: UNKNOWN: did not discovery any nodes") exit 3 end nrpe_results = nrpe.runcommand(:command => command) nrpe_results.each do |result| begin exitcode = result[:data][:exitcode].to_i rescue exitcode = 3 end statuscodes << exitcode stats[exitcode] << result[:sender] end # Nodes that don't respond are UNKNOWNs if nrpe.stats[:noresponsefrom].size > 0 stats[3] << nrpe.stats[:noresponsefrom] statuscodes << 3 stats[3].flatten! end # If we didn't discover any then thats an unknown statuscodes << 3 if nrpe.discover.size == 0 puts("#{command}: OK: %d WARNING: %d CRITICAL: %d UNKNOWN: %d|total=%d ok=%d warn=%d crit=%d unknown=%d checktime=%f" % [stats[0].size, stats[1].size, stats[2].size, stats[3].size, nrpe_results.size, stats[0].size, stats[1].size, stats[2].size, stats[3].size, nrpe.stats.blocktime]) [2,1,3].each do |e| if stats[e].size > 0 puts "#{labels[e]}:" puts " " + stats[e].join(" ") end end nrpe.disconnet # if there's any critical checks, exit critical # else just take the highest. UNKNOWN is 3 while # critical is 2, just exiting with max would hide # the fact that there are criticals. if statuscodes.include?(2) exit 2 else exit statuscodes.max end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/spec/000077500000000000000000000000001174600156000233455ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/nrpe/spec/nrpe_agent_spec.rb000066400000000000000000000074711174600156000270370ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "nrpe agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/nrpe.rb"]) @agent = MCollective::Test::LocalAgentTest.new("nrpe", :agent_file => agent_file).plugin end describe "#metda" do it "should have valid metadata" do @agent.should have_valid_metadata end end describe "#runcommand" do it "should reply with statusmessage 'OK' of exitcode is 0" do @agent.expects(:plugin_for_command).with("foo").returns:cmd => ("foo") @agent.expects(:run).with("foo", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:runcommand, :command => "foo") result.should be_successful result.should have_data_items(:exitcode=>0, :perfdata=>"") result[:statusmsg].should == "OK" end it "should reply with statusmessage 'WARNING' of exitcode is 1" do @agent.expects(:plugin_for_command).with("foo").returns:cmd => ("foo") @agent.expects(:run).with("foo", :stdout => :output, :chomp => true).returns(1) result = @agent.call(:runcommand, :command => "foo") result.should be_aborted_error result.should have_data_items(:exitcode=>1, :perfdata=>"") result[:statusmsg].should == "WARNING" end it "should reply with statusmessage 'CRITICAL' of exitcode is 2" do @agent.expects(:plugin_for_command).with("foo").returns:cmd => ("foo") @agent.expects(:run).with("foo", :stdout => :output, :chomp => true).returns(2) result = @agent.call(:runcommand, :command => "foo") result.should be_aborted_error result.should have_data_items(:exitcode=>2, :perfdata=>"") result[:statusmsg].should == "CRITICAL" end it "should reply with statusmessage UKNOWN if exitcode is something else" do @agent.expects(:plugin_for_command).with("foo").returns:cmd => ("foo") @agent.expects(:run).with("foo", :stdout => :output, :chomp => true) result = @agent.call(:runcommand, :command => "foo") result.should be_aborted_error result.should have_data_items(:exitcode=>nil, :perfdata=>"") result[:statusmsg].should == "UNKNOWN" end it "should fail on an unknown command" do @agent.expects(:plugin_for_command).with("foo").returns(nil) result = @agent.call(:runcommand, :command => "foo") result.should be_aborted_error result.should have_data_items(:output => "No such command: foo", :exitcode => 3) end end describe "#plugin_for_command" do it "should return the command from nrpe.conf_dir if it is set" do @agent.config.stubs(:pluginconf).returns("nrpe.conf_dir" => "/foo", "nrpe.conf_file" => "bar.cfg") @agent.stubs(:request).returns(:command => "command") File.expects(:exist?).with("/foo/bar.cfg").returns(true) File.expects(:readlines).with("/foo/bar.cfg").returns(["command[command]=run"]) @agent.expects(:run).with("run", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:runcommand, :command => "run") result.should be_successful result.should have_data_items(:exitcode=>0, :perfdata=>"") result[:statusmsg].should == "OK" end it "should return the command from /etc/nagios/nrpe.d if nrpe.conf_dir is unset" do @agent.config.stubs(:pluginconf).returns("") @agent.stubs(:request).returns(:command => "command") File.expects(:exist?).with("/etc/nagios/nrpe.d/command.cfg").returns(true) File.expects(:readlines).with("/etc/nagios/nrpe.d/command.cfg").returns(["command[command]=run"]) @agent.expects(:run).with("run", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:runcommand, :command => "run") result.should be_successful result.should have_data_items(:exitcode=>0, :perfdata=>"") result[:statusmsg].should == "OK" end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/000077500000000000000000000000001174600156000230425ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/agent/000077500000000000000000000000001174600156000241405ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/agent/package.ddl000066400000000000000000000076561174600156000262360ustar00rootroot00000000000000metadata :name => "Package Agent", :description => "Install and uninstall software packages", :author => "R.I.Pienaar", :license => "ASL2", :version => "2.2", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 180 ["install", "update", "uninstall", "purge"].each do |act| action act, :description => "#{act.capitalize} a package" do input :package, :prompt => "Package Name", :description => "Package to #{act}", :type => :string, :validation => '.', :optional => false, :maxlength => 90 output :output, :description => "Output from the package manager", :display_as => "Output" output :properties, :description => "Properties of the package after #{act.sub(/e$/, '')}ing", :display_as => "Properties" end end action "status", :description => "Get the status of a package" do display :always input :package, :prompt => "Package Name", :description => "Package to retrieve the status of", :type => :string, :validation => '.', :optional => false, :maxlength => 90 output :output, :description => "Output from the package manager", :display_as => "Output" output :properties, :description => "Package properties", :display_as => "Properties" end action "yum_clean", :description => "Clean the YUM cache" do input :mode, :prompt => "Yum clean mode", :description => "One of the various supported clean modes", :type => :list, :optional => true, :list => ["all", "headers", "packages", "metadata", "dbcache", "plugins", "expire-cache"] output :output, :description => "Output from YUM", :display_as => "Output" output :exitcode, :description => "The exitcode from the yum command", :display_as => "Exit Code" end action "apt_update", :description => "Update the apt cache" do output :output, :description => "Output from apt-get", :display_as => "Output" output :exitcode, :description => "The exitcode from the apt-get command", :display_as => "Exit Code" end action "yum_checkupdates", :description => "Check for YUM updates" do display :always output :output, :description => "Output from YUM", :display_as => "Output" output :oudated_packages, :description => "Outdated packages", :display_as => "Outdated Packages" output :exitcode, :description => "The exitcode from the yum command", :display_as => "Exit Code" end action "apt_checkupdates", :description => "Check for APT updates" do display :always output :output, :description => "Output from APT", :display_as => "Output" output :oudated_packages, :description => "Outdated packages", :display_as => "Outdated Packages" output :exitcode, :description => "The exitcode from the apt command", :display_as => "Exit Code" end action "checkupdates", :description => "Check for updates" do display :always output :package_manager, :description => "The detected package manager", :display_as => "Package Manager" output :output, :description => "Output from Package Manager", :display_as => "Output" output :outdated_packages, :description => "Outdated packages", :display_as => "Outdated Packages" output :exitcode, :description => "The exitcode from the package manager command", :display_as => "Exit Code" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/agent/puppet-package.rb000066400000000000000000000130031174600156000273700ustar00rootroot00000000000000require 'puppet' module MCollective module Agent # An agent that uses Puppet to manage packages # # See https://github.com/puppetlabs/mcollective-plugins # # Released under the terms of the Apache Software License, v2.0. # # As this agent is based on Simple RPC, it requires mcollective 0.4.7 or newer. class Package "Package Agent", :description => "Install and uninstall software packages", :author => "R.I.Pienaar", :license => "ASL2", :version => "2.2", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 180 ["install", "update", "uninstall", "purge", "status"].each do |act| action act do validate :package, :shellsafe do_pkg_action(request[:package], act.to_sym) end end action "yum_clean" do reply.fail! "Cannot find yum at /usr/bin/yum" unless File.exist?("/usr/bin/yum") if request[:mode] clean_mode = request[:mode] else clean_mode = @config.pluginconf["package.yum_clean_mode"] || "all" end if ["all", "headers", "packages", "metadata", "dbcache", "plugins", "expire-cache"].include?(clean_mode) reply[:exitcode] = run("/usr/bin/yum clean #{clean_mode}", :stdout => :output, :chomp => true) else reply.fail! "Unsupported yum clean mode: #{clean_mode}" end reply.fail! "Yum clean failed, exit code was #{reply[:exitcode]}" unless reply[:exitcode] == 0 end action "apt_update" do reply.fail! "Cannot find apt-get at /usr/bin/apt-get" unless File.exist?("/usr/bin/apt-get") reply[:exitcode] = run("/usr/bin/apt-get update", :stdout => :output, :chomp => true) reply.fail! "apt-get update failed, exit code was #{reply[:exitcode]}" unless reply[:exitcode] == 0 end action "checkupdates" do if File.exist?("/usr/bin/yum") reply[:package_manager] = "yum" yum_checkupdates_action elsif File.exist?("/usr/bin/apt-get") reply[:package_manager] = "apt" apt_checkupdates_action else reply.fail! "Cannot find a compatible package system to check updates for" end end action "yum_checkupdates" do reply.fail! "Cannot find yum at /usr/bin/yum" unless File.exist?("/usr/bin/yum") reply[:exitcode] = run("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true) if reply[:exitcode] == 0 reply[:outdated_packages] = [] # Exit code 100 means package updates available elsif reply[:exitcode] == 100 reply[:outdated_packages] = do_yum_outdated_packages(reply[:output]) else reply.fail! "Yum check-update failed, exit code was #{reply[:exitcode]}" end end action "apt_checkupdates" do reply.fail! "Cannot find apt at /usr/bin/apt-get" unless File.exist?("/usr/bin/apt-get") reply[:exitcode] = run("/usr/bin/apt-get --simulate dist-upgrade", :stdout => :output, :chomp => true) reply[:outdated_packages] = [] if reply[:exitcode] == 0 reply[:output].each_line do |line| next unless line =~ /^Inst/ # Inst emacs23 [23.1+1-4ubuntu7] (23.1+1-4ubuntu7.1 Ubuntu:10.04/lucid-updates) [] if line =~ /Inst (.+?) \[.+?\] \((.+?)\s(.+?)\)/ reply[:outdated_packages] << {:package => $1.strip, :version => $2.strip, :repo => $3.strip} end end else reply.fail! "APT check-update failed, exit code was #{reply[:exitcode]}" end end private def do_pkg_action(package, action) begin if ::Puppet.version =~ /0.24/ ::Puppet::Type.type(:package).clear pkg = ::Puppet::Type.type(:package).create(:name => package).provider else pkg = ::Puppet::Type.type(:package).new(:name => package).provider end reply[:output] = "" reply[:properties] = "unknown" case action when :install reply[:output] = pkg.install if [:absent, :purged].include?(pkg.properties[:ensure]) when :update reply[:output] = pkg.update unless [:absent, :purged].include?(pkg.properties[:ensure]) when :uninstall reply[:output] = pkg.uninstall unless [:absent, :purged].include?(pkg.properties[:ensure]) when :status pkg.flush reply[:output] = pkg.properties when :purge reply[:output] = pkg.purge else reply.fail "Unknown action #{action}" end pkg.flush reply[:properties] = pkg.properties rescue Exception => e reply.fail e.to_s end end def do_yum_outdated_packages(packages) outdated_pkgs = [] packages.strip.each_line do |line| # Don't handle obsoleted packages for now break if line =~ /^Obsoleting\sPackages/i pkg, ver, repo = line.split if pkg && ver && repo pkginfo = { :package => pkg.strip, :version => ver.strip, :repo => repo.strip } outdated_pkgs << pkginfo end end outdated_pkgs end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/application/000077500000000000000000000000001174600156000253455ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/application/package.rb000066400000000000000000000045371174600156000272760ustar00rootroot00000000000000class MCollective::Application::Package " The ACTION can be one of the following: install - install PACKAGE update - update PACKAGE uninstall - uninstall PACKAGE purge - uninstall PACKAGE and purge related config files status - determine whether PACKAGE is installed, and report its version END_OF_USAGE def post_option_parser(configuration) if ARGV.length == 2 configuration[:action] = ARGV.shift configuration[:package] = ARGV.shift unless configuration[:action] =~ /^(install|update|uninstall|purge|status)$/ puts("Action must be install, update, uninstall, purge, or status.") exit 1 end else puts("Please specify an action and a package.") exit 1 end end def validate_configuration(configuration) if MCollective::Util.empty_filter?(options[:filter]) print("Do you really want to operate on packages unfiltered? (y/n): ") STDOUT.flush exit unless STDIN.gets.chomp =~ /^y$/ end end def summarize(stats, versions) puts("\n---- package agent summary ----") puts(" Nodes: #{stats[:discovered]} / #{stats[:responses]}") print(" Versions: ") puts versions.keys.sort.map {|s| "#{versions[s]} * #{s}" }.join(", ") printf(" Elapsed Time: %.2f s\n\n", stats[:blocktime]) end def main pkg = rpcclient("package", :options => options) versions = {} pkg.send(configuration[:action], {:package => configuration[:package]}).each do |resp| status = resp[:data][:properties] if resp[:statuscode] == 0 if status.include?(:version) version = "#{status[:version]}-#{status[:release]}" elsif status.include?(:ensure) version = status[:ensure].to_s end versions.include?(version) ? versions[version] += 1 : versions[version] = 1 if status[:name] printf("%-40s version = %s-%s\n", resp[:sender], status[:name], version) else printf("%-40s version = %s\n", resp[:sender], version) end else printf("%-40s error = %s\n", resp[:sender], resp[:statusmsg]) end end summarize(pkg.stats, versions) end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/spec/000077500000000000000000000000001174600156000237745ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/spec/package_agent_spec.rb000077500000000000000000000413451174600156000301160ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "package agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/puppet-package.rb"]) @agent = MCollective::Test::LocalAgentTest.new("package", :agent_file => agent_file).plugin end after :all do MCollective::PluginManager.clear end describe "#yum_clean" do it "should fail if /usr/bin/yum doesn't exist" do File.expects(:exist?).with("/usr/bin/yum").returns(false) result = @agent.call(:yum_clean) result.should be_aborted_error result[:statusmsg].should == "Cannot find yum at /usr/bin/yum" end it "should succeed if run method returns 0" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.config.expects(:pluginconf).returns({"package.yum_clean_mode" => "all"}) @agent.expects(:run).with("/usr/bin/yum clean all", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:yum_clean) result.should be_successful result.should have_data_items(:exitcode => 0) end it "should fail if the run method doesn't return 0" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum clean all", :stdout => :output, :chomp => true).returns(1) @agent.config.expects(:pluginconf).returns({"package.yum_clean_mode" => "all"}) result = @agent.call(:yum_clean) result.should be_aborted_error result.should have_data_items(:exitcode => 1) end it "should default to 'all' mode" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.config.expects(:pluginconf).returns({}) @agent.expects(:run).with("/usr/bin/yum clean all", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:yum_clean) result.should be_successful result.should have_data_items(:exitcode => 0) end it "should support a configured mode" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.config.expects(:pluginconf).returns({"package.yum_clean_mode" => "headers"}) @agent.expects(:run).with("/usr/bin/yum clean headers", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:yum_clean) result.should be_successful result.should have_data_items(:exitcode => 0) end it "should support configured modes" do ["all", "headers", "packages", "metadata", "dbcache", "plugins", "expire-cache"].each do |mode| File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.config.expects(:pluginconf).returns({"package.yum_clean_mode" => "all"}) @agent.expects(:run).with("/usr/bin/yum clean #{mode}", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:yum_clean, :mode => mode) result.should be_successful result.should have_data_items(:exitcode => 0) end end end describe "#apt_update" do it "should fail if /usr/bin/apt-get doesn't exist" do File.expects(:exist?).with("/usr/bin/apt-get").returns(false) result = @agent.call(:apt_update) result.should be_aborted_error result[:statusmsg].should == "Cannot find apt-get at /usr/bin/apt-get" end it "should succeed if the agent responds to 'run' and the run method returns 0" do File.expects(:exist?).with("/usr/bin/apt-get").returns(true) @agent.expects(:run).with("/usr/bin/apt-get update", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:apt_update) result.should have_data_items(:exitcode => 0) result.should be_successful end it "should fail if the agent responds to 'run' and the run method doesn't return 0" do File.expects(:exist?).with("/usr/bin/apt-get").returns(true) @agent.expects(:run).with("/usr/bin/apt-get update", :stdout => :output, :chomp => true).returns(1) result = @agent.call(:apt_update) result.should have_data_items(:exitcode => 1) result.should be_aborted_error end end describe "#checkupdates" do it "should fail if neither /usr/bin/yum or /usr/bin/apt-get are present" do File.expects(:exist?).with("/usr/bin/yum").returns(false) File.expects(:exist?).with("/usr/bin/apt-get").returns(false) result = @agent.call(:checkupdates) result.should be_aborted_error result[:statusmsg].should == "Cannot find a compatible package system to check updates for" end it "should call yum_checkupdates if /usr/bin/yum exists" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:yum_checkupdates_action).returns(true) result = @agent.call(:checkupdates) result.should be_true result.should have_data_items(:package_manager=>"yum") end it "should call apt_checkupdates if /usr/bin/apt-get exists" do File.expects(:exist?).with("/usr/bin/yum").returns(false) File.expects(:exist?).with("/usr/bin/apt-get").returns(true) @agent.expects(:apt_checkupdates_action).returns(true) result = @agent.call(:checkupdates) result.should have_data_items(:package_manager=>"apt") result.should be_true end end describe "#yum_checkupdates" do it "should fail if /usr/bin/yum does not exist" do File.expects(:exist?).with("/usr/bin/yum").returns(false) result = @agent.call(:yum_checkupdates) result.should be_aborted_error result[:statusmsg].should == "Cannot find yum at /usr/bin/yum" end it "should succeed if it responds to run and there are no packages to update" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:yum_checkupdates) result.should be_successful result.should have_data_items(:exitcode=>0, :outdated_packages=>[]) end it "should succeed if it responds to run and there are packages to update" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true).returns(100) @agent.expects(:do_yum_outdated_packages) result = @agent.call(:yum_checkupdates) result.should be_successful result.should have_data_items(:outdated_packages=>nil, :exitcode=>100) end it "should fail if it responds to run but returns a different exit code than 0 or 100" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true).returns(2) result = @agent.call(:yum_checkupdates) result.should be_aborted_error result.should have_data_items(:exitcode=>2) end end describe "#apt_checkupdates" do it "should fail if /usr/bin/apy-get does not exist" do File.expects(:exist?).with("/usr/bin/apt-get").returns(false) result = @agent.call(:apt_checkupdates) result.should be_aborted_error result[:statusmsg].should == "Cannot find apt at /usr/bin/apt-get" end it "should succeed if it responds to run and returns exit code of 0" do @agent.stubs("reply").returns({:output => "Inst emacs23 [23.1+1-4ubuntu7] (23.1+1-4ubuntu7.1 Ubuntu:10.04/lucid-updates) []", :exitcode => 0}) File.expects(:exist?).with("/usr/bin/apt-get").returns(true) @agent.expects(:run).with("/usr/bin/apt-get --simulate dist-upgrade", :stdout => :output, :chomp => true).returns(0) result = @agent.call(:apt_checkupdates) result.should be_successful end it "should fail if it responds to 'run' but returns an error code that is not 0" do File.expects(:exist?).with("/usr/bin/apt-get").returns(true) @agent.expects(:run).with("/usr/bin/apt-get --simulate dist-upgrade", :stdout => :output, :chomp => true).returns(1) result = @agent.call(:apt_checkupdates) result.should be_aborted_error result.should have_data_items(:outdated_packages=>[], :exitcode=>1) end end describe "#do_pkg_action" do before(:each) do @puppet_type = mock @puppet_type.stubs(:clear) @puppet_package = mock end describe "#puppet provider" do it "should use the correct provider for version 0.24" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:install).returns(0) @puppet_package.expects(:properties).twice.returns({:ensure => :absent}) @puppet_package.expects(:flush) result = @agent.call(:install, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>:absent}, :output=>0) end it "should use the correct provider for a version that is not 0.24" do Puppet.expects(:version).returns("something else") Puppet::Type.expects(:type).with(:package).returns(@puppet_type) @puppet_type.expects(:new).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:install).returns(0) @puppet_package.expects(:properties).twice.returns({:ensure => :absent}) @puppet_package.expects(:flush) result = @agent.call(:install, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>:absent}, :output=>0) end end describe "#install" do it "should install if the package is absent" do [:absent, :purged].each do |status| Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:install).returns(0) @puppet_package.expects(:properties).twice.returns({:ensure => status}) @puppet_package.expects(:flush) result = @agent.call(:install, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>status}, :output=>0) end end it "should not install if the package is present" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:properties).twice.returns({:ensure => "123"}) @puppet_package.expects(:flush) result = @agent.call(:install, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>"123"}, :output=>"") end end describe "#update" do it "should update unless the package is absent" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:update).returns(0) @puppet_package.expects(:properties).twice.returns({:ensure => :not_absent}) @puppet_package.expects(:flush) result = @agent.call(:update, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>:not_absent}, :output=>0) end it "should not update if the package is not installed" do [:absent, :purged].each do |status| Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:properties).twice.returns({:ensure => status}) @puppet_package.expects(:flush) result = @agent.call(:update, :package => "package") result.should be_successful result.should have_data_items(:properties => {:ensure => status}, :output=>"") end end end describe "#uninstall" do it "should uninstall if the package is present" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:uninstall).returns(0) @puppet_package.expects(:properties).twice.returns({:ensure => :not_absent}) @puppet_package.expects(:flush) result = @agent.call(:uninstall, :package => "package") result.should be_successful result.should have_data_items(:properties=>{:ensure=>:not_absent}, :output=>0) end it "should not uninstall if the package is absent" do [:absent, :purged].each do |status| Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:properties).twice.returns({:ensure => status}) @puppet_package.expects(:flush) result = @agent.call(:uninstall, :package => "package") result.should be_successful result.should have_data_items(:properties => {:ensure => status}, :output => "") end end end describe "#status" do it "should return the status of the package" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:flush).twice @puppet_package.expects(:properties).twice.returns("Package Status") result = @agent.call(:status, :package => "package") result.should be_successful result.should have_data_items(:properties=>"Package Status", :output=>"Package Status") end end describe "#purge" do it "should run purge on the package object" do Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:package).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => "package").returns(@puppet_package) @puppet_package.expects(:provider).returns(@puppet_package) @puppet_package.expects(:flush) @puppet_package.expects(:purge).returns("Purged") @puppet_package.expects(:properties) result = @agent.call(:purge, :package => "package") result.should be_successful result.should have_data_items(:properties=>nil, :output=>"Purged") end end describe "#Exceptions" do it "should fail if exception is raised" do Puppet.expects(:version).raises("Exception") result = @agent.call(:install, :package => "package") result.should be_aborted_error result[:statusmsg].should == "Exception" end end end describe "#do_yum_outdated_packages" do it "should not do anything with obsoleted packages" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true).returns(100) @agent.stubs(:reply).returns(:output => "Obsoleting") result = @agent.call(:yum_checkupdates) result.should be_successful end it "should return packages which need to be updated" do File.expects(:exist?).with("/usr/bin/yum").returns(true) @agent.expects(:run).with("/usr/bin/yum -q check-update", :stdout => :output, :chomp => true).returns(100) @agent.stubs(:reply).returns(:output => "Package version repo", :outdated_packages => "foo") result = @agent.call(:yum_checkupdates) result.should be_successful end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/package/spec/package_application_spec.rb000077500000000000000000000115501174600156000313160ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' module MCollective describe "package application" do before do application_file = File.join([File.dirname(__FILE__), "../application/package.rb"]) @util = MCollective::Test::ApplicationTest.new("package", :application_file => application_file) @app = @util.plugin end describe "#application_description" do it "should have a description set" do @app.should have_a_description end end describe "#post_option_parser" do it "should raise an exception if package and actions are not specified" do @app.expects(:puts).with("Please specify an action and a package.") expect{ @app.post_option_parser({}) }.to raise_error("exit") end it "should raise an exception if action isn't install, update, uninstall, purge or status" do ARGV << "action" ARGV << "package" @app.expects(:puts).with("Action must be install, update, uninstall, purge, or status.") expect{ @app.post_option_parser({}) }.to raise_error("exit") end it "should set action and package" do ARGV << "install" ARGV << "package" configuration = {:action => "", :package => ""} @app.post_option_parser(configuration) configuration[:action].should == "install" configuration[:package].should == "package" end end describe "#validate_configuration" do it "should exit if filter is empty and user input is not 'y'" do @app.expects(:options).returns({:filter => nil}) @app.stubs(:print) MCollective::Util.expects(:empty_filter?).returns(true) STDOUT.expects(:flush) STDIN.expects(:gets).returns("n") expect{ @app.validate_configuration({}) }.to raise_error("exit") end it "should return if filter is empty and user input is 'y'" do @app.expects(:options).returns({:filter => nil}) @app.stubs(:print) MCollective::Util.expects(:empty_filter?).returns(true) STDOUT.expects(:flush) STDIN.expects(:gets).returns("y") @app.validate_configuration({}) end it "should return if filter is not empty" do @app.expects(:options).returns({:filter => nil}) MCollective::Util.expects(:empty_filter?).returns(false) @app.validate_configuration({}) end end describe "#summarize" do it "should print package statistics" do stats = {:discovered => 1, :responses => 1, :blocktime => 100} versions = {"1.1" => 1} @app.stubs(:print) @app.expects(:puts).with(" Nodes: 1 / 1") @app.expects(:puts).with("1 * 1.1") @app.expects(:printf).with(" Elapsed Time: %.2f s\n\n", 100) @app.summarize(stats, versions) end end describe "#main" do before do @rpcclient_mock = mock end it "should print error when response statuscode is not 0" do @app.stubs(:configuration).returns({:action => "install", :package => "package"}) @app.expects(:rpcclient).with("package", :options => nil).returns(@rpcclient_mock) @rpcclient_mock.expects(:send).with("install", :package => "package").returns([{:data => {:properties => ""}, :statuscode => 1, :sender => "node1", :statusmsg => "failure"}]) @rpcclient_mock.expects(:stats) @app.expects(:summarize) @app.expects(:printf).with("%-40s error = %s\n", "node1", "failure") @app.main end it "should set version and release if statuscode is 0 and the response includes a version number" do @app.stubs(:configuration).returns({:action => "install", :package => "package"}) @app.expects(:rpcclient).with("package", :options => nil).returns(@rpcclient_mock) @rpcclient_mock.expects(:send).with("install", :package => "package").returns([{:data => {:properties => {:version => "1", :release => "2", :name => "package"}}, :statuscode => 0, :sender => "node1", :statusmsg => "failure"}]) @app.expects(:printf).with("%-40s version = %s-%s\n", "node1", "package", "1-2") @rpcclient_mock.expects(:stats) @app.expects(:summarize) @app.main end it "should set ensure status as version if version is not defined" do @app.stubs(:configuration).returns({:action => "install", :package => "package"}) @app.expects(:rpcclient).with("package", :options => nil).returns(@rpcclient_mock) @rpcclient_mock.expects(:send).with("install", :package => "package").returns([{:data => {:properties => {:ensure => "latest"}}, :statuscode => 0, :sender => "node1", :statusmsg => "failure"}]) @app.expects(:printf).with("%-40s version = %s\n", "node1", "latest") @rpcclient_mock.expects(:stats) @app.expects(:summarize) @app.main end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/000077500000000000000000000000001174600156000231255ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/agent/000077500000000000000000000000001174600156000242235ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/agent/process.ddl000066400000000000000000000045041174600156000263710ustar00rootroot00000000000000metadata :name => "SimpleRPC Agent For Process Management", :description => "Agent To Manage Processes", :author => "R.I.Pienaar", :license => "Apache 2.0", :version => "1.2", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 10 action "list", :description => "List Processes" do input :pattern, :prompt => "Pattern to match", :description => "List only processes matching this patten", :type => :string, :validation => '^.+$', :optional => true, :maxlength => 50 input :just_zombies, :prompt => "Zombies Only", :description => "Restrict the process list to Zombie Processes only", :type => :boolean, :optional => true output :pslist, :description => "Process List", :display_as => "The process list" end action "kill", :description => "Kills a process" do input :pid, :prompt => "PID", :description => "The PID to kill", :type => :string, :validation => '^\d+$', :optional => false, :maxlength => 6 input :signal, :prompt => "Signal", :description => "The signal to send", :type => :string, :validation => '^.+$', :optional => false, :maxlength => 6 output :killed, :description => "Indicates if the process was signalled", :display_as => "Status" end action "pkill", :description => "Kill all processes matching filter" do input :pattern, :prompt => "Pattern to match", :description => "List only processes matching this patten", :type => :string, :validation => '^.+$', :optional => true, :maxlength => 50 input :signal, :prompt => "Signal", :description => "The signal to send", :type => :string, :validation => '^.+$', :optional => false, :maxlength => 6 output :killed, :description => "Number of processes signalled", :display_as => "Processes Signalled" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/agent/process.rb000066400000000000000000000065511174600156000262350ustar00rootroot00000000000000module MCollective module Agent class Process "SimpleRPC Agent For Process Management", :description => "Agent To Manage Processes", :author => "R.I.Pienaar", :license => "Apache 2.0", :version => "1.2", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 10 # List all processes, accepts an optional pattern action "list" do pattern = request[:pattern] || "." zombies = request[:just_zombies] || false reply[:pslist] = get_proc_list(pattern, zombies) end # Kills a certain pid with a signal action "kill" do validate :signal, :shellsafe validate :pid, /^\d+$/ killpid(request[:signal], request[:pid].to_i) if reply.statuscode == 0 reply[:killed] = 1 else reply[:killed] = 0 end end # Kills all processes matching a pattern # with a given signal action "pkill" do validate :signal, :shellsafe validate :pattern, String pids_to_kill = [] get_proc_list(request[:pattern], false).each do |ps| pids_to_kill << ps[:pid] end # Sanity check if get_proc_list(".", false).size == pids_to_kill.size reply.fail "Pattern matches all (#{pids_to_kill.size}) processes, refusing to kill" return else reply[:killed] = 0 pids_to_kill.each do |pid| system("logger -t mcolletive 'killing pid #{pid} based on pattern #{request[:pattern]}'") killpid(request[:signal], pid) # bail out if something went wrong last if reply.statuscode > 0 reply[:killed] += 1 end end end private # Sends a signal to a process def killpid(signal, pid) if signal =~ /^\d+$/ signal = signal.to_i else signal = signal end if pid.is_a?(String) if pid =~ /^\d+/ pid = pid.to_i else raise "pid should be a number" end end begin ::Process.kill(signal, pid) rescue Exception => e reply.fail e.to_s end end # Converts the weird structure of ProcTable into a plain old hash def ps_to_hash(ps) result = {} ps.each_pair do |k,v| if k == :uid begin result[:username] = Etc.getpwuid(v).name rescue Exception => e logger.debug("Could not get username for #{v}: #{e}") result[:username] = v end end result[k] = v end result end # Returns a hash of processes where cmdline matches pattern def get_proc_list(pattern, zombies) require 'sys/proctable' require 'etc' res = Sys::ProcTable.ps.map do |ps| ret = nil if ps["cmdline"] =~ /#{pattern}/ if zombies ret = ps_to_hash(ps) if ps[:state] == "Z" else ret = ps_to_hash(ps) end end ret end res.compact end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/application/000077500000000000000000000000001174600156000254305ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/process/application/pgrep.rb000066400000000000000000000044011174600156000270710ustar00rootroot00000000000000class MCollective::Application::Pgrep [-z]" option :just_zombies, :description => "Only list defunct processes", :arguments => ["-z", "--zombies"], :type => :bool class ::Numeric def bytes_to_human if self > 0 units = %w{B KB MB GB TB} e = (Math.log(self)/Math.log(1024)).floor s = "%.3f " % (to_f / 1024**e) s.sub(/\.?0*$/, units[e]) else "0 B" end end end def post_option_parser(configuration) if ARGV.length == 1 configuration[:pattern] = ARGV.shift else abort "Please provide a pattern to search for" end end def validate_configuration(configuration) abort "Please provide a pattern to search for" unless configuration.include?(:pattern) end def main ps = rpcclient("process") stats = {:count => 0, :hosts => 0, :vsize => 0, :rss => 0} ps.list(configuration).each_with_index do |result, i| begin if result[:data][:pslist].size > 0 stats[:hosts] += 1 puts result[:sender] result[:data][:pslist].each_with_index do |process, idx| puts " %5s %-10s %-15s %s" % ["PID", "USER", "VSZ", "COMMAND"] if idx == 0 process[:state] == "Z" ? cmdline = "[#{process[:cmdline]}]" : cmdline = process[:cmdline] puts " %5d %-10s %-15s %s" % [process[:pid], process[:username][0,10], process[:vsize].bytes_to_human, cmdline[0,60] ] if process[:cmdline].match configuration[:pattern] stats[:count] += 1 stats[:vsize] += process[:vsize] stats[:rss] += process[:rss] * 1024 end puts end rescue => e STDERR.puts "Failed to get results from #{result[:sender]}: #{e.class}: #{e}" end end puts " ---- process list stats ----" puts " Matched hosts: #{stats[:hosts]}" puts " Matched processes: #{stats[:count]}" puts " Resident Size: #{stats[:rss].bytes_to_human}" puts " Virtual Size: #{stats[:vsize].bytes_to_human}" printrpcstats end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/000077500000000000000000000000001174600156000232705ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/agent/000077500000000000000000000000001174600156000243665ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/agent/puppetca.ddl000066400000000000000000000047401174600156000267010ustar00rootroot00000000000000metadata :name => "SimpleRPC Service Agent", :description => "Agent to manage services using the Puppet service provider", :author => "R.I.Pienaar", :license => "Apache 2.0", :version => "1.2", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 20 action "clean", :description => "Performs a puppetca --clean on a certficate" do input :certname, :prompt => "Certificate Name", :description => "Certificate Name to clean", :type => :string, :validation => '^[a-zA-Z\-_\d\.]+$', :optional => false, :maxlength => 100 output :msg, :description => "Human readable status message", :display_as => "Result" end action "revoke", :description => "Revokes a certificate" do input :certname, :prompt => "Certificate Name", :description => "Certificate Name to revoke", :type => :string, :validation => '^[a-zA-Z\-_\d\.]+$', :optional => false, :maxlength => 100 output :out, :description => "Output from puppetca", :display_as => "Result" end action "sign", :description => "Signs a certificate request" do input :certname, :prompt => "Certificate Name", :description => "Certificate Name to sign", :type => :string, :validation => '^[a-zA-Z\-_\d\.]+$', :optional => false, :maxlength => 100 output :out, :description => "Output from puppetca", :display_as => "Result" end action "list", :description => "Lists all requested and signed certificates" do display :always output :requests, :description => "Waiting CSR Requests", :display_as => "Waiting CSRs" output :signed, :description => "Signed Certificates", :display_as => "Signed" end action "status", :description => "Find a certificate's status" do display :always input :certname, :prompt => "Certificate Name", :description => "Certificate Name to lookup", :type => :string, :validation => '^[a-zA-Z\-_\d\.]+$', :optional => false, :maxlength => 100 output :msg, :description => "Human readable status message", :display_as => "Result" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/agent/puppetca.rb000066400000000000000000000066541174600156000265470ustar00rootroot00000000000000module MCollective module Agent class Puppetca "Puppet CA Manager", :description => "Agent to manage Puppet certificates", :author => "R.I.Pienaar", :license => "Apache 2.0", :version => "2.0", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 20 def startup_hook @puppetca = @config.pluginconf["puppetca.puppetca"] || "/usr/sbin/puppetca" @cadir = @config.pluginconf["puppetca.cadir"] || "/var/lib/puppet/ssl/ca" end # Does what puppetca would do, deletes signed and csr # not just invoking puppetca since its slow. action "clean" do validate :certname, :shellsafe certname = request[:certname] signed = paths_for_cert(certname)[:signed] csr = paths_for_cert(certname)[:request] msg = [] if has_cert?(certname) File.unlink(signed) msg << "Removed signed cert: #{signed}." end if cert_waiting?(certname) File.unlink(csr) msg << "Removed csr: #{csr}." end if msg.size == 0 reply.fail "Could not find any certs to delete" else reply[:msg] = msg.join(" ") end end # revoke a cert, do the slow call to puppetca so we're 100% # certain we're doing the right thing action "revoke" do validate :certname, :shellsafe reply[:out] = run("#{@puppetca} --color=none --revoke '#{request[:certname]}'", :stdout => :output, :chomp => true) end # sign a cert if we have one waiting action "sign" do validate :certname, :shellsafe certname = request[:certname] reply.fail! "Already have a cert for #{certname} not attempting to sign again" if has_cert?(certname) if cert_waiting?(certname) reply[:out] = run("#{@puppetca} --color=none --sign '#{request[:certname]}'", :stdout => :output, :chomp => true) else reply.fail "No cert found to sign" end end # list all certs, signed and waiting action "list" do requests = Dir.entries("#{@cadir}/requests").grep(/pem/) signed = Dir.entries("#{@cadir}/signed").grep(/pem/) reply[:requests] = requests.map{|r| File.basename(r, ".pem")}.sort reply[:signed] = signed.map{|r| File.basename(r, ".pem")}.sort end # display status of a cert action "status" do validate :certname, :shellsafe certname = request[:certname] if has_cert?(certname) reply[:msg] = "signed" elsif cert_waiting?(certname) reply[:msg] = "awaiting signature" else reply[:msg] = "not found" end end private # checks if we have a signed cert matching certname def has_cert?(certname) File.exist?(paths_for_cert(certname)[:signed]) end # checks if we have a signing request waiting def cert_waiting?(certname) File.exist?(paths_for_cert(certname)[:request]) end # gets the paths to all files involved with a cert def paths_for_cert(certname) {:signed => "#{@cadir}/signed/#{certname}.pem", :request => "#{@cadir}/requests/#{certname}.pem"} end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/spec/000077500000000000000000000000001174600156000242225ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetca/spec/puppetca_agent_spec.rb000077500000000000000000000174311174600156000305710ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "puppetca agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/puppetca.rb"]) @agent = MCollective::Test::LocalAgentTest.new("puppetca", :agent_file => agent_file).plugin end describe "#meta" do it "should have valid metadata" do @agent.should have_valid_metadata end end describe "#clean" do it "should remove signed certs if they exist" do @agent.expects(:paths_for_cert).with("certname").twice.returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(true) File.expects(:unlink).with("signed") @agent.expects(:cert_waiting?).with("certname").returns(false) result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"Removed signed cert: signed.") end it "should remove unsigned certs if they exist" do @agent.expects(:paths_for_cert).with("certname").twice.returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(true) File.expects(:unlink).with("request") result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"Removed csr: request.") end it "should fail if there are no certs to delete" do @agent.expects(:paths_for_cert).with("certname").twice.returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(false) result = @agent.call(:clean, :certname => "certname") result.should be_aborted_error result[:statusmsg].should == "Could not find any certs to delete" end it "should return the message if there are no certs but msg.size is not 0" do @agent.expects(:paths_for_cert).with("certname").twice.returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(false) Array.any_instance.expects(:size).returns(1) result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg => "") end end describe "#revoke" do it "should revoke a cert" do @agent.expects(:run).with(" --color=none --revoke 'certname'", :stdout => :output, :chomp => true).returns("true") result = @agent.call(:revoke, :certname => "certname") result.should be_successful result.should have_data_items(:out => "true") end end describe "sign" do it "should fail if the cert has already been signed" do @agent.expects(:has_cert?).with("certname").returns(true) result = @agent.call(:sign, :certname => "certname") result.should be_aborted_error result[:statusmsg] = "Already have a cert for certname not attempting to sign again" end it "should fail if there are no certs to sign" do @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(false) result = @agent.call(:sign, :certname => "certname") result.should be_aborted_error result[:statusmsg].should == "No cert found to sign" end it "should sign a cert if there is one waiting" do @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(true) @agent.expects(:run).with(" --color=none --sign 'certname'", :stdout => :output, :chomp => true).returns("true") result = @agent.call(:sign, :certname => "certname") result.should be_successful result.should have_data_items(:out => "true") end end describe "list" do it "should list all certs, signed and waiting" do Dir.expects(:entries).with("/requests").returns(["requested.pem"]) Dir.expects(:entries).with("/signed").returns(["signed.pem"]) result = @agent.call(:list) result[:data].should == {:requests=>["requested"], :signed=>["signed"]} result.should be_successful result.should have_data_items(:signed=>["signed"], :requests=>["requested"]) end end describe "status" do it "should say so if the cert is signed" do @agent.expects(:has_cert?).with("certname").returns(true) result = @agent.call(:status, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"signed") end it "should say so if the cert is awaiting signature" do @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(true) result = @agent.call(:status, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"awaiting signature") end it "should say so if the cert is not found" do @agent.expects(:has_cert?).with("certname").returns(false) @agent.expects(:cert_waiting?).with("certname").returns(false) result = @agent.call(:status, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"not found") end end describe "has_cert" do it "should return true if we have a signed cert matching certname" do @agent.stubs(:paths_for_cert).with("certname").returns({:signed => "signed", :request => "request"}) File.expects(:exist?).with("signed").returns(true) File.expects(:unlink).with("signed") @agent.expects(:cert_waiting?).returns(false) result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"Removed signed cert: signed.") end it "should return false if we have a signed cert matching certname" do @agent.stubs(:paths_for_cert).with("certname").returns({:signed => "signed", :request => "request"}) File.expects(:exist?).with("signed").returns(false) @agent.expects(:cert_waiting?).returns(false) result = @agent.call(:clean, :certname => "certname") result.should be_aborted_error result[:statusmsg].should == "Could not find any certs to delete" end end describe "cert_waiting" do it "should return true if there is a signing request waiting" do @agent.stubs(:paths_for_cert).with("certname").returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(false) File.expects(:exist?).with("request").returns(true) File.expects(:unlink).with("request") result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"Removed csr: request.") end it "should return true if there is a signing request waiting" do @agent.stubs(:paths_for_cert).with("certname").returns({:signed => "signed", :request => "request"}) @agent.expects(:has_cert?).with("certname").returns(false) File.expects(:exist?).with("request").returns(false) result = @agent.call(:clean, :certname => "certname") result.should be_aborted_error result[:statusmsg].should == "Could not find any certs to delete" end end describe "paths_for_cert" do it "should return get paths to all files involged with a cert" do @agent.expects(:has_cert?).with("certname").returns(false) File.expects(:exist?).with("/requests/certname.pem").returns(true) File.expects(:unlink).with("/requests/certname.pem") result = @agent.call(:clean, :certname => "certname") result.should be_successful result.should have_data_items(:msg=>"Removed csr: /requests/certname.pem") end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/000077500000000000000000000000001174600156000231305ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/agent/000077500000000000000000000000001174600156000242265ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/agent/puppetd.ddl000066400000000000000000000051541174600156000264010ustar00rootroot00000000000000metadata :name => "Puppet Controller Agent", :description => "Run puppet agent, get its status, and enable/disable it", :author => "R.I.Pienaar", :license => "Apache License 2.0", :version => "1.5", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 20 action "last_run_summary", :description => "Get a summary of the last puppet run" do display :always output :time, :description => "Time per resource type", :display_as => "Times" output :resources, :description => "Overall resource counts", :display_as => "Resources" output :changes, :description => "Number of changes", :display_as => "Changes" output :events, :description => "Number of events", :display_as => "Events" end action "enable", :description => "Enable puppet agent" do output :output, :description => "String indicating status", :display_as => "Status" end action "disable", :description => "Disable puppet agent" do output :output, :description => "String indicating status", :display_as => "Status" end action "runonce", :description => "Invoke a single puppet run" do #input :forcerun, # :prompt => "Force puppet run", # :description => "Should the puppet run happen immediately?", # :type => :string, # :validation => '^.+$', # :optional => true, # :maxlength => 5 output :output, :description => "Output from puppet agent", :display_as => "Output" end action "status", :description => "Get puppet agent's status" do display :always output :status, :description => "The status of the puppet agent: disabled, running, idling or stopped", :display_as => "Status" output :enabled, :description => "Whether puppet agent is enabled", :display_as => "Enabled" output :running, :description => "Whether puppet agent is running", :display_as => "Running" output :idling, :description => "Whether puppet agent is idling", :display_as => "Idling" output :stopped, :description => "Whether puppet agent is stopped", :display_as => "Stopped" output :lastrun, :description => "When puppet agent last ran", :display_as => "Last Run" output :output, :description => "String displaying agent status", :display_as => "Status" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/agent/puppetd.rb000066400000000000000000000145721174600156000262450ustar00rootroot00000000000000module MCollective module Agent # An agent to manage the Puppet Daemon # # Configuration Options: # puppetd.splaytime - Number of seconds within which to splay; no splay # by default # puppetd.statefile - Where to find the state.yaml file; defaults to # /var/lib/puppet/state/state.yaml # puppetd.lockfile - Where to find the lock file; defaults to # /var/lib/puppet/state/puppetdlock # puppetd.puppetd - Where to find the puppet agent binary; defaults to # /usr/sbin/puppetd # puppetd.summary - Where to find the summary file written by Puppet # 2.6.8 and newer; defaults to # /var/lib/puppet/state/last_run_summary.yaml # puppetd.pidfile - Where to find puppet agent's pid file; defaults to # /var/run/puppet/agent.pid class Puppetd "Puppet Controller Agent", :description => "Run puppet agent, get its status, and enable/disable it", :author => "R.I.Pienaar", :license => "Apache License 2.0", :version => "1.5", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/AgentPuppetd", :timeout => 30 def startup_hook @splaytime = @config.pluginconf["puppetd.splaytime"].to_i || 0 @lockfile = @config.pluginconf["puppetd.lockfile"] || "/var/lib/puppet/state/puppetdlock" @statefile = @config.pluginconf["puppetd.statefile"] || "/var/lib/puppet/state/state.yaml" @pidfile = @config.pluginconf["puppet.pidfile"] || "/var/run/puppet/agent.pid" @puppetd = @config.pluginconf["puppetd.puppetd"] || "/usr/sbin/puppetd" @last_summary = @config.pluginconf["puppet.summary"] || "/var/lib/puppet/state/last_run_summary.yaml" end action "last_run_summary" do last_run_summary end action "enable" do enable end action "disable" do disable end action "runonce" do runonce end action "status" do set_status end private def last_run_summary summary = YAML.load_file(@last_summary) reply[:resources] = {"failed"=>0, "changed"=>0, "total"=>0, "restarted"=>0, "out_of_sync"=>0}.merge(summary["resources"]) ["time", "events", "changes"].each do |dat| reply[dat.to_sym] = summary[dat] end end def set_status reply[:status] = puppet_daemon_status reply[:running] = reply[:status] == 'running' ? 1 : 0 reply[:enabled] = reply[:status] == 'disabled' ? 0 : 1 reply[:idling] = reply[:status] == 'idling' ? 1 : 0 reply[:stopped] = reply[:status] == 'stopped' ? 1 : 0 reply[:lastrun] = 0 reply[:lastrun] = File.stat(@statefile).mtime.to_i if File.exists?(@statefile) reply[:output] = "Currently #{reply[:status]}; last completed run #{Time.now.to_i - reply[:lastrun]} seconds ago" end def puppet_daemon_status locked = File.exists?(@lockfile) disabled = locked && File::Stat.new(@lockfile).zero? has_pid = File.exists?(@pidfile) return 'disabled' if disabled return 'running' if locked && has_pid return 'idling' if ! locked && has_pid return 'stopped' if ! has_pid end def runonce set_status case (reply[:status]) when 'disabled' then # can't run reply.fail "Empty Lock file exists; puppet agent is disabled." when 'running' then # can't run two simultaniously reply.fail "Lock file and PID file exist; puppet agent is running." when 'idling' then # signal daemon pid = File.read(@pidfile) if pid !~ /^\d+$/ reply.fail "PID file does not contain a PID; got #{pid.inspect}" else begin ::Process.kill(0, Integer(pid)) # check that pid is alive # REVISIT: Should we add an extra round of security here, and # ensure that the PID file is securely owned, or that the target # process looks like Puppet? Otherwise a malicious user could # theoretically signal arbitrary processes with this... begin ::Process.kill("USR1", Integer(pid)) reply[:output] = "Signalled daemonized puppet agent to run (process #{Integer(pid)}); " + (reply[:output] || '') rescue Exception => e reply.fail "Failed to signal the puppet agent daemon (process #{pid}): #{e}" end rescue Errno::ESRCH => e # PID is invalid, run puppet onetime as usual runonce_background end end when 'stopped' then # just run runonce_background else reply.fail "Unknown puppet agent status: #{reply[:status]}" end end def runonce_background cmd = [@puppetd, "--onetime"] unless request[:forcerun] if @splaytime && @splaytime > 0 cmd << "--splaylimit" << @splaytime << "--splay" end end cmd = cmd.join(" ") output = reply[:output] || '' run(cmd, :stdout => :output, :chomp => true) reply[:output] = "Called #{cmd}, " + output + (reply[:output] || '') end def enable if File.exists?(@lockfile) stat = File::Stat.new(@lockfile) if stat.zero? File.unlink(@lockfile) reply[:output] = "Lock removed" else reply[:output] = "Currently running; can't remove lock" end else reply.fail "Already enabled" end end def disable if File.exists?(@lockfile) stat = File::Stat.new(@lockfile) stat.zero? ? reply.fail("Already disabled") : reply.fail("Currently running; can't remove lock") else begin File.open(@lockfile, "w") { |file| } reply[:output] = "Lock created" rescue Exception => e reply.fail "Could not create lock: #{e}" end end end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/application/000077500000000000000000000000001174600156000254335ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/application/puppetd.rb000066400000000000000000000112711174600156000274430ustar00rootroot00000000000000class MCollective::Application::Puppetd [CONCURRENCY] The ACTION can be one of the following: runall - invoke a puppet run on matching nodes, making sure to only run CONCURRENCY nodes at a time runonce - invoke a puppet run on matching nodes, limiting load with puppet agent's --splay option disable - create a lockfile that prevents puppet agent from running enable - remove any lockfile preventing puppet agent from running status - report whether puppet agent is enabled, whether it is currently running, and when the last run was summary - return detailed resource and timing info from the last puppet run count - return a total count of running, enabled, and disabled nodes END_OF_USAGE option :force, :description => "Force the puppet run to happen immediately without splay", :arguments => ["--force", "-f"], :type => :bool def post_option_parser(configuration) if ARGV.length >= 1 configuration[:command] = ARGV.shift configuration[:concurrency] = ARGV.shift.to_i or 0 unless configuration[:command].match(/^(enable|disable|runonce|runall|status|summary|count)$/) raise "Action must be enable, disable, runonce, runonce, runall, status, summary, or count" end else raise "Please specify an action." end end # Prints a log statement with a time def log(msg) puts("#{Time.now}> #{msg}") end # Checks concurrent runs every second and returns once its # below the given threshold def waitfor(concurrency, client) logged = false loop do running = 0 client.status do |resp| begin running += resp[:body][:data][:running].to_i rescue Exception => e log("Failed to get node status for #{e}; continuing") end end return running if running < concurrency log("Currently #{running} nodes running; waiting") unless logged logged = true sleep 2 end end def main mc = rpcclient("puppetd", :options => options) case configuration[:command] when "runall" if configuration[:concurrency] > 0 log("Running all machines with a concurrency of #{configuration[:concurrency]}") log("Discovering hosts to run") mc.progress = false hosts = mc.discover.sort log("Found #{hosts.size} hosts") # For all hosts: # - check for concurrent runs, wait till its below threshold # - do a run on the single host, regardless of if its already running # - log the output from the schedule command # - sleep a second hosts.each do |host| running = waitfor(configuration[:concurrency], mc) log("Running #{host}, concurrency is #{running}") result = mc.custom_request("runonce", {:forcerun => true}, host, {"identity" => host}) begin log("#{host} schedule status: #{result[0][:statusmsg]}") rescue log("#{host} returned unknown output: #{result.pretty_inspect}") end sleep 1 end else puts("Concurrency is #{configuration[:concurrency]}; not running any nodes") exit 1 end when "runonce" printrpc mc.runonce(:forcerun => configuration[:force]) when "status" mc.send(configuration[:command]).each do |node| if node[:statuscode] == 0 msg = node[:data][:output] else msg = node[:statusmsg] end puts "%-40s %s" % [ node[:sender], msg ] end when "summary" printrpc mc.last_run_summary when "count" running = enabled = total = stopped = idling = 0 mc.progress = false mc.status do |resp| begin running += resp[:body][:data][:running].to_i enabled += resp[:body][:data][:enabled].to_i idling += resp[:body][:data][:idling].to_i stopped += resp[:body][:data][:stopped].to_i total += 1 rescue Exception => e log("Failed to get node status for #{e}; continuing") end end disabled = total - enabled puts puts " Nodes currently enabled: #{enabled}" puts " Nodes currently disabled: #{disabled}" puts "Nodes currently doing puppet runs: #{running}" puts " Nodes currently stopped: #{stopped}" puts " Nodes currently idling: #{idling}" puts else printrpc mc.send(configuration[:command]) end mc.disconnect printrpcstats end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/commander/000077500000000000000000000000001174600156000250755ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/commander/puppetcommander.cfg000066400000000000000000000002261174600156000307610ustar00rootroot00000000000000--- :filter: "/dev_server/ country=de" :randomize: true :interval: 30 :concurrency: false :logfile: "/var/log/puppetcommander.log" :daemonize: false mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/commander/puppetcommander.init000077500000000000000000000041641174600156000311750ustar00rootroot00000000000000#! /bin/sh #puppetcommander Details to follow # #chkconfig: 345 24 76 # # #desciption Details to follow # # # ### BEGIN INIT INFO # Provides: puppetcommanderd # Required-Start: $remote_fs # Required-Stop: $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts daemon at boot time # Description: Enable Service provided by daemon ### END INIT INFO puppetcommanderd="/usr/sbin/puppetcommanderd" # Lockfile if [ -d /var/lock/subsys ]; then #Redhat/Centos subsystems lock="/var/lock/subsys/puppetcommander" else #Everything else lock="/var/lock/puppetcommander" fi # pidfile pidfile="/var/run/puppetcommander.pid" # Source function library . /lib/lsb/init-functions # Load options, set things like PSK's and SSL keys in here if [ -f /etc/sysconfig/puppetcommanderd ]; then . /etc/sysconfig/puppetcommanderd fi # Check if binary exists if ! [ -f $puppetcommanderd ] then echo "PuppetCommander binary not found." exit 0 fi # See how we were called case "$1" in start) echo -n "Starting PuppetCommander" if [ -f $lock ]; then if [ -s $pidfile ]; then kill `cat ${pidfile}` >/dev/null 2>&1 fi rm -f ${pidfile} rm -f ${lock} fi rm -f ${pidfile} ${puppetcommanderd} -d if [ $? = 0 ]; then log_success_msg touch $lock exit 0 else log_failure_msg exit 1 fi ;; stop) echo -n "Shutting down PuppetCommander" if [ -s $pidfile ]; then kill `cat ${pidfile}` >/dev/null 2>&1 fi rm -f ${pidfile} log_success_msg rm -f ${lock} ;; restart) $0 stop sleep 2 $0 start ;; condrestart) if [ -f ${lock} ]; then $0 stop sleep 2 $0 start fi ;; status) if [ -f ${lock} ]; then if [ -s ${pidfile} ]; then if [ -e /proc/`cat ${pidfile}` ]; then echo "PuppetCommander `cat ${pidfile}` is running." exit 0 else echo "PuppetCommander `cat ${pidfile}` is not running." exit 1 fi fi else echo "PuppetCommander service has not been started." exit 1 fi ;; force-reload) echo "Not implemented" ;; *) echo "Usage puppetcommanderd {start|stop|restart|condrestart|status}" exit 1 ;; esac exit 0 mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/commander/puppetcommanderd000077500000000000000000000155171174600156000304030ustar00rootroot00000000000000#!/usr/bin/ruby # A command and control bot to manage a number of puppet daemons # via the puppet agent for mcollective. This daemon will ensure that # puppet daemons run at a set interval and it will control the concurrency # across the cluster as a whole. # # The object is to make best use of the puppetmaster resources and to provide # a nice predictable utilization graphs of a master to assist in capacity planning. # # We also give priority to the Systems Administrator, if she runs an interactive # puppetd --test the background ones will back off - assuming her run triggers # the concurrency threshold. # # The concepts here was first introduced on a blog post below: # # http://www.devco.net/archives/2010/03/17/scheduling_puppet_with_mcollective.php # # This code is released under the Apache 2 licence. # # See https://github.com/puppetlabs/mcollective-plugins for more information. require 'mcollective' require 'yaml' require 'pp' require 'logger' include MCollective::RPC # Precedence order of configs are: # - defaults set here # - settings sotred in the config file # - settings passed on the command line # @config = { :interval => 30, :concurrency => false, :logfile => "/dev/null", :filter => "", :randomize => false, :daemonize => false} if File.exist?("/etc/puppetcommander.cfg") begin config = YAML::load_file('/etc/puppetcommander.cfg') if config.keys == @config.keys @config = YAML::load_file('/etc/puppetcommander.cfg') else raise("Could not parse config, not all options given") end rescue Exception => e puts "Failed to load config file /etc/puppetcommander.cfg: #{e}" exit 1 end end @options = rpcoptions do |parser, options| parser.define_head "Command and Control agent to schedule puppet runs in a cluster" parser.on('--interval [MINUTES]', '-i', 'Interval to run clients at') do |v| @config[:interval] = v.to_i end parser.on('--max-concurrent [ACTIVE]', '-m [ACTIVE]', 'Maximum run concurrency to allow') do |v| @config[:concurrency] = v.to_i end parser.on('--logfile [LOGFILE]', '-l [LOGFILE]', 'Log file to write') do |v| @config[:logfile] = v end parser.on("--daemonize", "-d", "Daemonizes the script") do |v| @config[:daemonize] = true end end @logger = Logger.new(@config[:logfile]) def log(msg) @logger.add(Logger::INFO) { msg } rescue Exception => e puts "Could not log '#{msg}': #{e}" end def debug(msg) if @options[:verbose] @logger.add(Logger::DEBUG) { msg } end rescue Exception => e puts "Could not log '#{msg}': #{e}" end # Does a SimpleRPC call and calculates the total amount of # puppet daemons that are currently running a catalog def concurrent_count debug("Getting puppet status") running = 0 @puppet.status do |resp| begin running += resp[:body][:data][:running].to_i rescue Exception => e debug("Failed to get node status: #{e}, continuing") end end running end # This sends a request to a specific client only. def run_client(client) log("Running agent for #{client}") @puppet.custom_request("runonce", {:forcerun => true}, client, {"identity" => client}) end # This is the beef of things, we take a desired interval and # maximum allowed concurrency and basically performs the following # pseudo actions: # # - count the clients that match the supplied filter # - figure out a desired sleep interval to run the number of clients # in the supplied maximum interval # - optionally shuffle the list of nodes based on the :randomize config option # - traverse the list of discovered clients alphabetically and does a # run of each one as long as the concurrency is below the limit. # - after running the node we sleep for the remaining time of the sleep interval # - once we've run all nodes, we rediscover and run again - this will pick # up new nodes and recover gracefully from network outages and such. # # TODO: Add exception handling. def run(interval, concurrency) @puppet.reset log("Doing discovery start of run") Timeout::timeout(5) { @clients = @puppet.discover :verbose => false } log("Discovered #{@puppet.discover.size} nodes") unless @clients.nil? or @clients.size == 0 sleeptime = interval * 60 / @clients.size log("Found #{@clients.size} puppet nodes, sleeping for ~#{sleeptime} seconds between runs") if @config[:randomize] @clients = @clients.sort_by { rand } else @clients.sort! end @clients.each do |client| starttime = Time.now.to_i begin cur_concurrency = concurrent_count log("Current puppetd's running: #{cur_concurrency}") if concurrency if cur_concurrency < concurrency run_client(client) else log("Puppet run for client #{client} skipped due to current concurrency of #{cur_concurrency}") end else run_client(client) end rescue Exception => e log("Runner raised an exception for client #{client}: #{e}") log(e) ensure sleeptime = (interval * 60 / @clients.size) - (Time.now.to_i - starttime) log("Sleeping for #{sleeptime} seconds") sleep(sleeptime > 0 ? sleeptime : 1) end end else log("No Puppet clients found.") end end def connect @puppet = rpcclient("puppetd", :options => @options) @puppet.progress = false # do some naive parsing of the filter, this is pretty poor and should be set using # the new in 1.1.0 MCOLLECTIVE_EXTRA_OPTS setting if @puppet.options[:filter]["fact"] == [] || @puppet.options[:filter]["cf_class"] == [] @config[:filter].split(" ").each do |f| if f =~ /^(.+?)=(.+)/ @puppet.fact_filter $1, $2 else @puppet.class_filter f end end end end # starts the worker loop in the foreground def foreground connect loop do run(@config[:interval], @config[:concurrency]) end end # starts the worker loop in the background def background pid = fork do Process.setsid exit if fork Dir.chdir('/tmp') STDIN.reopen('/dev/null') STDOUT.reopen('/dev/null', 'a') STDERR.reopen('/dev/null', 'a') File.open("/var/run/puppetcommander.pid", "w") do |pidfile| pidfile.puts $$ end connect loop do begin log("Starting run: interval: #{@config[:interval]} concurrency: #{@config[:concurrency]}") run(@config[:interval], @config[:concurrency]) log("Run ended, restarting") rescue Exception => e log("Runner raised an exception: #{e}") log(e) sleep 5 retry end end end Process.detach(pid) end log("Looping clients with an interval of #{@config[:interval]} minutes") log("Restricting to #{@config[:concurrency]} concurrent puppet runs") if @config[:concurrency] ["TERM", "HUP"].each do |sig| Signal.trap(sig) do exit! end end if @config[:daemonize] background else foreground end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/spec/000077500000000000000000000000001174600156000240625ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/spec/puppetd_agent_spec.rb000077500000000000000000000251161174600156000302700ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "puppetd agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/puppetd.rb"]) @agent = MCollective::Test::LocalAgentTest.new("puppetd", :agent_file => agent_file).plugin @agent.instance_variable_set("@lockfile", "spec_test_lock_file") @agent.instance_variable_set("@pidfile", "spec_test_pid_file") end describe "#meta" do it "should have valid metadata" do @agent.should have_valid_metadata end end describe "#last_run_summary" do it "should return the last run summary" do @agent.instance_variable_set("@last_summary", "spec_test_last_summary") YAML.expects(:load_file).with("spec_test_last_summary").returns({"time" => nil, "events" => nil, "changes" => nil, "resources" => {"failed" => 0,"changed" => 0, "total" => 0, "restarted" => 0, "out_of_sync" => 0}}) result = @agent.call(:last_run_summary) result.should be_successful result.should have_data_items({:changes => nil, :events => nil, :resources => {"failed"=>0, "changed"=>0, "total"=>0, "restarted"=>0, "out_of_sync"=>0}, :time => nil}) end end describe "#enable" do it "should fail if the lockfile doesn't exist" do File.expects(:exists?).returns(false) result = @agent.call(:enable) result.should be_aborted_error result[:statusmsg].should == "Already enabled" end it "should attempt to remove zero byte lockfiles" do stat = mock stat.stubs(:zero?).returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) File.expects(:exists?).with("spec_test_lock_file").returns(true) File.expects(:unlink).with("spec_test_lock_file").returns(true) result = @agent.call(:enable) result.should be_successful result.should have_data_items(:output=>"Lock removed") end it "should not remove lock files for running puppetds" do stat = mock stat.stubs(:zero?).returns(false) File.expects(:exists?).with("spec_test_lock_file").returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) result = @agent.call(:enable) result.should have_data_items(:output=>"Currently running; can't remove lock") end end describe "#disable" do it "should fail if puppetd is already disabled" do stat = mock stat.stubs(:zero?).returns(true) File.expects(:exists?).returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) result = @agent.call(:disable) result.should be_aborted_error result[:statusmsg].should == "Already disabled" end it "should fail if puppetd is running" do stat = mock stat.stubs(:zero?).returns(false) File.expects(:exists?).returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) result = @agent.call(:disable) result.should be_aborted_error result[:statusmsg].should == "Currently running; can't remove lock" end it "should create the lock if the lockfile doesn't exist" do File.expects(:exists?).returns(false) File.expects(:open).with("spec_test_lock_file", "w").yields(nil) result = @agent.call(:disable) result.should have_data_items(:output=>"Lock created") end #Note, this test will not pass until the puppetd agent's #disable method fails on a raised exception it "should raise an exception if the file cannot be written" do File.expects(:exists?).returns(false) File.expects(:open).with("spec_test_lock_file", "w").raises("foo") result = @agent.call(:disable) result.should be_aborted_error result[:statusmsg].should == "Could not create lock: foo" end end describe "#runonce" do before do @agent.instance_variable_set("@statefile", "spec_test_state_file") end it "with puppet agent disabled" do @agent.expects(:puppet_daemon_status).returns('disabled') result = @agent.call(:runonce) result.should be_aborted_error result[:statusmsg].should == "Empty Lock file exists; puppet agent is disabled." end it "with puppet agent actively running" do @agent.expects(:puppet_daemon_status).returns('running') result = @agent.call(:runonce) result[:statusmsg].should == "Lock file and PID file exist; puppet agent is running." result.should be_aborted_error end it "with puppet agent stopped" do @agent.expects(:puppet_daemon_status).returns('stopped') @agent.instance_variable_set("@puppetd", "spec_test_puppetd") @agent.expects(:run).with("spec_test_puppetd --onetime", :stdout => :output, :chomp => true) result = @agent.call(:runonce) result[:statusmsg].should == "OK" result.should be_successful end it "with puppet agent idling as a daemon" do @agent.expects(:puppet_daemon_status).returns('idling') File.expects(:read).with("spec_test_pid_file").returns("99999999\n") ::Process.expects(:kill).with(0, 99999999).returns(1) ::Process.expects(:kill).with("USR1", 99999999).once result = @agent.call(:runonce) result[:statusmsg].should == "OK" result[:data][:output].should =~ /Signalled daemonized puppet agent to run \(process 99999999\); Currently idling; last completed run/ result.should be_successful end it "with puppet agent stale pid file" do @agent.expects(:puppet_daemon_status).returns('idling') File.expects(:read).with("spec_test_pid_file").returns("99999999\n") ::Process.expects(:kill).with(0, 99999999).raises(Errno::ESRCH) ::Process.expects(:kill).with("USR1", 99999999).never @agent.instance_variable_set("@puppetd", "spec_test_puppetd") @agent.expects(:run).with("spec_test_puppetd --onetime", :stdout => :output, :chomp => true) result = @agent.call(:runonce) result[:statusmsg].should == "OK" result.should be_successful end it "with PID file containing rubbish" do @agent.expects(:puppet_daemon_status).returns('idling') File.expects(:read).with("spec_test_pid_file").returns("fred\nwilma\nbarney\n") result = @agent.call(:runonce) result[:statusmsg].should == "PID file does not contain a PID; got \"fred\\nwilma\\nbarney\\n\"" result.should be_aborted_error end it "runs puppet if it is not already running, with splaytime if request[:forcerun] is true" do @agent.instance_variable_set("@puppetd", "spec_test_puppetd") @agent.instance_variable_set("@splaytime", 1) @agent.expects(:run).with("spec_test_puppetd --onetime --splaylimit 1 --splay", :stdout => :output, :chomp => true) result = @agent.call(:runonce) result.should be_successful end it "runs puppet if it is not already running, without splaytime if require[:forcerun] is false" do @agent.instance_variable_set("@puppetd", "spec_test_puppetd") @agent.expects(:run).with("spec_test_puppetd --onetime", :stdout => :output, :chomp => true) result = @agent.call(:runonce, :forcerun => true) result.should be_successful end end describe "#status" do it "is disabled when the lockfile exists and its size is 0" do stat = mock stat.stubs(:zero?).returns(true) @agent.instance_variable_set("@statefile", "spec_test_state_file") @agent.instance_variable_set("@pidfile", "spec_test_pid_file") File.expects(:exists?).with("spec_test_lock_file").returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) File.expects(:exists?).with("spec_test_state_file").returns(false) File.expects(:exists?).with("spec_test_pid_file").returns(false) result = @agent.call(:status) result.should be_successful result.should have_data_items({ :status => 'disabled', :running => 0, :enabled => 0, :idling => 0, :stopped => 0, :lastrun => 0, :output => /Currently disabled; last completed run \d+ seconds ago/ }) end it "is enabled and running when the pidfile exists and the lockfile exists and its size is not 0" do stat = mock stat.stubs(:zero?).returns(false) @agent.instance_variable_set("@statefile", "spec_test_state_file") @agent.instance_variable_set("@pidfile", "spec_test_pid_file") File.expects(:exists?).with("spec_test_lock_file").returns(true) File::Stat.expects(:new).with("spec_test_lock_file").returns(stat) File.expects(:exists?).with("spec_test_state_file").returns(false) File.expects(:exists?).with("spec_test_pid_file").returns(true) result = @agent.call(:status) result.should be_successful result.should have_data_items({ :status => 'running', :running => 1, :enabled => 1, :idling => 0, :stopped => 0, :lastrun => 0, :output => /Currently running; last completed run \d+ seconds ago/ }) end it "is enabled and idling if the pidfile exists but the lockfile does not exist" do File.expects(:exists?).with("spec_test_lock_file").returns(false) File.expects(:exists?).with("spec_test_state_file").returns(false) File.expects(:exists?).with("spec_test_pid_file").returns(true) @agent.instance_variable_set("@statefile", "spec_test_state_file") result = @agent.call(:status) result.should be_successful result.should have_data_items({ :status => 'idling', :running => 0, :enabled => 1, :idling => 1, :stopped => 0, :lastrun => 0, :output => /Currently idling; last completed run \d+ seconds ago/ }) end it "is enabled and stopped if the pidfile does not exist and the lockfile does not exist" do File.expects(:exists?).with("spec_test_lock_file").returns(false) File.expects(:exists?).with("spec_test_state_file").returns(false) File.expects(:exists?).with("spec_test_pid_file").returns(false) @agent.instance_variable_set("@statefile", "spec_test_state_file") result = @agent.call(:status) result.should be_successful result.should have_data_items({ :status => 'stopped', :running => 0, :enabled => 1, :idling => 0, :stopped => 1, :lastrun => 0, :output => /Currently stopped; last completed run \d+ seconds ago/ }) end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetd/spec/puppetd_application_spec.rb000077500000000000000000000170641174600156000315000ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' module Mcollective describe "puppetd application" do before do application_file = File.join([File.dirname(__FILE__), "../application/puppetd.rb"]) @util = MCollective::Test::ApplicationTest.new("puppetd", :application_file => application_file) @app = @util.plugin end describe "#application_description" do it "should have a description" do @app.should have_a_description end end describe "#post_option_parser" do it "should raise an exception if no command has been specified" do expect{ @app.post_option_parser({}) }.to raise_error("Please specify an action.") end it "should raise an exception if the given command doesn't match the list of puppetd commands" do ARGV << "invalid" expect{ @app.post_option_parser(:command => "") }.to raise_error("Action must be enable, disable, runonce, runonce, runall, status, summary, or count") end it "should set command and concurrency" do ARGV << "enable" ARGV << "1" configuration = {:command => "", :concurrency => ""} @app.post_option_parser(configuration) configuration[:command].should == "enable" configuration[:concurrency].should == 1 end end describe "#log" do it "should print a log statement with a time" do @app.expects(:puts) @app.log("foo") end end describe "#waitfor" do it "should return if concurrency is greater than the amount of puppet runs" do rpcclient_mock = mock rpcclient_mock.expects(:status).yields({:body => {:data => {:running => 0}}}) @app.waitfor(1, rpcclient_mock) end it "should log a warning and continue of the rpcclient response is misformed" do rpcclient_mock = mock rpcclient_mock.expects(:status).yields(nil) @app.expects(:log).with("Failed to get node status for undefined method `[]' for nil:NilClass; continuing") @app.waitfor(1, rpcclient_mock) end it "should wait if the amount of puppet runs are greater than the concurrency" do rpcclient_mock = mock rpcclient_mock.expects(:status).yields({:body => {:data => {:running => 0}}}) rpcclient_mock.expects(:status).yields({:body => {:data => {:running => 1}}}) @app.expects(:log).with("Currently 1 nodes running; waiting") @app.expects(:sleep).with(2) @app.waitfor(1, rpcclient_mock) end end describe "#main" do it "should exit with message if command is runall and concurrency is 0" do @app.stubs(:configuration).returns({:command => "runall", :concurrency => 0}) @app.expects(:puts).with("Concurrency is 0; not running any nodes") @app.expects(:rpcclient).with("puppetd", :options => nil) expect{ @app.main }.to raise_error("exit") end it "should run puppet on all nodes if command is runall and concurrency is > 0" do rpcclient_mock = mock @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) @app.stubs(:configuration).returns({:command => "runall", :concurrency => 1}) @app.stubs(:log) rpcclient_mock.expects(:progress=).with(false) rpcclient_mock.expects(:discover).returns(["node1","node2"]) @app.expects(:waitfor).with(1, rpcclient_mock).twice rpcclient_mock.expects(:custom_request).with("runonce", {:forcerun => true}, "node1", {"identity" => "node1"}).returns([:statusmsg => "success"]) rpcclient_mock.expects(:custom_request).with("runonce", {:forcerun => true}, "node2", {"identity" => "node2"}).returns(false) @app.expects(:log).with("node1 schedule status: success") @app.expects(:log).with("node2 returned unknown output: false\n") @app.stubs(:sleep).with(1) rpcclient_mock.expects(:disconnect) @app.main end it "should do a single puppet run if command is runonce" do rpcclient_mock = mock @app.stubs(:configuration).returns({:command => "runonce", :force => true}) @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) @app.expects(:printrpc) rpcclient_mock.expects(:runonce).with(:forcerun => true) rpcclient_mock.expects(:disconnect) @app.main end it "should display status if command is status" do rpcclient_mock = mock @app.stubs(:configuration).returns({:command => "status"}) @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) rpcclient_mock.expects(:send).returns([ { :sender => "node1", :statuscode => 0, :data => {:output => "Currently idling; success"} }, { :sender => "node2", :statuscode => 1, :statusmsg => "failure" } ]) @app.expects(:puts).with("node1 Currently idling; success") @app.expects(:puts).with("node2 failure") @util.config.stubs(:color) rpcclient_mock.expects(:disconnect) @app.main end it "should dislpay last run summary if command is summary" do rpcclient_mock = mock @app.stubs(:configuration).returns({:command => "summary"}) @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) @app.expects(:printrpc) rpcclient_mock.expects(:last_run_summary) rpcclient_mock.expects(:disconnect) @app.main end it "should raise an exception and continue if node status cannot be retrieved and command is set to count" do rpcclient_mock = mock @app.stubs(:configuration).returns({:command => "count"}) @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) rpcclient_mock.expects(:progress=).with(false) rpcclient_mock.expects(:status).yields(nil) rpcclient_mock.expects(:disconnect) @app.expects(:log).with("Failed to get node status for undefined method `[]' for nil:NilClass; continuing") @app.main end it "should display node counts if command is set to count" do rpcclient_mock = mock @app.stubs(:configuration).returns({:command => "count"}) @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) rpcclient_mock.expects(:progress=).with(false) rpcclient_mock.expects(:status).yields(:body => {:data => { :running => "1", :enabled => "1", :stopped => "0", :idling => "0" }}) @app.expects(:puts).with(" Nodes currently enabled: 1") @app.expects(:puts).with(" Nodes currently disabled: 0") @app.expects(:puts).with("Nodes currently doing puppet runs: 1") @app.expects(:puts).with(" Nodes currently stopped: 0") @app.expects(:puts).with(" Nodes currently idling: 0") rpcclient_mock.expects(:disconnect) @app.main end it "should print the response from any other rpc's" do rpcclient_mock = mock @app.stubs(:configuration).returns(:command => "enable") @app.expects(:rpcclient).with("puppetd", :options => nil).returns(rpcclient_mock) rpcclient_mock.expects(:send).with("enable") @app.expects(:printrpc) rpcclient_mock.expects(:disconnect) @app.main end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/000077500000000000000000000000001174600156000234635ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/agent/000077500000000000000000000000001174600156000245615ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/agent/puppetral.ddl000066400000000000000000000061601174600156000272650ustar00rootroot00000000000000metadata :name => "Resource Abstraction Layer Agent", :description => "View and edit resources with Puppet's resource abstraction layer", :author => "R.I.Pienaar, Max Martin", :license => "ASL2", :version => "0.2", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 180 action "create", :description => "Add a resource via the RAL" do display :always input :type, :prompt => "Resource type", :description => "Type of resource to add", :type => :string, :validation => '.', :optional => false, :maxlength => 90 input :title, :prompt => "Resource name", :description => "Name of resource to add", :type => :string, :validation => '.', :optional => false, :maxlength => 90 input :avoid_conflict, :prompt => "Avoid conflict", :description => "Resource property to ignore if there's a conflict", :type => :string, :validation => '.', :optional => true, :maxlength => 90 output :status, :description => "Message indicating success or failure of the action", :display_as => "Status" output :resource, :description => "Resource that was created", :display_as => "Resource" end action "find", :description => "Get the attributes and status of a resource" do display :always input :type, :prompt => "Resource type", :description => "Type of resource to check", :type => :string, :validation => '.', :optional => false, :maxlength => 90 input :title, :prompt => "Resource title", :description => "Name of resource to check", :type => :string, :validation => '.', :optional => false, :maxlength => 90 output :type, :description => "Type of the inspected resource", :display_as => "Type" output :title, :description => "Title of the inspected resource", :display_as => "Title" output :tags, :description => "Tags of the inspected resource", :display_as => "Tags" output :exported, :description => "Boolean flag indicating export status", :display_as => "Exported" output :parameters, :description => "Parameters of the inspected resource", :display_as => "Parameters" end action "search", :description => "Get detailed info for all resources of a given type" do display :always input :type, :prompt => "Resource type", :description => "Type of resource to check", :type => :string, :validation => '.', :optional => false, :maxlength => 90 output :result, :description => "The values of the inspected resources", :display_as => "Result" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/agent/puppetral.rb000066400000000000000000000104111174600156000271170ustar00rootroot00000000000000require 'puppet' module MCollective module Agent # An agent that uses the Puppet resource abstraction layer (RAL) to perform # any action that Puppet supports. # # To use this you can make requests like: # # mco rpc puppetral create type=user name=foo comment="Example user" # # ...which will add a user foo with a descriptive comment. To delete the # user, run: # # mco rpc puppetral create type=user name=foo comment="Foo User" ensure=absent # # You can use puppetral to declare instances of any sensible Puppet type, # as long as you supply all of the attributes that the type requires. class Puppetral "Resource Abstraction Layer Agent", :description => "View and edit resources with Puppet's resource abstraction layer", :author => "R.I.Pienaar , Max Martin ", :license => "ASL2", :version => "0.2", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 180 action "create" do type = request[:type] title = request[:title] parameters = request[:parameters] parameters = remove_conflicts(type, title, parameters, request[:avoid_conflict]) resource = Puppet::Resource.new(type, title, :parameters => parameters) result, report = Puppet::Resource.indirection.save(resource) success = true if report && report.resource_statuses.first.last.failed reply[:status] = report.resource_statuses.first.last.events.first.message || "Resource was not created for an unknown reason." success = false end if success reply[:status] = "Resource was created" reply[:resource] = retain_params(Puppet::Resource.indirection.find([type, title].join('/'))) end end # Remove the avoid_conflict property if it clashes in one or more of # the following ways: # 1) A resource of the same type exists and has the same value for # the avoid_conflict property. # 2) A resource of the same type and title exists. def remove_conflicts(type, title, parameters, key) if key && parameters.has_key?(key) value = parameters[key] search_result = Puppet::Resource.indirection.search(type, {}) search_result.each do |result| resource = result.to_pson_data_hash if resource['parameters'][key.to_sym].to_s == value.to_s || resource['title'] == title parameters.delete key return parameters end end end parameters end # Before returning resources we will prune the parameters # so only properties remain, but certain types should have some of their # parameters retained (mostly, packages need provider info) def retain_params(resource) provider = resource[:provider] if resource.type.downcase == 'package' result = resource.respond_to?(:prune_parameters) ? resource.prune_parameters.to_pson_data_hash : resource.to_pson_data_hash result['parameters'][:provider] = provider if provider result end action "find" do type = request[:type] title = request[:title] typeobj = Puppet::Type.type(type) or raise "Could not find type #{type}" if typeobj resource = Puppet::Resource.indirection.find([type, title].join('/')) retain_params(resource).each { |k,v| reply[k] = v } begin managed_resources = File.readlines(Puppet[:resourcefile]) managed_resources = managed_resources.map{|r|r.chomp} reply[:managed] = managed_resources.include?("#{type}[#{title}]") rescue reply[:managed] = "unknown" end end end action "search" do type = request[:type] typeobj = Puppet::Type.type(type) or raise "Could not find type #{type}" if typeobj result = Puppet::Resource.indirection.search(type, {}).map do |r| retain_params(r) end result.each {|r| reply[r["title"]] = r} end end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/spec/000077500000000000000000000000001174600156000244155ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/puppetral/spec/puppetral_spec.rb000077500000000000000000000141031174600156000277720ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "puppetral agent" do before :all do agent_file = File.join([File.dirname(__FILE__), "../agent/puppetral.rb"]) @agent = MCollective::Test::LocalAgentTest.new("puppetral", :agent_file => agent_file).plugin end describe "#find" do it "should retrieve information about the type and title passed" do result = @agent.call(:find, :type => 'User', :title => 'bob') result[:data]['title'].should == "bob" result[:data]['type'].should == "User" end it "should specify an ensure value for the resource" do result = @agent.call(:find, :type => 'User', :title => 'bob') result[:data]['parameters'].keys.should include :ensure end it "should respond with an error if passed an invalid type" do result = @agent.call(:find, :type => 'Foobar', :title => 'Foobaz') result[:statusmsg].should =~ /Could not find type Foobar/ end it "should prune parameters from the result" do result = @agent.call(:find, :type => 'User', :title => 'root') result[:data]["parameters"].should_not have_key(:loglevel) end it "should leave the provider parameter on the result when looking up packages" do result = @agent.call(:find, :type => 'Package', :title => 'rspec') result[:data]['parameters'].should have_key(:provider) end end describe "#search" do it "should return a list of all resources of the type passed indexed by resource title" do result = @agent.call(:search, :type => 'User') result[:statusmsg].should == "OK" result[:data].each do |k,v| k.class.should == String v.keys.should =~ ["exported", "title", "parameters", "tags", "type"] end end it "should respond with an error if passed an invalid type" do result = @agent.call(:search, :type => 'Foobar') result[:statusmsg].should =~ /Could not find type Foobar/ end end describe "#create" do before :each do @tmpfile = '/tmp/foo' end after :each do File.delete(@tmpfile) if File.exist?(@tmpfile) end def group_with_gid(gid) [ stub({ :to_pson_data_hash => { "exported" => false, "title" => "wheel", "tags" => ["group", "wheel"], "type" => "Group", "parameters" => { :provider => :directoryservice, :attribute_membership => :minimum, :auth_membership => true, :loglevel => :notice, :ensure => :present, :members => ["root"], :gid => gid } } }) ] end describe "when avoiding conflicts" do it "should remove the passed parameter if there is a conflict with it" do Puppet::Resource.expects(:new).with('group', 'testgroup', :parameters => {:ensure=>'present'}) Puppet::Resource.indirection.expects(:save).returns({:ensure=>:present}) Puppet::Resource.indirection.expects(:search).with('group', {}).returns(group_with_gid(0)) result = @agent.call(:create, :type => 'group', :title => 'testgroup', :parameters => {:ensure => 'present', :gid => "0"}, :avoid_conflict => :gid) end it "should remove the passed parameter if there is a resource with a conflicting title" do Puppet::Resource.expects(:new).with('group', 'wheel', :parameters => {:ensure=>'present'}) Puppet::Resource.indirection.expects(:save).returns({:ensure=>:present}) Puppet::Resource.indirection.expects(:search).with('group', {}).returns(group_with_gid(2)) result = @agent.call(:create, :type => 'group', :title => 'wheel', :parameters => {:ensure => 'present', :gid => "0"}, :avoid_conflict => :gid) end it "should do nothing if there is no conflict" do Puppet::Resource.expects(:new).with('group', 'testgroup', :parameters => {:ensure=>'present', :gid => '555'}) Puppet::Resource.indirection.expects(:save).returns({:ensure=>:present}) Puppet::Resource.indirection.expects(:search).with('group', {}).returns(group_with_gid(0)) result = @agent.call(:create, :type => 'group', :title => 'testgroup', :parameters => {:ensure => 'present', :gid => "555"}, :avoid_conflict => :gid) end end describe "when not avoiding conflicts" do it "should create the resource described and respond with a success message" do result = @agent.call(:create, :type => 'file', :title => @tmpfile, :parameters => {:ensure => 'present', :content => "Hello, world!"}) File.open(@tmpfile, 'r') { |f| f.read.should == "Hello, world!" } result[:data][:status].should == "Resource was created" end it "should respond with error information if creating the resource fails" do badpath = "\\thisisa/bad\\path!!" result = @agent.call(:create, :type => 'file', :title => badpath, :parameters => {:ensure => 'present', :content => "Hello, world!"}) result[:statusmsg].should =~ /File paths must be fully qualified/ result[:statuscode].should_not == 0 end it "should report an error if the resource was not created" do badpath = "/etc/notpermitted" result = @agent.call(:create, :type => 'file', :title => badpath, :parameters => {:ensure => 'present', :content => "Hello, world!"}) result[:data][:status].should =~ /change from absent to present failed/ end it "should overwrite an existing resource with the same type and title with different properties" do File.open(@tmpfile,'w') { |f| f.puts "Goodbye, cruel world!" } result = @agent.call(:create, :type => 'file', :title => @tmpfile, :parameters => {:ensure => 'present', :content => "Hello, world!"}) File.open(@tmpfile, 'r') { |f| f.read.should == "Hello, world!" } end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/000077500000000000000000000000001174600156000256045ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/check_mcollective.rb000077500000000000000000000026021174600156000315770ustar00rootroot00000000000000#!/usr/bin/env ruby # # Original one (file based) by R.I. Pienaar : http://www.devco.net # Mongo version by Nicolas Szalay : http://www.rottenbytes.info require 'getoptlong' require 'mongo' opts = GetoptLong.new( [ '--interval', '-i', GetoptLong::REQUIRED_ARGUMENT], [ '--host', '-h', GetoptLong::REQUIRED_ARGUMENT], [ '--database', '-d', GetoptLong::REQUIRED_ARGUMENT], [ '--collection', '-c', GetoptLong::REQUIRED_ARGUMENT] ) total = 0 old = 0 interval = 300 mongohost="localhost" mongodb="puppet" collection="nodes" opts.each do |opt, arg| case opt when '--interval' interval = arg.to_i when '--host' mongohost = arg when '--database' mongodb = arg when '--collection' collection = arg end end dbh=Mongo::Connection.new(mongohost).db(mongodb) coll=dbh.collection(collection) coll.find().each { |row| seen = row["lastseen"] fqdn = row["fqdn"] total +=1 if (Time.now.to_i - seen) > interval old+=1 end } if old > 0 puts("CRITICAL: #{old} / #{total} hosts not checked in within #{interval} seconds| totalhosts=#{total} oldhosts=#{old} currenthosts=#{total - old}") exit 2 else puts("OK: #{total} / #{total} hosts checked in within #{interval} seconds| totalhosts=#{total} oldhosts=#{old} currenthosts=#{total - old}") exit 0 end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/000077500000000000000000000000001174600156000271215ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/README.markdown000066400000000000000000000017001174600156000316200ustar00rootroot00000000000000# A search engine for Puppet based on MCollective Registration These are puppet parser functions and utility scripts that use MCollective registration system to build a searchable database from within manifests and templates. Due to limitation with Puppets plugin system you need to install these in your ruby site lib.
   /usr/lib/ruby/site_ruby/1.8/puppet/util/mongoquery.rb
   /usr/lib/ruby/site_ruby/1.8/puppet/parser/function/load_node.rb
   /usr/lib/ruby/site_ruby/1.8/puppet/parser/function/search_nodes.rb
   /usr/lib/ruby/site_ruby/1.8/puppet/parser/function/search_setup.rb
You should add to your site.pp the following - assuming you already have the mongo registration setup and data flowing in there: search_setup("localhost", "puppet", "nodes") You can now use the search_nodes and load_node functions to search and load nodes. The load_nodes function require Puppet 2.6 hash support. See http://srt.ly/3t for use cases. mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/load_node.rb000066400000000000000000000005301174600156000313700ustar00rootroot00000000000000module Puppet::Parser::Functions newfunction(:load_node, :type => :rvalue) do |args| require 'puppet/util/mongoquery' results = Puppet::Util::MongoQuery.instance.find_nodes({"fqdn" => args[0]}) if results.is_a?(Array) return results.first else raise "Could not find a node matching #{args[0]}" end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/mongoquery.rb000066400000000000000000000036131174600156000316560ustar00rootroot00000000000000require 'rubygems' require 'mongo' class Puppet::Util::MongoQuery include Singleton def initialize @connected = false @host = nil @db = nil @collection = nil end # Returns just fqdn matching the query def find_node_names(query) query(query, {:fields => ["fqdn"]}).map{|result| result["fqdn"]} end # Finds all about nodes matching query def find_nodes(query) query(query).to_a end def setup(host, db, collection) unless (host == @host && db == @db && collection == @collection && connected?) @host = host @db = db @collection = collection connect end end private # Connect to mongodb def connect begin # Only reconnect if we see a new set of connection params and we arent # already connected to those unless (@host.nil? || @db.nil? || @collection.nil?) Puppet.notice("Creating new mongo db handle: host=#{@host} db=#{@db} collection=#{@collection}") @dbh = Mongo::Connection.new(@host).db(@db) @coll =@dbh.collection(@collection) @connected = true else raise Puppet::ParseError, "Cannot connect to mongo db until setup was called" end rescue Exception => e raise Puppet::ParseError, "Failed to connect to mongo db host=#{@host} db=#{@db} collection=#{@collection}: #{e}" end end # Checks if we're connected, reconnect if needed def query(q, opts={}) tries = 0 begin connect unless connected? @coll.find(q, opts) rescue retries += 1 if retries == 5 raise Puppet::ParseError, "Failed to query Mongo after 5 attempts" end connect retry end end # Are we connected? def connected? begin return true if @connected && @dbh.connection.connected? rescue Exception => e Puppet.notice("Not connected to mongo db: #{e}") return false end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/search_nodes.rb000066400000000000000000000003531174600156000321040ustar00rootroot00000000000000module Puppet::Parser::Functions newfunction(:search_nodes, :type => :rvalue) do |args| require 'puppet/util/mongoquery' searchspec = eval(args[0]) Puppet::Util::MongoQuery.instance.find_node_names(searchspec) end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/puppet/search_setup.rb000066400000000000000000000005651174600156000321410ustar00rootroot00000000000000module Puppet::Parser::Functions newfunction(:search_setup) do |args| require 'puppet/util/mongoquery' unless args.size == 3 raise Puppet::ParseError, "search_setup requires host, db and collection arguments" end host = args[0] db = args[1] collection = args[2] Puppet::Util::MongoQuery.instance.setup(host, db, collection) end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-mongodb/registration.rb000066400000000000000000000073121174600156000306460ustar00rootroot00000000000000module MCollective module Agent # A registration agent that places information from the meta # registration class into a mongo db instance. # # To get this going you need: # # - The meta registration plugin everywhere [1] # - A mongodb instance # - The mongo gem installed ideally with the bson_ext extension # # The following configuration options exist: # - plugin.registration.mongohost where the mongodb is default: localhost # - plugin.registration.mongodb the db name default: puppet # - plugin.registration.collection the collection name default: nodes # - plugin.registration.extra_yaml_dir the presense of this key indicates a directory # containing additional yaml files to push into an 'extra' key in the registration data. All files # in the directory are pushed into a hash with keys being the file name (with the # path and .yml extension stripped) and the value being the contents. default: false # Each document will have the following data: # - fqdn - the fqdn of the sender # - lastseen - last time we got data from it # - facts - a collection of facts # - agentlist - a collection of agents on the node # - classes - a collection of classes # # A unique constraint index will be created on the fqdn of the sending # hosts. # # Released under the terms of the Apache 2 licence, contact # rip@devco.net with questions class Registration attr_reader :timeout, :meta def initialize @meta = {:license => "Apache 2", :author => "R.I.Pienaar ", :url => "https://github.com/puppetlabs/mcollective-plugins"} require 'mongo' @timeout = 2 @config = Config.instance @mongohost = @config.pluginconf["registration.mongohost"] || "localhost" @mongodb = @config.pluginconf["registration.mongodb"] || "puppet" @collection = @config.pluginconf["registration.collection"] || "nodes" @yaml_dir = @config.pluginconf["registration.extra_yaml_dir"] || false Log.instance.debug("Connecting to mongodb @ #{@mongohost} db #{@mongodb} collection #{@collection}") @dbh = Mongo::Connection.new(@mongohost).db(@mongodb) @coll = @dbh.collection(@collection) @coll.create_index("fqdn", {:unique => true, :dropDups => true}) end def handlemsg(msg, connection) req = msg[:body] if (req.kind_of?(Array)) Log.instance.warn("Got no facts - did you forget to add 'registration = Meta' to your server.cfg?"); return nill end req[:fqdn] = req[:facts]["fqdn"] req[:lastseen] = Time.now.to_i # Optionally send a list of extra yaml files if (@yaml_dir != false) req[:extra] = {} Dir[@yaml_dir + "/*.yaml"].each do | f | req[:extra][File.basename(f).split('.')[0]] = YAML.load_file(f) end end # Sometimes facter doesnt send a fqdn?! if req[:fqdn].nil? Log.instance.debug("Got stats without a FQDN in facts") return nil end by_fqdn = {:fqdn => req[:fqdn]} doc_id = nil before = Time.now.to_f begin doc = @coll.find_and_modify(:query => by_fqdn, :update => {'$set' => req}, :new => true) doc_id = doc['_id'] rescue Mongo::OperationFailure doc_id = @coll.insert(req, {:safe => true}) ensure after = Time.now.to_f Log.instance.debug("Updated data for host #{req[:fqdn]} with id #{doc_id} in #{after - before}s") end nil end def help end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-monitor/000077500000000000000000000000001174600156000256465ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-monitor/check_mcollective.rb000077500000000000000000000027501174600156000316450ustar00rootroot00000000000000#!/usr/bin/env ruby # Nagios plugin to check mcollective if the registration-monitor # is in use. # # https://github.com/puppetlabs/mcollective-plugins require 'getoptlong' opts = GetoptLong.new( [ '--directory', '-d', GetoptLong::REQUIRED_ARGUMENT], [ '--interval', '-i', GetoptLong::REQUIRED_ARGUMENT], [ '--verbose', '-v', GetoptLong::NO_ARGUMENT] ) dir = "/var/tmp/mcollective" interval = 300 total = 0 old = 0 verbose = false opts.each do |opt, arg| case opt when '--directory' dir = arg when '--interval' interval = arg.to_i when '--verbose' verbose = true end end hosts = [ ] Dir.open(dir) do |files| files.each do |f| next if f.match /^\./ fage = File.stat("#{dir}/#{f}").mtime.to_i total += 1 if (Time.now.to_i - fage) > interval + 30 hosts.push f if verbose old += 1 end end end if old > 0 if verbose failed = hosts.join(', ') puts("CRITICAL: #{old} / #{total} hosts not checked in within #{interval} seconds - failed: #{failed}| totalhosts=#{total} oldhosts=#{old} currenthosts=#{total - old}") else puts("CRITICAL: #{old} / #{total} hosts not checked in within #{interval} seconds| totalhosts=#{total} oldhosts=#{old} currenthosts=#{total - old}") end exit 2 else puts("OK: #{total} / #{total} hosts checked in within #{interval} seconds| totalhosts=#{total} oldhosts=#{old} currenthosts=#{total - old}") exit 0 end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/registration-monitor/registration.rb000066400000000000000000000026241174600156000307110ustar00rootroot00000000000000module MCollective module Agent class Registration attr_reader :timeout, :meta def initialize @timeout = 1 @config = Config.instance @meta = {:license => "GPLv2", :author => "R.I.Pienaar ", :url => "https://github.com/puppetlabs/mcollective-plugins"} end def handlemsg(msg, connection) req = msg[:body] if @config.pluginconf.include?("registration.directory") dir = @config.pluginconf["registration.directory"] else dir = "/var/tmp/mcollective" end FileUtils.mkdir_p(dir) unless File.directory?(dir) File.open("#{dir}/#{msg[:senderid]}", 'w') {|f| f.write(YAML.dump(req)) } nil end def help <<-EOH Registration Agent ============= A simple registration agent that writes out a file per sender every time the sender checks in. The intention is to allow a simple means of detecting when a node has fallen off the grid. You can configure the directory where temp files get written using the plugin.registration.directory option in the config file. A simple nagios plugin is included to provide the monitoring.. EOH end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/000077500000000000000000000000001174600156000231075ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/agent/000077500000000000000000000000001174600156000242055ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/agent/puppet-service.rb000066400000000000000000000047731174600156000275200ustar00rootroot00000000000000require 'puppet' module MCollective module Agent # An agent that uses Puppet to manage services # # See https://github.com/puppetlabs/mcollective-plugins # # Released under the terms of the Apache Software License, v2.0. # # As this agent is based on Simple RPC, it requires mcollective 0.4.7 or newer. class Service "Service Agent", :description => "Start and stop system services", :author => "R.I.Pienaar", :license => "ASL2", :version => "2.0", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 60 ["stop", "start", "restart", "status"].each do |act| action act do do_service_action(act) end end private # Creates an instance of the Puppet service provider, supports config options: # # - service.hasrestart - set this if your OS provides restart options on services # - service.hasstatus - set this if your OS provides status options on services def get_puppet(service) hasstatus = false hasrestart = false if @config.pluginconf.include?("service.hasrestart") hasrestart = true if @config.pluginconf["service.hasrestart"] =~ /^1|y|t/ end if @config.pluginconf.include?("service.hasstatus") hasstatus = true if @config.pluginconf["service.hasstatus"] =~ /^1|y|t/ end if ::Puppet.version =~ /0.24/ ::Puppet::Type.type(:service).clear svc = ::Puppet::Type.type(:service).create(:name => service, :hasstatus => hasstatus, :hasrestart => hasrestart).provider else svc = ::Puppet::Type.type(:service).new(:name => service, :hasstatus => hasstatus, :hasrestart => hasrestart).provider end svc end # Does the actual work with the puppet provider and sets appropriate reply options def do_service_action(action) validate :service, String service = request[:service] begin Log.instance.debug("Doing #{action} for service #{service}") svc = get_puppet(service) unless action == "status" svc.send action sleep 0.5 end reply["status"] = svc.status.to_s rescue Exception => e reply.fail "#{e}" end end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/agent/service.ddl000066400000000000000000000026041174600156000263340ustar00rootroot00000000000000metadata :name => "Service Agent", :description => "Start and stop system services", :author => "R.I.Pienaar", :license => "ASL2", :version => "1.2", :url => "https://github.com/puppetlabs/mcollective-plugins", :timeout => 60 action "status", :description => "Gets the status of a service" do display :always input :service, :prompt => "Service Name", :description => "The service to get the status for", :type => :string, :validation => '^[a-zA-Z\.\-_\d]+$', :optional => false, :maxlength => 90 output "status", :description => "The status of the service", :display_as => "Service Status" end ["stop", "start", "restart"].each do |act| action act, :description => "#{act.capitalize} a service" do display :failed input :service, :prompt => "Service Name", :description => "The service to #{act}", :type => :string, :validation => '^[a-zA-Z\.\-_\d]+$', :optional => false, :maxlength => 90 output "status", :description => "The status of the service after #{act.sub(/p$/, 'pp')}ing", :display_as => "Service Status" end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/application/000077500000000000000000000000001174600156000254125ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/application/service.rb000066400000000000000000000076661174600156000274160ustar00rootroot00000000000000module MCollective class Application::Service < Application description "Start and stop system services" usage <<-END_OF_USAGE mco service [OPTIONS] [FILTERS] The ACTION can be one of the following: start - start service stop - stop service restart - restart or reload service status - determine current status of the remote service END_OF_USAGE def print_statistics(statistics, status_counter) print "\n---- service summary ----\n" puts " Nodes: #{statistics[:responses] + statistics[:noresponsefrom].size} / #{statistics[:responses]}" print " Statuses: " if status_counter.size > 0 status_counter.keys.sort.each do |status| case status when /^running$/ print "started=#{status_counter[status]} " when /^stopped$/ print "stopped=#{status_counter[status]} " when /^error$/ print "errors=#{status_counter[status]} " else print "unknown (#{status})=#{status_counter[status]} " end end else print "No responses received" end printf("\n Elapsed Time: %.2f s\n\n", statistics[:blocktime]) end def post_option_parser(configuration) if ARGV.size < 2 raise "Please specify service name and action" else service = ARGV.shift action = ARGV.shift unless action.match(/^(start|stop|restart|status)$/) raise "Action can only be start, stop, restart or status" end configuration[:service] = service configuration[:action] = action end end def validate_configuration(configuration) if MCollective::Util.empty_filter?(options[:filter]) print "Do you really want to operate on " + "services unfiltered? (y/n): " STDOUT.flush # Only match letter "y" or complete word "yes" ... exit! unless STDIN.gets.strip.match(/^(?:y|yes)$/i) end end def main # # We have to change our process name in order to hide name of the # service we are looking for from our execution arguments. Puppet # provider will look at the process list for the name of the service # it wants to manage and it might find us with our arguments there # which is not what we really want ... # $0 = "mco" status_counter = {} action = configuration[:action] service = configuration[:service] rpc_service = rpcclient("service", { :options => options }) rpc_service.send(action, { :service => service }).each do |node| # We want new line here ... puts if status_counter.size.zero? and not rpc_service.progress sender = node[:sender] data = node[:data] # # If the status code is non-zero and data is empty then we # assume that something out of an ordinary had place and # therefore assume that there was some sort of error ... # unless node[:statuscode].zero? and data status = "error" else status = data["status"] end status_counter.include?(status) ? status_counter[status] += 1 : status_counter[status] = 1 if rpc_service.verbose printf("%-40s status=%s\n", sender, status) puts "\t\t#{node[:statusmsg]}" else case action when /^start$/ unless status.match(/^running$/) printf("%-40s status=%s\n", sender, status) end when /^stop$/ unless status.match(/^stopped$/) printf("%-40s status=%s\n", sender, status) end when /^status$/ printf("%-40s status=%s\n", sender, status) end end end rpc_service.disconnect print_statistics(rpc_service.stats, status_counter) end end end # vim: set ts=4 sw=4 et : mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/spec/000077500000000000000000000000001174600156000240415ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/spec/service_agent_spec.rb000077500000000000000000000113721174600156000302250ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' describe "service agent" do before do agent_file = File.join([File.dirname(__FILE__), "../agent/puppet-service.rb"]) @agent = MCollective::Test::LocalAgentTest.new("service", :agent_file => agent_file).plugin @agent.stubs(:require).with('puppet').returns(true) end describe "#meta" do it "should have valid metadata" do @agent.should have_valid_metadata end end describe "#do_service_action" do before do logger = mock logger.stubs(:log) logger.stubs(:start) @plugin = mock @puppet_type = mock @puppet_service = mock @puppet_provider = mock MCollective::Log.configure(logger) end it "should succeed when action is not status and puppet version is not 0.24" do service = "service" action = "stop" @agent.config.expects(:pluginconf).times(3).returns(@plugin) @plugin.expects(:include?).with("service.hasrestart").returns(true) @plugin.expects(:[]).with("service.hasrestart").returns("1") @plugin.expects(:include?).with("service.hasstatus").returns(false) @puppet_service.expects(:provider).returns(@puppet_provider) Puppet.expects(:version).returns("0.xx") Puppet::Type.expects(:type).with(:service).returns(@puppet_type) @puppet_type.expects(:new).with(:name => service, :hasstatus => false, :hasrestart => true).returns(@puppet_service) @puppet_provider.expects(:send).with(action) @puppet_provider.expects(:status).returns(0) result = @agent.call("stop", :service => service) result.should be_successful result.should have_data_items({"status" => "0"}) end it "should succeed when action is status and puppet version is not 0.24" do service = "service" action = "status" @agent.config.expects(:pluginconf).times(3).returns(@plugin) @plugin.expects(:include?).with("service.hasrestart").returns(true) @plugin.expects(:[]).with("service.hasrestart").returns("1") @plugin.expects(:include?).with("service.hasstatus").returns(false) @puppet_service.expects(:provider).returns(@puppet_provider) Puppet.expects(:version).returns("0.xx") Puppet::Type.expects(:type).with(:service).returns(@puppet_type) @puppet_type.expects(:new).with(:name => service, :hasstatus => false, :hasrestart => true).returns(@puppet_service) @puppet_provider.expects(:status).returns(0) result = @agent.call("status", :service => service) result.should be_successful result.should have_data_items({"status" => "0"}) end it "should succeed when action is not status and puppet version is 0.24" do service = "service" action = "stop" @agent.config.expects(:pluginconf).times(3).returns(@plugin) @plugin.expects(:include?).with("service.hasrestart").returns(true) @plugin.expects(:[]).with("service.hasrestart").returns("1") @plugin.expects(:include?).with("service.hasstatus").returns(false) @puppet_service.expects(:provider).returns(@puppet_provider) Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:service).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => service, :hasstatus => false, :hasrestart => true).returns(@puppet_service) @puppet_provider.expects(:send).with(action) @puppet_provider.expects(:status).returns(0) result = @agent.call("stop", :service => service) result.should be_successful result.should have_data_items({"status" => "0"}) end it "should succeed when action is status and puppet version is 0.24" do service = "service" action = "status" @agent.config.expects(:pluginconf).times(3).returns(@plugin) @plugin.expects(:include?).with("service.hasrestart").returns(true) @plugin.expects(:[]).with("service.hasrestart").returns("1") @plugin.expects(:include?).with("service.hasstatus").returns(false) @puppet_service.expects(:provider).returns(@puppet_provider) Puppet.expects(:version).returns("0.24") Puppet::Type.expects(:type).with(:service).twice.returns(@puppet_type) @puppet_type.expects(:clear) @puppet_type.expects(:create).with(:name => service, :hasstatus => false, :hasrestart => true).returns(@puppet_service) @puppet_provider.expects(:status).returns(0) result = @agent.call("status", :service => service) result.should be_successful result.should have_data_items({"status" => "0"}) end it "should fail on exception raised" do @agent.expects(:get_puppet).raises("Exception") result = @agent.call("status", :service =>"service") result.should be_aborted_error end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/service/spec/service_application_spec.rb000077500000000000000000000106631174600156000314340ustar00rootroot00000000000000#!/usr/bin/env rspec require 'spec_helper' module MCollective describe "service application" do before do application_file = File.join([File.dirname(__FILE__), "../application/service.rb"]) @util = MCollective::Test::ApplicationTest.new("service", :application_file => application_file) @app = @util.plugin end describe "#application_description" do it "should have a description set" do @app.should have_a_description end end describe "#print_statistics" do it "should not print statistics if status_counter is 0" do @app.stubs(:print) @app.expects(:print).with("No responses received") @app.print_statistics({:responses => 1, :noresponsefrom => []}, {}) end it "should print statistics if status_counter is > 0" do @app.stubs(:print) @app.expects(:print).with("started=1 ") @app.expects(:print).with("stopped=0 ") @app.expects(:print).with("errors=0 ") @app.print_statistics({:responses => 1, :noresponsefrom => []}, {"running" => 1, "stopped" => 0, "error" => 0}) end end describe "#post_option_parser" do it "should raise an exception if service name and action are not specified" do expect{ @app.post_option_parser({}) }.to raise_error("Please specify service name and action") end it "should raise and exception if action name isn't stop, start, restart or status" do ARGV << "service" ARGV << "action" expect{ @app.post_option_parser({}) }.to raise_error("Action can only be start, stop, restart or status") end it "should set service and action" do ARGV << "service" ARGV << "start" configuration = {:service => "", :action => ""} @app.post_option_parser(configuration) configuration[:service].should == "service" configuration[:action].should == "start" end end describe "#validate_configuration" do it "should exit if filter is empty and user response is not y or yes" do MCollective::Util.expects(:empty_filter?).returns(true) @app.expects(:options).returns(:filter => nil) @app.stubs(:print) STDOUT.expects(:flush) STDIN.expects(:gets).returns("n") @app.expects(:exit!) @app.validate_configuration({}) end it "should return if filter is empty and user response is y or yes" do MCollective::Util.expects(:empty_filter?).returns(true) @app.expects(:options).returns(:filter => nil) @app.stubs(:print) STDOUT.expects(:flush) STDIN.expects(:gets).returns("y") @app.validate_configuration({}) end it "should return if filter is not empty" do MCollective::Util.expects(:empty_filter?).returns(false) @app.expects(:options).returns(:filter => nil) @app.validate_configuration({}) end end describe "#main" do before do @rpcclient_mock = mock end it "should run a service command and list sender and status if verbose is false" do @app.stubs(:configuration).returns({:action => "start", :service => "service"}) @app.expects(:rpcclient).with("service", :options => nil).returns(@rpcclient_mock) @rpcclient_mock.expects(:send).with("start", :service => "service").returns([{:sender => "node1", :data => {"status" => "running"}, :statuscode => 0, :statusmsg => "success"}]) @rpcclient_mock.expects(:progress).returns(true) @rpcclient_mock.expects(:verbose).returns(false) @rpcclient_mock.expects(:disconnect) @rpcclient_mock.expects(:stats) @app.expects(:print_statistics) @app.main end it "should run a service command and display verbosely if verbose is true" do @app.stubs(:configuration).returns({:action => "start", :service => "service"}) @app.expects(:rpcclient).with("service", :options => nil).returns(@rpcclient_mock) @rpcclient_mock.expects(:send).with("start", :service => "service").returns([{:sender => "node1", :data => {"status" => "running"}, :statuscode => 0, :statusmsg => "success"}]) @rpcclient_mock.expects(:progress).returns(true) @rpcclient_mock.expects(:verbose).returns(true) @rpcclient_mock.expects(:disconnect) @rpcclient_mock.expects(:stats) @app.expects(:print_statistics) @app.main end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/000077500000000000000000000000001174600156000235075ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/agent/000077500000000000000000000000001174600156000246055ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/agent/stomputil.ddl000066400000000000000000000027731174600156000273430ustar00rootroot00000000000000metadata :name => "STOMP Connector Utility Agent", :description => "Various helpers and useful actions for the STOMP connector", :author => "R.I.Pienaar ", :license => "Apache v 2.0", :version => "1.0", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 12 action "collective_info", :description => "Info about the main and sub collectives" do display :always output :main_collective, :description => "The main collective", :display_as => "Main Collective" output :collectives, :description => "The sub collectives", :display_as => "Sub Collectives" end action "peer_info", :description => "Get STOMP Connection Peer" do display :always output :protocol, :description => "IP Protocol in use", :display_as => "Protocol" output :destport, :description => "Destination Port", :display_as => "Port" output :desthost, :description => "Destination Host", :display_as => "Host" output :destaddr, :description => "Destination Address", :display_as => "Address" end action "reconnect", :description => "Re-creates the connection to the STOMP network" do display :always output :restarted, :description => "Did the restart complete succesfully?", :display_as => "Restarted" end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/agent/stomputil.rb000066400000000000000000000026601174600156000271760ustar00rootroot00000000000000module MCollective module Agent class Stomputil "STOMP Connector Utility Agent", :description => "Various helpers and useful actions for the STOMP connector", :author => "R.I.Pienaar ", :license => "Apache v 2.0", :version => "1.0", :url => "http://projects.puppetlabs.com/projects/mcollective-plugins/wiki", :timeout => 10 # Get the Stomp connection peer information action "peer_info" do peer = PluginManager["connector_plugin"].connection.socket.peeraddr reply[:protocol] = peer[0] reply[:destport] = peer[1] reply[:desthost] = peer[2] reply[:destaddr] = peer[3] end action "collective_info" do config = Config.instance reply[:main_collective] = config.main_collective reply[:collectives] = config.collectives end action "reconnect" do PluginManager["connector_plugin"].disconnect sleep 0.5 PluginManager["connector_plugin"].connect ::Process.kill("USR1", $$) logger.info("Reconnected to middleware and reloaded all agents") reply[:restarted] = 1 end private def get_pid(process) pid = `pidof #{process}`.chomp.grep(/\d+/) pid.first end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/sbin/000077500000000000000000000000001174600156000244425ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/sbin/mc-collectivemap000066400000000000000000000017011174600156000276100ustar00rootroot00000000000000#!/usr/bin/ruby # Given a MCollective 1.1.3 or newer collective # this will print as a dot graph a map of the collective # and sub collectives require 'mcollective' include MCollective::RPC def getcollectives(client) collectives = {} client.collective_info do |resp| data = resp[:body][:data] if data.include?(:collectives) data[:collectives].each do |c| collectives[c] = [] unless collectives.include?(c) collectives[c] << resp[:senderid] end end end collectives end shelper = rpcclient("stomputil") shelper.progress = false collectives = getcollectives(shelper) puts "graph {" collectives.keys.sort.each do |collective| puts "\tsubgraph #{collective} {" collectives[collective].each do |member| member_name = member.gsub('.', '_').gsub('-', '_') puts "\t\t'#{member}' -- #{collective};" end puts "\t}" end puts "}" mcollective-plugins-0.0.0~git20120507.df2fa81/agent/stomputil/sbin/mc-peermap000066400000000000000000000037241174600156000264210ustar00rootroot00000000000000#!/usr/bin/ruby # Produce a simple textual map of a STOMP based # mcollective network # # If you give a fact name as argument each STOMP # server will display this fact next to some stats require 'mcollective' include MCollective::RPC if ARGV.size > 0 fact = ARGV[0] else fact = nil end # gets all the peers from the network def getpeers(client) peers = {} peer_resp_times = {} starttime = nil client.peer_info do |resp| starttime = Time.now.to_f unless starttime data = resp[:body][:data] desthost = data[:desthost] peers[desthost] ||= {:hosts => [], :pings => []} peers[desthost][:hosts] << resp[:senderid] peers[desthost][:pings] << Time.now.to_f - starttime end peers end # Shows all the nodes connected to a specific stomp server def showserver(peers, server, fact) rpc = rpcclient("rpcutil") rpc.progress = false facts = rpc.custom_request("get_fact", {:fact => fact}, server, {"identity" => server}) if fact nodecount = peers[server][:hosts].size avgping = peers[server][:pings].inject{ |sum, el| sum + el } / peers[server][:pings].size * 1000 if fact puts "%s -+ %i nodes with %.2fms average ping [%s] " % [server, nodecount, avgping, facts.first[:data][:value] ] else puts "%s -+ %i nodes with %.2fms average ping" % [server, nodecount, avgping ] end peers[server][:hosts].each do |s| print " " * server.length puts " |-#{s}" end end # Walks all the servers and shows each def shownetwork(peers, server=nil, fact=nil) if server if peers.include?(server) showserver(peers, server, fact) else puts "The network doesn't include #{server}" end else peers.keys.sort.each do |peer| showserver(peers, peer, fact) puts end end end shelper = rpcclient("stomputil") shelper.progress = false shownetwork(getpeers(shelper), nil, fact) mcollective-plugins-0.0.0~git20120507.df2fa81/audit/000077500000000000000000000000001174600156000214575ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/000077500000000000000000000000001174600156000243165ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/agent/000077500000000000000000000000001174600156000254145ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/agent/centralrpclog-mongodb.rb000066400000000000000000000032131174600156000322220ustar00rootroot00000000000000module MCollective module Agent # An agent that receives and logs RPC Audit messages sent from the accompanying Audit plugin # # It stores them in MongoDB, you can configure the mongo parameters: # # plugin.centralrpclog.mongohost = localhost # plugin.centralrpclog.mongodb = mcollective # plugin.centralrpclog.collection = rpclog # # These are the defaults. You need the mongo gem installed. class Centralrpclog attr_reader :timeout, :meta def initialize @timeout = 1 @config = Config.instance @meta = {:license => "Apache 2", :author => "R.I.Pienaar ", :url => "https://github.com/puppetlabs/mcollective-plugins"} require 'mongo' @mongohost = @config.pluginconf["centralrpclog.mongohost"] || "localhost" @mongodb = @config.pluginconf["centralrpclog.mongodb"] || "mcollective" @collection = @config.pluginconf["centralrpclog.collection"] || "rpclog" Log.instance.debug("Connecting to mongodb @ #{@mongohost} db #{@mongodb} collection #{@collection}") @dbh = Mongo::Connection.new(@mongohost).db(@mongodb) @coll = @dbh.collection(@collection) end def handlemsg(msg, connection) request = msg[:body] log = request.to_hash.merge({:senderid => msg[:senderid], :requestid => request.uniqid, :caller => "#{request.caller}@#{request.sender}", :time => Time.now.to_i}) @coll.save(log) # never reply nil end def help <<-EOH EOH end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/agent/centralrpclog.rb000066400000000000000000000027511174600156000306050ustar00rootroot00000000000000module MCollective module Agent # An agent that receives and logs RPC Audit messages sent from the accompanying Audit plugin class Centralrpclog attr_reader :timeout, :meta def initialize @timeout = 1 @config = Config.instance @meta = {:license => "Apache 2", :author => "R.I.Pienaar ", :url => "https://github.com/puppetlabs/mcollective-plugins"} end def handlemsg(msg, connection) request = msg[:body] require 'pp' logfile = Config.instance.pluginconf["centralrpclog.logfile"] || "/var/log/mcollective-rpcaudit.log" File.open(logfile, "a") do |f| f.puts("#{Time.new.strftime("%D %T")} #{msg[:senderid]}> #{request.uniqid}: #{Time.at(request.time).strftime("%D %T")} caller=#{request.caller}@#{request.sender} agent=#{request.agent} action=#{request.action} #{request.data.pretty_print_inspect}") end # never reply nil end def help <<-EOH RPC Central Audit Agent ======================= An agent that receives audit requests from an SimpleRPC Audit plugin and logs locally using this you can build a central audit of all SimpleRPC requests. Provide it with a config option: plugin.centralrpclog.logfile = /var/log/simplerpc-audit.log EOH end end end end # vi:tabstop=2:expandtab:ai:filetype=ruby mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/audit/000077500000000000000000000000001174600156000254245ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/audit/centralrpclog/audit/centralrpclog.rb000066400000000000000000000026551174600156000306200ustar00rootroot00000000000000module MCollective module RPC # A RPC::Audit plugin that sends all audit messages to a non SimpleRPC agent called # centralrpclog where it can then process them however it feels like # # https://github.com/puppetlabs/mcollective-plugins class Centralrpclog "centralrpclog"} req = PluginManager["security_plugin"].encoderequest(config.identity, target, request, reqid, filter) if connection.respond_to?(:publish) connection.publish(target, req) else connection.send(target, req) end rescue Exception => e Log.instance.error("Failed to send audit request: #{e}") end end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/audit/logstash/000077500000000000000000000000001174600156000233035ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/audit/logstash/logstash.rb000066400000000000000000000026611174600156000254610ustar00rootroot00000000000000module MCollective module RPC # An audit plugin that just logs to a logstash queue # # You can configure which queue it emits events to with the setting # # plugin.logstash.target # class Logstash Config.instance.identity, "@tags" => [], "@type" => "mcollective-audit", "@source" => "mcollective-audit", "@timestamp" => now_iso8601, "@fields" => {"uniqid" => request.uniqid, "request_time" => request.time, "caller" => request.caller, "callerhost" => request.sender, "agent" => request.agent, "action" => request.action, "data" => request.data.pretty_print_inspect}, "@message" => "#{Config.instance.identity}: #{request.caller}@#{request.sender} invoked agent #{request.agent}##{request.action}"} target = Config.instance.pluginconf["logstash.target"] || "/queue/mcollective.audit" if connection.respond_to?(:publish) connection.publish(target, req) else connection.send(target, req) end end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/facts/000077500000000000000000000000001174600156000214515ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/facts/facter/000077500000000000000000000000001174600156000227155ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/facts/facter/facter.rb000066400000000000000000000045061174600156000245130ustar00rootroot00000000000000module MCollective module Facts require 'facter' # A factsource for Reductive Labs Facter # # This plugin by default works with puppet facts loaded via pluginsync # and the deprecated factsync. If your facts are in a custom location, or # you use non-standard puppet dirs, then set plugin.facter.facterlib # in the server.cfg # # It caches facts for 300 seconds to speed things up a bit, # generally though using this plugin will slow down discovery by # a second or so. # - the cache time can be altered by setting plugin.facter.cache_time in the server.cfg # # See: http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/FactsFacter # # Plugin released under the terms of the GPL. class Facter cache_time.to_i ) logger.debug("Resetting facter cache after #{cache_time} seconds") reload_facts end rescue Exception => e logger.error("Failed to load facts: #{e}") @@last_facts_load = Time.now.to_i end return @@facts end end private def reload_facts ::Facter.reset @@facts = {} facts = ::Facter.to_hash Log.instance.info("Loaded #{facts.keys.size} facts from Facter") facts.each_pair do |key,value| @@facts[key] = value.to_s end # Somehow, sometimes, we get empty fact hashes? if @@facts.empty? Log.instance.error("Got empty facts, resetting to last known good") @@facts = @last_good_facts.clone else @@last_good_facts = @@facts.clone end @@last_facts_load = Time.now.to_i end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/facts/facter/facter_facts.rb000066400000000000000000000020501174600156000256630ustar00rootroot00000000000000module MCollective module Facts require 'facter' # A factsource for Puppet Labs Facter # # This plugin by default works with puppet facts loaded via pluginsync # and the deprecated factsync. If your facts are in a custom location, or # you use non-standard puppet dirs, then set plugin.facter.facterlib # in the server.cfg # # See: http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/FactsFacter # # NOTE: This version of this plugin requires mcollective 1.1.0 or newer # # Plugin released under the terms of the GPL. class Facter_facts 3000) Log.instance.debug("Reloading facts from Ohai") oh = Ohai::System.new oh.all_plugins @@facts = {} oh.data.each_pair do |key, val| ohai_flatten(key,val, [], @@facts) end @@last_facts_load = Time.now.to_i end rescue @@last_facts_load = Time.now.to_i end @@facts end private # Flattens the Ohai structure into something like: # # "languages.java.version"=>"1.6.0" def ohai_flatten(key, val, keys, result) keys << key if val.is_a?(Mash) val.each_pair do |nkey, nval| ohai_flatten(nkey, nval, keys, result) keys.delete_at(keys.size - 1) end else key = keys.join(".") if val.is_a?(Array) result[key] = val.join(", ") else result[key] = val end end end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/facts/ohai/opscodeohai_facts.rb000066400000000000000000000024761174600156000264040ustar00rootroot00000000000000require 'ohai' module MCollective module Facts # A factsource for OpsCode Chef # # Generally using this plugin will slow down discovery by a couple of seconds # # See: http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/FactsOhai # # NOTE: This version of this plugin requires mcollective 1.1.0 or newer # # Plugin released under the terms of the Apache Licence v 2. class Opscodeohai_facts"1.6.0" def ohai_flatten(key, val, keys, result) keys << key if val.is_a?(Mash) val.each_pair do |nkey, nval| ohai_flatten(nkey, nval, keys, result) keys.delete_at(keys.size - 1) end else key = keys.join(".") if val.is_a?(Array) result[key] = val.join(", ") else result[key] = val end end end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/registration/000077500000000000000000000000001174600156000230635ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/registration/meta.rb000066400000000000000000000015571174600156000243460ustar00rootroot00000000000000module MCollective module Registration # A registration plugin that sends in all the metadata we have for a node, # including: # # - all facts # - all agents # - all classes (if applicable) # # will add cf classes soon # # http://projects.puppetlabs.com/projects/mcollective-plugins/wiki/RegistrationMetaData # Author: R.I.Pienaar # Licence: Apache 2 class Meta [], :facts => {}, :classes => []} cfile = Config.instance.classesfile if File.exist?(cfile) result[:classes] = File.readlines(cfile).map {|i| i.chomp} end result[:agentlist] = Agents.agentlist result[:facts] = PluginManager["facts_plugin"].get_facts result end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/security/000077500000000000000000000000001174600156000222205ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/security/none/000077500000000000000000000000001174600156000231575ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/security/none/none.rb000066400000000000000000000024611174600156000244460ustar00rootroot00000000000000module MCollective module Security # A YAML based security plugin that doesnt actually secure anything # it just does the needed serialization. # # You should *NOT* use this for every day running, this exist mostly # to make development and testing a bit less painful class None < Base require 'etc' require 'yaml' # Decodes a message by unserializing all the bits etc, it also validates # it as valid using the psk etc def decodemsg(msg) YAML.load(msg.payload) end # Encodes a reply def encodereply(sender, target, msg, requestid, requestcallerid=nil) YAML.dump(create_reply(requestid, sender, target, msg)) end # Encodes a request msg def encoderequest(sender, target, msg, requestid, filter, target_agent, target_collective) request = create_request(requestid, target, filter, msg, @initiated_by, target_agent, target_collective) YAML.dump(request) end # Checks the md5 hash in the request body against our psk, the request sent for validation # should not have been deserialized already def validrequest?(req) @stats.validated return true end def callerid "user=#{Etc.getlogin}" end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/security/sshkey/000077500000000000000000000000001174600156000235265ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/security/sshkey/sshkey.rb000066400000000000000000000124371174600156000253700ustar00rootroot00000000000000require "rubygems" gem "sshkeyauth", ">= 0.0.4" require "ssh/key/signer" require "ssh/key/verifier" require "etc" module MCollective module Security # A security plugin for MCollective that uses ssh keys for message # signing and verification # # For clients (things issuing RPC calls): # * Message signing will use your ssh-agent. # * Message verification is done using the public key of the host that # sent the message. This means your system needs to be aware of the public # key before you can verify a message. Generally, this involves your # ~/.ssh/known_hosts file, but we invoke 'ssh-keygen -F ', where # HOST is read from from the SimpleRPC senderid (defaults to the # hostname). # # Clients identify themselves in their RPC 'callerid' as your current # user (via Etc::getlogin). # # For nodes/agents: # * Message signing uses the value of 'plugin.sshkey' in server.cfg. # We recommend you use the path of your host's SSH RSA key. (For example: # '/etc/ssh/ssh_host_rsa_key'.) # * Message verification uses your user's authorized_keys file. The # 'user' comes from the RPC 'callerid' field. Said user must exist on # this node. # # In cases of configurable paths, like the location of your authorized_keys # file, the 'sshkeyauth' library will try to parse it from the # sshd_config file (defaults to '/etc/ssh/sshd_config'). # # Since there is no challenge-reponse in MCollective RPC, we can't emulate # ssh's "try each key until one is accepted" method. Instead, we will # sign each method with *all* keys in your agent and the receiver will # try to verify against any of them. # # Serialization uses Marshal. # # NOTE: This plugin should be considered experimental at this point, as it # has a few gotchas and drawbacks. # # * Nodes cannot easily send messages now; this means registration is # not supported. # * Automated systems that wish to manage a collective with this plugin # will somehow need access to ssh agents; this can be insecure and # problematic in general. # # We're including this plugin as an early preview of what is being worked on # in order to solicit feedback. # # Configuration: # # For clients: # securityprovider = sshkey # # For nodes: # securityprovider = sshkey # plugin.sshkey = /etc/ssh/ssh_host_rsa_key class Sshkey < Base # Decodes a message by unserializing all the bits etc # TODO(sissel): refactor this into Base? def decodemsg(msg) body = Marshal.load(msg.payload) if validrequest?(body) body[:body] = Marshal.load(body[:body]) return body else nil end end # Encodes a reply def encodereply(sender, target, msg, requestid, requestcallerid=nil) serialized = Marshal.dump(msg) digest = makehash(serialized) req = create_reply(requestid, sender, target, serialized) req[:hash] = digest Marshal.dump(req) end # Encodes a request msg def encoderequest(sender, target, msg, requestid, filter={}, target_agent=nil, target_collective=nil) serialized = Marshal.dump(msg) digest = makehash(serialized) req = create_request(requestid, target, filter, serialized, @initiated_by, target_agent, target_collective) req[:hash] = digest Marshal.dump(req) end def callerid return Etc.getlogin end # Checks the md5 hash in the request body against our psk, the # request sent for validation # should not have been deserialized already def validrequest?(req) Log.info "Caller id: #{req[:callerid]}" Log.info "Sender id: #{req[:senderid]}" message = req[:body] #@log.info req.awesome_inspect identity = (req[:callerid] or req[:senderid]) verifier = SSH::Key::Verifier.new(identity) Log.info "Using name '#{identity}'" # If no callerid, this is a 'response' message and we should # attempt to authenticate using the senderid (hostname, usually) # and that ssh key in known_hosts. if !req[:callerid] # Search known_hosts for the senderid hostname verifier.add_key_from_host(identity) verifier.use_agent = false verifier.use_authorized_keys = false end signatures = Marshal.load(req[:hash]) if verifier.verify?(signatures, req[:body]) @stats.validated return true else @stats.unvalidated raise(SecurityValidationFailed, "Received an invalid signature in message") end end private # Signs a message. If 'public.sshkey' is set, then we will sign # with only that key. Otherwise, we will sign with your ssh-agente. def makehash(body) signer = SSH::Key::Signer.new if @config.pluginconf["sshkey"] signer.add_key_file(@config.pluginconf["sshkey"]) signer.use_agent = false end signatures = signer.sign(body).collect { |s| s.signature } return Marshal.dump(signatures) end end end end # vi:tabstop=2:expandtab:ai mcollective-plugins-0.0.0~git20120507.df2fa81/simplerpc_authorization/000077500000000000000000000000001174600156000253275ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/simplerpc_authorization/action_policy/000077500000000000000000000000001174600156000301635ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/simplerpc_authorization/action_policy/actionpolicy.rb000066400000000000000000000130711174600156000332070ustar00rootroot00000000000000module MCollective module Util # A class to do SimpleRPC authorization checks using a per-agent # policy file. Policy files can allow or deny requests based on # facts and classes on the node and the unix user id of the caller. # # A policy file gets stored in /etc/mcollective/policies/.policy # # Sample: # # # /etc/mcollective/policies/service.policy # policy default deny # allow uid=500 status enable disable country=uk apache # allow uid=0 * * * # # This will deny almost all service agent requests, but allows caller # userid 500 to invoke the 'status,' 'enable,' and 'disable' actions on # nodes which have the 'country=uk' fact and the 'apache' class. # Unix UID 0 will be able to do all actions regardless of facts and classes. # # Policy files can be commented with lines beginning with #, and blank lines # are ignored. Fields in each policy line should be tab-separated. # You can specify multiple facts, actions, and classes as space-separated # lists. # # If no policy for an agent is found, this plugin will disallow requests by # default. You can set plugin.actionpolicy.allow_unconfigured = 1 to # allow these requests, but this is not recommended. # # Released under the Apache v2 License - R.I.Pienaar class ActionPolicy def self.authorize(request) config = Config.instance if config.pluginconf.include?("actionpolicy.allow_unconfigured") if config.pluginconf["actionpolicy.allow_unconfigured"] =~ /^1|y/i policy_allow = true else policy_allow = false end else policy_allow = false end logger = Log.instance configdir = config.configdir policyfile = "#{configdir}/policies/#{request.agent}.policy" logger.debug("Looking for policy in #{policyfile}") # if a policy file with the same name doesn't exist, check if we've enabled # default policies. if so change policyfile to default and check again after unless File.exist?(policyfile) if config.pluginconf.include?("actionpolicy.enable_default") if config.pluginconf["actionpolicy.enable_default"] =~ /^1|y/i # did user set a custom default policyfile name? if config.pluginconf.include?("actionpolicy.default_name") defaultname = config.pluginconf["actionpolicy.default_name"] else defaultname = "default" end policyfile = "#{configdir}/policies/#{defaultname}.policy" logger.debug("Initial lookup failed; looking for policy in #{policyfile}") end end end if File.exist?(policyfile) File.open(policyfile).each do |line| next if line =~ /^#/ next if line =~ /^$/ if line =~ /^policy\sdefault\s(\w+)/ $1 == "allow" ? policy_allow = true : policy_allow = false elsif line =~ /^(allow|deny)\t+(.+?)\t+(.+?)\t+(.+?)\t+(.+?)$/ policyresult = check_policy($1, $2, $3, $4, $5, request) # deny or allow the rpc request based on the policy check if policyresult == true if $1 == "allow" return true else deny("Denying based on explicit 'deny' policy rule") end end else logger.debug("Cannot parse policy line: #{line}") end end end # If we get here then none of the policy lines matched so # we should just do whatever the default policy states if policy_allow == true return true else deny("Denying based on default policy") end end private def self.check_policy(auth, rpccaller, actions, facts, classes, request) # If we have a wildcard caller or the caller matches our policy line # then continue else skip this policy line if (rpccaller != "*") && (rpccaller != request.caller) return false end # If we have a wildcard actions list or the request action is in the list # of actions in the policy line continue, else skip this policy line if (actions != "*") && (actions.split.grep(request.action).size == 0) return false end # Facts and Classes that do not match what we have indicates # that we should skip checking this auth line. Both support # a wildcard match unless facts == "*" facts.split.each do |fact| if fact =~ /(.+)=(.+)/ return false unless Util.get_fact($1) == $2 end end end unless classes == "*" classes.split.each do |klass| return false unless Util.has_cf_class?(klass) end end # If we get here all the facts, classes, caller and actions match # our request. We should now allow or deny it based on the auth # in the policy line if auth == "allow" return true else deny("Denying based on policy") if auth == "deny" end end # Logs why we are not authorizing a request then raise an appropriate # exception to block the action def self.deny(logline) Log.instance.debug(logline) raise RPCAborted, "You are not authorized to call this agent or action." end end end end mcollective-plugins-0.0.0~git20120507.df2fa81/simplerpc_authorization/action_policy/puppetd.policy000066400000000000000000000001121174600156000330570ustar00rootroot00000000000000policy default deny allow uid=500 status enable disable country=uk apache mcollective-plugins-0.0.0~git20120507.df2fa81/spec/000077500000000000000000000000001174600156000213035ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/spec/spec.opts000066400000000000000000000000401174600156000231360ustar00rootroot00000000000000--format s --colour --backtrace mcollective-plugins-0.0.0~git20120507.df2fa81/spec/spec_helper.rb000066400000000000000000000005551174600156000241260ustar00rootroot00000000000000$: << File.join([File.dirname(__FILE__), "lib"]) require 'rubygems' require 'rspec' require 'mcollective' require 'mcollective/test' require 'rspec/mocks' require 'mocha' require 'tempfile' RSpec.configure do |config| config.mock_with :mocha config.include(MCollective::Test::Matchers) config.before :each do MCollective::PluginManager.clear end end mcollective-plugins-0.0.0~git20120507.df2fa81/utilities/000077500000000000000000000000001174600156000223645ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/utilities/mc-ssh/000077500000000000000000000000001174600156000235565ustar00rootroot00000000000000mcollective-plugins-0.0.0~git20120507.df2fa81/utilities/mc-ssh/mc-ssh000066400000000000000000000032631174600156000246770ustar00rootroot00000000000000#!/usr/bin/env ruby # Frontend to ssh that uses mcollective discovery to find hosts. # # It requires the rdialog gem to be available, basic usage is: # # mc-ssh --with-class /webserver/ -- -l root # # This will present you with a list of hosts and run: # # ssh -l root # # on your chosen host. # # Released under Apache Licence 2, R.I.Pienaar # # https://github.com/puppetlabs/mcollective-plugins require 'mcollective' require 'rdialog' include MCollective::RPC options = rpcoptions do |parser, options| parser.define_head "MCollective discovery enabled ssh" parser.separator "" parser.separator "Usage: mc-ssh [filters and options] -- [ssh options]" end if ARGV.length >= 1 sshoptions = ARGV.join(" ") end client = rpcclient("discovery", :options => options) @dialog = RDialog.new @dialog.backtitle = "Marionette Collective SSH" class CancelPressed e puts("Failed to run mc-ssh: #{e}") exit! end mcollective-plugins-0.0.0~git20120507.df2fa81/utilities/mc-ssh/mc-ssh-highline000066400000000000000000000031041174600156000264560ustar00rootroot00000000000000#!/usr/bin/ruby # Frontend to ssh that uses mcollective discovery to find hosts. # # It requires the highline gem to be available, basic usage is: # # mc-ssh --with-class /webserver/ -- -l root # # This will present you with a list of hosts and run: # # ssh -l root # # on your chosen host. # # Released under Apache Licence 2, R.I.Pienaar # # https://github.com/puppetlabs/mcollective-plugins require 'mcollective' require 'highline/import' HighLine.track_eof = false oparser = MCollective::Optionparser.new({}, "filter") options = oparser.parse{|parser, options| parser.define_head "MCollective iscovery enabled ssh" parser.separator "" parser.separator "Usage: mc-ssh [filters and options] -- [ssh options]" } if ARGV.length >= 1 sshoptions = ARGV.join(" ") end client = MCollective::Client.new(options[:config]) client.options = options # Shows a list of options and lets the user choose one def pick(choices, title) return choices[0][1] if choices.size == 1 choose do |menu| menu.prompt = title choices.each do |choice| menu.choice choice[1] end menu.choice "Exit" do exit! end end end choices = [] client.discover(options[:filter], options[:disctimeout]).each_with_index do |host,i| choices << [i, host] end begin hostname = pick(choices, "").to_i hostname = choices[hostname][1] puts("Running: ssh #{hostname} #{sshoptions}") exec("ssh #{hostname} #{sshoptions}") rescue Exception => e puts("Failed to run mc-ssh: #{e}") puts e.backtrace exit! end