From 7aa083d25924b1e32649f7656aad5476a027aec7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Aug 2012 21:12:41 -0700 Subject: [PATCH] `vagrant reload` now works with the new machine abstraction --- plugins/commands/reload/command.rb | 20 +- plugins/providers/virtualbox/action.rb | 55 +++ .../action/clear_forwarded_ports.rb | 18 + .../action/clear_network_interfaces.rb | 31 ++ .../virtualbox/action/clear_shared_folders.rb | 17 + .../providers/virtualbox/action/customize.rb | 36 ++ .../virtualbox/action/forward_ports.rb | 92 ++++ .../providers/virtualbox/action/host_name.rb | 21 + .../providers/virtualbox/action/network.rb | 404 ++++++++++++++++++ plugins/providers/virtualbox/action/nfs.rb | 185 ++++++++ .../providers/virtualbox/action/provision.rb | 62 +++ .../virtualbox/action/sane_defaults.rb | 75 ++++ .../virtualbox/action/share_folders.rb | 115 +++++ 13 files changed, 1118 insertions(+), 13 deletions(-) create mode 100644 plugins/providers/virtualbox/action/clear_forwarded_ports.rb create mode 100644 plugins/providers/virtualbox/action/clear_network_interfaces.rb create mode 100644 plugins/providers/virtualbox/action/clear_shared_folders.rb create mode 100644 plugins/providers/virtualbox/action/customize.rb create mode 100644 plugins/providers/virtualbox/action/forward_ports.rb create mode 100644 plugins/providers/virtualbox/action/host_name.rb create mode 100644 plugins/providers/virtualbox/action/network.rb create mode 100644 plugins/providers/virtualbox/action/nfs.rb create mode 100644 plugins/providers/virtualbox/action/provision.rb create mode 100644 plugins/providers/virtualbox/action/sane_defaults.rb create mode 100644 plugins/providers/virtualbox/action/share_folders.rb diff --git a/plugins/commands/reload/command.rb b/plugins/commands/reload/command.rb index 79f88eab0..718a45917 100644 --- a/plugins/commands/reload/command.rb +++ b/plugins/commands/reload/command.rb @@ -14,10 +14,10 @@ module VagrantPlugins def execute options = {} - opts = OptionParser.new do |opts| - opts.banner = "Usage: vagrant reload [vm-name]" - opts.separator "" - build_start_options(opts, options) + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant reload [vm-name]" + o.separator "" + build_start_options(o, options) end # Parse the options @@ -25,19 +25,13 @@ module VagrantPlugins return if !argv @logger.debug("'reload' each target VM...") - with_target_vms(argv) do |vm| - if vm.created? - @logger.info("Reloading: #{vm.name}") - vm.reload(options) - else - @logger.info("Not created: #{vm.name}. Not reloading.") - vm.ui.info I18n.t("vagrant.commands.common.vm_not_created") - end + with_target_vms(argv) do |machine| + machine.action(:reload) end # Success, exit status 0 0 - end + end end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 5d75e9e11..aa58b285f 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -10,17 +10,28 @@ module VagrantPlugins autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) + autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) + autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) + autoload :ClearSharedFolders, File.expand_path("../action/clear_shared_folders", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) + autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) autoload :DestroyConfirm, File.expand_path("../action/destroy_confirm", __FILE__) autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) + autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Halt, File.expand_path("../action/halt", __FILE__) + autoload :HostName, File.expand_path("../action/host_name", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) + autoload :Network, File.expand_path("../action/network", __FILE__) + autoload :NFS, File.expand_path("../action/nfs", __FILE__) + autoload :Provision, File.expand_path("../action/provision", __FILE__) autoload :ProvisionerCleanup, File.expand_path("../action/provisioner_cleanup", __FILE__) autoload :PruneNFSExports, File.expand_path("../action/prune_nfs_exports", __FILE__) autoload :Resume, File.expand_path("../action/resume", __FILE__) + autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) + autoload :ShareFolders, File.expand_path("../action/share_folders", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level @@ -74,6 +85,25 @@ module VagrantPlugins end end + # This action is responsible for reloading the machine, which + # brings it down, sucks in new configuration, and brings the + # machine back up with the new configuration. + def self.action_reload + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env1, b2| + if !env1[:result] + b2.use MessageNotCreated + next + end + + b2.use Vagrant::Action::General::Validate + b2.use action_halt + b2.use action_start + end + end + end + # This is the action that is primarily responsible for resuming # suspended machines. def self.action_resume @@ -113,6 +143,31 @@ module VagrantPlugins end end + # This action starts a VM, assuming it is already imported and exists. + # A precondition of this action is that the VM exists. + def self.action_start + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Vagrant::Action::General::Validate + b.use CheckAccessible + b.use CleanMachineFolder + b.use ClearForwardedPorts + b.use EnvSet, :port_collision_handler => :correct + b.use ForwardPorts + b.use Provision + b.use PruneNFSExports + b.use NFS + b.use ClearSharedFolders + b.use ShareFolders + b.use ClearNetworkInterfaces + b.use Network + b.use HostName + b.use SaneDefaults + b.use Customize + b.use Boot + end + end + # This is the action that is primarily responsible for suspending # the virtual machine. def self.action_suspend diff --git a/plugins/providers/virtualbox/action/clear_forwarded_ports.rb b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb new file mode 100644 index 000000000..c4e87610b --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_forwarded_ports.rb @@ -0,0 +1,18 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearForwardedPorts + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info I18n.t("vagrant.actions.vm.clear_forward_ports.deleting") + env[:machine].provider.driver.clear_forwarded_ports + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/clear_network_interfaces.rb b/plugins/providers/virtualbox/action/clear_network_interfaces.rb new file mode 100644 index 000000000..0098692f3 --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_network_interfaces.rb @@ -0,0 +1,31 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearNetworkInterfaces + def initialize(app, env) + @app = app + end + + def call(env) + # Create the adapters array to make all adapters nothing. + # We do adapters 2 to 8 because that is every built-in adapter + # excluding the NAT adapter on port 1 which Vagrant always + # expects to exist. + adapters = [] + 2.upto(8).each do |i| + adapters << { + :adapter => i, + :type => :none + } + end + + # "Enable" all the adapters we setup. + env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") + env[:machine].provider.driver.enable_adapters(adapters) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/clear_shared_folders.rb b/plugins/providers/virtualbox/action/clear_shared_folders.rb new file mode 100644 index 000000000..1aa617ff1 --- /dev/null +++ b/plugins/providers/virtualbox/action/clear_shared_folders.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ClearSharedFolders + def initialize(app, env) + @app = app + end + + def call(env) + env[:machine].provider.driver.clear_shared_folders + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/customize.rb b/plugins/providers/virtualbox/action/customize.rb new file mode 100644 index 000000000..ea699ac33 --- /dev/null +++ b/plugins/providers/virtualbox/action/customize.rb @@ -0,0 +1,36 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class Customize + def initialize(app, env) + @app = app + end + + def call(env) + customizations = env[:machine].config.vm.customizations + if !customizations.empty? + env[:ui].info I18n.t("vagrant.actions.vm.customize.running") + + # Execute each customization command. + customizations.each do |command| + processed_command = command.collect do |arg| + arg = env[:machine].id if arg == :id + arg.to_s + end + + result = env[:machine].provider.driver.execute_command(processed_command) + if result.exit_code != 0 + raise Errors::VMCustomizationFailed, { + :command => processed_command.inspect, + :error => result.stderr + } + end + end + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/forward_ports.rb b/plugins/providers/virtualbox/action/forward_ports.rb new file mode 100644 index 000000000..0afc71115 --- /dev/null +++ b/plugins/providers/virtualbox/action/forward_ports.rb @@ -0,0 +1,92 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class ForwardPorts + def initialize(app, env) + @app = app + end + + #-------------------------------------------------------------- + # Execution + #-------------------------------------------------------------- + def call(env) + @env = env + + # Get the ports we're forwarding + ports = forward_port_definitions + + # Warn if we're port forwarding to any privileged ports... + threshold_check(ports) + + env[:ui].info I18n.t("vagrant.actions.vm.forward_ports.forwarding") + forward_ports(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| + guest_port_mapping[options[:guestport]] = 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) + ports = [] + + interfaces = @env[:machine].provider.driver.read_network_interfaces + + mappings.each do |options| + message_attributes = { + :guest_port => options[:guestport], + :host_port => options[:hostport], + :adapter => options[:adapter] + } + + # Assuming the only reason to establish port forwarding is + # because the VM is using Virtualbox NAT networking. Host-only + # bridged networking don't require port-forwarding and establishing + # forwarded ports on these attachment types has uncertain behaviour. + @env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.forwarding_entry", + message_attributes)) + + # Port forwarding requires the network interface to be a NAT interface, + # so verify that that is the case. + if interfaces[options[: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]) + end + + if !ports.empty? + # We only need to forward ports if there are any to forward + @env[:machine].provider.driver.forward_ports(ports) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/host_name.rb b/plugins/providers/virtualbox/action/host_name.rb new file mode 100644 index 000000000..40cc934fe --- /dev/null +++ b/plugins/providers/virtualbox/action/host_name.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class HostName + def initialize(app, env) + @app = app + end + + def call(env) + @app.call(env) + + host_name = env[:machine].config.vm.host_name + if !host_name.nil? + env[:ui].info I18n.t("vagrant.actions.vm.host_name.setting") + env[:machine].guest.change_host_name(host_name) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb new file mode 100644 index 000000000..31a6fa38d --- /dev/null +++ b/plugins/providers/virtualbox/action/network.rb @@ -0,0 +1,404 @@ +require "set" + +require "log4r" + +require "vagrant/util/network_ip" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Network + # Utilities to deal with network addresses + include Vagrant::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[:machine].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) + network[:_auto_config] = true if 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 + @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. + @app.call(env) + + 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] } + 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, + :use_dhcp_assigned_default_route => false + }.merge(options) + end + + def bridged_adapter(config) + # Find the bridged interfaces that are available + bridgedifs = @env[:machine].provider.driver.read_bridged_interfaces + bridgedifs.delete_if { |interface| interface[:status] == "Down" } + + # The name of the chosen bridge interface will be assigned to this + # variable. + chosen_bridge = nil + + if config[:bridge] + @logger.debug("Bridge was directly specified in config, searching for: #{config[:bridge]}") + + # Search for a matching bridged interface + bridgedifs.each do |interface| + if interface[:name].downcase == config[:bridge].downcase + @logger.debug("Specific bridge found as configured in the Vagrantfile. Using it.") + chosen_bridge = interface[:name] + break + end + end + + # If one wasn't found, then we notify the user here. + if !chosen_bridge + @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.specific_not_found", + :bridge => config[:bridge]) + end + end + + # If we still don't have a bridge chosen (this means that one wasn't + # specified in the Vagrantfile, or the bridge specified in the Vagrantfile + # wasn't found), then we fall back to the normal means of searchign for a + # bridged network. + if !chosen_bridge + if bridgedifs.length == 1 + # One bridgable interface? Just use it. + chosen_bridge = bridgedifs[0][:name] + @logger.debug("Only one bridged interface available. Using it by default.") + else + # More than one bridgable interface requires a user decision, so + # show options to choose from. + @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 + + chosen_bridge = bridgedifs[choice - 1][:name] + end + end + + @logger.info("Bridging adapter #{config[:adapter]} to #{chosen_bridge}") + + # Given the choice we can now define the adapter we're using + return { + :adapter => config[:adapter], + :type => :bridged, + :bridge => chosen_bridge, + :mac_address => config[:mac], + :nic_type => config[:nic_type] + } + end + + def bridged_network_config(config) + return { + :type => :dhcp, + :use_dhcp_assigned_default_route => config[:use_dhcp_assigned_default_route] + } + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/nfs.rb b/plugins/providers/virtualbox/action/nfs.rb new file mode 100644 index 000000000..90eaae320 --- /dev/null +++ b/plugins/providers/virtualbox/action/nfs.rb @@ -0,0 +1,185 @@ +require 'digest/md5' +require 'fileutils' +require 'pathname' + +require 'log4r' + +module VagrantPlugins + module ProviderVirtualBox + module Action + class NFS + def initialize(app,env) + @logger = Log4r::Logger.new("vagrant::action::vm::nfs") + @app = app + @env = env + + verify_settings if nfs_enabled? + end + + def call(env) + @env = env + + extract_folders + + if !folders.empty? + prepare_folders + export_folders + end + + @app.call(env) + + mount_folders if !folders.empty? + end + + # Returns the folders which are to be synced via NFS. + def folders + @folders ||= {} + end + + # Removes the NFS enabled shared folders from the configuration, + # so they will no longer be mounted by the actual shared folder + # task. + def extract_folders + # Load the NFS enabled shared folders + @folders = {} + @env[:machine].config.vm.shared_folders.each do |key, opts| + if opts[:nfs] + # Duplicate the options, set the hostpath, and set disabled on the original + # options so the ShareFolders middleware doesn't try to mount it. + folder = opts.dup + hostpath = Pathname.new(opts[:hostpath]).expand_path(@env[:root_path]) + + if !hostpath.directory? && opts[:create] + # Host path doesn't exist, so let's create it. + @logger.debug("Host path doesn't exist, creating: #{hostpath}") + + begin + FileUtils.mkpath(hostpath) + rescue Errno::EACCES + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s + end + end + + # Set the hostpath now that it exists. + folder[:hostpath] = hostpath.to_s + + # Assign the folder to our instance variable for later use + @folders[key] = folder + + # Disable the folder so that regular shared folders don't try to + # mount it. + opts[:disabled] = true + end + end + end + + # Prepares the settings for the NFS folders, such as setting the + # options on the NFS folders. + def prepare_folders + @folders = @folders.inject({}) do |acc, data| + key, opts = data + opts[:map_uid] = prepare_permission(:uid, opts) + opts[:map_gid] = prepare_permission(:gid, opts) + opts[:nfs_version] ||= 3 + + # The poor man's UUID. An MD5 hash here is sufficient since + # we need a 32 character "uuid" to represent the filesystem + # of an export. Hashing the host path is safe because two of + # the same host path will hash to the same fsid. + opts[:uuid] = Digest::MD5.hexdigest(opts[:hostpath]) + + acc[key] = opts + acc + end + end + + # Prepares the UID/GID settings for a single folder. + def prepare_permission(perm, opts) + key = "map_#{perm}".to_sym + return nil if opts.has_key?(key) && opts[key].nil? + + # The options on the hash get priority, then the default + # values + value = opts.has_key?(key) ? opts[key] : @env[:machine].config.nfs.send(key) + return value if value != :auto + + # Get UID/GID from folder if we've made it this far + # (value == :auto) + stat = File.stat(opts[:hostpath]) + return stat.send(perm) + end + + # Uses the host class to export the folders via NFS. This typically + # involves adding a line to `/etc/exports` for this VM, but it is + # up to the host class to define the specific behavior. + def export_folders + @env[:ui].info I18n.t("vagrant.actions.vm.nfs.exporting") + @env[:host].nfs_export(@env[:machine].id, guest_ip, folders) + end + + # Uses the system class to mount the NFS folders. + def mount_folders + @env[:ui].info I18n.t("vagrant.actions.vm.nfs.mounting") + + # Only mount the folders which have a guest path specified + mount_folders = {} + folders.each do |name, opts| + if opts[:guestpath] + mount_folders[name] = opts.dup + end + end + + @env[:machine].guest.mount_nfs(host_ip, mount_folders) + end + + # Returns the IP address of the first host only network adapter + # + # @return [String] + def host_ip + @env[:machine].provider.driver.read_network_interfaces.each do |adapter, opts| + if opts[:type] == :hostonly + @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| + if interface[:name] == opts[:hostonly] + return interface[:ip] + end + end + end + end + + nil + end + + # Returns the IP address of the guest by looking at the first + # enabled host only network. + # + # @return [String] + def guest_ip + @env[:machine].config.vm.networks.each do |type, args| + if type == :hostonly && args[0].is_a?(String) + return args[0] + end + end + + nil + end + + # Checks if there are any NFS enabled shared folders. + def nfs_enabled? + @env[:machine].config.vm.shared_folders.each do |key, opts| + return true if opts[:nfs] + end + + false + end + + # Verifies that the host is set and supports NFS. + def verify_settings + raise Vagrant::Errors::NFSHostRequired if @env[:host].nil? + raise Vagrant::Errors::NFSNotSupported if !@env[:host].nfs? + raise Vagrant::Errors::NFSNoHostNetwork if !guest_ip + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/provision.rb b/plugins/providers/virtualbox/action/provision.rb new file mode 100644 index 000000000..d2304c609 --- /dev/null +++ b/plugins/providers/virtualbox/action/provision.rb @@ -0,0 +1,62 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class Provision + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::provision") + @app = app + + env["provision.enabled"] = true if !env.has_key?("provision.enabled") + end + + def call(env) + @env = env + + provisioners = nil + + # We set this here so that even if this value is changed in the future, + # it stays constant to what we expect here in this moment. + enabled = env["provision.enabled"] + + # Instantiate and prepare the provisioners. Preparation must happen here + # so that shared folders and such can properly take effect. + provisioners = enabled_provisioners + provisioners.map { |p| p.prepare } + + @app.call(env) + + if enabled + # Take prepared provisioners and run the provisioning + provisioners.each do |instance| + @env[:ui].info I18n.t("vagrant.actions.vm.provision.beginning", + :provisioner => instance.class) + instance.provision! + end + end + end + + def enabled_provisioners + enabled = [] + @env[:machine].config.vm.provisioners.each do |provisioner| + if @env["provision.types"] + # If we've specified types of provisioners to enable, then we + # only use those provisioners, and skip any that we haven't + # specified. + if !@env["provision.types"].include?(provisioner.shortcut.to_s) + @logger.debug("Skipping provisioner: #{provisioner.shortcut}") + next + end + end + + enabled << provisioner.provisioner.new(@env, provisioner.config) + end + + # Return the enable provisioners + enabled + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/sane_defaults.rb b/plugins/providers/virtualbox/action/sane_defaults.rb new file mode 100644 index 000000000..21d8e9055 --- /dev/null +++ b/plugins/providers/virtualbox/action/sane_defaults.rb @@ -0,0 +1,75 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class SaneDefaults + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::sanedefaults") + @app = app + end + + def call(env) + # Set the env on an instance variable so we can access it in + # helpers. + @env = env + + # Enable the host IO cache on the sata controller. Note that + # if this fails then its not a big deal, so we don't raise any + # errors. The Host IO cache vastly improves disk IO performance + # for VMs. + command = [ + "storagectl", env[:machine].id, + "--name", "SATA Controller", + "--hostiocache", "on" + ] + attempt_and_log(command, "Enabling the Host I/O cache on the SATA controller...") + + enable_dns_proxy = true + begin + contents = File.read("/etc/resolv.conf") + + if contents =~ /^nameserver 127\.0\.0\.1$/ + # The use of both natdnsproxy and natdnshostresolver break on + # Ubuntu 12.04 that uses resolvconf with localhost. When used + # VirtualBox will give the client dns server 10.0.2.3, while + # not binding to that address itself. Therefore disable this + # feature if host uses the resolvconf server 127.0.0.1 + @logger.info("Disabling DNS proxy since resolv.conf contains 127.0.0.1") + enable_dns_proxy = false + end + rescue Errno::ENOENT; end + + # Enable/disable the NAT DNS proxy as necessary + if enable_dns_proxy + command = [ + "modifyvm", env[:machine].id, + "--natdnsproxy1", "on" + ] + attempt_and_log(command, "Enable the NAT DNS proxy on adapter 1...") + else + command = [ "modifyvm", env[:machine].id, "--natdnsproxy1", "off" ] + attempt_and_log(command, "Disable the NAT DNS proxy on adapter 1...") + command = [ "modifyvm", env[:machine].id, "--natdnshostresolver1", "off" ] + attempt_and_log(command, "Disable the NAT DNS resolver on adapter 1...") + end + + @app.call(env) + end + + protected + + # This is just a helper method that executes a single command, logs + # the given string to the log, and also includes the exit status in + # the log message. + # + # @param [Array] command Command to run + # @param [String] log Log message to write. + def attempt_and_log(command, log) + result = @env[:machine].provider.driver.execute_command(command) + @logger.info("#{log} (exit status = #{result.exit_code})") + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/share_folders.rb b/plugins/providers/virtualbox/action/share_folders.rb new file mode 100644 index 000000000..a68beb926 --- /dev/null +++ b/plugins/providers/virtualbox/action/share_folders.rb @@ -0,0 +1,115 @@ +require "pathname" + +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class ShareFolders + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::action::vm::share_folders") + @app = app + end + + def call(env) + @env = env + + prepare_folders + create_metadata + + @app.call(env) + + mount_shared_folders + end + + # This method returns an actual list of VirtualBox shared + # folders to create and their proper path. + def shared_folders + @env[:machine].config.vm.shared_folders.inject({}) do |acc, data| + key, value = data + + next acc if value[:disabled] + + # This to prevent overwriting the actual shared folders data + value = value.dup + acc[key] = value + acc + end + end + + # Prepares the shared folders by verifying they exist and creating them + # if they don't. + def prepare_folders + shared_folders.each do |name, options| + hostpath = Pathname.new(options[:hostpath]).expand_path(@env[:root_path]) + + if !hostpath.directory? && options[:create] + # Host path doesn't exist, so let's create it. + @logger.debug("Host path doesn't exist, creating: #{hostpath}") + + begin + hostpath.mkpath + rescue Errno::EACCES + raise Vagrant::Errors::SharedFolderCreateFailed, + :path => hostpath.to_s + end + end + end + end + + def create_metadata + @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.creating") + + folders = [] + shared_folders.each do |name, data| + folders << { + :name => name, + :hostpath => File.expand_path(data[:hostpath], @env[:root_path]), + :transient => data[:transient] + } + end + + @env[:machine].provider.driver.share_folders(folders) + end + + def mount_shared_folders + @env[:ui].info I18n.t("vagrant.actions.vm.share_folders.mounting") + + # short guestpaths first, so we don't step on ourselves + folders = shared_folders.sort_by do |name, data| + if data[:guestpath] + data[:guestpath].length + else + # A long enough path to just do this at the end. + 10000 + end + end + + # Go through each folder and mount + folders.each do |name, data| + if data[:guestpath] + # Guest path specified, so mount the folder to specified point + @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", + :name => name, + :guest_path => data[:guestpath])) + + # Dup the data so we can pass it to the guest API + data = data.dup + + # Calculate the owner and group + data[:owner] ||= @env[:machine].config.ssh.username + data[:group] ||= @env[:machine].config.ssh.username + + # Mount the actual folder + @env[:machine].guest.mount_shared_folder(name, data[:guestpath], data) + else + # If no guest path is specified, then automounting is disabled + @env[:ui].info(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", + :name => name)) + end + end + end + end + end + end +end