diff --git a/.gitignore b/.gitignore index 0f005ff..889e1db 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tags .vagrant .vagrant_dns.json +.idea \ No newline at end of file diff --git a/Gemfile b/Gemfile index 931a136..da6384e 100644 --- a/Gemfile +++ b/Gemfile @@ -5,11 +5,12 @@ source 'https://rubygems.org' gem 'rubydns', '1.0.2' gem 'rexec' -gem 'rake' +gem 'rake', '~> 10.0' # Vagrant's special group group :plugins do gem 'landrush', path: '.' + gem 'landrush-ip', '~> 0.2.1' end group :test do diff --git a/README.md b/README.md index 283a839..3796514 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,17 @@ Landrush assigns your vm's hostname from either the vagrant config (see the `exa Every time a VM is started, its IP address is automatically detected and a DNS record is created that maps the hostname to its IP. -If for any reason the auto-detection detects no IP address or the wrong IP address, or you want to override it, you can do like so: +If for any reason the auto-detection detects no IP address or the wrong IP address you can control how it looks for it. + +You can start by excluding interfaces matching an array of regex patterns (the value shown here is the default): + + config.landrush.exclude = [/lo[0-9]*/, /docker[0-9]+/, /tun[0-9]+/] + +Or, if you know which interface you need the IP of you can specify that too (default is none): + + config.landrush.interface = 'eth0' + +If all else fails, you can override it entirely: config.landrush.host_ip_address = '1.2.3.4' diff --git a/Rakefile b/Rakefile index 3074b5d..8149aa2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,20 @@ +require 'rubygems' +require 'bundler/setup' require 'bundler/gem_tasks' require 'rake/testtask' require 'rubocop/rake_task' +# Immediately sync all stdout +$stdout.sync = true +$stderr.sync = true + +# Change to the directory of this file +Dir.chdir(File.expand_path('../', __FILE__)) + +# Rubocop +RuboCop::RakeTask.new + +# Tests Rake::TestTask.new do |t| t.pattern = 'test/**/*_test.rb' t.libs << 'test' @@ -15,5 +28,3 @@ task default: [ task :generate_diagrams do sh 'cd doc; seqdiag --fontmap=support/seqdiag.fontmap -Tsvg vagrant_dns_without_landrush.diag' end - -RuboCop::RakeTask.new diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..0505a40 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,27 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure('2') do |config| + config.vm.define 'landrush-test-debian' do |machine| + machine.vm.box = 'debian/jessie64' + + # Add a DHCP network so we don't know its IP :P + machine.vm.network 'private_network', type: 'dhcp' + + machine.vm.provider :virtualbox do |provider, _| + provider.memory = 512 + provider.cpus = 2 + end + + machine.landrush_ip.override = true + + machine.vm.hostname = 'landrush-dev' + machine.vm.network 'private_network', type: 'dhcp' + + # Landrush (DNS) + machine.landrush.enabled = true + machine.landrush.tld = 'landrush' + machine.landrush.interface = 'eth1' + machine.landrush.exclude = [/lo[0-9]*/, /docker[0-9]+/, /tun[0-9]+/] + end +end diff --git a/lib/landrush/cap/all/read_host_visible_ip_address.rb b/lib/landrush/cap/all/read_host_visible_ip_address.rb new file mode 100644 index 0000000..8af885e --- /dev/null +++ b/lib/landrush/cap/all/read_host_visible_ip_address.rb @@ -0,0 +1,49 @@ +module Landrush + module Cap + module All + module ReadHostVisibleIpAddress + def self.filter_addresses(addresses) + unless @machine.config.landrush.exclude.nil? + re = Regexp.union(@machine.config.landrush.exclude) + + addresses = addresses.select do |addr| + !addr['name'].match(re) + end + end + + addresses + end + + def self.read_host_visible_ip_address(machine) + @machine = machine + + addr = nil + addresses = machine.guest.capability(:landrush_ip_get) + + # Short circuit this one first: if an explicit interface is defined, look for it and return it if found. + # Technically, we could do a single loop, but execution time is not vital here. + # This allows us to be more accurate, especially with logging what's going on. + unless machine.config.landrush.interface.nil? + addr = addresses.detect { |a| a['name'] == machine.config.landrush.interface } + + machine.env.ui.warn "[landrush] Unable to find interface #{machine.config.landrush.interface}" if addr.nil? + end + + if addr.nil? + addresses = filter_addresses addresses + + raise 'No addresses found' if addresses.empty? + + addr = addresses.last + end + + ip = IPAddr.new(addr['ipv4']) + + machine.env.ui.info "[landrush] Using #{addr['name']} (#{addr['ipv4']})" + + ip.to_s + end + end + end + end +end diff --git a/lib/landrush/cap/linux/read_host_visible_ip_address.rb b/lib/landrush/cap/linux/read_host_visible_ip_address.rb deleted file mode 100644 index e34e08f..0000000 --- a/lib/landrush/cap/linux/read_host_visible_ip_address.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Landrush - module Cap - module Linux - module ReadHostVisibleIpAddress - # - # !!!!!!!!!!!! - # !! NOTE !! - # !!!!!!!!!!!! - # - # This is a fragile heuristic: we are simply assuming the IP address of - # the last interface non-localhost IP address is the host-visible one. - # - # For VMWare, the interface that Vagrant uses is host accessible, so we - # expect this to be the same as `read_ip_address`. - # - # For VirtualBox, the Vagrant interface is not host visible, so we add - # our own private_network, which we expect this to return for us. - # - # If the Vagrantfile sets up any sort of fancy networking, this has the - # potential to fail, which will break things. - # - # TODO: Find a better heuristic for this implementation. - # - def self.read_host_visible_ip_address(machine) - result = "" - machine.communicate.execute(command) do |type, data| - result << data if type == :stdout - end - - last_line = result.chomp.split("\n").last || '' - addresses = last_line.split(/\s+/).map { |address| IPAddr.new(address) } - addresses = addresses.reject { |address| address.ipv6? } - - if addresses.empty? - raise "Cannot detect IP address, command `#{command}` returned `#{result}`" - end - - addresses.last.to_s - end - - def self.command - %Q(hostname -I) - end - end - end - end -end diff --git a/lib/landrush/config.rb b/lib/landrush/config.rb index 56e9fd3..6098a5b 100644 --- a/lib/landrush/config.rb +++ b/lib/landrush/config.rb @@ -6,13 +6,17 @@ class Config < Vagrant.plugin('2', :config) attr_accessor :upstream_servers attr_accessor :host_ip_address attr_accessor :guest_redirect_dns + attr_accessor :interface + attr_accessor :exclude DEFAULTS = { :enabled => false, :tld => 'vagrant.test', :upstream_servers => [[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]], :host_ip_address => nil, - :guest_redirect_dns => true + :guest_redirect_dns => true, + :interface => nil, + :exclude => [/lo[0-9]*/, /docker[0-9]+/, /tun[0-9]+/] } def initialize @@ -22,6 +26,8 @@ def initialize @upstream_servers = UNSET_VALUE @host_ip_address = UNSET_VALUE @guest_redirect_dns = UNSET_VALUE + @interface = UNSET_VALUE + @exclude = UNSET_VALUE end def enable diff --git a/lib/landrush/plugin.rb b/lib/landrush/plugin.rb index dad8357..89ee18e 100644 --- a/lib/landrush/plugin.rb +++ b/lib/landrush/plugin.rb @@ -98,8 +98,8 @@ def self.post_boot_actions end guest_capability('linux', 'read_host_visible_ip_address') do - require_relative 'cap/linux/read_host_visible_ip_address' - Cap::Linux::ReadHostVisibleIpAddress + require_relative 'cap/all/read_host_visible_ip_address' + Cap::All::ReadHostVisibleIpAddress end end end diff --git a/test/landrush/cap/all/read_host_visible_ip_address_test.rb b/test/landrush/cap/all/read_host_visible_ip_address_test.rb new file mode 100644 index 0000000..2fe6f5c --- /dev/null +++ b/test/landrush/cap/all/read_host_visible_ip_address_test.rb @@ -0,0 +1,87 @@ +require 'test_helper' + +module Landrush + module Cap + module All + describe ReadHostVisibleIpAddress do + let(:machine) { fake_machine } + let(:addresses) { fake_addresses } + + def call_cap(machine) + Landrush::Cap::All::ReadHostVisibleIpAddress.read_host_visible_ip_address(machine) + end + + before do + # TODO: Is there a way to only unstub it for read_host_visible_ip_address + machine.guest.unstub(:capability) + machine.guest.stubs(:capability).with(:landrush_ip_get).returns(fake_addresses) + end + + describe 'read_host_visible_ip_address' do + # First, test with an empty response (no addresses) + it 'should throw an error when there are no addresses' do + machine.guest.stubs(:capability).with(:landrush_ip_get).returns([]) + + lambda do + call_cap(machine) + end.must_raise(RuntimeError, 'No addresses found') + end + + # Step 1: nothing excluded, nothing explicitly selected + it 'should return the last address' do + machine.config.landrush.interface = nil + machine.config.landrush.exclude = [] + + expected = addresses.last['ipv4'] + + # call_cap(machine).must_equal expected + call_cap(machine).must_equal expected + end + + # Test exclusion mechanics; it should select the las + it 'should ignore interfaces that are excluded and select the last not excluded interface' do + machine.config.landrush.interface = nil + machine.config.landrush.exclude = [/exclude[0-9]+/] + + expected = addresses.detect { |a| a['name'] == 'include3' } + expected = expected['ipv4'] + + call_cap(machine).must_equal expected + end + + # Explicitly select one; this supersedes the exclusion mechanic + it 'should select the desired interface' do + machine.config.landrush.interface = 'include1' + machine.config.landrush.exclude = [/exclude[0-9]+/] + + expected = addresses.detect { |a| a['name'] == 'include1' } + expected = expected['ipv4'] + + call_cap(machine).must_equal expected + end + + # Now make sure it returns the last not excluded interface when the desired interface does not exist + it 'should return the last not excluded interface if the desired interface does not exist' do + machine.config.landrush.interface = 'dummy' + machine.config.landrush.exclude = [/exclude[0-9]+/] + + expected = addresses.detect { |a| a['name'] == 'include3' } + expected = expected['ipv4'] + + call_cap(machine).must_equal expected + end + + # Now make sure it returns the last interface overall when nothing is excluded + it 'should return the last interface if the desired interface does not exist' do + machine.config.landrush.interface = 'dummy' + machine.config.landrush.exclude = [] + + expected = addresses.last['ipv4'] + + call_cap(machine).must_equal expected + end + end + end + end + end +end diff --git a/test/landrush/cap/linux/read_host_visible_ip_address_test.rb b/test/landrush/cap/linux/read_host_visible_ip_address_test.rb deleted file mode 100644 index 33b4107..0000000 --- a/test/landrush/cap/linux/read_host_visible_ip_address_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'test_helper' - -module Landrush - module Cap - module Linux - - describe ReadHostVisibleIpAddress do - describe 'read_host_visible_ip_address' do - let (:machine) { fake_machine } - - it 'should read the last address' do - machine.communicate.stub_command(Landrush::Cap::Linux::ReadHostVisibleIpAddress.command, "1.2.3.4 5.6.7.8\n") - machine.guest.capability(:read_host_visible_ip_address).must_equal '5.6.7.8' - end - - it 'should ignore IPv6 addresses' do - machine.communicate.stub_command(Landrush::Cap::Linux::ReadHostVisibleIpAddress.command, "1.2.3.4 5.6.7.8 fdb2:2c26:f4e4:0:21c:42ff:febc:ea4f\n") - machine.guest.capability(:read_host_visible_ip_address).must_equal '5.6.7.8' - end - - it 'should fail on invalid address' do - machine.communicate.stub_command(Landrush::Cap::Linux::ReadHostVisibleIpAddress.command, "hello world\n") - lambda { - machine.guest.capability(:read_host_visible_ip_address) - }.must_raise(IPAddr::InvalidAddressError) - end - - it 'should fail without address' do - machine.communicate.stub_command(Landrush::Cap::Linux::ReadHostVisibleIpAddress.command, "\n") - lambda { - machine.guest.capability(:read_host_visible_ip_address) - }.must_raise(RuntimeError, 'Cannot detect IP address, command `hostname -I` returned ``') - end - end - end - - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index b0bb6f1..00a245f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,12 +5,25 @@ require 'landrush' require 'landrush/cap/linux/configured_dns_servers' -require 'landrush/cap/linux/read_host_visible_ip_address' require 'landrush/cap/linux/redirect_dns' +require 'landrush/cap/all/read_host_visible_ip_address' require 'minitest/autorun' require 'mocha/mini_test' +# Make sure to keep the numbering sequential here +# Putting include/exclude out of order is kind of the point though ;) +def fake_addresses + [ + { 'name' => 'exclude1', 'ipv4' => '172.28.128.1', 'ipv6' => '::1' }, + { 'name' => 'include1', 'ipv4' => '172.28.128.2', 'ipv6' => '::2' }, + { 'name' => 'include2', 'ipv4' => '172.28.128.3', 'ipv6' => '::3' }, + { 'name' => 'include3', 'ipv4' => '172.28.128.4', 'ipv6' => '::4' }, + { 'name' => 'exclude2', 'ipv4' => '172.28.128.5', 'ipv6' => '::5' }, + { 'name' => 'exclude3', 'ipv4' => '172.28.128.6', 'ipv6' => '::6' } + ] +end + def fake_environment(options = { enabled: true }) { machine: fake_machine(options), ui: FakeUI } end @@ -91,14 +104,14 @@ def fake_machine(options={}) ) machine.instance_variable_set("@communicator", RecordingCommunicator.new) - machine.communicate.stub_command( - Landrush::Cap::Linux::ReadHostVisibleIpAddress.command, - "#{options.fetch(:ip, '1.2.3.4')}\n" - ) machine.config.landrush.enabled = options.fetch(:enabled, false) + machine.config.landrush.interface = nil + machine.config.landrush.exclude = [/exclude[0-9]+/] machine.config.vm.hostname = options.fetch(:hostname, 'somehost.vagrant.test') + machine.guest.stubs(:capability).with(:read_host_visible_ip_address).returns("#{options.fetch(:ip, '1.2.3.4')}") + machine end