diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb index 854188ad1..82199bebc 100644 --- a/lib/puppet/application/device.rb +++ b/lib/puppet/application/device.rb @@ -1,238 +1,251 @@ require 'puppet/application' require 'puppet/util/network_device' class Puppet::Application::Device < Puppet::Application run_mode :agent attr_accessor :args, :agent, :host def app_defaults super.merge({ :catalog_terminus => :rest, :catalog_cache_terminus => :json, :node_terminus => :rest, :facts_terminus => :network_device, }) end def preinit # Do an initial trap, so that cancels don't get a stack trace. Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end { :waitforcert => nil, :detailed_exitcodes => false, :verbose => false, :debug => false, :centrallogs => false, :setdest => false, }.each do |opt,val| options[opt] = val end @args = {} end option("--centrallogging") option("--debug","-d") option("--verbose","-v") option("--detailed-exitcodes") do |arg| options[:detailed_exitcodes] = true end option("--logdest DEST", "-l DEST") do |arg| handle_logdest_arg(arg) end option("--waitforcert WAITFORCERT", "-w") do |arg| options[:waitforcert] = arg.to_i end option("--port PORT","-p") do |arg| @args[:Port] = arg end def help <<-'HELP' puppet-device(8) -- Manage remote network devices ======== SYNOPSIS -------- Retrieves all configurations from the puppet master and apply them to the remote devices configured in /etc/puppet/device.conf. Currently must be run out periodically, using cron or something similar. USAGE ----- puppet device [-d|--debug] [--detailed-exitcodes] [-V|--version] [-h|--help] [-l|--logdest syslog||console] [-v|--verbose] [-w|--waitforcert ] DESCRIPTION ----------- Once the client has a signed certificate for a given remote device, it will retrieve its configuration and apply it. USAGE NOTES ----------- One need a /etc/puppet/device.conf file with the following content: [remote.device.fqdn] type url where: * type: the current device type (the only value at this time is cisco) * url: an url allowing to connect to the device Supported url must conforms to: scheme://user:password@hostname/?query with: * scheme: either ssh or telnet * user: username, can be omitted depending on the switch/router configuration * password: the connection password * query: this is device specific. Cisco devices supports an enable parameter whose value would be the enable password. OPTIONS ------- Note that any setting that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' as an argument. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit - code of '2' means there were changes, an exit code of '4' means there were - failures during the transaction, and an exit code of '6' means there were both - changes and failures. + code of '1' means at least one device had a compile failure, an exit code of + '2' means at least one device had resource changes, and an exit code of '4' + means at least one device had resource failures. Exit codes of '3', '5', '6', + or '7' means that a bitwise combination of the preceeding exit codes happened. * --help: Print this help message * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to syslog, or the console if debugging or verbosity is enabled. * --verbose: Turn on verbose reporting. * --waitforcert: This option only matters for daemons that do not yet have certificates and it is enabled by default, with a value of 120 (seconds). This causes +puppet agent+ to connect to the server every 2 minutes and ask it to sign a certificate request. This is useful for the initial setup of a puppet client. You can turn off waiting for certificates by specifying a time of 0. EXAMPLE ------- $ puppet device --server puppet.domain.com AUTHOR ------ Brice Figureau COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def main vardir = Puppet[:vardir] confdir = Puppet[:confdir] certname = Puppet[:certname] # find device list require 'puppet/util/network_device/config' devices = Puppet::Util::NetworkDevice::Config.devices if devices.empty? Puppet.err "No device found in #{Puppet[:deviceconfig]}" exit(1) end - devices.each_value do |device| + returns = devices.collect do |devicename,device| begin Puppet.info "starting applying configuration to #{device.name} at #{device.url}" # override local $vardir and $certname Puppet[:confdir] = ::File.join(Puppet[:devicedir], device.name) Puppet[:vardir] = ::File.join(Puppet[:devicedir], device.name) Puppet[:certname] = device.name # this will reload and recompute default settings and create the devices sub vardir, or we hope so :-) Puppet.settings.use :main, :agent, :ssl # this init the device singleton, so that the facts terminus # and the various network_device provider can use it Puppet::Util::NetworkDevice.init(device) # ask for a ssl cert if needed, but at least # setup the ssl system for this device. setup_host require 'puppet/configurer' configurer = Puppet::Configurer.new configurer.run(:network_device => true, :pluginsync => Puppet[:pluginsync]) rescue => detail Puppet.log_exception(detail) + # If we rescued an error, then we return 1 as the exit code + 1 ensure Puppet[:vardir] = vardir Puppet[:confdir] = confdir Puppet[:certname] = certname Puppet::SSL::Host.reset end end + if ! returns or returns.compact.empty? + exit(1) + elsif options[:detailed_exitcodes] + # Bitwise OR the return codes together, puppet style + exit(returns.compact.reduce(:|)) + elsif returns.include? 1 + exit(1) + else + exit(0) + end end def setup_host @host = Puppet::SSL::Host.new waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert]) @host.wait_for_cert(waitforcert) end def setup setup_logs args[:Server] = Puppet[:server] if options[:centrallogs] logdest = args[:Server] logdest += ":" + args[:Port] if args.include?(:Port) Puppet::Util::Log.newdestination(logdest) end Puppet.settings.use :main, :agent, :device, :ssl # Always ignoreimport for agent. It really shouldn't even try to import, # but this is just a temporary band-aid. Puppet[:ignoreimport] = true # We need to specify a ca location for all of the SSL-related # indirected classes to work; in fingerprint mode we just need # access to the local files and we don't need a ca. Puppet::SSL::Host.ca_location = :remote Puppet::Transaction::Report.indirection.terminus_class = :rest if Puppet[:catalog_cache_terminus] Puppet::Resource::Catalog.indirection.cache_class = Puppet[:catalog_cache_terminus].intern end end end diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb index 25a4f83de..c8505b74c 100755 --- a/spec/unit/application/device_spec.rb +++ b/spec/unit/application/device_spec.rb @@ -1,415 +1,448 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/application/device' require 'puppet/util/network_device/config' require 'ostruct' require 'puppet/configurer' describe Puppet::Application::Device do include PuppetSpec::Files before :each do @device = Puppet::Application[:device] @device.preinit Puppet::Util::Log.stubs(:newdestination) Puppet::Node.indirection.stubs(:terminus_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) end it "should operate in agent run_mode" do @device.class.run_mode.name.should == :agent end it "should declare a main command" do @device.should respond_to(:main) end it "should declare a preinit block" do @device.should respond_to(:preinit) end describe "in preinit" do before :each do @device.stubs(:trap) end it "should catch INT" do Signal.expects(:trap).with { |arg,block| arg == :INT } @device.preinit end it "should init waitforcert to nil" do @device.preinit @device.options[:waitforcert].should be_nil end end describe "when handling options" do before do @device.command_line.stubs(:args).returns([]) end [:centrallogging, :debug, :verbose,].each do |option| it "should declare handle_#{option} method" do @device.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @device.options.expects(:[]=).with(option, 'arg') @device.send("handle_#{option}".to_sym, 'arg') end end it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do Puppet[:onetime] = true Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(0) @device.setup_host end it "should use supplied waitforcert when --onetime is specified" do Puppet[:onetime] = true @device.handle_waitforcert(60) Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(60) @device.setup_host end it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(120) @device.setup_host end it "should use the waitforcert setting when checking for a signed certificate" do Puppet[:waitforcert] = 10 Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(10) @device.setup_host end it "should set the log destination with --logdest" do @device.options.stubs(:[]=).with { |opt,val| opt == :setdest } Puppet::Log.expects(:newdestination).with("console") @device.handle_logdest("console") end it "should put the setdest options to true" do @device.options.expects(:[]=).with(:setdest,true) @device.handle_logdest("console") end it "should parse the log destination from the command line" do @device.command_line.stubs(:args).returns(%w{--logdest /my/file}) Puppet::Util::Log.expects(:newdestination).with("/my/file") @device.parse_options end it "should store the waitforcert options with --waitforcert" do @device.options.expects(:[]=).with(:waitforcert,42) @device.handle_waitforcert("42") end it "should set args[:Port] with --port" do @device.handle_port("42") @device.args[:Port].should == "42" end end describe "during setup" do before :each do @device.options.stubs(:[]) Puppet.stubs(:info) Puppet[:libdir] = "/dev/null/lib" Puppet::SSL::Host.stubs(:ca_location=) Puppet::Transaction::Report.indirection.stubs(:terminus_class=) Puppet::Resource::Catalog.indirection.stubs(:terminus_class=) Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) @host = stub_everything 'host' Puppet::SSL::Host.stubs(:new).returns(@host) Puppet.stubs(:settraps) end it "should call setup_logs" do @device.expects(:setup_logs) @device.setup end describe "when setting up logs" do before :each do Puppet::Util::Log.stubs(:newdestination) end it "should set log level to debug if --debug was passed" do @device.options.stubs(:[]).with(:debug).returns(true) @device.setup_logs Puppet::Util::Log.level.should == :debug end it "should set log level to info if --verbose was passed" do @device.options.stubs(:[]).with(:verbose).returns(true) @device.setup_logs Puppet::Util::Log.level.should == :info end [:verbose, :debug].each do |level| it "should set console as the log destination with level #{level}" do @device.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @device.setup_logs end end it "should set a default log destination if no --logdest" do @device.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:setup_default) @device.setup_logs end end it "should set a central log destination with --centrallogs" do @device.options.stubs(:[]).with(:centrallogs).returns(true) Puppet[:server] = "puppet.reductivelabs.com" Puppet::Util::Log.stubs(:newdestination).with(:syslog) Puppet::Util::Log.expects(:newdestination).with("puppet.reductivelabs.com") @device.setup end it "should use :main, :agent, :device and :ssl config" do Puppet.settings.expects(:use).with(:main, :agent, :device, :ssl) @device.setup end it "should install a remote ca location" do Puppet::SSL::Host.expects(:ca_location=).with(:remote) @device.setup end it "should tell the report handler to use REST" do Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest) @device.setup end it "should default the catalog_terminus setting to 'rest'" do @device.initialize_app_defaults Puppet[:catalog_terminus].should == :rest end it "should default the node_terminus setting to 'rest'" do @device.initialize_app_defaults Puppet[:node_terminus].should == :rest end it "has an application default :catalog_cache_terminus setting of 'json'" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:json) @device.initialize_app_defaults @device.setup end it "should tell the catalog cache class based on the :catalog_cache_terminus setting" do Puppet[:catalog_cache_terminus] = "yaml" Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml) @device.initialize_app_defaults @device.setup end it "should not set catalog cache class if :catalog_cache_terminus is explicitly nil" do Puppet[:catalog_cache_terminus] = nil Puppet::Resource::Catalog.indirection.expects(:cache_class=).never @device.initialize_app_defaults @device.setup end it "should default the facts_terminus setting to 'network_device'" do @device.initialize_app_defaults Puppet[:facts_terminus].should == :network_device end end describe "when initializing each devices SSL" do before(:each) do @host = stub_everything 'host' Puppet::SSL::Host.stubs(:new).returns(@host) end it "should create a new ssl host" do Puppet::SSL::Host.expects(:new).returns(@host) @device.setup_host end it "should wait for a certificate" do @device.options.stubs(:[]).with(:waitforcert).returns(123) @host.expects(:wait_for_cert).with(123) @device.setup_host end end describe "when running" do before :each do @device.options.stubs(:[]).with(:fingerprint).returns(false) Puppet.stubs(:notice) + @device.options.stubs(:[]).with(:detailed_exitcodes).returns(false) @device.options.stubs(:[]).with(:client) Puppet::Util::NetworkDevice::Config.stubs(:devices).returns({}) end it "should dispatch to main" do @device.stubs(:main) @device.run_command end it "should get the device list" do device_hash = stub_everything 'device hash' Puppet::Util::NetworkDevice::Config.expects(:devices).returns(device_hash) - @device.main + expect { @device.main }.to exit_with 1 end it "should exit if the device list is empty" do expect { @device.main }.to exit_with 1 end describe "for each device" do before(:each) do Puppet[:vardir] = make_absolute("/dummy") Puppet[:confdir] = make_absolute("/dummy") Puppet[:certname] = "certname" @device_hash = { "device1" => OpenStruct.new(:name => "device1", :url => "url", :provider => "cisco"), "device2" => OpenStruct.new(:name => "device2", :url => "url", :provider => "cisco"), } Puppet::Util::NetworkDevice::Config.stubs(:devices).returns(@device_hash) Puppet.stubs(:[]=) Puppet.settings.stubs(:use) @device.stubs(:setup_host) Puppet::Util::NetworkDevice.stubs(:init) @configurer = stub_everything 'configurer' Puppet::Configurer.stubs(:new).returns(@configurer) end it "should set vardir to the device vardir" do Puppet.expects(:[]=).with(:vardir, make_absolute("/dummy/devices/device1")) - @device.main + expect { @device.main }.to exit_with 1 end it "should set confdir to the device confdir" do Puppet.expects(:[]=).with(:confdir, make_absolute("/dummy/devices/device1")) - @device.main + expect { @device.main }.to exit_with 1 end it "should set certname to the device certname" do Puppet.expects(:[]=).with(:certname, "device1") Puppet.expects(:[]=).with(:certname, "device2") - @device.main + expect { @device.main }.to exit_with 1 end it "should make sure all the required folders and files are created" do Puppet.settings.expects(:use).with(:main, :agent, :ssl).twice - @device.main + expect { @device.main }.to exit_with 1 end it "should initialize the device singleton" do Puppet::Util::NetworkDevice.expects(:init).with(@device_hash["device1"]).then.with(@device_hash["device2"]) - @device.main + expect { @device.main }.to exit_with 1 end it "should setup the SSL context" do @device.expects(:setup_host).twice - @device.main + expect { @device.main }.to exit_with 1 end it "should launch a configurer for this device" do @configurer.expects(:run).twice - @device.main + expect { @device.main }.to exit_with 1 + end + + it "exits 1 when configurer raises error" do + @configurer.stubs(:run).raises(Puppet::Error).then.returns(0) + expect { @device.main }.to exit_with 1 + end + + it "exits 0 when run happens without puppet errors but with failed run" do + @configurer.stubs(:run).returns(6,2) + expect { @device.main }.to exit_with 0 + end + + it "exits 2 when --detailed-exitcodes and successful runs" do + @device.options.stubs(:[]).with(:detailed_exitcodes).returns(true) + @configurer.stubs(:run).returns(0,2) + expect { @device.main }.to exit_with 2 + end + + it "exits 1 when --detailed-exitcodes and failed parse" do + @configurer = stub_everything 'configurer' + Puppet::Configurer.stubs(:new).returns(@configurer) + @device.options.stubs(:[]).with(:detailed_exitcodes).returns(true) + @configurer.stubs(:run).returns(6,1) + expect { @device.main }.to exit_with 7 + end + + it "exits 6 when --detailed-exitcodes and failed run" do + @configurer = stub_everything 'configurer' + Puppet::Configurer.stubs(:new).returns(@configurer) + @device.options.stubs(:[]).with(:detailed_exitcodes).returns(true) + @configurer.stubs(:run).returns(6,2) + expect { @device.main }.to exit_with 6 end [:vardir, :confdir].each do |setting| it "should cleanup the #{setting} setting after the run" do all_devices = Set.new(@device_hash.keys.map do |device_name| make_absolute("/dummy/devices/#{device_name}") end) found_devices = Set.new() # a block to use in a few places later to validate the updated settings p = Proc.new do |my_setting, my_value| if my_setting == setting && all_devices.include?(my_value) found_devices.add(my_value) true else false end end seq = sequence("clean up dirs") all_devices.size.times do ## one occurrence of set / run / set("/dummy") for each device Puppet.expects(:[]=).with(&p).in_sequence(seq) @configurer.expects(:run).in_sequence(seq) Puppet.expects(:[]=).with(setting, make_absolute("/dummy")).in_sequence(seq) end - @device.main + expect { @device.main }.to exit_with 1 expect(found_devices).to eq(all_devices) end end it "should cleanup the certname setting after the run" do all_devices = Set.new(@device_hash.keys) found_devices = Set.new() # a block to use in a few places later to validate the updated settings p = Proc.new do |my_setting, my_value| if my_setting == :certname && all_devices.include?(my_value) found_devices.add(my_value) true else false end end seq = sequence("clean up certname") all_devices.size.times do ## one occurrence of set / run / set("certname") for each device Puppet.expects(:[]=).with(&p).in_sequence(seq) @configurer.expects(:run).in_sequence(seq) Puppet.expects(:[]=).with(:certname, "certname").in_sequence(seq) end - @device.main + expect { @device.main }.to exit_with 1 # make sure that we were called with each of the defined devices expect(found_devices).to eq(all_devices) end it "should expire all cached attributes" do Puppet::SSL::Host.expects(:reset).twice - @device.main + expect { @device.main }.to exit_with 1 end end end end