mcollective-client 2.2.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of mcollective-client might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mcollective/application.rb +25 -34
  3. data/lib/mcollective/client.rb +91 -33
  4. data/lib/mcollective/config.rb +42 -43
  5. data/lib/mcollective/data/base.rb +1 -1
  6. data/lib/mcollective/data/result.rb +6 -2
  7. data/lib/mcollective/ddl/agentddl.rb +28 -1
  8. data/lib/mcollective/ddl/base.rb +8 -6
  9. data/lib/mcollective/log.rb +11 -3
  10. data/lib/mcollective/logger/file_logger.rb +4 -4
  11. data/lib/mcollective/matcher.rb +9 -1
  12. data/lib/mcollective/message.rb +14 -23
  13. data/lib/mcollective/optionparser.rb +9 -1
  14. data/lib/mcollective/pluginpackager.rb +24 -3
  15. data/lib/mcollective/pluginpackager/agent_definition.rb +12 -12
  16. data/lib/mcollective/pluginpackager/standard_definition.rb +12 -12
  17. data/lib/mcollective/rpc/agent.rb +15 -12
  18. data/lib/mcollective/rpc/client.rb +67 -31
  19. data/lib/mcollective/rpc/helpers.rb +7 -1
  20. data/lib/mcollective/rpc/reply.rb +3 -1
  21. data/lib/mcollective/shell.rb +45 -8
  22. data/lib/mcollective/util.rb +37 -1
  23. data/lib/mcollective/windows_daemon.rb +14 -3
  24. data/spec/spec_helper.rb +2 -0
  25. data/spec/unit/application_spec.rb +45 -26
  26. data/spec/unit/cache_spec.rb +3 -3
  27. data/spec/unit/client_spec.rb +269 -24
  28. data/spec/unit/config_spec.rb +89 -26
  29. data/spec/unit/data/base_spec.rb +1 -0
  30. data/spec/unit/data/result_spec.rb +19 -1
  31. data/spec/unit/data_spec.rb +3 -0
  32. data/spec/unit/ddl/agentddl_spec.rb +32 -0
  33. data/spec/unit/ddl/base_spec.rb +4 -0
  34. data/spec/unit/ddl/dataddl_spec.rb +1 -1
  35. data/spec/unit/log_spec.rb +44 -27
  36. data/spec/unit/logger/base_spec.rb +1 -1
  37. data/spec/unit/matcher_spec.rb +14 -0
  38. data/spec/unit/message_spec.rb +24 -0
  39. data/spec/unit/optionparser_spec.rb +99 -0
  40. data/spec/unit/pluginpackager/agent_definition_spec.rb +48 -17
  41. data/spec/unit/pluginpackager/standard_definition_spec.rb +44 -20
  42. data/spec/unit/pluginpackager_spec.rb +31 -7
  43. data/spec/unit/plugins/mcollective/agent/rpcutil_spec.rb +176 -0
  44. data/spec/unit/plugins/mcollective/application/plugin_spec.rb +81 -0
  45. data/spec/unit/plugins/mcollective/audit/logfile_spec.rb +44 -0
  46. data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +118 -27
  47. data/spec/unit/plugins/mcollective/connector/rabbitmq_spec.rb +168 -34
  48. data/spec/unit/plugins/mcollective/data/agent_data_spec.rb +1 -0
  49. data/spec/unit/plugins/mcollective/data/fstat_data_spec.rb +1 -0
  50. data/spec/unit/plugins/mcollective/discovery/flatfile_spec.rb +10 -0
  51. data/spec/unit/plugins/mcollective/discovery/stdin_spec.rb +65 -0
  52. data/spec/unit/plugins/mcollective/facts/yaml_facts_spec.rb +65 -0
  53. data/spec/unit/plugins/mcollective/packagers/debpackage_packager_spec.rb +240 -219
  54. data/spec/unit/plugins/mcollective/packagers/modulepackage_packager_spec.rb +209 -0
  55. data/spec/unit/plugins/mcollective/packagers/rpmpackage_packager_spec.rb +223 -109
  56. data/spec/unit/rpc/actionrunner_spec.rb +2 -1
  57. data/spec/unit/rpc/agent_spec.rb +130 -1
  58. data/spec/unit/rpc/client_spec.rb +169 -3
  59. data/spec/unit/security/base_spec.rb +0 -1
  60. data/spec/unit/shell_spec.rb +76 -3
  61. data/spec/unit/util_spec.rb +69 -1
  62. data/spec/unit/windows_daemon_spec.rb +30 -9
  63. metadata +104 -90
  64. data/spec/unit/plugins/mcollective/connector/stomp/eventlogger_spec.rb +0 -34
  65. data/spec/unit/plugins/mcollective/connector/stomp_spec.rb +0 -424
  66. data/spec/unit/plugins/mcollective/validator/any_validator_spec.rb +0 -15
