chef/cookbooks/barclamp/libraries/nic.rb
# Library of routines for handling network interfaces
# Base class representing a network interface.
class ::Nic
include Comparable
private
@@interfaces = Hash.new
@nic = nil
@nicdir = nil
@addresses = ::Array.new
@dependents = nil
VIRTUAL_BASEDIR = "/sys/devices/virtual/net".freeze
BASEDIRS = ["/sys/class/net", VIRTUAL_BASEDIR].freeze
# Helper method for reading values from sysfs for a nic.
def sysfs(file)
::File.read("#{@nicdir}/#{file}").strip
end
def sysfs_put(file,val)
::File.open("#{@nicdir}/#{file}","a+") do |f|
f.syswrite(val.to_s)
end
end
# Basic initialization routine for subclasses of Nic.
def initialize(nic)
@nic = nic.dup.freeze
@nicdir = BASEDIRS.map{ |d|::File.join(d,nic) }.find do |i|
::File.directory?(i)
end
raise RuntimeError.new("Cannot find sysfs dir for #{nic}") unless @nicdir
refresh()
end
# Helper for running ip commands.
def run_ip(arg)
::Kernel.system("ip #{arg}")
end
# Helper for running ethtool commands.
def run_ethtool(*args)
::Kernel.system("ethtool", *args)
end
# Return an unsorted array of all visible nics on the system.
# This will skip virtual nics that OVS creates.
def self.__nics
res = []
::Dir.entries("/sys/class/net").each do |d|
next if d == "." or d == ".."
next unless ::File.directory?("/sys/class/net/#{d}")
res << Nic.new(d)
end
res
end
public
# Return an array of all the nics present on the system.
# This array will be sorted in nic dependency order.
def self.nics
res = Array.new
Nic.__nics.each do |nic|
len = nic.dependents.length
res[len] ||= Array.new
res[len] << nic
end
res.compact.map{ |r| r.sort }.flatten
end
def self.refresh_all
@@interfaces.each_value{ |n|n.refresh }
end
# Some class functions for determining what kind of nic
# we are looking at.
def self.exists?(nic)
nic.kind_of?(::Nic) or BASEDIRS.any?{ |d|::File.exists?("#{d}/#{nic}") }
end
def self.virtual_device?(nic)
name = if nic.is_a?(::Nic)
nic.name
else
nic
end
File.exist?("#{VIRTUAL_BASEDIR}/#{name}")
end
# Consider everything that is either a physical device or a VLAN or bond
# a base device
def self.base_interface?(nic)
!Nic.virtual_device?(nic) || Nic.vlan?(nic) || Nic.bond?(nic)
end
def self.coerce(nic)
return nic if nic.kind_of?(::Nic)
::Nic.new(nic)
end
def self.bridge?(nic)
nic.kind_of?(::Nic::Bridge) or
::File.exists?("/sys/class/net/#{nic}/bridge/bridge_id")
end
def self.ovs_bridge?(nic)
nic.kind_of?(::Nic::OvsBridge) or
# If no ovs tools are installed there's no ovs switch
if ::Kernel.system("which ovs-vsctl > /dev/null 2>&1")
::Kernel.system("ovs-vsctl br-exists #{nic}")
end
end
def self.bond?(nic)
nic.kind_of?(::Nic::Bond) or
::File.exists?("/sys/class/net/#{nic}/bonding/slaves")
end
def self.vlan?(nic)
nic.kind_of?(::Nic::Vlan) or
::File.exists?("/proc/net/vlan/#{nic}")
end
# ifindex and iflink track parent -> child relationships
# among nics. We only really use it for vlan nics for now.
def ifindex
sysfs("ifindex").to_i
end
def iflink
sysfs("iflink").to_i
end
# We only have this to reduce the number of times we have to call
# ip to get the addresses for an interface. If we can get this
# info in a more efficient way (via an ioctl or whatever) it can go away.
def refresh
@addresses = ::Array.new
@dependents = nil
::IO.popen("ip -o addr show dev #{@nic}") do |f|
f.each do |line|
parts = line.gsub('\\',"").split
next unless parts[2] =~ /^inet/
next if parts[5] == "link"
addr = IP.coerce(parts[3])
@addresses << addr
end
end
self
end
# Get all the routes that pass through us.
def routes
res=[]
::IO.popen("ip -o route show dev #{@nic}") do |f|
f.each do |line|
next if line =~ /proto kernel/
res << line.strip
end
end
res
end
# Get a list of all IP4 and IP6 addresses bound to a nic.
def addresses
@addresses
end
# IP address manipulation routines
def add_address(addr)
addr = ::IP.coerce(addr).dup.freeze
return self if @addresses.include?(addr)
if run_ip("addr add #{addr.to_s} dev #{@nic}")
@addresses << addr
self
else
raise ::RuntimeError.new("Could not add #{addr.to_s} to #{@nic}")
false
end
end
def remove_address(addr)
addr = ::IP.coerce(addr)
return self unless @addresses.include?(addr)
unless run_ip("addr del #{addr.to_s} dev #{@nic}")
raise ::RuntimeError.new("Could not remove #{addr.to_s} from #{@nic}.")
end
@addresses.delete(addr)
self
end
# This kills all IP addresses and routes set to go through a nic.
# Use with caution.
def flush
run_ip("-4 route flush dev #{@nic}")
run_ip("-6 route flush dev #{@nic}")
run_ip("addr flush dev #{@nic}")
@addresses = ::Array.new
self
end
# Several helper routines for querying the state of a nic.
# Does this nic have a cable plugged in to it?
def link_up?
sysfs("carrier") == "1"
end
# Get the mac address of a nic.
def mac
sysfs("address") rescue "00:00:00:00:00:00"
end
# Get the speed a this nic is operating at.
# May not always be accurate.
def speed
sysfs("speed").to_i rescue 0
end
# Get and set the MTU for an interface.
def mtu
sysfs("mtu").to_i rescue 0
end
def mtu=(mtu)
run_ip("link set #{@nic} mtu #{mtu}")
end
# Set rx offloading for an interface
def rx_offloading=(on)
run_ethtool("-K", @nic, "rx", on ? "on" : "off")
end
# Set tx offloading for an interface
def tx_offloading=(on)
run_ethtool("-K", @nic, "tx", on ? "on" : "off")
end
def flags
sysfs("flags").hex
end
# Is this nic configured to be up?
def up?
(flags & 1) > 0
end
# Is this nic in broadcast mode?
def broadcast?
(flags & 2) > 0
end
def debug?
(flags & 4) > 0
end
# Is this a loopback interface?
def loopback?
(flags & 8) > 0
end
# Is this nic a pointtopoint interface?
def pointtopoint?
(flags & 16) > 0
end
# Is this nic operating in notrailers mode?
def notrailers?
(flags & 32) > 0
end
# Is this nic operating?
def running?
(flags & 64) > 0
end
# Is this nic in noarp mode?
def noarp?
(flags & 128) > 0
end
# Is this nic in promiscuous mode?
def promiscuous?
(flags & 256) > 0
end
def allmulti?
(flags & 512) > 0
end
# Is this nic a master (i.e, a bond)?
def master?
(flags & 1024) > 0
end
# Is this nic enalaved to something else?
def slave?
(flags & 2048) > 0
end
def multicast?
(flags & 4096) > 0
end
def portsel?
(flags & 8192) > 0
end
def automedia?
(flags & 16384) > 0
end
def dynamic?
(flags & 32786) > 0
end
def loopback?
sysfs("type") == "772"
end
# Set a nic to be up.
def up
return self if up?
run_ip("link set #{@nic} up")
self
end
# Set a nic to be down.
def down
return self unless up?
run_ip("link set #{@nic} down")
self
end
# Get the name of this nic.
def name
@nic
end
# The next two routines onlt really work for interfaces that
# are link-basd subinterfaces -- i.e, vlan interfaces and the like.
# They do not return useful information for bonds and bridges.
# Get this nic's parents based on ifindex -> iflink matching.
def parents
return [] unless is_a?(::Nic::Vlan)
return [] if self.ifindex == self.iflink
self.class.__nics.select do |n|
(n.ifindex == self.iflink) && ! (n == self)
end
end
# Ditto, except get children instead.
def children
self.class.__nics.select do |n|
(n.iflink == self.ifindex) && ! ( n == self )
end
end
# Get the slaves of this interface. Unless you are a bond or a bridge,
# you don't have any. This is here promarily to make the sort logic
# a little simpler.
def slaves
[]
end
# Usurp config from another nic.
def usurp(victim)
self.up
victim = ::Nic.coerce(victim)
victim.refresh
new_routes = (victim.routes - routes)
new_addrs = victim.addresses
return [[], []] if new_routes.empty? && new_addrs.empty?
victim.flush
new_addrs.each do |addr| add_address(addr) end
new_routes.each do |route| run_ip("route add #{route} dev #{@nic}") end
[new_addrs, new_routes]
end
# Usurp all the addresses and routes from our slaves.
def usurp_all
s = slaves
return if s.empty?
s.each do |slave|
slave.usurp_all
usurp(slave)
end
end
# Return the bond we are enslaved to, or nil if we are not in a bond.
def bond_master
return nil unless File.exists?("#{@nicdir}/master")
master = File.readlink("#{@nicdir}/master").split("/")[-1]
return nil unless Nic.bond?(master)
Nic.new(master)
end
# Return the bridge we are enslaved to, or nil if we are not in a bridge.
def bridge_master
return nil unless File.exists?("#{@nicdir}/brport/bridge")
Nic.new(File.readlink("#{@nicdir}/brport/bridge").split("/")[-1])
end
# Return the ovs virtual switch we are enslaved to, or nil if we are not
# port of a ovs switch.
def ovs_master
# If no ovs tools are installed there's no ovs switch
if ::Kernel.system("which ovs-vsctl > /dev/null 2>&1")
res = ""
::IO.popen("ovs-vsctl iface-to-br #{@nic} 2> /dev/null") do |f|
f.each do |line|
# There is only one line of output in case the
# interface is enslaved to anything. Otherwise
# there is none, or the exit code it != 0
res = line.strip
end
end
if $?.success? and not res.empty?
return Nic.new(res)
end
end
return nil
end
# Return the interface we are enslaved to, if we are enslaved to something.
def master
self.bond_master || self.bridge_master || self.ovs_master
end
# Figure out all the interfaces we depend on.
def dependents(loop_breaker = nil)
loop_breaker ||= []
return [] if loop_breaker.include? name
loop_breaker.push(name)
return @dependents.map { |d| Nic.new(d) } if @dependents
res = []
parents.each do |d|
res.push(d)
res.concat(d.dependents(loop_breaker))
end
slaves.each do |s|
res.push(s)
res.concat(s.dependents(loop_breaker))
end
@dependents = res.map(&:name)
res
end
def <=>(other)
case
when self.name == other.name then 0
when self.name == "lo" then -1
when other.name == "lo" then 1
when self.dependents.member?(other) then 1
when other.dependents.member?(self) then -1
else self.name <=> other.name
end
end
def to_s
@nic
end
# Base case for the destroy function.
# Children of Nic should actually destroy themselves instead of
# leaving themselves unconfigured and down.
# Note that destroy also destroys any children of this nic.
# Use with care.
def destroy
children.each do |child|
child.destroy
end
master = self.master()
master.remove_slave(self) if master
self.flush
self.down
@@interfaces.delete(@nic)
end
# Override the usual new function for Nic. All nic types shold
# be created through Nic.new, and not through the subclasses.
# Nic.new is intended to instantiate an object that tracks a nic
# that is already present on the system.
# If you want to create a new interface, call one of the
# create methods on a subclass.
def self.new(nic)
logstr=""
if nic.is_a?(::Nic)
return nic
elsif o = @@interfaces[nic]
return o
elsif vlan?(nic)
o = ::Nic::Vlan.allocate
logstr = "Returning new VLAN interface"
elsif bridge?(nic)
o = ::Nic::Bridge.allocate
logstr = "Returning new bridge interface"
elsif ovs_bridge?(nic)
o = ::Nic::OvsBridge.allocate
logstr = "Returning new ovs-bridge interface"
elsif bond?(nic)
o = ::Nic::Bond.allocate
logstr = "Returning new bond interface"
elsif exists?(nic)
o = ::Nic.allocate
logstr = "Returning new interface"
else
raise ArgumentError.new("#{nic} does not exist! Did you mean Nic.create?")
end
o.send(:initialize, nic)
Chef::Log.info("#{logstr} #{o.inspect}")
@@interfaces[nic] = o
return o
end
# Base class for a bond.
# We handle all bond manipulation via sysfs for maximum flexibility.
class ::Nic::Bond < ::Nic
MASTER="/sys/class/net/bonding_masters"
private
def self.kill_bond(nic)
::File.open(MASTER,"w") do |f|
f.write("-#{nic}")
end
end
def self.create_bond(nic)
5.times do
return if self.exists?(nic) && self.bond?(nic)
::File.open(MASTER,"w") do |f|
f.write("+#{nic}")
end
sleep(0.1)
end
self.kill_bond(nic)
raise ::RuntimeError.new("Could not create bond #{net}")
end
public
# Find a bond that includes these nics.
def self.find(nics)
t = Hash.new
nics.each do |n|
n = Nic.coerce(n)
t[n.name]=n
end
self.__nics.each do |nic|
next unless Nic.bond?(nic)
q = Hash.new
nic.slaves.each do |n|
q[n.name] = n
end
next unless t == q
return nic
end
nil
end
def slaves
sysfs("bonding/slaves").split.map{ |i| ::Nic.new(i) }
end
def add_slave(slave)
unless ::Nic.exists?(slave)
raise ::ArgumentError.new("#{slave} does not exist, cannot add to bond #{@nic}")
end
slave = Nic.coerce(slave)
return slave if self.slaves.member?(slave)
if current_master = slave.master()
current_master.remove_slave(slave)
end
usurp(slave)
slave.down
sysfs_put("bonding/slaves","+#{slave}")
slave
end
def remove_slave(slave)
slave = self.class.coerce(slave)
unless self.slaves.member?(slave)
raise ::ArgumentError.new("#{slave} is not a member of bond #{@nic}")
end
sysfs_put("bonding/slaves","-#{slave}")
slave
end
def mode
sysfs("bonding/mode").split[1].to_i
end
def mode=(new_mode)
myslaves = slaves
begin
myslaves.each do |s| remove_slave(s) end
self.down
sysfs_put("bonding/mode",new_mode)
ensure
self.up
myslaves.each do |s| add_slave(s) end
end
self
end
def miimon
sysfs("bonding/miimon").to_i
end
def miimon=(millisecs)
sysfs_put("bonding/miimon",millisecs)
self
end
def xmit_hash_policy
sysfs("bonding/xmit_hash_policy").split[0]
end
def xmit_hash_policy=(xmit_hash_policy)
sysfs_put("bonding/xmit_hash_policy", xmit_hash_policy)
self
end
def down
slaves.each{ |s|s.down }
super
end
def up
super
slaves.each{ |s|s.up }
self
end
def destroy
slaves.each do |slave|
remove_slave(slave)
end
super
::Nic::Bond.kill_bond(@nic)
nil
end
def self.create(nic, mode=6, miimon=100, xmit_hash_policy="layer2")
Chef::Log.info("Creating new bond #{nic}")
if self.exists?(nic)
raise ::ArgumentError.new("#{nic} already exists.")
elsif ! ::File.exists?("/sys/module/bonding")
unless ::Kernel.system("modprobe bonding max_bonds=0")
raise ::RuntimeError.new("Unable to load bonding module.")
end
end
self.create_bond(nic)
iface = ::Nic.new(nic)
iface.mode = mode
iface.miimon = miimon
iface.xmit_hash_policy = xmit_hash_policy
iface.up
iface
end
end
# Base class for a bridge. We handle most bridge manipulation via brctl.
class ::Nic::Bridge < ::Nic
def slaves
::Dir.entries("#{@nicdir}/brif").select do |i|
link = File.join("#{@nicdir}/brif",i)
# OVS likes to create links to devices that do not really exist.
# Skip them.
File.symlink?(link) &&
File.exists?(File.expand_path(File.readlink(link), "#{@nicdir}/brif"))
end.map{ |i| ::Nic.new(i) }
end
def add_slave(slave)
unless ::Nic.exists?(slave)
raise ::ArgumentError.new("#{slave} does not exist, cannot add to bridge#{@nic}")
end
slave = self.class.coerce(slave)
return slave if slaves.member?(slave)
if current_master = slave.master
current_master.remove_slave(slave)
end
slave.up
usurp(slave)
::Kernel.system("brctl addif #{@nic} #{slave}")
slave
end
def remove_slave(slave)
slave = self.class.coerce(slave)
unless self.slaves.member?(slave)
raise ::ArgumentError.new("#{slave} is not a member of bridge #{@nic}")
end
::Kernel.system("brctl delif #{@nic} #{slave}")
slave.down
slave
end
def stp
sysfs("bridge/stp_state").to_i == 1
end
def stp=(val)
sysfs_put("bridge/stp_state",
case val
when true then 1
when false then 0
else raise ::ArgumentError.new("Bridge STP state must be either true or false.")
end)
self
end
def forward_delay
sysfs("bridge/forward_delay").to_i
end
def forward_delay=(delay)
sysfs_put("bridge/forward_delay",delay)
self
end
def mtu=(mtu)
slaves.each do |slave|
slave.mtu = mtu
end
super
end
def up
slaves.each(&:up)
super
end
def destroy
slaves.each do |slave|
remove_slave(slave)
end
super
::Kernel.system("brctl delbr #{@nic}")
nil
end
def self.create(nic, slaves = [])
Chef::Log.info("Creating new bridge #{nic}")
if self.exists?(nic)
raise ::ArgumentError.new("#{nic} already exists.")
end
unless ::File.exists?("/sys/module/bridge")
::Kernel.system("modprobe bridge")
end
::Kernel.system("brctl addbr #{nic}")
# It might take a little until the sysfs files for the bridge appear
# so let's wait for that
5.times do
if self.exists?(nic) && self.bridge?(nic)
iface = ::Nic.new(nic)
slaves.each do |slave|
iface.add_slave slave
end
iface.up
return iface
end
sleep(0.1)
end
raise ::ArgumentError.new("Unable to create new bridge #{nic}")
end
end
# Base class for an ovs-bridge.
class ::Nic::OvsBridge < ::Nic
def slaves
ports = []
::IO.popen("ovs-vsctl list-ports #{@nic} 2> /dev/null") do |f|
f.each do |line|
port = line.strip
ports << port if ::File.directory?("/sys/class/net/#{port}")
end
end
ports.map{ |i| ::Nic.new(i) }
end
def add_slave(slave)
unless ::Nic.exists?(slave)
raise ::ArgumentError.new("#{slave} does not exist, cannot add to bridge#{@nic}")
end
slave = self.class.coerce(slave)
return slave if slaves.member?(slave)
if current_master = slave.master
current_master.remove_slave(slave)
end
slave.up
usurp(slave)
::Kernel.system("ovs-vsctl add-port #{@nic} #{slave}")
slave
end
def remove_slave(slave)
slave = self.class.coerce(slave)
unless self.slaves.member?(slave)
raise ::ArgumentError.new("#{slave} is not a member of bridge #{@nic}")
end
::Kernel.system("ovs-vsctl del-port #{@nic} #{slave}")
slave.down
slave
end
def up
slaves.each(&:up)
super
end
def destroy
slaves.each do |slave|
remove_slave(slave)
end
super
::Kernel.system("ovs-vsctl del-br #{@nic}")
nil
end
def unplug(slave)
slave = self.class.coerce(slave)
unless slaves.member?(slave)
raise ::ArgumentError, "#{slave} is not a member of bridge #{@nic}"
end
::Kernel.system("ovs-vsctl del-port #{@nic} #{slave}")
end
def plug(slave)
slave = self.class.coerce(slave)
if slaves.member?(slave)
raise ::ArgumentError, "#{slave} is already a member of bridge #{@nic}"
end
::Kernel.system("ovs-vsctl add-port #{@nic} #{slave}")
end
def ovs_forward_bpdu(forward)
::Kernel.system("ovs-vsctl set Bridge #{@nic} other_config:forward-bpdu=#{forward}")
end
def self.create(nic, slaves = [])
Chef::Log.info("Creating new OVS bridge #{nic}")
if self.exists?(nic)
raise ::ArgumentError.new("#{nic} already exists.")
end
::Kernel.system("ovs-vsctl add-br #{nic}")
# It might take a little until the sysfs files for the bridge appear
# so let's wait for that
5.times do
if self.exists?(nic) && self.ovs_bridge?(nic)
iface = ::Nic.new(nic)
slaves.each do |slave|
iface.add_slave slave
end
iface.up
return iface
end
sleep(0.1)
end
raise ::ArgumentError.new("Unable to create new ovs-bridge #{nic}")
end
end
# Base class for a vlan nic.
# All vlan nics must be created as link types on a parent nic.
class ::Nic::Vlan < ::Nic
def vlan
::IO.readlines("/proc/net/vlan/config").each do |line|
line = line.split("|")
next unless line[0].strip == @nic
return line[1].strip.to_i
end
end
def parent
::IO.readlines("/proc/net/vlan/config").each do |line|
line = line.split("|")
next unless line[0].strip == @nic
return line[2].strip
end
end
def destroy
super
::Kernel.system("vconfig rem #{@nic}")
nil
end
def up
parents.each{ |p|p.up }
super
end
def self.create(parent,vlan)
nic = "#{parent}.#{vlan}"
Chef::Log.info("Creating new VLAN interface #{nic}")
if self.exists?(nic)
raise ::ArgumentError.new("#{nic} already exists.")
end
unless self.exists?(parent)
raise ::ArgumentError.new("Parent #{parent} for #{nic} does not exist")
end
unless (1..4094).member?(vlan)
raise ::RangeError.new("#{vlan} must be between 1 and 4094.")
end
unless ::File.exists?("/sys/module/8021q")
::Kernel.system("modprobe 8021q")
end
parent.up
Kernel.system("vconfig set_name_type DEV_PLUS_VID_NO_PAD")
Kernel.system("vconfig add #{parent} #{vlan}")
5.times do
if self.exists?(nic) && self.vlan?(nic)
n = ::Nic.new(nic)
n.up
return n
end
sleep(0.1)
end
raise ::ArgumentError.new("Unable to create VLAN interface #{nic}")
end
end
end