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.

Reply via email to