@@ -65,7 +65,7 @@ module MCollective
65
65
 
66
66
  begin
67
67
  # Incoming requests need to be validated against the DDL thus reusing
68
- # all the work users put into creating DDLs and creating a consistant
68
+ # all the work users put into creating DDLs and creating a consistent
69
69
  # quality of input validation everywhere with the a simple once off
70
70
  # investment of writing a DDL
71
71
  @request.validate!
@@ -85,7 +85,7 @@ module MCollective
85
85
  if respond_to?("#{@request.action}_action")
86
86
  send("#{@request.action}_action")
87
87
  else
88
- raise UnknownRPCAction, "Unknown action: #{@request.action}"
88
+ raise UnknownRPCAction, "Unknown action '#{@request.action}' for agent '#{@request.agent}'"
89
89
  end
90
90
  rescue RPCAborted => e
91
91
  @reply.fail e.to_s, 1
@@ -96,13 +96,17 @@ module MCollective
96
96
  rescue MissingRPCData => e
97
97
  @reply.fail e.to_s, 3
98
98
 
99
- rescue InvalidRPCData => e
99
+ rescue InvalidRPCData, DDLValidationError => e
100
100
  @reply.fail e.to_s, 4
101
101
 
102
102
  rescue UnknownRPCError => e
103
+ Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
104
+ Log.error(e.backtrace.join("\n\t"))
103
105
  @reply.fail e.to_s, 5
104
106
 
105
107
  rescue Exception => e
108
+ Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
109
+ Log.error(e.backtrace.join("\n\t"))
106
110
  @reply.fail e.to_s, 5
107
111
 
108
112
  end
@@ -137,16 +141,13 @@ module MCollective
137
141
 
138
142
  Log.debug("Starting default activation checks for #{agent_name}")
139
143
 
140
- should_activate = Config.instance.pluginconf["#{agent_name}.activate_agent"]
144
+ should_activate = Util.str_to_bool(Config.instance.pluginconf.fetch("#{agent_name}.activate_agent", true))
141
145
 
142
- if should_activate
143
- Log.debug("Found plugin config #{agent_name}.activate_agent with value #{should_activate}")
144
- unless should_activate =~ /^1|y|true$/
145
- return false
146
- end
146
+ unless should_activate
147
+ Log.debug("Found plugin configuration '#{agent_name}.activate_agent' with value '#{should_activate}'")
147
148
  end
148
149
 
149
- return true
150
+ return should_activate
150
151
  end
151
152
 
152
153
  # Returns an array of actions this agent support
@@ -211,7 +212,7 @@ module MCollective
211
212
  end
212
213
  end
213
214
 
214
- [:stdin, :cwd, :environment].each do |k|
215
+ [:stdin, :cwd, :environment, :timeout].each do |k|
215
216
  if options.include?(k)
216
217
  shellopts[k] = options[k]
217
218
  end
@@ -231,7 +232,9 @@ module MCollective
231
232
 
232
233
  # Registers meta data for the introspection hash
233
234
  def self.metadata(data)
234
- Log.warn("%s: setting meta data in agents have been deprecated, DDL files are now being used for this information." % File.basename(caller.first))
235
+ agent = File.basename(caller.first).split(":").first
236
+
237
+ Log.warn("Setting metadata in agents has been deprecated, DDL files are now being used for this information. Please update the '#{agent}' agent")
235
238
  end
236
239
 
237
240
  # Creates the needed activate? class in a manner similar to the other
@@ -5,7 +5,7 @@ module MCollective
5
5
  class Client
6
6
  attr_accessor :timeout, :verbose, :filter, :config, :progress, :ttl, :reply_to
