diff options
| author | James Turnbull <james@lovedthanlost.net> | 2011-04-12 05:15:00 +1000 |
|---|---|---|
| committer | James Turnbull <james@lovedthanlost.net> | 2011-04-12 05:15:00 +1000 |
| commit | dce851cac79393f86950f4ebfc48b9ac67dcd8f7 (patch) | |
| tree | cd2a4a92183b43eba985633f34de6557937b9e37 /lib/puppet/util | |
| parent | 46d67fd86819d1dfe4813f22b192213985e3a587 (diff) | |
| parent | 49dcc24195d262437cc35997a811eb724d65b48b (diff) | |
| download | puppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.tar.gz puppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.tar.xz puppet-dce851cac79393f86950f4ebfc48b9ac67dcd8f7.zip | |
Merge branch 'tickets/master/7021' into next
* tickets/master/7021:
Updated confine in Spec test for RSpec 2
Add management of router/switchs global vlans
Cisco Switch/Router Interface management
Base class for network device based providers
Ssh transport for network device management
Telnet transport to connect to remote network device
Remote Network Device transport system
Introduce a module for some IP computations
Diffstat (limited to 'lib/puppet/util')
| -rw-r--r-- | lib/puppet/util/network_device.rb | 2 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/base.rb | 29 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/cisco.rb | 4 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/cisco/device.rb | 246 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/cisco/interface.rb | 82 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/ipcalc.rb | 68 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/transport.rb | 5 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/transport/base.rb | 26 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/transport/ssh.rb | 115 | ||||
| -rw-r--r-- | lib/puppet/util/network_device/transport/telnet.rb | 42 |
10 files changed, 619 insertions, 0 deletions
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 |
