choria-mcorpc-support 0.0.1

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.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/bin/mco +64 -0
  3. data/lib/mcollective.rb +63 -0
  4. data/lib/mcollective/agent.rb +5 -0
  5. data/lib/mcollective/agents.rb +149 -0
  6. data/lib/mcollective/aggregate.rb +85 -0
  7. data/lib/mcollective/aggregate/average.ddl +33 -0
  8. data/lib/mcollective/aggregate/average.rb +29 -0
  9. data/lib/mcollective/aggregate/base.rb +40 -0
  10. data/lib/mcollective/aggregate/result.rb +9 -0
  11. data/lib/mcollective/aggregate/result/base.rb +25 -0
  12. data/lib/mcollective/aggregate/result/collection_result.rb +19 -0
  13. data/lib/mcollective/aggregate/result/numeric_result.rb +13 -0
  14. data/lib/mcollective/aggregate/sum.ddl +33 -0
  15. data/lib/mcollective/aggregate/sum.rb +18 -0
  16. data/lib/mcollective/aggregate/summary.ddl +33 -0
  17. data/lib/mcollective/aggregate/summary.rb +53 -0
  18. data/lib/mcollective/application.rb +365 -0
  19. data/lib/mcollective/application/completion.rb +104 -0
  20. data/lib/mcollective/application/describe_filter.rb +87 -0
  21. data/lib/mcollective/application/facts.rb +62 -0
  22. data/lib/mcollective/application/find.rb +23 -0
  23. data/lib/mcollective/application/help.rb +28 -0
  24. data/lib/mcollective/application/inventory.rb +344 -0
  25. data/lib/mcollective/application/ping.rb +82 -0
  26. data/lib/mcollective/application/plugin.rb +369 -0
  27. data/lib/mcollective/application/rpc.rb +111 -0
  28. data/lib/mcollective/applications.rb +134 -0
  29. data/lib/mcollective/cache.rb +145 -0
  30. data/lib/mcollective/client.rb +353 -0
  31. data/lib/mcollective/config.rb +245 -0
  32. data/lib/mcollective/connector.rb +18 -0
  33. data/lib/mcollective/connector/base.rb +26 -0
  34. data/lib/mcollective/data.rb +91 -0
  35. data/lib/mcollective/data/agent_data.ddl +22 -0
  36. data/lib/mcollective/data/agent_data.rb +17 -0
  37. data/lib/mcollective/data/base.rb +67 -0
  38. data/lib/mcollective/data/collective_data.ddl +20 -0
  39. data/lib/mcollective/data/collective_data.rb +9 -0
  40. data/lib/mcollective/data/fact_data.ddl +28 -0
  41. data/lib/mcollective/data/fact_data.rb +55 -0
  42. data/lib/mcollective/data/fstat_data.ddl +89 -0
  43. data/lib/mcollective/data/fstat_data.rb +56 -0
  44. data/lib/mcollective/data/result.rb +45 -0
  45. data/lib/mcollective/ddl.rb +113 -0
  46. data/lib/mcollective/ddl/agentddl.rb +253 -0
  47. data/lib/mcollective/ddl/base.rb +217 -0
  48. data/lib/mcollective/ddl/dataddl.rb +56 -0
  49. data/lib/mcollective/ddl/discoveryddl.rb +52 -0
  50. data/lib/mcollective/ddl/validatorddl.rb +6 -0
  51. data/lib/mcollective/discovery.rb +143 -0
  52. data/lib/mcollective/discovery/flatfile.ddl +11 -0
  53. data/lib/mcollective/discovery/flatfile.rb +48 -0
  54. data/lib/mcollective/discovery/mc.ddl +11 -0
  55. data/lib/mcollective/discovery/mc.rb +30 -0
  56. data/lib/mcollective/discovery/stdin.ddl +11 -0
  57. data/lib/mcollective/discovery/stdin.rb +68 -0
  58. data/lib/mcollective/exceptions.rb +28 -0
  59. data/lib/mcollective/facts.rb +39 -0
  60. data/lib/mcollective/facts/base.rb +100 -0
  61. data/lib/mcollective/facts/yaml_facts.rb +65 -0
  62. data/lib/mcollective/generators.rb +7 -0
  63. data/lib/mcollective/generators/agent_generator.rb +51 -0
  64. data/lib/mcollective/generators/base.rb +46 -0
  65. data/lib/mcollective/generators/data_generator.rb +51 -0
  66. data/lib/mcollective/generators/templates/action_snippet.erb +13 -0
  67. data/lib/mcollective/generators/templates/data_input_snippet.erb +7 -0
  68. data/lib/mcollective/generators/templates/ddl.erb +8 -0
  69. data/lib/mcollective/generators/templates/plugin.erb +7 -0
  70. data/lib/mcollective/log.rb +118 -0
  71. data/lib/mcollective/logger.rb +5 -0
  72. data/lib/mcollective/logger/base.rb +77 -0
  73. data/lib/mcollective/logger/console_logger.rb +61 -0
  74. data/lib/mcollective/logger/file_logger.rb +53 -0
  75. data/lib/mcollective/logger/syslog_logger.rb +53 -0
  76. data/lib/mcollective/matcher.rb +224 -0
  77. data/lib/mcollective/matcher/parser.rb +128 -0
  78. data/lib/mcollective/matcher/scanner.rb +241 -0
  79. data/lib/mcollective/message.rb +248 -0
  80. data/lib/mcollective/monkey_patches.rb +152 -0
  81. data/lib/mcollective/optionparser.rb +197 -0
  82. data/lib/mcollective/pluginmanager.rb +180 -0
  83. data/lib/mcollective/pluginpackager.rb +98 -0
  84. data/lib/mcollective/pluginpackager/agent_definition.rb +94 -0
  85. data/lib/mcollective/pluginpackager/debpackage_packager.rb +237 -0
  86. data/lib/mcollective/pluginpackager/modulepackage_packager.rb +127 -0
  87. data/lib/mcollective/pluginpackager/ospackage_packager.rb +59 -0
  88. data/lib/mcollective/pluginpackager/rpmpackage_packager.rb +180 -0
  89. data/lib/mcollective/pluginpackager/standard_definition.rb +69 -0
  90. data/lib/mcollective/pluginpackager/templates/debian/Makefile.erb +7 -0
  91. data/lib/mcollective/pluginpackager/templates/debian/changelog.erb +5 -0
  92. data/lib/mcollective/pluginpackager/templates/debian/compat.erb +1 -0
  93. data/lib/mcollective/pluginpackager/templates/debian/control.erb +15 -0
  94. data/lib/mcollective/pluginpackager/templates/debian/copyright.erb +8 -0
  95. data/lib/mcollective/pluginpackager/templates/debian/rules.erb +6 -0
  96. data/lib/mcollective/pluginpackager/templates/module/Modulefile.erb +5 -0
  97. data/lib/mcollective/pluginpackager/templates/module/README.md.erb +37 -0
  98. data/lib/mcollective/pluginpackager/templates/module/_manifest.pp.erb +9 -0
  99. data/lib/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +63 -0
  100. data/lib/mcollective/registration/base.rb +91 -0
  101. data/lib/mcollective/rpc.rb +182 -0
  102. data/lib/mcollective/rpc/actionrunner.rb +158 -0
  103. data/lib/mcollective/rpc/agent.rb +374 -0
  104. data/lib/mcollective/rpc/audit.rb +38 -0
  105. data/lib/mcollective/rpc/client.rb +1066 -0
  106. data/lib/mcollective/rpc/helpers.rb +321 -0
  107. data/lib/mcollective/rpc/progress.rb +63 -0
  108. data/lib/mcollective/rpc/reply.rb +87 -0
  109. data/lib/mcollective/rpc/request.rb +86 -0
  110. data/lib/mcollective/rpc/result.rb +90 -0
  111. data/lib/mcollective/rpc/stats.rb +294 -0
  112. data/lib/mcollective/runnerstats.rb +90 -0
  113. data/lib/mcollective/security.rb +26 -0
  114. data/lib/mcollective/security/base.rb +244 -0
  115. data/lib/mcollective/shell.rb +126 -0
  116. data/lib/mcollective/ssl.rb +285 -0
  117. data/lib/mcollective/util.rb +579 -0
  118. data/lib/mcollective/validator.rb +85 -0
  119. data/lib/mcollective/validator/array_validator.ddl +7 -0
  120. data/lib/mcollective/validator/array_validator.rb +9 -0
  121. data/lib/mcollective/validator/ipv4address_validator.ddl +7 -0
  122. data/lib/mcollective/validator/ipv4address_validator.rb +16 -0
  123. data/lib/mcollective/validator/ipv6address_validator.ddl +7 -0
  124. data/lib/mcollective/validator/ipv6address_validator.rb +16 -0
  125. data/lib/mcollective/validator/length_validator.ddl +7 -0
  126. data/lib/mcollective/validator/length_validator.rb +11 -0
  127. data/lib/mcollective/validator/regex_validator.ddl +7 -0
  128. data/lib/mcollective/validator/regex_validator.rb +9 -0
  129. data/lib/mcollective/validator/shellsafe_validator.ddl +7 -0
  130. data/lib/mcollective/validator/shellsafe_validator.rb +13 -0
  131. data/lib/mcollective/validator/typecheck_validator.ddl +7 -0
  132. data/lib/mcollective/validator/typecheck_validator.rb +28 -0
  133. metadata +215 -0
