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 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(< "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(< :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"], :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(< :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 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