From 5e18ae7b82401adda38b7b5a83c75db37f13b484 Mon Sep 17 00:00:00 2001 From: Hardy Ferentschik Date: Wed, 15 Jun 2016 21:37:39 +0200 Subject: [PATCH] Issue #171 Adding test harness. Experimenting with VBoxManage --- .gitignore | 16 +-- Gemfile.lock | 175 ----------------------- lib/landrush.rb | 1 + lib/landrush/action/setup.rb | 19 ++- lib/landrush/registry_config.rb | 80 ----------- lib/landrush/resolver_config.rb | 10 +- lib/landrush/win_network_config.rb | 121 ++++++++++++++++ test/landrush/resolver_config_test.rb | 2 - test/landrush/win_network_config_test.rb | 80 +++++++++++ test/test_helper.rb | 13 ++ 10 files changed, 241 insertions(+), 276 deletions(-) delete mode 100644 Gemfile.lock delete mode 100644 lib/landrush/registry_config.rb create mode 100644 lib/landrush/win_network_config.rb create mode 100644 test/landrush/win_network_config_test.rb diff --git a/.gitignore b/.gitignore index d3c74b5..b0661ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,22 @@ +# Bundler +Gemfile.lock +.bundle + +# Ruby / RVM .ruby-gemset .ruby-version -*.rbc -.bundle + +# Rake / Build .yardoc -InstalledFiles _yardoc coverage lib/bundler/man pkg rdoc spec/reports -test/tmp -test/version_tmp build tmp -tags - -.vagrant +# IDE *.iml .idea diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 31cac4b..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,175 +0,0 @@ -GIT - remote: git://github.com/mitchellh/vagrant.git - revision: bedaeee190b2bc0a7962b143ad82eec49d7f0d60 - ref: v1.8.4 - specs: - vagrant (1.8.4) - bundler (= 1.12.5) - childprocess (~> 0.5.0) - erubis (~> 2.7.0) - hashicorp-checkpoint (~> 0.1.1) - i18n (>= 0.6.0, <= 0.8.0) - listen (~> 3.0.2) - log4r (~> 1.1.9, < 1.1.11) - net-scp (~> 1.1.0) - net-sftp (~> 2.1) - net-ssh (~> 3.0.1) - nokogiri (= 1.6.7.1) - rb-kqueue (~> 0.2.0) - rest-client (>= 1.6.0, < 2.0) - wdm (~> 0.1.0) - winrm (~> 1.6) - winrm-fs (~> 0.3.0) - -PATH - remote: . - specs: - landrush (1.1.0.dev) - rubydns (= 0.8.5) - win32-process - -GEM - remote: https://rubygems.org/ - specs: - aruba (0.14.1) - childprocess (~> 0.5.6) - contracts (~> 0.9) - cucumber (>= 1.3.19) - ffi (~> 1.9.10) - rspec-expectations (>= 2.99) - thor (~> 0.19) - ast (2.3.0) - builder (3.2.2) - byebug (9.0.5) - childprocess (0.5.9) - ffi (~> 1.0, >= 1.0.11) - contracts (0.14.0) - cucumber (2.4.0) - builder (>= 2.1.2) - cucumber-core (~> 1.5.0) - cucumber-wire (~> 0.0.1) - diff-lcs (>= 1.1.3) - gherkin (~> 4.0) - multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.1.2) - cucumber-core (1.5.0) - gherkin (~> 4.0) - cucumber-wire (0.0.1) - diff-lcs (1.2.5) - domain_name (0.5.20160615) - unf (>= 0.0.5, < 1.0.0) - erubis (2.7.0) - eventmachine (1.0.9.1) - events (0.9.8) - ffi (1.9.10) - gherkin (4.0.0) - gssapi (1.2.0) - ffi (>= 1.0.1) - gyoku (1.3.1) - builder (>= 2.1.2) - hashicorp-checkpoint (0.1.4) - http-cookie (1.0.2) - domain_name (~> 0.5) - httpclient (2.8.0) - i18n (0.7.0) - json (1.8.3) - komenda (0.1.6) - events (~> 0.9.8) - landrush-ip (0.2.3) - listen (3.0.8) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - little-plugger (1.1.4) - log4r (1.1.10) - logging (2.1.0) - little-plugger (~> 1.1) - multi_json (~> 1.10) - metaclass (0.0.4) - mime-types (2.99.2) - mini_portile2 (2.0.0) - minitest (5.9.0) - mocha (1.1.0) - metaclass (~> 0.0.1) - multi_json (1.12.1) - multi_test (0.1.2) - net-scp (1.1.2) - net-ssh (>= 2.6.5) - net-sftp (2.1.2) - net-ssh (>= 2.6.5) - net-ssh (3.0.2) - netrc (0.11.0) - nokogiri (1.6.7.1) - mini_portile2 (~> 2.0.0.rc2) - nori (2.6.0) - parser (2.3.1.2) - ast (~> 2.2) - powerpack (0.1.1) - rainbow (2.1.0) - rake (10.5.0) - rb-fsevent (0.9.7) - rb-inotify (0.9.7) - ffi (>= 0.5.0) - rb-kqueue (0.2.4) - ffi (>= 0.5.0) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) - rspec-expectations (3.4.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - rubocop (0.38.0) - parser (>= 2.3.0.6, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) - ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) - rubydns (0.8.5) - eventmachine (~> 1.0.0) - rubyntlm (0.6.0) - rubyzip (1.2.0) - thor (0.19.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.2) - unicode-display_width (1.0.5) - wdm (0.1.1) - win32-process (0.8.3) - ffi (>= 1.0.0) - winrm (1.8.1) - builder (>= 2.1.2) - gssapi (~> 1.2) - gyoku (~> 1.0) - httpclient (~> 2.2, >= 2.2.0.2) - logging (>= 1.6.1, < 3.0) - nori (~> 2.0) - rubyntlm (~> 0.6.0) - winrm-fs (0.3.2) - erubis (~> 2.7) - logging (>= 1.6.1, < 3.0) - rubyzip (~> 1.1) - winrm (~> 1.5) - -PLATFORMS - ruby - -DEPENDENCIES - aruba (~> 0.13) - byebug - cucumber (~> 2.1) - json - komenda (~> 0.1.6) - landrush! - landrush-ip (~> 0.2.3) - minitest - mocha - rake (~> 10) - rubocop (~> 0.38.0) - rubydns (= 0.8.5) - vagrant! - win32-process - -BUNDLED WITH - 1.12.5 diff --git a/lib/landrush.rb b/lib/landrush.rb index be16029..7126277 100644 --- a/lib/landrush.rb +++ b/lib/landrush.rb @@ -14,6 +14,7 @@ require 'landrush/dependent_vms' require 'landrush/plugin' require 'landrush/resolver_config' +require 'landrush/win_network_config' require 'landrush/server' require 'landrush/store' require 'landrush/version' diff --git a/lib/landrush/action/setup.rb b/lib/landrush/action/setup.rb index 7cd0e62..3b4eea8 100644 --- a/lib/landrush/action/setup.rb +++ b/lib/landrush/action/setup.rb @@ -15,24 +15,35 @@ def call(env) # This is after the middleware stack returns, which, since we're right # before the Network action, should mean that all interfaces are good # to go. - record_machine_dns_entry if enabled? - setup_static_dns if enabled? + post_boot_setup if enabled? end def pre_boot_setup record_dependent_vm add_prerequisite_network_interface - setup_host_resolver configure_server start_server end + def post_boot_setup + record_machine_dns_entry + setup_static_dns + setup_host_resolver + end + def record_dependent_vm DependentVMs.add(machine_hostname) end def setup_host_resolver - ResolverConfig.new(env).ensure_config_exists! + if Vagrant::Util::Platform.windows? + #WinNetworkConfig.check_prerequistes(env) + WinNetworkConfig.update_network_adapter host_ip_address(), '127.0.0.1', 'foo' + elsif Vagrant::Util::Platform.darwin? + ResolverConfig.new(env).ensure_config_exists! + else + #TODO + end end def add_prerequisite_network_interface diff --git a/lib/landrush/registry_config.rb b/lib/landrush/registry_config.rb deleted file mode 100644 index 0637db1..0000000 --- a/lib/landrush/registry_config.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'win32/registry' - -module Landrush - class RegistryConfig - - # Windows registry path under which network interface configuration is stored - INTERFACES = 'SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces' - - # Default access type - KEY_READ = Win32::Registry::KEY_READ | 0x100 - - # KEY_ALL_ACCESS enables you to write and delete keys - # the default access is KEY_READ if you specify nothing - ALL_ACCESS = Win32::Registry::KEY_ALL_ACCESS - - def initialize(env={}) - @env = env - end - - def info(msg) - @env[:ui].info("[landrush] #{msg}") - end - - def self.update_network_adapter ip - ensure_admin_privileges __FILE__.to_s, ip - - # TODO, Need to flesh this out - Win32::Registry::HKEY_LOCAL_MACHINE.open(INTERFACES, ALL_ACCESS) do |reg| - reg.each_key do |name| - interface_path = INTERFACES + "\\#{name}" - if registry_key_exists? interface_path, 'IPAddress' - Win32::Registry::HKEY_LOCAL_MACHINE.open(interface_path, ALL_ACCESS) do |reg| - interface_ip = reg['IPAddress'] - if interface_ip[0] == ip - p "Updating interface with IP #{ip}" - reg['NameServer'] = '127.0.0.1' - # TODO, Need to become a parameter - reg['Domain'] = 'pdk.dev' - end - sleep 5 - end - end - end - end - end - - # private methods - def self.ensure_admin_privileges(file, args) - unless admin_mode? - require 'win32ole' - shell = WIN32OLE.new('Shell.Application') - shell.ShellExecute('ruby', "#{file} #{args}", nil, 'runas', 1) - exit - end - end - - def self.admin_mode? - # If this registry query succeeds we assume we have Admin rights - # http://stackoverflow.com/questions/8268154/run-ruby-script-in-elevated-mode/27954953 - (`reg query HKU\\S-1-5-19 2>&1` =~ /ERROR/).nil? - end - - def self.registry_key_exists?(path, key) - begin - Win32::Registry::HKEY_LOCAL_MACHINE.open(path, KEY_READ) { |reg| reg[key] } - rescue - false - end - end - - private_class_method :ensure_admin_privileges, :admin_mode?, :registry_key_exists? - end -end - -# Only run the following code when this file is the main file being run -# instead of having been required or loaded by another file -if __FILE__ == $0 - # TODO, Add some argument checks - Landrush::RegistryConfig.update_network_adapter ARGV[0] -end \ No newline at end of file diff --git a/lib/landrush/resolver_config.rb b/lib/landrush/resolver_config.rb index 2b3f12a..faae68f 100644 --- a/lib/landrush/resolver_config.rb +++ b/lib/landrush/resolver_config.rb @@ -29,10 +29,6 @@ def desired_contents; <<-EOS.gsub(/^ /, '') EOS end - def osx? - `uname`.chomp == 'Darwin' - end - def config_dir self.class.config_dir end @@ -46,7 +42,7 @@ def contents_match? end def write_config! - info "Momentarily using sudo to put the host config in place..." + info 'Momentarily using sudo to put the host config in place...' system "#{self.class.sudo} mkdir #{config_dir}" unless config_dir.directory? Tempfile.open('vagrant_landrush_host_config') do |f| f.write(desired_contents) @@ -58,9 +54,9 @@ def write_config! def ensure_config_exists! if contents_match? - info "Host DNS resolver config looks good." + info 'Host DNS resolver config looks good.' else - info "Need to configure the host." + info 'Need to configure the host.' write_config! end end diff --git a/lib/landrush/win_network_config.rb b/lib/landrush/win_network_config.rb new file mode 100644 index 0000000..dc09f4e --- /dev/null +++ b/lib/landrush/win_network_config.rb @@ -0,0 +1,121 @@ +require 'win32/registry' +require 'ipaddr' + +# This class configures the network interface on Windows for use with the Landrush DNS server. +# It makes use of the netsh executable which is assumed to be available on the Windows host. +module Landrush + class WinNetworkConfig + + # Windows registry path under which network interface configuration is stored + INTERFACES = 'SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces' + + # Default access type + KEY_READ = Win32::Registry::KEY_READ | 0x100 + + # KEY_ALL_ACCESS enables you to write and delete keys + # the default access is KEY_READ if you specify nothing + ALL_ACCESS = Win32::Registry::KEY_ALL_ACCESS + + def self.check_prerequistes(env) + env[:ui].info("hello") + end + + # Does the actual update of the network configuration + def self.update_network_adapter ip, name_server, domain + ensure_admin_privileges(__FILE__.to_s, ip, name_server, domain) + network_name = get_network_name(ip) + network_guid = get_guid(network_name) + interface_path = INTERFACES + "\\{#{network_guid}}" + Win32::Registry::HKEY_LOCAL_MACHINE.open(interface_path, ALL_ACCESS) do |reg| + reg['NameServer'] = name_server + reg['Domain'] = domain + end + end + + # Cross-platform way of finding an executable in the $PATH. + def which(cmd) + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each { |ext| + exe = File.join(path, "#{cmd}#{ext}") + return exe if File.executable?(exe) && !File.directory?(exe) + } + end + return nil + end + + # private methods + def self.get_guid(network_name) + # TODO here we potentially need to start the dot3svc service + # net start dot3svc - start the service + # net start dot3svc | grep Wired AutoConfig - determine whether service is started + cmd_out = `netsh lan show interfaces` + interface_details = cmd_out.split(/\n\n/).select { |settings| settings.match(/#{Regexp.quote(network_name)}/m) } + # TODO better error handling + interface_details[0].split(/\n/)[2].match(/.*:(.*)/).captures[0].strip + end + + # Given an IP determines the network name, if any. Uses netsh which generates output like this: + # + # ... + # \n\n + # Configuration for interface "Ethernet 2" + # DHCP enabled: Yes + # IP Address: 10.10.10.1 + # Subnet Prefix: 10.10.10.0/24 (mask 255.255.255.0) + # InterfaceMetric: 10 + # DNS servers configured through DHCP: None + # Register with which suffix: Primary only + # WINS servers configured through DHCP: None + # \n\n + # ... + def self.get_network_name(ip) + cmd_out = `netsh interface ip show config` + network_details = cmd_out.split(/\n\n/).select do |settings| + lines = settings.split(/\n/).reject(&:empty?) + subnet = lines[3] + next false unless subnet.match(/Subnet Prefix/) + + mask = IPAddr.new(subnet.match(/.* (\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}\/\d{1,3}).*/).captures[0]) + address = IPAddr.new(ip) + + mask.include?(address) + #settings.match(/#{Regexp.quote(ip)}/m) + end + return nil if network_details[0].nil? + network_details[0].split(/\n/)[0].match(/Configuration for interface "(.*)"/).captures[0].strip + end + + def self.ensure_admin_privileges(file, *args) + unless admin_mode? + require 'win32ole' + shell = WIN32OLE.new('Shell.Application') + shell.ShellExecute('ruby', "#{file} #{args.join(' ')}", nil, 'runas', 1) + exit + end + end + + def self.admin_mode? + # If this registry query succeeds we assume we have Admin rights + # http://stackoverflow.com/questions/8268154/run-ruby-script-in-elevated-mode/27954953 + (`reg query HKU\\S-1-5-19 2>&1` =~ /ERROR/).nil? + end + + def self.registry_key_exists?(path, key) + begin + Win32::Registry::HKEY_LOCAL_MACHINE.open(path, KEY_READ) { |reg| reg[key] } + rescue + false + end + end + + private_class_method :get_guid, :ensure_admin_privileges, :admin_mode?, :registry_key_exists? + end +end + +# Only run the following code when this file is the main file being run +# instead of having been required or loaded by another file +if __FILE__ == $0 + # TODO, Add some argument checks + Landrush::WinNetworkConfig.update_network_adapter(ARGV[0], ARGV[1], ARGV[2]) +end \ No newline at end of file diff --git a/test/landrush/resolver_config_test.rb b/test/landrush/resolver_config_test.rb index 2c4849d..1455e14 100644 --- a/test/landrush/resolver_config_test.rb +++ b/test/landrush/resolver_config_test.rb @@ -5,8 +5,6 @@ module Landrush describe 'ensure_config_exists' do it 'writes a resolver config on the host if one is not already there' do resolver_config = ResolverConfig.new(fake_environment) - skip("Only supported on OSX") unless resolver_config.osx? - resolver_config.config_file.exist?.must_equal false resolver_config.ensure_config_exists! resolver_config.config_file.exist?.must_equal true diff --git a/test/landrush/win_network_config_test.rb b/test/landrush/win_network_config_test.rb new file mode 100644 index 0000000..dae4764 --- /dev/null +++ b/test/landrush/win_network_config_test.rb @@ -0,0 +1,80 @@ +require_relative '../test_helper' + +module Landrush + TEST_IP = '10.42.42.42' + + describe WinNetworkConfig do + before do + @vboxmanage_found = !which('VBoxManage').nil? + end + + describe 'modify DNS settings of network adapter' do + it 'sets 127.0.0.1 as DNS server on the interface' do + skip('Only supported on Windows') unless Vagrant::Util::Platform.windows? && @vboxmanage_found + + # VBoxManage uses the network description for its commands whereas netsh uses the name + # We need to get both + begin + old_network_state = network_state + network_description = create_test_interface + new_network_state = network_state + network_name = get_network_name(old_network_state, new_network_state) + + get_dns_for_name(network_name).must_be_nil + + Landrush::WinNetworkConfig.update_network_adapter(TEST_IP, '127.0.0.1', 'landrush.test') + + get_dns_for_name(network_name).must_equal '127.0.0.1' + ensure + delete_test_interface network_description + end + end + end + + describe 'Prerequisites' do + it 'checks that all needed tools are installed' do + skip('Only supported on Windows') unless Vagrant::Util::Platform.windows? + Landrush::WinNetworkConfig.check_prerequistes(fake_environment) + end + end + + def network_state + `netsh interface ip show config`.split(/\n/).reject(&:empty?) + end + + def get_network_name(old_network_state, new_network_state) + new_network_state.reject! { |line| old_network_state.include? line } + new_network_state[0].match(/.*\"(.*)\"$/).captures[0] + end + + # Creates a test interface using VBoxMange and sets a known test IP + def create_test_interface + cmd_out = `VBoxManage hostonlyif create` + network_description = cmd_out.match(/.*'(.*)'.*/).captures[0] + `VBoxManage.exe hostonlyif ipconfig \"#{network_description}\" --ip #{TEST_IP}` + sleep 3 + return network_description + end + + def delete_test_interface(name) + `VBoxManage hostonlyif remove \"#{name}\"` + end + + # def get_ip_for_name(name) + # # netsh interface ip show config name="VirtualBox Host-Only Network #3" + # cmd_out = `netsh interface ip show ipaddresses interface=\"#{name}\"` + # cmd_out.match(/Address (\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}) Parameters/).captures[0] + # end + + def get_dns_for_name(name) + cmd_out = `netsh interface ip show config name=\"#{name}\"` + dns = cmd_out.split(/\n/).select { |settings| settings.match(/Statically Configured DNS Servers/m) } + # TODO better error handling + begin + dns[0].match(/.* (\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}).*/).captures[0] + rescue + return nil + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8d7dc43..1813a23 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,6 +7,7 @@ require 'landrush/cap/linux/configured_dns_servers' require 'landrush/cap/linux/redirect_dns' require 'landrush/cap/all/read_host_visible_ip_address' +require 'landrush/win_network_config' require 'minitest/autorun' require 'mocha/mini_test' @@ -121,6 +122,18 @@ def fake_machine(options={}) machine end +# Cross-platform way of finding an executable in the $PATH. +def which(cmd) + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each { |ext| + exe = File.join(path, "#{cmd}#{ext}") + return exe if File.executable?(exe) && !File.directory?(exe) + } + end + return nil +end + def fake_static_entry(env, hostname, ip) env[:machine].config.landrush.host(hostname, ip) Landrush::Store.hosts.set(hostname, ip)