85f57ebf0e3de82f0f6f37c6aa310e3c1e3b5c3e
[packages/precise/mcollective.git] / lib / mcollective / application.rb
1 module MCollective
2   class Application
3     include RPC
4
5     class << self
6       # Intialize a blank set of options if its the first time used
7       # else returns active options
8       def application_options
9         intialize_application_options unless @application_options
10         @application_options
11       end
12
13       # set an option in the options hash
14       def []=(option, value)
15         intialize_application_options unless @application_options
16         @application_options[option] = value
17       end
18
19       # retrieves a specific option
20       def [](option)
21         intialize_application_options unless @application_options
22         @application_options[option]
23       end
24
25       # Sets the application description, there can be only one
26       # description per application so multiple calls will just
27       # change the description
28       def description(descr)
29         self[:description] = descr
30       end
31
32       # Supplies usage information, calling multiple times will
33       # create multiple usage lines in --help output
34       def usage(usage)
35         self[:usage] << usage
36       end
37
38       def exclude_argument_sections(*sections)
39         sections = [sections].flatten
40
41         sections.each do |s|
42           raise "Unknown CLI argument section #{s}" unless ["rpc", "common", "filter"].include?(s)
43         end
44
45         intialize_application_options unless @application_options
46         self[:exclude_arg_sections] = sections
47       end
48
49       # Wrapper to create command line options
50       #
51       #  - name: varaible name that will be used to access the option value
52       #  - description: textual info shown in --help
53       #  - arguments: a list of possible arguments that can be used
54       #    to activate this option
55       #  - type: a data type that ObjectParser understand of :bool or :array
56       #  - required: true or false if this option has to be supplied
57       #  - validate: a proc that will be called with the value used to validate
58       #    the supplied value
59       #
60       #   option :foo,
61       #          :description => "The foo option"
62       #          :arguments   => ["--foo ARG"]
63       #
64       # after this the value supplied will be in configuration[:foo]
65       def option(name, arguments)
66         opt = {:name => name,
67                :description => nil,
68                :arguments => [],
69                :type => String,
70                :required => false,
71                :validate => Proc.new { true }}
72
73         arguments.each_pair{|k,v| opt[k] = v}
74
75         self[:cli_arguments] << opt
76       end
77
78       # Creates an empty set of options
79       def intialize_application_options
80         @application_options = {:description          => nil,
81                                 :usage                => [],
82                                 :cli_arguments        => [],
83                                 :exclude_arg_sections => []}
84       end
85     end
86
87     # The application configuration built from CLI arguments
88     def configuration
89       @application_configuration ||= {}
90       @application_configuration
91     end
92
93     # The active options hash used for MC::Client and other configuration
94     def options
95       @options
96     end
97
98     # Calls the supplied block in an option for validation, an error raised
99     # will log to STDERR and exit the application
100     def validate_option(blk, name, value)
101       validation_result = blk.call(value)
102
103       unless validation_result == true
104         STDERR.puts "Validation of #{name} failed: #{validation_result}"
105         exit 1
106       end
107     end
108
109     # Creates a standard options hash, pass in a block to add extra headings etc
110     # see Optionparser
111     def clioptions(help)
112       oparser = Optionparser.new({:verbose => false, :progress_bar => true}, "filter", application_options[:exclude_arg_sections])
113
114       options = oparser.parse do |parser, options|
115         if block_given?
116           yield(parser, options)
117         end
118
119         RPC::Helpers.add_simplerpc_options(parser, options) unless application_options[:exclude_arg_sections].include?("rpc")
120       end
121
122       return oparser.parser.help if help
123
124       validate_cli_options
125
126       post_option_parser(configuration) if respond_to?(:post_option_parser)
127
128       return options
129     rescue Exception => e
130       application_failure(e)
131     end
132
133     # Builds an ObjectParser config, parse the CLI options and validates based
134     # on the option config
135     def application_parse_options(help=false)
136       @options ||= {:verbose => false}
137
138       @options = clioptions(help) do |parser, options|
139         parser.define_head application_description if application_description
140         parser.banner = ""
141
142         if application_usage
143           parser.separator ""
144
145           application_usage.each do |u|
146             parser.separator "Usage: #{u}"
147           end
148
149           parser.separator ""
150         end
151
152         parser.separator "Application Options" unless application_cli_arguments.empty?
153
154         parser.define_tail ""
155         parser.define_tail "The Marionette Collective #{MCollective.version}"
156
157
158         application_cli_arguments.each do |carg|
159           opts_array = []
160
161           opts_array << :on
162
163           # if a default is set from the application set it up front
164           if carg.include?(:default)
165             configuration[carg[:name]] = carg[:default]
166           end
167
168           # :arguments are multiple possible ones
169           if carg[:arguments].is_a?(Array)
170             carg[:arguments].each {|a| opts_array << a}
171           else
172             opts_array << carg[:arguments]
173           end
174
175           # type was given and its not one of our special types, just pass it onto optparse
176           opts_array << carg[:type] if carg[:type] && ![:boolean, :bool, :array].include?(carg[:type])
177
178           opts_array << carg[:description]
179
180           # Handle our special types else just rely on the optparser to handle the types
181           if [:bool, :boolean].include?(carg[:type])
182             parser.send(*opts_array) do |v|
183               validate_option(carg[:validate], carg[:name], v)
184
185               configuration[carg[:name]] = v
186             end
187
188           elsif carg[:type] == :array
189             parser.send(*opts_array) do |v|
190               validate_option(carg[:validate], carg[:name], v)
191
192               configuration[carg[:name]] = [] unless configuration.include?(carg[:name])
193               configuration[carg[:name]] << v
194             end
195
196           else
197             parser.send(*opts_array) do |v|
198               validate_option(carg[:validate], carg[:name], v)
199
200               configuration[carg[:name]] = v
201             end
202           end
203         end
204       end
205     end
206
207     def validate_cli_options
208       # Check all required parameters were set
209       validation_passed = true
210       application_cli_arguments.each do |carg|
211         # Check for required arguments
212         if carg[:required]
213           unless configuration[ carg[:name] ]
214             validation_passed = false
215             STDERR.puts "The #{carg[:name]} option is mandatory"
216           end
217         end
218       end
219
220       unless validation_passed
221         STDERR.puts "\nPlease run with --help for detailed help"
222         exit 1
223       end
224
225
226     end
227
228     # Retrieves the full hash of application options
229     def application_options
230       self.class.application_options
231     end
232
233     # Retrieve the current application description
234     def application_description
235       application_options[:description]
236     end
237
238     # Return the current usage text false if nothing is set
239     def application_usage
240       usage = application_options[:usage]
241
242       usage.empty? ? false : usage
243     end
244
245     # Returns an array of all the arguments built using
246     # calls to optin
247     def application_cli_arguments
248       application_options[:cli_arguments]
249     end
250
251     # Handles failure, if we're far enough in the initialization
252     # phase it will log backtraces if its in verbose mode only
253     def application_failure(e, err_dest=STDERR)
254       # peole can use exit() anywhere and not get nasty backtraces as a result
255       if e.is_a?(SystemExit)
256         disconnect
257         raise(e)
258       end
259
260       if e.is_a?(CodedError)
261         err_dest.puts "\nThe %s application failed to run: %s: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:bold, e.code), Util.colorize(:red, e.to_s)]
262
263         err_dest.puts
264         if options[:verbose]
265           err_dest.puts "Use the 'mco doc %s' command for details about this error" % e.code
266         else
267           err_dest.puts "Use the 'mco doc %s' command for details about this error, use -v for full error backtrace details" % e.code
268         end
269       else
270         if options[:verbose]
271           err_dest.puts "\nThe %s application failed to run: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)]
272         else
273           err_dest.puts "\nThe %s application failed to run, use -v for full error backtrace details: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)]
274         end
275       end
276
277       if options.nil? || options[:verbose]
278         e.backtrace.first << Util.colorize(:red, "  <----")
279         err_dest.puts "\n%s %s" % [ Util.colorize(:red, e.to_s), Util.colorize(:bold, "(#{e.class.to_s})")]
280         e.backtrace.each{|l| err_dest.puts "\tfrom #{l}"}
281       end
282
283       disconnect
284
285       exit 1
286     end
287
288     def help
289       application_parse_options(true)
290     end
291
292     # The main logic loop, builds up the options, validate configuration and calls
293     # the main as supplied by the user.  Disconnects when done and pass any exception
294     # onto the application_failure helper
295     def run
296       application_parse_options
297
298       validate_configuration(configuration) if respond_to?(:validate_configuration)
299
300       Util.setup_windows_sleeper if Util.windows?
301
302       main
303
304       disconnect
305
306     rescue Exception => e
307       application_failure(e)
308     end
309
310     def disconnect
311       MCollective::PluginManager["connector_plugin"].disconnect
312     rescue
313     end
314
315     # Fake abstract class that logs if the user tries to use an application without
316     # supplying a main override method.
317     def main
318       STDERR.puts "Applications need to supply a 'main' method"
319       exit 1
320     end
321
322     def halt_code(stats)
323       request_stats = {:discoverytime => 0,
324                        :discovered => 0,
325                        :okcount => 0,
326                        :failcount => 0}.merge(stats.to_hash)
327
328       return 4 if request_stats[:discoverytime] == 0 && request_stats[:responses] == 0
329       return 3 if request_stats[:discovered] > 0 && request_stats[:responses] == 0
330       return 2 if request_stats[:discovered] > 0 && request_stats[:failcount] > 0
331       return 1 if request_stats[:discovered] == 0
332       return 0 if request_stats[:discoverytime] == 0 && request_stats[:discovered] == request_stats[:okcount]
333       return 0 if request_stats[:discovered] == request_stats[:okcount]
334     end
335
336     # A helper that creates a consistent exit code for applications by looking at an
337     # instance of MCollective::RPC::Stats
338     #
339     # Exit with 0 if nodes were discovered and all passed
340     # Exit with 0 if no discovery were done and > 0 responses were received, all ok
341     # Exit with 1 if no nodes were discovered
342     # Exit with 2 if nodes were discovered but some RPC requests failed
343     # Exit with 3 if nodes were discovered, but no responses received
344     # Exit with 4 if no discovery were done and no responses were received
345     def halt(stats)
346       exit(halt_code(stats))
347     end
348
349     # Wrapper around MC::RPC#rpcclient that forcably supplies our options hash
350     # if someone forgets to pass in options in an application the filters and other
351     # cli options wouldnt take effect which could have a disasterous outcome
352     def rpcclient(agent, flags = {})
353       flags[:options] = options unless flags.include?(:options)
354       flags[:exit_on_failure] = false
355
356       super
357     end
358   end
359 end