Merge branch 'abstract-networks'

This introduces the new network configuration syntax for Vagrant 1.1
and forward.

== The Problem

With multiple providers, the concept of networking as it stands in Vagrant
1.0.x becomes really muddy. We have `config.vm.forward_port` and
`config.vm.network :hostonly` and `config.vm.network :bridged`. But what
if someone writes an AWS provider? What is a bridged network in AWS? It
just doesn't make sense.

Networking working out of the box with Vagrant is a core part of what
makes Vagrant "magic" to new users. It is a core part of what makes Vagrant
simple to use. One option to punt networking to provider-specific
configuration was considered, but I found the whole idea of networking
too core to Vagrant to simply punt.

Because of this, a whole new method of networking is introduced.

== The Solution

The solution is to have a high-level notion of networking for Vagrant
configuration. This should cover the most _common_ cases of networking, and
every provider should do their best to implement these high-level
abstractions, to ensure the "just works" nature of Vagrant.

In addition to this high-level networking, low-level networking options
should be exposed on the provider configuration. This allows users to do
advanced provider-specific networking configuration if they want, but aren't
required to.

== High-Level Abstractions

=== Available Types

The high-level abstractions built into Vagrant will be the following:

* Forwarded ports - A mapping of host port to guest port that one can hit
  using `localhost`.
* Private network - A private network, the machine should ideally be
  protected from public access.
* Public network - A public network, one that is easily accessible by
  others.

I'm not sure if these are the proper abstractions. They can change up
until 2.0, but these are what we have so far.

Theoretically, here is how mappings would work. Note that this is just
an example, and the mappings in practice of such providers may or
may not map to this as follows.

**VirtualBox**
* Forwarded ports - NAT network, forwared ports.
* Private network - Hostonly network, static IP assigned.
* Public network - Bridged network, IP assigned via DHCP from router.

**VMWare**
* Forwarded ports - NAT network, forwarded ports.
* Private network - Hostonly network, static IP assigned.
* Public network - Bridged network, IP assigned via DHCP from router.

**AWS**
* Forwarded ports - Unimplemented.
* Private network - Public DNS in EC2, private IP in VPC.
* Public network - Elastic IP in EC2 and VPC.

=== Syntax

Networks are configured at the top-level of a Vagrantfile:

```ruby
Vagrant.configure("2") do |config|
  # ...

  config.vm.network :forwarded_port, 80, 8080
  config.vm.network :private_network, "192.168.1.12"
  config.vm.network :public_network
end
```

Providers should do their best to honor these configurations.

=== Advanced Options

While providers should do their best to satisfy the requirements for the
high-level abstractions, it is expected that provider-specific configuration
may be possible per network, even for the high-level configurations. For
this, provider-prefixed configuration options should be done:

```ruby
config.vm.network :forwarded_port, 80, 8000,
  :vmware__device => "vmnet8"

config.vm.network :public_network,
  :aws__elastic_ip => "1.2.3.4",
  :vmware__device => "en0"
```

If at all possible, providers should **not** require advanced options for
these to function.

== Low-level Configuration

While the high-level configuration should satisfy the common case and make
Vagrant work out of the box for most providers, one of the large benefits of
many providers is the ability to do certain networking tricks. For example,
KVM, Hyper-V, vSphere, etc. can create and be a part of true VLANs, which
may be required for certain upstream networking rules/ACLs. For things like
this, the network configuration should go directly into the provider
configuration in some way.

Examples:

```ruby
config.vm.provider :virtualbox do |vb|
  vb.network_adapter 2, :hostonly
  vb.network_adapter 3, :nat
end

config.vm.provider :aws do |aws|
  aws.routing_table = "route-123456"
end
```

It is up to the provider implementation to define the configuration
syntax as well as the implementation details of such an option. Other
providers are unable to see provider configurations other than their own
so it is truly private to the provider.
This commit is contained in:
Mitchell Hashimoto 2013-01-11 16:18:09 -08:00
commit 2d8a048946
16 changed files with 567 additions and 415 deletions

View File

@ -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", ".")

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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