mcollective-client 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/mc-call-agent +54 -0
- data/bin/mco +27 -0
- data/lib/mcollective.rb +70 -0
- data/lib/mcollective/agents.rb +160 -0
- data/lib/mcollective/application.rb +354 -0
- data/lib/mcollective/applications.rb +145 -0
- data/lib/mcollective/client.rb +292 -0
- data/lib/mcollective/config.rb +202 -0
- data/lib/mcollective/connector.rb +18 -0
- data/lib/mcollective/connector/base.rb +24 -0
- data/lib/mcollective/facts.rb +39 -0
- data/lib/mcollective/facts/base.rb +86 -0
- data/lib/mcollective/log.rb +103 -0
- data/lib/mcollective/logger.rb +5 -0
- data/lib/mcollective/logger/base.rb +73 -0
- data/lib/mcollective/logger/console_logger.rb +61 -0
- data/lib/mcollective/logger/file_logger.rb +46 -0
- data/lib/mcollective/logger/syslog_logger.rb +53 -0
- data/lib/mcollective/matcher.rb +16 -0
- data/lib/mcollective/matcher/parser.rb +93 -0
- data/lib/mcollective/matcher/scanner.rb +123 -0
- data/lib/mcollective/message.rb +201 -0
- data/lib/mcollective/monkey_patches.rb +104 -0
- data/lib/mcollective/optionparser.rb +164 -0
- data/lib/mcollective/pluginmanager.rb +180 -0
- data/lib/mcollective/pluginpackager.rb +26 -0
- data/lib/mcollective/pluginpackager/agent_definition.rb +79 -0
- data/lib/mcollective/pluginpackager/standard_definition.rb +59 -0
- data/lib/mcollective/registration.rb +16 -0
- data/lib/mcollective/registration/base.rb +75 -0
- data/lib/mcollective/rpc.rb +188 -0
- data/lib/mcollective/rpc/actionrunner.rb +142 -0
- data/lib/mcollective/rpc/agent.rb +441 -0
- data/lib/mcollective/rpc/audit.rb +38 -0
- data/lib/mcollective/rpc/client.rb +793 -0
- data/lib/mcollective/rpc/ddl.rb +258 -0
- data/lib/mcollective/rpc/helpers.rb +339 -0
- data/lib/mcollective/rpc/progress.rb +63 -0
- data/lib/mcollective/rpc/reply.rb +61 -0
- data/lib/mcollective/rpc/request.rb +51 -0
- data/lib/mcollective/rpc/result.rb +41 -0
- data/lib/mcollective/rpc/stats.rb +185 -0
- data/lib/mcollective/runnerstats.rb +90 -0
- data/lib/mcollective/security.rb +26 -0
- data/lib/mcollective/security/base.rb +237 -0
- data/lib/mcollective/shell.rb +87 -0
- data/lib/mcollective/ssl.rb +246 -0
- data/lib/mcollective/unix_daemon.rb +37 -0
- data/lib/mcollective/util.rb +274 -0
- data/lib/mcollective/vendor.rb +41 -0
- data/lib/mcollective/vendor/require_vendored.rb +2 -0
- data/lib/mcollective/windows_daemon.rb +25 -0
- data/spec/Rakefile +16 -0
- data/spec/fixtures/application/test.rb +7 -0
- data/spec/fixtures/test-cert.pem +15 -0
- data/spec/fixtures/test-private.pem +15 -0
- data/spec/fixtures/test-public.pem +6 -0
- data/spec/monkey_patches/instance_variable_defined.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/unit/agents_spec.rb +280 -0
- data/spec/unit/application_spec.rb +636 -0
- data/spec/unit/applications_spec.rb +155 -0
- data/spec/unit/array.rb +30 -0
- data/spec/unit/config_spec.rb +148 -0
- data/spec/unit/facts/base_spec.rb +118 -0
- data/spec/unit/facts_spec.rb +39 -0
- data/spec/unit/log_spec.rb +71 -0
- data/spec/unit/logger/base_spec.rb +110 -0
- data/spec/unit/logger/syslog_logger_spec.rb +86 -0
- data/spec/unit/matcher/parser_spec.rb +106 -0
- data/spec/unit/matcher/scanner_spec.rb +71 -0
- data/spec/unit/message_spec.rb +401 -0
- data/spec/unit/optionparser_spec.rb +113 -0
- data/spec/unit/pluginmanager_spec.rb +173 -0
- data/spec/unit/pluginpackager/agent_definition_spec.rb +130 -0
- data/spec/unit/pluginpackager/standard_definition_spec.rb +75 -0
- data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +533 -0
- data/spec/unit/plugins/mcollective/connector/stomp/eventlogger_spec.rb +34 -0
- data/spec/unit/plugins/mcollective/connector/stomp_spec.rb +417 -0
- data/spec/unit/plugins/mcollective/packagers/ospackage_spec.rb +229 -0
- data/spec/unit/plugins/mcollective/security/psk_spec.rb +156 -0
- data/spec/unit/registration/base_spec.rb +77 -0
- data/spec/unit/rpc/actionrunner_spec.rb +213 -0
- data/spec/unit/rpc/agent_spec.rb +155 -0
- data/spec/unit/rpc/client_spec.rb +523 -0
- data/spec/unit/rpc/ddl_spec.rb +388 -0
- data/spec/unit/rpc/helpers_spec.rb +55 -0
- data/spec/unit/rpc/reply_spec.rb +143 -0
- data/spec/unit/rpc/request_spec.rb +115 -0
- data/spec/unit/rpc/result_spec.rb +66 -0
- data/spec/unit/rpc/stats_spec.rb +288 -0
- data/spec/unit/runnerstats_spec.rb +40 -0
- data/spec/unit/security/base_spec.rb +279 -0
- data/spec/unit/shell_spec.rb +144 -0
- data/spec/unit/ssl_spec.rb +244 -0
- data/spec/unit/symbol.rb +11 -0
- data/spec/unit/unix_daemon.rb +41 -0
- data/spec/unit/util_spec.rb +342 -0
- data/spec/unit/vendor_spec.rb +34 -0
- data/spec/unit/windows_daemon.rb +43 -0
- data/spec/windows_spec.opts +1 -0
- metadata +242 -0
data/bin/mc-call-agent
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'mcollective'
|
|
4
|
+
require 'pp'
|
|
5
|
+
|
|
6
|
+
oparser = MCollective::Optionparser.new({:verbose => true}, "filter")
|
|
7
|
+
|
|
8
|
+
options = oparser.parse{|parser, options|
|
|
9
|
+
parser.define_head "Call an agent parsing an argument to it"
|
|
10
|
+
parser.banner = "Usage: mc-call-agent [options] --agent agent --argument arg"
|
|
11
|
+
|
|
12
|
+
parser.on('-a', '--agent AGENT', 'Agent to call') do |v|
|
|
13
|
+
options[:agent] = v
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
parser.on('--arg', '--argument ARGUMENT', 'Argument to pass to agent') do |v|
|
|
17
|
+
options[:argument] = v
|
|
18
|
+
end
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if options[:agent] == nil || options[:argument] == nil
|
|
22
|
+
puts("Please use either --agent or --argument")
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
options[:filter]["agent"] << options[:agent]
|
|
28
|
+
|
|
29
|
+
client = MCollective::Client.new(options[:config])
|
|
30
|
+
client.options = options
|
|
31
|
+
|
|
32
|
+
c = 0
|
|
33
|
+
|
|
34
|
+
stats = client.discovered_req(options[:argument], options[:agent]) do |resp|
|
|
35
|
+
next if resp == nil
|
|
36
|
+
|
|
37
|
+
c += 1
|
|
38
|
+
|
|
39
|
+
if options[:verbose]
|
|
40
|
+
puts("#{resp[:senderid]}>")
|
|
41
|
+
pp resp[:body]
|
|
42
|
+
else
|
|
43
|
+
puts if c % 4 == 1
|
|
44
|
+
printf("%-30s", resp[:senderid])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
client.disconnect
|
|
49
|
+
rescue Exception => e
|
|
50
|
+
STDERR.puts "Could not call remote agent: #{e}"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
client.display_stats(stats)
|
data/bin/mco
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'mcollective'
|
|
4
|
+
|
|
5
|
+
Version = MCollective.version
|
|
6
|
+
known_applications = MCollective::Applications.list
|
|
7
|
+
|
|
8
|
+
# links from mc-ping to mc will result in ping being run
|
|
9
|
+
if $0 =~ /mc\-([a-zA-Z\-_\.]+)$/
|
|
10
|
+
app_name = $1
|
|
11
|
+
else
|
|
12
|
+
app_name = ARGV.first
|
|
13
|
+
ARGV.delete_at(0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if known_applications.include?(app_name)
|
|
17
|
+
# make sure the various options classes shows the right help etc
|
|
18
|
+
$0 = app_name
|
|
19
|
+
|
|
20
|
+
MCollective::Applications.run(app_name)
|
|
21
|
+
else
|
|
22
|
+
puts "The Marionette Collective version #{MCollective.version}"
|
|
23
|
+
puts
|
|
24
|
+
puts "#{$0}: command (options)"
|
|
25
|
+
puts
|
|
26
|
+
puts "Known commands: #{known_applications.join " "}"
|
|
27
|
+
end
|
data/lib/mcollective.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'stomp'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'digest/md5'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
require 'singleton'
|
|
7
|
+
require 'socket'
|
|
8
|
+
require 'erb'
|
|
9
|
+
require 'shellwords'
|
|
10
|
+
require 'mcollective/monkey_patches'
|
|
11
|
+
require 'tempfile'
|
|
12
|
+
require 'rbconfig'
|
|
13
|
+
require 'tmpdir'
|
|
14
|
+
|
|
15
|
+
# == The Marionette Collective
|
|
16
|
+
#
|
|
17
|
+
# Framework to build and run Systems Administration agents running on a
|
|
18
|
+
# publish/subscribe middleware system. The system allows you to treat your
|
|
19
|
+
# network as the only true source of the state of your platform via discovery agents
|
|
20
|
+
# and allow you to run agents matching discovery criteria.
|
|
21
|
+
#
|
|
22
|
+
# For an overview of the idea behind this and what it enables please see:
|
|
23
|
+
# http://www.devco.net/archives/2009/10/18/middleware_for_systems_administration.php
|
|
24
|
+
module MCollective
|
|
25
|
+
# Exceptions for the RPC system
|
|
26
|
+
class RPCError<StandardError;end
|
|
27
|
+
class RPCAborted<RPCError;end
|
|
28
|
+
class UnknownRPCAction<RPCError;end
|
|
29
|
+
class MissingRPCData<RPCError;end
|
|
30
|
+
class InvalidRPCData<RPCError;end
|
|
31
|
+
class UnknownRPCError<RPCError;end
|
|
32
|
+
class NotTargettedAtUs<RuntimeError;end
|
|
33
|
+
class SecurityValidationFailed<RuntimeError;end
|
|
34
|
+
class DDLValidationError<RuntimeError;end
|
|
35
|
+
class MsgTTLExpired<RuntimeError;end
|
|
36
|
+
class MsgDoesNotMatchRequestID < RuntimeError; end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
autoload :Config, "mcollective/config"
|
|
40
|
+
autoload :Log, "mcollective/log"
|
|
41
|
+
autoload :Logger, "mcollective/logger"
|
|
42
|
+
autoload :Runner, "mcollective/runner"
|
|
43
|
+
autoload :RunnerStats, "mcollective/runnerstats"
|
|
44
|
+
autoload :Agents, "mcollective/agents"
|
|
45
|
+
autoload :Client, "mcollective/client"
|
|
46
|
+
autoload :Util, "mcollective/util"
|
|
47
|
+
autoload :Optionparser, "mcollective/optionparser"
|
|
48
|
+
autoload :Connector, "mcollective/connector"
|
|
49
|
+
autoload :Security, "mcollective/security"
|
|
50
|
+
autoload :Facts, "mcollective/facts"
|
|
51
|
+
autoload :Registration, "mcollective/registration"
|
|
52
|
+
autoload :PluginManager, "mcollective/pluginmanager"
|
|
53
|
+
autoload :RPC, "mcollective/rpc"
|
|
54
|
+
autoload :Matcher, "mcollective/matcher"
|
|
55
|
+
autoload :Message, "mcollective/message"
|
|
56
|
+
autoload :SSL, "mcollective/ssl"
|
|
57
|
+
autoload :Application, "mcollective/application"
|
|
58
|
+
autoload :Applications, "mcollective/applications"
|
|
59
|
+
autoload :Vendor, "mcollective/vendor"
|
|
60
|
+
autoload :Shell, "mcollective/shell"
|
|
61
|
+
autoload :PluginPackager, "mcollective/pluginpackager"
|
|
62
|
+
|
|
63
|
+
MCollective::Vendor.load_vendored
|
|
64
|
+
|
|
65
|
+
VERSION="@DEVELOPMENT_VERSION@"
|
|
66
|
+
|
|
67
|
+
def self.version
|
|
68
|
+
VERSION
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
module MCollective
|
|
2
|
+
# A collection of agents, loads them, reloads them and dispatches messages to them.
|
|
3
|
+
# It uses the PluginManager to store, load and manage instances of plugins.
|
|
4
|
+
class Agents
|
|
5
|
+
def initialize(agents = {})
|
|
6
|
+
@config = Config.instance
|
|
7
|
+
raise ("Configuration has not been loaded, can't load agents") unless @config.configured
|
|
8
|
+
|
|
9
|
+
@@agents = agents
|
|
10
|
+
|
|
11
|
+
loadagents
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Deletes all agents
|
|
15
|
+
def clear!
|
|
16
|
+
@@agents.each_key do |agent|
|
|
17
|
+
PluginManager.delete "#{agent}_agent"
|
|
18
|
+
Util.unsubscribe(Util.make_subscriptions(agent, :broadcast))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@@agents = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Loads all agents from disk
|
|
25
|
+
def loadagents
|
|
26
|
+
Log.debug("Reloading all agents from disk")
|
|
27
|
+
|
|
28
|
+
clear!
|
|
29
|
+
|
|
30
|
+
@config.libdir.each do |libdir|
|
|
31
|
+
agentdir = "#{libdir}/mcollective/agent"
|
|
32
|
+
next unless File.directory?(agentdir)
|
|
33
|
+
|
|
34
|
+
Dir.new(agentdir).grep(/\.rb$/).each do |agent|
|
|
35
|
+
agentname = File.basename(agent, ".rb")
|
|
36
|
+
loadagent(agentname) unless PluginManager.include?("#{agentname}_agent")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Loads a specified agent from disk if available
|
|
42
|
+
def loadagent(agentname)
|
|
43
|
+
agentfile = findagentfile(agentname)
|
|
44
|
+
return false unless agentfile
|
|
45
|
+
classname = class_for_agent(agentname)
|
|
46
|
+
|
|
47
|
+
PluginManager.delete("#{agentname}_agent")
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
single_instance = ["registration", "discovery"].include?(agentname)
|
|
51
|
+
|
|
52
|
+
PluginManager.loadclass(classname)
|
|
53
|
+
|
|
54
|
+
if activate_agent?(agentname)
|
|
55
|
+
PluginManager << {:type => "#{agentname}_agent", :class => classname, :single_instance => single_instance}
|
|
56
|
+
|
|
57
|
+
Util.subscribe(Util.make_subscriptions(agentname, :broadcast)) unless @@agents.include?(agentname)
|
|
58
|
+
|
|
59
|
+
@@agents[agentname] = {:file => agentfile}
|
|
60
|
+
return true
|
|
61
|
+
else
|
|
62
|
+
Log.debug("Not activating agent #{agentname} due to agent policy in activate? method")
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
rescue Exception => e
|
|
66
|
+
Log.error("Loading agent #{agentname} failed: #{e}")
|
|
67
|
+
PluginManager.delete("#{agentname}_agent")
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Builds a class name string given a Agent name
|
|
73
|
+
def class_for_agent(agent)
|
|
74
|
+
"MCollective::Agent::#{agent.capitalize}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Checks if a plugin should be activated by
|
|
78
|
+
# calling #activate? on it if it responds to
|
|
79
|
+
# that method else always activate it
|
|
80
|
+
def activate_agent?(agent)
|
|
81
|
+
klass = Kernel.const_get("MCollective").const_get("Agent").const_get(agent.capitalize)
|
|
82
|
+
|
|
83
|
+
if klass.respond_to?("activate?")
|
|
84
|
+
return klass.activate?
|
|
85
|
+
else
|
|
86
|
+
Log.debug("#{klass} does not have an activate? method, activating as default")
|
|
87
|
+
return true
|
|
88
|
+
end
|
|
89
|
+
rescue Exception => e
|
|
90
|
+
Log.warn("Agent activation check for #{agent} failed: #{e.class}: #{e}")
|
|
91
|
+
return false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# searches the libdirs for agents
|
|
95
|
+
def findagentfile(agentname)
|
|
96
|
+
@config.libdir.each do |libdir|
|
|
97
|
+
agentfile = File.join([libdir, "mcollective", "agent", "#{agentname}.rb"])
|
|
98
|
+
if File.exist?(agentfile)
|
|
99
|
+
Log.debug("Found #{agentname} at #{agentfile}")
|
|
100
|
+
return agentfile
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
return false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Determines if we have an agent with a certain name
|
|
107
|
+
def include?(agentname)
|
|
108
|
+
PluginManager.include?("#{agentname}_agent")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns the help for an agent after first trying to get
|
|
112
|
+
# rid of some indentation infront
|
|
113
|
+
def help(agentname)
|
|
114
|
+
raise("No such agent") unless include?(agentname)
|
|
115
|
+
|
|
116
|
+
body = PluginManager["#{agentname}_agent"].help.split("\n")
|
|
117
|
+
|
|
118
|
+
if body.first =~ /^(\s+)\S/
|
|
119
|
+
indent = $1
|
|
120
|
+
|
|
121
|
+
body = body.map {|b| b.gsub(/^#{indent}/, "")}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
body.join("\n")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Dispatches a message to an agent, accepts a block that will get run if there are
|
|
128
|
+
# any replies to process from the agent
|
|
129
|
+
def dispatch(request, connection)
|
|
130
|
+
Log.debug("Dispatching a message to agent #{request.agent}")
|
|
131
|
+
|
|
132
|
+
Thread.new do
|
|
133
|
+
begin
|
|
134
|
+
agent = PluginManager["#{request.agent}_agent"]
|
|
135
|
+
|
|
136
|
+
Timeout::timeout(agent.timeout) do
|
|
137
|
+
replies = agent.handlemsg(request.payload, connection)
|
|
138
|
+
|
|
139
|
+
# Agents can decide if they wish to reply or not,
|
|
140
|
+
# returning nil will mean nothing goes back to the
|
|
141
|
+
# requestor
|
|
142
|
+
unless replies == nil
|
|
143
|
+
yield(replies)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
rescue Timeout::Error => e
|
|
147
|
+
Log.warn("Timeout while handling message for #{request.agent}")
|
|
148
|
+
rescue Exception => e
|
|
149
|
+
Log.error("Execution of #{request.agent} failed: #{e}")
|
|
150
|
+
Log.error(e.backtrace.join("\n\t\t"))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get a list of agents that we have
|
|
156
|
+
def self.agentlist
|
|
157
|
+
@@agents.keys
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
module MCollective
|
|
2
|
+
class Application
|
|
3
|
+
include RPC
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
# Intialize a blank set of options if its the first time used
|
|
7
|
+
# else returns active options
|
|
8
|
+
def application_options
|
|
9
|
+
intialize_application_options unless @application_options
|
|
10
|
+
@application_options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# set an option in the options hash
|
|
14
|
+
def []=(option, value)
|
|
15
|
+
intialize_application_options unless @application_options
|
|
16
|
+
@application_options[option] = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# retrieves a specific option
|
|
20
|
+
def [](option)
|
|
21
|
+
intialize_application_options unless @application_options
|
|
22
|
+
@application_options[option]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sets the application description, there can be only one
|
|
26
|
+
# description per application so multiple calls will just
|
|
27
|
+
# change the description
|
|
28
|
+
def description(descr)
|
|
29
|
+
self[:description] = descr
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Supplies usage information, calling multiple times will
|
|
33
|
+
# create multiple usage lines in --help output
|
|
34
|
+
def usage(usage)
|
|
35
|
+
self[:usage] << usage
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def exclude_argument_sections(*sections)
|
|
39
|
+
sections = [sections].flatten
|
|
40
|
+
|
|
41
|
+
sections.each do |s|
|
|
42
|
+
raise "Unknown CLI argument section #{s}" unless ["rpc", "common", "filter"].include?(s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
intialize_application_options unless @application_options
|
|
46
|
+
self[:exclude_arg_sections] = sections
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Wrapper to create command line options
|
|
50
|
+
#
|
|
51
|
+
# - name: varaible name that will be used to access the option value
|
|
52
|
+
# - description: textual info shown in --help
|
|
53
|
+
# - arguments: a list of possible arguments that can be used
|
|
54
|
+
# to activate this option
|
|
55
|
+
# - type: a data type that ObjectParser understand of :bool or :array
|
|
56
|
+
# - required: true or false if this option has to be supplied
|
|
57
|
+
# - validate: a proc that will be called with the value used to validate
|
|
58
|
+
# the supplied value
|
|
59
|
+
#
|
|
60
|
+
# option :foo,
|
|
61
|
+
# :description => "The foo option"
|
|
62
|
+
# :arguments => ["--foo ARG"]
|
|
63
|
+
#
|
|
64
|
+
# after this the value supplied will be in configuration[:foo]
|
|
65
|
+
def option(name, arguments)
|
|
66
|
+
opt = {:name => name,
|
|
67
|
+
:description => nil,
|
|
68
|
+
:arguments => [],
|
|
69
|
+
:type => String,
|
|
70
|
+
:required => false,
|
|
71
|
+
:validate => Proc.new { true }}
|
|
72
|
+
|
|
73
|
+
arguments.each_pair{|k,v| opt[k] = v}
|
|
74
|
+
|
|
75
|
+
self[:cli_arguments] << opt
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Creates an empty set of options
|
|
79
|
+
def intialize_application_options
|
|
80
|
+
@application_options = {:description => nil,
|
|
81
|
+
:usage => [],
|
|
82
|
+
:cli_arguments => [],
|
|
83
|
+
:exclude_arg_sections => []}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The application configuration built from CLI arguments
|
|
88
|
+
def configuration
|
|
89
|
+
@application_configuration ||= {}
|
|
90
|
+
@application_configuration
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The active options hash used for MC::Client and other configuration
|
|
94
|
+
def options
|
|
95
|
+
@options
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Calls the supplied block in an option for validation, an error raised
|
|
99
|
+
# will log to STDERR and exit the application
|
|
100
|
+
def validate_option(blk, name, value)
|
|
101
|
+
validation_result = blk.call(value)
|
|
102
|
+
|
|
103
|
+
unless validation_result == true
|
|
104
|
+
STDERR.puts "Validation of #{name} failed: #{validation_result}"
|
|
105
|
+
exit 1
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Creates a standard options hash, pass in a block to add extra headings etc
|
|
110
|
+
# see Optionparser
|
|
111
|
+
def clioptions(help)
|
|
112
|
+
oparser = Optionparser.new({:verbose => false, :progress_bar => true}, "filter", application_options[:exclude_arg_sections])
|
|
113
|
+
|
|
114
|
+
options = oparser.parse do |parser, options|
|
|
115
|
+
if block_given?
|
|
116
|
+
yield(parser, options)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
RPC::Helpers.add_simplerpc_options(parser, options) unless application_options[:exclude_arg_sections].include?("rpc")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return oparser.parser.help if help
|
|
123
|
+
|
|
124
|
+
validate_cli_options
|
|
125
|
+
|
|
126
|
+
post_option_parser(configuration) if respond_to?(:post_option_parser)
|
|
127
|
+
|
|
128
|
+
return options
|
|
129
|
+
rescue Exception => e
|
|
130
|
+
application_failure(e)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Builds an ObjectParser config, parse the CLI options and validates based
|
|
134
|
+
# on the option config
|
|
135
|
+
def application_parse_options(help=false)
|
|
136
|
+
@options ||= {:verbose => false}
|
|
137
|
+
|
|
138
|
+
@options = clioptions(help) do |parser, options|
|
|
139
|
+
parser.define_head application_description if application_description
|
|
140
|
+
parser.banner = ""
|
|
141
|
+
|
|
142
|
+
if application_usage
|
|
143
|
+
parser.separator ""
|
|
144
|
+
|
|
145
|
+
application_usage.each do |u|
|
|
146
|
+
parser.separator "Usage: #{u}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
parser.separator ""
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
parser.define_tail ""
|
|
153
|
+
parser.define_tail "The Marionette Collective #{MCollective.version}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
application_cli_arguments.each do |carg|
|
|
157
|
+
opts_array = []
|
|
158
|
+
|
|
159
|
+
opts_array << :on
|
|
160
|
+
|
|
161
|
+
# if a default is set from the application set it up front
|
|
162
|
+
if carg.include?(:default)
|
|
163
|
+
configuration[carg[:name]] = carg[:default]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# :arguments are multiple possible ones
|
|
167
|
+
if carg[:arguments].is_a?(Array)
|
|
168
|
+
carg[:arguments].each {|a| opts_array << a}
|
|
169
|
+
else
|
|
170
|
+
opts_array << carg[:arguments]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# type was given and its not one of our special types, just pass it onto optparse
|
|
174
|
+
opts_array << carg[:type] if carg[:type] and ! [:bool, :array].include?(carg[:type])
|
|
175
|
+
|
|
176
|
+
opts_array << carg[:description]
|
|
177
|
+
|
|
178
|
+
# Handle our special types else just rely on the optparser to handle the types
|
|
179
|
+
if carg[:type] == :bool
|
|
180
|
+
parser.send(*opts_array) do |v|
|
|
181
|
+
validate_option(carg[:validate], carg[:name], v)
|
|
182
|
+
|
|
183
|
+
configuration[carg[:name]] = true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
elsif carg[:type] == :array
|
|
187
|
+
parser.send(*opts_array) do |v|
|
|
188
|
+
validate_option(carg[:validate], carg[:name], v)
|
|
189
|
+
|
|
190
|
+
configuration[carg[:name]] = [] unless configuration.include?(carg[:name])
|
|
191
|
+
configuration[carg[:name]] << v
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
else
|
|
195
|
+
parser.send(*opts_array) do |v|
|
|
196
|
+
validate_option(carg[:validate], carg[:name], v)
|
|
197
|
+
|
|
198
|
+
configuration[carg[:name]] = v
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def validate_cli_options
|
|
206
|
+
# Check all required parameters were set
|
|
207
|
+
validation_passed = true
|
|
208
|
+
application_cli_arguments.each do |carg|
|
|
209
|
+
# Check for required arguments
|
|
210
|
+
if carg[:required]
|
|
211
|
+
unless configuration[ carg[:name] ]
|
|
212
|
+
validation_passed = false
|
|
213
|
+
STDERR.puts "The #{carg[:name]} option is mandatory"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
unless validation_passed
|
|
219
|
+
STDERR.puts "\nPlease run with --help for detailed help"
|
|
220
|
+
exit 1
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Retrieves the full hash of application options
|
|
227
|
+
def application_options
|
|
228
|
+
self.class.application_options
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Retrieve the current application description
|
|
232
|
+
def application_description
|
|
233
|
+
application_options[:description]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Return the current usage text false if nothing is set
|
|
237
|
+
def application_usage
|
|
238
|
+
usage = application_options[:usage]
|
|
239
|
+
|
|
240
|
+
usage.empty? ? false : usage
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Returns an array of all the arguments built using
|
|
244
|
+
# calls to optin
|
|
245
|
+
def application_cli_arguments
|
|
246
|
+
application_options[:cli_arguments]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Handles failure, if we're far enough in the initialization
|
|
250
|
+
# phase it will log backtraces if its in verbose mode only
|
|
251
|
+
def application_failure(e, err_dest=STDERR)
|
|
252
|
+
# peole can use exit() anywhere and not get nasty backtraces as a result
|
|
253
|
+
if e.is_a?(SystemExit)
|
|
254
|
+
disconnect
|
|
255
|
+
raise(e)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
err_dest.puts "#{$0} failed to run: #{e} (#{e.class})"
|
|
259
|
+
|
|
260
|
+
if options.nil? || options[:verbose]
|
|
261
|
+
e.backtrace.each{|l| err_dest.puts "\tfrom #{l}"}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
disconnect
|
|
265
|
+
|
|
266
|
+
exit 1
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def help
|
|
270
|
+
application_parse_options(true)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The main logic loop, builds up the options, validate configuration and calls
|
|
274
|
+
# the main as supplied by the user. Disconnects when done and pass any exception
|
|
275
|
+
# onto the application_failure helper
|
|
276
|
+
def run
|
|
277
|
+
application_parse_options
|
|
278
|
+
|
|
279
|
+
validate_configuration(configuration) if respond_to?(:validate_configuration)
|
|
280
|
+
|
|
281
|
+
Util.setup_windows_sleeper if Util.windows?
|
|
282
|
+
|
|
283
|
+
main
|
|
284
|
+
|
|
285
|
+
disconnect
|
|
286
|
+
|
|
287
|
+
rescue Exception => e
|
|
288
|
+
application_failure(e)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def disconnect
|
|
292
|
+
MCollective::PluginManager["connector_plugin"].disconnect
|
|
293
|
+
rescue
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Fake abstract class that logs if the user tries to use an application without
|
|
297
|
+
# supplying a main override method.
|
|
298
|
+
def main
|
|
299
|
+
STDERR.puts "Applications need to supply a 'main' method"
|
|
300
|
+
exit 1
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# A helper that creates a consistent exit code for applications by looking at an
|
|
304
|
+
# instance of MCollective::RPC::Stats
|
|
305
|
+
#
|
|
306
|
+
# Exit with 0 if nodes were discovered and all passed
|
|
307
|
+
# Exit with 0 if no discovery were done and > 0 responses were received
|
|
308
|
+
# Exit with 1 if no nodes were discovered
|
|
309
|
+
# Exit with 2 if nodes were discovered but some RPC requests failed
|
|
310
|
+
# Exit with 3 if nodes were discovered, but not responses receivedif
|
|
311
|
+
# Exit with 4 if no discovery were done and no responses were received
|
|
312
|
+
def halt(stats)
|
|
313
|
+
request_stats = {:discoverytime => 0,
|
|
314
|
+
:discovered => 0,
|
|
315
|
+
:failcount => 0}.merge(stats.to_hash)
|
|
316
|
+
|
|
317
|
+
# was discovery done?
|
|
318
|
+
if request_stats[:discoverytime] != 0
|
|
319
|
+
# was any nodes discovered
|
|
320
|
+
if request_stats[:discovered] == 0
|
|
321
|
+
exit 1
|
|
322
|
+
|
|
323
|
+
# nodes were discovered, did we get responses
|
|
324
|
+
elsif request_stats[:responses] == 0
|
|
325
|
+
exit 3
|
|
326
|
+
|
|
327
|
+
else
|
|
328
|
+
# we got responses and discovery was done, no failures
|
|
329
|
+
if request_stats[:failcount] == 0
|
|
330
|
+
exit 0
|
|
331
|
+
else
|
|
332
|
+
exit 2
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
else
|
|
336
|
+
# discovery wasnt done and we got no responses
|
|
337
|
+
if request_stats[:responses] == 0
|
|
338
|
+
exit 4
|
|
339
|
+
else
|
|
340
|
+
exit 0
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Wrapper around MC::RPC#rpcclient that forcably supplies our options hash
|
|
346
|
+
# if someone forgets to pass in options in an application the filters and other
|
|
347
|
+
# cli options wouldnt take effect which could have a disasterous outcome
|
|
348
|
+
def rpcclient(agent, flags = {})
|
|
349
|
+
flags[:options] = options unless flags.include?(:options)
|
|
350
|
+
|
|
351
|
+
super
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|