diff --git a/plugins/guests/alpine/cap/change_host_name.rb b/plugins/guests/alpine/cap/change_host_name.rb new file mode 100644 index 000000000..a08086998 --- /dev/null +++ b/plugins/guests/alpine/cap/change_host_name.rb @@ -0,0 +1,83 @@ +module VagrantPlugins + module GuestAlpine + module Cap + class ChangeHostName + def self.change_host_name(machine, name) + new(machine, name).change! + end + + attr_reader :machine, :new_hostname + + def initialize(machine, new_hostname) + @machine = machine + @new_hostname = new_hostname + end + + def change! + return unless should_change? + + update_etc_hostname + update_etc_hosts + refresh_hostname_service + update_mailname + renew_dhcp + end + + def should_change? + new_hostname != current_hostname + end + + def current_hostname + @current_hostname ||= fetch_current_hostname + end + + def fetch_current_hostname + hostname = '' + machine.communicate.sudo 'hostname -f' do |type, data| + hostname = data.chomp if type == :stdout && hostname.empty? + end + + hostname + end + + def update_etc_hostname + machine.communicate.sudo("echo '#{short_hostname}' > /etc/hostname") + end + + # /etc/hosts should resemble: + # 127.0.0.1 localhost + # 127.0.1.1 host.fqdn.com host.fqdn host + def update_etc_hosts + if machine.communicate.test("grep '#{current_hostname}' /etc/hosts") + # Current hostname entry is in /etc/hosts + ip_address = '([0-9]{1,3}\.){3}[0-9]{1,3}' + search = "^(#{ip_address})\\s+#{Regexp.escape(current_hostname)}(\\s.*)?$" + replace = "\\1 #{new_hostname} #{short_hostname}" + expression = ['s', search, replace, 'g'].join('@') + + machine.communicate.sudo("sed -ri '#{expression}' /etc/hosts") + else + # Current hostname entry isn't in /etc/hosts, just append it + machine.communicate.sudo("echo '127.0.1.1 #{new_hostname} #{short_hostname}' >>/etc/hosts") + end + end + + def refresh_hostname_service + machine.communicate.sudo('hostname -F /etc/hostname') + end + + def update_mailname + machine.communicate.sudo('hostname -f > /etc/mailname') + end + + def renew_dhcp + machine.communicate.sudo('ifdown -a; ifup -a; ifup eth0') + end + + def short_hostname + new_hostname.split('.').first + end + end + end + end +end diff --git a/plugins/guests/alpine/cap/configure_networks.rb b/plugins/guests/alpine/cap/configure_networks.rb new file mode 100644 index 000000000..361e64610 --- /dev/null +++ b/plugins/guests/alpine/cap/configure_networks.rb @@ -0,0 +1,64 @@ +# rubocop:disable Metrics/MethodLength +# rubocop:disable Metrics/AbcSize +# rubocop:disable Style/BracesAroundHashParameters +# +# FIXME: address disabled warnings +# +require 'set' +require 'tempfile' +require 'pathname' +require 'vagrant/util/template_renderer' + +module VagrantPlugins + module GuestAlpine + module Cap + class ConfigureNetworks + include Vagrant::Util + def self.configure_networks(machine, networks) + machine.communicate.tap do |comm| + # First, remove any previous network modifications + # from the interface file. + comm.sudo("sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre") + comm.sudo("sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tail -n +2 > /tmp/vagrant-network-interfaces.post") + + # Accumulate the configurations to add to the interfaces file as + # well as what interfaces we're actually configuring since we use that + # later. + interfaces = Set.new + entries = [] + networks.each do |network| + interfaces.add(network[:interface]) + entry = TemplateRenderer.render("guests/alpine/network_#{network[:type]}", { options: network }) + entries << entry + end + + # Perform the careful dance necessary to reconfigure + # the network interfaces + temp = Tempfile.new('vagrant') + temp.binmode + temp.write(entries.join("\n")) + temp.close + + comm.upload(temp.path, '/tmp/vagrant-network-entry') + + # Bring down all the interfaces we're reconfiguring. By bringing down + # each specifically, we avoid reconfiguring eth0 (the NAT interface) so + # SSH never dies. + interfaces.each do |interface| + comm.sudo("/sbin/ifdown eth#{interface} 2> /dev/null") + comm.sudo("/sbin/ip addr flush dev eth#{interface} 2> /dev/null") + end + + comm.sudo('cat /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post > /etc/network/interfaces') + comm.sudo('rm -f /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post') + + # Bring back up each network interface, reconfigured + interfaces.each do |interface| + comm.sudo("/sbin/ifup eth#{interface}") + end + end + end + end + end + end +end diff --git a/plugins/guests/alpine/cap/halt.rb b/plugins/guests/alpine/cap/halt.rb new file mode 100644 index 000000000..550c15452 --- /dev/null +++ b/plugins/guests/alpine/cap/halt.rb @@ -0,0 +1,21 @@ +# rubocop:disable Style/RedundantBegin +# rubocop:disable Lint/HandleExceptions +# +# FIXME: address disabled warnings +# +module VagrantPlugins + module GuestAlpine + module Cap + class Halt + def self.halt(machine) + begin + machine.communicate.sudo('poweroff') + rescue Net::SSH::Disconnect, IOError + # Ignore, this probably means connection closed because it + # shut down and SSHd was stopped. + end + end + end + end + end +end diff --git a/plugins/guests/alpine/cap/nfs_client.rb b/plugins/guests/alpine/cap/nfs_client.rb new file mode 100644 index 000000000..b9d803134 --- /dev/null +++ b/plugins/guests/alpine/cap/nfs_client.rb @@ -0,0 +1,14 @@ +module VagrantPlugins + module GuestAlpine + module Cap + class NFSClient + def self.nfs_client_install(machine) + machine.communicate.sudo('apk update') + machine.communicate.sudo('apk add --upgrade nfs-utils') + machine.communicate.sudo('rc-update add rpc.statd') + machine.communicate.sudo('rc-service rpc.statd start') + end + end + end + end +end diff --git a/plugins/guests/alpine/cap/rsync.rb b/plugins/guests/alpine/cap/rsync.rb new file mode 100644 index 000000000..436bfbc20 --- /dev/null +++ b/plugins/guests/alpine/cap/rsync.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module GuestAlpine + module Cap + class RSync + def self.rsync_installed(machine) + machine.communicate.test('test -f /usr/bin/rsync') + end + + def self.rsync_install(machine) + machine.communicate.tap do |comm| + comm.sudo('apk add rsync') + end + end + end + end + end +end diff --git a/plugins/guests/alpine/cap/smb.rb b/plugins/guests/alpine/cap/smb.rb new file mode 100644 index 000000000..55e52394c --- /dev/null +++ b/plugins/guests/alpine/cap/smb.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module GuestAlpine + module Cap + class SMB + def self.smb_install(machine) + machine.communicate.tap do |comm| + comm.sudo('apk add cifs-utils') + end + end + end + end + end +end \ No newline at end of file diff --git a/plugins/guests/alpine/guest.rb b/plugins/guests/alpine/guest.rb new file mode 100644 index 000000000..e0330f916 --- /dev/null +++ b/plugins/guests/alpine/guest.rb @@ -0,0 +1,11 @@ +require 'vagrant' + +module VagrantPlugins + module GuestAlpine + class Guest < Vagrant.plugin('2', :guest) + def detect?(machine) + machine.communicate.test('cat /etc/alpine-release') + end + end + end +end diff --git a/plugins/guests/alpine/plugin.rb b/plugins/guests/alpine/plugin.rb new file mode 100644 index 000000000..62e8ec6fd --- /dev/null +++ b/plugins/guests/alpine/plugin.rb @@ -0,0 +1,50 @@ +require 'vagrant' + +module VagrantPlugins + module GuestAlpine + class Plugin < Vagrant.plugin('2') + name 'Alpine guest' + description 'Alpine Linux guest support.' + + guest(:alpine, :linux) do + require File.expand_path('../guest', __FILE__) + Guest + end + + guest_capability(:alpine, :configure_networks) do + require_relative 'cap/configure_networks' + Cap::ConfigureNetworks + end + + guest_capability(:alpine, :halt) do + require_relative 'cap/halt' + Cap::Halt + end + + guest_capability(:alpine, :change_host_name) do + require_relative 'cap/change_host_name' + Cap::ChangeHostName + end + + guest_capability(:alpine, :nfs_client_install) do + require_relative 'cap/nfs_client' + Cap::NFSClient + end + + guest_capability(:alpine, :rsync_installed) do + require_relative 'cap/rsync' + Cap::RSync + end + + guest_capability(:alpine, :rsync_install) do + require_relative 'cap/rsync' + Cap::RSync + end + + guest_capability(:alpine, :smb_install) do + require_relative 'cap/smb' + Cap::SMB + end + end + end +end diff --git a/templates/guests/alpine/network_dhcp.erb b/templates/guests/alpine/network_dhcp.erb new file mode 100644 index 000000000..5a4ed783c --- /dev/null +++ b/templates/guests/alpine/network_dhcp.erb @@ -0,0 +1,13 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +auto eth<%= options[:interface] %> +iface eth<%= options[:interface] %> inet dhcp +<% if !options[:use_dhcp_assigned_default_route] %> + post-up route del default dev $IFACE || true +<% else %> + # We need to disable eth0, see GH-2648 + post-up route del default dev eth0 + post-up dhclient $IFACE + pre-down route add default dev eth0 +<% end %> +#VAGRANT-END diff --git a/templates/guests/alpine/network_static.erb b/templates/guests/alpine/network_static.erb new file mode 100644 index 000000000..fa505afa9 --- /dev/null +++ b/templates/guests/alpine/network_static.erb @@ -0,0 +1,7 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +auto eth<%= options[:interface] %> +iface eth<%= options[:interface] %> inet static + address <%= options[:ip] %> + netmask <%= options[:netmask] %> +#VAGRANT-END diff --git a/test/unit/plugins/guests/alpine/cap/change_host_name_test.rb b/test/unit/plugins/guests/alpine/cap/change_host_name_test.rb new file mode 100644 index 000000000..3f159ad75 --- /dev/null +++ b/test/unit/plugins/guests/alpine/cap/change_host_name_test.rb @@ -0,0 +1,127 @@ +require_relative "../../../../base" + +describe 'VagrantPlugins::GuestAlpine::Cap::ChangeHostname' do + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:change_host_name) + end + let(:machine) { double('machine') } + let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + let(:old_hostname) { 'oldhostname.olddomain.tld' } + + before do + allow(machine).to receive(:communicate).and_return(communicator) + communicator.stub_command('hostname -f', stdout: old_hostname) + end + + after do + communicator.verify_expectations! + end + + describe '.change_host_name' do + it 'updates /etc/hostname on the machine' do + communicator.expect_command("echo 'newhostname' > /etc/hostname") + described_class.change_host_name(machine, 'newhostname.newdomain.tld') + end + + it 'does nothing when the provided hostname is not different' do + described_class.change_host_name(machine, 'oldhostname.olddomain.tld') + expect(communicator.received_commands).to eq(['hostname -f']) + end + + it 'refreshes the hostname service with the hostname command' do + communicator.expect_command('hostname -F /etc/hostname') + described_class.change_host_name(machine, 'newhostname.newdomain.tld') + end + + it 'renews dhcp on the system with the new hostname' do + communicator.expect_command('ifdown -a; ifup -a; ifup eth0') + described_class.change_host_name(machine, 'newhostname.newdomain.tld') + end + + describe 'flipping out the old hostname in /etc/hosts' do + let(:sed_command) do + # Here we run the change_host_name through and extract the recorded sed + # command from the dummy communicator + described_class.change_host_name(machine, 'newhostname.newdomain.tld') + communicator.received_commands.find { |cmd| cmd =~ /^sed/ } + end + + # Now we extract the regexp from that sed command so we can do some + # verification on it + let(:expression) { sed_command.sub(%r{^sed -ri '\(.*\)' /etc/hosts$}, "\1") } + let(:search) { Regexp.new(expression.split('@')[1], Regexp::EXTENDED) } + let(:replace) { expression.split('@')[2] } + + let(:grep_command) { "grep '#{old_hostname}' /etc/hosts" } + + before do + communicator.stub_command(grep_command, exit_code: 0) + end + + it 'works on an simple /etc/hosts file' do + original_etc_hosts = <<-ETC_HOSTS.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 oldhostname.olddomain.tld oldhostname + ETC_HOSTS + + modified_etc_hosts = original_etc_hosts.gsub(search, replace) + + expect(modified_etc_hosts).to eq <<-RESULT.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 newhostname.newdomain.tld newhostname + RESULT + end + + it 'does not modify lines which contain similar hostnames' do + original_etc_hosts = <<-ETC_HOSTS.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 oldhostname.olddomain.tld oldhostname + # common prefix, but different fqdn + 192.168.12.34 oldhostname.olddomain.tld.different + # different characters at the dot + 192.168.34.56 oldhostname-olddomain.tld + ETC_HOSTS + + modified_etc_hosts = original_etc_hosts.gsub(search, replace) + + expect(modified_etc_hosts).to eq <<-RESULT.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 newhostname.newdomain.tld newhostname + # common prefix, but different fqdn + 192.168.12.34 oldhostname.olddomain.tld.different + # different characters at the dot + 192.168.34.56 oldhostname-olddomain.tld + RESULT + end + + it "appends 127.0.1.1 if it isn't there" do + communicator.stub_command(grep_command, exit_code: 1) + described_class.change_host_name(machine, 'newhostname.newdomain.tld') + + sed = communicator.received_commands.find { |cmd| cmd =~ /^sed/ } + expect(sed).to be_nil + + echo = communicator.received_commands.find { |cmd| cmd =~ /^echo/ } + expect(echo).to_not be_nil + end + + context 'when the old fqdn has a trailing dot' do + let(:old_hostname) { 'oldhostname.withtrailing.dot.' } + + it 'modifies /etc/hosts properly' do + original_etc_hosts = <<-ETC_HOSTS.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 oldhostname.withtrailing.dot. oldhostname + ETC_HOSTS + + modified_etc_hosts = original_etc_hosts.gsub(search, replace) + + expect(modified_etc_hosts).to eq <<-RESULT.gsub(/^ */, '') + 127.0.0.1 localhost + 127.0.1.1 newhostname.newdomain.tld newhostname + RESULT + end + end + end + end +end diff --git a/test/unit/plugins/guests/alpine/cap/configure_networks_test.rb b/test/unit/plugins/guests/alpine/cap/configure_networks_test.rb new file mode 100644 index 000000000..31a361beb --- /dev/null +++ b/test/unit/plugins/guests/alpine/cap/configure_networks_test.rb @@ -0,0 +1,39 @@ +require_relative "../../../../base" + +describe 'VagrantPlugins::GuestAlpine::Cap::ConfigureNetworks' do + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:configure_networks) + end + let(:machine) { double('machine') } + let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(communicator) + end + + after do + communicator.verify_expectations! + end + + it 'should configure networks' do + networks = [ + { type: :static, ip: '192.168.10.10', netmask: '255.255.255.0', interface: 0, name: 'eth0' }, + { type: :dhcp, interface: 1, name: 'eth1' } + ] + + expect(communicator).to receive(:sudo).with("sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre") + expect(communicator).to receive(:sudo).with("sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tail -n +2 > /tmp/vagrant-network-interfaces.post") + expect(communicator).to receive(:sudo).with('/sbin/ifdown eth0 2> /dev/null') + expect(communicator).to receive(:sudo).with('/sbin/ip addr flush dev eth0 2> /dev/null') + expect(communicator).to receive(:sudo).with('/sbin/ifdown eth1 2> /dev/null') + expect(communicator).to receive(:sudo).with('/sbin/ip addr flush dev eth1 2> /dev/null') + expect(communicator).to receive(:sudo).with('cat /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post > /etc/network/interfaces') + expect(communicator).to receive(:sudo).with('rm -f /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post') + expect(communicator).to receive(:sudo).with('/sbin/ifup eth0') + expect(communicator).to receive(:sudo).with('/sbin/ifup eth1') + + allow_message_expectations_on_nil + + described_class.configure_networks(machine, networks) + end +end diff --git a/test/unit/plugins/guests/alpine/cap/halt_test.rb b/test/unit/plugins/guests/alpine/cap/halt_test.rb new file mode 100644 index 000000000..e10c7dc99 --- /dev/null +++ b/test/unit/plugins/guests/alpine/cap/halt_test.rb @@ -0,0 +1,23 @@ +require_relative "../../../../base" + +describe 'VagrantPlugins::GuestAlpine::Cap::Halt' do + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:halt) + end + let(:machine) { double('machine') } + let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(communicator) + end + + after do + communicator.verify_expectations! + end + + it 'should halt guest' do + expect(communicator).to receive(:sudo).with('poweroff') + allow_message_expectations_on_nil + described_class.halt(machine) + end +end diff --git a/test/unit/plugins/guests/alpine/cap/nfs_client_test.rb b/test/unit/plugins/guests/alpine/cap/nfs_client_test.rb new file mode 100644 index 000000000..9273707c3 --- /dev/null +++ b/test/unit/plugins/guests/alpine/cap/nfs_client_test.rb @@ -0,0 +1,27 @@ +require_relative "../../../../base" + +describe 'VagrantPlugins::GuestAlpine::Cap::NFSClient' do + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:nfs_client_install) + end + + let(:machine) { double('machine') } + let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(communicator) + end + + after do + communicator.verify_expectations! + end + + it 'should install nfs client' do + described_class.nfs_client_install(machine) + + expect(communicator.received_commands[0]).to match(/apk update/) + expect(communicator.received_commands[1]).to match(/apk add --upgrade nfs-utils/) + expect(communicator.received_commands[2]).to match(/rc-update add rpc.statd/) + expect(communicator.received_commands[3]).to match(/rc-service rpc.statd start/) + end +end diff --git a/test/unit/plugins/guests/alpine/cap/rsync_test.rb b/test/unit/plugins/guests/alpine/cap/rsync_test.rb new file mode 100644 index 000000000..6b305a5f9 --- /dev/null +++ b/test/unit/plugins/guests/alpine/cap/rsync_test.rb @@ -0,0 +1,36 @@ +require_relative "../../../../base" + +describe 'VagrantPlugins::GuestAlpine::Cap::RSync' do + let(:machine) { double('machine') } + let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(communicator) + end + + after do + communicator.verify_expectations! + end + + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:rsync_install) + end + + it 'should install rsync' do + # communicator.should_receive(:sudo).with('apk add rsync') + expect(communicator).to receive(:sudo).with('apk add rsync') + allow_message_expectations_on_nil + described_class.rsync_install(machine) + end + + let(:described_class) do + VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:rsync_installed) + end + + it 'should verify rsync installed' do + # communicator.should_receive(:test).with('test -f /usr/bin/rsync') + expect(communicator).to receive(:test).with('test -f /usr/bin/rsync') + allow_message_expectations_on_nil + described_class.rsync_installed(machine) + end +end