Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/linux_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
require 'linux_admin/time_date'
require 'linux_admin/ip_address'
require 'linux_admin/dns'
require 'linux_admin/network_interface'

module LinuxAdmin
extend Common
Expand Down
2 changes: 2 additions & 0 deletions lib/linux_admin/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ def initialize(result)
super("Invalid username or password", result)
end
end

class NetworkInterfaceError < AwesomeSpawn::CommandResultError; end
end
182 changes: 182 additions & 0 deletions lib/linux_admin/network_interface.rb
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
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It prevents a warning


# 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Distros.rhel include centos?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an attr, you don't need the @return [String] syntax for YARD (I don't think)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing it gives you (Object) interface in the documentation.


# @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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should netmask be net_mask or perhaps just mask? subnet_mask?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we document private methods?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File.dirname(__FILE__) can be replaced with __dir__ (or do we still support old rubies?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How old? I think we have travis running for 1.9.3.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
139 changes: 139 additions & 0 deletions lib/linux_admin/network_interface/network_interface_rh.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
require 'ipaddr'
require 'pathname'

module LinuxAdmin
class NetworkInterfaceRH < NetworkInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the RH in NetworkInterfaceRH? Does that stand for Red Hat (the company) or RHEL (the OS)? If the latter, then I would rename to NetworkInterfaceRHEL.

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be private?

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like yardoc takes care of it, this is what is shows:

 search (Array<String>) (defaults to: nil) — list of search domains

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
Loading