Re-implement how networking is done internally

This commit is contained in:
Mitchell Hashimoto 2012-01-02 19:40:19 -08:00
parent 76a77432b8
commit 97f7fa633d
14 changed files with 393 additions and 78 deletions

View File

@ -26,7 +26,6 @@ module Vagrant
module VM
autoload :Boot, 'vagrant/action/vm/boot'
autoload :BridgedNetwork, 'vagrant/action/vm/bridged_network'
autoload :CheckAccessible, 'vagrant/action/vm/check_accessible'
autoload :CheckBox, 'vagrant/action/vm/check_box'
autoload :CheckGuestAdditions, 'vagrant/action/vm/check_guest_additions'
@ -46,7 +45,7 @@ module Vagrant
autoload :HostName, 'vagrant/action/vm/host_name'
autoload :Import, 'vagrant/action/vm/import'
autoload :MatchMACAddress, 'vagrant/action/vm/match_mac_address'
autoload :HostOnlyNetwork, 'vagrant/action/vm/host_only_network'
autoload :Network, 'vagrant/action/vm/network'
autoload :NFS, 'vagrant/action/vm/nfs'
autoload :Package, 'vagrant/action/vm/package'
autoload :PackageVagrantfile, 'vagrant/action/vm/package_vagrantfile'

View File

@ -30,8 +30,7 @@ module Vagrant
use VM::ShareFolders
use VM::HostName
use VM::ClearNetworkInterfaces
use VM::BridgedNetwork
use VM::HostOnlyNetwork
use VM::Network
use VM::Customize
use VM::Boot
end

View File

@ -55,7 +55,7 @@ module Vagrant
# Port forwarding requires the network interface to be a NAT interface,
# so verify that that is the case.
if interfaces[adapter][:type] != "nat"
if interfaces[adapter][:type] != :nat
@env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.non_nat",
message_attributes))
next

View File