7
7
  attr_reader :client, :stats, :ddl, :agent, :limit_targets, :limit_method, :output_format, :batch_size, :batch_sleep_time, :batch_mode
8
- attr_reader :discovery_options, :discovery_method, :limit_seed
8
+ attr_reader :discovery_options, :discovery_method, :default_discovery_method, :limit_seed
9
9
 
10
10
  @@initial_options = nil
11
11
 
@@ -57,7 +57,13 @@ module MCollective
57
57
  @output_format = initial_options[:output_format] || :console
58
58
  @force_direct_request = false
59
59
  @reply_to = initial_options[:reply_to]
60
- @discovery_method = initial_options[:discovery_method] || Config.instance.default_discovery_method
60
+ @discovery_method = initial_options[:discovery_method]
61
+ if !@discovery_method
62
+ @discovery_method = Config.instance.default_discovery_method
63
+ @default_discovery_method = true
64
+ else
65
+ @default_discovery_method = false
66
+ end
61
67
  @discovery_options = initial_options[:discovery_options] || []
62
68
  @force_display_mode = initial_options[:force_display_mode] || false
63
69
 
@@ -71,6 +77,8 @@ module MCollective
71
77
 
72
78
  @collective = @client.collective
73
79
  @ttl = initial_options[:ttl] || Config.instance.ttl
80
+ @publish_timeout = initial_options[:publish_timeout] || Config.instance.publish_timeout
81
+ @threaded = initial_options[:threaded] || Config.instance.threaded
74
82
 
75
83
  # if we can find a DDL for the service override
76
84
  # the timeout of the client so we always magically
@@ -149,6 +157,18 @@ module MCollective
149
157
  :data => data}
150
158
  end
151
159
 
160
+ # For the provided arguments and action the input arguments get
161
+ # modified by supplying any defaults provided in the DDL for arguments
162
+ # that were not supplied in the request
163
+ #
164
+ # We then pass the modified arguments to the DDL for validation
165
+ def validate_request(action, args)
166
+ raise "No DDL found for agent %s cannot validate inputs" % @agent unless @ddl
167
+
168
+ @ddl.set_default_input_arguments(action, args)
169
+ @ddl.validate_rpc_request(action, args)
170
+ end
171
+
152
172
  # Magic handler to invoke remote methods
153
173
  #
154
174
  # Once the stub is created using the constructor or the RPC#rpcclient helper you can
@@ -217,7 +237,7 @@ module MCollective
217
237
 
218
238
  @stats.reset
219
239
 
220
- @ddl.validate_rpc_request(action, args) if @ddl
240
+ validate_request(action, args)
221
241
 
222
242
  # if a global batch size is set just use that else set it
223
243
  # in the case that it was passed as an argument
@@ -274,7 +294,7 @@ module MCollective
274
294
  # request which technically does not need filters. If you try to use this
275
295
  # mode with direct addressing disabled an exception will be raise
276
296
  def custom_request(action, args, expected_agents, filter = {}, &block)
277
- @ddl.validate_rpc_request(action, args) if @ddl
297
+ validate_request(action, args)
278
298
 
279
299
  if filter == {} && !Config.instance.direct_addressing
280
300
  raise "Attempted to do a filterless custom_request without direct_addressing enabled, preventing unexpected call to all nodes"
@@ -355,6 +375,7 @@ module MCollective
355
375
  # since that is the user supplied timeout either via initial options
356
376
  # or via specifically setting it on the client.
357
377
  def discovery_method=(method)
378
+ @default_discovery_method = false
358
379
  @discovery_method = method
359
380
 
360
381
  if @initial_options[:discovery_options]
@@ -375,7 +396,7 @@ module MCollective
375
396
 
376
397
  # Sets the class filter
377
398
  def class_filter(klass)
378
- @filter["cf_class"] << klass
399
+ @filter["cf_class"] = @filter["cf_class"] | [klass]
379
400
  @filter["cf_class"].compact!
380
401
  reset
381
402
  end
@@ -387,10 +408,10 @@ module MCollective
387
408
 
388
409
  if value.nil?
389
410
  parsed = Util.parse_fact_string(fact)
