mcollective-client 2.5.3 → 2.6.0

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 (41) hide show
  1. data/lib/mcollective.rb +1 -1
  2. data/lib/mcollective/application.rb +21 -6
  3. data/lib/mcollective/client.rb +7 -0
  4. data/lib/mcollective/config.rb +13 -1
  5. data/lib/mcollective/connector/base.rb +2 -0
  6. data/lib/mcollective/facts/base.rb +18 -5
  7. data/lib/mcollective/log.rb +7 -0
  8. data/lib/mcollective/logger/base.rb +12 -8
  9. data/lib/mcollective/logger/file_logger.rb +7 -0
  10. data/lib/mcollective/message.rb +1 -1
  11. data/lib/mcollective/optionparser.rb +4 -0
  12. data/lib/mcollective/registration/base.rb +24 -10
  13. data/lib/mcollective/rpc/agent.rb +7 -1
  14. data/lib/mcollective/rpc/client.rb +89 -35
  15. data/lib/mcollective/rpc/helpers.rb +8 -3
  16. data/lib/mcollective/rpc/result.rb +4 -0
  17. data/lib/mcollective/rpc/stats.rb +6 -2
  18. data/lib/mcollective/shell.rb +2 -0
  19. data/lib/mcollective/ssl.rb +5 -0
  20. data/lib/mcollective/util.rb +29 -1
  21. data/lib/mcollective/validator.rb +9 -4
  22. data/spec/spec_helper.rb +6 -0
  23. data/spec/unit/config_spec.rb +10 -0
  24. data/spec/unit/connector/base_spec.rb +28 -0
  25. data/spec/unit/facts/base_spec.rb +35 -0
  26. data/spec/unit/log_spec.rb +9 -0
  27. data/spec/unit/logger/base_spec.rb +12 -2
  28. data/spec/unit/logger/file_logger_spec.rb +82 -0
  29. data/spec/unit/plugins/mcollective/application/plugin_spec.rb +1 -0
  30. data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +44 -17
  31. data/spec/unit/plugins/mcollective/connector/rabbitmq_spec.rb +20 -19
  32. data/spec/unit/plugins/mcollective/data/fact_data_spec.rb +92 -0
  33. data/spec/unit/registration/base_spec.rb +46 -0
  34. data/spec/unit/rpc/agent_spec.rb +37 -0
  35. data/spec/unit/rpc/client_spec.rb +68 -15
  36. data/spec/unit/rpc/result_spec.rb +21 -0
  37. data/spec/unit/runner_spec.rb +97 -19
  38. data/spec/unit/shell_spec.rb +5 -0
  39. data/spec/unit/ssl_spec.rb +5 -0
  40. data/spec/unit/util_spec.rb +163 -1
  41. metadata +215 -209
data/lib/mcollective.rb CHANGED
@@ -59,7 +59,7 @@ module MCollective
59
59
 
60
60
  MCollective::Vendor.load_vendored
61
61
 
62
- VERSION="2.5.3"
62
+ VERSION="@DEVELOPMENT_VERSION@"
63
63
 
64
64
  def self.version
65
65
  VERSION
@@ -314,12 +314,27 @@ module MCollective
314
314
  :okcount => 0,
315
315
  :failcount => 0}.merge(stats.to_hash)
316
316
 
317
- return 4 if request_stats[:discoverytime] == 0 && request_stats[:responses] == 0
318
- return 3 if request_stats[:discovered] > 0 && request_stats[:responses] == 0
319
- return 2 if request_stats[:discovered] > 0 && request_stats[:failcount] > 0
320
- return 1 if request_stats[:discovered] == 0
321
- return 0 if request_stats[:discoverytime] == 0 && request_stats[:discovered] == request_stats[:okcount]
322
- return 0 if request_stats[:discovered] == request_stats[:okcount]
317
+ if (request_stats[:discoverytime] == 0 && request_stats[:responses] == 0)
318
+ return 4
319
+ end
320
+
321
+ if (request_stats[:discovered] > 0)
322
+ if (request_stats[:responses] == 0)
323
+ return 3
324
+ elsif (request_stats[:failcount] > 0)
325
+ return 2
326
+ end
327
+ end
328
+
329
+ if (request_stats[:discovered] == 0)
330
+ if (request_stats[:responses] && request_stats[:responses] > 0)
331
+ return 0
332
+ else
333
+ return 1
334
+ end
335
+ end
336
+
337
+ return 0
323
338
  end