@ -0,0 +1,284 @@
require 'set'
require 'log4r'
require 'vagrant/util/network_ip'
module Vagrant
module Action
module VM
# This action handles all `config.vm.network` configurations by
# setting up the VM properly and enabling the networks afterword.
class Network
# Utilities to deal with network addresses
include Util::NetworkIP
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant::action::vm::network")
@app = app
end
def call(env)
@env = env
# First we have to get the array of adapters that we need
# to create on the virtual machine itself, as well as the
# driver-agnostic network configurations for each.
@logger.debug("Determining adapters and networks...")
adapters = []
networks = []
env[:vm].config.vm.networks.each do |type, args|
# Get the normalized configuration we'll use around
config = send("#{type}_config", args)
# Get the virtualbox adapter configuration
adapter = send("#{type}_adapter", config)
adapters << adapter
# Get the network configuration
network = send("#{type}_network_config", config)
networks << network
end
# Automatically assign an adapter number to any adapters
# that aren't explicitly set.
@logger.debug("Assigning adapter locations...")
assign_adapter_locations(adapters)
# Create all the network interfaces
@logger.info("Enabling adapters...")
env[:ui].info I18n.t("vagrant.actions.vm.network.preparing")
env[:vm].driver.enable_adapters(adapters)
# Continue the middleware chain. We're done with our VM
# setup until after it is booted.
@app.call(env)
# Determine the interface numbers for the guest.
assign_interface_numbers(networks, adapters)
# Configure all the network interfaces on the guest.
env[:ui].info I18n.t("vagrant.actions.vm.network.configuring")
env[:vm].guest.configure_networks(networks)
end
# This method assigns the adapter to use for the adapter.
# e.g. it says that the first adapter is actually on the
# virtual machine's 2nd adapter location.
#
# It determines the adapter numbers by simply finding the
# "next available" in each case.
#
# The adapters are modified in place by adding an ":adapter"
# field to each.
def assign_adapter_locations(adapters)
available = Set.new(1..8)
# Determine which NICs are actually available.
interfaces = @env[:vm].driver.read_network_interfaces
interfaces.each do |number, nic|
# Remove the number from the available NICs if the
# NIC is in use.
available.delete(number) if nic[:type] != :none
end
# Based on the available set, assign in order to
# the adapters.
available = available.to_a.sort
@logger.debug("Available NICs: #{available.inspect}")
adapters.each do |adapter|
# Ignore the adapters that already have been assigned
if !adapter[:adapter]
adapter[:adapter] = available.shift
end
end
end
# Assigns the actual interface number of a network based on the
# enabled NICs on the virtual machine.
#
# This interface number is used by the guest to configure the
# NIC on the guest VM.
#
# The networks are modified in place by adding an ":interface"
# field to each.
def assign_interface_numbers(networks, adapters)
current = 0
adapter_to_interface = {}
# Make a first pass to assign interface numbers by adapter location
vm_adapters = @env[:vm].driver.read_network_interfaces
vm_adapters.each do |number, adapter|
if adapter[:type] != :none
# Not used, so assign the interface number and increment
adapter_to_interface[number] = current
current += 1
end
end
# Make a pass through the adapters to assign the :interface
# key to each network configuration.
adapters.each_index do |i|
adapter = adapters[i]
network = networks[i]
# Figure out the interface number by simple lookup
network[:interface] = adapter_to_interface[adapter[:adapter]]
end
end
def hostonly_config(args)
ip = args[0]
options = args[1] || {}
options = {
:ip => ip,
:netmask => "255.255.255.0",
:adapter => nil,
:mac => nil,
:name => nil
}.merge(options)
# Verify that this hostonly network wouldn't conflict with any
# bridged interfaces
verify_no_bridge_collision(options)
# Return the hostonly network configuration
return options
end
def hostonly_adapter(config)
@logger.debug("Searching for matching network: #{config[:ip]}")
interface = find_matching_hostonly_network(config)
if !interface
@logger.debug("Network not found. Creating if we can.")
# It is an error case if a specific name was given but the network
# doesn't exist.
if config[:name]
raise Errors::NetworkNotFound, :name => config[:name]
end
# Otherwise, we create a new network and put the net network
# in the list of available networks so other network definitions
# can use it!
interface = create_hostonly_network(config)
@logger.debug("Created network: #{interface[:name]}")
end
return {
:adapter => config[:adapter],
:type => :hostonly,
:hostonly => interface[:name],
:mac_address => config[:mac]
}
end
def hostonly_network_config(config)
return {
:type => :static,
:ip => config[:ip],
:netmask => config[:netmask]
}
end
# Creates a new hostonly network that matches the network requested
# by the given host-only network configuration.
def create_hostonly_network(config)
# First we need to determine a good IP for the host machine
# of this interface. We choose to use the network address
# plus 1, which is usually what is expected.
netaddr = network_address(config[:ip], config[:netmask])
parts = netaddr.split(".").map { |i| i.to_i }
parts[3] += 1
ip = parts.join(".")
# Create the options that are going to be used to create our
# new network.
options = config.dup
options[:ip] = ip
@env[:vm].driver.create_host_only_network(options)
end
# Finds a host only network that matches our configuration on VirtualBox.
# This will return nil if a matching network does not exist.
def find_matching_hostonly_network(config)
this_netaddr = network_address(config[:ip], config[:netmask])
@env[:vm].driver.read_host_only_interfaces.each do |interface|
if config[:name] && config[:name] == interface[:name]
return interface
elsif this_netaddr == network_address(interface[:ip], interface[:netmask])
return interface
end
end
nil
end
# Verifies that a host-only network subnet would not collide with
# a bridged networking interface.
#
# If the subnets overlap in any way then the host only network
# will not work because the routing tables will force the traffic
# onto the real interface rather than the virtualbox interface.
def verify_no_bridge_collision(options)
this_netaddr = network_address(options[:ip], options[:netmask])
@env[:vm].driver.read_bridged_interfaces.each do |interface|
that_netaddr = network_address(interface[:ip], interface[:netmask])
raise Errors::NetworkCollision if this_netaddr == that_netaddr
end
end
def bridged_config(args)
options = args[0] || {}
return {
:adapter => nil,
:mac => nil
}.merge(options)
end
def bridged_adapter(config)
bridgedifs = @env[:vm].driver.read_bridged_interfaces
# Output all the interfaces that are available as choices
@env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.available",
:prefix => false)
bridgedifs.each_index do |index|
interface = bridgedifs[index]
@env[:ui].info("#{index + 1}) #{interface[:name]}", :prefix => false)
end
# The range of valid choices
valid = Range.new(1, bridgedifs.length)
# The choice that the user has chosen as the bridging interface
choice = nil
while !valid.include?(choice)
choice = @env[:ui].ask("What interface should the network bridge to? ")
choice = choice.to_i
end
# Given the choice we can now define the adapter we're using
return {
:adapter => config[:adapter],
:type => :bridged,
:bridge => bridgedifs[choice - 1][:name],
:mac_address => config[:mac]
}
end
def bridged_network_config(config)
return {
:type => :dhcp
}
end
end
end
end
end