390
- @filter["fact"] << parsed unless parsed == false
411
+ @filter["fact"] = @filter["fact"] | [parsed] unless parsed == false
391
412
  else
392
413
  parsed = Util.parse_fact_string("#{fact}#{operator}#{value}")
393
- @filter["fact"] << parsed unless parsed == false
414
+ @filter["fact"] = @filter["fact"] | [parsed] unless parsed == false
394
415
  end
395
416
 
396
417
  @filter["fact"].compact!
@@ -399,21 +420,21 @@ module MCollective
399
420
 
400
421
  # Sets the agent filter
401
422
  def agent_filter(agent)
402
- @filter["agent"] << agent
423
+ @filter["agent"] = @filter["agent"] | [agent]
403
424
  @filter["agent"].compact!
404
425
  reset
405
426
  end
406
427
 
407
428
  # Sets the identity filter
408
429
  def identity_filter(identity)
409
- @filter["identity"] << identity
430
+ @filter["identity"] = @filter["identity"] | [identity]
410
431
  @filter["identity"].compact!
411
432
  reset
412
433
  end
413
434
 
414
435
  # Set a compound filter
415
436
  def compound_filter(filter)
416
- @filter["compound"] << Matcher.create_compound_callstack(filter)
437
+ @filter["compound"] = @filter["compound"] | [Matcher.create_compound_callstack(filter)]
417
438
  reset
418
439
  end
419
440
 
@@ -479,24 +500,8 @@ module MCollective
479
500
  @discovered_agents = hosts
480
501
  @force_direct_request = true
481
502
 
482
- # if an identity filter is supplied and it is all strings no regex we can use that
483
- # as discovery data, technically the identity filter is then redundant if we are
484
- # in direct addressing mode and we could empty it out but this use case should
485
- # only really be for a few -I's on the CLI
486
- #
487
- # For safety we leave the filter in place for now, that way we can support this
488
- # enhancement also in broadcast mode.
489
- #
490
- # This is only needed for the 'mc' discovery method, other methods might change
491
- # the concept of identity to mean something else so we should pass the full
492
- # identity filter to them
493
- elsif options[:filter]["identity"].size > 0 && @discovery_method == "mc"
494
- regex_filters = options[:filter]["identity"].select{|i| i.match("^\/")}.size
495
-
496
- if regex_filters == 0
497
- @discovered_agents = options[:filter]["identity"].clone
498
- @force_direct_request = true if Config.instance.direct_addressing
499
- end
503
+ else
504
+ identity_filter_discovery_optimization
500
505
  end
501
506
  end
502
507
 
@@ -559,7 +564,9 @@ module MCollective
559
564
  :discovery_method => @discovery_method,
560
565
  :discovery_options => @discovery_options,
561
566
  :force_display_mode => @force_display_mode,
562
- :config => @config}
567
+ :config => @config,
568
+ :publish_timeout => @publish_timeout,
569
+ :threaded => @threaded}
563
570
  end
564
571
 
565
572
  # Sets the collective we are communicating with
@@ -701,7 +708,9 @@ module MCollective
701
708
  #
702
709
  # Should only be called via method_missing
703
710
  def fire_and_forget_request(action, args, filter=nil)
704
- @ddl.validate_rpc_request(action, args) if @ddl
711
+ validate_request(action, args)
712
+
713
+ identity_filter_discovery_optimization
705
714
 
706
715
  req = new_request(action.to_s, args)
707
716
 
@@ -710,7 +719,34 @@ module MCollective
710
719
  message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => filter, :options => options})
711
720
  message.reply_to = @reply_to if @reply_to
712
721
 
