-
Notifications
You must be signed in to change notification settings - Fork 30
Add distro specific network interface class #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
964e81e
0a72146
a864f2d
68b617c
b293388
a9e2896
ba1cd58
6994bba
f39a725
fa94559
5f1992b
74ba406
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
require 'ipaddr' | ||
|
||
module LinuxAdmin | ||
class NetworkInterface | ||
include Common | ||
|
||
# Cached class instance variable for what distro we are running on | ||
@dist_class = nil | ||
|
||
# Gets the subclass specific to the local Linux distro | ||
# | ||
# @param clear_cache [Boolean] Determines if the cached value will be reevaluated | ||
# @return [Class] The proper class to be used | ||
def self.dist_class(clear_cache = false) | ||
@dist_class = nil if clear_cache | ||
@dist_class ||= begin | ||
if [Distros.rhel, Distros.fedora].include?(Distros.local) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does Distros.rhel include centos? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
NetworkInterfaceRH | ||
else | ||
NetworkInterfaceGeneric | ||
end | ||
end | ||
end | ||
|
||
# Creates an instance of the correct NetworkInterface subclass for the local distro | ||
def self.new(*args) | ||
self == LinuxAdmin::NetworkInterface ? dist_class.new(*args) : super | ||
end | ||
|
||
# @return [String] the interface for networking operations | ||
attr_reader :interface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For an attr, you don't need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing it gives you |
||
|
||
# @param interface [String] Name of the network interface to manage | ||
# @raise [NetworkInterfaceError] if network information cannot be retrieved | ||
def initialize(interface) | ||
@interface = interface | ||
reload | ||
end | ||
|
||
# Gathers current network information for this interface | ||
# | ||
# @return [Boolean] true if network information was gathered successfully | ||
# @raise [NetworkInterfaceError] if network information cannot be retrieved | ||
def reload | ||
@network_conf = {} | ||
return false unless (ip_output = ip_show) | ||
|
||
parse_ip4(ip_output) | ||
parse_ip6(ip_output, :global) | ||
parse_ip6(ip_output, :link) | ||
|
||
@network_conf[:mac] = parse_ip_output(ip_output, %r{link/ether}, 1) | ||
|
||
ip_route_res = run!(cmd("ip"), :params => ["route"]) | ||
@network_conf[:gateway] = parse_ip_output(ip_route_res.output, /^default/, 2) if ip_route_res.success? | ||
true | ||
rescue AwesomeSpawn::CommandResultError => e | ||
raise NetworkInterfaceError.new(e.message, e.result) | ||
end | ||
|
||
# Retrieve the IPv4 address assigned to the interface | ||
# | ||
# @return [String] IPv4 address for the managed interface | ||
def address | ||
@network_conf[:address] | ||
end | ||
|
||
# Retrieve the IPv6 address assigned to the interface | ||
# | ||
# @return [String] IPv6 address for the managed interface | ||
# @raise [ArgumentError] if the given scope is not `:global` or `:link` | ||
def address6(scope = :global) | ||
case scope | ||
when :global | ||
@network_conf[:address6_global] | ||
when :link | ||
@network_conf[:address6_link] | ||
else | ||
raise ArgumentError, "Unrecognized address scope #{scope}" | ||
end | ||
end | ||
|
||
# Retrieve the MAC address associated with the interface | ||
# | ||
# @return [String] the MAC address | ||
def mac_address | ||
@network_conf[:mac] | ||
end | ||
|
||
# Retrieve the IPv4 sub-net mask assigned to the interface | ||
# | ||
# @return [String] IPv4 netmask | ||
def netmask | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with any other than just mask. I think it is pretty intuitive either way, the terms netmask and subnet mask are used pretty interchangeably. |
||
@network_conf[:mask] | ||
end | ||
|
||
# Retrieve the IPv6 sub-net mask assigned to the interface | ||
# | ||
# @return [String] IPv6 netmask | ||
# @raise [ArgumentError] if the given scope is not `:global` or `:link` | ||
def netmask6(scope = :global) | ||
if scope == :global | ||
@network_conf[:mask6_global] | ||
elsif scope == :link | ||
@network_conf[:mask6_link] | ||
else | ||
raise ArgumentError, "Unrecognized address scope #{scope}" | ||
end | ||
end | ||
|
||
# Retrieve the IPv4 default gateway associated with the interface | ||
# | ||
# @return [String] IPv4 gateway address | ||
def gateway | ||
@network_conf[:gateway] | ||
end | ||
|
||
# Brings up the network interface | ||
# | ||
# @return [Boolean] whether the command succeeded or not | ||
def start | ||
run(cmd("ifup"), :params => [@interface]).success? | ||
end | ||
|
||
# Brings down the network interface | ||
# | ||
# @return [Boolean] whether the command succeeded or not | ||
def stop | ||
run(cmd("ifdown"), :params => [@interface]).success? | ||
end | ||
|
||
private | ||
|
||
# Parses the output of `ip addr show` | ||
# | ||
# @param output [String] The command output | ||
# @param regex [Regexp] Regular expression to match the desired output line | ||
# @param col [Fixnum] The whitespace delimited column to be returned | ||
# @return [String] The parsed data | ||
def parse_ip_output(output, regex, col) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we document private methods? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't show up in the generated html docs, but I added it for the developers. |
||
the_line = output.split("\n").detect { |l| l =~ regex } | ||
the_line.nil? ? nil : the_line.strip.split(' ')[col] | ||
end | ||
|
||
# Runs the command `ip addr show <interface>` | ||
# | ||
# @return [String] The command output | ||
# @raise [NetworkInterfaceError] if the command fails | ||
def ip_show | ||
run!(cmd("ip"), :params => ["addr", "show", @interface]).output | ||
rescue AwesomeSpawn::CommandResultError => e | ||
raise NetworkInterfaceError.new(e.message, e.result) | ||
end | ||
|
||
# Parses the IPv4 information from the output of `ip addr show <device>` | ||
# | ||
# @param ip_output [String] The command output | ||
def parse_ip4(ip_output) | ||
cidr_ip = parse_ip_output(ip_output, /inet/, 1) | ||
return unless cidr_ip | ||
|
||
@network_conf[:address] = cidr_ip.split('/')[0] | ||
@network_conf[:mask] = IPAddr.new('255.255.255.255').mask(cidr_ip.split('/')[1]).to_s | ||
end | ||
|
||
# Parses the IPv6 information from the output of `ip addr show <device>` | ||
# | ||
# @param ip_output [String] The command output | ||
# @param scope [Symbol] The IPv6 scope (either `:global` or `:local`) | ||
def parse_ip6(ip_output, scope) | ||
mask_addr = IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff') | ||
cidr_ip = parse_ip_output(ip_output, /inet6 .* scope #{scope}/, 1) | ||
return unless cidr_ip | ||
|
||
parts = cidr_ip.split('/') | ||
@network_conf["address6_#{scope}".to_sym] = parts[0] | ||
@network_conf["mask6_#{scope}".to_sym] = mask_addr.mask(parts[1]).to_s | ||
end | ||
end | ||
end | ||
|
||
Dir.glob(File.join(File.dirname(__FILE__), "network_interface", "*.rb")).each { |f| require f } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How old? I think we have travis running for 1.9.3. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, can't remember when that was introduce but it might be 2.0 only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it's definitely not in 1.9.3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
module LinuxAdmin | ||
class NetworkInterfaceGeneric < NetworkInterface | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
require 'ipaddr' | ||
require 'pathname' | ||
|
||
module LinuxAdmin | ||
class NetworkInterfaceRH < NetworkInterface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My intention was "Red Hat" as the class is able to provide network configuration for CentOS, Fedora, and RHEL. |
||
IFACE_DIR = "/etc/sysconfig/network-scripts" | ||
|
||
# @return [Hash<String, String>] Key value mappings in the interface file | ||
attr_reader :interface_config | ||
|
||
# @param interface [String] Name of the network interface to manage | ||
def initialize(interface) | ||
super | ||
@interface_file = Pathname.new(IFACE_DIR).join("ifcfg-#{@interface}") | ||
parse_conf | ||
end | ||
|
||
# Parses the interface configuration file into the @interface_config hash | ||
def parse_conf | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be private? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to leave this public in case something else changed the file and there was still a reference to the object out there. |
||
@interface_config = {} | ||
|
||
File.foreach(@interface_file) do |line| | ||
next if line =~ /^\s*#/ | ||
|
||
key, value = line.split('=').collect(&:strip) | ||
@interface_config[key] = value | ||
end | ||
@interface_config["NM_CONTROLLED"] = "no" | ||
end | ||
|
||
# Set the IPv4 address for this interface | ||
# | ||
# @param address [String] | ||
# @raise ArgumentError if the address is not formatted properly | ||
def address=(address) | ||
validate_ip(address) | ||
@interface_config["BOOTPROTO"] = "static" | ||
@interface_config["IPADDR"] = address | ||
end | ||
|
||
# Set the IPv4 gateway address for this interface | ||
# | ||
# @param address [String] | ||
# @raise ArgumentError if the address is not formatted properly | ||
def gateway=(address) | ||
validate_ip(address) | ||
@interface_config["GATEWAY"] = address | ||
end | ||
|
||
# Set the IPv4 sub-net mask for this interface | ||
# | ||
# @param mask [String] | ||
# @raise ArgumentError if the mask is not formatted properly | ||
def netmask=(mask) | ||
validate_ip(mask) | ||
@interface_config["NETMASK"] = mask | ||
end | ||
|
||
# Sets one or both DNS servers for this network interface | ||
# | ||
# @param servers [Array<String>] The DNS servers | ||
def dns=(*servers) | ||
server1, server2 = servers.flatten | ||
@interface_config["DNS1"] = server1 | ||
@interface_config["DNS2"] = server2 if server2 | ||
end | ||
|
||
# Sets the search domain list for this network interface | ||
# | ||
# @param domains [Array<String>] the list of search domains | ||
def search_order=(*domains) | ||
@interface_config["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" | ||
end | ||
|
||
# Set up the interface to use DHCP | ||
# Removes any previously set static networking information | ||
def enable_dhcp | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bdunne Should we also clear the DNS server and search order here and assume that will be set by DHCP? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
@interface_config["BOOTPROTO"] = "dhcp" | ||
@interface_config.delete("IPADDR") | ||
@interface_config.delete("NETMASK") | ||
@interface_config.delete("GATEWAY") | ||
@interface_config.delete("PREFIX") | ||
@interface_config.delete("DNS1") | ||
@interface_config.delete("DNS2") | ||
@interface_config.delete("DOMAIN") | ||
end | ||
|
||
# Applies the given static network configuration to the interface | ||
# | ||
# @param ip [String] IPv4 address | ||
# @param mask [String] subnet mask | ||
# @param gw [String] gateway address | ||
# @param dns [Array<String>] list of dns servers | ||
# @param search [Array<String>] list of search domains | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declare in the documentation that this is an "Optional list of search domains" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like yardoc takes care of it, this is what is shows:
Is that good enough or do you also want me to put a note about it being optional in there? |
||
# @return [Boolean] true on success, false otherwise | ||
# @raise ArgumentError if an IP is not formatted properly | ||
def apply_static(ip, mask, gw, dns, search = nil) | ||
self.address = ip | ||
self.netmask = mask | ||
self.gateway = gw | ||
self.dns = dns | ||
self.search_order = search if search | ||
save | ||
end | ||
|
||
# Writes the contents of @interface_config to @interface_file as `key`=`value` pairs | ||
# and resets the interface | ||
# | ||
# @return [Boolean] true if the interface was successfully brought up with the | ||
# new configuration, false otherwise | ||
def save | ||
old_contents = File.read(@interface_file) | ||
|
||
return false unless stop | ||
|
||
File.write(@interface_file, @interface_config.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) | ||
|
||
unless start | ||
File.write(@interface_file, old_contents) | ||
start | ||
return false | ||
end | ||
|
||
true | ||
end | ||
|
||
private | ||
|
||
# Validate that the given address is formatted correctly | ||
# | ||
# @param ip [String] | ||
# @raise ArgumentError if the address is not correctly formatted | ||
def validate_ip(ip) | ||
IPAddr.new(ip) | ||
rescue ArgumentError | ||
raise ArgumentError, "#{ip} is not a valid IPv4 or IPv6 address" | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This initialization is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It prevents a warning