From 964e81e18310a2e3146a8c5297873ceb6ec082d1 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 8 Oct 2015 17:26:25 -0400 Subject: [PATCH 01/12] Added distro specific network interface management class The NetworkInterface switches which object gets instantiated based on the distro that we are running on. https://trello.com/c/rWKh4KQs --- lib/linux_admin.rb | 1 + lib/linux_admin/network_interface.rb | 131 +++++++++++++ .../network_interface_generic.rb | 4 + .../network_interface/network_interface_rh.rb | 102 ++++++++++ .../network_interface_generic_spec.rb | 114 +++++++++++ .../network_interface_rh_spec.rb | 184 ++++++++++++++++++ spec/network_interface_spec.rb | 44 +++++ 7 files changed, 580 insertions(+) create mode 100644 lib/linux_admin/network_interface.rb create mode 100644 lib/linux_admin/network_interface/network_interface_generic.rb create mode 100644 lib/linux_admin/network_interface/network_interface_rh.rb create mode 100644 spec/network_interface/network_interface_generic_spec.rb create mode 100644 spec/network_interface/network_interface_rh_spec.rb create mode 100644 spec/network_interface_spec.rb diff --git a/lib/linux_admin.rb b/lib/linux_admin.rb index efc46b5..fb597ea 100644 --- a/lib/linux_admin.rb +++ b/lib/linux_admin.rb @@ -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 diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb new file mode 100644 index 0000000..267eb15 --- /dev/null +++ b/lib/linux_admin/network_interface.rb @@ -0,0 +1,131 @@ +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 reload [Boolean] Determines if the cached value will be reloaded + # @return [Class] The proper class to be used + def self.dist_class(reload = false) + @dist_class = nil if reload + @dist_class ||= begin + if [Distros.rhel, Distros.fedora].include?(Distros.local) + NetworkInterfaceRH + else + NetworkInterfaceGeneric + end + end + end + + class << self + private + + alias_method :orig_new, :new + end + + # Creates an instance of the correct NetworkInterface subclass for the local distro + def self.new(*args) + if self == LinuxAdmin::NetworkInterface + dist_class.new(*args) + else + orig_new(*args) + end + end + + # @return [String] the interface for networking operations + attr_reader :interface + + # @param interface [String] Name of the network interface to manage + def initialize(interface) + @interface = interface + end + + # Retrieve the IPv4 address assigned to the interface + # + # @return [String] IPv4 address for the managed interface + def address + return unless (ip_output = ip_show) + + cidr_ip = parse_ip_output(ip_output, /inet/, 1) + cidr_ip.split('/')[0] if cidr_ip + end + + # Retrieve the IPv6 address assigned to the interface + # + # @return [String] IPv6 address for the managed interface + def address6(scope = :global) + return unless (ip_output = ip_show) + + ip_regex = /inet6 .* scope #{scope}/ + cidr_ip = parse_ip_output(ip_output, ip_regex, 1) + cidr_ip.split('/')[0] if cidr_ip + end + + # Retrieve the MAC address associated with the interface + # + # @return [String] the MAC address + def mac_address + return unless (ip_output = ip_show) + parse_ip_output(ip_output, %r{link/ether}, 1) + end + + # Retrieve the IPv4 sub-net mask assigned to the interface + # + # @return [String] IPv4 netmask + def netmask + return unless (ip_output = ip_show) + + cidr_ip = parse_ip_output(ip_output, /inet/, 1) + IPAddr.new('255.255.255.255').mask(cidr_ip.split('/')[1]).to_s if cidr_ip + end + + # Retrieve the IPv6 sub-net mask assigned to the interface + # + # @return [String] IPv6 netmask + def netmask6(scope = :global) + return unless (ip_output = ip_show) + + ip_regex = /inet6 .* scope #{scope}/ + cidr_ip = parse_ip_output(ip_output, ip_regex, 1) + IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff').mask(cidr_ip.split('/')[1]).to_s if cidr_ip + end + + # Retrieve the IPv4 default gateway associated with the interface + # + # @return [String] IPv4 gateway address + def gateway + result = run(cmd("ip"), :params => ["route"]) + return nil if result.failure? + + parse_ip_output(result.output, /^default/, 2) + 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) + 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 ` + # + # @return [String] The command output, nil on failure + def ip_show + result = run(cmd("ip"), :params => ["addr", "show", @interface]) + result.success? ? result.output : nil + end + end +end + +Dir.glob(File.join(File.dirname(__FILE__), "network_interface", "*.rb")).each { |f| require f } diff --git a/lib/linux_admin/network_interface/network_interface_generic.rb b/lib/linux_admin/network_interface/network_interface_generic.rb new file mode 100644 index 0000000..4aaada8 --- /dev/null +++ b/lib/linux_admin/network_interface/network_interface_generic.rb @@ -0,0 +1,4 @@ +module LinuxAdmin + class NetworkInterfaceGeneric < NetworkInterface + end +end diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb new file mode 100644 index 0000000..d60e001 --- /dev/null +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -0,0 +1,102 @@ +require 'ipaddr' +require 'pathname' + +module LinuxAdmin + class NetworkInterfaceRH < NetworkInterface + IFACE_DIR = "/etc/sysconfig/network-scripts" + + # @return [Hash] Key value mappings in the interface file + attr_reader :interface_conf + + # @param interface [String] Name of the network interface to manage + def initialize(interface) + super + @iface_file = Pathname.new(IFACE_DIR).join("ifcfg-#{@interface}") + reload + end + + # Parses the interface configuration file into the @interface_conf hash + def reload + @interface_conf = {"NM_CONTROLLED" => "no"} + contents = File.read(@iface_file) + + contents.each_line do |line| + next if line =~ /^\s*#/ + + pair = line.split('=').collect(&:strip) + @interface_conf[pair[0]] = pair[1] + end + 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_conf["BOOTPROTO"] = "static" + @interface_conf["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_conf["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_conf["NETMASK"] = mask + end + + # Sets one or both DNS servers for this network interface + # + # @param servers [Array] The DNS servers + def dns=(*servers) + server1, server2 = servers.flatten + @interface_conf["DNS1"] = server1 + @interface_conf["DNS2"] = server2 if server2 + end + + # Sets the search domain list for this network interface + # + # @param domains [Array] the list of search domains + def search_order=(*domains) + @interface_conf["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" + end + + # Set up the interface to use DHCP + # Removes any previously set static networking information + def enable_dhcp + @interface_conf["BOOTPROTO"] = "dhcp" + @interface_conf.delete("IPADDR") + @interface_conf.delete("NETMASK") + @interface_conf.delete("GATEWAY") + @interface_conf.delete("PREFIX") + end + + # Writes the contents of @interface_conf to @iface_file as `key`=`value` pairs + def save + File.write(@iface_file, @interface_conf.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) + 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 IPAddr::InvalidAddressError + raise ArgumentError, "#{ip} is not a valid IPv4 or IPv6 address" + end + end +end diff --git a/spec/network_interface/network_interface_generic_spec.rb b/spec/network_interface/network_interface_generic_spec.rb new file mode 100644 index 0000000..2950967 --- /dev/null +++ b/spec/network_interface/network_interface_generic_spec.rb @@ -0,0 +1,114 @@ +describe LinuxAdmin::NetworkInterfaceGeneric do + common_inst = Class.new { include LinuxAdmin::Common }.new + + IP_SHOW_ARGS = [ + common_inst.cmd("ip"), + :params => %w(addr show eth0) + ] + + IP_ROUTE_ARGS = [ + common_inst.cmd("ip"), + :params => %w(route) + ] + + IP_ADDR_SHOW_ETH0 = <<-IP_OUT +2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 + link/ether 00:0c:29:ed:0e:8b brd ff:ff:ff:ff:ff:ff + inet 192.168.1.9/24 brd 192.168.1.255 scope global dynamic eth0 + valid_lft 1297sec preferred_lft 1297sec + inet6 fe80::20c:29ff:feed:e8b/64 scope link + valid_lft forever preferred_lft forever + inet6 fd12:3456:789a:1::1/64 scope global + valid_lft forever preferred_lft forever +IP_OUT + + IP_ROUTE = <<-IP_OUT +default via 192.168.1.1 dev eth0 proto static metric 100 +192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.9 metric 100 +IP_OUT + + def result(output, exit_status) + AwesomeSpawn::CommandResult.new("", output, "", exit_status) + end + + subject do + described_class.new("eth0") + end + + describe "#address" do + it "returns an address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) + expect(subject.address).to eq("192.168.1.9") + end + + it "returns nil when no address is found" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.address).to be_nil + end + end + + describe "#address6" do + it "returns the global address by default" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) + expect(subject.address6).to eq("fd12:3456:789a:1::1") + end + + it "returns the link local address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) + expect(subject.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") + end + + it "returns nil when no address is found" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.address6).to be_nil + end + end + + describe "#mac_address" do + it "returns the correct MAC address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) + expect(subject.mac_address).to eq("00:0c:29:ed:0e:8b") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.mac_address).to be_nil + end + + it "returns nil if the link/ether line is not present" do + bad_output = IP_ADDR_SHOW_ETH0.gsub(%r{link/ether}, "") + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(bad_output, 0)) + expect(subject.mac_address).to be_nil + end + end + + describe "#netmask" do + it "returns the correct netmask" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) + expect(subject.netmask).to eq("255.255.255.0") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.netmask).to be_nil + end + end + + describe "#gateway" do + it "returns the correct gateway address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE, 0)) + expect(subject.gateway).to eq("192.168.1.1") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) + expect(subject.gateway).to be_nil + end + + it "returns nil if the default line is not present" do + bad_output = IP_ROUTE.gsub(/default/, "") + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(bad_output, 0)) + expect(subject.gateway).to be_nil + end + end +end diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb new file mode 100644 index 0000000..a9604b7 --- /dev/null +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -0,0 +1,184 @@ +describe LinuxAdmin::NetworkInterfaceRH do + DEVICE_NAME = "eth0" + IFCFG_FILE_DHCP = <<-EOF +#A comment is here +DEVICE=eth0 +BOOTPROTO=dhcp +UUID=3a48a5b5-b80b-4712-82f7-e517e4088999 +ONBOOT=yes +TYPE=Ethernet +NAME="System eth0" +EOF + + IFCFG_FILE_STATIC = <<-EOF +#A comment is here +DEVICE=eth0 +BOOTPROTO=static +UUID=3a48a5b5-b80b-4712-82f7-e517e4088999 +ONBOOT=yes +TYPE=Ethernet +NAME="System eth0" +IPADDR=192.168.1.100 +NETMASK=255.255.255.0 +GATEWAY=192.168.1.1 +EOF + + subject(:dhcp_interface) do + allow(File).to receive(:read).and_return(IFCFG_FILE_DHCP) + described_class.new(DEVICE_NAME) + end + + subject(:static_interface) do + allow(File).to receive(:read).and_return(IFCFG_FILE_STATIC) + described_class.new(DEVICE_NAME) + end + + describe ".new" do + it "loads the configuration" do + conf = dhcp_interface.interface_conf + expect(conf["NM_CONTROLLED"]).to eq("no") + expect(conf["DEVICE"]).to eq("eth0") + expect(conf["BOOTPROTO"]).to eq("dhcp") + expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") + expect(conf["ONBOOT"]).to eq("yes") + expect(conf["TYPE"]).to eq("Ethernet") + expect(conf["NAME"]).to eq('"System eth0"') + end + end + + describe "#reload" do + it "reloads the interface configuration" do + interface = dhcp_interface + allow(File).to receive(:read).and_return(IFCFG_FILE_STATIC) + interface.reload + + conf = interface.interface_conf + expect(conf["NM_CONTROLLED"]).to eq("no") + expect(conf["DEVICE"]).to eq("eth0") + expect(conf["BOOTPROTO"]).to eq("static") + expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") + expect(conf["ONBOOT"]).to eq("yes") + expect(conf["TYPE"]).to eq("Ethernet") + expect(conf["NAME"]).to eq('"System eth0"') + expect(conf["IPADDR"]).to eq("192.168.1.100") + expect(conf["NETMASK"]).to eq("255.255.255.0") + expect(conf["GATEWAY"]).to eq("192.168.1.1") + end + end + + describe "#address=" do + it "sets the address" do + address = "192.168.1.100" + + dhcp_interface.address = address + + conf = dhcp_interface.interface_conf + expect(conf["IPADDR"]).to eq(address) + expect(conf["BOOTPROTO"]).to eq("static") + end + + it "raises argument error when given a bad address" do + expect { dhcp_interface.address = "garbage" }.to raise_error(ArgumentError) + end + end + + describe "#gateway=" do + it "sets the gateway address" do + address = "192.168.1.1" + dhcp_interface.gateway = address + expect(dhcp_interface.interface_conf["GATEWAY"]).to eq(address) + end + + it "raises argument error when given a bad address" do + expect { dhcp_interface.gateway = "garbage" }.to raise_error(ArgumentError) + end + end + + describe "#netmask=" do + it "sets the sub-net mask" do + mask = "255.255.255.0" + dhcp_interface.netmask = mask + expect(dhcp_interface.interface_conf["NETMASK"]).to eq(mask) + end + + it "raises argument error when given a bad address" do + expect { dhcp_interface.netmask = "garbage" }.to raise_error(ArgumentError) + end + end + + describe "#dns=" do + it "sets the correct configuration" do + dns1 = "192.168.1.1" + dns2 = "192.168.1.10" + + static_interface.dns = dns1, dns2 + + conf = static_interface.interface_conf + expect(conf["DNS1"]).to eq(dns1) + expect(conf["DNS2"]).to eq(dns2) + end + + it "sets the correct configuration when given an array" do + dns = %w(192.168.1.1 192.168.1.10) + + static_interface.dns = dns + + conf = static_interface.interface_conf + expect(conf["DNS1"]).to eq(dns[0]) + expect(conf["DNS2"]).to eq(dns[1]) + end + + it "sets only DNS1 if given one value" do + dns = "192.168.1.1" + + static_interface.dns = dns + + conf = static_interface.interface_conf + expect(conf["DNS1"]).to eq(dns) + expect(conf["DNS2"]).to be_nil + end + end + + describe "#search_order=" do + it "sets the search domain list" do + search1 = "localhost" + search2 = "test.example.com" + search3 = "example.com" + static_interface.search_order = search1, search2, search3 + expect(static_interface.interface_conf["DOMAIN"]).to eq("\"#{search1} #{search2} #{search3}\"") + end + + it "sets the search domain list when given an array" do + search_list = %w(localhost test.example.com example.com) + static_interface.search_order = search_list + expect(static_interface.interface_conf["DOMAIN"]).to eq("\"#{search_list.join(' ')}\"") + end + end + + describe "#enable_dhcp" do + it "sets the correct configuration" do + static_interface.enable_dhcp + conf = static_interface.interface_conf + expect(conf["BOOTPROTO"]).to eq("dhcp") + expect(conf["IPADDR"]).to be_nil + expect(conf["NETMASK"]).to be_nil + expect(conf["GATEWAY"]).to be_nil + expect(conf["PREFIX"]).to be_nil + end + end + + describe "#save" do + it "writes the configuration" do + expect(File).to receive(:write) do |file, contents| + expect(file).to eq(Pathname.new("/etc/sysconfig/network-scripts/ifcfg-#{DEVICE_NAME}")) + expect(contents).to include("DEVICE=eth0") + expect(contents).to include("BOOTPROTO=dhcp") + expect(contents).to include("UUID=3a48a5b5-b80b-4712-82f7-e517e4088999") + expect(contents).to include("ONBOOT=yes") + expect(contents).to include("TYPE=Ethernet") + expect(contents).to include('NAME="System eth0"') + end + dhcp_interface.save + end + end +end diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb new file mode 100644 index 0000000..e682eaa --- /dev/null +++ b/spec/network_interface_spec.rb @@ -0,0 +1,44 @@ +describe LinuxAdmin::NetworkInterface do + context "on redhat systems" do + subject do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.rhel) + described_class.dist_class(true) + allow(File).to receive(:read).and_return("") + described_class.new("eth0") + end + + describe ".dist_class" do + it "returns NetworkInterfaceRH" do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.rhel) + expect(described_class.dist_class(true)).to eq(LinuxAdmin::NetworkInterfaceRH) + end + end + + describe ".new" do + it "creates a NetworkInterfaceRH instance" do + expect(subject).to be_an_instance_of(LinuxAdmin::NetworkInterfaceRH) + end + end + end + + context "on other linux systems" do + subject do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) + described_class.dist_class(true) + described_class.new("eth0") + end + + describe ".dist_class" do + it "returns NetworkInterfaceGeneric" do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) + expect(described_class.dist_class(true)).to eq(LinuxAdmin::NetworkInterfaceGeneric) + end + end + + describe ".new" do + it "creates a NetworkInterfaceGeneric instance" do + expect(subject).to be_an_instance_of(LinuxAdmin::NetworkInterfaceGeneric) + end + end + end +end From 0a7214695a517caa0720aad092bba536e7243ffc Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 13 Oct 2015 10:55:01 -0400 Subject: [PATCH 02/12] Move tests for common methods into base class spec file https://trello.com/c/rWKh4KQs --- .../network_interface_generic_spec.rb | 114 --------------- spec/network_interface_spec.rb | 135 ++++++++++++++++++ 2 files changed, 135 insertions(+), 114 deletions(-) delete mode 100644 spec/network_interface/network_interface_generic_spec.rb diff --git a/spec/network_interface/network_interface_generic_spec.rb b/spec/network_interface/network_interface_generic_spec.rb deleted file mode 100644 index 2950967..0000000 --- a/spec/network_interface/network_interface_generic_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -describe LinuxAdmin::NetworkInterfaceGeneric do - common_inst = Class.new { include LinuxAdmin::Common }.new - - IP_SHOW_ARGS = [ - common_inst.cmd("ip"), - :params => %w(addr show eth0) - ] - - IP_ROUTE_ARGS = [ - common_inst.cmd("ip"), - :params => %w(route) - ] - - IP_ADDR_SHOW_ETH0 = <<-IP_OUT -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 00:0c:29:ed:0e:8b brd ff:ff:ff:ff:ff:ff - inet 192.168.1.9/24 brd 192.168.1.255 scope global dynamic eth0 - valid_lft 1297sec preferred_lft 1297sec - inet6 fe80::20c:29ff:feed:e8b/64 scope link - valid_lft forever preferred_lft forever - inet6 fd12:3456:789a:1::1/64 scope global - valid_lft forever preferred_lft forever -IP_OUT - - IP_ROUTE = <<-IP_OUT -default via 192.168.1.1 dev eth0 proto static metric 100 -192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.9 metric 100 -IP_OUT - - def result(output, exit_status) - AwesomeSpawn::CommandResult.new("", output, "", exit_status) - end - - subject do - described_class.new("eth0") - end - - describe "#address" do - it "returns an address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) - expect(subject.address).to eq("192.168.1.9") - end - - it "returns nil when no address is found" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.address).to be_nil - end - end - - describe "#address6" do - it "returns the global address by default" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) - expect(subject.address6).to eq("fd12:3456:789a:1::1") - end - - it "returns the link local address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) - expect(subject.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") - end - - it "returns nil when no address is found" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.address6).to be_nil - end - end - - describe "#mac_address" do - it "returns the correct MAC address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) - expect(subject.mac_address).to eq("00:0c:29:ed:0e:8b") - end - - it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.mac_address).to be_nil - end - - it "returns nil if the link/ether line is not present" do - bad_output = IP_ADDR_SHOW_ETH0.gsub(%r{link/ether}, "") - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(bad_output, 0)) - expect(subject.mac_address).to be_nil - end - end - - describe "#netmask" do - it "returns the correct netmask" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_SHOW_ETH0, 0)) - expect(subject.netmask).to eq("255.255.255.0") - end - - it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.netmask).to be_nil - end - end - - describe "#gateway" do - it "returns the correct gateway address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE, 0)) - expect(subject.gateway).to eq("192.168.1.1") - end - - it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) - expect(subject.gateway).to be_nil - end - - it "returns nil if the default line is not present" do - bad_output = IP_ROUTE.gsub(/default/, "") - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(bad_output, 0)) - expect(subject.gateway).to be_nil - end - end -end diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index e682eaa..89e8384 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -41,4 +41,139 @@ end end end + + context "on all systems" do + subject do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) + described_class.dist_class(true) + described_class.new("eth0") + end + + common_inst = Class.new { include LinuxAdmin::Common }.new + + IP_SHOW_ARGS = [ + common_inst.cmd("ip"), + :params => %w(addr show eth0) + ] + + IP_ROUTE_ARGS = [ + common_inst.cmd("ip"), + :params => %w(route) + ] + + IP_ADDR_OUT = <<-IP_OUT +2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 + link/ether 00:0c:29:ed:0e:8b brd ff:ff:ff:ff:ff:ff + inet 192.168.1.9/24 brd 192.168.1.255 scope global dynamic eth0 + valid_lft 1297sec preferred_lft 1297sec + inet6 fe80::20c:29ff:feed:e8b/64 scope link + valid_lft forever preferred_lft forever + inet6 fd12:3456:789a:1::1/64 scope global + valid_lft forever preferred_lft forever +IP_OUT + + IP_ROUTE_OUT = <<-IP_OUT +default via 192.168.1.1 dev eth0 proto static metric 100 +192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.9 metric 100 +IP_OUT + + subject(:subj) do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) + described_class.dist_class(true) + + allow(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + allow(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE_OUT, 0)) + described_class.new("eth0") + end + + subject(:error_subj) do + allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) + described_class.dist_class(true) + + allow(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + allow(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) + described_class.new("eth0") + end + + def result(output, exit_status) + AwesomeSpawn::CommandResult.new("", output, "", exit_status) + end + + describe "#address" do + it "returns an address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + expect(subject.address).to eq("192.168.1.9") + end + + it "returns nil when no address is found" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.address).to be_nil + end + end + + describe "#address6" do + it "returns the global address by default" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + expect(subject.address6).to eq("fd12:3456:789a:1::1") + end + + it "returns the link local address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + expect(subject.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") + end + + it "returns nil when no address is found" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.address6).to be_nil + end + end + + describe "#mac_address" do + it "returns the correct MAC address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + expect(subject.mac_address).to eq("00:0c:29:ed:0e:8b") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.mac_address).to be_nil + end + + it "returns nil if the link/ether line is not present" do + bad_output = IP_ADDR_OUT.gsub(%r{link/ether}, "") + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(bad_output, 0)) + expect(subject.mac_address).to be_nil + end + end + + describe "#netmask" do + it "returns the correct netmask" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + expect(subject.netmask).to eq("255.255.255.0") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) + expect(subject.netmask).to be_nil + end + end + + describe "#gateway" do + it "returns the correct gateway address" do + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE, 0)) + expect(subject.gateway).to eq("192.168.1.1") + end + + it "returns nil when the command fails" do + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) + expect(subject.gateway).to be_nil + end + + it "returns nil if the default line is not present" do + bad_output = IP_ROUTE.gsub(/default/, "") + expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(bad_output, 0)) + expect(subject.gateway).to be_nil + end + end + end end From a864f2d9d1ae89dbad611260b436d3cd0764e39b Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 13 Oct 2015 14:28:32 -0400 Subject: [PATCH 03/12] Parse network information on NetworkInterface object creation This prevents us from shelling out every time a user asks for some network setting. Also created a reload method to re-parse the settings. https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface.rb | 93 ++++++++++++++++++++-------- spec/network_interface_spec.rb | 77 +++++++++++------------ 2 files changed, 102 insertions(+), 68 deletions(-) diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 267eb15..09b0547 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -9,10 +9,10 @@ class NetworkInterface # Gets the subclass specific to the local Linux distro # - # @param reload [Boolean] Determines if the cached value will be reloaded + # @param test_dist [Boolean] Determines if the cached value will be reevaluated # @return [Class] The proper class to be used - def self.dist_class(reload = false) - @dist_class = nil if reload + def self.dist_class(test_dist = false) + @dist_class = nil if test_dist @dist_class ||= begin if [Distros.rhel, Distros.fedora].include?(Distros.local) NetworkInterfaceRH @@ -43,66 +43,80 @@ def self.new(*args) # @param interface [String] Name of the network interface to manage def initialize(interface) @interface = interface + reload + end + + # Gathers current network information for this interface + 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 end # Retrieve the IPv4 address assigned to the interface # # @return [String] IPv4 address for the managed interface def address - return unless (ip_output = ip_show) - - cidr_ip = parse_ip_output(ip_output, /inet/, 1) - cidr_ip.split('/')[0] if cidr_ip + @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) - return unless (ip_output = ip_show) - - ip_regex = /inet6 .* scope #{scope}/ - cidr_ip = parse_ip_output(ip_output, ip_regex, 1) - cidr_ip.split('/')[0] if cidr_ip + 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 - return unless (ip_output = ip_show) - parse_ip_output(ip_output, %r{link/ether}, 1) + @network_conf[:mac] end # Retrieve the IPv4 sub-net mask assigned to the interface # # @return [String] IPv4 netmask def netmask - return unless (ip_output = ip_show) - - cidr_ip = parse_ip_output(ip_output, /inet/, 1) - IPAddr.new('255.255.255.255').mask(cidr_ip.split('/')[1]).to_s if cidr_ip + @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) - return unless (ip_output = ip_show) - - ip_regex = /inet6 .* scope #{scope}/ - cidr_ip = parse_ip_output(ip_output, ip_regex, 1) - IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff').mask(cidr_ip.split('/')[1]).to_s if cidr_ip + 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 - result = run(cmd("ip"), :params => ["route"]) - return nil if result.failure? - - parse_ip_output(result.output, /^default/, 2) + @network_conf[:gateway] end private @@ -125,6 +139,31 @@ def ip_show result = run(cmd("ip"), :params => ["addr", "show", @interface]) result.success? ? result.output : nil end + + # Parses the IPv4 information from the output of `ip addr show ` + # + # @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 ` + # + # @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 diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index 89e8384..499202b 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -1,6 +1,7 @@ describe LinuxAdmin::NetworkInterface do context "on redhat systems" do subject do + allow_any_instance_of(described_class).to receive(:ip_show).and_return(nil) allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.rhel) described_class.dist_class(true) allow(File).to receive(:read).and_return("") @@ -23,6 +24,7 @@ context "on other linux systems" do subject do + allow_any_instance_of(described_class).to receive(:ip_show).and_return(nil) allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) described_class.dist_class(true) described_class.new("eth0") @@ -43,12 +45,6 @@ end context "on all systems" do - subject do - allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) - described_class.dist_class(true) - described_class.new("eth0") - end - common_inst = Class.new { include LinuxAdmin::Common }.new IP_SHOW_ARGS = [ @@ -68,7 +64,7 @@ valid_lft 1297sec preferred_lft 1297sec inet6 fe80::20c:29ff:feed:e8b/64 scope link valid_lft forever preferred_lft forever - inet6 fd12:3456:789a:1::1/64 scope global + inet6 fd12:3456:789a:1::1/96 scope global valid_lft forever preferred_lft forever IP_OUT @@ -101,78 +97,77 @@ def result(output, exit_status) describe "#address" do it "returns an address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - expect(subject.address).to eq("192.168.1.9") + expect(subj.address).to eq("192.168.1.9") end it "returns nil when no address is found" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.address).to be_nil + expect(error_subj.address).to be_nil end end describe "#address6" do it "returns the global address by default" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - expect(subject.address6).to eq("fd12:3456:789a:1::1") + expect(subj.address6).to eq("fd12:3456:789a:1::1") end it "returns the link local address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - expect(subject.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") + expect(subj.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") end it "returns nil when no address is found" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.address6).to be_nil + expect(error_subj.address6).to be_nil + end + + it "raises ArgumentError when given a bad scope" do + expect { subj.address6(:garbage) }.to raise_error(ArgumentError) end end describe "#mac_address" do it "returns the correct MAC address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - expect(subject.mac_address).to eq("00:0c:29:ed:0e:8b") + expect(subj.mac_address).to eq("00:0c:29:ed:0e:8b") end it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.mac_address).to be_nil - end - - it "returns nil if the link/ether line is not present" do - bad_output = IP_ADDR_OUT.gsub(%r{link/ether}, "") - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(bad_output, 0)) - expect(subject.mac_address).to be_nil + expect(error_subj.mac_address).to be_nil end end describe "#netmask" do it "returns the correct netmask" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - expect(subject.netmask).to eq("255.255.255.0") + expect(subj.netmask).to eq("255.255.255.0") end it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - expect(subject.netmask).to be_nil + expect(error_subj.netmask).to be_nil end end - describe "#gateway" do - it "returns the correct gateway address" do - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE, 0)) - expect(subject.gateway).to eq("192.168.1.1") + describe "#netmask6" do + it "returns the correct global netmask" do + expect(subj.netmask6).to eq("ffff:ffff:ffff:ffff:ffff:ffff::") + end + + it "returns the correct link local netmask" do + expect(subj.netmask6(:link)).to eq("ffff:ffff:ffff:ffff::") end it "returns nil when the command fails" do - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) - expect(subject.gateway).to be_nil + expect(error_subj.netmask6).to be_nil end - it "returns nil if the default line is not present" do - bad_output = IP_ROUTE.gsub(/default/, "") - expect(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(bad_output, 0)) - expect(subject.gateway).to be_nil + it "raises ArgumentError when given a bad scope" do + expect { subj.netmask6(:garbage) }.to raise_error(ArgumentError) + end + end + + describe "#gateway" do + it "returns the correct gateway address" do + expect(subj.gateway).to eq("192.168.1.1") + end + + it "returns nil when the command fails" do + expect(error_subj.gateway).to be_nil end end end From 68b617c6614569ee421a3cbc01b4187358ef6d16 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 13 Oct 2015 14:33:46 -0400 Subject: [PATCH 04/12] Change NetworkInterfaceRH#reload to #parse_conf to avoid conflict with NetworkInterface#reload https://trello.com/c/rWKh4KQs --- .../network_interface/network_interface_rh.rb | 8 +++----- .../network_interface_rh_spec.rb | 16 +++++++++++----- spec/network_interface_spec.rb | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index d60e001..31e69d1 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -12,15 +12,13 @@ class NetworkInterfaceRH < NetworkInterface def initialize(interface) super @iface_file = Pathname.new(IFACE_DIR).join("ifcfg-#{@interface}") - reload + parse_conf end # Parses the interface configuration file into the @interface_conf hash - def reload + def parse_conf @interface_conf = {"NM_CONTROLLED" => "no"} - contents = File.read(@iface_file) - - contents.each_line do |line| + File.foreach(@interface_file) do |line| next if line =~ /^\s*#/ pair = line.split('=').collect(&:strip) diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb index a9604b7..dfefc7a 100644 --- a/spec/network_interface/network_interface_rh_spec.rb +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -23,13 +23,19 @@ GATEWAY=192.168.1.1 EOF + def stub_foreach_to_string(string) + allow(File).to receive(:foreach) do |&block| + string.each_line { |l| block.call(l) } + end + end + subject(:dhcp_interface) do - allow(File).to receive(:read).and_return(IFCFG_FILE_DHCP) + stub_foreach_to_string(IFCFG_FILE_DHCP) described_class.new(DEVICE_NAME) end subject(:static_interface) do - allow(File).to receive(:read).and_return(IFCFG_FILE_STATIC) + stub_foreach_to_string(IFCFG_FILE_STATIC) described_class.new(DEVICE_NAME) end @@ -46,11 +52,11 @@ end end - describe "#reload" do + describe "#parse_conf" do it "reloads the interface configuration" do interface = dhcp_interface - allow(File).to receive(:read).and_return(IFCFG_FILE_STATIC) - interface.reload + stub_foreach_to_string(IFCFG_FILE_STATIC) + interface.parse_conf conf = interface.interface_conf expect(conf["NM_CONTROLLED"]).to eq("no") diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index 499202b..d9518c8 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -4,7 +4,7 @@ allow_any_instance_of(described_class).to receive(:ip_show).and_return(nil) allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.rhel) described_class.dist_class(true) - allow(File).to receive(:read).and_return("") + allow(File).to receive(:foreach).and_return("") described_class.new("eth0") end From b2933883d7c5633887708813e6b0897258f0a78e Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 20 Oct 2015 15:03:32 -0400 Subject: [PATCH 05/12] Use ArgumentError over IPAddr::InvalidAddressError as it is not in ruby 1.9.3 https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface/network_interface_rh.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index 31e69d1..24ef330 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -93,7 +93,7 @@ def save # @raise ArgumentError if the address is not correctly formatted def validate_ip(ip) IPAddr.new(ip) - rescue IPAddr::InvalidAddressError + rescue ArgumentError raise ArgumentError, "#{ip} is not a valid IPv4 or IPv6 address" end end From a9e2896ede86bcc577af8afaf0820430b4fe9feb Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Wed, 14 Oct 2015 09:51:19 -0400 Subject: [PATCH 06/12] Also remove DNS info when enabling DHCP https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface.rb | 6 +++--- .../network_interface/network_interface_rh.rb | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 09b0547..797c27e 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -9,10 +9,10 @@ class NetworkInterface # Gets the subclass specific to the local Linux distro # - # @param test_dist [Boolean] Determines if the cached value will be reevaluated + # @param clear_cache [Boolean] Determines if the cached value will be reevaluated # @return [Class] The proper class to be used - def self.dist_class(test_dist = false) - @dist_class = nil if test_dist + def self.dist_class(clear_cache = false) + @dist_class = nil if clear_cache @dist_class ||= begin if [Distros.rhel, Distros.fedora].include?(Distros.local) NetworkInterfaceRH diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index 24ef330..69f4d14 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -17,13 +17,15 @@ def initialize(interface) # Parses the interface configuration file into the @interface_conf hash def parse_conf - @interface_conf = {"NM_CONTROLLED" => "no"} + @interface_conf = {} + File.foreach(@interface_file) do |line| next if line =~ /^\s*#/ - pair = line.split('=').collect(&:strip) - @interface_conf[pair[0]] = pair[1] + key, value = line.split('=').collect(&:strip) + @interface_conf[key] = value end + @interface_conf["NM_CONTROLLED"] = "no" end # Set the IPv4 address for this interface @@ -78,6 +80,9 @@ def enable_dhcp @interface_conf.delete("NETMASK") @interface_conf.delete("GATEWAY") @interface_conf.delete("PREFIX") + @interface_conf.delete("DNS1") + @interface_conf.delete("DNS2") + @interface_conf.delete("DOMAIN") end # Writes the contents of @interface_conf to @iface_file as `key`=`value` pairs From ba1cd58c96745a120c99be8c43787d5b87387d9a Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 15 Oct 2015 11:47:23 -0400 Subject: [PATCH 07/12] Add start and stop methods and use them in #save https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface.rb | 14 ++++++++ .../network_interface/network_interface_rh.rb | 16 +++++++++ .../network_interface_rh_spec.rb | 33 ++++++++++++++++-- spec/network_interface_spec.rb | 34 +++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 797c27e..2a1c03d 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -119,6 +119,20 @@ 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` diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index 69f4d14..fa8e6b2 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -86,8 +86,24 @@ def enable_dhcp end # Writes the contents of @interface_conf to @iface_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(@iface_file) + + return false unless stop + File.write(@iface_file, @interface_conf.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) + + unless start + File.write(@iface_file, old_contents) + start + return false + end + + true end private diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb index dfefc7a..09de2e4 100644 --- a/spec/network_interface/network_interface_rh_spec.rb +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -174,9 +174,11 @@ def stub_foreach_to_string(string) end describe "#save" do - it "writes the configuration" do + let(:iface_file) { Pathname.new("/etc/sysconfig/network-scripts/ifcfg-#{DEVICE_NAME}") } + + def expect_old_contents expect(File).to receive(:write) do |file, contents| - expect(file).to eq(Pathname.new("/etc/sysconfig/network-scripts/ifcfg-#{DEVICE_NAME}")) + expect(file).to eq(iface_file) expect(contents).to include("DEVICE=eth0") expect(contents).to include("BOOTPROTO=dhcp") expect(contents).to include("UUID=3a48a5b5-b80b-4712-82f7-e517e4088999") @@ -184,7 +186,32 @@ def stub_foreach_to_string(string) expect(contents).to include("TYPE=Ethernet") expect(contents).to include('NAME="System eth0"') end - dhcp_interface.save + end + + it "writes the configuration" do + expect(File).to receive(:read).with(iface_file) + expect(dhcp_interface).to receive(:stop).and_return(true) + expect(dhcp_interface).to receive(:start).and_return(true) + expect_old_contents + expect(dhcp_interface.save).to be true + end + + it "returns false when the interface cannot be brought down" do + expect(File).to receive(:read).with(iface_file) + expect(dhcp_interface).to receive(:stop).and_return(false) + expect(File).not_to receive(:write) + expect(dhcp_interface.save).to be false + end + + it "returns false and writes the old contents when the interface fails to come back up" do + dhcp_interface # evaluate the subject first so the expectations stub the right calls + expect(File).to receive(:read).with(iface_file).and_return("old stuff") + expect(dhcp_interface).to receive(:stop).and_return(true) + expect_old_contents + expect(dhcp_interface).to receive(:start).and_return(false) + expect(File).to receive(:write).with(iface_file, "old stuff") + expect(dhcp_interface).to receive(:start) + expect(dhcp_interface.save).to be false end end end diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index d9518c8..a5f1ebe 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -57,6 +57,16 @@ :params => %w(route) ] + IFUP_ARGS = [ + common_inst.cmd("ifup"), + :params => ["eth0"] + ] + + IFDOWN_ARGS = [ + common_inst.cmd("ifdown"), + :params => ["eth0"] + ] + IP_ADDR_OUT = <<-IP_OUT 2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 00:0c:29:ed:0e:8b brd ff:ff:ff:ff:ff:ff @@ -170,5 +180,29 @@ def result(output, exit_status) expect(error_subj.gateway).to be_nil end end + + describe "#start" do + it "returns true on success" do + expect(AwesomeSpawn).to receive(:run).with(*IFUP_ARGS).and_return(result("", 0)) + expect(subj.start).to be true + end + + it "returns false on failure" do + expect(AwesomeSpawn).to receive(:run).with(*IFUP_ARGS).and_return(result("", 1)) + expect(subj.start).to be false + end + end + + describe "#stop" do + it "returns true on success" do + expect(AwesomeSpawn).to receive(:run).with(*IFDOWN_ARGS).and_return(result("", 0)) + expect(subj.stop).to be true + end + + it "returns false on failure" do + expect(AwesomeSpawn).to receive(:run).with(*IFDOWN_ARGS).and_return(result("", 1)) + expect(subj.stop).to be false + end + end end end From 6994bba44345dd6b52fca7d0b0c6321b6fd6bdc5 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 15 Oct 2015 12:12:07 -0400 Subject: [PATCH 08/12] Added #apply_static wrapper method https://trello.com/c/rWKh4KQs --- .../network_interface/network_interface_rh.rb | 18 ++++++++++++++++++ .../network_interface_rh_spec.rb | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index fa8e6b2..c13bc4f 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -85,6 +85,24 @@ def enable_dhcp @interface_conf.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] list of dns servers + # @param search [Array] list of search domains + # @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_conf to @iface_file as `key`=`value` pairs # and resets the interface # diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb index 09de2e4..45ab713 100644 --- a/spec/network_interface/network_interface_rh_spec.rb +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -173,6 +173,22 @@ def stub_foreach_to_string(string) end end + describe "#apply_static" do + it "sets the correct configuration" do + expect(dhcp_interface).to receive(:save) + dhcp_interface.apply_static("192.168.1.12", "255.255.255.0", "192.168.1.1", ["192.168.1.1", nil], ["localhost"]) + + conf = dhcp_interface.interface_conf + expect(conf["BOOTPROTO"]).to eq("static") + expect(conf["IPADDR"]).to eq("192.168.1.12") + expect(conf["NETMASK"]).to eq("255.255.255.0") + expect(conf["GATEWAY"]).to eq("192.168.1.1") + expect(conf["DNS1"]).to eq("192.168.1.1") + expect(conf["DNS2"]).to be_nil + expect(conf["DOMAIN"]).to eq("\"localhost\"") + end + end + describe "#save" do let(:iface_file) { Pathname.new("/etc/sysconfig/network-scripts/ifcfg-#{DEVICE_NAME}") } From f39a7257bc9813b7cfa927ef75a5b61a2e52f571 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Mon, 19 Oct 2015 10:28:24 -0400 Subject: [PATCH 09/12] Normalize instance variable names https://trello.com/c/rWKh4KQs --- .../network_interface/network_interface_rh.rb | 50 ++++++------ .../network_interface_rh_spec.rb | 76 +++++++++---------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/lib/linux_admin/network_interface/network_interface_rh.rb b/lib/linux_admin/network_interface/network_interface_rh.rb index c13bc4f..d7c8858 100644 --- a/lib/linux_admin/network_interface/network_interface_rh.rb +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -6,26 +6,26 @@ class NetworkInterfaceRH < NetworkInterface IFACE_DIR = "/etc/sysconfig/network-scripts" # @return [Hash] Key value mappings in the interface file - attr_reader :interface_conf + attr_reader :interface_config # @param interface [String] Name of the network interface to manage def initialize(interface) super - @iface_file = Pathname.new(IFACE_DIR).join("ifcfg-#{@interface}") + @interface_file = Pathname.new(IFACE_DIR).join("ifcfg-#{@interface}") parse_conf end - # Parses the interface configuration file into the @interface_conf hash + # Parses the interface configuration file into the @interface_config hash def parse_conf - @interface_conf = {} + @interface_config = {} File.foreach(@interface_file) do |line| next if line =~ /^\s*#/ key, value = line.split('=').collect(&:strip) - @interface_conf[key] = value + @interface_config[key] = value end - @interface_conf["NM_CONTROLLED"] = "no" + @interface_config["NM_CONTROLLED"] = "no" end # Set the IPv4 address for this interface @@ -34,8 +34,8 @@ def parse_conf # @raise ArgumentError if the address is not formatted properly def address=(address) validate_ip(address) - @interface_conf["BOOTPROTO"] = "static" - @interface_conf["IPADDR"] = address + @interface_config["BOOTPROTO"] = "static" + @interface_config["IPADDR"] = address end # Set the IPv4 gateway address for this interface @@ -44,7 +44,7 @@ def address=(address) # @raise ArgumentError if the address is not formatted properly def gateway=(address) validate_ip(address) - @interface_conf["GATEWAY"] = address + @interface_config["GATEWAY"] = address end # Set the IPv4 sub-net mask for this interface @@ -53,7 +53,7 @@ def gateway=(address) # @raise ArgumentError if the mask is not formatted properly def netmask=(mask) validate_ip(mask) - @interface_conf["NETMASK"] = mask + @interface_config["NETMASK"] = mask end # Sets one or both DNS servers for this network interface @@ -61,28 +61,28 @@ def netmask=(mask) # @param servers [Array] The DNS servers def dns=(*servers) server1, server2 = servers.flatten - @interface_conf["DNS1"] = server1 - @interface_conf["DNS2"] = server2 if server2 + @interface_config["DNS1"] = server1 + @interface_config["DNS2"] = server2 if server2 end # Sets the search domain list for this network interface # # @param domains [Array] the list of search domains def search_order=(*domains) - @interface_conf["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" + @interface_config["DOMAIN"] = "\"#{domains.flatten.join(' ')}\"" end # Set up the interface to use DHCP # Removes any previously set static networking information def enable_dhcp - @interface_conf["BOOTPROTO"] = "dhcp" - @interface_conf.delete("IPADDR") - @interface_conf.delete("NETMASK") - @interface_conf.delete("GATEWAY") - @interface_conf.delete("PREFIX") - @interface_conf.delete("DNS1") - @interface_conf.delete("DNS2") - @interface_conf.delete("DOMAIN") + @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 @@ -103,20 +103,20 @@ def apply_static(ip, mask, gw, dns, search = nil) save end - # Writes the contents of @interface_conf to @iface_file as `key`=`value` pairs + # 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(@iface_file) + old_contents = File.read(@interface_file) return false unless stop - File.write(@iface_file, @interface_conf.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) + File.write(@interface_file, @interface_config.delete_blanks.collect { |k, v| "#{k}=#{v}" }.join("\n")) unless start - File.write(@iface_file, old_contents) + File.write(@interface_file, old_contents) start return false end diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb index 45ab713..b27e572 100644 --- a/spec/network_interface/network_interface_rh_spec.rb +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -41,14 +41,14 @@ def stub_foreach_to_string(string) describe ".new" do it "loads the configuration" do - conf = dhcp_interface.interface_conf + conf = dhcp_interface.interface_config expect(conf["NM_CONTROLLED"]).to eq("no") - expect(conf["DEVICE"]).to eq("eth0") - expect(conf["BOOTPROTO"]).to eq("dhcp") - expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") - expect(conf["ONBOOT"]).to eq("yes") - expect(conf["TYPE"]).to eq("Ethernet") - expect(conf["NAME"]).to eq('"System eth0"') + expect(conf["DEVICE"]).to eq("eth0") + expect(conf["BOOTPROTO"]).to eq("dhcp") + expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") + expect(conf["ONBOOT"]).to eq("yes") + expect(conf["TYPE"]).to eq("Ethernet") + expect(conf["NAME"]).to eq('"System eth0"') end end @@ -58,17 +58,17 @@ def stub_foreach_to_string(string) stub_foreach_to_string(IFCFG_FILE_STATIC) interface.parse_conf - conf = interface.interface_conf + conf = interface.interface_config expect(conf["NM_CONTROLLED"]).to eq("no") - expect(conf["DEVICE"]).to eq("eth0") - expect(conf["BOOTPROTO"]).to eq("static") - expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") - expect(conf["ONBOOT"]).to eq("yes") - expect(conf["TYPE"]).to eq("Ethernet") - expect(conf["NAME"]).to eq('"System eth0"') - expect(conf["IPADDR"]).to eq("192.168.1.100") - expect(conf["NETMASK"]).to eq("255.255.255.0") - expect(conf["GATEWAY"]).to eq("192.168.1.1") + expect(conf["DEVICE"]).to eq("eth0") + expect(conf["BOOTPROTO"]).to eq("static") + expect(conf["UUID"]).to eq("3a48a5b5-b80b-4712-82f7-e517e4088999") + expect(conf["ONBOOT"]).to eq("yes") + expect(conf["TYPE"]).to eq("Ethernet") + expect(conf["NAME"]).to eq('"System eth0"') + expect(conf["IPADDR"]).to eq("192.168.1.100") + expect(conf["NETMASK"]).to eq("255.255.255.0") + expect(conf["GATEWAY"]).to eq("192.168.1.1") end end @@ -78,8 +78,8 @@ def stub_foreach_to_string(string) dhcp_interface.address = address - conf = dhcp_interface.interface_conf - expect(conf["IPADDR"]).to eq(address) + conf = dhcp_interface.interface_config + expect(conf["IPADDR"]).to eq(address) expect(conf["BOOTPROTO"]).to eq("static") end @@ -92,7 +92,7 @@ def stub_foreach_to_string(string) it "sets the gateway address" do address = "192.168.1.1" dhcp_interface.gateway = address - expect(dhcp_interface.interface_conf["GATEWAY"]).to eq(address) + expect(dhcp_interface.interface_config["GATEWAY"]).to eq(address) end it "raises argument error when given a bad address" do @@ -104,7 +104,7 @@ def stub_foreach_to_string(string) it "sets the sub-net mask" do mask = "255.255.255.0" dhcp_interface.netmask = mask - expect(dhcp_interface.interface_conf["NETMASK"]).to eq(mask) + expect(dhcp_interface.interface_config["NETMASK"]).to eq(mask) end it "raises argument error when given a bad address" do @@ -119,7 +119,7 @@ def stub_foreach_to_string(string) static_interface.dns = dns1, dns2 - conf = static_interface.interface_conf + conf = static_interface.interface_config expect(conf["DNS1"]).to eq(dns1) expect(conf["DNS2"]).to eq(dns2) end @@ -129,7 +129,7 @@ def stub_foreach_to_string(string) static_interface.dns = dns - conf = static_interface.interface_conf + conf = static_interface.interface_config expect(conf["DNS1"]).to eq(dns[0]) expect(conf["DNS2"]).to eq(dns[1]) end @@ -139,7 +139,7 @@ def stub_foreach_to_string(string) static_interface.dns = dns - conf = static_interface.interface_conf + conf = static_interface.interface_config expect(conf["DNS1"]).to eq(dns) expect(conf["DNS2"]).to be_nil end @@ -151,25 +151,25 @@ def stub_foreach_to_string(string) search2 = "test.example.com" search3 = "example.com" static_interface.search_order = search1, search2, search3 - expect(static_interface.interface_conf["DOMAIN"]).to eq("\"#{search1} #{search2} #{search3}\"") + expect(static_interface.interface_config["DOMAIN"]).to eq("\"#{search1} #{search2} #{search3}\"") end it "sets the search domain list when given an array" do search_list = %w(localhost test.example.com example.com) static_interface.search_order = search_list - expect(static_interface.interface_conf["DOMAIN"]).to eq("\"#{search_list.join(' ')}\"") + expect(static_interface.interface_config["DOMAIN"]).to eq("\"#{search_list.join(' ')}\"") end end describe "#enable_dhcp" do it "sets the correct configuration" do static_interface.enable_dhcp - conf = static_interface.interface_conf + conf = static_interface.interface_config expect(conf["BOOTPROTO"]).to eq("dhcp") - expect(conf["IPADDR"]).to be_nil - expect(conf["NETMASK"]).to be_nil - expect(conf["GATEWAY"]).to be_nil - expect(conf["PREFIX"]).to be_nil + expect(conf["IPADDR"]).to be_nil + expect(conf["NETMASK"]).to be_nil + expect(conf["GATEWAY"]).to be_nil + expect(conf["PREFIX"]).to be_nil end end @@ -178,14 +178,14 @@ def stub_foreach_to_string(string) expect(dhcp_interface).to receive(:save) dhcp_interface.apply_static("192.168.1.12", "255.255.255.0", "192.168.1.1", ["192.168.1.1", nil], ["localhost"]) - conf = dhcp_interface.interface_conf + conf = dhcp_interface.interface_config expect(conf["BOOTPROTO"]).to eq("static") - expect(conf["IPADDR"]).to eq("192.168.1.12") - expect(conf["NETMASK"]).to eq("255.255.255.0") - expect(conf["GATEWAY"]).to eq("192.168.1.1") - expect(conf["DNS1"]).to eq("192.168.1.1") - expect(conf["DNS2"]).to be_nil - expect(conf["DOMAIN"]).to eq("\"localhost\"") + expect(conf["IPADDR"]).to eq("192.168.1.12") + expect(conf["NETMASK"]).to eq("255.255.255.0") + expect(conf["GATEWAY"]).to eq("192.168.1.1") + expect(conf["DNS1"]).to eq("192.168.1.1") + expect(conf["DNS2"]).to be_nil + expect(conf["DOMAIN"]).to eq("\"localhost\"") end end From fa94559f90288f34547b2794f0af5ee71af0dc23 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 20 Oct 2015 14:49:42 -0400 Subject: [PATCH 10/12] Change command execution to use `run!` over `run` Also defined an exception type for the NetworkInterface classes and raise that when `run!` raises a CommandResultError https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface.rb | 18 +++++-- .../network_interface_rh_spec.rb | 6 +++ spec/network_interface_spec.rb | 54 +++++++------------ 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 2a1c03d..642f885 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -4,6 +4,8 @@ module LinuxAdmin class NetworkInterface include Common + NetworkInterfaceError = Class.new(StandardError) + # Cached class instance variable for what distro we are running on @dist_class = nil @@ -41,12 +43,16 @@ def self.new(*args) attr_reader :interface # @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) @@ -57,9 +63,11 @@ def reload @network_conf[:mac] = parse_ip_output(ip_output, %r{link/ether}, 1) - ip_route_res = run(cmd("ip"), :params => ["route"]) + 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, e.message end # Retrieve the IPv4 address assigned to the interface @@ -148,10 +156,12 @@ def parse_ip_output(output, regex, col) # Runs the command `ip addr show ` # - # @return [String] The command output, nil on failure + # @return [String] The command output + # @raise [NetworkInterfaceError] if the command fails def ip_show - result = run(cmd("ip"), :params => ["addr", "show", @interface]) - result.success? ? result.output : nil + run!(cmd("ip"), :params => ["addr", "show", @interface]).output + rescue AwesomeSpawn::CommandResultError => e + raise NetworkInterfaceError, e.message end # Parses the IPv4 information from the output of `ip addr show ` diff --git a/spec/network_interface/network_interface_rh_spec.rb b/spec/network_interface/network_interface_rh_spec.rb index b27e572..954662d 100644 --- a/spec/network_interface/network_interface_rh_spec.rb +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -29,13 +29,19 @@ def stub_foreach_to_string(string) end end + def result(output, exit_status) + AwesomeSpawn::CommandResult.new("", output, "", exit_status) + end + subject(:dhcp_interface) do stub_foreach_to_string(IFCFG_FILE_DHCP) + allow(AwesomeSpawn).to receive(:run!).twice.and_return(result("", 0)) described_class.new(DEVICE_NAME) end subject(:static_interface) do stub_foreach_to_string(IFCFG_FILE_STATIC) + allow(AwesomeSpawn).to receive(:run!).twice.and_return(result("", 0)) described_class.new(DEVICE_NAME) end diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index a5f1ebe..d82b98f 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -87,17 +87,8 @@ allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) described_class.dist_class(true) - allow(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) - allow(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE_OUT, 0)) - described_class.new("eth0") - end - - subject(:error_subj) do - allow(LinuxAdmin::Distros).to receive(:local).and_return(LinuxAdmin::Distros.generic) - described_class.dist_class(true) - - allow(AwesomeSpawn).to receive(:run).with(*IP_SHOW_ARGS).and_return(result("", 1)) - allow(AwesomeSpawn).to receive(:run).with(*IP_ROUTE_ARGS).and_return(result("", 1)) + allow(AwesomeSpawn).to receive(:run!).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + allow(AwesomeSpawn).to receive(:run!).with(*IP_ROUTE_ARGS).and_return(result(IP_ROUTE_OUT, 0)) described_class.new("eth0") end @@ -105,14 +96,27 @@ def result(output, exit_status) AwesomeSpawn::CommandResult.new("", output, "", exit_status) end + describe "#reload" do + it "raises when ip addr show fails" do + subj + awesome_error = AwesomeSpawn::CommandResultError.new("", nil) + allow(AwesomeSpawn).to receive(:run!).with(*IP_SHOW_ARGS).and_raise(awesome_error) + expect { subj.reload }.to raise_error(described_class::NetworkInterfaceError) + end + + it "raises when ip route fails" do + subj + awesome_error = AwesomeSpawn::CommandResultError.new("", nil) + allow(AwesomeSpawn).to receive(:run!).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) + allow(AwesomeSpawn).to receive(:run!).with(*IP_ROUTE_ARGS).and_raise(awesome_error) + expect { subj.reload }.to raise_error(described_class::NetworkInterfaceError) + end + end + describe "#address" do it "returns an address" do expect(subj.address).to eq("192.168.1.9") end - - it "returns nil when no address is found" do - expect(error_subj.address).to be_nil - end end describe "#address6" do @@ -124,10 +128,6 @@ def result(output, exit_status) expect(subj.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") end - it "returns nil when no address is found" do - expect(error_subj.address6).to be_nil - end - it "raises ArgumentError when given a bad scope" do expect { subj.address6(:garbage) }.to raise_error(ArgumentError) end @@ -137,20 +137,12 @@ def result(output, exit_status) it "returns the correct MAC address" do expect(subj.mac_address).to eq("00:0c:29:ed:0e:8b") end - - it "returns nil when the command fails" do - expect(error_subj.mac_address).to be_nil - end end describe "#netmask" do it "returns the correct netmask" do expect(subj.netmask).to eq("255.255.255.0") end - - it "returns nil when the command fails" do - expect(error_subj.netmask).to be_nil - end end describe "#netmask6" do @@ -162,10 +154,6 @@ def result(output, exit_status) expect(subj.netmask6(:link)).to eq("ffff:ffff:ffff:ffff::") end - it "returns nil when the command fails" do - expect(error_subj.netmask6).to be_nil - end - it "raises ArgumentError when given a bad scope" do expect { subj.netmask6(:garbage) }.to raise_error(ArgumentError) end @@ -175,10 +163,6 @@ def result(output, exit_status) it "returns the correct gateway address" do expect(subj.gateway).to eq("192.168.1.1") end - - it "returns nil when the command fails" do - expect(error_subj.gateway).to be_nil - end end describe "#start" do From 5f1992bd39ebd51a073d0a32f4e75d79d7436255 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Wed, 21 Oct 2015 10:26:21 -0400 Subject: [PATCH 11/12] Remove orig_new alias from NetworkInterface https://trello.com/c/rWKh4KQs --- lib/linux_admin/network_interface.rb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 642f885..8c7ee15 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -24,19 +24,9 @@ def self.dist_class(clear_cache = false) end end - class << self - private - - alias_method :orig_new, :new - end - # Creates an instance of the correct NetworkInterface subclass for the local distro def self.new(*args) - if self == LinuxAdmin::NetworkInterface - dist_class.new(*args) - else - orig_new(*args) - end + self == LinuxAdmin::NetworkInterface ? dist_class.new(*args) : super end # @return [String] the interface for networking operations From 74ba40641ec14ddc7694f6202744af90419f91c9 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Wed, 21 Oct 2015 10:58:33 -0400 Subject: [PATCH 12/12] Moved NetworkInterfaceError to exceptions.rb https://trello.com/c/rWKh4KQs --- lib/linux_admin/exceptions.rb | 2 ++ lib/linux_admin/network_interface.rb | 6 ++---- spec/network_interface_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/linux_admin/exceptions.rb b/lib/linux_admin/exceptions.rb index 0a3c00c..acb1426 100644 --- a/lib/linux_admin/exceptions.rb +++ b/lib/linux_admin/exceptions.rb @@ -4,4 +4,6 @@ def initialize(result) super("Invalid username or password", result) end end + + class NetworkInterfaceError < AwesomeSpawn::CommandResultError; end end diff --git a/lib/linux_admin/network_interface.rb b/lib/linux_admin/network_interface.rb index 8c7ee15..47fa6fb 100644 --- a/lib/linux_admin/network_interface.rb +++ b/lib/linux_admin/network_interface.rb @@ -4,8 +4,6 @@ module LinuxAdmin class NetworkInterface include Common - NetworkInterfaceError = Class.new(StandardError) - # Cached class instance variable for what distro we are running on @dist_class = nil @@ -57,7 +55,7 @@ def reload @network_conf[:gateway] = parse_ip_output(ip_route_res.output, /^default/, 2) if ip_route_res.success? true rescue AwesomeSpawn::CommandResultError => e - raise NetworkInterfaceError, e.message + raise NetworkInterfaceError.new(e.message, e.result) end # Retrieve the IPv4 address assigned to the interface @@ -151,7 +149,7 @@ def parse_ip_output(output, regex, col) def ip_show run!(cmd("ip"), :params => ["addr", "show", @interface]).output rescue AwesomeSpawn::CommandResultError => e - raise NetworkInterfaceError, e.message + raise NetworkInterfaceError.new(e.message, e.result) end # Parses the IPv4 information from the output of `ip addr show ` diff --git a/spec/network_interface_spec.rb b/spec/network_interface_spec.rb index d82b98f..590c54a 100644 --- a/spec/network_interface_spec.rb +++ b/spec/network_interface_spec.rb @@ -101,7 +101,7 @@ def result(output, exit_status) subj awesome_error = AwesomeSpawn::CommandResultError.new("", nil) allow(AwesomeSpawn).to receive(:run!).with(*IP_SHOW_ARGS).and_raise(awesome_error) - expect { subj.reload }.to raise_error(described_class::NetworkInterfaceError) + expect { subj.reload }.to raise_error(LinuxAdmin::NetworkInterfaceError) end it "raises when ip route fails" do @@ -109,7 +109,7 @@ def result(output, exit_status) awesome_error = AwesomeSpawn::CommandResultError.new("", nil) allow(AwesomeSpawn).to receive(:run!).with(*IP_SHOW_ARGS).and_return(result(IP_ADDR_OUT, 0)) allow(AwesomeSpawn).to receive(:run!).with(*IP_ROUTE_ARGS).and_raise(awesome_error) - expect { subj.reload }.to raise_error(described_class::NetworkInterfaceError) + expect { subj.reload }.to raise_error(LinuxAdmin::NetworkInterfaceError) end end