summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Turnbull <james@lovedthanlost.net>2011-04-12 05:15:00 +1000
committerJames Turnbull <james@lovedthanlost.net>2011-04-12 05:15:00 +1000
commitdce851cac79393f86950f4ebfc48b9ac67dcd8f7 (patch)
treecd2a4a92183b43eba985633f34de6557937b9e37
parent46d67fd86819d1dfe4813f22b192213985e3a587 (diff)
parent49dcc24195d262437cc35997a811eb724d65b48b (diff)
downloadpuppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.tar.gz
puppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.tar.xz
puppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.zip
Merge branch 'tickets/master/7021' into next
* tickets/master/7021: Updated confine in Spec test for RSpec 2 Add management of router/switchs global vlans Cisco Switch/Router Interface management Base class for network device based providers Ssh transport for network device management Telnet transport to connect to remote network device Remote Network Device transport system Introduce a module for some IP computations
-rw-r--r--lib/puppet/feature/ssh.rb4
-rw-r--r--lib/puppet/provider/interface/base.rb0
-rw-r--r--lib/puppet/provider/interface/cisco.rb33
-rw-r--r--lib/puppet/provider/network_device.rb59
-rw-r--r--lib/puppet/provider/vlan/cisco.rb34
-rw-r--r--lib/puppet/type/interface.rb107
-rw-r--r--lib/puppet/type/router.rb14
-rw-r--r--lib/puppet/type/vlan.rb24
-rw-r--r--lib/puppet/util/network_device.rb2
-rw-r--r--lib/puppet/util/network_device/base.rb29
-rw-r--r--lib/puppet/util/network_device/cisco.rb4
-rw-r--r--lib/puppet/util/network_device/cisco/device.rb246
-rw-r--r--lib/puppet/util/network_device/cisco/interface.rb82
-rw-r--r--lib/puppet/util/network_device/ipcalc.rb68
-rw-r--r--lib/puppet/util/network_device/transport.rb5
-rw-r--r--lib/puppet/util/network_device/transport/base.rb26
-rw-r--r--lib/puppet/util/network_device/transport/ssh.rb115
-rw-r--r--lib/puppet/util/network_device/transport/telnet.rb42
-rw-r--r--spec/unit/provider/interface/cisco_spec.rb64
-rw-r--r--spec/unit/provider/network_device_spec.rb148
-rw-r--r--spec/unit/provider/vlan/cisco_spec.rb62
-rw-r--r--spec/unit/type/interface_spec.rb93
-rw-r--r--spec/unit/type/vlan_spec.rb40
-rw-r--r--spec/unit/util/network_device/cisco/device_spec.rb521
-rw-r--r--spec/unit/util/network_device/cisco/interface_spec.rb89
-rw-r--r--spec/unit/util/network_device/ipcalc_spec.rb63
-rw-r--r--spec/unit/util/network_device/transport/base_spec.rb42
-rw-r--r--spec/unit/util/network_device/transport/ssh_spec.rb211
-rw-r--r--spec/unit/util/network_device/transport/telnet_spec.rb76
29 files changed, 2303 insertions, 0 deletions
diff --git a/lib/puppet/feature/ssh.rb b/lib/puppet/feature/ssh.rb
new file mode 100644
index 000000000..82fe19882
--- /dev/null
+++ b/lib/puppet/feature/ssh.rb
@@ -0,0 +1,4 @@
+require 'puppet/util/feature'
+
+Puppet.features.rubygems?
+Puppet.features.add(:ssh, :libs => %{net/ssh})
diff --git a/lib/puppet/provider/interface/base.rb b/lib/puppet/provider/interface/base.rb
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/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 000000000..f3bd202e9
--- /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/provider/network_device.rb b/lib/puppet/provider/network_device.rb
new file mode 100644
index 000000000..58865fddc
--- /dev/null
+++ b/lib/puppet/provider/network_device.rb
@@ -0,0 +1,59 @@
+
+# This is the base class of all prefetched network device provider
+class Puppet::Provider::NetworkDevice < Puppet::Provider
+
+ def self.lookup(url, name)
+ raise "This provider doesn't implement the necessary lookup method"
+ end
+
+ def self.prefetch(resources)
+ resources.each do |name, resource|
+ if result = lookup(resource[:device_url], name)
+ result[:ensure] = :present
+ resource.provider = new(result)
+ else
+ resource.provider = new(:ensure => :absent)
+ end
+ end
+ end
+
+ def exists?
+ @property_hash[:ensure] != :absent
+ end
+
+ def initialize(*args)
+ super
+
+ # Make a duplicate, so that we have a copy for comparison
+ # at the end.
+ @properties = @property_hash.dup
+ end
+
+ def create
+ @property_hash[:ensure] = :present
+ self.class.resource_type.validproperties.each do |property|
+ if val = resource.should(property)
+ @property_hash[property] = val
+ end
+ end
+ end
+
+ def destroy
+ @property_hash[:ensure] = :absent
+ end
+
+ def flush
+ @property_hash.clear
+ end
+
+ def self.instances
+ end
+
+ def former_properties
+ @properties.dup
+ end
+
+ def properties
+ @property_hash.dup
+ end
+end \ No newline at end of file
diff --git a/lib/puppet/provider/vlan/cisco.rb b/lib/puppet/provider/vlan/cisco.rb
new file mode 100644
index 000000000..46e172c73
--- /dev/null
+++ b/lib/puppet/provider/vlan/cisco.rb
@@ -0,0 +1,34 @@
+require 'puppet/util/network_device/cisco/device'
+require 'puppet/provider/network_device'
+
+Puppet::Type.type(:vlan).provide :cisco, :parent => Puppet::Provider::NetworkDevice do
+
+ desc "Cisco switch/router provider for vlans."
+
+ mk_resource_methods
+
+ def self.lookup(url, id)
+ vlans = {}
+ device = Puppet::Util::NetworkDevice::Cisco::Device.new(url)
+ device.command do |d|
+ vlans = d.parse_vlans || {}
+ end
+ vlans[id]
+ end
+
+ def initialize(*args)
+ super
+ end
+
+ # Clear out the cached values.
+ def flush
+ device.command do |device|
+ device.update_vlan(resource[:name], 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 000000000..7560a0552
--- /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 000000000..648389d39
--- /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@host/."
+ isnamevar
+ end
+ end
+end
diff --git a/lib/puppet/type/vlan.rb b/lib/puppet/type/vlan.rb
new file mode 100644
index 000000000..6708ea4f5
--- /dev/null
+++ b/lib/puppet/type/vlan.rb
@@ -0,0 +1,24 @@
+#
+# Manages a Vlan on a given router or switch
+#
+
+Puppet::Type.newtype(:vlan) do
+ @doc = "This represents a router or switch vlan."
+
+ ensurable
+
+ newparam(:name) do
+ desc "Vlan id. It must be a number"
+ isnamevar
+
+ newvalues(/^\d+/)
+ end
+
+ newproperty(:description) do
+ desc "Vlan name"
+ end
+
+ newparam(:device_url) do
+ desc "Url to connect to a router or switch."
+ end
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device.rb b/lib/puppet/util/network_device.rb
new file mode 100644
index 000000000..bca66016b
--- /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 000000000..ff96c8693
--- /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 000000000..c03a00104
--- /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 000000000..1f350991a
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco/device.rb
@@ -0,0 +1,246 @@
+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 = { :name => $1, :description => $2, :status => $3, :interfaces => [] }
+ if $4.strip.length > 0
+ vlan[:interfaces] = $4.strip.split(/\s*,\s*/).map{ |ifn| canonalize_ifname(ifn) }
+ end
+ vlans[vlan[:name]] = 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 update_vlan(id, is = {}, should = {})
+ if should[:ensure] == :absent
+ Puppet.info "Removing #{id} from device vlan"
+ transport.command("conf t")
+ transport.command("no vlan #{id}")
+ transport.command("exit")
+ return
+ end
+
+ # We're creating or updating an entry
+ transport.command("conf t")
+ transport.command("vlan #{id}")
+ [is.keys, should.keys].flatten.uniq.each do |property|
+ Puppet.debug("trying property: #{property}: #{should[property]}")
+ next if property != :description
+ transport.command("name #{should[property]}")
+ end
+ transport.command("exit")
+ transport.command("exit")
+ 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 000000000..63d5492a7
--- /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/lib/puppet/util/network_device/ipcalc.rb b/lib/puppet/util/network_device/ipcalc.rb
new file mode 100644
index 000000000..2b4f360b7
--- /dev/null
+++ b/lib/puppet/util/network_device/ipcalc.rb
@@ -0,0 +1,68 @@
+
+require 'puppet/util/network_device'
+module Puppet::Util::NetworkDevice::IPCalc
+
+ # This is a rip-off of authstore
+ Octet = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])'
+ IPv4 = "#{Octet}\.#{Octet}\.#{Octet}\.#{Octet}"
+ IPv6_full = "_:_:_:_:_:_:_:_|_:_:_:_:_:_::_?|_:_:_:_:_::((_:)?_)?|_:_:_:_::((_:){0,2}_)?|_:_:_::((_:){0,3}_)?|_:_::((_:){0,4}_)?|_::((_:){0,5}_)?|::((_:){0,6}_)?"
+ IPv6_partial = "_:_:_:_:_:_:|_:_:_:_::(_:)?|_:_::(_:){0,2}|_::(_:){0,3}"
+ IP = "#{IPv4}|#{IPv6_full}".gsub(/_/,'([0-9a-fA-F]{1,4})').gsub(/\(/,'(?:')
+
+ def parse(value)
+ case value
+ when /^(#{IP})\/(\d+)$/ # 12.34.56.78/24, a001:b002::efff/120, c444:1000:2000::9:192.168.0.1/112
+ [$2.to_i,IPAddr.new($1)]
+ when /^(#{IP})$/ # 10.20.30.40,
+ value = IPAddr.new(value)
+ [bits(value.family),value]
+ end
+ end
+
+ def bits(family)
+ family == Socket::AF_INET6 ? 128 : 32
+ end
+
+ def fullmask(family)
+ (1 << bits(family)) - 1
+ end
+
+ def mask(family, length)
+ (1 << (bits(family) - length)) - 1
+ end
+
+ # returns ip address netmask from prefix length
+ def netmask(family, length)
+ IPAddr.new(fullmask(family) & ~mask(family, length) , family)
+ end
+
+ # returns an IOS wildmask
+ def wildmask(family, length)
+ IPAddr.new(mask(family, length) , family)
+ end
+
+ # returns ip address prefix length from netmask
+ def prefix_length(netmask)
+ mask_addr = netmask.to_i
+ return 0 if mask_addr == 0
+ length=32
+ if (netmask.ipv6?)
+ length=128
+ end
+
+ mask = mask_addr < 2**length ? length : 128
+
+ mask.times do
+ if ((mask_addr & 1) == 1)
+ break
+ end
+ mask_addr = mask_addr >> 1
+ mask = mask - 1
+ end
+ mask
+ end
+
+ def linklocal?(ip)
+ end
+
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/transport.rb b/lib/puppet/util/network_device/transport.rb
new file mode 100644
index 000000000..e64fe9b69
--- /dev/null
+++ b/lib/puppet/util/network_device/transport.rb
@@ -0,0 +1,5 @@
+# stub
+module Puppet::Util::NetworkDevice
+ module Transport
+ end
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/transport/base.rb b/lib/puppet/util/network_device/transport/base.rb
new file mode 100644
index 000000000..1d62209cb
--- /dev/null
+++ b/lib/puppet/util/network_device/transport/base.rb
@@ -0,0 +1,26 @@
+
+require 'puppet/util/network_device'
+require 'puppet/util/network_device/transport'
+
+class Puppet::Util::NetworkDevice::Transport::Base
+ attr_accessor :user, :password, :host, :port
+ attr_accessor :default_prompt, :timeout
+
+ def initialize
+ @timeout = 10
+ end
+
+ def send(cmd)
+ end
+
+ def expect(prompt)
+ end
+
+ def command(cmd, options = {})
+ send(cmd)
+ expect(options[:prompt] || default_prompt) do |output|
+ yield output if block_given?
+ end
+ end
+
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/transport/ssh.rb b/lib/puppet/util/network_device/transport/ssh.rb
new file mode 100644
index 000000000..b3cf51b8a
--- /dev/null
+++ b/lib/puppet/util/network_device/transport/ssh.rb
@@ -0,0 +1,115 @@
+
+require 'puppet/util/network_device'
+require 'puppet/util/network_device/transport'
+require 'puppet/util/network_device/transport/base'
+require 'net/ssh'
+
+# This is an adaptation/simplification of gem net-ssh-telnet, which aims to have
+# a sane interface to Net::SSH. Credits goes to net-ssh-telnet authors
+class Puppet::Util::NetworkDevice::Transport::Ssh < Puppet::Util::NetworkDevice::Transport::Base
+
+ attr_accessor :buf, :ssh, :channel, :verbose
+
+ def initialize
+ super
+ end
+
+ def handles_login?
+ true
+ end
+
+ def eof?
+ !! @eof
+ end
+
+ def connect(&block)
+ @output = []
+ @channel_data = ""
+
+ begin
+ Puppet.debug("connecting to #{host} as #{user}")
+ @ssh = Net::SSH.start(host, user, :port => port, :password => password, :timeout => timeout)
+ rescue TimeoutError
+ raise TimeoutError, "timed out while opening an ssh connection to the host"
+ end
+
+ @buf = ""
+ @eof = false
+ @channel = nil
+ @ssh.open_channel do |channel|
+ channel.request_pty { |ch,success| raise "failed to open pty" unless success }
+
+ channel.send_channel_request("shell") do |ch, success|
+ raise "failed to open ssh shell channel" unless success
+
+ ch.on_data { |ch,data| @buf << data }
+ ch.on_extended_data { |ch,type,data| @buf << data if type == 1 }
+ ch.on_close { @eof = true }
+
+ @channel = ch
+ expect(default_prompt, &block)
+ # this is a little bit unorthodox, we're trying to escape
+ # the ssh loop there while still having the ssh connection up
+ # otherwise we wouldn't be able to return ssh stdout/stderr
+ # for a given call of command.
+ return
+ end
+
+ end
+ @ssh.loop
+
+ end
+
+ def close
+ @channel.close if @channel
+ @channel = nil
+ @ssh.close if @ssh
+ end
+
+ def expect(prompt)
+ line = ''
+ sock = @ssh.transport.socket
+
+ while not @eof
+ break if line =~ prompt and @buf == ''
+ break if sock.closed?
+
+ IO::select([sock], [sock], nil, nil)
+
+ process_ssh
+
+ # at this point we have accumulated some data in @buf
+ # or the channel has been closed
+ if @buf != ""
+ line += @buf.gsub(/\r\n/no, "\n")
+ @buf = ''
+ yield line if block_given?
+ elsif @eof
+ # channel has been closed
+ break if line =~ prompt
+ if line == ''
+ line = nil
+ yield nil if block_given?
+ end
+ break
+ end
+ end
+ Puppet.debug("ssh: expected #{line}") if @verbose
+ line
+ end
+
+ def send(line)
+ Puppet.debug("ssh: send #{line}") if @verbose
+ @channel.send_data(line + "\n")
+ end
+
+ def process_ssh
+ while @buf == "" and not eof?
+ begin
+ @channel.connection.process(0.1)
+ rescue IOError
+ @eof = true
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/transport/telnet.rb b/lib/puppet/util/network_device/transport/telnet.rb
new file mode 100644
index 000000000..e55079e06
--- /dev/null
+++ b/lib/puppet/util/network_device/transport/telnet.rb
@@ -0,0 +1,42 @@
+require 'puppet/util/network_device'
+require 'puppet/util/network_device/transport'
+require 'puppet/util/network_device/transport/base'
+require 'net/telnet'
+
+class Puppet::Util::NetworkDevice::Transport::Telnet < Puppet::Util::NetworkDevice::Transport::Base
+ def initialize
+ super
+ end
+
+ def handles_login?
+ false
+ end
+
+ def connect
+ @telnet = Net::Telnet::new("Host" => host, "Port" => port || 23,
+ "Timeout" => 10,
+ "Prompt" => default_prompt, "Output_log" => "/tmp/out.log")
+ end
+
+ def close
+ @telnet.close if @telnet
+ @telnet = nil
+ end
+
+ def expect(prompt)
+ @telnet.waitfor(prompt) do |out|
+ yield out if block_given?
+ end
+ end
+
+ def command(cmd, options = {})
+ send(cmd)
+ expect(options[:prompt] || default_prompt) do |output|
+ yield output if block_given?
+ end
+ end
+
+ def send(line)
+ @telnet.puts(line)
+ 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 000000000..7904711f5
--- /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/provider/network_device_spec.rb b/spec/unit/provider/network_device_spec.rb
new file mode 100644
index 000000000..3e6d382ee
--- /dev/null
+++ b/spec/unit/provider/network_device_spec.rb
@@ -0,0 +1,148 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+require 'puppet/provider/network_device'
+
+Puppet::Type.type(:vlan).provide :test, :parent => Puppet::Provider::NetworkDevice do
+ mk_resource_methods
+ def self.lookup(device_url, name)
+ end
+end
+
+provider_class = Puppet::Type.type(:vlan).provider(:test)
+
+describe provider_class do
+ before do
+ @resource = stub("resource", :name => "test")
+ @provider = provider_class.new(@resource)
+ end
+
+ it "should be able to prefetch instances from the device" do
+ provider_class.should respond_to(:prefetch)
+ end
+
+ it "should have an instances method" do
+ provider_class.should respond_to(:instances)
+ end
+
+ describe "when prefetching" do
+ before do
+ @resource = stub_everything 'resource'
+ @resources = {"200" => @resource}
+ provider_class.stubs(:lookup)
+ end
+
+ it "should lookup an entry for each passed resource" do
+ provider_class.expects(:lookup).with(nil, "200").returns nil
+
+ provider_class.stubs(:new)
+ @resource.stubs(:provider=)
+ provider_class.prefetch(@resources)
+ end
+
+ describe "resources that do not exist" do
+ it "should create a provider with :ensure => :absent" do
+ provider_class.stubs(:lookup).returns(nil)
+ provider_class.expects(:new).with(:ensure => :absent).returns "myprovider"
+ @resource.expects(:provider=).with("myprovider")
+ provider_class.prefetch(@resources)
+ end
+ end
+
+ describe "resources that exist" do
+ it "should create a provider with the results of the find and ensure at present" do
+ provider_class.stubs(:lookup).returns({ :name => "200", :description => "myvlan"})
+
+ provider_class.expects(:new).with(:name => "200", :description => "myvlan", :ensure => :present).returns "myprovider"
+ @resource.expects(:provider=).with("myprovider")
+
+ provider_class.prefetch(@resources)
+ end
+ end
+ end
+
+ describe "when being initialized" do
+ describe "with a hash" do
+ before do
+ @resource_class = mock 'resource_class'
+ provider_class.stubs(:resource_type).returns @resource_class
+
+ @property_class = stub 'property_class', :array_matching => :all, :superclass => Puppet::Property
+ @resource_class.stubs(:attrclass).with(:one).returns(@property_class)
+ @resource_class.stubs(:valid_parameter?).returns true
+ end
+
+ it "should store a copy of the hash as its vlan_properties" do
+ instance = provider_class.new(:one => :two)
+ instance.former_properties.should == {:one => :two}
+ end
+ end
+ end
+
+ describe "when an instance" do
+ before do
+ @instance = provider_class.new
+
+ @property_class = stub 'property_class', :array_matching => :all, :superclass => Puppet::Property
+ @resource_class = stub 'resource_class', :attrclass => @property_class, :valid_parameter? => true, :validproperties => [:description]
+ provider_class.stubs(:resource_type).returns @resource_class
+ end
+
+ it "should have a method for creating the instance" do
+ @instance.should respond_to(:create)
+ end
+
+ it "should have a method for removing the instance" do
+ @instance.should respond_to(:destroy)
+ end
+
+ it "should indicate when the instance already exists" do
+ @instance = provider_class.new(:ensure => :present)
+ @instance.exists?.should be_true
+ end
+
+ it "should indicate when the instance does not exist" do
+ @instance = provider_class.new(:ensure => :absent)
+ @instance.exists?.should be_false
+ end
+
+ describe "is being flushed" do
+ it "should flush properties" do
+ @instance = provider_class.new(:ensure => :present, :name => "200", :description => "myvlan")
+ @instance.flush
+ @instance.properties.should be_empty
+ end
+ end
+
+ describe "is being created" do
+ before do
+ @rclass = mock 'resource_class'
+ @rclass.stubs(:validproperties).returns([:description])
+ @resource = stub_everything 'resource'
+ @resource.stubs(:class).returns @rclass
+ @resource.stubs(:should).returns nil
+ @instance.stubs(:resource).returns @resource
+ end
+
+ it "should set its :ensure value to :present" do
+ @instance.create
+ @instance.properties[:ensure].should == :present
+ end
+
+ it "should set all of the other attributes from the resource" do
+ @resource.expects(:should).with(:description).returns "myvlan"
+
+ @instance.create
+ @instance.properties[:description].should == "myvlan"
+ end
+ end
+
+ describe "is being destroyed" do
+ it "should set its :ensure value to :absent" do
+ @instance.destroy
+ @instance.properties[:ensure].should == :absent
+ end
+ end
+ end
+end
diff --git a/spec/unit/provider/vlan/cisco_spec.rb b/spec/unit/provider/vlan/cisco_spec.rb
new file mode 100644
index 000000000..0951367e6
--- /dev/null
+++ b/spec/unit/provider/vlan/cisco_spec.rb
@@ -0,0 +1,62 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/provider/vlan/cisco'
+
+provider_class = Puppet::Type.type(:vlan).provider(:cisco)
+
+describe provider_class do
+ before do
+ @resource = stub("resource", :name => "200")
+ @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, "200")
+ end
+
+ it "should delegate to the device vlans" do
+ @device.expects(:parse_vlans)
+ provider_class.lookup("", "200")
+ end
+
+ it "should return only the given vlan" do
+ @device.expects(:parse_vlans).returns({"200" => { :description => "thisone" }, "1" => { :description => "nothisone" }})
+ provider_class.lookup("", "200").should == {:description => "thisone" }
+ end
+
+ end
+
+ describe "when an instance is being flushed" do
+ it "should call the device update_vlan method with its vlan id, current attributes, and desired attributes" do
+ @instance = provider_class.new(:ensure => :present, :name => "200", :description => "myvlan")
+ @instance.description = "myvlan2"
+ @instance.resource = @resource
+ @resource.stubs(:[]).with(:name).returns("200")
+ device = stub_everything 'device'
+ @instance.stubs(:device).returns(device)
+ device.expects(:command).yields(device)
+ device.expects(:update_vlan).with(@instance.name, {:ensure => :present, :name => "200", :description => "myvlan"},
+ {:ensure => :present, :name => "200", :description => "myvlan2"})
+
+ @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 000000000..630e45aa9
--- /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/type/vlan_spec.rb b/spec/unit/type/vlan_spec.rb
new file mode 100644
index 000000000..607d7116d
--- /dev/null
+++ b/spec/unit/type/vlan_spec.rb
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Puppet::Type.type(:vlan) do
+ it "should have a 'name' parameter'" do
+ Puppet::Type.type(:vlan).new(:name => "200")[:name].should == "200"
+ end
+
+ it "should have a 'device_url' parameter'" do
+ Puppet::Type.type(:vlan).new(:name => "200", :device_url => :device)[:device_url].should == :device
+ end
+
+ it "should have an ensure property" do
+ Puppet::Type.type(:vlan).attrtype(:ensure).should == :property
+ end
+
+ it "should have a description property" do
+ Puppet::Type.type(:vlan).attrtype(:description).should == :property
+ end
+
+ describe "when validating attribute values" do
+ before do
+ @provider = stub 'provider', :class => Puppet::Type.type(:vlan).defaultprovider, :clear => nil
+ Puppet::Type.type(:vlan).defaultprovider.stubs(:new).returns(@provider)
+ end
+
+ it "should support :present as a value to :ensure" do
+ Puppet::Type.type(:vlan).new(:name => "200", :ensure => :present)
+ end
+
+ it "should support :absent as a value to :ensure" do
+ Puppet::Type.type(:vlan).new(:name => "200", :ensure => :absent)
+ end
+
+ it "should fail if vlan name is not a number" do
+ lambda { Puppet::Type.type(:vlan).new(:name => "notanumber", :ensure => :present) }.should raise_error
+ 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 000000000..31aec920e
--- /dev/null
+++ b/spec/unit/util/network_device/cisco/device_spec.rb
@@ -0,0 +1,521 @@
+#!/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 updating device vlans" do
+ describe "when removing a vlan" do
+ it "should issue the no vlan command" do
+ @transport.expects(:command).with("no vlan 200")
+ @cisco.update_vlan("200", {:ensure => :present, :name => "200"}, { :ensure=> :absent})
+ end
+ end
+
+ describe "when updating a vlan" do
+ it "should issue the vlan command to enter global vlan modifications" do
+ @transport.expects(:command).with("vlan 200")
+ @cisco.update_vlan("200", {:ensure => :present, :name => "200"}, { :ensure=> :present, :name => "200"})
+ end
+
+ it "should issue the name command to modify the vlan description" do
+ @transport.expects(:command).with("name myvlan")
+ @cisco.update_vlan("200", {:ensure => :present, :name => "200"}, { :ensure=> :present, :name => "200", :description => "myvlan"})
+ end
+ 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"], :description=>"management", :name=>"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"], :description=>"default", :name=>"1"}, "10"=>{:status=>"active", :interfaces=>[], :description=>"VLAN0010", :name=>"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 000000000..f6aa14747
--- /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",@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
diff --git a/spec/unit/util/network_device/ipcalc_spec.rb b/spec/unit/util/network_device/ipcalc_spec.rb
new file mode 100644
index 000000000..6f55a66e4
--- /dev/null
+++ b/spec/unit/util/network_device/ipcalc_spec.rb
@@ -0,0 +1,63 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+require 'puppet/util/network_device/ipcalc'
+
+describe Puppet::Util::NetworkDevice::IPCalc do
+ class TestIPCalc
+ include Puppet::Util::NetworkDevice::IPCalc
+ end
+
+ before(:each) do
+ @ipcalc = TestIPCalc.new
+ end
+
+ describe "when parsing ip/prefix" do
+ it "should parse ipv4 without prefixes" do
+ @ipcalc.parse('127.0.0.1').should == [32,IPAddr.new('127.0.0.1')]
+ end
+
+ it "should parse ipv4 with prefixes" do
+ @ipcalc.parse('127.0.1.2/8').should == [8,IPAddr.new('127.0.1.2')]
+ end
+
+ it "should parse ipv6 without prefixes" do
+ @ipcalc.parse('FE80::21A:2FFF:FE30:ECF0').should == [128,IPAddr.new('FE80::21A:2FFF:FE30:ECF0')]
+ end
+
+ it "should parse ipv6 with prefixes" do
+ @ipcalc.parse('FE80::21A:2FFF:FE30:ECF0/56').should == [56,IPAddr.new('FE80::21A:2FFF:FE30:ECF0')]
+ end
+ end
+
+ describe "when building netmask" do
+ it "should produce the correct ipv4 netmask from prefix length" do
+ @ipcalc.netmask(Socket::AF_INET, 27).should == IPAddr.new('255.255.255.224')
+ end
+
+ it "should produce the correct ipv6 netmask from prefix length" do
+ @ipcalc.netmask(Socket::AF_INET6, 56).should == IPAddr.new('ffff:ffff:ffff:ff00::0')
+ end
+ end
+
+ describe "when building wildmask" do
+ it "should produce the correct ipv4 wildmask from prefix length" do
+ @ipcalc.wildmask(Socket::AF_INET, 27).should == IPAddr.new('0.0.0.31')
+ end
+
+ it "should produce the correct ipv6 wildmask from prefix length" do
+ @ipcalc.wildmask(Socket::AF_INET6, 126).should == IPAddr.new('::3')
+ end
+ end
+
+ describe "when computing prefix length from netmask" do
+ it "should produce the correct ipv4 prefix length" do
+ @ipcalc.prefix_length(IPAddr.new('255.255.255.224')).should == 27
+ end
+
+ it "should produce the correct ipv6 prefix length" do
+ @ipcalc.prefix_length(IPAddr.new('fffe::0')).should == 15
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/unit/util/network_device/transport/base_spec.rb b/spec/unit/util/network_device/transport/base_spec.rb
new file mode 100644
index 000000000..5d52574f7
--- /dev/null
+++ b/spec/unit/util/network_device/transport/base_spec.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+
+require 'puppet/util/network_device/transport/base'
+
+describe Puppet::Util::NetworkDevice::Transport::Base do
+ class TestTransport < Puppet::Util::NetworkDevice::Transport::Base
+ end
+
+ before(:each) do
+ @transport = TestTransport.new
+ end
+
+ describe "when sending commands" do
+ it "should send the command to the telnet session" do
+ @transport.expects(:send).with("line")
+ @transport.command("line")
+ end
+
+ it "should expect an output matching the given prompt" do
+ @transport.expects(:expect).with(/prompt/)
+ @transport.command("line", :prompt => /prompt/)
+ end
+
+ it "should expect an output matching the default prompt" do
+ @transport.default_prompt = /defprompt/
+ @transport.expects(:expect).with(/defprompt/)
+ @transport.command("line")
+ end
+
+ it "should yield telnet output to the given block" do
+ @transport.expects(:expect).yields("output")
+ @transport.command("line") { |out| out.should == "output" }
+ end
+
+ it "should return telnet output to the caller" do
+ @transport.expects(:expect).returns("output")
+ @transport.command("line").should == "output"
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/unit/util/network_device/transport/ssh_spec.rb b/spec/unit/util/network_device/transport/ssh_spec.rb
new file mode 100644
index 000000000..706dee43a
--- /dev/null
+++ b/spec/unit/util/network_device/transport/ssh_spec.rb
@@ -0,0 +1,211 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+
+require 'puppet/util/network_device/transport/ssh'
+
+describe Puppet::Util::NetworkDevice::Transport::Ssh, :if => Puppet.features.ssh? do
+
+ before(:each) do
+ @transport = Puppet::Util::NetworkDevice::Transport::Ssh.new()
+ end
+
+ it "should handle login through the transport" do
+ @transport.should be_handles_login
+ end
+
+ it "should connect to the given host and port" do
+ Net::SSH.expects(:start).with { |host, user, args| host == "localhost" && args[:port] == 22 }.returns stub_everything
+ @transport.host = "localhost"
+ @transport.port = 22
+
+ @transport.connect
+ end
+
+ it "should connect using the given username and password" do
+ Net::SSH.expects(:start).with { |host, user, args| user == "user" && args[:password] == "pass" }.returns stub_everything
+ @transport.user = "user"
+ @transport.password = "pass"
+
+ @transport.connect
+ end
+
+ describe "when connected" do
+ before(:each) do
+ @ssh = stub_everything 'ssh'
+ @channel = stub_everything 'channel'
+ Net::SSH.stubs(:start).returns @ssh
+ @ssh.stubs(:open_channel).yields(@channel)
+ @transport.stubs(:expect)
+ end
+
+ it "should open a channel" do
+ @ssh.expects(:open_channel)
+
+ @transport.connect
+ end
+
+ it "should request a pty" do
+ @channel.expects(:request_pty)
+
+ @transport.connect
+ end
+
+ it "should create a shell channel" do
+ @channel.expects(:send_channel_request).with("shell")
+ @transport.connect
+ end
+
+ it "should raise an error if shell channel creation fails" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, false)
+ lambda { @transport.connect }.should raise_error
+ end
+
+ it "should register an on_data and on_extended_data callback" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, true)
+ @channel.expects(:on_data)
+ @channel.expects(:on_extended_data)
+ @transport.connect
+ end
+
+ it "should accumulate data to the buffer on data" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, true)
+ @channel.expects(:on_data).yields(@channel, "data")
+
+ @transport.connect
+ @transport.buf.should == "data"
+ end
+
+ it "should accumulate data to the buffer on extended data" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, true)
+ @channel.expects(:on_extended_data).yields(@channel, 1, "data")
+
+ @transport.connect
+ @transport.buf.should == "data"
+ end
+
+ it "should mark eof on close" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, true)
+ @channel.expects(:on_close).yields(@channel)
+
+ @transport.connect
+ @transport.should be_eof
+ end
+
+ it "should expect output to conform to the default prompt" do
+ @channel.expects(:send_channel_request).with("shell").yields(@channel, true)
+ @transport.expects(:default_prompt).returns("prompt")
+ @transport.expects(:expect).with("prompt")
+ @transport.connect
+ end
+
+ it "should start the ssh loop" do
+ @ssh.expects(:loop)
+ @transport.connect
+ end
+ end
+
+ describe "when closing" do
+ before(:each) do
+ @ssh = stub_everything 'ssh'
+ @channel = stub_everything 'channel'
+ Net::SSH.stubs(:start).returns @ssh
+ @ssh.stubs(:open_channel).yields(@channel)
+ @channel.stubs(:send_channel_request).with("shell").yields(@channel, true)
+ @transport.stubs(:expect)
+ @transport.connect
+ end
+
+ it "should close the channel" do
+ @channel.expects(:close)
+ @transport.close
+ end
+
+ it "should close the ssh session" do
+ @ssh.expects(:close)
+ @transport.close
+ end
+ end
+
+ describe "when sending commands" do
+ before(:each) do
+ @ssh = stub_everything 'ssh'
+ @channel = stub_everything 'channel'
+ Net::SSH.stubs(:start).returns @ssh
+ @ssh.stubs(:open_channel).yields(@channel)
+ @channel.stubs(:send_channel_request).with("shell").yields(@channel, true)
+ @transport.stubs(:expect)
+ @transport.connect
+ end
+
+ it "should send data to the ssh channel" do
+ @channel.expects(:send_data).with("data\n")
+ @transport.command("data")
+ end
+
+ it "should expect the default prompt afterward" do
+ @transport.expects(:default_prompt).returns("prompt")
+ @transport.expects(:expect).with("prompt")
+ @transport.command("data")
+ end
+
+ it "should expect the given prompt" do
+ @transport.expects(:expect).with("myprompt")
+ @transport.command("data", :prompt => "myprompt")
+ end
+
+ it "should yield the buffer output to given block" do
+ @transport.expects(:expect).yields("output")
+ @transport.command("data") do |out|
+ out.should == "output"
+ end
+ end
+
+ it "should return buffer output" do
+ @transport.expects(:expect).returns("output")
+ @transport.command("data").should == "output"
+ end
+ end
+
+ describe "when expecting output" do
+ before(:each) do
+ @connection = stub_everything 'connection'
+ @socket = stub_everything 'socket'
+ transport = stub 'transport', :socket => @socket
+ @ssh = stub_everything 'ssh', :transport => transport
+ @channel = stub_everything 'channel', :connection => @connection
+ @transport.ssh = @ssh
+ @transport.channel = @channel
+ end
+
+ it "should process the ssh event loop" do
+ IO.stubs(:select)
+ @transport.buf = "output"
+ @transport.expects(:process_ssh)
+ @transport.expect(/output/)
+ end
+
+ it "should return the output" do
+ IO.stubs(:select)
+ @transport.buf = "output"
+ @transport.stubs(:process_ssh)
+ @transport.expect(/output/).should == "output"
+ end
+
+ it "should return the output" do
+ IO.stubs(:select)
+ @transport.buf = "output"
+ @transport.stubs(:process_ssh)
+ @transport.expect(/output/).should == "output"
+ end
+
+ describe "when processing the ssh loop" do
+ it "should advance one tick in the ssh event loop and exit on eof" do
+ @transport.buf = ''
+ @connection.expects(:process).then.raises(EOFError)
+ @transport.process_ssh
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/util/network_device/transport/telnet_spec.rb b/spec/unit/util/network_device/transport/telnet_spec.rb
new file mode 100644
index 000000000..7499b528e
--- /dev/null
+++ b/spec/unit/util/network_device/transport/telnet_spec.rb
@@ -0,0 +1,76 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../../spec_helper'
+
+require 'puppet/util/network_device/transport/telnet'
+
+describe Puppet::Util::NetworkDevice::Transport::Telnet do
+
+ before(:each) do
+ @transport = Puppet::Util::NetworkDevice::Transport::Telnet.new()
+ end
+
+ it "should not handle login through the transport" do
+ @transport.should_not be_handles_login
+ end
+
+ it "should connect to the given host and port" do
+ Net::Telnet.expects(:new).with { |args| args["Host"] == "localhost" && args["Port"] == 23 }.returns stub_everything
+ @transport.host = "localhost"
+ @transport.port = 23
+
+ @transport.connect
+ end
+
+ it "should connect and specify the default prompt" do
+ @transport.default_prompt = "prompt"
+ Net::Telnet.expects(:new).with { |args| args["Prompt"] == "prompt" }.returns stub_everything
+ @transport.host = "localhost"
+ @transport.port = 23
+
+ @transport.connect
+ end
+
+ describe "when connected" do
+ before(:each) do
+ @telnet = stub_everything 'telnet'
+ Net::Telnet.stubs(:new).returns(@telnet)
+ @transport.connect
+ end
+
+ it "should send line to the telnet session" do
+ @telnet.expects(:puts).with("line")
+ @transport.send("line")
+ end
+
+ describe "when expecting output" do
+ it "should waitfor output on the telnet session" do
+ @telnet.expects(:waitfor).with(/regex/)
+ @transport.expect(/regex/)
+ end
+
+ it "should return telnet session output" do
+ @telnet.expects(:waitfor).returns("output")
+ @transport.expect(/regex/).should == "output"
+ end
+
+ it "should yield telnet session output to the given block" do
+ @telnet.expects(:waitfor).yields("output")
+ @transport.expect(/regex/) { |out| out.should == "output" }
+ end
+ end
+ end
+
+ describe "when closing" do
+ before(:each) do
+ @telnet = stub_everything 'telnet'
+ Net::Telnet.stubs(:new).returns(@telnet)
+ @transport.connect
+ end
+
+ it "should close the telnet session" do
+ @telnet.expects(:close)
+ @transport.close
+ end
+ end
+end \ No newline at end of file