324
339
 
325
340
  # A helper that creates a consistent exit code for applications by looking at an
@@ -18,6 +18,11 @@ module MCollective
18
18
  @connection.connect
19
19
  end
20
20
 
21
+ @@request_sequence = 0
22
+ def self.request_sequence
23
+ @@request_sequence
24
+ end
25
+
21
26
  # Returns the configured main collective if no
22
27
  # specific collective is specified as options
23
28
  def collective
@@ -55,6 +60,8 @@ module MCollective
55
60
  request.reply_to = @options[:reply_to] if @options[:reply_to]
56
61
  end
57
62
 
63
+ @@request_sequence += 1
64
+
58
65
  request.encode!
59
66
  subscribe(agent, :reply) unless request.reply_to
60
67
  request
@@ -15,7 +15,8 @@ module MCollective
15
15
  attr_reader :main_collective, :ssl_cipher, :registration_collective
16
16
  attr_reader :direct_addressing, :direct_addressing_threshold, :ttl
17
17
  attr_reader :default_discovery_method, :default_discovery_options
18
- attr_reader :publish_timeout, :threaded, :soft_shutdown
18
+ attr_reader :publish_timeout, :threaded, :soft_shutdown, :activate_agents
19
+ attr_reader :registration_splay, :discovery_timeout, :soft_shutdown_timeout
19
20
 
20
21
  def initialize
21
22
  @configured = false
@@ -43,6 +44,8 @@ module MCollective
43
44
  @registration_collective = val
44
45
  when "registerinterval"
45
46
  @registerinterval = Integer(val)
47
+ when "registration_splay"
48
+ @registration_splay = Util.str_to_bool(val)
46
49
  when "collectives"
47
50
  @collectives = val.split(",").map {|c| c.strip}
48
51
  when "main_collective"
@@ -87,6 +90,8 @@ module MCollective
87
90
  @classesfile = val
88
91
  when /^plugin.(.+)$/
89
92
  @pluginconf[$1] = val
93
+ when "discovery_timeout"
94
+ @discovery_timeout = Integer(val)
90
95
  when "publish_timeout"
91
96
  @publish_timeout = Integer(val)
92
97
  when "rpcaudit"
@@ -115,6 +120,10 @@ module MCollective
115
120
  @default_discovery_method = val
116
121
  when "soft_shutdown"
117
122
  @soft_shutdown = Util.str_to_bool(val)
123
+ when "soft_shutdown_timeout"
124
+ @soft_shutdown_timeout = Integer(val)
125
+ when "activate_agents"
126
+ @activate_agents = Util.str_to_bool(val)
118
127
  when "topicprefix", "topicsep", "queueprefix", "rpchelptemplate", "helptemplatedir"
119
128
  Log.warn("Use of deprecated '#{key}' option. This option is ignored and should be removed from '#{configfile}'")
120
129
  else
@@ -165,6 +174,7 @@ module MCollective
165
174
  @registration = "Agentlist"
166
175
  @registerinterval = 0
167
176
  @registration_collective = nil
177
+ @registration_splay = false
168
178
  @classesfile = "/var/lib/puppet/state/classes.txt"
169
179
  @rpcaudit = false
170
180
  @rpcauditprovider = ""
@@ -193,6 +203,8 @@ module MCollective
193
203
  @publish_timeout = 2
194
204
  @threaded = false
195
205
  @soft_shutdown = false
206
+ @soft_shutdown_timeout = nil
207
+ @activate_agents = true
196
208
  end
197
209
 
198
210
  def read_plugin_config_dir(dir)
@@ -17,6 +17,8 @@ module MCollective
17
17
  module Connector
