diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb index 854188ad1..f3941b3dc 100644 --- a/lib/puppet/application/device.rb +++ b/lib/puppet/application/device.rb @@ -1,238 +1,242 @@ 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. * --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}" + device_url = URI.parse(device.url) + # Handle nil scheme & port + scheme = "#{device_url.scheme}://" if device_url.scheme + port = ":#{device_url.port}" if device_url.port + Puppet.info "starting applying configuration to #{device.name} at #{scheme}#{device_url.host}#{port}#{device_url.path}" # 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) ensure Puppet[:vardir] = vardir Puppet[:confdir] = confdir Puppet[:certname] = certname Puppet::SSL::Host.reset end 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/lib/puppet/util/network_device/config.rb b/lib/puppet/util/network_device/config.rb index 2d6283c64..3c40b6a93 100644 --- a/lib/puppet/util/network_device/config.rb +++ b/lib/puppet/util/network_device/config.rb @@ -1,93 +1,98 @@ require 'ostruct' require 'puppet/util/watched_file' require 'puppet/util/network_device' class Puppet::Util::NetworkDevice::Config def self.main @main ||= self.new end def self.devices main.devices || [] end attr_reader :devices def exists? Puppet::FileSystem.exist?(@file.to_str) end def initialize @file = Puppet::Util::WatchedFile.new(Puppet[:deviceconfig]) @devices = {} read(true) # force reading at start end # Read the configuration file. def read(force = false) return unless exists? parse if force or @file.changed? end private def parse begin devices = {} device = nil File.open(@file) { |f| count = 1 f.each { |line| case line when /^\s*#/ # skip comments count += 1 next when /^\s*$/ # skip blank lines count += 1 next when /^\[([\w.-]+)\]\s*$/ # [device.fqdn] name = $1 name.chomp! raise Puppet::Error, "Duplicate device found at line #{count}, already found at #{device.line}" if devices.include?(name) device = OpenStruct.new device.name = name device.line = count device.options = { :debug => false } Puppet.debug "found device: #{device.name} at #{device.line}" devices[name] = device when /^\s*(type|url|debug)(\s+(.+))*$/ parse_directive(device, $1, $3, count) else raise Puppet::Error, "Invalid line #{count}: #{line}" end count += 1 } } rescue Errno::EACCES => detail Puppet.err "Configuration error: Cannot read #{@file}; cannot serve" #raise Puppet::Error, "Cannot read #{@config}" rescue Errno::ENOENT => detail Puppet.err "Configuration error: '#{@file}' does not exit; cannot serve" end @devices = devices end def parse_directive(device, var, value, count) case var when "type" device.provider = value when "url" + begin + URI.parse(value) + rescue URI::InvalidURIError + raise Puppet::Error, "#{value} is an invalid url" + end device.url = value when "debug" device.options[:debug] = true else raise Puppet::Error, "Invalid argument '#{var}' at line #{count}" end end end diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb index 25a4f83de..150913ef8 100755 --- a/spec/unit/application/device_spec.rb +++ b/spec/unit/application/device_spec.rb @@ -1,415 +1,420 @@ #! /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(: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 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"), + "device1" => OpenStruct.new(:name => "device1", :url => "ssh://user:pass@testhost", :provider => "cisco"), + "device2" => OpenStruct.new(:name => "device2", :url => "https://user:pass@testhost/some/path", :provider => "rest"), } 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 end it "should set confdir to the device confdir" do Puppet.expects(:[]=).with(:confdir, make_absolute("/dummy/devices/device1")) @device.main end it "should set certname to the device certname" do Puppet.expects(:[]=).with(:certname, "device1") Puppet.expects(:[]=).with(:certname, "device2") @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 print the device url scheme, host, and port" do + Puppet.expects(:info).with "starting applying configuration to device1 at ssh://testhost" + Puppet.expects(:info).with "starting applying configuration to device2 at https://testhost:443/some/path" + @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 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(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 # 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 end end end end diff --git a/spec/unit/util/network_device/config_spec.rb b/spec/unit/util/network_device/config_spec.rb index 06daf96fc..e6276d0bc 100755 --- a/spec/unit/util/network_device/config_spec.rb +++ b/spec/unit/util/network_device/config_spec.rb @@ -1,86 +1,92 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/network_device/config' describe Puppet::Util::NetworkDevice::Config do include PuppetSpec::Files before(:each) do Puppet[:deviceconfig] = tmpfile('deviceconfig') end describe "when parsing device" do let(:config) { Puppet::Util::NetworkDevice::Config.new } def write_device_config(*lines) File.open(Puppet[:deviceconfig], 'w') {|f| f.puts lines} end it "should skip comments" do write_device_config(' # comment') config.devices.should be_empty end it "should increment line number even on commented lines" do write_device_config(' # comment','[router.puppetlabs.com]') config.devices.should be_include('router.puppetlabs.com') end it "should skip blank lines" do write_device_config(' ') config.devices.should be_empty end it "should produce the correct line number" do write_device_config(' ', '[router.puppetlabs.com]') config.devices['router.puppetlabs.com'].line.should == 2 end it "should throw an error if the current device already exists" do write_device_config('[router.puppetlabs.com]', '[router.puppetlabs.com]') end it "should accept device certname containing dashes" do write_device_config('[router-1.puppetlabs.com]') config.devices.should include('router-1.puppetlabs.com') end it "should create a new device for each found device line" do write_device_config('[router.puppetlabs.com]', '[swith.puppetlabs.com]') config.devices.size.should == 2 end it "should parse the device type" do write_device_config('[router.puppetlabs.com]', 'type cisco') config.devices['router.puppetlabs.com'].provider.should == 'cisco' end it "should parse the device url" do write_device_config('[router.puppetlabs.com]', 'type cisco', 'url ssh://test/') config.devices['router.puppetlabs.com'].url.should == 'ssh://test/' end + it "should error with a malformed device url" do + write_device_config('[router.puppetlabs.com]', 'type cisco', 'url ssh://test node/') + + expect { config.devices['router.puppetlabs.com'] }.to raise_error Puppet::Error + end + it "should parse the debug mode" do write_device_config('[router.puppetlabs.com]', 'type cisco', 'url ssh://test/', 'debug') config.devices['router.puppetlabs.com'].options.should == { :debug => true } end it "should set the debug mode to false by default" do write_device_config('[router.puppetlabs.com]', 'type cisco', 'url ssh://test/') config.devices['router.puppetlabs.com'].options.should == { :debug => false } end end end