diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb index 02c7d2095..f3536edcf 100644 --- a/lib/vagrant/action/builtin.rb +++ b/lib/vagrant/action/builtin.rb @@ -13,6 +13,7 @@ module Vagrant use VM::Customize use VM::ForwardPorts use VM::ShareFolders + use VM::Network use VM::Boot end diff --git a/lib/vagrant/action/vm/network.rb b/lib/vagrant/action/vm/network.rb new file mode 100644 index 000000000..3c3c184e9 --- /dev/null +++ b/lib/vagrant/action/vm/network.rb @@ -0,0 +1,137 @@ +module Vagrant + class Action + module VM + # Networking middleware for Vagrant. This enables host only + # networking on VMs if configured as such. + class Network + def initialize(app, env) + @app = app + @env = env + + env["config"].vm.network_options.compact.each do |network_options| + if !verify_no_bridge_collision(network_options) + env.error!(:network_collides) + return + end + end + end + + def call(env) + @env = env + assign_network if enable_network? + + @app.call(env) + + if !env.error? && enable_network? + @env.logger.info "Enabling host only network..." + @env["vm"].system.prepare_host_only_network + @env.env.config.vm.network_options.compact.each do |network_options| + @env["vm"].system.enable_host_only_network(network_options) + end + end + end + + # Verifies that there is no collision with a bridged network interface + # for the given network options. + def verify_no_bridge_collision(net_options) + interfaces = VirtualBox::Global.global.host.network_interfaces + interfaces.each do |ni| + next if ni.interface_type == :host_only + + result = if net_options[:name] + true if net_options[:name] == ni.name + else + true if matching_network?(ni, net_options) + end + + return false if result + end + + true + end + + def enable_network? + !@env.env.config.vm.network_options.compact.empty? + end + + # Enables and assigns the host only network to the proper + # adapter on the VM, and saves the adapter. + def assign_network + @env.logger.info "Preparing host only network..." + + @env.env.config.vm.network_options.compact.each do |network_options| + adapter = @env["vm"].vm.network_adapters[network_options[:adapter]] + adapter.enabled = true + adapter.attachment_type = :host_only + adapter.host_interface = network_name(network_options) + adapter.save + end + end + + # Returns the name of the proper host only network, or creates + # it if it does not exist. Vagrant determines if the host only + # network exists by comparing the netmask and the IP. + def network_name(net_options) + # First try to find a matching network + interfaces = VirtualBox::Global.global.host.network_interfaces + interfaces.each do |ni| + # Ignore non-host only interfaces which may also match, + # since they're not valid options. + next if ni.interface_type != :host_only + + if net_options[:name] + return ni.name if net_options[:name] == ni.name + else + return ni.name if matching_network?(ni, net_options) + end + end + + return @env.error!(:network_not_found, :name => net_options[:name]) if net_options[:name] + + # One doesn't exist, create it. + @env.logger.info "Creating new host only network for environment..." + + ni = interfaces.create + ni.enable_static(network_ip(net_options[:ip], net_options[:netmask]), + net_options[:netmask]) + ni.name + end + + # Tests if a network matches the given options by applying the + # netmask to the IP of the network and also to the IP of the + # virtual machine and see if they match. + def matching_network?(interface, net_options) + interface.network_mask == net_options[:netmask] && + apply_netmask(interface.ip_address, interface.network_mask) == + apply_netmask(net_options[:ip], net_options[:netmask]) + end + + # Applies a netmask to an IP and returns the corresponding + # parts. + def apply_netmask(ip, netmask) + ip = split_ip(ip) + netmask = split_ip(netmask) + + ip.map do |part| + part & netmask.shift + end + end + + # Splits an IP and converts each portion into an int. + def split_ip(ip) + ip.split(".").map do |i| + i.to_i + end + end + + # Returns a "network IP" which is a "good choice" for the IP + # for the actual network based on the netmask. + def network_ip(ip, netmask) + parts = apply_netmask(ip, netmask) + parts[3] += 1; + parts.join(".") + end + end + end + end +end diff --git a/test/vagrant/action/vm/network_test.rb b/test/vagrant/action/vm/network_test.rb new file mode 100644 index 000000000..9769e078c --- /dev/null +++ b/test/vagrant/action/vm/network_test.rb @@ -0,0 +1,246 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') + +class NetworkVMActionTest < Test::Unit::TestCase + setup do + @klass = Vagrant::Action::VM::Network + @app, @env = mock_action_data + + @vm = mock("vm") + @vm.stubs(:name).returns("foo") + @vm.stubs(:ssh).returns(mock("ssh")) + @vm.stubs(:system).returns(mock("system")) + @env["vm"] = @vm + + @internal_vm = mock("internal") + @vm.stubs(:vm).returns(@internal_vm) + end + + context "initializing" do + should "verify no bridge collisions for each network enabled" do + @env.env.config.vm.network("foo") + @klass.any_instance.expects(:verify_no_bridge_collision).once.with() do |options| + assert_equal "foo", options[:ip] + true + end + + @klass.new(@app, @env) + end + end + + context "with an instance" do + setup do + @klass.any_instance.stubs(:verify_no_bridge_collision) + @instance = @klass.new(@app, @env) + + @interfaces = [] + VirtualBox::Global.global.host.stubs(:network_interfaces).returns(@interfaces) + end + + def mock_interface(options=nil) + options = { + :interface_type => :host_only, + :name => "foo" + }.merge(options || {}) + + interface = mock("interface") + options.each do |k,v| + interface.stubs(k).returns(v) + end + + @interfaces << interface + interface + end + + context "calling" do + setup do + @env.env.config.vm.network("foo") + @instance.stubs(:enable_network?).returns(false) + end + + should "do nothing if network should not be enabled" do + @instance.expects(:assign_network).never + @app.expects(:call).with(@env).once + @vm.system.expects(:prepare_host_only_network).never + @vm.system.expects(:enable_host_only_network).never + + @instance.call(@env) + end + + should "assign and enable the network if networking enabled" do + @instance.stubs(:enable_network?).returns(true) + + run_seq = sequence("run") + @instance.expects(:assign_network).once.in_sequence(run_seq) + @app.expects(:call).with(@env).once.in_sequence(run_seq) + @vm.system.expects(:prepare_host_only_network).once.in_sequence(run_seq) + @vm.system.expects(:enable_host_only_network).once.in_sequence(run_seq) + + @instance.call(@env) + end + end + + context "checking if network is enabled" do + should "return true if the network options are set" do + @env.env.config.vm.network("foo") + assert @instance.enable_network? + end + + should "return false if the network was not set" do + assert !@instance.enable_network? + end + end + + context "assigning the network" do + setup do + @network_name = "foo" + @instance.stubs(:network_name).returns(@network_name) + + @network_adapters = [] + @internal_vm.stubs(:network_adapters).returns(@network_adapters) + + @options = { + :ip => "foo", + :adapter => 7 + } + + @env.env.config.vm.network(@options[:ip], @options) + end + + should "setup the specified network adapter" do + adapter = mock("adapter") + @network_adapters[@options[:adapter]] = adapter + + adapter.expects(:enabled=).with(true).once + adapter.expects(:attachment_type=).with(:host_only).once + adapter.expects(:host_interface=).with(@network_name).once + adapter.expects(:save).once + + @instance.assign_network + end + end + + context "network name" do + setup do + @instance.stubs(:matching_network?).returns(false) + + @options = { :ip => :foo, :netmask => :bar, :name => nil } + end + + should "return the network which matches" do + result = mock("result") + interface = mock_interface(:name => result) + + @instance.expects(:matching_network?).with(interface, @options).returns(true) + assert_equal result, @instance.network_name(@options) + end + + should "ignore non-host only interfaces" do + @options[:name] = "foo" + mock_interface(:name => @options[:name], + :interface_type => :bridged) + + @instance.network_name(@options) + assert @env.error? + end + + should "return the network which matches the name if given" do + @options[:name] = "foo" + + interface = mock_interface(:name => @options[:name]) + assert_equal @options[:name], @instance.network_name(@options) + end + + should "error and exit if the given network name is not found" do + @options[:name] = "foo" + + @interfaces.expects(:create).never + @instance.network_name(@options) + assert @env.error? + assert_equal :network_not_found, @env.error.first + end + + should "create a network for the IP and netmask" do + result = mock("result") + network_ip = :foo + + interface = mock_interface(:name => result) + interface.expects(:enable_static).with(network_ip, @options[:netmask]) + @interfaces.expects(:create).returns(interface) + @instance.expects(:network_ip).with(@options[:ip], @options[:netmask]).once.returns(network_ip) + + assert_equal result, @instance.network_name(@options) + end + end + + context "checking for a matching network" do + setup do + @interface = mock("interface") + @interface.stubs(:network_mask).returns("foo") + @interface.stubs(:ip_address).returns("192.168.0.1") + + @options = { + :netmask => "foo", + :ip => "baz" + } + end + + should "return false if the netmasks don't match" do + @options[:netmask] = "bar" + assert @interface.network_mask != @options[:netmask] # sanity + assert !@instance.matching_network?(@interface, @options) + end + + should "return true if the netmasks yield the same IP" do + tests = [["255.255.255.0", "192.168.0.1", "192.168.0.45"], + ["255.255.0.0", "192.168.45.1", "192.168.28.7"]] + + tests.each do |netmask, interface_ip, guest_ip| + @options[:netmask] = netmask + @options[:ip] = guest_ip + @interface.stubs(:network_mask).returns(netmask) + @interface.stubs(:ip_address).returns(interface_ip) + + assert @instance.matching_network?(@interface, @options) + end + end + end + + context "applying the netmask" do + should "return the proper result" do + tests = { + ["192.168.0.1","255.255.255.0"] => [192,168,0,0], + ["192.168.45.10","255.255.255.0"] => [192,168,45,0] + } + + tests.each do |k,v| + assert_equal v, @instance.apply_netmask(*k) + end + end + end + + context "splitting an IP" do + should "return the proper result" do + tests = { + "192.168.0.1" => [192,168,0,1] + } + + tests.each do |k,v| + assert_equal v, @instance.split_ip(k) + end + end + end + + context "network IP" do + should "return the proper result" do + tests = { + ["192.168.0.45", "255.255.255.0"] => "192.168.0.1" + } + + tests.each do |args, result| + assert_equal result, @instance.network_ip(*args) + end + end + end + + end +end