diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 3799669e2..265ccbe2b 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -26,7 +26,6 @@ module Vagrant module VM autoload :Boot, 'vagrant/action/vm/boot' - autoload :BridgedNetwork, 'vagrant/action/vm/bridged_network' autoload :CheckAccessible, 'vagrant/action/vm/check_accessible' autoload :CheckBox, 'vagrant/action/vm/check_box' autoload :CheckGuestAdditions, 'vagrant/action/vm/check_guest_additions' @@ -46,7 +45,7 @@ module Vagrant autoload :HostName, 'vagrant/action/vm/host_name' autoload :Import, 'vagrant/action/vm/import' autoload :MatchMACAddress, 'vagrant/action/vm/match_mac_address' - autoload :HostOnlyNetwork, 'vagrant/action/vm/host_only_network' + autoload :Network, 'vagrant/action/vm/network' autoload :NFS, 'vagrant/action/vm/nfs' autoload :Package, 'vagrant/action/vm/package' autoload :PackageVagrantfile, 'vagrant/action/vm/package_vagrantfile' diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb index 04dd36fa2..6ae1a9f1b 100644 --- a/lib/vagrant/action/builtin.rb +++ b/lib/vagrant/action/builtin.rb @@ -30,8 +30,7 @@ module Vagrant use VM::ShareFolders use VM::HostName use VM::ClearNetworkInterfaces - use VM::BridgedNetwork - use VM::HostOnlyNetwork + use VM::Network use VM::Customize use VM::Boot end diff --git a/lib/vagrant/action/vm/forward_ports.rb b/lib/vagrant/action/vm/forward_ports.rb index 1e9bb6da2..b996f6c32 100644 --- a/lib/vagrant/action/vm/forward_ports.rb +++ b/lib/vagrant/action/vm/forward_ports.rb @@ -55,7 +55,7 @@ module Vagrant # Port forwarding requires the network interface to be a NAT interface, # so verify that that is the case. - if interfaces[adapter][:type] != "nat" + if interfaces[adapter][:type] != :nat @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.non_nat", message_attributes)) next diff --git a/lib/vagrant/action/vm/network.rb b/lib/vagrant/action/vm/network.rb new file mode 100644 index 000000000..0a6a0a1a2 --- /dev/null +++ b/lib/vagrant/action/vm/network.rb @@ -0,0 +1,284 @@ +require 'set' + +require 'log4r' + +require 'vagrant/util/network_ip' + +module Vagrant + module Action + module VM + # This action handles all `config.vm.network` configurations by + # setting up the VM properly and enabling the networks afterword. + class Network + # Utilities to deal with network addresses + include Util::NetworkIP + + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::network") + + @app = app + end + + def call(env) + @env = env + + # First we have to get the array of adapters that we need + # to create on the virtual machine itself, as well as the + # driver-agnostic network configurations for each. + @logger.debug("Determining adapters and networks...") + adapters = [] + networks = [] + env[:vm].config.vm.networks.each do |type, args| + # Get the normalized configuration we'll use around + config = send("#{type}_config", args) + + # Get the virtualbox adapter configuration + adapter = send("#{type}_adapter", config) + adapters << adapter + + # Get the network configuration + network = send("#{type}_network_config", config) + networks << network + end + + # Automatically assign an adapter number to any adapters + # that aren't explicitly set. + @logger.debug("Assigning adapter locations...") + assign_adapter_locations(adapters) + + # Create all the network interfaces + @logger.info("Enabling adapters...") + env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") + env[:vm].driver.enable_adapters(adapters) + + # Continue the middleware chain. We're done with our VM + # setup until after it is booted. + @app.call(env) + + # Determine the interface numbers for the guest. + assign_interface_numbers(networks, adapters) + + # Configure all the network interfaces on the guest. + env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") + env[:vm].guest.configure_networks(networks) + end + + # This method assigns the adapter to use for the adapter. + # e.g. it says that the first adapter is actually on the + # virtual machine's 2nd adapter location. + # + # It determines the adapter numbers by simply finding the + # "next available" in each case. + # + # The adapters are modified in place by adding an ":adapter" + # field to each. + def assign_adapter_locations(adapters) + available = Set.new(1..8) + + # Determine which NICs are actually available. + interfaces = @env[:vm].driver.read_network_interfaces + interfaces.each do |number, nic| + # Remove the number from the available NICs if the + # NIC is in use. + available.delete(number) if nic[:type] != :none + end + + # Based on the available set, assign in order to + # the adapters. + available = available.to_a.sort + @logger.debug("Available NICs: #{available.inspect}") + adapters.each do |adapter| + # Ignore the adapters that already have been assigned + if !adapter[:adapter] + adapter[:adapter] = available.shift + end + end + end + + # Assigns the actual interface number of a network based on the + # enabled NICs on the virtual machine. + # + # This interface number is used by the guest to configure the + # NIC on the guest VM. + # + # The networks are modified in place by adding an ":interface" + # field to each. + def assign_interface_numbers(networks, adapters) + current = 0 + adapter_to_interface = {} + + # Make a first pass to assign interface numbers by adapter location + vm_adapters = @env[:vm].driver.read_network_interfaces + vm_adapters.each do |number, adapter| + if adapter[:type] != :none + # Not used, so assign the interface number and increment + adapter_to_interface[number] = current + current += 1 + end + end + + # Make a pass through the adapters to assign the :interface + # key to each network configuration. + adapters.each_index do |i| + adapter = adapters[i] + network = networks[i] + + # Figure out the interface number by simple lookup + network[:interface] = adapter_to_interface[adapter[:adapter]] + end + end + + def hostonly_config(args) + ip = args[0] + options = args[1] || {} + + options = { + :ip => ip, + :netmask => "255.255.255.0", + :adapter => nil, + :mac => nil, + :name => nil + }.merge(options) + + # Verify that this hostonly network wouldn't conflict with any + # bridged interfaces + verify_no_bridge_collision(options) + + # Return the hostonly network configuration + return options + end + + def hostonly_adapter(config) + @logger.debug("Searching for matching network: #{config[:ip]}") + interface = find_matching_hostonly_network(config) + + if !interface + @logger.debug("Network not found. Creating if we can.") + + # It is an error case if a specific name was given but the network + # doesn't exist. + if config[:name] + raise Errors::NetworkNotFound, :name => config[:name] + end + + # Otherwise, we create a new network and put the net network + # in the list of available networks so other network definitions + # can use it! + interface = create_hostonly_network(config) + @logger.debug("Created network: #{interface[:name]}") + end + + return { + :adapter => config[:adapter], + :type => :hostonly, + :hostonly => interface[:name], + :mac_address => config[:mac] + } + end + + def hostonly_network_config(config) + return { + :type => :static, + :ip => config[:ip], + :netmask => config[:netmask] + } + end + + # Creates a new hostonly network that matches the network requested + # by the given host-only network configuration. + def create_hostonly_network(config) + # First we need to determine a good IP for the host machine + # of this interface. We choose to use the network address + # plus 1, which is usually what is expected. + netaddr = network_address(config[:ip], config[:netmask]) + parts = netaddr.split(".").map { |i| i.to_i } + parts[3] += 1 + ip = parts.join(".") + + # Create the options that are going to be used to create our + # new network. + options = config.dup + options[:ip] = ip + + @env[:vm].driver.create_host_only_network(options) + end + + # Finds a host only network that matches our configuration on VirtualBox. + # This will return nil if a matching network does not exist. + def find_matching_hostonly_network(config) + this_netaddr = network_address(config[:ip], config[:netmask]) + + @env[:vm].driver.read_host_only_interfaces.each do |interface| + if config[:name] && config[:name] == interface[:name] + return interface + elsif this_netaddr == network_address(interface[:ip], interface[:netmask]) + return interface + end + end + + nil + end + + # Verifies that a host-only network subnet would not collide with + # a bridged networking interface. + # + # If the subnets overlap in any way then the host only network + # will not work because the routing tables will force the traffic + # onto the real interface rather than the virtualbox interface. + def verify_no_bridge_collision(options) + this_netaddr = network_address(options[:ip], options[:netmask]) + + @env[:vm].driver.read_bridged_interfaces.each do |interface| + that_netaddr = network_address(interface[:ip], interface[:netmask]) + raise Errors::NetworkCollision if this_netaddr == that_netaddr + end + end + + def bridged_config(args) + options = args[0] || {} + + return { + :adapter => nil, + :mac => nil + }.merge(options) + end + + def bridged_adapter(config) + bridgedifs = @env[:vm].driver.read_bridged_interfaces + + # Output all the interfaces that are available as choices + @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.available", + :prefix => false) + bridgedifs.each_index do |index| + interface = bridgedifs[index] + @env[:ui].info("#{index + 1}) #{interface[:name]}", :prefix => false) + end + + # The range of valid choices + valid = Range.new(1, bridgedifs.length) + + # The choice that the user has chosen as the bridging interface + choice = nil + while !valid.include?(choice) + choice = @env[:ui].ask("What interface should the network bridge to? ") + choice = choice.to_i + end + + # Given the choice we can now define the adapter we're using + return { + :adapter => config[:adapter], + :type => :bridged, + :bridge => bridgedifs[choice - 1][:name], + :mac_address => config[:mac] + } + end + + def bridged_network_config(config) + return { + :type => :dhcp + } + end + end + end + end +end diff --git a/lib/vagrant/driver/virtualbox.rb b/lib/vagrant/driver/virtualbox.rb index d38be92b9..249862102 100644 --- a/lib/vagrant/driver/virtualbox.rb +++ b/lib/vagrant/driver/virtualbox.rb @@ -340,7 +340,7 @@ module Vagrant execute("showvminfo", @uuid, "--machinereadable").split("\n").each do |line| if line =~ /^nic(\d+)="(.+?)"$/ adapter = $1.to_i - type = $2.to_s + type = $2.to_sym nics[adapter] ||= {} nics[adapter][:type] = type diff --git a/lib/vagrant/guest/base.rb b/lib/vagrant/guest/base.rb index 974ce064e..aace3becb 100644 --- a/lib/vagrant/guest/base.rb +++ b/lib/vagrant/guest/base.rb @@ -66,36 +66,22 @@ module Vagrant # via the host are already done. def mount_nfs(ip, folders); end - # Prepares the system for host only networks. This is called - # once prior to any `enable_host_only_network` calls. - def prepare_host_only_network(net_options=nil) - raise BaseError, :_key => :unsupported_host_only - end - - # Setup the system by adding a new host only network. This - # method should configure and bring up the interface for the - # given options. + # Configures the given list of networks on the virtual machine. # - # @param [Hash] net_options The options for the network. - def enable_host_only_network(net_options); end - - # Prepares the guest for bridged networks. + # The networks parameter will be an array of hashes where the hashes + # represent the configuration of a network interface. The structure + # of the hash will be roughly the following: # - # @param [Array] networks Array of networks to prepare for. - def prepare_bridged_networks(networks) - raise BaseError, :_key => :unsupported_host_only - end - - # Enable bridged networks on a guest. + # { + # :type => :static, + # :ip => "33.33.33.10", + # :netmask => "255.255.255.0", + # :interface => 1 + # } # - # @param [Array] networks Array of networks to prepare for. - def enable_bridged_networks(networks) - raise BaseError, :_key => :unsupported_bridged - end + def configure_networks(networks); end - def change_host_name(name) - raise BaseError, :_key => :unsupported_bridged - end + def change_host_name(name); end end end end diff --git a/lib/vagrant/guest/debian.rb b/lib/vagrant/guest/debian.rb index da6bb7988..54be9e6d4 100644 --- a/lib/vagrant/guest/debian.rb +++ b/lib/vagrant/guest/debian.rb @@ -1,49 +1,49 @@ +require 'set' + +require 'vagrant/util/template_renderer' + module Vagrant module Guest class Debian < Linux - def prepare_host_only_network(net_options=nil) - # Remove any previous host only network additions to the interface file. + # Make the TemplateRenderer top-level + include Vagrant::Util + + def configure_networks(networks) + # First, remove any previous network modifications + # from the interface file. vm.ssh.execute do |ssh| - ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN-HOSTONLY/,/^#VAGRANT-END-HOSTONLY/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces") + ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces") ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-interfaces > /etc/network/interfaces'") end - end - def enable_host_only_network(net_options) - entry = TemplateRenderer.render('guests/debian/network_hostonly', - :net_options => net_options) - vm.ssh.upload!(StringIO.new(entry), "/tmp/vagrant-network-entry") - - vm.ssh.execute do |ssh| - ssh.exec!("sudo /sbin/ifdown eth#{net_options[:adapter]} 2> /dev/null") - ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-entry >> /etc/network/interfaces'") - ssh.exec!("sudo /sbin/ifup eth#{net_options[:adapter]}") + # 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]) + entries << TemplateRenderer.render("guests/debian/network_#{network[:type]}", + :options => network) end - end - def prepare_bridged_networks(networks) - # Remove any previous bridged network additions to the interface file. - vm.ssh.execute do |ssh| - ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN-BRIDGED/,/^#VAGRANT-END-BRIDGED/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces") - ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-interfaces > /etc/network/interfaces'") - end - end - - def enable_bridged_networks(networks) - entry = TemplateRenderer.render('guests/debian/network_bridged', - :networks => networks) - - vm.ssh.upload!(StringIO.new(entry), "/tmp/vagrant-network-entry") + # Perform the careful dance necessary to to reconfigure + # the network interfaces + vm.ssh.upload!(StringIO.new(entries.join("\n")), "/tmp/vagrant-network-entry") vm.ssh.execute do |ssh| - networks.each do |network| - ssh.exec!("sudo /sbin/ifdown eth#{network[:adapter]} 2> /dev/null") + # 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| + ssh.exec!("sudo /sbin/ifdown eth#{interface} 2> /dev/null") end ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-entry >> /etc/network/interfaces'") - networks.each do |network| - ssh.exec!("sudo /sbin/ifup eth#{network[:adapter]}") + # Bring back up each network interface, reconfigured + interfaces.each do |interface| + ssh.exec!("sudo /sbin/ifup eth#{interface}") end end end diff --git a/lib/vagrant/util/network_ip.rb b/lib/vagrant/util/network_ip.rb new file mode 100644 index 000000000..fca98f8b4 --- /dev/null +++ b/lib/vagrant/util/network_ip.rb @@ -0,0 +1,28 @@ +module Vagrant + module Util + module NetworkIP + # Returns the network address of the given IP and subnet. + # + # @return [String] + def network_address(ip, subnet) + ip = ip_parts(ip) + netmask = ip_parts(subnet) + + # Bitwise-AND each octet to get the network address + # in octets and join each part with a period to get + # the resulting network address. + ip.map { |part| part & netmask.shift }.join(".") + end + + protected + + # Splits an IP into the four octets and returns each as an + # integer in an array. + # + # @return [Array] + def ip_parts(ip) + ip.split(".").map { |i| i.to_i } + end + end + end +end diff --git a/templates/guests/debian/network_bridged.erb b/templates/guests/debian/network_bridged.erb deleted file mode 100644 index c9342b9b6..000000000 --- a/templates/guests/debian/network_bridged.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% networks.each do |network| -%> -#VAGRANT-BEGIN-BRIDGED -# The contents below are automatically generated by Vagrant. Do not modify. -auto eth<%= network[:adapter] %> -iface eth<%= network[:adapter] %> inet dhcp -#VAGRANT-END-BRIDGED -<% end -%> \ No newline at end of file diff --git a/templates/guests/debian/network_dhcp.erb b/templates/guests/debian/network_dhcp.erb new file mode 100644 index 000000000..1bdc3e1ef --- /dev/null +++ b/templates/guests/debian/network_dhcp.erb @@ -0,0 +1,5 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +auto eth<%= options[:interface] %> +iface eth<%= options[:interface] %> inet dhcp +#VAGRANT-END diff --git a/templates/guests/debian/network_hostonly.erb b/templates/guests/debian/network_hostonly.erb deleted file mode 100644 index 61d2653f3..000000000 --- a/templates/guests/debian/network_hostonly.erb +++ /dev/null @@ -1,8 +0,0 @@ -#VAGRANT-BEGIN-HOSTONLY -# The contents below are automatically generated by Vagrant. -# Please do not modify any of these contents. -auto eth<%= net_options[:adapter] %> -iface eth<%= net_options[:adapter] %> inet static - address <%= net_options[:ip] %> - netmask <%= net_options[:netmask] %> -#VAGRANT-END-HOSTONLY diff --git a/templates/guests/debian/network_static.erb b/templates/guests/debian/network_static.erb new file mode 100644 index 000000000..fa505afa9 --- /dev/null +++ b/templates/guests/debian/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/templates/locales/en.yml b/templates/locales/en.yml index fd2c4a9f4..4838b62b1 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -394,6 +394,11 @@ en: to work properly (and hence port forwarding, SSH, etc.). Specifying this MAC address is typically up to the box and box maintiner. Please contact the relevant person to solve this issue. + network: + preparing: |- + Preparing network interfaces based on configuration... + configuring: |- + Configuring and enabling network interfaces... host_only_network: collides: |- The specified host network collides with a non-hostonly network! diff --git a/test/unit/vagrant/util/network_ip_test.rb b/test/unit/vagrant/util/network_ip_test.rb new file mode 100644 index 000000000..0100f6f78 --- /dev/null +++ b/test/unit/vagrant/util/network_ip_test.rb @@ -0,0 +1,17 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/network_ip" + +describe Vagrant::Util::NetworkIP do + let(:klass) do + Class.new do + extend Vagrant::Util::NetworkIP + end + end + + describe "network address" do + it "calculates it properly" do + klass.network_address("192.168.2.234", "255.255.255.0").should == "192.168.2.0" + end + end +end