diff --git a/lib/vagrant/util/guest_inspection.rb b/lib/vagrant/util/guest_inspection.rb index 033bb3e97..04350ee96 100644 --- a/lib/vagrant/util/guest_inspection.rb +++ b/lib/vagrant/util/guest_inspection.rb @@ -15,6 +15,13 @@ module Vagrant comm.test("systemctl | grep '^-\.mount'") end + # systemd-networkd.service is in use + # + # @return [Boolean] + def systemd_networkd?(comm) + comm.test("sudo systemctl status systemd-networkd.service") + end + # systemd hostname set is via hostnamectl # # @return [Boolean] @@ -22,6 +29,15 @@ module Vagrant comm.test("hostnamectl") end + ## netplan helpers + + # netplan is installed + # + # @return [Boolean] + def netplan?(comm) + comm.test("netplan -h") + end + ## nmcli helpers # nmcli is installed diff --git a/plugins/guests/debian/cap/configure_networks.rb b/plugins/guests/debian/cap/configure_networks.rb index 99d6681de..f48bd4601 100644 --- a/plugins/guests/debian/cap/configure_networks.rb +++ b/plugins/guests/debian/cap/configure_networks.rb @@ -7,14 +7,108 @@ module VagrantPlugins module Cap class ConfigureNetworks include Vagrant::Util + extend Vagrant::Util::GuestInspection::Linux + + NETPLAN_DEFAULT_VERSION = 2 + NETPLAN_DEFAULT_RENDERER = "networkd".freeze + NETPLAN_DIRECTORY = "/etc/netplan".freeze + NETWORKD_DIRECTORY = "/etc/systemd/network".freeze + def self.configure_networks(machine, networks) comm = machine.communicate - - commands = [] - entries = [] interfaces = machine.guest.capability(:network_interfaces) + if systemd?(comm) + if netplan?(comm) + configure_netplan(machine, interfaces, comm, networks) + elsif systemd_networkd?(comm) + configure_networkd(machine, interfaces, comm, networks) + else + configure_nettools(machine, interfaces, comm, networks) + end + else + configure_nettools(machine, interfaces, comm, networks) + end + end + + # Configure networking using netplan + def self.configure_netplan(machine, interfaces, comm, networks) + ethernets = {}.tap do |e_nets| + networks.each do |network| + e_config = {}.tap do |entry| + if network[:ip] + mask = network[:netmask] + if mask && IPAddr.new(network[:ip]).ipv4? + begin + mask = IPAddr.new(mask).to_i.to_s(2).count("1") + rescue IPAddr::Error + # ignore and use given value + end + end + entry["addresses"] = [[network[:ip], mask].compact.join("/")] + else + entry["dhcp4"] = true + end + if network[:gateway] + entry["gateway4"] = network[:gateway] + end + end + e_nets[interfaces[network[:interface]]] = e_config + end + end + np_config = {"network" => {"version" => NETPLAN_DEFAULT_VERSION, + "renderer" => NETPLAN_DEFAULT_RENDERER, "ethernets" => ethernets}} + + remote_path = upload_tmp_file(comm, np_config.to_yaml) + dest_path = "#{NETPLAN_DIRECTORY}/99-vagrant.yaml" + comm.sudo(["mv -f '#{remote_path}' '#{dest_path}'", + "chown root:root '#{dest_path}'", + "chmod 0644 '#{dest_path}'", + "netplan apply"].join("\n")) + end + + # Configure guest networking using networkd + def self.configure_networkd(machine, interfaces, comm, networks) + net_conf = [] + + root_device = interfaces.first + networks.each do |network| + dev_name = interfaces[network[:interface]] + net_conf << "[Match]" + net_conf << "Name=#{dev_name}" + net_conf << "[Network]" + if network[:ip] + mask = network[:netmask] + if mask && IPAddr.new(network[:ip]).ipv4? + begin + mask = IPAddr.new(mask).to_i.to_s(2).count("1") + rescue IPAddr::Error + # ignore and use given value + end + end + address = [network[:ip], mask].compact.join("/") + net_conf << "DHCP=no" + net_conf << "Address=#{address}" + net_conf << "Gateway=#{network[:gateway]}" if network[:gateway] + else + net_conf << "DHCP=yes" + end + end + + remote_path = upload_tmp_file(comm, net_conf.join("\n")) + dest_path = "#{NETWORKD_DIRECTORY}/99-vagrant.network" + comm.sudo(["mkdir -p #{NETWORKD_DIRECTORY}", + "mv -f '#{remote_path}' '#{dest_path}'", + "chown root:root '#{dest_path}'", + "chmod 0644 '#{dest_path}'", + "systemctl restart systemd-networkd.service"].join("\n")) + end + + # Configure guest networking using net-tools + def self.configure_nettools(machine, interfaces, comm, networks) + commands = [] + entries = [] root_device = interfaces.first networks.each do |network| network[:device] = interfaces[network[:interface]] @@ -25,13 +119,9 @@ module VagrantPlugins entries << entry end - Tempfile.open("vagrant-debian-configure-networks") do |f| - f.binmode - f.write(entries.join("\n")) - f.fsync - f.close - comm.upload(f.path, "/tmp/vagrant-network-entry") - end + content = entries.join("\n") + remote_path = "/tmp/vagrant-network-entry" + upload_tmp_file(comm, content, remote_path) networks.each do |network| # Ubuntu 16.04+ returns an error when downing an interface that @@ -46,13 +136,11 @@ module VagrantPlugins # Remove any previous network modifications from the interfaces file sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tac | sed -e '/^#VAGRANT-END/,$ d' | tac > /tmp/vagrant-network-interfaces.post - cat \\ /tmp/vagrant-network-interfaces.pre \\ /tmp/vagrant-network-entry \\ /tmp/vagrant-network-interfaces.post \\ > /etc/network/interfaces - rm -f /tmp/vagrant-network-interfaces.pre rm -f /tmp/vagrant-network-entry rm -f /tmp/vagrant-network-interfaces.post @@ -62,11 +150,27 @@ module VagrantPlugins networks.each do |network| commands << "/sbin/ifup '#{network[:device]}'" end - - # Run all the commands in one session to prevent partial configuration - # due to a severed network. comm.sudo(commands.join("\n")) end + + # Simple helper to upload content to guest temporary file + # + # @param [Vagrant::Plugin::Communicator] comm + # @param [String] content + # @return [String] remote path + def self.upload_tmp_file(comm, content, remote_path=nil) + if remote_path.nil? + remote_path = "/tmp/vagrant-network-entry-#{Time.now.to_i}" + end + Tempfile.open("vagrant-debian-configure-networks") do |f| + f.binmode + f.write(content) + f.fsync + f.close + comm.upload(f.path, remote_path) + end + remote_path + end end end end diff --git a/test/unit/plugins/guests/debian/cap/configure_networks_test.rb b/test/unit/plugins/guests/debian/cap/configure_networks_test.rb index 13a742cae..84030d96c 100644 --- a/test/unit/plugins/guests/debian/cap/configure_networks_test.rb +++ b/test/unit/plugins/guests/debian/cap/configure_networks_test.rb @@ -11,6 +11,7 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do let(:machine) { double("machine", guest: guest) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + before do allow(machine).to receive(:communicate).and_return(comm) end @@ -19,6 +20,25 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do comm.verify_expectations! end + describe "#build_interface_entries" do + let(:network_0) do + { + interface: 0, + type: "dhcp", + } + end + + let(:network_1) do + { + interface: 1, + type: "static", + ip: "33.33.33.10", + netmask: "255.255.0.0", + gateway: "33.33.0.1", + } + end + end + describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } @@ -44,7 +64,13 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do } end - it "creates and starts the networks" do + before do + allow(comm).to receive(:test).with("systemctl | grep '^-.mount'").and_return(false) + allow(comm).to receive(:test).with("sudo systemctl status systemd-networkd.service").and_return(false) + allow(comm).to receive(:test).with("netplan -h").and_return(false) + end + + it "creates and starts the networks using net-tools" do cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth1' || true") @@ -54,5 +80,53 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do expect(comm.received_commands[0]).to match("/sbin/ifup 'eth1'") expect(comm.received_commands[0]).to match("/sbin/ifup 'eth2'") end + + context "with systemd" do + before do + expect(comm).to receive(:test).with("systemctl | grep '^-.mount'").and_return(true) + allow(comm).to receive(:test).with("netplan -h").and_return(false) + end + + it "creates and starts the networks using net-tools" do + cap.configure_networks(machine, [network_0, network_1]) + + expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth1' || true") + expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth1'") + expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth2' || true") + expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth2'") + expect(comm.received_commands[0]).to match("/sbin/ifup 'eth1'") + expect(comm.received_commands[0]).to match("/sbin/ifup 'eth2'") + end + + context "with systemd-networkd" do + before do + expect(comm).to receive(:test).with("sudo systemctl status systemd-networkd.service").and_return(true) + end + + it "creates and starts the networks using systemd-networkd" do + cap.configure_networks(machine, [network_0, network_1]) + + expect(comm.received_commands[0]).to match("mv -f '/tmp/vagrant-network-entry.*' '/etc/systemd/network/.*network'") + expect(comm.received_commands[0]).to match("chown") + expect(comm.received_commands[0]).to match("chmod") + expect(comm.received_commands[0]).to match("systemctl restart") + end + end + + context "with netplan" do + before do + expect(comm).to receive(:test).with("netplan -h").and_return(true) + end + + it "creates and starts the networks for systemd with netplan" do + cap.configure_networks(machine, [network_0, network_1]) + + expect(comm.received_commands[0]).to match("mv -f '/tmp/vagrant-network-entry.*' '/etc/netplan/.*.yaml'") + expect(comm.received_commands[0]).to match("chown") + expect(comm.received_commands[0]).to match("chmod") + expect(comm.received_commands[0]).to match("netplan apply") + end + end + end end end