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..47a108782 100644 --- a/plugins/guests/debian/cap/configure_networks.rb +++ b/plugins/guests/debian/cap/configure_networks.rb @@ -7,14 +7,111 @@ module VagrantPlugins module Cap class ConfigureNetworks include Vagrant::Util + extend Vagrant::Util::GuestInspection::Linux - def self.configure_networks(machine, networks) - comm = machine.communicate + def self.generate_netplan_cfg(options) + cfg = {"network" => {"version" => 2, + "renderer" => "networkd", + "ethernets" => {}}} - commands = [] - entries = [] - interfaces = machine.guest.capability(:network_interfaces) + options.each do |option| + cfg["network"]["ethernets"].merge!(option) + end + return cfg + end + def self.build_interface_entries(interface) + entry = {interface[:device] => {"dhcp4" => true}} + if interface[:ip] + # is this always the right prefix length to pick?? + entry[interface[:device]].merge!({"addresses" => ["#{interface[:ip]}/24"]}) + entry[interface[:device]]["dhcp4"] = false + end + + if interface[:gateway] + entry[interface[:device]].merge!({"gateway4" => interface[:gateway]}) + end + return entry + end + + def self.determine_systemd_networkd(comm) + return systemd?(comm) && systemd_networkd?(comm) + end + + def self.upload_tmp_file(comm, content, remote_path) + Tempfile.open("vagrant-debian-configure-networks") do |f| + f.binmode + f.write(content) + f.fsync + f.close + comm.upload(f.path, remote_path) + end + end + + def self.configure_netplan_networks(machine, interfaces, comm, networks) + commands = [] + entries = [] + + root_device = interfaces.first + networks.each do |network| + network[:device] = interfaces[network[:interface]] + + options = network.merge(:root_device => root_device) + entry = build_interface_entries(options) + entries << entry + end + + remote_path = "/tmp/vagrant-network-entry" + + netplan_cfg = generate_netplan_cfg(entries) + content = netplan_cfg.to_yaml + upload_tmp_file(comm, content, remote_path) + + commands << <<-EOH.gsub(/^ {12}/, "") + mv '#{remote_path}' /etc/netplan/99-vagrant.yaml + sudo netplan apply + EOH + + return commands + end + + def self.configure_networkd_networks(machine, interfaces, comm, networks) + commands = [] + entries = [] + + root_device = interfaces.first + networks.each.with_index do |network,i| + network[:device] = interfaces[network[:interface]] + # generic systemd-networkd config file + # update for debian + entry = TemplateRenderer.render("guests/debian/networkd/network_#{network[:type]}", + options: network, + ) + + remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now.to_i}-#{i}" + upload_tmp_file(comm, entry, remote_path) + + commands << <<-EOH.gsub(/^ {14}/, '').rstrip + # Configure #{network[:device]} + mv '#{remote_path}' '/etc/systemd/network/#{network[:device]}.network' && + sudo chown root:root '/etc/systemd/network/#{network[:device]}.network' && + sudo chmod 644 '/etc/systemd/network/#{network[:device]}.network' && + ip link set '#{network[:device]}' down && + sudo rm /etc/resolv.conf && + sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf && + sudo systemctl enable systemd-resolved.service && + sudo systemctl start systemd-resolved.service && + sudo systemctl enable systemd-networkd.service + sudo systemctl start systemd-networkd.service + EOH + end + + return commands + end + + def self.configure_other_networks(machine, interfaces, comm, networks) + commands = [] + entries = [] root_device = interfaces.first networks.each do |network| network[:device] = interfaces[network[:interface]] @@ -25,13 +122,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 + remote_path = "/tmp/vagrant-network-entry" + content = entries.join("\n") + upload_tmp_file(comm, content, remote_path) networks.each do |network| # Ubuntu 16.04+ returns an error when downing an interface that @@ -46,13 +139,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 @@ -63,6 +154,28 @@ module VagrantPlugins commands << "/sbin/ifup '#{network[:device]}'" end + return commands + end + + def self.configure_networks(machine, networks) + comm = machine.communicate + + commands = [] + interfaces = machine.guest.capability(:network_interfaces) + + systemd_controlled = determine_systemd_networkd(comm) + netplan_cli = netplan?(comm) + + if systemd_controlled + if netplan_cli + commands = configure_netplan_networks(machine, interfaces, comm, networks) + else + commands = configure_networkd_networks(machine, interfaces, comm, networks) + end + else + commands = configure_other_networks(machine, interfaces, comm, networks) + end + # Run all the commands in one session to prevent partial configuration # due to a severed network. comm.sudo(commands.join("\n")) diff --git a/templates/guests/debian/networkd/network_dhcp.erb b/templates/guests/debian/networkd/network_dhcp.erb new file mode 100644 index 000000000..377784574 --- /dev/null +++ b/templates/guests/debian/networkd/network_dhcp.erb @@ -0,0 +1,4 @@ +Description='A basic dhcp ethernet connection' +Interface=<%= options[:device] %> +Connection=ethernet +IP=dhcp diff --git a/templates/guests/debian/networkd/network_static.erb b/templates/guests/debian/networkd/network_static.erb new file mode 100644 index 000000000..52b2f2d77 --- /dev/null +++ b/templates/guests/debian/networkd/network_static.erb @@ -0,0 +1,8 @@ +Connection=ethernet +Description='A basic static ethernet connection' +Interface=<%= options[:device] %> +IP=static +Address=('<%= options[:ip]%>/<%= options[:netmask] %>') +<% if options[:gateway] -%> +Gateway='<%= options[:gateway] %>' +<% end -%> diff --git a/templates/guests/debian/networkd/network_static6.erb b/templates/guests/debian/networkd/network_static6.erb new file mode 100644 index 000000000..e64ffd5ca --- /dev/null +++ b/templates/guests/debian/networkd/network_static6.erb @@ -0,0 +1,8 @@ +Connection=ethernet +Description='A basic IPv6 ethernet connection' +Interface=<%= options[:device] %> +IP6=static +Address6=('<%= options[:ip]%>/<%= options[:netmask] %>') +<% if options[:gateway] -%> +Gateway6='<%= options[:gateway] %>' +<% 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..f0d8ab9e3 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,32 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do comm.verify_expectations! end + describe "#generate_netplan_cfg" do + 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 + + it "builds an interface entry" do + end + + end + describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } @@ -44,7 +71,11 @@ describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do } end - it "creates and starts the networks" do + it "creates and starts the networks for non systemd" do + allow(comm).to receive(:test).with("systemctl | grep '^-.mount'").and_return(false) + allow(comm).to receive(:test).with("systemctl status systemd-networkd.service").and_return(false) + allow(comm).to receive(:test).with("netplan -h").and_return(false) + cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth1' || true") @@ -54,5 +85,16 @@ 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 + + it "creates and starts the networks for systemd with netplan" do + allow(comm).to receive(:test).with("systemctl | grep '^-.mount'").and_return(true) + allow(comm).to receive(:test).with("sudo systemctl status systemd-networkd.service").and_return(true) + allow(comm).to receive(:test).with("netplan -h").and_return(true) + + cap.configure_networks(machine, [network_0, network_1]) + + expect(comm.received_commands[0]).to match("mv '/tmp/vagrant-network-entry' /etc/netplan/99-vagrant.yaml") + expect(comm.received_commands[0]).to match("sudo netplan apply") + end end end