diff options
author | Brice Figureau <brice-puppet@daysofwonder.com> | 2008-12-21 20:38:48 +0100 |
---|---|---|
committer | Brice Figureau <brice-puppet@daysofwonder.com> | 2009-02-16 20:12:02 +0100 |
commit | 97e716a97de81bb31b665c70489ee1657ddc5424 (patch) | |
tree | cf98fa5199cfd1654995c7d8c0d2bd496af7bb9e | |
parent | 495ad6654643ee7cfc500a174d36fe67eb8970b1 (diff) | |
download | puppet-97e716a97de81bb31b665c70489ee1657ddc5424.tar.gz puppet-97e716a97de81bb31b665c70489ee1657ddc5424.tar.xz puppet-97e716a97de81bb31b665c70489ee1657ddc5424.zip |
Introducing the Application Controller
Signed-off-by: Brice Figureau <brice-puppet@daysofwonder.com>
-rw-r--r-- | lib/puppet/application.rb | 292 | ||||
-rw-r--r-- | lib/puppet/util/settings.rb | 28 | ||||
-rwxr-xr-x | spec/unit/application.rb | 405 |
3 files changed, 725 insertions, 0 deletions
diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb new file mode 100644 index 000000000..3cd7598bb --- /dev/null +++ b/lib/puppet/application.rb @@ -0,0 +1,292 @@ +require 'puppet' +require 'optparse' + +# This class handles all the aspects of a Puppet application/executable +# * setting up options +# * setting up logs +# * choosing what to run +# +# === Usage +# The application is a Puppet::Application object that register itself in the list +# of available application. Each application needs a +name+ and a getopt +options+ +# description array. +# +# The executable uses the application object like this: +# Puppet::Application[:example].run +# +# +# Puppet::Application.new(:example) do +# +# preinit do +# # perform some pre initialization +# @all = false +# end +# +# # dispatch is called to know to what command to call +# dispatch do +# ARGV.shift +# end +# +# option("--arg ARGUMENT") do |v| +# @args << v +# end +# +# option("--debug", "-d") do |v| +# @debug = v +# end +# +# option("--all", "-a:) do |v| +# @all = v +# end +# +# unknown do |opt,arg| +# # last chance to manage an option +# ... +# # let's say to the framework we finally handle this option +# true +# end +# +# command(:read) do +# # read action +# end +# +# command(:write) do +# # writeaction +# end +# +# end +# +# === Preinit +# The preinit block is the first code to be called in your application, before option parsing, +# setup or command execution. +# +# === Options +# Puppet::Application uses +OptionParser+ to manage the application options. +# Options are defined with the +option+ method to which are passed various +# arguments, including the long option, the short option, a description... +# Refer to +OptionParser+ documentation for the exact format. +# * If the option method is given a block, this one will be called whenever +# the option is encountered in the command-line argument. +# * If the option method has no block, a default functionnality will be used, that +# stores the argument (or true/false if the option doesn't require an argument) in +# the global (to the application) options array. +# * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings: +# * if +unknown+ was used with a block, it will be called with the option name and argument +# * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for +# a default behavior +# +# --help is managed directly by the Puppet::Application class, but can be overriden. +# +# === Setup +# Applications can use the setup block to perform any initialization. +# The defaul +setup+ behaviour is to: read Puppet configuration and manage log level and destination +# +# === What and how to run +# If the +dispatch+ block is defined it is called. This block should return the name of the registered command +# to be run. +# If it doesn't exist, it defaults to execute the +main+ command if defined. +# +class Puppet::Application + include Puppet::Util + + @@applications = {} + class << self + include Puppet::Util + end + + attr_reader :options, :opt_parser + + def self.[](name) + name = symbolize(name) + @@applications[name] + end + + def should_parse_config + @parse_config = true + end + + def should_not_parse_config + @parse_config = false + end + + def should_parse_config? + unless @parse_config.nil? + return @parse_config + end + @parse_config = true + end + + # used to declare a new command + def command(name, &block) + meta_def(symbolize(name), &block) + end + + # used as a catch-all for unknown option + def unknown(&block) + meta_def(:handle_unknown, &block) + end + + # used to declare code that handle an option + def option(*options, &block) + long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') + fname = "handle_#{long}" + if (block_given?) + meta_def(symbolize(fname), &block) + else + meta_def(symbolize(fname)) do |value| + self.options["#{long}".to_sym] = value + end + end + @opt_parser.on(*options) do |value| + self.send(symbolize(fname), value) + end + end + + # used to declare accessor in a more natural way in the + # various applications + def attr_accessor(*args) + args.each do |arg| + meta_def(arg) do + instance_variable_get("@#{arg}".to_sym) + end + meta_def("#{arg}=") do |value| + instance_variable_set("@#{arg}".to_sym, value) + end + end + end + + # used to declare code run instead the default setup + def setup(&block) + meta_def(:run_setup, &block) + end + + # used to declare code to choose which command to run + def dispatch(&block) + meta_def(:get_command, &block) + end + + # used to execute code before running anything else + def preinit(&block) + meta_def(:run_preinit, &block) + end + + def initialize(name, banner = nil, &block) + @opt_parser = OptionParser.new(banner) + + name = symbolize(name) + + setup do + default_setup + end + + dispatch do + :main + end + + # empty by default + preinit do + end + + option("--help", "-h") do |v| + help + end + + @options = {} + + instance_eval(&block) if block_given? + + @@applications[name] = self + end + + # This is the main application entry point + def run + run_preinit + parse_options + Puppet.parse_config if should_parse_config? + run_setup + run_command + end + + def main + raise NotImplementedError, "No valid command or main" + end + + def run_command + if command = get_command() and respond_to?(command) + send(command) + else + main + end + end + + def default_setup + # Handle the logging settings + if options[:debug] or options[:verbose] + Puppet::Util::Log.newdestination(:console) + if options[:debug] + Puppet::Util::Log.level = :debug + else + Puppet::Util::Log.level = :info + end + end + + unless options[:setdest] + Puppet::Util::Log.newdestination(:syslog) + end + end + + def parse_options + # get all puppet options + optparse_opt = [] + optparse_opt = Puppet.settings.optparse_addargs(optparse_opt) + + # convert them to OptionParser format + optparse_opt.each do |option| + @opt_parser.on(*option) do |arg| + handlearg(option[0], arg) + end + end + + # scan command line argument + begin + @opt_parser.parse! + rescue OptionParser::ParseError => detail + $stderr.puts detail + $stderr.puts "Try '#{$0} --help'" + exit(1) + end + end + + def handlearg(opt, arg) + # rewrite --[no-]option to --no-option if that's what was given + if opt =~ /\[no-\]/ and !arg + opt = opt.gsub(/\[no-\]/,'no-') + end + # otherwise remove the [no-] prefix to not confuse everybody + opt = opt.gsub(/\[no-\]/, '') + unless respond_to?(:handle_unknown) and send(:handle_unknown, opt, arg) + # Puppet.settings.handlearg doesn't handle direct true/false :-) + if arg.is_a?(FalseClass) + arg = "false" + elsif arg.is_a?(TrueClass) + arg = "true" + end + Puppet.settings.handlearg(opt, arg) + end + end + + # this is used for testing + def self.exit(code) + exit(code) + end + + def help + if Puppet.features.usage? + ::RDoc::usage && exit + else + puts "No help available unless you have RDoc::usage installed" + exit + end + end + +end
\ No newline at end of file diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index 0af842c8d..27cee8c9f 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -55,6 +55,17 @@ class Puppet::Util::Settings return options end + # Generate the list of valid arguments, in a format that OptionParser can + # understand, and add them to the passed option list. + def optparse_addargs(options) + # Add all of the config parameters as valid options. + self.each { |name, element| + options << element.optparse_args + } + + return options + end + # Is our parameter a boolean parameter? def boolean?(param) param = param.to_sym @@ -912,6 +923,15 @@ Generated on #{Time.now}. end end + # get the arguments in OptionParser format + def optparse_args + if short + ["--#{name}", "-#{short}", desc, :REQUIRED] + else + ["--#{name}", desc, :REQUIRED] + end + end + def hook=(block) meta_def :handle, &block end @@ -1108,6 +1128,14 @@ Generated on #{Time.now}. end end + def optparse_args + if short + ["--[no-]#{name}", "-#{short}", desc, :NONE ] + else + ["--[no-]#{name}", desc, :NONE] + end + end + def munge(value) case value when true, "true": return true diff --git a/spec/unit/application.rb b/spec/unit/application.rb new file mode 100755 index 000000000..a10f4fbe7 --- /dev/null +++ b/spec/unit/application.rb @@ -0,0 +1,405 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../spec_helper' + +require 'puppet/application' +require 'puppet' +require 'getoptlong' + +describe Puppet::Application do + + before :each do + @app = Puppet::Application.new(:test) + end + + it "should have a run entry-point" do + @app.should respond_to(:run) + end + + it "should have a read accessor to options" do + @app.should respond_to(:options) + end + + it "should create a default run_setup method" do + @app.should respond_to(:run_setup) + end + + it "should create a default run_preinit method" do + @app.should respond_to(:run_preinit) + end + + it "should create a default get_command method" do + @app.should respond_to(:get_command) + end + + it "should return :main as default get_command" do + @app.get_command.should == :main + end + + describe "when parsing command-line options" do + + before :each do + @argv_bak = ARGV.dup + ARGV.clear + + Puppet.settings.stubs(:optparse_addargs).returns([]) + @app = Puppet::Application.new(:test) + end + + after :each do + ARGV.clear + ARGV << @argv_bak + end + + it "should get options from Puppet.settings.optparse_addargs" do + Puppet.settings.expects(:optparse_addargs).returns([]) + + @app.parse_options + end + + it "should add Puppet.settings options to OptionParser" do + Puppet.settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option"]]) + + @app.opt_parser.expects(:on).with { |*arg,&block| arg == ["--option","-o", "Funny Option"] } + + @app.parse_options + end + + it "should ask OptionParser to parse the command-line argument" do + @app.opt_parser.expects(:parse!) + + @app.parse_options + end + + describe "when using --help" do + confine "rdoc" => Puppet.features.usage? + + it "should call RDoc::usage and exit" do + @app.expects(:exit) + RDoc.expects(:usage).returns(true) + + @app.handle_help(nil) + end + + end + + describe "when dealing with an argument not declared directly by the application" do + it "should pass it to handle_unknown if this method exists" do + Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled"]]) + @app.opt_parser.stubs(:on).yields("value") + + @app.expects(:handle_unknown).with("--not-handled", "value").returns(true) + + @app.parse_options + end + + it "should pass it to Puppet.settings if handle_unknown says so" do + Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet"]]) + @app.opt_parser.stubs(:on).yields("value") + + @app.stubs(:handle_unknown).with("--topuppet", "value").returns(false) + + Puppet.settings.expects(:handlearg).with("--topuppet", "value") + @app.parse_options + end + + it "should pass it to Puppet.settings if there is no handle_unknown method" do + Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet"]]) + @app.opt_parser.stubs(:on).yields("value") + + @app.stubs(:respond_to?).returns(false) + + Puppet.settings.expects(:handlearg).with("--topuppet", "value") + @app.parse_options + end + + it "should transform boolean false value to string for Puppet.settings" do + Puppet.settings.expects(:handlearg).with("--option", "false") + @app.handlearg("--option", false) + end + + it "should transform boolean true value to string for Puppet.settings" do + Puppet.settings.expects(:handlearg).with("--option", "true") + @app.handlearg("--option", true) + end + + it "should transform boolean option to normal form for Puppet.settings" do + Puppet.settings.expects(:handlearg).with("--option", "true") + @app.handlearg("--[no-]option", true) + end + + it "should transform boolean option to no- form for Puppet.settings" do + Puppet.settings.expects(:handlearg).with("--no-option", "false") + @app.handlearg("--[no-]option", false) + end + + end + + it "should exit if OptionParser raises an error" do + $stderr.stubs(:puts) + @app.opt_parser.stubs(:parse!).raises(OptionParser::ParseError.new("blah blah")) + + @app.expects(:exit) + + lambda { @app.parse_options }.should_not raise_error + end + + end + + describe "when calling default setup" do + + before :each do + @app = Puppet::Application.new(:test) + @app.stubs(:should_parse_config?).returns(false) + @app.options.stubs(:[]) + end + + [ :debug, :verbose ].each do |level| + it "should honor option #{level}" do + @app.options.stubs(:[]).with(level).returns(true) + Puppet::Util::Log.stubs(:newdestination) + + Puppet::Util::Log.expects(:level=).with(level == :verbose ? :info : :debug) + + @app.run_setup + end + end + + it "should honor setdest option" do + @app.options.stubs(:[]).with(:setdest).returns(false) + + Puppet::Util::Log.expects(:newdestination).with(:syslog) + + @app.run_setup + end + + end + + describe "when running" do + + before :each do + @app = Puppet::Application.new(:test) + @app.stubs(:run_preinit) + @app.stubs(:run_setup) + @app.stubs(:parse_options) + end + + it "should call run_preinit" do + @app.stubs(:run_command) + + @app.expects(:run_preinit) + + @app.run + end + + it "should call parse_options" do + @app.stubs(:run_command) + + @app.expects(:parse_options) + + @app.run + end + + it "should call run_command" do + + @app.expects(:run_command) + + @app.run + end + + it "should parse Puppet configuration if should_parse_config is called" do + @app.stubs(:run_command) + @app.should_parse_config + + Puppet.expects(:parse_config) + + @app.run + end + + it "should not parse_option if should_not_parse_config is called" do + @app.stubs(:run_command) + @app.should_not_parse_config + + Puppet.expects(:parse_config).never + + @app.run + end + + it "should parse Puppet configuration if needed" do + @app.stubs(:run_command) + @app.stubs(:should_parse_config?).returns(true) + + Puppet.expects(:parse_config) + + @app.run + end + + it "should call the action matching what returned command" do + @app.stubs(:get_command).returns(:backup) + @app.stubs(:respond_to?).with(:backup).returns(true) + + @app.expects(:backup) + + @app.run + end + + it "should call main as the default command" do + @app.expects(:main) + + @app.run + end + + it "should raise an error if no command can be called" do + lambda { @app.run }.should raise_error(NotImplementedError) + end + + it "should raise an error if dispatch returns no command" do + @app.stubs(:get_command).returns(nil) + + lambda { @app.run }.should raise_error(NotImplementedError) + end + + it "should raise an error if dispatch returns an invalid command" do + @app.stubs(:get_command).returns(:this_function_doesnt_exist) + + lambda { @app.run }.should raise_error(NotImplementedError) + end + + end + + describe "when metaprogramming" do + + before :each do + @app = Puppet::Application.new(:test) + end + + it "should create a new method with command" do + @app.command(:test) do + end + + @app.should respond_to(:test) + end + + describe "when calling attr_accessor" do + it "should create a reader method" do + @app.attr_accessor(:attribute) + + @app.should respond_to(:attribute) + end + + it "should create a reader that delegates to instance_variable_get" do + @app.attr_accessor(:attribute) + + @app.expects(:instance_variable_get).with(:@attribute) + @app.attribute + end + + it "should create a writer method" do + @app.attr_accessor(:attribute) + + @app.should respond_to(:attribute=) + end + + it "should create a writer that delegates to instance_variable_set" do + @app.attr_accessor(:attribute) + + @app.expects(:instance_variable_set).with(:@attribute, 1234) + @app.attribute=1234 + end + end + + describe "when calling option" do + it "should create a new method named after the option" do + @app.option("--test1","-t") do + end + + @app.should respond_to(:handle_test1) + end + + it "should transpose in option name any '-' into '_'" do + @app.option("--test-dashes-again","-t") do + end + + @app.should respond_to(:handle_test_dashes_again) + end + + it "should create a new method called handle_test2 with option(\"--[no-]test2\")" do + @app.option("--[no-]test2","-t") do + end + + @app.should respond_to(:handle_test2) + end + + describe "when a block is passed" do + it "should create a new method with it" do + @app.option("--[no-]test2","-t") do + raise "I can't believe it, it works!" + end + + lambda { @app.handle_test2 }.should raise_error + end + + it "should declare the option to OptionParser" do + @app.opt_parser.expects(:on).with { |*arg,&block| arg[0] == "--[no-]test3" } + + @app.option("--[no-]test3","-t") do + end + end + + it "should pass a block that calls our defined method" do + @app.opt_parser.stubs(:on).yields(nil) + + @app.expects(:send).with(:handle_test4, nil) + + @app.option("--test4","-t") do + end + end + end + + describe "when no block is given" do + it "should declare the option to OptionParser" do + @app.opt_parser.expects(:on).with("--test4","-t") + + @app.option("--test4","-t") + end + + it "should give to OptionParser a block that adds the the value to the options array" do + @app.opt_parser.stubs(:on).with("--test4","-t").yields(nil) + + @app.options.expects(:[]=).with(:test4,nil) + + @app.option("--test4","-t") + end + end + end + + it "should create a method called run_setup with setup" do + @app.setup do + end + + @app.should respond_to(:run_setup) + end + + it "should create a method called run_preinit with preinit" do + @app.preinit do + end + + @app.should respond_to(:run_preinit) + end + + it "should create a method called handle_unknown with unknown" do + @app.unknown do + end + + @app.should respond_to(:handle_unknown) + end + + + it "should create a method called get_command with dispatch" do + @app.dispatch do + end + + @app.should respond_to(:get_command) + end + end +end
\ No newline at end of file |