summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/application.rb292
-rw-r--r--lib/puppet/util/settings.rb28
-rwxr-xr-xspec/unit/application.rb405
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