diff --git a/lib/puppet/util/network_device/cisco/device.rb b/lib/puppet/util/network_device/cisco/device.rb index f19036648..8efe5902d 100644 --- a/lib/puppet/util/network_device/cisco/device.rb +++ b/lib/puppet/util/network_device/cisco/device.rb @@ -1,266 +1,264 @@ 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 'puppet/util/network_device/cisco/facts' 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 connect transport.connect login transport.command("terminal length 0") do |out| enable if out =~ />\s?\z/n end find_capabilities end def disconnect transport.close end def command(cmd = nil) - Puppet.debug("command #{cmd}") connect out = execute(cmd) if cmd yield self if block_given? disconnect out end def execute(cmd) - Puppet.debug("Executing: #{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 = execute("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}, :GigabitEthernet => %w{GigabitEthernet GigEthernet GigEth GE Gi G}, :TenGigabitEthernet => %w{TenGigabitEthernet TE Te}, :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 } found = /^#{found}(.+)\Z/i.match(interface) return "#{k.to_s}#{found[1]}".gsub(/\s+/,'') end end interface end def facts @facts ||= Puppet::Util::NetworkDevice::Cisco::Facts.new(transport) facts = {} command do |ng| facts = @facts.retrieve end facts 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 = execute("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 = execute("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 = execute(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" execute("conf t") execute("no vlan #{id}") execute("exit") return end # We're creating or updating an entry execute("conf t") execute("vlan #{id}") [is.keys, should.keys].flatten.uniq.each do |property| Puppet.debug("trying property: #{property}: #{should[property]}") next if property != :description execute("name #{should[property]}") end execute("exit") execute("exit") end def parse_trunking(interface) trunking = {} out = execute("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/ipcalc.rb b/lib/puppet/util/network_device/ipcalc.rb index b2e3aa673..8ca5295a7 100644 --- a/lib/puppet/util/network_device/ipcalc.rb +++ b/lib/puppet/util/network_device/ipcalc.rb @@ -1,68 +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 diff --git a/lib/puppet/util/network_device/transport/ssh.rb b/lib/puppet/util/network_device/transport/ssh.rb index dca5600bd..fb738ea40 100644 --- a/lib/puppet/util/network_device/transport/ssh.rb +++ b/lib/puppet/util/network_device/transport/ssh.rb @@ -1,121 +1,121 @@ require 'puppet/util/network_device' require 'puppet/util/network_device/transport' require 'puppet/util/network_device/transport/base' # 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 + attr_accessor :buf, :ssh, :channel def initialize super unless Puppet.features.ssh? raise 'Connecting with ssh to a network device requires the \'net/ssh\' ruby library' end 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" rescue Net::SSH::AuthenticationFailed raise Puppet::Error, "SSH authentication failure connecting to #{host} as #{user}" rescue Net::SSH::Exception => detail raise Puppet::Error, "SSH connection failure to #{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 + Puppet.debug("ssh: expected #{line}") line end def send(line) - Puppet.debug("ssh: send #{line}") if @verbose + Puppet.debug("ssh: send #{line}") @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 diff --git a/lib/puppet/util/network_device/transport/telnet.rb b/lib/puppet/util/network_device/transport/telnet.rb index e9322f81b..5c66f4bea 100644 --- a/lib/puppet/util/network_device/transport/telnet.rb +++ b/lib/puppet/util/network_device/transport/telnet.rb @@ -1,42 +1,43 @@ 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) 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) + Puppet.debug("telnet: send #{line}") @telnet.puts(line) end end