[Pkg-puppet-devel] [SCM] Puppet packaging for Debian branch, experimental, updated. debian/2.6.8-1-844-g7ec39d5

James Turnbull james at lovedthanlost.net
Tue May 10 08:11:43 UTC 2011


The following commit has been merged in the experimental branch:
commit 1cb18410732a4b51efa0a106d4a1437daef08fc5
Author: Brice Figureau <brice-puppet at daysofwonder.com>
Date:   Mon Jan 3 19:47:03 2011 +0100

    Cisco Switch/Router Interface management
    
    This patch introduces managing remotely cisco IOS network devices
    through ssh or telnet with a puppet type/provider.
    
    This patch allows to manage router/switch interface
    with the interface type:
    
    interface {
    	"FastEthernet 0/1":
    		device_url => "ssh://user:pass@cisco2960.domain.com/",
    		mode => trunk,
    		encapsulation => dot1q,
    		trunk_allowed_vlans => "1-99,200,253",
    		description => "to back bone router"
    }
    
    It is possible with this patch to set interface:
     * mode (access or trunk)
     * native vlan (only for access mode)
     * speed (auto or a given speed)
     * duplex (auto, half or full)
     * trunk encapsulation
     * allowed trunk vlan
     * ipv4 addresses
     * ipv6 addresses
     * etherchannel membership
    
    The interface name (at least for the cisco provider) can be any
    shorthand interface name a switch or router can use.
    
    The device url should conform to:
     * scheme: either telnet or ssh
     * user: can be absent depending on switch/router line config
     * pass: must be present
     * port: optional
     * an optional enable password can be mentioned in the url query string
    
    Ex:
    To connect to a switch with a line password and an enable password:
    "telnet://:letmein@cisco29224XL.domain.com/?enable=letmeinagain"
    
    To connect to a switch/router through ssh and a privileged user:
    "ssh://brice:letmein@cisco1841L.domain.com/"
    
    Note:
    This patch only includes a Cisco IOS provider. Also terminology adopted
    in the various types are mostly the ones used in Cisco devices.
    This patch was tested against:
    * (really old) Cisco switch 2924XL with ios 12.0(5)WC10
    * Cisco router 1841 with ios 12.4(15)T8
    * Cisco router 877 with ios 12.4(11)XJ4
    * Cisco switch 2960G with ios 12.2(44)SE
    
    Signed-off-by: Brice Figureau <brice-puppet at daysofwonder.com>