View File

@ -340,7 +340,7 @@ module Vagrant
execute("showvminfo", @uuid, "--machinereadable").split("\n").each do |line|
if line =~ /^nic(\d+)="(.+?)"$/
adapter = $1.to_i
type = $2.to_s
type = $2.to_sym
nics[adapter] ||= {}
nics[adapter][:type] = type

View File

@ -66,36 +66,22 @@ module Vagrant
# via the host are already done.
def mount_nfs(ip, folders); end
# Prepares the system for host only networks. This is called
# once prior to any `enable_host_only_network` calls.
def prepare_host_only_network(net_options=nil)
raise BaseError, :_key => :unsupported_host_only
end
# Setup the system by adding a new host only network. This
# method should configure and bring up the interface for the
# given options.
# Configures the given list of networks on the virtual machine.
#
# @param [Hash] net_options The options for the network.
def enable_host_only_network(net_options); end
# Prepares the guest for bridged networks.
# The networks parameter will be an array of hashes where the hashes
# represent the configuration of a network interface. The structure
# of the hash will be roughly the following:
#
# @param [Array] networks Array of networks to prepare for.
def prepare_bridged_networks(networks)
raise BaseError, :_key => :unsupported_host_only
end
# Enable bridged networks on a guest.
# {
# :type => :static,
# :ip => "33.33.33.10",
# :netmask => "255.255.255.0",
# :interface => 1
# }
#
# @param [Array] networks Array of networks to prepare for.
def enable_bridged_networks(networks)
raise BaseError, :_key => :unsupported_bridged
end
def configure_networks(networks); end
def change_host_name(name)
raise BaseError, :_key => :unsupported_bridged
end
def change_host_name(name); end
end
end
end

View File

@ -1,49 +1,49 @@
require 'set'
require 'vagrant/util/template_renderer'
module Vagrant
module Guest
class Debian < Linux
def prepare_host_only_network(net_options=nil)
# Remove any previous host only network additions to the interface file.
# Make the TemplateRenderer top-level
include Vagrant::Util
def configure_networks(networks)
# First, remove any previous network modifications
# from the interface file.
vm.ssh.execute do |ssh|
ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN-HOSTONLY/,/^#VAGRANT-END-HOSTONLY/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces")
ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces")
ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-interfaces > /etc/network/interfaces'")
end
end
def enable_host_only_network(net_options)
entry = TemplateRenderer.render('guests/debian/network_hostonly',
:net_options => net_options)
vm.ssh.upload!(StringIO.new(entry), "/tmp/vagrant-network-entry")
vm.ssh.execute do |ssh|
ssh.exec!("sudo /sbin/ifdown eth#{net_options[:adapter]} 2> /dev/null")
ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-entry >> /etc/network/interfaces'")
ssh.exec!("sudo /sbin/ifup eth#{net_options[:adapter]}")
# Accumulate the configurations to add to the interfaces file as
# well as what interfaces we're actually configuring since we use that
# later.
interfaces = Set.new
entries = []
networks.each do |network|
interfaces.add(network[:interface])
entries << TemplateRenderer.render("guests/debian/network_#{network[:type]}",
:options => network)
end
end
def prepare_bridged_networks(networks)
# Remove any previous bridged network additions to the interface file.
vm.ssh.execute do |ssh|
ssh.exec!("sudo sed -e '/^#VAGRANT-BEGIN-BRIDGED/,/^#VAGRANT-END-BRIDGED/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces")
ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-interfaces > /etc/network/interfaces'")
end
end
def enable_bridged_networks(networks)
entry = TemplateRenderer.render('guests/debian/network_bridged',
:networks => networks)
vm.ssh.upload!(StringIO.new(entry), "/tmp/vagrant-network-entry")
# Perform the careful dance necessary to to reconfigure
# the network interfaces
vm.ssh.upload!(StringIO.new(entries.join("\n")), "/tmp/vagrant-network-entry")
vm.ssh.execute do |ssh|
networks.each do |network|
ssh.exec!("sudo /sbin/ifdown eth#{network[:adapter]} 2> /dev/null")
# Bring down all the interfaces we're reconfiguring. By bringing down
# each specifically, we avoid reconfiguring eth0 (the NAT interface) so
# SSH never dies.
interfaces.each do |interface|
ssh.exec!("sudo /sbin/ifdown eth#{interface} 2> /dev/null")
end
ssh.exec!("sudo su -c 'cat /tmp/vagrant-network-entry >> /etc/network/interfaces'")
networks.each do |network|
ssh.exec!("sudo /sbin/ifup eth#{network[:adapter]}")
# Bring back up each network interface, reconfigured
interfaces.each do |interface|
ssh.exec!("sudo /sbin/ifup eth#{interface}")
end
end
end