18
18
  class Base
19
19
  def self.inherited(klass)
20
+ plugin_name = klass.to_s.split("::").last.downcase
21
+ ddl = DDL.new(plugin_name, :connector)
20
22
  PluginManager << {:type => "connector_plugin", :class => klass.to_s}
21
23
  end
22
24
  end
@@ -34,11 +34,7 @@ module MCollective
34
34
  # Force reset to last known good state on empty facts
35
35
  raise "Got empty facts" if tfacts.empty?
36
36
 
37
- @facts.clear
38
-
39
- tfacts.each_pair do |key,value|
40
- @facts[key.to_s] = value.to_s
41
- end
37
+ @facts = normalize_facts(tfacts)
42
38
 
43
39
  @last_good_facts = @facts.clone
44
40
  @last_facts_load = Time.now.to_i
@@ -81,6 +77,23 @@ module MCollective
81
77
  def force_reload?
82
78
  false
83
79
  end
80
+
81
+ private
82
+
83
+ def normalize_facts(value)
84
+ case value
85
+ when Array
86
+ return value.map { |v| normalize_facts(v) }
87
+ when Hash
88
+ new_hash = {}
89
+ value.each do |k,v|
90
+ new_hash[k.to_s] = normalize_facts(v)
91
+ end
92
+ return new_hash
93
+ else
94
+ return value.to_s
95
+ end
96
+ end
84
97
  end
85
98
  end
86
99
  end
@@ -44,6 +44,13 @@ module MCollective
44
44
  @logger.cycle_level if @configured
45
45
  end
46
46
 
47
+ # reopen log files
48
+ def reopen
49
+ if @configured
50
+ @logger.reopen
51
+ end
52
+ end
53
+
47
54
  # logs a message at a certain level
48
55
  def log(level, msg)
49
56
  configure unless @configured
@@ -34,6 +34,18 @@ module MCollective
34
34
  @active_level = level.to_sym
35
35
  end
36
36
 
37
+ def start
38
+ raise "The logging class did not supply a start method"
39
+ end
40
+
41
+ def log(level, from, msg)
42
+ raise "The logging class did not supply a log method"
43
+ end
44
+
45
+ def reopen
46
+ # reopen may not make sense to all Loggers, but we expect it of the API
47
+ end
48
+
37
49
  private
38
50
  def map_level(level)
39
51
  raise "Logger class do not know how to handle #{level} messages" unless valid_levels.include?(level.to_sym)
@@ -60,14 +72,6 @@ module MCollective
60
72
  def valid_levels
61
73
  raise "The logging class did not supply a valid_levels method"
62
74
  end
63
-
64
- def log(level, from, msg)
65
- raise "The logging class did not supply a log method"
66
- end
67
-
68
- def start
69
- raise "The logging class did not supply a start method"
70
- end
71
75
  end
72
76
  end
73
77
  end
@@ -41,6 +41,13 @@ module MCollective
41
41
  # STDERR it as last resort
42
42
  STDERR.puts("#{level}: #{msg}")
43
43
  end
44
+
45
+ def reopen
46
+ level = @logger.level
47
+ @logger.close
48
+ start
49
+ @logger.level = level
50
+ end
44
51
  end
45
52
  end
46
53
  end
@@ -200,7 +200,7 @@ module MCollective
200
200
  if msg_age > ttl
201
201
  PluginManager["global_stats"].ttlexpired
202
202
 
203
- raise(MsgTTLExpired, "message #{requestid} from #{cid} created at #{msgtime} is #{msg_age} seconds old, TTL is #{ttl}")
203
+ raise(MsgTTLExpired, "message #{requestid} from #{cid} created at #{msgtime} is #{msg_age} seconds old, TTL is #{ttl}. Rejecting message.")
204
204
  end
205
205
  end
206
206
 
@@ -177,6 +177,10 @@ module MCollective
177
177
  @parser.on("--threaded", "Start publishing requests and receiving responses in threaded mode.") do |v|
178
178
  @options[:threaded] = true
179
179
  end