diff --git a/examples/etc/otherfile b/lib/puppet/provider/interface/base.rb
similarity index 100%
copy from examples/etc/otherfile
copy to lib/puppet/provider/interface/base.rb
diff --git a/lib/puppet/provider/interface/cisco.rb b/lib/puppet/provider/interface/cisco.rb
new file mode 100644
index 0000000..f3bd202
--- /dev/null
+++ b/lib/puppet/provider/interface/cisco.rb
@@ -0,0 +1,33 @@
+require 'puppet/util/network_device/cisco/device'
+require 'puppet/provider/network_device'
+
+Puppet::Type.type(:interface).provide :cisco, :parent => Puppet::Provider::NetworkDevice do
+
+  desc "Cisco switch/router provider for interface."
+
+  mk_resource_methods
+
+  def self.lookup(url, name)
+    interface = nil
+    network_gear = Puppet::Util::NetworkDevice::Cisco::Device.new(url)
+    network_gear.command do |ng|
+      interface = network_gear.interface(name)
+    end
+    interface
+  end
+
+  def initialize(*args)
+    super
+  end
+
+  def flush
+    device.command do |device|
+      device.new_interface(name).update(former_properties, properties)
+    end
+    super
+  end
+
+  def device
+    @device ||= Puppet::Util::NetworkDevice::Cisco::Device.new(resource[:device_url])
+  end
+end
diff --git a/lib/puppet/type/interface.rb b/lib/puppet/type/interface.rb
new file mode 100644
index 0000000..7560a05
--- /dev/null
+++ b/lib/puppet/type/interface.rb
@@ -0,0 +1,107 @@
+#
+# Manages an interface on a given router or switch
+#
+
+require 'puppet/util/network_device/ipcalc'
+
+Puppet::Type.newtype(:interface) do
+
+    @doc = "This represents a router or switch interface. It is possible to manage
+    interface mode (access or trunking, native vlan and encapsulation), 
+    switchport characteristics (speed, duplex)."
+
+    ensurable do
+      defaultvalues
+
+      aliasvalue :shutdown, :absent
+      aliasvalue :no_shutdown, :present
+
+      defaultto { :no_shutdown }
+    end
+
+    newparam(:name) do
+      desc "Interface name"
+    end
+
+    newparam(:device_url) do
+      desc "Url to connect to a router or switch."
+    end
+
+    newproperty(:description) do
+      desc "Interface description."
+
+      defaultto { @resource[:name] }
+    end
+
+    newproperty(:speed) do
+      desc "Interface speed."
+      newvalues(:auto, /^\d+/)
+    end
+
+    newproperty(:duplex) do
+      desc "Interface duplex."
+      newvalues(:auto, :full, :half)
+    end
+
+    newproperty(:native_vlan) do
+      desc "Interface native vlan (for access mode only)."
+      newvalues(/^\d+/)
+    end
+
+    newproperty(:encapsulation) do
+      desc "Interface switchport encapsulation."
+      newvalues(:none, :dot1q, :isl )
+    end
+
+    newproperty(:mode) do
+      desc "Interface switchport mode."
+      newvalues(:access, :trunk)
+    end
+
+    newproperty(:allowed_trunk_vlans) do
+      desc "Allowed list of Vlans that this trunk can forward."
+      newvalues(:all, /./)
+    end
+
+    newproperty(:etherchannel) do
+      desc "Channel group this interface is part of."
+      newvalues(/^\d+/)
+    end
+
+    newproperty(:ipaddress, :array_matching => :all) do
+      include Puppet::Util::NetworkDevice::IPCalc
+
+      desc "IP Address of this interface (it might not be possible to set an interface IP address 
+      it depends on the interface type and device type).
+      Valid format of ip addresses are:
+       * IPV4, like 127.0.0.1
+       * IPV4/prefixlength like 127.0.1.1/24
+       * IPV6/prefixlength like FE80::21A:2FFF:FE30:ECF0/128
+       * an optional suffix for IPV6 addresses from this list: eui-64, link-local
+      It is also possible to use an array of values. 
+      "
+
+      validate do |values|
+        values = [values] unless values.is_a?(Array)
+        values.each do |value|
+          self.fail "Invalid interface ip address" unless parse(value.gsub(/\s*(eui-64|link-local)\s*$/,''))
+        end
+      end
+
+      munge do |value|
+        option = value =~ /eui-64|link-local/i ? value.gsub(/^.*?\s*(eui-64|link-local)\s*$/,'\1') : nil
+        [parse(value.gsub(/\s*(eui-64|link-local)\s*$/,'')), option].flatten
+      end
+
+      def value_to_s(value)
+        value = [value] unless value.is_a?(Array)
+        value.map{ |v| "#{v[1].to_s}/#{v[0]} #{v[2]}"}.join(",")
+      end
+
+      def change_to_s(currentvalue, newvalue)
+        currentvalue = value_to_s(currentvalue) if currentvalue != :absent
+        newvalue = value_to_s(newvalue)
+        super(currentvalue, newvalue)
+      end
+    end
+end
diff --git a/lib/puppet/type/router.rb b/lib/puppet/type/router.rb
new file mode 100644
index 0000000..648389d
--- /dev/null
+++ b/lib/puppet/type/router.rb
@@ -0,0 +1,14 @@
+#
+# Manage a router abstraction
+#
+
+module Puppet
+  newtype(:router) do
+    @doc = "Manages connected router."
+
+    newparam(:url) do
+      desc "An URL to access the router of the form (ssh|telnet)://user:pass:enable at host/."
+      isnamevar
+    end
+  end
+end
diff --git a/lib/puppet/util/network_device.rb b/lib/puppet/util/network_device.rb
new file mode 100644
index 0000000..bca6601
--- /dev/null
+++ b/lib/puppet/util/network_device.rb
@@ -0,0 +1,2 @@
+module Puppet::Util::NetworkDevice
+end
\ No newline at end of file
diff --git a/lib/puppet/util/network_device/base.rb b/lib/puppet/util/network_device/base.rb
new file mode 100644
index 0000000..ff96c86
--- /dev/null
+++ b/lib/puppet/util/network_device/base.rb
@@ -0,0 +1,29 @@
+require 'puppet/util/autoload'
+require 'uri'
+require 'puppet/util/network_device/transport'
+require 'puppet/util/network_device/transport/base'
+
+module Puppet::Util::NetworkDevice
+  class Base
+
+    attr_accessor :url, :transport
+
+    def initialize(url)
+      @url = URI.parse(url)
+
+      @autoloader = Puppet::Util::Autoload.new(
+        self,
+        "puppet/util/network_device/transport",
+        :wrap => false
+      )
+
+      if @autoloader.load(@url.scheme)
+        @transport = Puppet::Util::NetworkDevice::Transport.const_get(@url.scheme.capitalize).new
+        @transport.host = @url.host
+        @transport.port = @url.port || case @url.scheme ; when "ssh" ; 22 ; when "telnet" ; 23 ; end
+        @transport.user = @url.user
+        @transport.password = @url.password
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/lib/puppet/util/network_device/cisco.rb b/lib/puppet/util/network_device/cisco.rb
new file mode 100644
index 0000000..c03a001
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco.rb
@@ -0,0 +1,4 @@
+
+module Puppet::Util::NetworkDevice::Cisco
+
+end
\ No newline at end of file
diff --git a/lib/puppet/util/network_device/cisco/device.rb b/lib/puppet/util/network_device/cisco/device.rb
new file mode 100644
index 0000000..97489bd
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco/device.rb
@@ -0,0 +1,225 @@
+require 'puppet'
+require 'puppet/util'
+require 'puppet/util/network_device/base'
+require 'puppet/util/network_device/ipcalc'
+require 'puppet/util/network_device/cisco/interface'
+require 'ipaddr'
+
+class Puppet::Util::NetworkDevice::Cisco::Device < Puppet::Util::NetworkDevice::Base
+
+  include Puppet::Util::NetworkDevice::IPCalc
+
+  attr_accessor :enable_password
+
+  def initialize(url, options = {})
+    super(url)
+    @enable_password = options[:enable_password] || parse_enable(@url.query)
+    transport.default_prompt = /[#>]\s?\z/n
+  end
+
+  def parse_enable(query)
+    return $1 if query =~ /enable=(.*)/
+  end
+
+  def command(cmd=nil)
+    Puppet.debug("command #{cmd}")
+    transport.connect
+    login
+    transport.command("terminal length 0") do |out|
+      enable if out =~ />\s?\z/n
+    end
+    find_capabilities
+    out = execute(cmd) if cmd
+    yield self if block_given?
+    transport.close
+    out
+  end
+
+  def execute(cmd)
+    transport.command(cmd)
+  end
+
+  def login
+    return if transport.handles_login?
+    if @url.user != ''
+      transport.command(@url.user, :prompt => /^Password:/)
+    else
+      transport.expect(/^Password:/)
+    end
+    transport.command(@url.password)
+  end
+
+  def enable
+    raise "Can't issue \"enable\" to enter privileged, no enable password set" unless enable_password
+    transport.command("enable", :prompt => /^Password:/)
+    transport.command(enable_password)
+  end
+
+  def support_vlan_brief?
+    !! @support_vlan_brief
+  end
+
+  def find_capabilities
+    out = transport.command("sh vlan brief")
+    lines = out.split("\n")
+    lines.shift; lines.pop
+
+    @support_vlan_brief = ! (lines.first =~ /^%/)
+  end
+
+  IF={
+    :FastEthernet => %w{FastEthernet FastEth Fast FE Fa F},
+    :GigEthernet => %w{GigabitEthernet GigEthernet GigEth GE Gi G},
+    :Ethernet => %w{Ethernet Eth E},
+    :Serial => %w{Serial Se S},
+    :PortChannel => %w{PortChannel Port-Channel Po},
+    :POS => %w{POS P},
+    :VLAN => %w{VLAN VL V},
+    :Loopback => %w{Loopback Loop Lo},
+    :ATM => %w{ATM AT A},
+    :Dialer => %w{Dialer Dial Di D},
+    :VirtualAccess => %w{Virtual-Access Virtual-A Virtual Virt}
+  }
+
+  def canonalize_ifname(interface)
+    IF.each do |k,ifnames|
+      if found = ifnames.find { |ifname| interface =~ /^#{ifname}\s*\d/i }
+        interface =~ /^#{found}(.+)\b/i
+        return "#{k.to_s}#{$1}".gsub(/\s+/,'')
+      end
+    end
+    interface
+  end
+
+  def interface(name)
+    ifname = canonalize_ifname(name)
+    interface = parse_interface(ifname)
+    return { :ensure => :absent } if interface.empty?
+    interface.merge!(parse_trunking(ifname))
+    interface.merge!(parse_interface_config(ifname))
+  end
+
+  def new_interface(name)
+    Puppet::Util::NetworkDevice::Cisco::Interface.new(canonalize_ifname(name), transport)
+  end
+
+  def parse_interface(name)
+    resource = {}
+    out = transport.command("sh interface #{name}")
+    lines = out.split("\n")
+    lines.shift; lines.pop
+    lines.each do |l|
+      if l =~ /#{name} is (.+), line protocol is /
+        resource[:ensure] = ($1 == 'up' ? :present : :absent);
+      end
+      if l =~ /Auto Speed \(.+\),/ or l =~ /Auto Speed ,/ or l =~ /Auto-speed/
+        resource[:speed] = :auto
+      end
+      if l =~ /, (.+)Mb\/s/
+        resource[:speed] = $1
+      end
+      if l =~ /\s+Auto-duplex \((.{4})\),/
+        resource[:duplex] = :auto
+      end
+      if l =~ /\s+(.+)-duplex/
+        resource[:duplex] = $1 == "Auto" ? :auto : $1.downcase.to_sym
+      end
+      if l =~ /Description: (.+)/
+        resource[:description] = $1
+      end
+    end
+    resource
+  end
+
+  def parse_interface_config(name)
+    resource = Hash.new { |hash, key| hash[key] = Array.new ; }
+    out = transport.command("sh running-config interface #{name} | begin interface")
+    lines = out.split("\n")
+    lines.shift; lines.pop
+    lines.each do |l|
+      if l =~ /ip address (#{IP}) (#{IP})\s+secondary\s*$/
+        resource[:ipaddress] << [prefix_length(IPAddr.new($2)), IPAddr.new($1), 'secondary']
+      end
+      if l =~ /ip address (#{IP}) (#{IP})\s*$/
+        resource[:ipaddress] << [prefix_length(IPAddr.new($2)), IPAddr.new($1), nil]
+      end
+      if l =~ /ipv6 address (#{IP})\/(\d+) (eui-64|link-local)/
+        resource[:ipaddress] << [$2.to_i, IPAddr.new($1), $3]
+      end
+      if l =~ /channel-group\s+(\d+)/
+        resource[:etherchannel] = $1
+      end
+    end
+    resource
+  end
+
+  def parse_vlans
+    vlans = {}
+    out = transport.command(support_vlan_brief? ? "sh vlan brief" : "sh vlan-switch brief")
+    lines = out.split("\n")
+    lines.shift; lines.shift; lines.shift; lines.pop
+    vlan = nil
+    lines.each do |l|
+      case l
+            # vlan    name    status
+      when /^(\d+)\s+(\w+)\s+(\w+)\s+([a-zA-Z0-9,\/. ]+)\s*$/
+        vlan = { :id => $1, :name => $2, :status => $3, :interfaces => [] }
+        if $4.strip.length > 0
+          vlan[:interfaces] = $4.strip.split(/\s*,\s*/).map{ |ifn| canonalize_ifname(ifn) }
+        end
+        vlans[vlan[:id]] = vlan
+      when /^\s+([a-zA-Z0-9,\/. ]+)\s*$/
+        raise "invalid sh vlan summary output" unless vlan
+        if $1.strip.length > 0
+          vlan[:interfaces] += $1.strip.split(/\s*,\s*/).map{ |ifn| canonalize_ifname(ifn) }
+        end
+      else
+      end
+    end
+    vlans
+  end
+
+  def parse_trunking(interface)
+    trunking = {}
+    out = transport.command("sh interface #{interface} switchport")
+    lines = out.split("\n")
+    lines.shift; lines.pop
+    lines.each do |l|
+      case l
+      when /^Administrative mode:\s+(.*)$/i
+        case $1
+        when "trunk"
+          trunking[:mode] = :trunk
+        when "static access"
+          trunking[:mode] = :access
+        else
+          raise "Unknown switchport mode: #{$1} for #{interface}"
+        end
+      when /^Administrative Trunking Encapsulation:\s+(.*)$/
+        case $1
+        when "dot1q","isl"
+          trunking[:encapsulation] = $1.to_sym if trunking[:mode] == :trunk
+        else
+          raise "Unknown switchport encapsulation: #{$1} for #{interface}"
+        end
+      when /^Access Mode VLAN:\s+(.*) \(\(Inactive\)\)$/
+        # nothing
+      when /^Access Mode VLAN:\s+(.*) \(.*\)$/
+        trunking[:native_vlan] = $1 if trunking[:mode] == :access
+      when /^Trunking VLANs Enabled:\s+(.*)$/
+        next if trunking[:mode] == :access
+        vlans = $1
+        trunking[:allowed_trunk_vlans] = case vlans
+        when /all/i
+          :all
+        when /none/i
+          :none
+        else
+          vlans
+        end
+      end
+    end
+    trunking
+  end
+
+end
diff --git a/lib/puppet/util/network_device/cisco/interface.rb b/lib/puppet/util/network_device/cisco/interface.rb
new file mode 100644
index 0000000..63d5492
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco/interface.rb
@@ -0,0 +1,82 @@
+require 'puppet/util/network_device/cisco'
+require 'puppet/util/network_device/ipcalc'
+
+# this manages setting properties to an interface in a cisco switch or router
+class Puppet::Util::NetworkDevice::Cisco::Interface
+
+  include Puppet::Util::NetworkDevice::IPCalc
+  extend Puppet::Util::NetworkDevice::IPCalc
+
+  attr_reader :transport, :name
+
+  def initialize(name, transport)
+    @name = name
+    @transport = transport
+  end
+
+  COMMANDS = {
+    # property     => order, ios command/block/array
+    :description   => [1, "description %s"],
+    :speed         => [2, "speed %s"],
+    :duplex        => [3, "duplex %s"],
+    :native_vlan   => [4, "switchport access vlan %s"],
+    :encapsulation => [5, "switchport trunk encapsulation %s"],
+    :mode          => [6, "switchport mode %s"],
+    :allowed_trunk_vlans => [7, "switchport trunk allowed vlan %s"],
+    :etherchannel  => [8, ["channel-group %s", "port group %s"]],
+    :ipaddress     => [9,
+      lambda do |prefix,ip,option|
+        ip.ipv6? ? "ipv6 address #{ip.to_s}/#{prefix} #{option}" :
+                   "ip address #{ip.to_s} #{netmask(Socket::AF_INET,prefix)}"
+      end],
+      :ensure        => [10, lambda { |value| value == :present ? "no shutdown" : "shutdown" } ]
+  }
+
+  def update(is={}, should={})
+    Puppet.debug("Updating interface #{name}")
+    command("conf t")
+    command("interface #{name}")
+
+    # apply changes in a defined orders for cisco IOS devices
+    [is.keys, should.keys].flatten.uniq.sort {|a,b| COMMANDS[a][0] <=> COMMANDS[b][0] }.each do |property|
+      # They're equal, so do nothing.
+      next if is[property] == should[property]
+
+      # We're deleting it
+      if should[property] == :absent or should[property].nil?
+        execute(property, is[property], "no ")
+        next
+      end
+
+      # We're replacing an existing value or creating a new one
+      execute(property, should[property])
+    end
+
+    command("exit")
+    command("exit")
+  end
+
+  def execute(property, value, prefix='')
+    case COMMANDS[property][1]
+    when Array
+      COMMANDS[property][1].each do |command|
+        transport.command(prefix + command % value) do |out|
+          break unless out =~ /^%/
+        end
+      end
+    when String
+      command(prefix + COMMANDS[property][1] % value)
+    when Proc
+      value = [value] unless value.is_a?(Array)
+      value.each do |value|
+        command(prefix + COMMANDS[property][1].call(*value))
+      end
+    end
+  end
+
+  def command(command)
+    transport.command(command) do |out|
+      Puppet.err "Error while executing #{command}, device returned #{out}" if out =~ /^%/mo
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/unit/provider/interface/cisco_spec.rb b/spec/unit/provider/interface/cisco_spec.rb
new file mode 100644
index 0000000..7904711
--- /dev/null
+++ b/spec/unit/provider/interface/cisco_spec.rb
@@ -0,0 +1,64 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/provider/interface/cisco'
+
+provider_class = Puppet::Type.type(:interface).provider(:cisco)
+
+describe provider_class do
+  before do
+    @resource = stub("resource", :name => "Fa0/1")
+    @provider = provider_class.new(@resource)
+  end
+
+  it "should have a parent of Puppet::Provider::NetworkDevice" do
+    provider_class.should < Puppet::Provider::NetworkDevice
+  end
+
+  it "should have an instances method" do
+    provider_class.should respond_to(:instances)
+  end
+
+  describe "when looking up instances at prefetch" do
+    before do
+      @device = stub_everything 'device'
+      Puppet::Util::NetworkDevice::Cisco::Device.stubs(:new).returns(@device)
+      @device.stubs(:command).yields(@device)
+    end
+
+    it "should initialize the network device with the given url" do
+      Puppet::Util::NetworkDevice::Cisco::Device.expects(:new).with(:url).returns(@device)
+      provider_class.lookup(:url, "Fa0/1")
+    end
+
+    it "should delegate to the device interface fetcher" do
+      @device.expects(:interface)
+      provider_class.lookup("", "Fa0/1")
+    end
+
+    it "should return the given interface data" do
+      @device.expects(:interface).returns({ :description => "thisone", :mode => :access})
+      provider_class.lookup("", "Fa0").should == {:description => "thisone", :mode => :access }
+    end
+
+  end
+
+  describe "when an instance is being flushed" do
+    it "should call the device interface update method with current and past properties" do
+      @instance = provider_class.new(:ensure => :present, :name => "Fa0/1", :description => "myinterface")
+      @instance.description = "newdesc"
+      @instance.resource = @resource
+      @resource.stubs(:[]).with(:name).returns("Fa0/1")
+      device = stub_everything 'device'
+      @instance.stubs(:device).returns(device)
+      device.expects(:command).yields(device)
+      interface = stub 'interface'
+      device.expects(:new_interface).with("Fa0/1").returns(interface)
+      interface.expects(:update).with( {:ensure => :present, :name => "Fa0/1", :description => "myinterface"},
+                                       {:ensure => :present, :name => "Fa0/1", :description => "newdesc"})
+
+      @instance.flush
+    end
+  end
+end
diff --git a/spec/unit/type/interface_spec.rb b/spec/unit/type/interface_spec.rb
new file mode 100644
index 0000000..630e45a
--- /dev/null
+++ b/spec/unit/type/interface_spec.rb
@@ -0,0 +1,93 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Puppet::Type.type(:interface) do
+  it "should have a 'name' parameter'" do
+    Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1")[:name].should == "FastEthernet 0/1"
+  end
+
+  it "should have a 'device_url' parameter'" do
+    Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :device_url => :device)[:device_url].should == :device
+  end
+
+  it "should have an ensure property" do
+    Puppet::Type.type(:interface).attrtype(:ensure).should == :property
+  end
+
+  [:description, :speed, :duplex, :native_vlan, :encapsulation, :mode, :allowed_trunk_vlans, :etherchannel, :ipaddress].each do |p|
+    it "should have a #{p} property" do
+      Puppet::Type.type(:interface).attrtype(p).should == :property
+    end
+  end
+
+  describe "when validating attribute values" do
+    before do
+      @provider = stub 'provider', :class => Puppet::Type.type(:interface).defaultprovider, :clear => nil
+      Puppet::Type.type(:interface).defaultprovider.stubs(:new).returns(@provider)
+    end
+
+    it "should support :present as a value to :ensure" do
+      Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ensure => :present)
+    end
+
+    it "should support :shutdown as a value to :ensure" do
+      Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ensure => :shutdown)
+    end
+
+    it "should support :no_shutdown as a value to :ensure" do
+      Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ensure => :no_shutdown)
+    end
+
+    describe "especially speed" do
+      it "should allow a number" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :speed => "100")
+      end
+
+      it "should allow :auto" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :speed => :auto)
+      end
+    end
+
+    describe "especially duplex" do
+      it "should allow :half" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :duplex => :half)
+      end
+
+      it "should allow :full" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :duplex => :full)
+      end
+
+      it "should allow :auto" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :duplex => :auto)
+      end
+    end
+
+    describe "especially ipaddress" do
+      it "should allow ipv4 addresses" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => "192.168.0.1/24")
+      end
+
+      it "should allow arrays of ipv4 addresses" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => ["192.168.0.1/24", "192.168.1.0/24"])
+      end
+
+      it "should allow ipv6 addresses" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => "f0e9::/64")
+      end
+
+      it "should allow ipv6 options" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => "f0e9::/64 link-local")
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => "f0e9::/64 eui-64")
+      end
+
+      it "should allow a mix of ipv4 and ipv6" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => ["192.168.0.1/24", "f0e9::/64 link-local"])
+      end
+
+      it "should munge ip addresses to a computer format" do
+        Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1", :ipaddress => "192.168.0.1/24")[:ipaddress].should == [[24, IPAddr.new('192.168.0.1'), nil]]
+      end
+    end
+  end
+end
diff --git a/spec/unit/util/network_device/cisco/device_spec.rb b/spec/unit/util/network_device/cisco/device_spec.rb
new file mode 100644
index 0000000..9021bbd
--- /dev/null
+++ b/spec/unit/util/network_device/cisco/device_spec.rb
@@ -0,0 +1,501 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+
+require 'puppet/util/network_device/cisco/device'
+
+describe Puppet::Util::NetworkDevice::Cisco::Device do
+  before(:each) do
+    @transport = stub_everything 'transport', :is_a? => true, :command => ""
+    @cisco = Puppet::Util::NetworkDevice::Cisco::Device.new("telnet://user:password@localhost:23/")
+    @cisco.transport = @transport
+  end
+
+  describe "when creating the device" do
+    it "should find the enable password from the url" do
+      cisco = Puppet::Util::NetworkDevice::Cisco::Device.new("telnet://user:password@localhost:23/?enable=enable_password")
+      cisco.enable_password.should == "enable_password"
+    end
+
+    it "should find the enable password from the options" do
+      cisco = Puppet::Util::NetworkDevice::Cisco::Device.new("telnet://user:password@localhost:23/?enable=enable_password", :enable_password => "mypass")
+      cisco.enable_password.should == "mypass"
+    end
+  end
+
+  describe "when connecting to the physical device" do
+    it "should connect to the transport" do
+      @transport.expects(:connect)
+      @cisco.command
+    end
+
+    it "should attempt to login" do
+      @cisco.expects(:login)
+      @cisco.command
+    end
+
+    it "should tell the device to not page" do
+      @transport.expects(:command).with("terminal length 0")
+      @cisco.command
+    end
+
+    it "should enter the enable password if returned prompt is not privileged" do
+      @transport.stubs(:command).yields("Switch>").returns("")
+      @cisco.expects(:enable)
+      @cisco.command
+    end
+
+    it "should find device capabilities" do
+      @cisco.expects(:find_capabilities)
+      @cisco.command
+    end
+
+    it "should execute given command" do
+      @transport.expects(:command).with("mycommand")
+      @cisco.command("mycommand")
+    end
+
+    it "should yield to the command block if one is provided" do
+      @transport.expects(:command).with("mycommand")
+      @cisco.command do |c|
+        c.command("mycommand")
+      end
+    end
+
+    it "should close the device transport" do
+      @transport.expects(:close)
+      @cisco.command
+    end
+
+    describe "when login in" do
+      it "should not login if transport handles login" do
+        @transport.expects(:handles_login?).returns(true)
+        @transport.expects(:command).never
+        @transport.expects(:expect).never
+        @cisco.login
+      end
+
+      it "should send username if one has been provided" do
+        @transport.expects(:command).with("user", :prompt => /^Password:/)
+        @cisco.login
+      end
+
+      it "should send password after the username" do
+        @transport.expects(:command).with("user", :prompt => /^Password:/)
+        @transport.expects(:command).with("password")
+        @cisco.login
+      end
+
+      it "should expect the Password: prompt if no user was sent" do
+        @cisco.url.user = ''
+        @transport.expects(:expect).with(/^Password:/)
+        @transport.expects(:command).with("password")
+        @cisco.login
+      end
+    end
+
+    describe "when entering enable password" do
+      it "should raise an error if no enable password has been set" do
+        @cisco.enable_password = nil
+        lambda{ @cisco.enable }.should raise_error
+      end
+
+      it "should send the enable command and expect an enable prompt" do
+        @cisco.enable_password = 'mypass'
+        @transport.expects(:command).with("enable", :prompt => /^Password:/)
+        @cisco.enable
+      end
+
+      it "should send the enable password" do
+        @cisco.enable_password = 'mypass'
+        @transport.stubs(:command).with("enable", :prompt => /^Password:/)
+        @transport.expects(:command).with("mypass")
+        @cisco.enable
+      end
+    end
+  end
+
+  describe "when finding network device capabilities" do
+    it "should try to execute sh vlan brief" do
+      @transport.expects(:command).with("sh vlan brief").returns("")
+      @cisco.find_capabilities
+    end
+
+    it "should detect errors" do
+      @transport.stubs(:command).with("sh vlan brief").returns(<<eos)
+Switch#sh vlan brief  
+% Ambiguous command:  "sh vlan brief"
+Switch#
+eos
+
+      @cisco.find_capabilities
+      @cisco.should_not be_support_vlan_brief
+    end
+  end
+
+
+  {
+    "Fa 0/1" => "FastEthernet0/1",
+    "Fa0/1" => "FastEthernet0/1",
+    "FastEth 0/1" => "FastEthernet0/1",
+    "Gi1" => "GigEthernet1",
+    "Di9" => "Dialer9",
+    "Ethernet 0/0/1" => "Ethernet0/0/1",
+    "E0" => "Ethernet0",
+    "ATM 0/1.1" => "ATM0/1.1",
+    "VLAN99" => "VLAN99"
+  }.each do |input,expected|
+    it "should canonicalize #{input} to #{expected}" do
+      @cisco.canonalize_ifname(input).should == expected
+    end
+  end
+
+
+  describe "when parsing interface" do
+
+    it "should parse interface output" do
+      @cisco.expects(:parse_interface).returns({ :ensure => :present })
+
+      @cisco.interface("FastEthernet0/1").should == { :ensure => :present }
+    end
+
+    it "should parse trunking and merge results" do
+      @cisco.stubs(:parse_interface).returns({ :ensure => :present })
+      @cisco.expects(:parse_trunking).returns({ :native_vlan => "100" })
+
+      @cisco.interface("FastEthernet0/1").should == { :ensure => :present, :native_vlan => "100" }
+    end
+
+    it "should return an absent interface if parse_interface returns nothing" do
+      @cisco.stubs(:parse_interface).returns({})
+
+      @cisco.interface("FastEthernet0/1").should == { :ensure => :absent }
+    end
+
+    it "should parse ip address information and merge results" do
+      @cisco.stubs(:parse_interface).returns({ :ensure => :present })
+      @cisco.expects(:parse_interface_config).returns({ :ipaddress => [24,IPAddr.new('192.168.0.24'), nil] })
+
+      @cisco.interface("FastEthernet0/1").should == { :ensure => :present, :ipaddress => [24,IPAddr.new('192.168.0.24'), nil] }
+    end
+
+    it "should parse the sh interface command" do
+      @transport.stubs(:command).with("sh interface FastEthernet0/1").returns(<<eos)
+Switch#sh interfaces FastEthernet 0/1
+FastEthernet0/1 is down, line protocol is down 
+  Hardware is Fast Ethernet, address is 00d0.bbe2.19c1 (bia 00d0.bbe2.19c1)
+  MTU 1500 bytes, BW 100000 Kbit, DLY 100 usec, 
+     reliability 255/255, txload 1/255, rxload 1/255
+  Encapsulation ARPA, loopback not set
+  Keepalive not set
+  Auto-duplex , Auto Speed , 100BaseTX/FX
+  ARP type: ARPA, ARP Timeout 04:00:00
+  Last input never, output 5d04h, output hang never
+  Last clearing of "show interface" counters never
+  Queueing strategy: fifo
+  Output queue 0/40, 0 drops; input queue 0/75, 0 drops
+  5 minute input rate 0 bits/sec, 0 packets/sec
+  5 minute output rate 0 bits/sec, 0 packets/sec
+     580 packets input, 54861 bytes
+     Received 6 broadcasts, 0 runts, 0 giants, 0 throttles
+     0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
+     0 watchdog, 1 multicast
+     0 input packets with dribble condition detected
+     845 packets output, 80359 bytes, 0 underruns
+     0 output errors, 0 collisions, 1 interface resets
+     0 babbles, 0 late collision, 0 deferred
+     0 lost carrier, 0 no carrier
+     0 output buffer failures, 0 output buffers swapped out
+Switch#
+eos
+
+      @cisco.parse_interface("FastEthernet0/1").should == { :ensure => :absent, :duplex => :auto, :speed => :auto }
+    end
+
+    it "should be able to parse the sh vlan brief command output" do
+      @cisco.stubs(:support_vlan_brief?).returns(true)
+      @transport.stubs(:command).with("sh vlan brief").returns(<<eos)
+Switch#sh vlan brief
+VLAN Name                             Status    Ports
+---- -------------------------------- --------- -------------------------------
+1    default                          active    Fa0/3, Fa0/4, Fa0/5, Fa0/6,
+                                                Fa0/7, Fa0/8, Fa0/9, Fa0/10,
+                                                Fa0/11, Fa0/12, Fa0/13, Fa0/14,
+                                                Fa0/15, Fa0/16, Fa0/17, Fa0/18,
+                                                Fa0/23, Fa0/24
+10   VLAN0010                         active    
+100  management                       active    Fa0/1, Fa0/2
+Switch#
+eos
+
+      @cisco.parse_vlans.should == {"100"=>{:status=>"active", :interfaces=>["FastEthernet0/1", "FastEthernet0/2"], :name=>"management", :id=>"100"}, "1"=>{:status=>"active", :interfaces=>["FastEthernet0/3", "FastEthernet0/4", "FastEthernet0/5", "FastEthernet0/6", "FastEthernet0/7", "FastEthernet0/8", "FastEthernet0/9", "FastEthernet0/10", "FastEthernet0/11", "FastEthernet0/12", "FastEthernet0/13", "FastEthernet0/14", "FastEthernet0/15", "FastEthernet0/16", "FastEthernet0/17", "FastEthernet0/18", "FastEthernet0/23", "FastEthernet0/24"], :name=>"default", :id=>"1"}, "10"=>{:status=>"active", :interfaces=>[], :name=>"VLAN0010", :id=>"10"}}
+    end
+
+    it "should parse trunk switchport information" do
+      @transport.stubs(:command).with("sh interface FastEthernet0/21 switchport").returns(<<eos)
+Switch#sh interfaces FastEthernet 0/21 switchport
+Name: Fa0/21
+Switchport: Enabled
+Administrative mode: trunk
+Operational Mode: trunk
+Administrative Trunking Encapsulation: dot1q
+Operational Trunking Encapsulation: dot1q
+Negotiation of Trunking: Disabled
+Access Mode VLAN: 0 ((Inactive))
+Trunking Native Mode VLAN: 1 (default)
+Trunking VLANs Enabled: ALL
+Trunking VLANs Active: 1,10,100
+Pruning VLANs Enabled: 2-1001
+
+Priority for untagged frames: 0
+Override vlan tag priority: FALSE
+Voice VLAN: none
+Appliance trust: none
+Self Loopback: No
+Switch#
+eos
+
+      @cisco.parse_trunking("FastEthernet0/21").should == { :mode => :trunk, :encapsulation => :dot1q, :allowed_trunk_vlans=>:all, }
+    end
+
+    it "should parse trunk switchport information with allowed vlans" do
+      @transport.stubs(:command).with("sh interface GigabitEthernet 0/1 switchport").returns(<<eos)
+c2960#sh interfaces GigabitEthernet 0/1 switchport 
+Name: Gi0/1
+Switchport: Enabled
+Administrative Mode: trunk
+Operational Mode: trunk
+Administrative Trunking Encapsulation: dot1q
+Operational Trunking Encapsulation: dot1q
+Negotiation of Trunking: On
+Access Mode VLAN: 1 (default)
+Trunking Native Mode VLAN: 1 (default)
+Administrative Native VLAN tagging: enabled
+Voice VLAN: none
+Administrative private-vlan host-association: none 
+Administrative private-vlan mapping: none 
+Administrative private-vlan trunk native VLAN: none
+Administrative private-vlan trunk Native VLAN tagging: enabled
+Administrative private-vlan trunk encapsulation: dot1q
+Administrative private-vlan trunk normal VLANs: none
+Administrative private-vlan trunk associations: none
+Administrative private-vlan trunk mappings: none
+Operational private-vlan: none
+Trunking VLANs Enabled: 1,99
+Pruning VLANs Enabled: 2-1001
+Capture Mode Disabled
+Capture VLANs Allowed: ALL
+
+Protected: false
+Unknown unicast blocked: disabled
+Unknown multicast blocked: disabled
+Appliance trust: none
+c2960#
+eos
+
+      @cisco.parse_trunking("GigabitEthernet 0/1").should == { :mode => :trunk, :encapsulation => :dot1q, :allowed_trunk_vlans=>"1,99", }
+    end
+
+    it "should parse access switchport information" do
+      @transport.stubs(:command).with("sh interface FastEthernet0/1 switchport").returns(<<eos)
+Switch#sh interfaces FastEthernet 0/1 switchport  
+Name: Fa0/1
+Switchport: Enabled
+Administrative mode: static access
+Operational Mode: static access
+Administrative Trunking Encapsulation: isl
+Operational Trunking Encapsulation: isl
+Negotiation of Trunking: Disabled
+Access Mode VLAN: 100 (SHDSL)
+Trunking Native Mode VLAN: 1 (default)
+Trunking VLANs Enabled: NONE
+Pruning VLANs Enabled: NONE
+
+Priority for untagged frames: 0
+Override vlan tag priority: FALSE
+Voice VLAN: none
+Appliance trust: none
+Self Loopback: No
+Switch#
+eos
+
+      @cisco.parse_trunking("FastEthernet0/1").should == { :mode => :access, :native_vlan => "100" }
+    end
+
+    it "should parse ip addresses" do
+      @transport.stubs(:command).with("sh running-config interface Vlan 1 | begin interface").returns(<<eos)
+router#sh running-config interface Vlan 1 | begin interface
+interface Vlan1
+ description $ETH-SW-LAUNCH$$INTF-INFO-HWIC 4ESW$$FW_INSIDE$
+ ip address 192.168.0.24 255.255.255.0 secondary
+ ip address 192.168.0.1 255.255.255.0
+ ip access-group 100 in
+ no ip redirects
+ no ip proxy-arp
+ ip nbar protocol-discovery
+ ip dns view-group dow
+ ip nat inside
+ ip virtual-reassembly
+ ip route-cache flow
+ ipv6 address 2001:7A8:71C1::/64 eui-64
+ ipv6 enable
+ ipv6 traffic-filter DENY-ACL6 out
+ ipv6 mtu 1280
+ ipv6 nd prefix 2001:7A8:71C1::/64
+ ipv6 nd ra interval 60
+ ipv6 nd ra lifetime 180
+ ipv6 verify unicast reverse-path
+ ipv6 inspect STD6 out
+end
+
+router#
+eos
+      @cisco.parse_interface_config("Vlan 1").should == {:ipaddress=>[[24, IPAddr.new('192.168.0.24'), 'secondary'],
+                                                                      [24, IPAddr.new('192.168.0.1'), nil],
+                                                                      [64, IPAddr.new('2001:07a8:71c1::'), "eui-64"]]}
+    end
+
+    it "should parse etherchannel membership" do
+      @transport.stubs(:command).with("sh running-config interface Gi0/17 | begin interface").returns(<<eos)
+c2960#sh running-config interface Gi0/17 | begin interface
+interface GigabitEthernet0/17
+ description member of Po1
+ switchport mode access
+ channel-protocol lacp
+ channel-group 1 mode passive
+ spanning-tree portfast
+ spanning-tree bpduguard enable
+end
+
+c2960#
+eos
+      @cisco.parse_interface_config("Gi0/17").should == {:etherchannel=>"1"}
+    end
+  end
+end
+
+# static access
+# Switch#sh interfaces FastEthernet 0/1 switchport  
+# Name: Fa0/1
+# Switchport: Enabled
+# Administrative mode: static access
+# Operational Mode: static access
+# Administrative Trunking Encapsulation: isl
+# Operational Trunking Encapsulation: isl
+# Negotiation of Trunking: Disabled
+# Access Mode VLAN: 100 (SHDSL)
+# Trunking Native Mode VLAN: 1 (default)
+# Trunking VLANs Enabled: NONE
+# Pruning VLANs Enabled: NONE
+# 
+# Priority for untagged frames: 0
+# Override vlan tag priority: FALSE
+# Voice VLAN: none
+# Appliance trust: none
+# Self Loopback: No
+# Switch#
+
+# c2960#sh interfaces GigabitEthernet 0/1 switchport 
+# Name: Gi0/1
+# Switchport: Enabled
+# Administrative Mode: trunk
+# Operational Mode: trunk
+# Administrative Trunking Encapsulation: dot1q
+# Operational Trunking Encapsulation: dot1q
+# Negotiation of Trunking: On
+# Access Mode VLAN: 1 (default)
+# Trunking Native Mode VLAN: 1 (default)
+# Administrative Native VLAN tagging: enabled
+# Voice VLAN: none
+# Administrative private-vlan host-association: none 
+# Administrative private-vlan mapping: none 
+# Administrative private-vlan trunk native VLAN: none
+# Administrative private-vlan trunk Native VLAN tagging: enabled
+# Administrative private-vlan trunk encapsulation: dot1q
+# Administrative private-vlan trunk normal VLANs: none
+# Administrative private-vlan trunk associations: none
+# Administrative private-vlan trunk mappings: none
+# Operational private-vlan: none
+# Trunking VLANs Enabled: 1,99
+# Pruning VLANs Enabled: 2-1001
+# Capture Mode Disabled
+# Capture VLANs Allowed: ALL
+# 
+# Protected: false
+# Unknown unicast blocked: disabled
+# Unknown multicast blocked: disabled
+# Appliance trust: none
+# c2960#
+
+# c2960#sh interfaces GigabitEthernet 0/2 switchport 
+# Name: Gi0/2
+# Switchport: Enabled
+# Administrative Mode: static access
+# Operational Mode: static access
+# Administrative Trunking Encapsulation: dot1q
+# Operational Trunking Encapsulation: native
+# Negotiation of Trunking: Off
+# Access Mode VLAN: 99 (MGMT)
+# Trunking Native Mode VLAN: 1 (default)
+# Administrative Native VLAN tagging: enabled
+# Voice VLAN: none
+# Administrative private-vlan host-association: none 
+# Administrative private-vlan mapping: none 
+# Administrative private-vlan trunk native VLAN: none
+# Administrative private-vlan trunk Native VLAN tagging: enabled
+# Administrative private-vlan trunk encapsulation: dot1q
+# Administrative private-vlan trunk normal VLANs: none
+# Administrative private-vlan trunk associations: none
+# Administrative private-vlan trunk mappings: none
+# Operational private-vlan: none
+# Trunking VLANs Enabled: ALL
+# Pruning VLANs Enabled: 2-1001
+# Capture Mode Disabled
+# Capture VLANs Allowed: ALL
+# 
+# Protected: false
+# Unknown unicast blocked: disabled
+# Unknown multicast blocked: disabled
+# Appliance trust: none
+# c2960#
+
+# c877#sh interfaces FastEthernet 1 switchport 
+# Name: Fa1
+# Switchport: Enabled
+# Administrative Mode: trunk
+# Operational Mode: trunk
+# Administrative Trunking Encapsulation: dot1q
+# Operational Trunking Encapsulation: dot1q
+# Negotiation of Trunking: Disabled
+# Access Mode VLAN: 0 ((Inactive))
+# Trunking Native Mode VLAN: 1 (default)
+# Trunking VLANs Enabled: ALL
+# Trunking VLANs Active: 1
+# Protected: false
+# Priority for untagged frames: 0
+# Override vlan tag priority: FALSE
+# Voice VLAN: none 
+# Appliance trust: none
+
+
+# c2960#sh etherchannel summary 
+# Flags:  D - down        P - bundled in port-channel
+#         I - stand-alone s - suspended
+#         H - Hot-standby (LACP only)
+#         R - Layer3      S - Layer2
+#         U - in use      f - failed to allocate aggregator
+# 
+#         M - not in use, minimum links not met
+#         u - unsuitable for bundling
+#         w - waiting to be aggregated
+#         d - default port
+# 
+# 
+# Number of channel-groups in use: 1
+# Number of aggregators:           1
+# 
+# Group  Port-channel  Protocol    Ports
+# ------+-------------+-----------+-----------------------------------------------
+# 1      Po1(SU)         LACP      Gi0/17(P)   Gi0/18(P)   
+# 
+# c2960#
diff --git a/spec/unit/util/network_device/cisco/interface_spec.rb b/spec/unit/util/network_device/cisco/interface_spec.rb
new file mode 100644
index 0000000..f6aa147
--- /dev/null
+++ b/spec/unit/util/network_device/cisco/interface_spec.rb
@@ -0,0 +1,89 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+
+require 'puppet/util/network_device'
+require 'puppet/util/network_device/cisco/interface'
+
+describe Puppet::Util::NetworkDevice::Cisco::Interface do
+  before(:each) do
+    @transport = stub_everything 'transport'
+    @interface = Puppet::Util::NetworkDevice::Cisco::Interface.new("FastEthernet0/1", at transport)
+  end
+
+  it "should include IPCalc" do
+    @interface.class.include?(Puppet::Util::NetworkDevice::IPCalc)
+  end
+
+  describe "when updating the physical device" do
+    it "should enter global configuration mode" do
+      @transport.expects(:command).with("conf t")
+      @interface.update
+    end
+
+    it "should enter interface configuration mode" do
+      @transport.expects(:command).with("interface FastEthernet0/1")
+      @interface.update
+    end
+
+    it "should 'execute' all differing properties" do
+      @interface.expects(:execute).with(:description, "b")
+      @interface.expects(:execute).with(:mode, :access).never
+      @interface.update({ :description => "a", :mode => :access }, { :description => "b", :mode => :access })
+    end
+
+    it "should execute in cisco ios defined order" do
+      speed = states('speed').starts_as('notset')
+      @interface.expects(:execute).with(:speed, :auto).then(speed.is('set'))
+      @interface.expects(:execute).with(:duplex, :auto).when(speed.is('set'))
+      @interface.update({ :duplex => :half, :speed => "10" }, { :duplex => :auto, :speed => :auto  })
+    end
+
+    it "should execute absent properties with a no prefix" do
+      @interface.expects(:execute).with(:description, "a", "no ")
+      @interface.update({ :description => "a"}, { })
+    end
+
+    it "should exit twice" do
+      @transport.expects(:command).with("exit").twice
+      @interface.update
+    end
+  end
+
+  describe "when executing commands" do
+    it "should execute string commands directly" do
+      @transport.expects(:command).with("speed auto")
+      @interface.execute(:speed, :auto)
+    end
+
+    it "should execute string commands with the given prefix" do
+      @transport.expects(:command).with("no speed auto")
+      @interface.execute(:speed, :auto, "no ")
+    end
+
+    it "should stop at executing the first command that works for array" do
+      @transport.expects(:command).with("channel-group 1").yields("% Invalid command")
+      @transport.expects(:command).with("port group 1")
+      @interface.execute(:etherchannel, "1")
+    end
+
+    it "should execute the block for block commands" do
+      @transport.expects(:command).with("ip address 192.168.0.1 255.255.255.0")
+      @interface.execute(:ipaddress, [[24, IPAddr.new('192.168.0.1'), nil]])
+    end
+
+    it "should execute the block for block commands" do
+      @transport.expects(:command).with("ipv6 address fe08::/76 link-local")
+      @interface.execute(:ipaddress, [[76, IPAddr.new('fe08::'), 'link-local']])
+    end
+
+  end
+
+  describe "when sending commands to the device" do
+    it "should detect errors" do
+      Puppet.expects(:err)
+      @transport.stubs(:command).yields("% Invalid Command")
+      @interface.command("sh ver")
+    end
+  end
+end
\ No newline at end of file

-- 
Puppet packaging for Debian



More information about the Pkg-puppet-devel mailing list