diff --git a/lib/vagrant/util/guest_inspection.rb b/lib/vagrant/util/guest_inspection.rb index 468bb658d..93be3b78a 100644 --- a/lib/vagrant/util/guest_inspection.rb +++ b/lib/vagrant/util/guest_inspection.rb @@ -23,6 +23,25 @@ module Vagrant comm.test("systemctl -q is-active systemd-networkd.service", sudo: true) end + # Check if a unit file with the given name is defined. Name can + # be a pattern or explicit name. + # + # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator + # @param [String] name Name or pattern to search + # @return [Boolean] + def systemd_unit_file?(comm, name) + comm.test("systemctl -q list-unit-files | grep \"#{name}\"") + end + + # Check if a unit is currently active within systemd + # + # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator + # @param [String] name Name or pattern to search + # @return [Boolean] + def systemd_unit?(comm, name) + comm.test("systemctl -q list-units | grep \"#{name}\"") + end + # Check if given service is controlled by systemd # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator diff --git a/plugins/guests/coreos/cap/change_host_name.rb b/plugins/guests/coreos/cap/change_host_name.rb index fe9b21865..f43d604c8 100644 --- a/plugins/guests/coreos/cap/change_host_name.rb +++ b/plugins/guests/coreos/cap/change_host_name.rb @@ -1,18 +1,35 @@ +require "tempfile" +require "yaml" + module VagrantPlugins module GuestCoreOS module Cap class ChangeHostName + extend Vagrant::Util::GuestInspection::Linux + def self.change_host_name(machine, name) comm = machine.communicate - if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) - basename = name.split(".", 2)[0] - comm.sudo("hostname '#{basename}'") + if systemd_unit_file?(comm, "system-cloudinit*") + file = Tempfile.new("vagrant-coreos-hostname") + file.puts("#cloud-config\n") + file.puts({"hostname" => name}.to_yaml) + file.close - # Note that when working with CoreOS, we explicitly do not add the - # entry to /etc/hosts because this file does not exist on CoreOS. - # We could create it, but the recommended approach on CoreOS is to - # use Fleet to manage /etc/hosts files. + dst = "/var/tmp/hostname.yml" + svc_path = dst.tr("/", "-")[1..-1] + comm.upload(file.path, dst) + comm.sudo("systemctl start system-cloudinit@#{svc_path}.service") + else + if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) + basename = name.split(".", 2)[0] + comm.sudo("hostname '#{basename}'") + + # Note that when working with CoreOS, we explicitly do not add the + # entry to /etc/hosts because this file does not exist on CoreOS. + # We could create it, but the recommended approach on CoreOS is to + # use Fleet to manage /etc/hosts files. + end end end end diff --git a/plugins/guests/coreos/cap/configure_networks.rb b/plugins/guests/coreos/cap/configure_networks.rb index fcff79da2..a954b45bd 100644 --- a/plugins/guests/coreos/cap/configure_networks.rb +++ b/plugins/guests/coreos/cap/configure_networks.rb @@ -6,79 +6,63 @@ module VagrantPlugins module GuestCoreOS module Cap class ConfigureNetworks - include Vagrant::Util + extend Vagrant::Util::GuestInspection::Linux + + DEFAULT_ENVIRONMENT_IP = "127.0.0.1".freeze def self.configure_networks(machine, networks) - machine.communicate.tap do |comm| - # Read network interface names - interfaces = [] - comm.sudo("ifconfig | grep -E '(e[n,t][h,s,p][[:digit:]]([a-z][[:digit:]])?)' | cut -f1 -d:") do |_, result| - interfaces = result.split("\n") + cloud_config = {} + # Locate configured IP addresses to drop in /etc/environment + # for export. If no addresses found, fall back to default + public_ip = catch(:public_ip) do + machine.config.vm.networks.each do |type, opts| + next if type != :public_network + throw(:public_ip, opts[:ip]) if opts[:ip] end + DEFAULT_ENVIRONMENT_IP + end + private_ip = catch(:private_ip) do + machine.config.vm.networks.each do |type, opts| + next if type != :private_network + throw(:private_ip, opts[:ip]) if opts[:ip] + end + public_ip + end + cloud_config["write_files"] = [ + {"path" => "/etc/environment", + "content" => "COREOS_PUBLIC_IPV4=#{public_ip}\nCOREOS_PRIVATE_IPV4=#{private_ip}"} + ] - primary_machine_config = machine.env.active_machines.first - primary_machine = machine.env.machine(*primary_machine_config, true) - - primary_machine_ip = get_ip(primary_machine) - current_ip = get_ip(machine) - if current_ip == primary_machine_ip - entry = TemplateRenderer.render("guests/coreos/etcd.service", options: { - my_ip: current_ip, - }) + # Generate configuration for any static network interfaces + # which have been defined + interfaces = machine.guest.capability(:network_interfaces) + units = networks.map do |network| + iface = network[:interface].to_i + unit_name = "50-vagrant#{iface}.network" + device = interfaces[iface] + if network[:type].to_s == "dhcp" + network_content = "DHCP=yes" else - connection_string = "#{primary_machine_ip}:7001" - entry = TemplateRenderer.render("guests/coreos/etcd.service", options: { - connection_string: connection_string, - my_ip: current_ip, - }) + prefix = IPAddr.new("255.255.255.255/#{network[:netmask]}").to_i.to_s(2).count("1") + address = "#{network[:ip]}/#{prefix}" + network_content = "Address=#{address}" end - - Tempfile.open("vagrant-coreos-configure-networks") do |f| - f.binmode - f.write(entry) - f.fsync - f.close - comm.upload(f.path, "/tmp/etcd-cluster.service") - end - - # Build a list of commands - commands = [] - - # Stop default systemd - commands << "systemctl stop etcd" - - # Configure interfaces - # FIXME: fix matching of interfaces with IP addresses - networks.each do |network| - iface = interfaces[network[:interface].to_i] - commands << "ifconfig #{iface} #{network[:ip]} netmask #{network[:netmask]}".squeeze(" ") - end - - commands << <<-EOH.gsub(/^ {14}/, '') - mv /tmp/etcd-cluster.service /media/state/units/ - systemctl restart local-enable.service - - # Restart default etcd - systemctl start etcd - EOH - - # Run all network configuration commands in one communicator session. - comm.sudo(commands.join("\n")) + {"name" => unit_name, + "runtime" => "no", + "content" => "[Match]\nName=#{device}\n[Network]\n#{network_content}"} end - end + cloud_config["coreos"] = {"units" => units.compact} - private + # Upload configuration and apply + file = Tempfile.new("vagrant-coreos-networks") + file.puts("#cloud-config\n") + file.puts(cloud_config.to_yaml) + file.close - def self.get_ip(machine) - ip = nil - machine.config.vm.networks.each do |type, opts| - if type == :private_network && opts[:ip] - ip = opts[:ip] - break - end - end - - ip + dst = "/var/tmp/networks.yml" + svc_path = dst.tr("/", "-")[1..-1] + machine.communicate.upload(file.path, dst) + machine.communicate.sudo("systemctl start system-cloudinit@#{svc_path}.service") end end end diff --git a/test/unit/plugins/guests/coreos/cap/change_host_name_test.rb b/test/unit/plugins/guests/coreos/cap/change_host_name_test.rb index 8d9603c2d..1f0d7c53a 100644 --- a/test/unit/plugins/guests/coreos/cap/change_host_name_test.rb +++ b/test/unit/plugins/guests/coreos/cap/change_host_name_test.rb @@ -21,17 +21,47 @@ describe "VagrantPlugins::GuestCoreOS::Cap::ChangeHostName" do describe ".change_host_name" do let(:name) { "banana-rama.example.com" } + let(:has_cloudinit) { false } - it "sets the hostname" do - comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) - comm.expect_command("hostname 'banana-rama'") - described_class.change_host_name(machine, name) + before do + allow(described_class).to receive(:systemd_unit_file?). + with(anything, /cloudinit/).and_return(has_cloudinit) end - it "does not change the hostname if already set" do - comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) - described_class.change_host_name(machine, name) - expect(comm.received_commands.size).to eq(1) + context "with systemd cloud-init" do + let(:has_cloudinit) { true } + + it "should upload cloudinit configuration file" do + expect(comm).to receive(:upload) + described_class.change_host_name(machine, name) + end + + it "should set hostname in configuration file" do + expect(comm).to receive(:upload) do |src, dst| + contents = File.read(src) + expect(contents).to include(name) + end + described_class.change_host_name(machine, name) + end + + it "should start the cloudinit service" do + expect(comm).to receive(:sudo).with(/systemctl start system-cloudinit/) + described_class.change_host_name(machine, name) + end + end + + context "without systemd cloud-init" do + it "sets the hostname" do + comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) + comm.expect_command("hostname 'banana-rama'") + described_class.change_host_name(machine, name) + end + + it "does not change the hostname if already set" do + comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) + described_class.change_host_name(machine, name) + expect(comm.received_commands.size).to eq(1) + end end end end diff --git a/test/unit/plugins/guests/coreos/cap/configure_networks_test.rb b/test/unit/plugins/guests/coreos/cap/configure_networks_test.rb index e8262d45c..d1efad64f 100644 --- a/test/unit/plugins/guests/coreos/cap/configure_networks_test.rb +++ b/test/unit/plugins/guests/coreos/cap/configure_networks_test.rb @@ -8,25 +8,20 @@ describe "VagrantPlugins::GuestCoreOS::Cap::ConfigureNetworks" do .get(:configure_networks) end - let(:machine) { double("machine") } - let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } - + let(:machine) { double("machine", config: config, guest: guest) } + let(:guest) { double("guest") } + let(:config) { double("config", vm: vm) } + let(:vm) { double("vm") } + # let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + let(:comm) { double("comm") } let(:env) do double("env", machine: machine, active_machines: [machine]) end + let(:interfaces) { ["eth0", "eth1", "lo"] } before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:env).and_return(env) - - allow(described_class).to receive(:get_ip).and_return("1.2.3.4") - - comm.stub_command("ifconfig | grep -E '(e[n,t][h,s,p][[:digit:]]([a-z][[:digit:]])?)' | cut -f1 -d:", - stdout: "eth1\neth2") - end - - after do - comm.verify_expectations! end describe ".configure_networks" do @@ -36,7 +31,9 @@ describe "VagrantPlugins::GuestCoreOS::Cap::ConfigureNetworks" do type: "dhcp", } end - + let(:netconfig_1) do + [:public_interface, {}] + end let(:network_2) do { interface: 1, @@ -46,14 +43,145 @@ describe "VagrantPlugins::GuestCoreOS::Cap::ConfigureNetworks" do gateway: "33.33.0.1", } end + let(:netconfig_2) do + [:public_network, {ip: "33.33.33.10", netmask: 16}] + end + let(:network_3) do + { + interface: 2, + type: "static", + ip: "192.168.120.22", + netmask: "255.255.255.0", + gateway: "192.168.120.1" + } + end + let(:netconfig_3) do + [:private_network, {ip: "192.168.120.22", netmask: 24}] + end + let(:networks) { [network_1, network_2, network_3] } + let(:network_configs) { [netconfig_1, netconfig_2, netconfig_3] } + let(:vm) { double("vm") } + let(:default_env_ip) { described_class.const_get(:DEFAULT_ENVIRONMENT_IP) } - it "creates and starts the networks" do - described_class.configure_networks(machine, [network_1, network_2]) - expect(comm.received_commands[1]).to match(/systemctl stop etcd/) - expect(comm.received_commands[1]).to match(/ifconfig eth1 netmask/) - expect(comm.received_commands[1]).to match(/ifconfig eth2 33.33.33.10 netmask 255.255.0.0/) - expect(comm.received_commands[1]).to match(/systemctl restart local-enable.service/) - expect(comm.received_commands[1]).to match(/systemctl start etcd/) + before do + allow(guest).to receive(:capability).with(:network_interfaces). + and_return(interfaces) + allow(vm).to receive(:networks).and_return(network_configs) + allow(comm).to receive(:upload) + allow(comm).to receive(:sudo) + end + + it "should upload network configuration file" do + expect(comm).to receive(:upload) + described_class.configure_networks(machine, networks) + end + + it "should configure public ipv4 address" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PUBLIC_IPV4=#{netconfig_2.last[:ip]}") + end + described_class.configure_networks(machine, networks) + end + + it "should configure the private ipv4 address" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_3.last[:ip]}") + end + described_class.configure_networks(machine, networks) + end + + it "should configure network interfaces" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + interfaces.each { |i| expect(content).to include("Name=#{i}") } + end + described_class.configure_networks(machine, networks) + end + + it "should configure DHCP interface" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("DHCP=yes") + end + described_class.configure_networks(machine, networks) + end + + it "should configure static IP addresses" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + network_configs.map(&:last).find_all { |c| c[:ip] }.each { |c| + expect(content).to include("Address=#{c[:ip]}") + } + end + described_class.configure_networks(machine, networks) + end + + context "when no public network is defined" do + let(:networks) { [network_1, network_3] } + let(:network_configs) { [netconfig_1, netconfig_3] } + + + it "should set public IP to the default environment IP" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PUBLIC_IPV4=#{default_env_ip}") + end + described_class.configure_networks(machine, networks) + end + + it "should set the private IP to the private network" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_3.last[:ip]}") + end + described_class.configure_networks(machine, networks) + end + end + + context "when no private network is defined" do + let(:networks) { [network_1, network_2] } + let(:network_configs) { [netconfig_1, netconfig_2] } + + + it "should set public IP to the public network" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PUBLIC_IPV4=#{netconfig_2.last[:ip]}") + end + described_class.configure_networks(machine, networks) + end + + it "should set the private IP to the public IP" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_2.last[:ip]}") + end + described_class.configure_networks(machine, networks) + end + end + + context "when no public or private network is defined" do + let(:networks) { [network_1] } + let(:network_configs) { [netconfig_1] } + + + it "should set public IP to the default environment IP" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PUBLIC_IPV4=#{default_env_ip}") + end + described_class.configure_networks(machine, networks) + end + + it "should set the private IP to the default environment IP" do + expect(comm).to receive(:upload) do |src, dst| + content = File.read(src) + expect(content).to include("COREOS_PRIVATE_IPV4=#{default_env_ip}") + end + described_class.configure_networks(machine, networks) + end end end end