180
+
181
+ @parser.on("--sort", "Sort the output of an RPC call before processing.") do |v|
182
+ @options[:sort] = true
183
+ end
180
184
  end
181
185
 
182
186
  private
@@ -20,16 +20,7 @@ module MCollective
20
20
  return false if interval == 0
21
21
 
22
22
  Thread.new do
23
- loop do
24
- begin
25
- publish(body)
26
-
27
- sleep interval
28
- rescue Exception => e
29
- Log.error("Sending registration message failed: #{e}")
30
- sleep interval
31
- end
32
- end
23
+ publish_thread(connection)
33
24
  end
34
25
  end
35
26
 
@@ -72,6 +63,29 @@ module MCollective
72
63
  req.publish
73
64
  end
74
65
  end
66
+
67
+ def body
68
+ raise "Registration Plugins must implement the #body method"
69
+ end
70
+
71
+ private
72
+ def publish_thread(connnection)
73
+ if config.registration_splay
74
+ splay_delay = rand(interval)
75
+ Log.debug("registration_splay enabled. Registration will start in #{splay_delay} seconds")
76
+ sleep splay_delay
77
+ end
78
+
79
+ loop do
80
+ begin
81
+ publish(body)
82
+ sleep interval
83
+ rescue Exception => e
84
+ Log.error("Sending registration message failed: #{e}")
85
+ sleep interval
86
+ end
87
+ end
88
+ end
75
89
  end
76
90
  end
77
91
  end
@@ -138,10 +138,16 @@ module MCollective
138
138
  # end
139
139
  def self.activate?
140
140
  agent_name = self.to_s.split("::").last.downcase
141
+ config = Config.instance
141
142
 
142
143
  Log.debug("Starting default activation checks for #{agent_name}")
143
144
 
144
- should_activate = Util.str_to_bool(Config.instance.pluginconf.fetch("#{agent_name}.activate_agent", true))
145
+ # Check global state to determine if agent should be loaded
146
+ should_activate = config.activate_agents
147
+
148
+ # Check agent specific state to determine if agent should be loaded
149
+ should_activate = Util.str_to_bool(config.pluginconf.fetch("#{agent_name}.activate_agent",
150
+ should_activate))
145
151
 
146
152
  unless should_activate
147
153
  Log.debug("Found plugin configuration '#{agent_name}.activate_agent' with value '#{should_activate}'")
@@ -25,7 +25,12 @@ module MCollective
25
25
  initial_options = Marshal.load(@@initial_options)
26
26
 
27
27
  else
28
- oparser = MCollective::Optionparser.new({:verbose => false, :progress_bar => true, :mcollective_limit_targets => false, :batch_size => nil, :batch_sleep_time => 1}, "filter")
28
+ oparser = MCollective::Optionparser.new({ :verbose => false,
29
+ :progress_bar => true,
30
+ :mcollective_limit_targets => false,
31
+ :batch_size => nil,
32
+ :batch_sleep_time => 1 },
33
+ "filter")
29
34
 
30
35
  initial_options = oparser.parse do |parser, opts|
31
36
  if block_given?
@@ -67,13 +72,13 @@ module MCollective
67
72
  @discovery_options = initial_options[:discovery_options] || []
68
73
  @force_display_mode = initial_options[:force_display_mode] || false
69
74
 
70
- @batch_size = Integer(initial_options[:batch_size] || 0)
75
+ @batch_size = initial_options[:batch_size] || 0
71
76
  @batch_sleep_time = Float(initial_options[:batch_sleep_time] || 1)
72
- @batch_mode = @batch_size > 0
77
+ @batch_mode = determine_batch_mode(@batch_size)
73
78
 
74
79
  agent_filter agent
75
80
 
76
- @discovery_timeout = @initial_options.fetch(:disctimeout, nil)
81
+ @discovery_timeout = @initial_options.fetch(:disctimeout, nil) || Config.instance.discovery_timeout
77
82
 
78
83
  @collective = @client.collective
79
84
  @ttl = initial_options[:ttl] || Config.instance.ttl
