lib/vagrant/action/vm/network.rb in vagrantup-0.8.10 vs lib/vagrant/action/vm/network.rb in vagrantup-0.9.0

- old
+ new

@@ -1,145 +1,351 @@ +require 'set' + +require 'log4r' + +require 'vagrant/util/network_ip' + module Vagrant - class Action + module Action module VM - # Networking middleware for Vagrant. This enables host only - # networking on VMs if configured as such. + # 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 - if enable_network? && Util::Platform.windows? && Util::Platform.bit64? - raise Errors::NetworkNotImplemented + # 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 - env["config"].vm.network_options.compact.each do |network_options| - raise Errors::NetworkCollision if !verify_no_bridge_collision(network_options) + if !adapters.empty? + # Automatically assign an adapter number to any adapters + # that aren't explicitly set. + @logger.debug("Assigning adapter locations...") + assign_adapter_locations(adapters) + + # Verify that our adapters are good just prior to enabling them. + verify_adapters(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) end + + # Continue the middleware chain. We're done with our VM + # setup until after it is booted. + @app.call(env) + + if !adapters.empty? + # 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 end - def call(env) - @env = env - assign_network if enable_network? + # 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) - @app.call(env) + # 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 - if enable_network? - @env.ui.info I18n.t("vagrant.actions.vm.network.enabling") + # 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] + # If we have no available adapters, then that is an exceptional + # event. + raise Errors::NetworkNoAdapters if available.empty? - # Prepare for new networks... - options = @env.env.config.vm.network_options.compact - options.each do |network_options| - @env["vm"].system.prepare_host_only_network(network_options) + # Otherwise, assign as the adapter the next available item + adapter[:adapter] = available.shift end + end + end - # Then enable the networks... - options.each do |network_options| - @env["vm"].system.enable_host_only_network(network_options) - end + # Verifies that the adapter configurations look good. This will + # raise an exception in the case that any errors occur. + def verify_adapters(adapters) + # Verify that there are no collisions in the adapters being used. + used = Set.new + adapters.each do |adapter| + raise Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) + used.add(adapter[:adapter]) 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 + # 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 = {} - result = if net_options[:name] - true if net_options[:name] == ni.name - else - true if matching_network?(ni, net_options) + # 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 - - return false if result end - true - 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] - def enable_network? - !@env.env.config.vm.network_options.compact.empty? + # Figure out the interface number by simple lookup + network[:interface] = adapter_to_interface[adapter[:adapter]] + end end - # Enables and assigns the host only network to the proper - # adapter on the VM, and saves the adapter. - def assign_network - @env.ui.info I18n.t("vagrant.actions.vm.network.preparing") + def hostonly_config(args) + ip = args[0] + options = args[1] || {} - @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_only_interface = network_name(network_options) - adapter.mac_address = network_options[:mac].gsub(':', '') if network_options[:mac] - adapter.save + # Determine if we're dealing with a static IP or a DHCP-served IP. + type = ip == :dhcp ? :dhcp : :static + + # Default IP is in the 20-bit private network block for DHCP based networks + ip = "172.28.128.1" if type == :dhcp + + options = { + :type => type, + :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) + + # Get the network address and IP parts which are used for many + # default calculations + netaddr = network_address(options[:ip], options[:netmask]) + ip_parts = netaddr.split(".").map { |i| i.to_i } + + # Calculate the adapter IP, which we assume is the IP ".1" at the + # end usually. + adapter_ip = ip_parts.dup + adapter_ip[3] += 1 + options[:adapter_ip] ||= adapter_ip.join(".") + + if type == :dhcp + # Calculate the DHCP server IP, which is the network address + # with the final octet + 2. So "172.28.0.0" turns into "172.28.0.2" + dhcp_ip = ip_parts.dup + dhcp_ip[3] += 2 + options[:dhcp_ip] ||= dhcp_ip.join(".") + + # Calculate the lower and upper bound for the DHCP server + dhcp_lower = ip_parts.dup + dhcp_lower[3] += 3 + options[:dhcp_lower] ||= dhcp_lower.join(".") + + dhcp_upper = ip_parts.dup + dhcp_upper[3] = 254 + options[:dhcp_upper] ||= dhcp_upper.join(".") end + + # Return the hostonly network configuration + return options 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 + def hostonly_adapter(config) + @logger.debug("Searching for matching network: #{config[:ip]}") + interface = find_matching_hostonly_network(config) - if net_options[:name] - return ni.name if net_options[:name] == ni.name + 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 + + if config[:type] == :dhcp + # Check that if there is a DHCP server attached on our interface, + # then it is identical. Otherwise, we can't set it. + if interface[:dhcp] + valid = interface[:dhcp][:ip] == config[:dhcp_ip] && + interface[:dhcp][:lower] == config[:dhcp_lower] && + interface[:dhcp][:upper] == config[:dhcp_upper] + + raise Errors::NetworkDHCPAlreadyAttached if !valid + + @logger.debug("DHCP server already properly configured") else - return ni.name if matching_network?(ni, net_options) + # Configure the DHCP server for the network. + @logger.debug("Creating a DHCP server...") + @env[:vm].driver.create_dhcp_server(interface[:name], config) end end - raise Errors::NetworkNotFound, :name => net_options[:name] if net_options[:name] + return { + :adapter => config[:adapter], + :type => :hostonly, + :hostonly => interface[:name], + :mac_address => config[:mac] + } + end - # One doesn't exist, create it. - @env.ui.info I18n.t("vagrant.actions.vm.network.creating") + def hostonly_network_config(config) + return { + :type => config[:type], + :ip => config[:ip], + :netmask => config[:netmask] + } + end - ni = interfaces.create - ni.enable_static(network_ip(net_options[:ip], net_options[:netmask]), - net_options[:netmask]) - ni.name + # Creates a new hostonly network that matches the network requested + # by the given host-only network configuration. + def create_hostonly_network(config) + # Create the options that are going to be used to create our + # new network. + options = config.dup + options[:ip] = options[:adapter_ip] + + @env[:vm].driver.create_host_only_network(options) 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]) + # 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 - # Applies a netmask to an IP and returns the corresponding - # parts. - def apply_netmask(ip, netmask) - ip = split_ip(ip) - netmask = split_ip(netmask) + # 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]) - ip.map do |part| - part & netmask.shift + @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 - # Splits an IP and converts each portion into an int. - def split_ip(ip) - ip.split(".").map do |i| - i.to_i + 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 - # 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(".") + def bridged_network_config(config) + return { + :type => :dhcp + } end end end end end