diff --git a/lib/puppet/provider/interface/base.rb b/lib/puppet/provider/interface/base.rb new file mode 100644 index 000000000..e69de29bb 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/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/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..97489bd8c --- /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 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/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/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/util/network_device/cisco/device_spec.rb b/spec/unit/util/network_device/cisco/device_spec.rb new file mode 100644 index 000000000..9021bbd2c --- /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(< "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(< :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(<{: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(< :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(< :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(< :access, :native_vlan => "100" } + end + + it "should parse ip addresses" do + @transport.stubs(:command).with("sh running-config interface Vlan 1 | begin interface").returns(<[[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(<"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