@@ -239,6 +244,11 @@ module MCollective
239
244
 
240
245
  validate_request(action, args)
241
246
 
247
+ # TODO(ploubser): The logic here seems poor. It implies that it is valid to
248
+ # pass arguments where batch_mode is set to false and batch_mode > 0.
249
+ # If this is the case we completely ignore the supplied value of batch_mode
250
+ # and do our own thing.
251
+
242
252
  # if a global batch size is set just use that else set it
243
253
  # in the case that it was passed as an argument
244
254
  batch_mode = args.include?(:batch_size) || @batch_mode
@@ -248,7 +258,7 @@ module MCollective
248
258
  # if we were given a batch_size argument thats 0 and batch_mode was
249
259
  # determined to be on via global options etc this will allow a batch_size
250
260
  # of 0 to disable or batch_mode for this call only
251
- batch_mode = (batch_mode && Integer(batch_size) > 0)
261
+ batch_mode = determine_batch_mode(batch_size)
252
262
 
253
263
  # Handle single target requests by doing discovery and picking
254
264
  # a random node. Then do a custom request specifying a filter
@@ -580,7 +590,13 @@ module MCollective
580
590
 
581
591
  # Sets and sanity checks the limit_targets variable
582
592
  # used to restrict how many nodes we'll target
593
+ # Limit targets can be reset by passing nil or false
583
594
  def limit_targets=(limit)
595
+ if !limit
596
+ @limit_targets = nil
597
+ return
598
+ end
599
+
584
600
  if limit.is_a?(String)
585
601
  raise "Invalid limit specified: #{limit} valid limits are /^\d+%*$/" unless limit =~ /^\d+%*$/
586
602
 
@@ -606,10 +622,14 @@ module MCollective
606
622
 
607
623
  # Sets the batch size, if the size is set to 0 that will disable batch mode
608
624
  def batch_size=(limit)
609
- raise "Can only set batch size if direct addressing is supported" unless Config.instance.direct_addressing
625
+ unless Config.instance.direct_addressing
626
+ raise "Can only set batch size if direct addressing is supported"
627
+ end
628
+
629
+ validate_batch_size(limit)
610
630
 
611
- @batch_size = Integer(limit)
612
- @batch_mode = @batch_size > 0
631
+ @batch_size = limit
632
+ @batch_mode = determine_batch_mode(@batch_size)
613
633
  end
614
634
 
615
635
  def batch_sleep_time=(time)
@@ -759,8 +779,8 @@ module MCollective
759
779
  def call_agent_batched(action, args, opts, batch_size, sleep_time, &block)
760
780
  raise "Batched requests requires direct addressing" unless Config.instance.direct_addressing
761
781
  raise "Cannot bypass result processing for batched requests" if args[:process_results] == false
782
+ validate_batch_size(batch_size)
762
783
 
763
- batch_size = Integer(batch_size)
764
784
  sleep_time = Float(sleep_time)
765
785
 
766
786
  Log.debug("Calling #{agent}##{action} in batches of #{batch_size} with sleep time of #{sleep_time}")
@@ -782,10 +802,22 @@ module MCollective
782
802
  @stdout.print twirl.twirl(respcount, discovered.size)
783
803
  end
784
804
 
805
+ if (batch_size =~ /^(\d+)%$/)
806
+ # determine batch_size as a percentage of the discovered array's size
807
+ batch_size = (discovered.size / 100.0 * Integer($1)).ceil
808
+ else
809
+ batch_size = Integer(batch_size)
810
+ end
811
+
785
812
  @stats.requestid = nil
813
+ processed_nodes = 0
786
814
 
787
- discovered.in_groups_of(batch_size) do |hosts, last_batch|
788
- message = Message.new(req, nil, {:agent => @agent, :type => :direct_request, :collective => @collective, :filter => opts[:filter], :options => opts})
815
+ discovered.in_groups_of(batch_size) do |hosts|
816
+ message = Message.new(req, nil, {:agent => @agent,
817
+ :type => :direct_request,
818
+ :collective => @collective,
819
+ :filter => opts[:filter],
820
+ :options => opts})
789
821
 
