This patch introduces a new application in the puppet ecosystem: puppet device.
This application parses the /etc/puppet/device.conf file and for each device asks a catalog to the master, and applies it to the remote device. Signed-off-by: Brice Figureau <[email protected]> --- lib/puppet/application/device.rb | 255 +++++++++++++++++++++++++ lib/puppet/defaults.rb | 5 + lib/puppet/util/command_line.rb | 3 +- spec/unit/application/device_spec.rb | 349 ++++++++++++++++++++++++++++++++++ 4 files changed, 611 insertions(+), 1 deletions(-) create mode 100644 lib/puppet/application/device.rb create mode 100644 spec/unit/application/device_spec.rb diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb new file mode 100644 index 0000000..9dc6b26 --- /dev/null +++ b/lib/puppet/application/device.rb @@ -0,0 +1,255 @@ +require 'puppet/application' +require 'puppet/util/network_device' + + +class Puppet::Application::Device < Puppet::Application + + should_parse_config + run_mode :agent + + attr_accessor :args, :agent, :host + + def preinit + # Do an initial trap, so that cancels don't get a stack trace. + 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| + begin + Puppet::Util::Log.newdestination(arg) + options[:setdest] = true + rescue => detail + puts detail.backtrace if Puppet[:debug] + $stderr.puts detail.to_s + end + 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|<file>|console] + [-v|--verbose] [-w|--waitforcert <seconds>] + + +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 <type> +url <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 configuration parameter 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 <servername>' 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, and an exit code of '4' means + that there were failures during the transaction. This option only makes + sense in conjunction with --onetime. + +* --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| + begin + Puppet.info "starting applying configuration to #{device.name} at #{device.url}" + + # override local $vardir and $certname + Puppet.settings.set_value(:confdir, File.join(Puppet[:devicedir], device.name), :cli) + Puppet.settings.set_value(:vardir, File.join(Puppet[:devicedir], device.name), :cli) + Puppet.settings.set_value(:certname, device.name, :cli) + + # 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 + report = configurer.run + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err detail.to_s + ensure + Puppet.settings.set_value(:vardir, vardir, :cli) + Puppet.settings.set_value(:confdir, confdir, :cli) + Puppet.settings.set_value(:certname, certname, :cli) + end + end + end + + # Handle the logging settings. + def setup_logs + 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 + + Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] + end + + def setup_host + @host = Puppet::SSL::Host.new + waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : 120) + cert = @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 i + # 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 + + # Override the default; puppetd needs this, usually. + # You can still override this on the command-line with, e.g., :compiler. + Puppet[:catalog_terminus] = :rest + + Puppet[:facts_terminus] = :network_device + + Puppet::Resource::Catalog.indirection.cache_class = :yaml + end +end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 680762b..dbd5a94 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -487,6 +487,11 @@ module Puppet This should match how often the hosts report back to the server."] ) + setdefaults(:device, + :devicedir => {:default => "$vardir/devices", :mode => "750", :desc => "The root directory of devices' $vardir"}, + :deviceconfig => ["$confdir/device.conf","Path to the device config file for puppet device"] + ) + setdefaults(:agent, :localconfig => { :default => "$statedir/localconfig", :owner => "root", diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index a884b86..714d03f 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -14,7 +14,8 @@ module Puppet 'queue' => 'puppetqd', 'resource' => 'ralsh', 'kick' => 'puppetrun', - 'master' => 'puppetmasterd' + 'master' => 'puppetmasterd', + 'device' => 'puppetdevice' ) def initialize(zero = $0, argv = ARGV, stdin = STDIN) diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb new file mode 100644 index 0000000..abdee95 --- /dev/null +++ b/spec/unit/application/device_spec.rb @@ -0,0 +1,349 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/application/device' +require 'puppet/util/network_device/config' +require 'ostruct' +require 'puppet/configurer' + +describe Puppet::Application::Device do + before :each do + @device = Puppet::Application[:device] +# @device.stubs(:puts) + @device.preinit + Puppet::Util::Log.stubs(:newdestination) + Puppet::Util::Log.stubs(:level=) + + Puppet::Node.stubs(:terminus_class=) + Puppet::Node.stubs(:cache_class=) + Puppet::Node::Facts.stubs(:terminus_class=) + end + + it "should operate in agent run_mode" do + @device.class.run_mode.name.should == :agent + end + + it "should ask Puppet::Application to parse Puppet configuration file" do + @device.should_parse_config?.should be_true + 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 + @device.expects(:trap).with { |arg,block| arg == :INT } + + @device.preinit + 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 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) + FileTest.stubs(:exists?).returns(true) + Puppet[:libdir] = "/dev/null/lib" + Puppet::SSL::Host.stubs(:ca_location=) + Puppet::Transaction::Report.stubs(:terminus_class=) + Puppet::Resource::Catalog.stubs(:terminus_class=) + Puppet::Resource::Catalog.stubs(:cache_class=) + Puppet::Node::Facts.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) + + Puppet::Util::Log.expects(:level=).with(:debug) + + @device.setup_logs + end + + it "should set log level to info if --verbose was passed" do + @device.options.stubs(:[]).with(:verbose).returns(true) + + Puppet::Util::Log.expects(:level=).with(:info) + + @device.setup_logs + 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 syslog as the log destination if no --logdest" do + @device.options.stubs(:[]).with(:setdest).returns(false) + + Puppet::Util::Log.expects(:newdestination).with(:syslog) + + @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 change the catalog_terminus setting to 'rest'" do + Puppet[:catalog_terminus] = :foo + @device.setup + Puppet[:catalog_terminus].should == :rest + end + + it "should tell the catalog handler to use cache" do + Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml) + + @device.setup + end + + it "should change the facts_terminus setting to 'network_device'" do + Puppet[:facts_terminus] = :foo + + @device.setup + 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(: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 + end + + it "should exit if the device list is empty" do + @device.expects(:exit).with(1) + @device.main + end + + describe "for each device" do + before(:each) do + Puppet[:vardir] = "/dummy" + Puppet[:confdir] = "/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.settings.stubs(:set_value) + 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.settings.expects(:set_value).with(:vardir, "/dummy/devices/device1", :cli) + @device.main + end + + it "should set confdir to the device confdir" do + Puppet.settings.expects(:set_value).with(:confdir, "/dummy/devices/device1", :cli) + @device.main + end + + it "should set certname to the device certname" do + Puppet.settings.expects(:set_value).with(:certname, "device1", :cli) + Puppet.settings.expects(:set_value).with(:certname, "device2", :cli) + @device.main + 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 + end + + it "should initialize the device singleton" do + Puppet::Util::NetworkDevice.expects(:init).with(@device_hash["device1"]).then.with(@device_hash["device2"]) + @device.main + end + + it "should setup the SSL context" do + @device.expects(:setup_host).twice + @device.main + end + + it "should launch a configurer for this device" do + @configurer.expects(:run).twice + @device.main + end + + [:vardir, :confdir].each do |setting| + it "should cleanup the #{setting} setting after the run" do + configurer = states('configurer').starts_as('notrun') + Puppet.settings.expects(:set_value).with(setting, "/dummy/devices/device1", :cli).when(configurer.is('notrun')) + @configurer.expects(:run).then(configurer.is('run')) + Puppet.settings.expects(:set_value).with(setting, "/dummy", :cli).when(configurer.is('run')) + + @device.main + end + end + + it "should cleanup the certname setting after the run" do + configurer = states('configurer').starts_as('notrun') + Puppet.settings.expects(:set_value).with(:certname, "device1", :cli).when(configurer.is('notrun')) + @configurer.expects(:run).then(configurer.is('run')) + Puppet.settings.expects(:set_value).with(:certname, "certname", :cli).when(configurer.is('run')) + + @device.main + end + + end + end +end -- 1.7.2.1 -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.