@@ -0,0 +1,111 @@
1
+ class MCollective::Application::Rpc<MCollective::Application
2
+ description "Generic RPC agent client application"
3
+
4
+ usage "mco rpc [options] [filters] --agent <agent> --action <action> [--argument <key=val> --argument ...]"
5
+ usage "mco rpc [options] [filters] <agent> <action> [<key=val> <key=val> ...]"
6
+
7
+ option :show_results,
8
+ :description => "Do not process results, just send request",
9
+ :arguments => ["--no-results", "--nr"],
10
+ :default => true,
11
+ :type => :bool
12
+
13
+ option :agent,
14
+ :description => "Agent to call",
15
+ :arguments => ["-a", "--agent AGENT"]
16
+
17
+ option :action,
18
+ :description => "Action to call",
19
+ :arguments => ["--action ACTION"]
20
+
21
+ option :arguments,
22
+ :description => "Arguments to pass to agent",
23
+ :arguments => ["--arg", "--argument ARGUMENT"],
24
+ :type => :array,
25
+ :default => [],
26
+ :validate => Proc.new {|val| val.match(/^(.+?)=(.+)$/) ? true : "Could not parse --arg #{val} should be of the form key=val" }
27
+
28
+ def post_option_parser(configuration)
29
+ # handle the alternative format that optparse cant parse
30
+ unless (configuration.include?(:agent) && configuration.include?(:action))
31
+ if ARGV.length >= 2
32
+ configuration[:agent] = ARGV[0]
33
+ ARGV.delete_at(0)
34
+
35
+ configuration[:action] = ARGV[0]
36
+ ARGV.delete_at(0)
37
+
38
+ ARGV.each do |v|
39
+ if v =~ /^(.+?)=(.+)$/
40
+ configuration[:arguments] = [] unless configuration.include?(:arguments)
41
+ configuration[:arguments] << v
42
+ else
43
+ STDERR.puts("Could not parse --arg #{v}")
44
+ exit(1)
45
+ end
46
+ end
47
+ else
48
+ STDERR.puts("No agent, action and arguments specified")
49
+ exit(1)
50
+ end
51
+ end
52
+
53
+ # convert arguments to symbols for keys to comply with simplerpc conventions
54
+ args = configuration[:arguments].clone
55
+ configuration[:arguments] = {}
56
+
57
+ args.each do |v|
58
+ if v =~ /^(.+?)=(.+)$/
59
+ configuration[:arguments][$1.to_sym] = $2
60
+ end
61
+ end
62
+ end
63
+
64
+ def string_to_ddl_type(arguments, ddl)
65
+ return if ddl.empty?
66
+
67
+ arguments.keys.each do |key|
68
+ if ddl[:input].keys.include?(key)
69
+ case ddl[:input][key][:type]
70
+ when :boolean
71
+ arguments[key] = MCollective::DDL.string_to_boolean(arguments[key])
72
+
73
+ when :number, :integer, :float
74
+ arguments[key] = MCollective::DDL.string_to_number(arguments[key])
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def main
81
+ mc = rpcclient(configuration[:agent])
82
+
83
+ mc.agent_filter(configuration[:agent])
84
+
85
+ string_to_ddl_type(configuration[:arguments], mc.ddl.action_interface(configuration[:action])) if mc.ddl
86
+
87
+ mc.validate_request(configuration[:action], configuration[:arguments])
88
+
89
+ if mc.reply_to
90
+ configuration[:arguments][:process_results] = true
91
+
92
+ puts "Request sent with id: " + mc.send(configuration[:action], configuration[:arguments]) + " replies to #{mc.reply_to}"
93
+ elsif !configuration[:show_results]
94
+ configuration[:arguments][:process_results] = false
95
+
96
+ puts "Request sent with id: " + mc.send(configuration[:action], configuration[:arguments])
97
+ else
98
+ discover_args = {:verbose => true}
99
+
100
+ mc.detect_and_set_stdin_discovery
101
+
102
+ mc.discover discover_args
103
+
104
+ printrpc mc.send(configuration[:action], configuration[:arguments])
105
+
106
+ printrpcstats :summarize => true, :caption => "#{configuration[:agent]}##{configuration[:action]} call stats" if mc.discover.size > 0
107
+
108
+ halt mc.stats
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,134 @@
1
+ module MCollective
2
+ class Applications
3
+ def self.[](appname)
4
+ load_application(appname)
5
+ PluginManager["#{appname}_application"]
6
+ end
7
+
8
+ def self.run(appname)
9
+ load_config
10
+
11
+ begin
12
+ load_application(appname)
13
+ rescue Exception => e
14
+ e.backtrace.first << Util.colorize(:red, " <----")
15
+ STDERR.puts "Application '#{appname}' failed to load:"
16
+ STDERR.puts
17
+ STDERR.puts Util.colorize(:red, " #{e} (#{e.class})")
18
+ STDERR.puts
19
+ STDERR.puts " %s" % [e.backtrace.join("\n ")]
20
+ exit 1
21
+ end
22
+
23
+ PluginManager["#{appname}_application"].run
24
+ end
25
+
26
+ def self.load_application(appname)
27
+ return if PluginManager.include?("#{appname}_application")
28
+
29
+ load_config
30
+
31
+ PluginManager.loadclass "MCollective::Application::#{appname.capitalize}"
32
+ PluginManager << {:type => "#{appname}_application", :class => "MCollective::Application::#{appname.capitalize}"}
33
+ end
34
+
35
+ # Returns an array of applications found in the lib dirs
36
+ def self.list
37
+ load_config
38
+
39
+ PluginManager.find("application")
40
+ rescue SystemExit
41
+ exit 1
42
+ rescue Exception => e
43
+ STDERR.puts "Failed to generate application list: #{e.class}: #{e}"
44
+ exit 1
45
+ end
46
+
47
+ # Filters a string of opts out using Shellwords
48
+ # keeping only things related to --config and -c
49
+ def self.filter_extra_options(opts)
50
+ res = ""
51
+ words = Shellwords.shellwords(opts)
52
+ words.each_with_index do |word,idx|
53
+ if word == "-c"
54
+ return "--config=#{words[idx + 1]}"
55
+ elsif word == "--config"
56
+ return "--config=#{words[idx + 1]}"
57
+ elsif word =~ /\-c=/
58
+ return word
59
+ elsif word =~ /\-\-config=/
60
+ return word
61
+ end
62
+ end
63
+
64
+ return ""
65
+ end
66
+
67
+ # We need to know the config file in order to know the libdir
68
+ # so that we can find applications.
69
+ #
70
+ # The problem is the CLI might be stuffed with options only the
71
+ # app in the libdir might understand so we have a chicken and
72
+ # egg situation.
73
+ #
74
+ # We're parsing and filtering MCOLLECTIVE_EXTRA_OPTS removing
75
+ # all but config related options and parsing the options looking
76
+ # just for the config file.
77
+ #
78
+ # We're handling failures gracefully and finally restoring the
79
+ # ARG and MCOLLECTIVE_EXTRA_OPTS to the state they were before
80
+ # we started parsing.
81
+ #
82
+ # This is mostly a hack, when we're redoing how config works
83
+ # this stuff should be made less sucky
84
+ def self.load_config
85
+ return if Config.instance.configured
86
+
87
+ original_argv = ARGV.clone
88
+ original_extra_opts = ENV["MCOLLECTIVE_EXTRA_OPTS"].clone rescue nil
89
+ configfile = nil
90
+
91
+ parser = OptionParser.new
92
+ parser.on("--config CONFIG", "-c", "Config file") do |f|
93
+ configfile = f
94
+ end
95
+
96
+ parser.program_name = $0
97
+
98
+ parser.on("--help")
99
+
100
+ # avoid option parsers own internal version handling that sux
101
+ parser.on("-v", "--verbose")
102
+
103
+ if original_extra_opts
104
+ begin
105
+ # optparse will parse the whole ENV in one go and refuse
106
+ # to play along with the retry trick I do below so in
107
+ # order to handle unknown options properly I parse out
108
+ # only -c and --config deleting everything else and
109
+ # then restore the environment variable later when I
110
+ # am done with it
111
+ ENV["MCOLLECTIVE_EXTRA_OPTS"] = filter_extra_options(ENV["MCOLLECTIVE_EXTRA_OPTS"].clone)
112
+ parser.environment("MCOLLECTIVE_EXTRA_OPTS")
113
+ rescue Exception => e
114
+ Log.error("Failed to parse MCOLLECTIVE_EXTRA_OPTS: #{e}")
115
+ end
116
+
117
+ ENV["MCOLLECTIVE_EXTRA_OPTS"] = original_extra_opts.clone
118
+ end
119
+
120
+ begin
121
+ parser.parse!
122
+ rescue OptionParser::InvalidOption => e
123
+ retry
124
+ end
125
+
126
+ ARGV.clear
127
+ original_argv.each {|a| ARGV << a}
128
+
129
+ configfile = Util.config_file_for_user unless configfile
130
+
131
+ Config.instance.loadconfig(configfile)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,145 @@
1
+ module MCollective
2
+ # A class to manage a number of named caches. Each cache can have a unique
3
+ # timeout for keys in it and each cache has an independent Mutex protecting
4
+ # access to it.
5
+ #
6
+ # This cache is setup early in the process of loading the mcollective
7
+ # libraries, before any threads are created etc making it suitable as a
8
+ # cross thread cache or just a store for Mutexes you need to use across
9
+ # threads like in an Agent or something.
10
+ #
11
+ # # sets up a new cache, noop if it already exist
12
+ # Cache.setup(:ddl, 600)
13
+ # => true
14
+ #
15
+ # # writes an item to the :ddl cache, this item will
16
+ # # be valid on the cache for 600 seconds
17
+ # Cache.write(:ddl, :something, "value")
18
+ # => "value"
19
+ #
20
+ # # reads from the cache, read failures due to non existing
21
+ # # data or expired data will raise an exception
22
+ # Cache.read(:ddl, :something)
23
+ # => "value"
24
+ #
25
+ # # time left till expiry, raises if nothing is found
26
+ # Cache.ttl(:ddl, :something)
27
+ # => 500
28
+ #
29
+ # # forcibly evict something from the cache
30
+ # Cache.invalidate!(:ddl, :something)
31
+ # => "value"
32
+ #
33
+ # # deletes an entire named cache and its mutexes
34
+ # Cache.delete!(:ddl)
35
+ # => true
36
+ #
37
+ # # you can also just use this cache as a global mutex store
38
+ # Cache.setup(:mylock)
39
+ #
40
+ # Cache.synchronize(:mylock) do
41
+ # do_something()
42
+ # end
43
+ #
44
+ module Cache
45
+ # protects access to @cache_locks and top level @cache
46
+ @locks_mutex = Mutex.new
47
+
48
+ # stores a mutex per named cache
49
+ @cache_locks = {}
50
+
51
+ # the named caches protected by items in @cache_locks
52
+ @cache = {}
53
+
54
+ def self.setup(cache_name, ttl=300)
55
+ @locks_mutex.synchronize do
56
+ break if @cache_locks.include?(cache_name)
57
+
58
+ @cache_locks[cache_name] = Mutex.new
59
+
60
+ @cache_locks[cache_name].synchronize do
61
+ @cache[cache_name] = {:max_age => Float(ttl)}
62
+ end
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ def self.has_cache?(cache_name)
69
+ @locks_mutex.synchronize do
70
+ @cache.include?(cache_name)
71
+ end
72
+ end
73
+
74
+ def self.delete!(cache_name)
75
+ @locks_mutex.synchronize do
76
+ raise("No cache called '%s'" % cache_name) unless @cache_locks.include?(cache_name)
77
+
78
+ @cache_locks.delete(cache_name)
79
+ @cache.delete(cache_name)
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ def self.write(cache_name, key, value)
86
+ raise("No cache called '%s'" % cache_name) unless @cache.include?(cache_name)
87
+
88
+ @cache_locks[cache_name].synchronize do
89
+ @cache[cache_name][key] ||= {}
90
+ @cache[cache_name][key][:cache_create_time] = Time.now
91
+ @cache[cache_name][key][:value] = value
92
+ end
93
+
94
+ value
95
+ end
96
+
97
+ def self.read(cache_name, key)
98
+ raise("No cache called '%s'" % cache_name) unless @cache.include?(cache_name)
99
+
100
+ unless ttl(cache_name, key) > 0
101
+ Log.debug("Cache expired on '%s' key '%s'" % [cache_name, key])
102
+ raise("Cache for item '%s' on cache '%s' has expired" % [key, cache_name])
103
+ end
104
+
105
+ Log.debug("Cache hit on '%s' key '%s'" % [cache_name, key])
106
+
107
+ @cache_locks[cache_name].synchronize do
108
+ @cache[cache_name][key][:value]
109
+ end
110
+ end
111
+
112
+ def self.ttl(cache_name, key)
113
+ raise("No cache called '%s'" % cache_name) unless @cache.include?(cache_name)
114
+
115
+ @cache_locks[cache_name].synchronize do
116
+ unless @cache[cache_name].include?(key)
117
+ Log.debug("Cache miss on '%s' key '%s'" % [cache_name, key])
118
+ raise("No item called '%s' for cache '%s'" % [key, cache_name])
119
+ end
120
+
121
+ @cache[cache_name][:max_age] - (Time.now - @cache[cache_name][key][:cache_create_time])
122
+ end
123
+ end
124
+
125
+ def self.synchronize(cache_name)
126
+ raise("No cache called '%s'" % cache_name) unless @cache.include?(cache_name)
127
+
128
+ raise ("No block supplied to synchronize") unless block_given?
129
+
130
+ @cache_locks[cache_name].synchronize do
131
+ yield
132
+ end
133
+ end
134
+
135
+ def self.invalidate!(cache_name, key)
136
+ raise("No cache called '%s'" % cache_name) unless @cache.include?(cache_name)
137
+
138
+ @cache_locks[cache_name].synchronize do
139
+ return false unless @cache[cache_name].include?(key)
140
+
141
+ @cache[cache_name].delete(key)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,353 @@
1
+ module MCollective
2
+ # Helpers for writing clients that can talk to agents, do discovery and so forth
3
+ class Client
4
+ attr_accessor :options, :stats, :discoverer, :connection_timeout
5
+
6
+ def initialize(options)
7
+ @config = Config.instance
8
+ @options = nil
9
+
10
+ if options.is_a?(String)
11
+ # String is the path to a config file
12
+ @config.loadconfig(options) unless @config.configured
13
+ elsif options.is_a?(Hash)
14
+ @config.loadconfig(options[:config]) unless @config.configured
15
+ @options = options
16
+ @connection_timeout = options[:connection_timeout]
17
+ else
18
+ raise "Invalid parameter passed to Client constructor. Valid types are Hash or String"
19
+ end
20
+
21
+ @connection_timeout ||= @config.connection_timeout
22
+
23
+ @connection = PluginManager["connector_plugin"]
24
+ @security = PluginManager["security_plugin"]
25
+
26
+ @security.initiated_by = :client
27
+ @subscriptions = {}
28
+
29
+ @discoverer = Discovery.new(self)
30
+
31
+ # Time box the connection if a timeout has been specified
32
+ # connection_timeout defaults to nil which means it will try forever if
33
+ # not specified
34
+ begin
35
+ Timeout::timeout(@connection_timeout, ClientTimeoutError) do
36
+ @connection.connect
37
+ end
38
+ rescue ClientTimeoutError => e
39
+ Log.error("Timeout occured while trying to connect to middleware")
40
+ raise e
41
+ end
42
+ end
43
+
44
+ @@request_sequence = 0
45
+ def self.request_sequence
46
+ @@request_sequence
47
+ end
48
+
49
+ # Returns the configured main collective if no
50
+ # specific collective is specified as options
51
+ def collective
52
+ if @options[:collective].nil?
53
+ @config.main_collective
54
+ else
55
+ @options[:collective]
56
+ end
57
+ end
58
+
59
+ # Disconnects cleanly from the middleware
60
+ def disconnect
61
+ Log.debug("Disconnecting from the middleware")
62
+ @connection.disconnect
63
+ end
64
+
65
+ # Sends a request and returns the generated request id, doesn't wait for
66
+ # responses and doesn't execute any passed in code blocks for responses
67
+ def sendreq(msg, agent, filter = {})
68
+ request = createreq(msg, agent, filter)
69
+ publish(request)
70
+ request.requestid
71
+ end
72
+
73
+ def createreq(msg, agent, filter ={})
74
+ if msg.is_a?(Message)
75
+ request = msg
76
+ agent = request.agent
77
+ else
78
+ ttl = @options[:ttl] || @config.ttl
79
+ request = Message.new(msg, nil, {:agent => agent, :type => :request, :collective => collective, :filter => filter, :ttl => ttl})
80
+ request.reply_to = @options[:reply_to] if @options[:reply_to]
81
+ end
82
+
83
+ @@request_sequence += 1
84
+
85
+ request.encode!
86
+ subscribe(agent, :reply) unless request.reply_to
87
+ request
88
+ end
89
+
90
+ def subscribe(agent, type)
91
+ unless @subscriptions.include?(agent)
92
+ subscription = Util.make_subscriptions(agent, type, collective)
93
+ Log.debug("Subscribing to #{type} target for agent #{agent}")
94
+
95
+ Util.subscribe(subscription)
96
+ @subscriptions[agent] = 1
97
+ end
98
+ end
99
+
100
+ def unsubscribe(agent, type)
101
+ if @subscriptions.include?(agent)
102
+ subscription = Util.make_subscriptions(agent, type, collective)
103
+ Log.debug("Unsubscribing #{type} target for #{agent}")
104
+
105
+ Util.unsubscribe(subscription)
106
+ @subscriptions.delete(agent)
107
+ end
108
+ end
109
+ # Blocking call that waits for ever for a message to arrive.
110
+ #
111
+ # If you give it a requestid this means you've previously send a request
112
+ # with that ID and now you just want replies that matches that id, in that
113
+ # case the current connection will just ignore all messages not directed at it
114
+ # and keep waiting for more till it finds a matching message.
115
+ def receive(requestid = nil)
116
+ reply = nil
117
+
118
+ begin
119
+ reply = @connection.receive
120
+ reply.type = :reply
121
+ reply.expected_msgid = requestid
122
+
123
+ reply.decode!
124
+
125
+ unless reply.requestid == requestid
126
+ raise(MsgDoesNotMatchRequestID, "Message reqid #{reply.requestid} does not match our reqid #{requestid}")
127
+ end
128
+
129
+ Log.debug("Received reply to #{reply.requestid} from #{reply.payload[:senderid]}")
130
+ rescue SecurityValidationFailed => e
131
+ Log.warn("Ignoring a message that did not pass security validations")
132
+ retry
133
+ rescue MsgDoesNotMatchRequestID => e
134
+ Log.debug("Ignoring a message for some other client : #{e.message}")
135
+ retry
136
+ end
137
+
138
+ reply
139
+ end
140
+
141
+ # Performs a discovery of nodes matching the filter passed
142
+ # returns an array of nodes
143
+ #
144
+ # An integer limit can be supplied this will have the effect
145
+ # of the discovery being cancelled soon as it reached the
146
+ # requested limit of hosts
147
+ def discover(filter, timeout, limit=0)
148
+ @discoverer.discover(filter.merge({'collective' => collective}), timeout, limit)
149
+ end
150
+
151
+ # Send a request, performs the passed block for each response
152
+ #
153
+ # times = req("status", "mcollectived", options, client) {|resp|
154
+ # pp resp
155
+ # }
156
+ #
157
+ # It returns a hash of times and timeouts for discovery and total run is taken from the options
158
+ # hash which in turn is generally built using MCollective::Optionparser
159
+ def req(body, agent=nil, options=false, waitfor=[], &block)
160
+ if body.is_a?(Message)
161
+ agent = body.agent
162
+ waitfor = body.discovered_hosts || []
163
+ @options = body.options
164
+ end
165
+
166
+ @options = options if options
167
+ threaded = @options[:threaded]
168
+ timeout = @discoverer.discovery_timeout(@options[:timeout], @options[:filter])
169
+ request = createreq(body, agent, @options[:filter])
170
+ publish_timeout = @options[:publish_timeout] || @config.publish_timeout
171
+ stat = {:starttime => Time.now.to_f, :discoverytime => 0, :blocktime => 0, :totaltime => 0}
172
+ STDOUT.sync = true
173
+ hosts_responded = 0
174
+
175
+ begin
176
+ if threaded
177
+ hosts_responded = threaded_req(request, publish_timeout, timeout, waitfor, &block)
178
+ else
179
+ hosts_responded = unthreaded_req(request, publish_timeout, timeout, waitfor, &block)
180
+ end
181
+ rescue Interrupt => e
182
+ ensure
183
+ unsubscribe(agent, :reply)
184
+ end
185
+
186
+ return update_stat(stat, hosts_responded, request.requestid)
187
+ end
188
+
189
+ # Starts the client receiver and publisher unthreaded.
190
+ # This is the default client behaviour.
191
+ def unthreaded_req(request, publish_timeout, timeout, waitfor, &block)
192
+ start_publisher(request, publish_timeout)
193
+ start_receiver(request.requestid, waitfor, timeout, &block)
194
+ end
195
+
196
+ # Starts the client receiver and publisher in threads.
197
+ # This is activated when the 'threader_client' configuration
198
+ # option is set.
199
+ def threaded_req(request, publish_timeout, timeout, waitfor, &block)
200
+ Log.debug("Starting threaded client")
201
+ publisher = Thread.new do
202
+ start_publisher(request, publish_timeout)
203
+ end
204
+
205
+ # When the client is threaded we add the publishing timeout to
206
+ # the agent timeout so that the receiver doesn't time out before
207
+ # publishing has finished in cases where publish_timeout >= timeout.
208
+ total_timeout = publish_timeout + timeout
209
+ hosts_responded = 0
210
+
211
+ receiver = Thread.new do
212
+ hosts_responded = start_receiver(request.requestid, waitfor, total_timeout, &block)
213
+ end
214
+
215
+ receiver.join
216
+ hosts_responded
217
+ end
218
+
219
+ # Starts the request publishing routine
220
+ def start_publisher(request, publish_timeout)
221
+ Log.debug("Starting publishing with publish timeout of #{publish_timeout}")
222
+ begin
223
+ Timeout.timeout(publish_timeout) do
224
+ publish(request)
225
+ end
226
+ rescue Timeout::Error => e
227
+ Log.warn("Could not publish all messages. Publishing timed out.")
228
+ end
229
+ end
230
+
231
+ def publish(request)
232
+ Log.info("Sending request #{request.requestid} for agent '#{request.agent}' with ttl #{request.ttl} in collective '#{request.collective}'")
233
+ request.publish
234
+ end
235
+
236
+ # Starts the response receiver routine
237
+ # Expected to return the amount of received responses.
238
+ def start_receiver(requestid, waitfor, timeout, &block)
239
+ Log.debug("Starting response receiver with timeout of #{timeout}")
240
+ hosts_responded = 0
241
+
242
+ if (waitfor.is_a?(Array))
243
+ unfinished = Hash.new(0)
244
+ waitfor.each {|w| unfinished[w] += 1}
245
+ else
246
+ unfinished = []
247
+ end
248
+
249
+ begin
250
+ Timeout.timeout(timeout) do
251
+ loop do
252
+ resp = receive(requestid)
253
+
254
+ if block.arity == 2
255
+ yield resp.payload, resp
256
+ else
257
+ yield resp.payload
258
+ end
259
+
260
+ hosts_responded += 1
261
+
262
+ if (waitfor.is_a?(Array))
263
+ sender = resp.payload[:senderid]
264
+ if unfinished[sender] <= 1
265
+ unfinished.delete(sender)
266
+ else
267
+ unfinished[sender] -= 1
268
+ end
269
+
270
+ break if !waitfor.empty? && unfinished.empty?
271
+ else
272
+ break unless waitfor == 0 || hosts_responded < waitfor
273
+ end
274
+ end
275
+ end
276
+ rescue Timeout::Error => e
277
+ if waitfor.is_a?(Array)
278
+ if !unfinished.empty?
279
+ Log.warn("Could not receive all responses. Did not receive responses from #{unfinished.keys.join(', ')}")
280
+ end
281
+ elsif (waitfor > hosts_responded)
282
+ Log.warn("Could not receive all responses. Expected : #{waitfor}. Received : #{hosts_responded}")
283
+ end
284
+ end
285
+
286
+ hosts_responded
287
+ end
288
+
289
+ def update_stat(stat, hosts_responded, requestid)
290
+ stat[:totaltime] = Time.now.to_f - stat[:starttime]
291
+ stat[:blocktime] = stat[:totaltime] - stat[:discoverytime]
292
+ stat[:responses] = hosts_responded
293
+ stat[:noresponsefrom] = []
294
+ stat[:unexpectedresponsefrom] = []
295
+ stat[:requestid] = requestid
296
+
297
+ @stats = stat
298
+ end
299
+
300
+ def discovered_req(body, agent, options=false)
301
+ raise "Client#discovered_req has been removed, please port your agent and client to the SimpleRPC framework"
302
+ end
303
+
304
+ # Prints out the stats returns from req and discovered_req in a nice way
305
+ def display_stats(stats, options=false, caption="stomp call summary")
306
+ options = @options unless options
307
+
308
+ if options[:verbose]
309
+ puts("\n---- #{caption} ----")
310
+
311
+ if stats[:discovered]
312
+ puts(" Nodes: #{stats[:discovered]} / #{stats[:responses]}")
313
+ else
314
+ puts(" Nodes: #{stats[:responses]}")
315
+ end
316
+
317
+ printf(" Start Time: %s\n", Time.at(stats[:starttime]))
318
+ printf(" Discovery Time: %.2fms\n", stats[:discoverytime] * 1000)
319
+ printf(" Agent Time: %.2fms\n", stats[:blocktime] * 1000)
320
+ printf(" Total Time: %.2fms\n", stats[:totaltime] * 1000)
321
+
322
+ else
323
+ if stats[:discovered]
324
+ printf("\nFinished processing %d / %d hosts in %.2f ms\n\n", stats[:responses], stats[:discovered], stats[:blocktime] * 1000)
325
+ else
326
+ printf("\nFinished processing %d hosts in %.2f ms\n\n", stats[:responses], stats[:blocktime] * 1000)
327
+ end
328
+ end
329
+
330
+ if stats[:noresponsefrom].size > 0
331
+ puts("\nNo response from:\n")
332
+
333
+ stats[:noresponsefrom].each do |c|
334
+ puts if c % 4 == 1
335
+ printf("%30s", c)
336
+ end
337
+
338
+ puts
339
+ end
340
+
341
+ if stats[:unexpectedresponsefrom].size > 0
342
+ puts("\nUnexpected response from:\n")
343
+
344
+ stats[:unexpectedresponsefrom].each do |c|
345
+ puts if c % 4 == 1
346
+ printf("%30s", c)
347
+ end
348
+
349
+ puts
350
+ end
351
+ end
352
+ end
353
+ end