diff --git a/config/default.rb b/config/default.rb index 180e0fc76..21f6c1cfa 100644 --- a/config/default.rb +++ b/config/default.rb @@ -11,12 +11,16 @@ Vagrant.configure("2") do |config| config.ssh.forward_x11 = false config.ssh.shell = "bash -l" - config.vm.auto_port_range = (2200..2250) + config.vm.usable_port_range = (2200..2250) config.vm.box_url = nil config.vm.base_mac = nil - config.vm.forward_port 22, 2222, :name => "ssh", :auto => true config.vm.guest = :linux + # Share SSH locally by default + config.vm.network :forwarded_port, 22, 2222, + :id => "ssh", + :auto_correct => true + # Share the root folder. This can then be overridden by # other Vagrantfiles, if they wish. config.vm.share_folder("v-root", "/vagrant", ".") diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index f6be7c111..9665eecea 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -437,6 +437,10 @@ module Vagrant error_key(:virtualbox_invalid_version) end + class VirtualBoxNoRoomForHighLevelNetwork < VagrantError + error_key(:virtualbox_no_room_for_high_level_network) + end + class VirtualBoxNotDetected < VagrantError status_code(8) error_key(:virtualbox_not_detected) diff --git a/lib/vagrant/util/scoped_hash_override.rb b/lib/vagrant/util/scoped_hash_override.rb new file mode 100644 index 000000000..ec4eb180d --- /dev/null +++ b/lib/vagrant/util/scoped_hash_override.rb @@ -0,0 +1,45 @@ +module Vagrant + module Util + # This allows for hash options to be overridden by a scope key + # prefix. An example speaks best here. Imagine the following hash: + # + # original = { + # :id => "foo", + # :mitchellh__id => "bar", + # :mitchellh__other => "foo" + # } + # + # scoped = scoped_hash_override(original, "mitchellh") + # + # scoped == { + # :id => "bar", + # :other => "foo" + # } + # + module ScopedHashOverride + def scoped_hash_override(original, scope) + # Convert the scope to a string in case a symbol was given since + # we use string comparisons for everything. + scope = scope.to_s + + # Shallow copy the hash for the result + result = original.dup + + original.each do |key, value| + parts = key.to_s.split("__", 2) + + # If we don't have the proper parts, then bail + next if parts.length != 2 + + # If this is our scope, then override + if parts[0] == scope + result[parts[1].to_sym] = value + result.delete(key) + end + end + + result + end + end + end +end diff --git a/plugins/kernel_v1/config/vm.rb b/plugins/kernel_v1/config/vm.rb index fd641bcc0..1c9f8b53e 100644 --- a/plugins/kernel_v1/config/vm.rb +++ b/plugins/kernel_v1/config/vm.rb @@ -1,5 +1,3 @@ -require "pathname" - module VagrantPlugins module Kernel_V1 # This is the Version 1.0.x Vagrant VM configuration. This is @@ -17,13 +15,11 @@ module VagrantPlugins attr_accessor :guest attr_accessor :host_name attr_reader :customizations - attr_reader :forwarded_ports attr_reader :networks attr_reader :provisioners attr_reader :shared_folders def initialize - @forwarded_ports = [] @shared_folders = {} @networks = [] @provisioners = [] @@ -32,14 +28,13 @@ module VagrantPlugins end def forward_port(guestport, hostport, options=nil) - @forwarded_ports << { - :name => "#{guestport.to_s(32)}-#{hostport.to_s(32)}", - :guestport => guestport, - :hostport => hostport, - :protocol => :tcp, - :adapter => 1, - :auto => false - }.merge(options || {}) + # Build up the network options for V2 + network_options = {} + network_options[:virtualbox__adapter] = options[:adapter] + network_options[:virtualbox__protocol] = options[:protocol] + + # Just append the forwarded port to the networks + @networks << [:forwarded_port, guestport, hostport, network_options] end def share_folder(name, guestpath, hostpath, opts=nil) @@ -83,12 +78,12 @@ module VagrantPlugins # Upgrade to a V2 configuration def upgrade(new) - new.vm.auto_port_range = self.auto_port_range if self.auto_port_range - new.vm.base_mac = self.base_mac if self.base_mac - new.vm.box = self.box if self.box - new.vm.box_url = self.box_url if self.box_url - new.vm.guest = self.guest if self.guest - new.vm.host_name = self.host_name if self.host_name + new.vm.base_mac = self.base_mac if self.base_mac + new.vm.box = self.box if self.box + new.vm.box_url = self.box_url if self.box_url + new.vm.guest = self.guest if self.guest + new.vm.host_name = self.host_name if self.host_name + new.vm.usable_port_range = self.auto_port_range if self.auto_port_range if self.boot_mode # Enable the GUI if the boot mode is GUI. @@ -101,15 +96,6 @@ module VagrantPlugins new.vm.providers[:virtualbox].config.customize(customization) end - # Take all the defined forwarded ports and re-define them - self.forwarded_ports.each do |fp| - options = fp.dup - guestport = options.delete(:guestport) - hostport = options.delete(:hostport) - - new.vm.forward_port(guestport, hostport, options) - end - # Re-define all networks. self.networks.each do |type, args| new.vm.network(type, *args) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 7178c3778..24d356332 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -11,12 +11,12 @@ module VagrantPlugins class VMConfig < Vagrant.plugin("2", :config) DEFAULT_VM_NAME = :default - attr_accessor :auto_port_range attr_accessor :base_mac attr_accessor :box attr_accessor :box_url attr_accessor :guest attr_accessor :host_name + attr_accessor :usable_port_range attr_reader :forwarded_ports attr_reader :shared_folders attr_reader :networks @@ -45,17 +45,6 @@ module VagrantPlugins result end - def forward_port(guestport, hostport, options=nil) - @forwarded_ports << { - :name => "#{guestport.to_s(32)}-#{hostport.to_s(32)}", - :guestport => guestport, - :hostport => hostport, - :protocol => :tcp, - :adapter => 1, - :auto => false - }.merge(options || {}) - end - def share_folder(name, guestpath, hostpath, opts=nil) @shared_folders[name] = { :guestpath => guestpath.to_s, @@ -69,6 +58,21 @@ module VagrantPlugins }.merge(opts || {}) end + # Define a way to access the machine via a network. This exposes a + # high-level abstraction for networking that may not directly map + # 1-to-1 for every provider. For example, AWS has no equivalent to + # "port forwarding." But most providers will attempt to implement this + # in a way that behaves similarly. + # + # `type` can be one of: + # + # * `:forwarded_port` - A port that is accessible via localhost + # that forwards into the machine. + # * `:private_network` - The machine gets an IP that is not directly + # publicly accessible, but ideally accessible from this machine. + # * `:public_network` - The machine gets an IP on a shared network. + # + # @param [Symbol] type Type of network def network(type, *args) @networks << [type, args] end @@ -139,39 +143,6 @@ module VagrantPlugins end end - # Validate some basic networking - # - # TODO: One day we need to abstract this out, since in the future - # providers other than VirtualBox will not be able to satisfy - # all types of networks. - networks.each do |type, args| - if type == :hostonly && args[0] == :dhcp - # Valid. There is no real way this can be invalid at the moment. - elsif type == :hostonly - # Validate the host-only network - ip = args[0] - - if !ip - errors.add(I18n.t("vagrant.config.vm.network_ip_required")) - else - ip_parts = ip.split(".") - - if ip_parts.length != 4 - errors.add(I18n.t("vagrant.config.vm.network_ip_invalid", - :ip => ip)) - elsif ip_parts.last == "1" - errors.add(I18n.t("vagrant.config.vm.network_ip_ends_one", - :ip => ip)) - end - end - elsif type == :bridged - else - # Invalid network type - errors.add(I18n.t("vagrant.config.vm.network_invalid", - :type => type.to_s)) - end - end - # Each provisioner can validate itself provisioners.each do |prov| prov.validate(env, errors) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index a775c72e3..de4d18f1f 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -58,7 +58,7 @@ module VagrantPlugins b.use CleanMachineFolder b.use ClearForwardedPorts b.use EnvSet, :port_collision_handler => :correct - b.use ForwardPorts + b.use CheckPortCollisions b.use Provision b.use PruneNFSExports b.use NFS @@ -66,6 +66,7 @@ module VagrantPlugins b.use ShareFolders b.use ClearNetworkInterfaces b.use Network + b.use ForwardPorts b.use HostName b.use SaneDefaults b.use Customize @@ -193,6 +194,7 @@ module VagrantPlugins b.use Call, Created do |env, b2| if env[:result] b2.use CheckAccessible + b2.use EnvSet, :port_collision_handler => :error b2.use CheckPortCollisions b2.use Resume else diff --git a/plugins/providers/virtualbox/action/check_port_collisions.rb b/plugins/providers/virtualbox/action/check_port_collisions.rb index 75ed07132..12731f1ad 100644 --- a/plugins/providers/virtualbox/action/check_port_collisions.rb +++ b/plugins/providers/virtualbox/action/check_port_collisions.rb @@ -1,9 +1,12 @@ +require "set" + require "vagrant/util/is_port_open" module VagrantPlugins module ProviderVirtualBox module Action class CheckPortCollisions + include Util::CompileForwardedPorts include Vagrant::Util::IsPortOpen def initialize(app, env) @@ -14,6 +17,17 @@ module VagrantPlugins # For the handlers... @env = env + # If we don't have forwarded ports set on the environment, then + # we compile them. + env[:forwarded_ports] ||= compile_forwarded_ports(env[:machine].config) + + existing = env[:machine].provider.driver.read_used_ports + + # Calculate the auto-correct port range + @usable_ports = Set.new(env[:machine].config.vm.usable_port_range) + @usable_ports.subtract(env[:forwarded_ports].collect { |fp| fp.host_port }) + @usable_ports.subtract(existing) + # Figure out how we handle port collisions. By default we error. handler = env[:port_collision_handler] || :error @@ -24,16 +38,15 @@ module VagrantPlugins current[name] = hostport.to_i end - existing = env[:machine].provider.driver.read_used_ports - env[:machine].config.vm.forwarded_ports.each do |options| + env[:forwarded_ports].each do |fp| # Use the proper port, whether that be the configured port or the # port that is currently on use of the VM. - hostport = options[:hostport].to_i - hostport = current[options[:name]] if current.has_key?(options[:name]) + host_port = fp.host_port + host_port = current[fp.id] if current.has_key?(fp.id) - if existing.include?(hostport) || is_port_open?("127.0.0.1", hostport) + if existing.include?(host_port) || is_port_open?("127.0.0.1", host_port) # We have a collision! Handle it - send("handle_#{handler}".to_sym, options, existing) + send("handle_#{handler}".to_sym, fp, existing) end end @@ -41,47 +54,41 @@ module VagrantPlugins end # Handles a port collision by raising an exception. - def handle_error(options, existing_ports) + def handle_error(fp, existing_ports) raise Vagrant::Errors::ForwardPortCollisionResume end # Handles a port collision by attempting to fix it. - def handle_correct(options, existing_ports) + def handle_correct(fp, existing_ports) # We need to keep this for messaging purposes - original_hostport = options[:hostport] + original_hostport = fp.host_port - if !options[:auto] + if !fp.auto_correct # Auto fixing is disabled for this port forward, so we # must throw an error so the user can fix it. raise Vagrant::Errors::ForwardPortCollision, - :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s + :host_port => fp.host_port.to_s, + :guest_port => fp.guest_port.to_s end - # Get the auto port range and get rid of the used ports and - # ports which are being used in other forwards so we're just - # left with available ports. - range = @env[:machine].config.vm.auto_port_range.to_a - range -= @env[:machine].config.vm.forwarded_ports.collect { |opts| opts[:hostport].to_i } - range -= existing_ports - - if range.empty? + if @usable_ports.empty? raise Vagrant::Errors::ForwardPortAutolistEmpty, :vm_name => @env[:machine].name, - :host_port => options[:hostport].to_s, - :guest_port => options[:guestport].to_s + :host_port => fp.host_port.to_s, + :guest_port => fp.guest_port.to_s end - # Set the port up to be the first one and add that port to - # the used list. - options[:hostport] = range.shift - existing_ports << options[:hostport] + # Get the first usable port and set it up + new_port = @usable_ports.to_a.sort[0] + @usable_ports.delete(new_port) + fp.correct_host_port(new_port) + existing_ports << new_port # Notify the user @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.fixed_collision", :host_port => original_hostport.to_s, - :guest_port => options[:guestport].to_s, - :new_port => options[:hostport])) + :guest_port => fp.guest_port.to_s, + :new_port => fp.host_port.to_s)) end end end diff --git a/plugins/providers/virtualbox/action/forward_ports.rb b/plugins/providers/virtualbox/action/forward_ports.rb index 97524b75e..1b3a62f6b 100644 --- a/plugins/providers/virtualbox/action/forward_ports.rb +++ b/plugins/providers/virtualbox/action/forward_ports.rb @@ -2,6 +2,8 @@ module VagrantPlugins module ProviderVirtualBox module Action class ForwardPorts + include Util::CompileForwardedPorts + def initialize(app, env) @app = app end @@ -13,54 +15,32 @@ module VagrantPlugins @env = env # Get the ports we're forwarding - ports = forward_port_definitions + env[:forwarded_ports] ||= compile_forwarded_ports(env[:machine].config) # Warn if we're port forwarding to any privileged ports... - threshold_check(ports) + env[:forwarded_ports].each do |fp| + if fp.host_port <= 1024 + env[:ui].warn I18n.t("vagrant.actions.vm.forward_ports.privileged_ports") + return + end + end env[:ui].info I18n.t("vagrant.actions.vm.forward_ports.forwarding") - forward_ports(ports) + forward_ports @app.call(env) end - # This returns an array of forwarded ports with overrides properly - # squashed. - def forward_port_definitions - # Get all the port mappings in the order they're defined and - # organize them by their guestport, taking the "last one wins" - # approach. - guest_port_mapping = {} - @env[:machine].config.vm.forwarded_ports.each do |options| - key = options[:protocol].to_s + options[:guestport].to_s - guest_port_mapping[key] = options - end - - # Return the values, since the order doesn't really matter - guest_port_mapping.values - end - - # This method checks for any forwarded ports on the host below - # 1024, which causes the forwarded ports to fail. - def threshold_check(ports) - ports.each do |options| - if options[:hostport] <= 1024 - @env[:ui].warn I18n.t("vagrant.actions.vm.forward_ports.privileged_ports") - return - end - end - end - - def forward_ports(mappings) + def forward_ports ports = [] interfaces = @env[:machine].provider.driver.read_network_interfaces - mappings.each do |options| + @env[:forwarded_ports].each do |fp| message_attributes = { - :guest_port => options[:guestport], - :host_port => options[:hostport], - :adapter => options[:adapter] + :adapter => fp.adapter, + :guest_port => fp.guest_port, + :host_port => fp.host_port } # Assuming the only reason to establish port forwarding is @@ -72,14 +52,20 @@ module VagrantPlugins # Port forwarding requires the network interface to be a NAT interface, # so verify that that is the case. - if interfaces[options[:adapter]][:type] != :nat + if interfaces[fp.adapter][:type] != :nat @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.non_nat", message_attributes)) next end # Add the options to the ports array to send to the driver later - ports << options.merge(:name => options[:name], :adapter => options[:adapter]) + ports << { + :adapter => fp.adapter, + :guestport => fp.guest_port, + :hostport => fp.host_port, + :name => fp.id, + :protocol => fp.protocol + } end if !ports.empty? diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb index 31a6fa38d..f48c3b756 100644 --- a/plugins/providers/virtualbox/action/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -3,320 +3,134 @@ require "set" require "log4r" require "vagrant/util/network_ip" +require "vagrant/util/scoped_hash_override" module VagrantPlugins module ProviderVirtualBox module Action + # This middleware class sets up all networking for the VirtualBox + # instance. This includes host only networks, bridged networking, + # forwarded ports, etc. + # + # This handles all the `config.vm.network` configurations. class Network - # Utilities to deal with network addresses include Vagrant::Util::NetworkIP + include Vagrant::Util::ScopedHashOverride def initialize(app, env) - @logger = Log4r::Logger.new("vagrant::action::vm::network") - - @app = app + @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::network") + @app = app end def call(env) + # TODO: Validate network configuration prior to anything below @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...") + # Get the list of network adapters from the configuration + network_adapters_config = env[:machine].provider_config.network_adapters.dup + + # Assign the adapter slot for each high-level network + available_slots = Set.new(1..8) + network_adapters_config.each do |slot, _data| + available_slots.delete(slot) + end + + @logger.debug("Available slots for high-level adapters: #{available_slots.inspect}") + @logger.info("Determinging network adapters required for high-level configuration...") + available_slots = available_slots.to_a.sort + env[:machine].config.vm.networks.each do |type, args| + # We only handle private and public networks + next if type != :private_network && type != :public_network + + options = nil + options = args.last if args.last.is_a?(Hash) + options ||= {} + options = scoped_hash_override(options, :virtualbox) + + # Figure out the slot that this adapter will go into + slot = options[:adapter] + if !slot + if available_slots.empty? + raise Vagrant::Errors::VirtualBoxNoRoomForHighLevelNetwork + end + + slot = available_slots.shift + end + + # Configure it + data = nil + if type == :private_network + # private_network = hostonly + + config_args = [args[0], options] + data = [:hostonly, config_args] + elsif type == :public_network + # public_network = bridged + + config_args = [options] + data = [:bridged, config_args] + end + + # Store it! + @logger.info(" -- Slot #{slot}: #{data[0]}") + network_adapters_config[slot] = data + end + + @logger.info("Determining adapters and compiling network configuration...") adapters = [] networks = [] - env[:machine].config.vm.networks.each do |type, args| - # Get the normalized configuration we'll use around - config = send("#{type}_config", args) + network_adapters_config.each do |slot, data| + type = data[0] + args = data[1] - # Get the virtualbox adapter configuration + @logger.info("Network slot #{slot}. Type: #{type}.") + + # Get the normalized configuration for this type + config = send("#{type}_config", args) + config[:adapter] = slot + @logger.debug("Normalized configuration: #{config.inspect}") + + # Get the VirtualBox adapter configuration adapter = send("#{type}_adapter", config) adapters << adapter + @logger.debug("Adapter configuration: #{adapter.inspect}") # Get the network configuration network = send("#{type}_network_config", config) - network[:_auto_config] = true if config[:auto_config] + network[:auto_config] = config[:auto_config] networks << network end 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 + # Enable the adapters @logger.info("Enabling adapters...") env[:ui].info I18n.t("vagrant.actions.vm.network.preparing") env[:machine].provider.driver.enable_adapters(adapters) end - # Continue the middleware chain. We're done with our VM - # setup until after it is booted. + # Continue the middleware chain. @app.call(env) + # If we have networks to configure, then we configure it now, since + # that requires the machine to be up and running. if !adapters.empty? && !networks.empty? - # Determine the interface numbers for the guest. assign_interface_numbers(networks, adapters) - # Configure all the network interfaces on the guest. We only - # want to configure the networks that have `auto_config` setup. - networks_to_configure = networks.select { |n| n[:_auto_config] } + # Only configure the networks the user requested us to configure + networks_to_configure = networks.select { |n| n[:auto_config] } env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") env[:machine].guest.configure_networks(networks_to_configure) end 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[:machine].provider.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] - # If we have no available adapters, then that is an exceptional - # event. - raise Vagrant::Errors::NetworkNoAdapters if available.empty? - - # Otherwise, assign as the adapter the next available item - adapter[:adapter] = available.shift - end - end - 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 Vagrant::Errors::NetworkAdapterCollision if used.include?(adapter[:adapter]) - used.add(adapter[:adapter]) - 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[:machine].provider.driver.read_network_interfaces - vm_adapters.sort.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] || {} - - # 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, - :auto_config => true - }.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 - - 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 Vagrant::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 Vagrant::Errors::NetworkDHCPAlreadyAttached if !valid - - @logger.debug("DHCP server already properly configured") - else - # Configure the DHCP server for the network. - @logger.debug("Creating a DHCP server...") - @env[:machine].provider.driver.create_dhcp_server(interface[:name], config) - end - end - - return { - :adapter => config[:adapter], - :type => :hostonly, - :hostonly => interface[:name], - :mac_address => config[:mac], - :nic_type => config[:nic_type] - } - end - - def hostonly_network_config(config) - return { - :type => config[:type], - :adapter_ip => config[:adapter_ip], - :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) - # Create the options that are going to be used to create our - # new network. - options = config.dup - options[:ip] = options[:adapter_ip] - - @env[:machine].provider.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[:machine].provider.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[:machine].provider.driver.read_bridged_interfaces.each do |interface| - that_netaddr = network_address(interface[:ip], interface[:netmask]) - raise Vagrant::Errors::NetworkCollision if this_netaddr == that_netaddr && interface[:status] != "Down" - end - end - def bridged_config(args) - options = args[0] || {} - options = {} if !options.is_a?(Hash) - return { - :adapter => nil, - :mac => nil, - :bridge => nil, - :auto_config => true, + :auto_config => true, + :bridge => nil, + :mac => nil, + :nic_type => nil, :use_dhcp_assigned_default_route => false - }.merge(options) + }.merge(args[0] || {}) end def bridged_adapter(config) @@ -398,6 +212,162 @@ module VagrantPlugins :use_dhcp_assigned_default_route => config[:use_dhcp_assigned_default_route] } end + + def hostonly_config(args) + ip = args[0] + options = { + :auto_config => true, + :netmask => "255.255.255.0" + }.merge(args[1] || {}) + + # Calculate our network address for the given IP/netmask + netaddr = network_address(ip, options[:netmask]) + + # Verify 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. + @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| + that_netaddr = network_address(interface[:ip], interface[:netmask]) + raise Vagrant::Errors::NetworkCollision if \ + netaddr == that_netaddr && interface[:status] != "Down" + end + + # Split the IP address into its components + 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(".") + + return { + :adapter_ip => adapter_ip, + :auto_config => options[:auto_config], + :ip => ip, + :mac => nil, + :netmask => options[:netmask], + :nic_type => nil, + :type => :static + } + end + + def hostonly_adapter(config) + @logger.info("Searching for matching hostonly network: #{config[:ip]}") + interface = hostonly_find_matching_network(config) + + if !interface + @logger.info("Network not found. Creating if we can.") + + # It is an error if a specific host only network name was specified + # but the network wasn't found. + if config[:name] + raise Vagrant::Errors::NetworkNotFound, :name => config[:name] + end + + # Create a new network + interface = hostonly_create_network(config) + @logger.info("Created network: #{interface[:name]}") + end + + return { + :adapter => config[:adapter], + :hostonly => interface[:name], + :mac => config[:mac], + :nic_type => config[:nic_type], + :type => :hostonly + } + end + + def hostonly_network_config(config) + return { + :type => config[:type], + :adapter_ip => config[:adapter_ip], + :ip => config[:ip], + :netmask => config[:netmask] + } + end + + def nat_config(options) + return { + :auto_config => false + } + end + + def nat_adapter(config) + return { + :adapter => config[:adapter], + :type => :nat, + } + end + + def nat_network_config(config) + return {} + end + + #----------------------------------------------------------------- + # Misc. helpers + #----------------------------------------------------------------- + # 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[:machine].provider.driver.read_network_interfaces + vm_adapters.sort.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 + + #----------------------------------------------------------------- + # Hostonly Helper Functions + #----------------------------------------------------------------- + # This creates a host only network for the given configuration. + def hostonly_create_network(config) + @env[:machine].provider.driver.create_host_only_network( + :adapter_ip => config[:adapter_ip], + :netmask => config[:netmask] + ) + end + + # This finds a matching host only network for the given configuration. + def hostonly_find_matching_network(config) + this_netaddr = network_address(config[:ip], config[:netmask]) + + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| + return interface if config[:name] && config[:name] == interface[:name] + return interface if this_netaddr == \ + network_address(interface[:ip], interface[:netmask]) + end + + nil + end end end end diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index c71c2f4d9..0897d4ab3 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -1,14 +1,28 @@ module VagrantPlugins module ProviderVirtualBox class Config < Vagrant.plugin("2", :config) + # An array of customizations to make on the VM prior to booting it. + # + # @return [Array] attr_reader :customizations # If set to `true`, then VirtualBox will be launched with a GUI. + # + # @return [Boolean] attr_accessor :gui + # The defined network adapters. + # + # @return [Hash] + attr_reader :network_adapters + def initialize - @customizations = [] - @gui = UNSET_VALUE + @customizations = [] + @network_adapters = {} + @gui = UNSET_VALUE + + # We require that network adapter 1 is a NAT device. + network_adapter(1, :nat) end # Customize the VM by calling `VBoxManage` with the given @@ -27,6 +41,15 @@ module VagrantPlugins @customizations << command end + # This defines a network adapter that will be added to the VirtualBox + # virtual machine in the given slot. + # + # @param [Integer] slot The slot for this network adapter. + # @param [Symbol] type The type of adapter. + def network_adapter(slot, type, *args) + @network_adapters[slot] = [type, args] + end + # This is the hook that is called to finalize the object before it # is put into use. def finalize! diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index 9510dfc8f..d8e9586a2 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -73,8 +73,8 @@ module VagrantPlugins execute("list", "vms").split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ info = execute("showvminfo", $1.to_s, "--machinereadable", :retryable => true) - info.split("\n").each do |line| - if line =~ /^hostonlyadapter\d+="(.+?)"$/ + info.split("\n").each do |inner_line| + if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ networks.delete($1.to_s) end end diff --git a/plugins/providers/virtualbox/model/forwarded_port.rb b/plugins/providers/virtualbox/model/forwarded_port.rb new file mode 100644 index 000000000..8cec67969 --- /dev/null +++ b/plugins/providers/virtualbox/model/forwarded_port.rb @@ -0,0 +1,58 @@ +module VagrantPlugins + module ProviderVirtualBox + module Model + # Represents a single forwarded port for VirtualBox. This has various + # helpers and defaults for a forwarded port. + class ForwardedPort + # The NAT adapter on which to attach the forwarded port. + # + # @return [Integer] + attr_reader :adapter + + # If true, this port should be auto-corrected. + # + # @return [Boolean] + attr_reader :auto_correct + + # The unique ID for the forwarded port. + # + # @return [String] + attr_reader :id + + # The protocol to forward. + # + # @return [String] + attr_reader :protocol + + # The port on the guest to be exposed on the host. + # + # @return [Integer] + attr_reader :guest_port + + # The port on the host used to access the port on the guest. + # + # @return [Integer] + attr_reader :host_port + + def initialize(id, host_port, guest_port, options) + @id = id + @guest_port = guest_port + @host_port = host_port + + options ||= {} + @auto_correct = true + @auto_correct = options[:auto_correct] if options.has_key?(:auto_correct) + @adapter = options[:adapter] || 1 + @protocol = options[:protocol] || "tcp" + end + + # This corrects the host port and changes it to the given new port. + # + # @param [Integer] new_port The new port + def correct_host_port(new_port) + @host_port = new_port + end + end + end + end +end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 3c876c340..5d8bcd6f1 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -30,5 +30,13 @@ module VagrantPlugins autoload :Version_4_1, File.expand_path("../driver/version_4_1", __FILE__) autoload :Version_4_2, File.expand_path("../driver/version_4_2", __FILE__) end + + module Model + autoload :ForwardedPort, File.expand_path("../model/forwarded_port", __FILE__) + end + + module Util + autoload :CompileForwardedPorts, File.expand_path("../util/compile_forwarded_ports", __FILE__) + end end end diff --git a/plugins/providers/virtualbox/util/compile_forwarded_ports.rb b/plugins/providers/virtualbox/util/compile_forwarded_ports.rb new file mode 100644 index 000000000..fe281c970 --- /dev/null +++ b/plugins/providers/virtualbox/util/compile_forwarded_ports.rb @@ -0,0 +1,33 @@ +require "vagrant/util/scoped_hash_override" + +module VagrantPlugins + module ProviderVirtualBox + module Util + module CompileForwardedPorts + include Vagrant::Util::ScopedHashOverride + + # This method compiles the forwarded ports into {ForwardedPort} + # models. + def compile_forwarded_ports(config) + mappings = {} + + config.vm.networks.each do |type, args| + if type == :forwarded_port + guest_port = args[0] + host_port = args[1] + options = args[2] || {} + options = scoped_hash_override(options, :virtualbox) + id = options[:id] || + "#{guest_port.to_s(32)}-#{host_port.to_s(32)}" + + mappings[host_port] = + Model::ForwardedPort.new(id, host_port, guest_port, options) + end + end + + mappings.values + end + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2eb4f2c9c..f8dbb3582 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -280,6 +280,13 @@ en: VirtualBox is complaining that the installation is incomplete. Please run `VBoxManage --version` to see the error message which should contain instructions on how to fix this error. + virtualbox_no_room_for_high_level_network: |- + There is no available slots on the VirtualBox VM for the configured + high-level network interfaces. "private_network" and "public_network" + network configurations consume a single network adapter slot on the + VirtualBox VM. VirtualBox limits the number of slots to 8, and it + appears that every slot is in use. Please lower the number of used + network adapters. virtualbox_not_detected: |- Vagrant could not detect VirtualBox! Make sure VirtualBox is properly installed. Vagrant uses the `VBoxManage` binary that ships with VirtualBox, and requires diff --git a/test/unit/vagrant/util/scoped_hash_override_test.rb b/test/unit/vagrant/util/scoped_hash_override_test.rb new file mode 100644 index 000000000..bdf277503 --- /dev/null +++ b/test/unit/vagrant/util/scoped_hash_override_test.rb @@ -0,0 +1,48 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/scoped_hash_override" + +describe Vagrant::Util::ScopedHashOverride do + let(:klass) do + Class.new do + extend Vagrant::Util::ScopedHashOverride + end + end + + it "should not mess with non-overrides" do + original = { + :key => "value", + :another_value => "foo" + } + + klass.scoped_hash_override(original, "foo").should == original + end + + it "should override if the scope matches" do + original = { + :key => "value", + :scope__key => "replaced" + } + + expected = { + :key => "replaced" + } + + klass.scoped_hash_override(original, "scope").should == expected + end + + it "should ignore non-matching scopes" do + original = { + :key => "value", + :scope__key => "replaced", + :another__key => "value" + } + + expected = { + :key => "replaced", + :another__key => "value" + } + + klass.scoped_hash_override(original, "scope").should == expected + end +end