View File

@ -0,0 +1,28 @@
module Vagrant
module Util
module NetworkIP
# Returns the network address of the given IP and subnet.
#
# @return [String]
def network_address(ip, subnet)
ip = ip_parts(ip)
netmask = ip_parts(subnet)
# Bitwise-AND each octet to get the network address
# in octets and join each part with a period to get
# the resulting network address.
ip.map { |part| part & netmask.shift }.join(".")
end
protected
# Splits an IP into the four octets and returns each as an
# integer in an array.
#
# @return [Array<Integer>]
def ip_parts(ip)
ip.split(".").map { |i| i.to_i }
end
end
end
end

View File

@ -1,7 +0,0 @@
<% networks.each do |network| -%>
#VAGRANT-BEGIN-BRIDGED
# The contents below are automatically generated by Vagrant. Do not modify.
auto eth<%= network[:adapter] %>
iface eth<%= network[:adapter] %> inet dhcp
#VAGRANT-END-BRIDGED
<% end -%>

View File

@ -0,0 +1,5 @@
#VAGRANT-BEGIN
# The contents below are automatically generated by Vagrant. Do not modify.
auto eth<%= options[:interface] %>
iface eth<%= options[:interface] %> inet dhcp
#VAGRANT-END

View File

@ -1,8 +0,0 @@
#VAGRANT-BEGIN-HOSTONLY
# The contents below are automatically generated by Vagrant.
# Please do not modify any of these contents.
auto eth<%= net_options[:adapter] %>
iface eth<%= net_options[:adapter] %> inet static
address <%= net_options[:ip] %>
netmask <%= net_options[:netmask] %>
#VAGRANT-END-HOSTONLY

View File

@ -0,0 +1,7 @@
#VAGRANT-BEGIN
# The contents below are automatically generated by Vagrant. Do not modify.
auto eth<%= options[:interface] %>
iface eth<%= options[:interface] %> inet static
address <%= options[:ip] %>
netmask <%= options[:netmask] %>
#VAGRANT-END

View File

@ -394,6 +394,11 @@ en:
to work properly (and hence port forwarding, SSH, etc.). Specifying this
MAC address is typically up to the box and box maintiner. Please contact
the relevant person to solve this issue.
network:
preparing: |-
Preparing network interfaces based on configuration...
configuring: |-
Configuring and enabling network interfaces...
host_only_network:
collides: |-
The specified host network collides with a non-hostonly network!

View File

@ -0,0 +1,17 @@
require File.expand_path("../../../base", __FILE__)
require "vagrant/util/network_ip"
describe Vagrant::Util::NetworkIP do
let(:klass) do
Class.new do
extend Vagrant::Util::NetworkIP
end
end
describe "network address" do
it "calculates it properly" do
klass.network_address("192.168.2.234", "255.255.255.0").should == "192.168.2.0"
end
end
end