summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorBrice Figureau <brice-puppet@daysofwonder.com>2011-01-03 19:47:03 +0100
committerJames Turnbull <james@lovedthanlost.net>2011-04-08 18:19:54 +1000
commit1cb18410732a4b51efa0a106d4a1437daef08fc5 (patch)
treef1a66bfa5abfe1631211d51ffdec00038cc93054 /lib
parent596571fd2b03957e7ed185856ee649c1e610716c (diff)
downloadpuppet-1cb18410732a4b51efa0a106d4a1437daef08fc5.tar.gz
puppet-1cb18410732a4b51efa0a106d4a1437daef08fc5.tar.xz
puppet-1cb18410732a4b51efa0a106d4a1437daef08fc5.zip
Cisco Switch/Router Interface management
This patch introduces managing remotely cisco IOS network devices through ssh or telnet with a puppet type/provider. This patch allows to manage router/switch interface with the interface type: interface { "FastEthernet 0/1": device_url => "ssh://user:pass@cisco2960.domain.com/", mode => trunk, encapsulation => dot1q, trunk_allowed_vlans => "1-99,200,253", description => "to back bone router" } It is possible with this patch to set interface: * mode (access or trunk) * native vlan (only for access mode) * speed (auto or a given speed) * duplex (auto, half or full) * trunk encapsulation * allowed trunk vlan * ipv4 addresses * ipv6 addresses * etherchannel membership The interface name (at least for the cisco provider) can be any shorthand interface name a switch or router can use. The device url should conform to: * scheme: either telnet or ssh * user: can be absent depending on switch/router line config * pass: must be present * port: optional * an optional enable password can be mentioned in the url query string Ex: To connect to a switch with a line password and an enable password: "telnet://:letmein@cisco29224XL.domain.com/?enable=letmeinagain" To connect to a switch/router through ssh and a privileged user: "ssh://brice:letmein@cisco1841L.domain.com/" Note: This patch only includes a Cisco IOS provider. Also terminology adopted in the various types are mostly the ones used in Cisco devices. This patch was tested against: * (really old) Cisco switch 2924XL with ios 12.0(5)WC10 * Cisco router 1841 with ios 12.4(15)T8 * Cisco router 877 with ios 12.4(11)XJ4 * Cisco switch 2960G with ios 12.2(44)SE Signed-off-by: Brice Figureau <brice-puppet@daysofwonder.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/puppet/provider/interface/base.rb0
-rw-r--r--lib/puppet/provider/interface/cisco.rb33
-rw-r--r--lib/puppet/type/interface.rb107
-rw-r--r--lib/puppet/type/router.rb14
-rw-r--r--lib/puppet/util/network_device.rb2
-rw-r--r--lib/puppet/util/network_device/base.rb29
-rw-r--r--lib/puppet/util/network_device/cisco.rb4
-rw-r--r--lib/puppet/util/network_device/cisco/device.rb225
-rw-r--r--lib/puppet/util/network_device/cisco/interface.rb82
9 files changed, 496 insertions, 0 deletions
diff --git a/lib/puppet/provider/interface/base.rb b/lib/puppet/provider/interface/base.rb
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/puppet/provider/interface/base.rb
diff --git a/lib/puppet/provider/interface/cisco.rb b/lib/puppet/provider/interface/cisco.rb
new file mode 100644
index 000000000..f3bd202e9
--- /dev/null
+++ b/lib/puppet/provider/interface/cisco.rb
@@ -0,0 +1,33 @@
+require 'puppet/util/network_device/cisco/device'
+require 'puppet/provider/network_device'
+
+Puppet::Type.type(:interface).provide :cisco, :parent => Puppet::Provider::NetworkDevice do
+
+ desc "Cisco switch/router provider for interface."
+
+ mk_resource_methods
+
+ def self.lookup(url, name)
+ interface = nil
+ network_gear = Puppet::Util::NetworkDevice::Cisco::Device.new(url)
+ network_gear.command do |ng|
+ interface = network_gear.interface(name)
+ end
+ interface
+ end
+
+ def initialize(*args)
+ super
+ end
+
+ def flush
+ device.command do |device|
+ device.new_interface(name).update(former_properties, properties)
+ end
+ super
+ end
+
+ def device
+ @device ||= Puppet::Util::NetworkDevice::Cisco::Device.new(resource[:device_url])
+ end
+end
diff --git a/lib/puppet/type/interface.rb b/lib/puppet/type/interface.rb
new file mode 100644
index 000000000..7560a0552
--- /dev/null
+++ b/lib/puppet/type/interface.rb
@@ -0,0 +1,107 @@
+#
+# Manages an interface on a given router or switch
+#
+
+require 'puppet/util/network_device/ipcalc'
+
+Puppet::Type.newtype(:interface) do
+
+ @doc = "This represents a router or switch interface. It is possible to manage
+ interface mode (access or trunking, native vlan and encapsulation),
+ switchport characteristics (speed, duplex)."
+
+ ensurable do
+ defaultvalues
+
+ aliasvalue :shutdown, :absent
+ aliasvalue :no_shutdown, :present
+
+ defaultto { :no_shutdown }
+ end
+
+ newparam(:name) do
+ desc "Interface name"
+ end
+
+ newparam(:device_url) do
+ desc "Url to connect to a router or switch."
+ end
+
+ newproperty(:description) do
+ desc "Interface description."
+
+ defaultto { @resource[:name] }
+ end
+
+ newproperty(:speed) do
+ desc "Interface speed."
+ newvalues(:auto, /^\d+/)
+ end
+
+ newproperty(:duplex) do
+ desc "Interface duplex."
+ newvalues(:auto, :full, :half)
+ end
+
+ newproperty(:native_vlan) do
+ desc "Interface native vlan (for access mode only)."
+ newvalues(/^\d+/)
+ end
+
+ newproperty(:encapsulation) do
+ desc "Interface switchport encapsulation."
+ newvalues(:none, :dot1q, :isl )
+ end
+
+ newproperty(:mode) do
+ desc "Interface switchport mode."
+ newvalues(:access, :trunk)
+ end
+
+ newproperty(:allowed_trunk_vlans) do
+ desc "Allowed list of Vlans that this trunk can forward."
+ newvalues(:all, /./)
+ end
+
+ newproperty(:etherchannel) do
+ desc "Channel group this interface is part of."
+ newvalues(/^\d+/)
+ end
+
+ newproperty(:ipaddress, :array_matching => :all) do
+ include Puppet::Util::NetworkDevice::IPCalc
+
+ desc "IP Address of this interface (it might not be possible to set an interface IP address
+ it depends on the interface type and device type).
+ Valid format of ip addresses are:
+ * IPV4, like 127.0.0.1
+ * IPV4/prefixlength like 127.0.1.1/24
+ * IPV6/prefixlength like FE80::21A:2FFF:FE30:ECF0/128
+ * an optional suffix for IPV6 addresses from this list: eui-64, link-local
+ It is also possible to use an array of values.
+ "
+
+ validate do |values|
+ values = [values] unless values.is_a?(Array)
+ values.each do |value|
+ self.fail "Invalid interface ip address" unless parse(value.gsub(/\s*(eui-64|link-local)\s*$/,''))
+ end
+ end
+
+ munge do |value|
+ option = value =~ /eui-64|link-local/i ? value.gsub(/^.*?\s*(eui-64|link-local)\s*$/,'\1') : nil
+ [parse(value.gsub(/\s*(eui-64|link-local)\s*$/,'')), option].flatten
+ end
+
+ def value_to_s(value)
+ value = [value] unless value.is_a?(Array)
+ value.map{ |v| "#{v[1].to_s}/#{v[0]} #{v[2]}"}.join(",")
+ end
+
+ def change_to_s(currentvalue, newvalue)
+ currentvalue = value_to_s(currentvalue) if currentvalue != :absent
+ newvalue = value_to_s(newvalue)
+ super(currentvalue, newvalue)
+ end
+ end
+end
diff --git a/lib/puppet/type/router.rb b/lib/puppet/type/router.rb
new file mode 100644
index 000000000..648389d39
--- /dev/null
+++ b/lib/puppet/type/router.rb
@@ -0,0 +1,14 @@
+#
+# Manage a router abstraction
+#
+
+module Puppet
+ newtype(:router) do
+ @doc = "Manages connected router."
+
+ newparam(:url) do
+ desc "An URL to access the router of the form (ssh|telnet)://user:pass:enable@host/."
+ isnamevar
+ end
+ end
+end
diff --git a/lib/puppet/util/network_device.rb b/lib/puppet/util/network_device.rb
new file mode 100644
index 000000000..bca66016b
--- /dev/null
+++ b/lib/puppet/util/network_device.rb
@@ -0,0 +1,2 @@
+module Puppet::Util::NetworkDevice
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/base.rb b/lib/puppet/util/network_device/base.rb
new file mode 100644
index 000000000..ff96c8693
--- /dev/null
+++ b/lib/puppet/util/network_device/base.rb
@@ -0,0 +1,29 @@
+require 'puppet/util/autoload'
+require 'uri'
+require 'puppet/util/network_device/transport'
+require 'puppet/util/network_device/transport/base'
+
+module Puppet::Util::NetworkDevice
+ class Base
+
+ attr_accessor :url, :transport
+
+ def initialize(url)
+ @url = URI.parse(url)
+
+ @autoloader = Puppet::Util::Autoload.new(
+ self,
+ "puppet/util/network_device/transport",
+ :wrap => false
+ )
+
+ if @autoloader.load(@url.scheme)
+ @transport = Puppet::Util::NetworkDevice::Transport.const_get(@url.scheme.capitalize).new
+ @transport.host = @url.host
+ @transport.port = @url.port || case @url.scheme ; when "ssh" ; 22 ; when "telnet" ; 23 ; end
+ @transport.user = @url.user
+ @transport.password = @url.password
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/cisco.rb b/lib/puppet/util/network_device/cisco.rb
new file mode 100644
index 000000000..c03a00104
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco.rb
@@ -0,0 +1,4 @@
+
+module Puppet::Util::NetworkDevice::Cisco
+
+end \ No newline at end of file
diff --git a/lib/puppet/util/network_device/cisco/device.rb b/lib/puppet/util/network_device/cisco/device.rb
new file mode 100644
index 000000000..97489bd8c
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco/device.rb
@@ -0,0 +1,225 @@
+require 'puppet'
+require 'puppet/util'
+require 'puppet/util/network_device/base'
+require 'puppet/util/network_device/ipcalc'
+require 'puppet/util/network_device/cisco/interface'
+require 'ipaddr'
+
+class Puppet::Util::NetworkDevice::Cisco::Device < Puppet::Util::NetworkDevice::Base
+
+ include Puppet::Util::NetworkDevice::IPCalc
+
+ attr_accessor :enable_password
+
+ def initialize(url, options = {})
+ super(url)
+ @enable_password = options[:enable_password] || parse_enable(@url.query)
+ transport.default_prompt = /[#>]\s?\z/n
+ end
+
+ def parse_enable(query)
+ return $1 if query =~ /enable=(.*)/
+ end
+
+ def command(cmd=nil)
+ Puppet.debug("command #{cmd}")
+ transport.connect
+ login
+ transport.command("terminal length 0") do |out|
+ enable if out =~ />\s?\z/n
+ end
+ find_capabilities
+ out = execute(cmd) if cmd
+ yield self if block_given?
+ transport.close
+ out
+ end
+
+ def execute(cmd)
+ transport.command(cmd)
+ end
+
+ def login
+ return if transport.handles_login?
+ if @url.user != ''
+ transport.command(@url.user, :prompt => /^Password:/)
+ else
+ transport.expect(/^Password:/)
+ end
+ transport.command(@url.password)
+ end
+
+ def enable
+ raise "Can't issue \"enable\" to enter privileged, no enable password set" unless enable_password
+ transport.command("enable", :prompt => /^Password:/)
+ transport.command(enable_password)
+ end
+
+ def support_vlan_brief?
+ !! @support_vlan_brief
+ end
+
+ def find_capabilities
+ out = transport.command("sh vlan brief")
+ lines = out.split("\n")
+ lines.shift; lines.pop
+
+ @support_vlan_brief = ! (lines.first =~ /^%/)
+ end
+
+ IF={
+ :FastEthernet => %w{FastEthernet FastEth Fast FE Fa F},
+ :GigEthernet => %w{GigabitEthernet GigEthernet GigEth GE Gi G},
+ :Ethernet => %w{Ethernet Eth E},
+ :Serial => %w{Serial Se S},
+ :PortChannel => %w{PortChannel Port-Channel Po},
+ :POS => %w{POS P},
+ :VLAN => %w{VLAN VL V},
+ :Loopback => %w{Loopback Loop Lo},
+ :ATM => %w{ATM AT A},
+ :Dialer => %w{Dialer Dial Di D},
+ :VirtualAccess => %w{Virtual-Access Virtual-A Virtual Virt}
+ }
+
+ def canonalize_ifname(interface)
+ IF.each do |k,ifnames|
+ if found = ifnames.find { |ifname| interface =~ /^#{ifname}\s*\d/i }
+ interface =~ /^#{found}(.+)\b/i
+ return "#{k.to_s}#{$1}".gsub(/\s+/,'')
+ end
+ end
+ interface
+ end
+
+ def interface(name)
+ ifname = canonalize_ifname(name)
+ interface = parse_interface(ifname)
+ return { :ensure => :absent } if interface.empty?
+ interface.merge!(parse_trunking(ifname))
+ interface.merge!(parse_interface_config(ifname))
+ end
+
+ def new_interface(name)
+ Puppet::Util::NetworkDevice::Cisco::Interface.new(canonalize_ifname(name), transport)
+ end
+
+ def parse_interface(name)
+ resource = {}
+ out = transport.command("sh interface #{name}")
+ lines = out.split("\n")
+ lines.shift; lines.pop
+ lines.each do |l|
+ if l =~ /#{name} is (.+), line protocol is /
+ resource[:ensure] = ($1 == 'up' ? :present : :absent);
+ end
+ if l =~ /Auto Speed \(.+\),/ or l =~ /Auto Speed ,/ or l =~ /Auto-speed/
+ resource[:speed] = :auto
+ end
+ if l =~ /, (.+)Mb\/s/
+ resource[:speed] = $1
+ end
+ if l =~ /\s+Auto-duplex \((.{4})\),/
+ resource[:duplex] = :auto
+ end
+ if l =~ /\s+(.+)-duplex/
+ resource[:duplex] = $1 == "Auto" ? :auto : $1.downcase.to_sym
+ end
+ if l =~ /Description: (.+)/
+ resource[:description] = $1
+ end
+ end
+ resource
+ end
+
+ def parse_interface_config(name)
+ resource = Hash.new { |hash, key| hash[key] = Array.new ; }
+ out = transport.command("sh running-config interface #{name} | begin interface")
+ lines = out.split("\n")
+ lines.shift; lines.pop
+ lines.each do |l|
+ if l =~ /ip address (#{IP}) (#{IP})\s+secondary\s*$/
+ resource[:ipaddress] << [prefix_length(IPAddr.new($2)), IPAddr.new($1), 'secondary']
+ end
+ if l =~ /ip address (#{IP}) (#{IP})\s*$/
+ resource[:ipaddress] << [prefix_length(IPAddr.new($2)), IPAddr.new($1), nil]
+ end
+ if l =~ /ipv6 address (#{IP})\/(\d+) (eui-64|link-local)/
+ resource[:ipaddress] << [$2.to_i, IPAddr.new($1), $3]
+ end
+ if l =~ /channel-group\s+(\d+)/
+ resource[:etherchannel] = $1
+ end
+ end
+ resource
+ end
+
+ def parse_vlans
+ vlans = {}
+ out = transport.command(support_vlan_brief? ? "sh vlan brief" : "sh vlan-switch brief")
+ lines = out.split("\n")
+ lines.shift; lines.shift; lines.shift; lines.pop
+ vlan = nil
+ lines.each do |l|
+ case l
+ # vlan name status
+ when /^(\d+)\s+(\w+)\s+(\w+)\s+([a-zA-Z0-9,\/. ]+)\s*$/
+ vlan = { :id => $1, :name => $2, :status => $3, :interfaces => [] }
+ if $4.strip.length > 0
+ vlan[:interfaces] = $4.strip.split(/\s*,\s*/).map{ |ifn| canonalize_ifname(ifn) }
+ end
+ vlans[vlan[:id]] = vlan
+ when /^\s+([a-zA-Z0-9,\/. ]+)\s*$/
+ raise "invalid sh vlan summary output" unless vlan
+ if $1.strip.length > 0
+ vlan[:interfaces] += $1.strip.split(/\s*,\s*/).map{ |ifn| canonalize_ifname(ifn) }
+ end
+ else
+ end
+ end
+ vlans
+ end
+
+ def parse_trunking(interface)
+ trunking = {}
+ out = transport.command("sh interface #{interface} switchport")
+ lines = out.split("\n")
+ lines.shift; lines.pop
+ lines.each do |l|
+ case l
+ when /^Administrative mode:\s+(.*)$/i
+ case $1
+ when "trunk"
+ trunking[:mode] = :trunk
+ when "static access"
+ trunking[:mode] = :access
+ else
+ raise "Unknown switchport mode: #{$1} for #{interface}"
+ end
+ when /^Administrative Trunking Encapsulation:\s+(.*)$/
+ case $1
+ when "dot1q","isl"
+ trunking[:encapsulation] = $1.to_sym if trunking[:mode] == :trunk
+ else
+ raise "Unknown switchport encapsulation: #{$1} for #{interface}"
+ end
+ when /^Access Mode VLAN:\s+(.*) \(\(Inactive\)\)$/
+ # nothing
+ when /^Access Mode VLAN:\s+(.*) \(.*\)$/
+ trunking[:native_vlan] = $1 if trunking[:mode] == :access
+ when /^Trunking VLANs Enabled:\s+(.*)$/
+ next if trunking[:mode] == :access
+ vlans = $1
+ trunking[:allowed_trunk_vlans] = case vlans
+ when /all/i
+ :all
+ when /none/i
+ :none
+ else
+ vlans
+ end
+ end
+ end
+ trunking
+ end
+
+end
diff --git a/lib/puppet/util/network_device/cisco/interface.rb b/lib/puppet/util/network_device/cisco/interface.rb
new file mode 100644
index 000000000..63d5492a7
--- /dev/null
+++ b/lib/puppet/util/network_device/cisco/interface.rb
@@ -0,0 +1,82 @@
+require 'puppet/util/network_device/cisco'
+require 'puppet/util/network_device/ipcalc'
+
+# this manages setting properties to an interface in a cisco switch or router
+class Puppet::Util::NetworkDevice::Cisco::Interface
+
+ include Puppet::Util::NetworkDevice::IPCalc
+ extend Puppet::Util::NetworkDevice::IPCalc
+
+ attr_reader :transport, :name
+
+ def initialize(name, transport)
+ @name = name
+ @transport = transport
+ end
+
+ COMMANDS = {
+ # property => order, ios command/block/array
+ :description => [1, "description %s"],
+ :speed => [2, "speed %s"],
+ :duplex => [3, "duplex %s"],
+ :native_vlan => [4, "switchport access vlan %s"],
+ :encapsulation => [5, "switchport trunk encapsulation %s"],
+ :mode => [6, "switchport mode %s"],
+ :allowed_trunk_vlans => [7, "switchport trunk allowed vlan %s"],
+ :etherchannel => [8, ["channel-group %s", "port group %s"]],
+ :ipaddress => [9,
+ lambda do |prefix,ip,option|
+ ip.ipv6? ? "ipv6 address #{ip.to_s}/#{prefix} #{option}" :
+ "ip address #{ip.to_s} #{netmask(Socket::AF_INET,prefix)}"
+ end],
+ :ensure => [10, lambda { |value| value == :present ? "no shutdown" : "shutdown" } ]
+ }
+
+ def update(is={}, should={})
+ Puppet.debug("Updating interface #{name}")
+ command("conf t")
+ command("interface #{name}")
+
+ # apply changes in a defined orders for cisco IOS devices
+ [is.keys, should.keys].flatten.uniq.sort {|a,b| COMMANDS[a][0] <=> COMMANDS[b][0] }.each do |property|
+ # They're equal, so do nothing.
+ next if is[property] == should[property]
+
+ # We're deleting it
+ if should[property] == :absent or should[property].nil?
+ execute(property, is[property], "no ")
+ next
+ end
+
+ # We're replacing an existing value or creating a new one
+ execute(property, should[property])
+ end
+
+ command("exit")
+ command("exit")
+ end
+
+ def execute(property, value, prefix='')
+ case COMMANDS[property][1]
+ when Array
+ COMMANDS[property][1].each do |command|
+ transport.command(prefix + command % value) do |out|
+ break unless out =~ /^%/
+ end
+ end
+ when String
+ command(prefix + COMMANDS[property][1] % value)
+ when Proc
+ value = [value] unless value.is_a?(Array)
+ value.each do |value|
+ command(prefix + COMMANDS[property][1].call(*value))
+ end
+ end
+ end
+
+ def command(command)
+ transport.command(command) do |out|
+ Puppet.err "Error while executing #{command}, device returned #{out}" if out =~ /^%/mo
+ end
+ end
+end \ No newline at end of file