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/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 new file mode 100644 index 0000000..47fa6fb --- /dev/null +++ b/lib/linux_admin/network_interface.rb @@ -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) + 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 + + # @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 + @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) + 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 + # @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 ` + # + # @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 + +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..d7c8858 --- /dev/null +++ b/lib/linux_admin/network_interface/network_interface_rh.rb @@ -0,0 +1,139 @@ +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_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 + @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] 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] 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 + @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] 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_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 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..954662d --- /dev/null +++ b/spec/network_interface/network_interface_rh_spec.rb @@ -0,0 +1,239 @@ +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 + + def stub_foreach_to_string(string) + allow(File).to receive(:foreach) do |&block| + string.each_line { |l| block.call(l) } + 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 + + describe ".new" do + it "loads the configuration" do + 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"') + end + end + + describe "#parse_conf" do + it "reloads the interface configuration" do + interface = dhcp_interface + stub_foreach_to_string(IFCFG_FILE_STATIC) + interface.parse_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") + end + end + + describe "#address=" do + it "sets the address" do + address = "192.168.1.100" + + dhcp_interface.address = address + + conf = dhcp_interface.interface_config + 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_config["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_config["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_config + 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_config + 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_config + 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_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_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_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 + 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_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\"") + end + end + + describe "#save" 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(iface_file) + 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 + 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 new file mode 100644 index 0000000..590c54a --- /dev/null +++ b/spec/network_interface_spec.rb @@ -0,0 +1,192 @@ +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(:foreach).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_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") + 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 + + context "on all systems" 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) + ] + + 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 + 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/96 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 + + 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(LinuxAdmin::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(LinuxAdmin::NetworkInterfaceError) + end + end + + describe "#address" do + it "returns an address" do + expect(subj.address).to eq("192.168.1.9") + end + end + + describe "#address6" do + it "returns the global address by default" do + expect(subj.address6).to eq("fd12:3456:789a:1::1") + end + + it "returns the link local address" do + expect(subj.address6(:link)).to eq("fe80::20c:29ff:feed:e8b") + 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(subj.mac_address).to eq("00:0c:29:ed:0e:8b") + end + end + + describe "#netmask" do + it "returns the correct netmask" do + expect(subj.netmask).to eq("255.255.255.0") + end + end + + 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 "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 + 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