mcollective-client 2.5.3 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
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