713
- return @client.sendreq(message, nil)
722
+ if @force_direct_request || @client.discoverer.force_direct_mode?
723
+ message.discovered_hosts = discover.clone
724
+ message.type = :direct_request
725
+ end
726
+
727
+ client.sendreq(message, nil)
728
+ end
729
+
730
+ # if an identity filter is supplied and it is all strings no regex we can use that
731
+ # as discovery data, technically the identity filter is then redundant if we are
732
+ # in direct addressing mode and we could empty it out but this use case should
733
+ # only really be for a few -I's on the CLI
734
+ #
735
+ # For safety we leave the filter in place for now, that way we can support this
736
+ # enhancement also in broadcast mode.
737
+ #
738
+ # This is only needed for the 'mc' discovery method, other methods might change
739
+ # the concept of identity to mean something else so we should pass the full
740
+ # identity filter to them
741
+ def identity_filter_discovery_optimization
742
+ if options[:filter]["identity"].size > 0 && @discovery_method == "mc"
743
+ regex_filters = options[:filter]["identity"].select{|i| i.match("^\/")}.size
744
+
745
+ if regex_filters == 0
746
+ @discovered_agents = options[:filter]["identity"].clone
747
+ @force_direct_request = true if Config.instance.direct_addressing
748
+ end
749
+ end
714
750
  end
715
751
 
716
752
  # Calls an agent in a way very similar to call_agent but it supports batching
@@ -100,6 +100,7 @@ module MCollective
100
100
  result_text << text_for_result(sender, status, message, result, ddl)
101
101
 
102
102
  when :flatten
103
+ Log.warn("The display option :flatten is being deprecated and will be removed in the next minor release")
103
104
  result_text << text_for_flattened_result(status, result)
104
105
 
105
106
  end
@@ -128,7 +129,7 @@ module MCollective
128
129
  result_text << " %s\n" % [Util.colorize(:yellow, msg)] unless msg == "OK"
129
130
 
130
131
  # only print good data, ignore data that results from failure
131
- if [0, 1].include?(status)
132
+ if status == 0
132
133
  if result.is_a?(Hash)
133
134
  # figure out the lengths of the display as strings, we'll use
134
135
  # it later to correctly justify the output
@@ -172,6 +173,10 @@ module MCollective
172
173
  result_text << " " << result[k].pretty_inspect.split("\n").join("\n" << padding) << "\n"
173
174
  end
174
175
  end
176
+ elsif status == 1
177
+ # for status 1 we dont want to show half baked
178
+ # data by default since the DDL will supply all the defaults
179
+ # it just doesnt look right
175
180
  else
176
181
  result_text << "\n\t" + result.pretty_inspect.split("\n").join("\n\t")
177
182
  end
@@ -249,6 +254,7 @@ module MCollective
249
254
  # Add SimpleRPC common options
250
255
  def self.add_simplerpc_options(parser, options)
251
256
  parser.separator ""
257
+ parser.separator "RPC Options"
252
258
 
253
259
  # add SimpleRPC specific options to all clients that use our library
254
260
  parser.on('--np', '--no-progress', 'Do not show the progress bar') do |v|
@@ -26,7 +26,9 @@ module MCollective
26
26
  interface = @ddl.action_interface(@action)
27
27
 
28
28
  interface[:output].keys.each do |output|
29
- @data[output] = interface[:output][output][:default]
29
+ # must deep clone this data to avoid accidental updates of the DDL in cases where the
30
+ # default is for example a string and someone does << on it
31
+ @data[output] = Marshal.load(Marshal.dump(interface[:output][output].fetch(:default, nil)))
30
32
  end
31
33
  end
32
34
 
@@ -17,9 +17,12 @@ module MCollective
17
17
  # stderr - a variable that will receive stdin, must support <<
18
18
  # environment - the shell environment, defaults to include LC_ALL=C
19
19
  # set to nil to clear the environment even of LC_ALL
20
+ # timeout - a timeout in seconds after which the subprocess is killed,
21
+ # the special value :on_thread_exit kills the subprocess
22
+ # when the invoking thread (typically the agent) has ended
20
23
  #
21
24
  class Shell
22
- attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd
25
+ attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd, :timeout
23
26
 
24
27
  def initialize(command, options={})
25
28
  @environment = {"LC_ALL" => "C"}
@@ -29,6 +32,7 @@ module MCollective
29
32
  @stderr = ""
30
33
  @stdin = nil
31
34
  @cwd = Dir.tmpdir
35
+ @timeout = nil
32
36
 
33
37
  options.each do |opt, val|
34
38
  case opt.to_s
@@ -54,6 +58,9 @@ module MCollective
54
58
  else
55
59
  @environment.merge!(val.dup)
56
60
  end
61
+ when "timeout"
62
+ raise "timeout should be a positive integer or the symbol :on_thread_exit symbol" unless val.eql?(:on_thread_exit) || ( val.is_a?(Fixnum) && val>0 )
63
+ @timeout = val
57
64
  end