790
822
  # first time round we let the Message object create a request id
791
823
  # we then re-use it for future requests to keep auditing sane etc
@@ -808,13 +840,20 @@ module MCollective
808
840
  end
809
841
  end
810
842
 
843
+ if @initial_options[:sort]
844
+ results.sort!
845
+ end
846
+
811
847
  @stats.noresponsefrom.concat @client.stats[:noresponsefrom]
812
848
  @stats.responses += @client.stats[:responses]
813
849
  @stats.blocktime += @client.stats[:blocktime] + sleep_time
814
850
  @stats.totaltime += @client.stats[:totaltime]
815
851
  @stats.discoverytime += @client.stats[:discoverytime]
816
852
 
817
- sleep sleep_time unless last_batch
853
+ processed_nodes += hosts.length
854
+ if (discovered.length > processed_nodes)
855
+ sleep sleep_time
856
+ end
818
857
  end
819
858
 
820
859
  @stats.aggregate_summary = aggregate.summarize if aggregate
@@ -898,6 +937,10 @@ module MCollective
898
937
  end
899
938
  end
900
939
 
940
+ if @initial_options[:sort]
941
+ results.sort!
942
+ end
943
+
901
944
  @stats.aggregate_summary = aggregate.summarize if aggregate
902
945
  @stats.aggregate_failures = aggregate.failed if aggregate
903
946
  @stats.client_stats = @client.stats
@@ -947,35 +990,46 @@ module MCollective
947
990
  result = rpc_result_from_reply(@agent, action, resp)
948
991
  aggregate = aggregate_reply(result, aggregate) if aggregate
949
992
 
950
- if resp[:body][:statuscode] == 0 || resp[:body][:statuscode] == 1
951
- @stats.ok if resp[:body][:statuscode] == 0
952
- @stats.fail if resp[:body][:statuscode] == 1
953
- @stats.time_block_execution :start
993
+ @stats.ok if resp[:body][:statuscode] == 0
994
+ @stats.fail if resp[:body][:statuscode] != 0
995
+ @stats.time_block_execution :start
954
996
 
955
- case block.arity
956
- when 1
957
- block.call(resp)
958
- when 2
959
- block.call(resp, result)
960
- end
997
+ case block.arity
998
+ when 1
999
+ block.call(resp)
1000
+ when 2
1001
+ block.call(resp, result)
1002
+ end
961
1003
 
962
- @stats.time_block_execution :end
963
- else
964
- @stats.fail
1004
+ @stats.time_block_execution :end
965
1005
 
966
- case resp[:body][:statuscode]
967
- when 2
968
- raise UnknownRPCAction, resp[:body][:statusmsg]
969
- when 3
970
- raise MissingRPCData, resp[:body][:statusmsg]
971
- when 4
972
- raise InvalidRPCData, resp[:body][:statusmsg]
973
- when 5
974
- raise UnknownRPCError, resp[:body][:statusmsg]
1006
+ return aggregate
1007
+ end
1008
+
1009
+ private
1010
+
1011
+ def determine_batch_mode(batch_size)
1012
+ if (batch_size != 0 && batch_size != "0")
1013
+ return true
1014
+ end
1015
+
1016
+ return false
1017
+ end
1018
+
1019
+ # Validate the bach_size based on the following criteria
1020
+ # batch_size is percentage string and it's more than 0 percent
1021
+ # batch_size is a string of digits
1022
+ # batch_size is of type Integer
1023
+ def validate_batch_size(batch_size)
1024
+ if (batch_size.is_a?(Integer))
1025
+ return
1026
+ elsif (batch_size.is_a?(String))
1027
+ if ((batch_size =~ /^(\d+)%$/ && Integer($1) != 0) || batch_size =~ /^(\d+)$/)
1028
+ return
975
1029
  end
976
1030
  end
977
1031
 
978
- return aggregate
1032
+ raise("batch_size must be an integer or match a percentage string (e.g. '24%'")
979
1033
  end
980
1034
  end
981
1035
  end