diff --git a/lib/vagrant/actions/vm/network.rb b/lib/vagrant/actions/vm/network.rb new file mode 100644 index 000000000..252998f59 --- /dev/null +++ b/lib/vagrant/actions/vm/network.rb @@ -0,0 +1,86 @@ +module Vagrant + module Actions + module VM + class Network < Base + def before_boot + assign_network if enable_network? + end + + def after_boot + + end + + def enable_network? + !runner.env.config.vm.network_options.nil? + end + + # Enables and assigns the host only network to the proper + # adapter on the VM, and saves the adapter. + def assign_network + logger.info "Enabling host only network..." + + options = runner.env.config.vm.network_options + adapter = runner.vm.network_adapters[options[:adapter]] + adapter.enabled = true + adapter.attachment_type = :host_only + adapter.host_interface = network_name(options) + adapter.save + 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(options) + # First try to find a matching network + interfaces = VirtualBox::Global.global.host.network_interfaces + interfaces.each do |ni| + return ni.name if matching_network?(ni, options) + end + + # One doesn't exist, create it. + logger.info "Creating new host only network for environment..." + + ni = interfaces.create + ni.enable_static(network_ip(options[:ip], options[:netmask]), + 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, options) + interface.network_mask == options[:netmask] && + apply_netmask(interface.ip_address, interface.network_mask) == + apply_netmask(options[:ip], 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/lib/vagrant/actions/vm/start.rb b/lib/vagrant/actions/vm/start.rb index 5553504a9..069f1d0b3 100644 --- a/lib/vagrant/actions/vm/start.rb +++ b/lib/vagrant/actions/vm/start.rb @@ -7,7 +7,7 @@ module Vagrant # of other actions in its place: steps = [Boot] if !@runner.vm || !@runner.vm.saved? - steps.unshift([Customize, ForwardPorts, SharedFolders]) + steps.unshift([Customize, ForwardPorts, SharedFolders, Network]) steps << Provision if provision? end diff --git a/lib/vagrant/config.rb b/lib/vagrant/config.rb index 64c403c4d..30702839d 100644 --- a/lib/vagrant/config.rb +++ b/lib/vagrant/config.rb @@ -90,6 +90,7 @@ module Vagrant attr_reader :rsync_required attr_reader :forwarded_ports attr_reader :shared_folders + attr_reader :network_options attr_accessor :hd_location attr_accessor :disk_image_format attr_accessor :provisioner @@ -101,6 +102,7 @@ module Vagrant @forwarded_ports = {} @shared_folders = {} @provisioner = nil + @network_options = nil end def forward_port(name, guestport, hostport, options=nil) @@ -130,6 +132,16 @@ module Vagrant } end + def network(ip, options=nil) + options = { + :ip => ip, + :netmask => "255.255.255.0", + :name => nil + }.merge(options || {}) + + @network_options = options + end + def hd_location=(val) raise Exception.new("disk_storage must be set to a directory") unless File.directory?(val) @hd_location=val diff --git a/test/vagrant/actions/vm/network_test.rb b/test/vagrant/actions/vm/network_test.rb new file mode 100644 index 000000000..2e3a5d622 --- /dev/null +++ b/test/vagrant/actions/vm/network_test.rb @@ -0,0 +1,168 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') + +class NetworkTest < Test::Unit::TestCase + setup do + @runner, @vm, @action = mock_action(Vagrant::Actions::VM::Network) + @runner.stubs(:system).returns(linux_system(@vm)) + end + + context "before boot" do + setup do + @action.stubs(:enable_network?).returns(false) + end + + should "do nothing if network should not be enabled" do + @action.expects(:assign_network).never + @action.before_boot + end + + should "assign the network if host only networking is enabled" do + @action.stubs(:enable_network?).returns(true) + @action.expects(:assign_network).once + @action.before_boot + end + end + + context "checking if network is enabled" do + should "return true if the network options are set" do + @runner.env.config.vm.network("foo") + assert @action.enable_network? + end + + should "return false if the network was not set" do + assert !@action.enable_network? + end + end + + context "assigning the network" do + setup do + @network_name = "foo" + @action.stubs(:network_name).returns(@network_name) + + @network_adapters = [] + @vm.stubs(:network_adapters).returns(@network_adapters) + + @options = { + :ip => "foo", + :adapter => 7 + } + + @runner.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 + + @action.assign_network + end + end + + context "network name" do + setup do + @interfaces = [] + VirtualBox::Global.global.host.stubs(:network_interfaces).returns(@interfaces) + + @action.stubs(:matching_network?).returns(false) + + @options = { :ip => :foo, :netmask => :bar } + end + + should "return the network which matches" do + result = mock("result") + interface = mock("interface") + interface.stubs(:name).returns(result) + @interfaces << interface + + @action.expects(:matching_network?).with(interface, @options).returns(true) + assert_equal result, @action.network_name(@options) + end + + should "create a network for the IP and netmask" do + result = mock("result") + interface = mock("interface") + network_ip = :foo + @interfaces.expects(:create).returns(interface) + @action.expects(:network_ip).with(@options[:ip], @options[:netmask]).once.returns(network_ip) + interface.expects(:enable_static).with(network_ip, @options[:netmask]) + interface.expects(:name).returns(result) + + assert_equal result, @action.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 !@action.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 @action.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, @action.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, @action.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, @action.network_ip(*args) + end + end + end +end diff --git a/test/vagrant/actions/vm/start_test.rb b/test/vagrant/actions/vm/start_test.rb index 426c4953e..0846ae7d6 100644 --- a/test/vagrant/actions/vm/start_test.rb +++ b/test/vagrant/actions/vm/start_test.rb @@ -30,14 +30,14 @@ class StartActionTest < Test::Unit::TestCase should "add customize to the beginning if its not saved" do @vm.expects(:saved?).returns(false) - @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders]) + @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders, Vagrant::Actions::VM::Network]) setup_action_expectations @action.prepare end should "add do additional if VM is not created yet" do @runner.stubs(:vm).returns(nil) - @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders]) + @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders, Vagrant::Actions::VM::Network]) setup_action_expectations @action.prepare end @@ -46,7 +46,7 @@ class StartActionTest < Test::Unit::TestCase @vm.env.config.vm.provisioner = :chef_solo @runner.stubs(:vm).returns(nil) - @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders]) + @default_order.unshift([Vagrant::Actions::VM::Customize, Vagrant::Actions::VM::ForwardPorts, Vagrant::Actions::VM::SharedFolders, Vagrant::Actions::VM::Network]) @default_order << Vagrant::Actions::VM::Provision setup_action_expectations @action.prepare