58
65
  end
59
66
  end
@@ -67,21 +74,51 @@ module MCollective
67
74
 
68
75
  opts["stdin"] = @stdin if @stdin
69
76
 
70
- # Running waitpid on the cid here will start a thread
71
- # with the waitpid in it, this way even if the thread
72
- # that started this process gets killed due to agent
73
- # timeout or such there will still be a waitpid waiting
74
- # for the child to exit and not leave zombies.
77
+
78
+ thread = Thread.current
79
+ # Start a double fork and exec with systemu which implies a guard thread.
80
+ # If a valid timeout is configured the guard thread will terminate the
81
+ # executing process and reap the pid.
82
+ # If no timeout is specified the process will run to completion with the
83
+ # guard thread reaping the pid on completion.
75
84
  @status = systemu(@command, opts) do |cid|
76
85
  begin
77
- sleep 1
78
- Process::waitpid(cid)
86
+ if timeout.is_a?(Fixnum)
87
+ # wait for the specified timeout
88
+ sleep timeout
89
+ else
90
+ # sleep while the agent thread is still alive
91
+ while(thread.alive?)
92
+ sleep 0.1
93
+ end
94
+ end
95
+
96
+ # if the process is still running
97
+ if (Process.kill(0, cid))
98
+ # and a timeout was specified
99
+ if timeout
100
+ if Util.windows?
101
+ Process.kill('KILL', cid)
102
+ else
103
+ # Kill the process
104
+ Process.kill('TERM', cid)
105
+ sleep 2
106
+ Process.kill('KILL', cid) if (Process.kill(0, cid))
107
+ end
108
+ end
109
+ # only wait if the parent thread is dead
110
+ Process.waitpid(cid) unless thread.alive?
111
+ end
79
112
  rescue SystemExit
113
+ rescue Errno::ESRCH
80
114
  rescue Errno::ECHILD
115
+ Log.warn("Could not reap process '#{cid}'.")
81
116
  rescue Exception => e
82
117
  Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}")
83
118
  end
84
119
  end
120
+ @status.thread.kill
121
+ @status
85
122
  end
86
123
  end
87
124
  end
@@ -430,7 +430,7 @@ module MCollective
430
430
  # Code originally from Puppet
431
431
  def self.versioncmp(version_a, version_b)
432
432
  vre = /[-.]|\d+|[^-.\d]+/
433
- ax = version_a.scan(vre)
433
+ ax = version_a.scan(vre)
434
434
  bx = version_b.scan(vre)
435
435
 
436
436
  while (ax.length>0 && bx.length>0)
@@ -456,5 +456,41 @@ module MCollective
456
456
 
457
457
  version_a <=> version_b;
458
458
  end
459
+
460
+ # we should really use Pathname#absolute? but it's not in all the
461
+ # ruby versions we support and it comes down to roughly this
462
+ def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR)
463
+ if alt_separator
464
+ path_matcher = /^([a-zA-Z]:){0,1}[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/
465
+ else
466
+ path_matcher = /^#{Regexp.quote separator}/
467
+ end
468
+
469
+ !!path.match(path_matcher)
470
+ end
471
+
472
+ # Converts a string into a boolean value
473
+ # Strings matching 1,y,yes,true or t will return TrueClass
474
+ # Any other value will return FalseClass
475
+ def self.str_to_bool(val)
476
+ clean_val = val.to_s.strip
477
+ if clean_val =~ /^(1|yes|true|y|t)$/i
478
+ return true
479
+ elsif clean_val =~ /^(0|no|false|n|f)$/i
480
+ return false
481
+ else
482
+ raise("Cannot convert string value '#{clean_val}' into a boolean.")
483
+ end
484
+ end
485
+
486
+ # Looks up the template directory and returns its full path
487
+ def self.templatepath(template_file)
488
+ config_dir = File.dirname(Config.instance.configfile)
489
+ template_path = File.join(config_dir, template_file)
490
+ return template_path if File.exists?(template_path)
491
+
492
+ template_path = File.join("/etc/mcollective", template_file)
493
+ return template_path
494
+ end
459
495
  end
460
496
  end