diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index c0eaffcc8..432147691 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -12,6 +12,7 @@ module Vagrant autoload :BoxCheckOutdated, "vagrant/action/builtin/box_check_outdated" autoload :BoxRemove, "vagrant/action/builtin/box_remove" autoload :Call, "vagrant/action/builtin/call" + autoload :CleanupDisks, "vagrant/action/builtin/cleanup_disks" autoload :Confirm, "vagrant/action/builtin/confirm" autoload :ConfigValidate, "vagrant/action/builtin/config_validate" autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm" diff --git a/lib/vagrant/action/builtin/cleanup_disks.rb b/lib/vagrant/action/builtin/cleanup_disks.rb new file mode 100644 index 000000000..27b2f887a --- /dev/null +++ b/lib/vagrant/action/builtin/cleanup_disks.rb @@ -0,0 +1,56 @@ +require "json" + +module Vagrant + module Action + module Builtin + class CleanupDisks + # Removes any attached disks no longer defined in a Vagrantfile config + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::disk") + end + + def call(env) + machine = env[:machine] + defined_disks = get_disks(machine, env) + + # Call into providers machine implementation for disk management + disk_meta_file = read_disk_metadata(machine) + + if !disk_meta_file.empty? + if machine.provider.capability?(:cleanup_disks) + machine.provider.capability(:cleanup_disks, defined_disks, disk_meta_file) + else + env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported", + provider: machine.provider_name)) + end + end + + # Continue On + @app.call(env) + end + + def read_disk_metadata(machine) + meta_file = machine.data_dir.join("disk_meta") + if File.file?(meta_file) + disk_meta = JSON.parse(meta_file.read) + else + @logger.info("No previous disk_meta file defined for guest #{machine.name}") + disk_meta = {} + end + + return disk_meta + end + + def get_disks(machine, env) + return @_disks if @_disks + + @_disks = [] + @_disks = machine.config.vm.disks + + @_disks + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/disk.rb b/lib/vagrant/action/builtin/disk.rb index cf0ac6a80..8fc776f95 100644 --- a/lib/vagrant/action/builtin/disk.rb +++ b/lib/vagrant/action/builtin/disk.rb @@ -1,3 +1,5 @@ +require "json" + module Vagrant module Action module Builtin @@ -12,19 +14,30 @@ module Vagrant defined_disks = get_disks(machine, env) # Call into providers machine implementation for disk management + configured_disks = {} if !defined_disks.empty? if machine.provider.capability?(:configure_disks) - machine.provider.capability(:configure_disks, defined_disks) + configured_disks = machine.provider.capability(:configure_disks, defined_disks) else env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported", provider: machine.provider_name)) end end + write_disk_metadata(machine, configured_disks) unless configured_disks.empty? + # Continue On @app.call(env) end + def write_disk_metadata(machine, current_disks) + meta_file = machine.data_dir.join("disk_meta") + @logger.debug("Writing disk metadata file to #{meta_file}") + File.open(meta_file.to_s, "w+") do |file| + file.write(JSON.dump(current_disks)) + end + end + def get_disks(machine, env) return @_disks if @_disks diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 90e5c42c1..ab7584eed 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -904,6 +904,10 @@ module Vagrant error_key(:virtualbox_broken_version_040214) end + class VirtualBoxDisksDefinedExceedLimit < VagrantError + error_key(:virtualbox_disks_defined_exceed_limit) + end + class VirtualBoxGuestPropertyNotFound < VagrantError error_key(:virtualbox_guest_property_not_found) end diff --git a/lib/vagrant/util/numeric.rb b/lib/vagrant/util/numeric.rb index 3a2989d97..93346b81d 100644 --- a/lib/vagrant/util/numeric.rb +++ b/lib/vagrant/util/numeric.rb @@ -49,6 +49,14 @@ module Vagrant bytes end + # Rounds actual value to two decimal places + # + # @param [Integer] bytes + # @return [Integer] megabytes - bytes representation in megabytes + def bytes_to_megabytes(bytes) + (bytes / MEGABYTE.to_f).round(2) + end + # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. diff --git a/plugins/kernel_v2/config/disk.rb b/plugins/kernel_v2/config/disk.rb index 219a74aed..5c3daf44f 100644 --- a/plugins/kernel_v2/config/disk.rb +++ b/plugins/kernel_v2/config/disk.rb @@ -29,6 +29,11 @@ module VagrantPlugins # @return [Symbol] attr_accessor :type + # Type of disk extension to create. Defaults to `vdi` + # + # @return [String] + attr_accessor :disk_ext + # Size of disk to create # # @return [Integer,String] @@ -61,6 +66,7 @@ module VagrantPlugins @size = UNSET_VALUE @primary = UNSET_VALUE @file = UNSET_VALUE + @disk_ext = UNSET_VALUE # Internal options @id = SecureRandom.uuid @@ -101,6 +107,8 @@ module VagrantPlugins @size = nil if @size == UNSET_VALUE @file = nil if @file == UNSET_VALUE + @disk_ext = "vdi" if @disk_ext == UNSET_VALUE + if @primary == UNSET_VALUE @primary = false end @@ -109,7 +117,7 @@ module VagrantPlugins if @primary @name = "vagrant_primary" else - @name = "name_#{@type.to_s}_#{@id.split("-").last}" + @name = nil end end @@ -127,6 +135,25 @@ module VagrantPlugins types: DEFAULT_DISK_TYPES.join(', ')) end + if @disk_ext + @disk_ext = @disk_ext.downcase + + if machine.provider.capability?(:validate_disk_ext) + if !machine.provider.capability(:validate_disk_ext, @disk_ext) + if machine.provider.capability?(:get_default_disk_ext) + disk_exts = machine.provider.capability(:get_default_disk_ext).join(', ') + else + disk_exts = "not found" + end + errors << I18n.t("vagrant.config.disk.invalid_ext", ext: @disk_ext, + name: @name, + exts: disk_exts) + end + else + @logger.warn("No provider capability defined to validate 'disk_ext' type") + end + end + if @size && !@size.is_a?(Integer) if @size.is_a?(String) @size = Vagrant::Util::Numeric.string_to_bytes(@size) @@ -154,6 +181,10 @@ module VagrantPlugins end end + if !@name + errors << I18n.t("vagrant.config.disk.no_name_set", machine: machine.name) + end + errors end diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 7abd9ee4b..744649a12 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -433,8 +433,8 @@ module VagrantPlugins # Add provider config disk_config.add_provider_config(provider_options, &block) - if !Vagrant::Util::Experimental.feature_enabled?("disk_base_config") - @logger.warn("Disk config defined, but experimental feature is not enabled. To use this feature, enable it with the experimental flag `disk_base_config`. Disk will not be added to internal config, and will be ignored.") + if !Vagrant::Util::Experimental.feature_enabled?("disks") + @logger.warn("Disk config defined, but experimental feature is not enabled. To use this feature, enable it with the experimental flag `disks`. Disk will not be added to internal config, and will be ignored.") return end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index edfbbcbe0..00bfe09db 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -79,6 +79,7 @@ module VagrantPlugins b.use ForwardPorts b.use SetHostname b.use SaneDefaults + b.use CleanupDisks b.use Disk b.use Customize, "pre-boot" b.use Boot diff --git a/plugins/providers/virtualbox/cap/cleanup_disks.rb b/plugins/providers/virtualbox/cap/cleanup_disks.rb new file mode 100644 index 000000000..d9e11f7ff --- /dev/null +++ b/plugins/providers/virtualbox/cap/cleanup_disks.rb @@ -0,0 +1,54 @@ +require "log4r" +require "vagrant/util/experimental" + +module VagrantPlugins + module ProviderVirtualBox + module Cap + module CleanupDisks + LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::cleanup_disks") + + # @param [Vagrant::Machine] machine + # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks + # @param [Hash] disk_meta_file - A hash of all the previously defined disks from the last configure_disk action + def self.cleanup_disks(machine, defined_disks, disk_meta_file) + return if disk_meta_file.values.flatten.empty? + + return if !Vagrant::Util::Experimental.feature_enabled?("disks") + + handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) + # TODO: Floppy and DVD disks + end + + protected + + # @param [Vagrant::Machine] machine + # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks + # @param [Hash] disk_meta - A hash of all the previously defined disks from the last configure_disk action + def self.handle_cleanup_disk(machine, defined_disks, disk_meta) + vm_info = machine.provider.driver.show_vm_info + primary_disk = vm_info["SATA Controller-ImageUUID-0-0"] + + disk_meta.each do |d| + dsk = defined_disks.select { |dk| dk.name == d["name"] } + if !dsk.empty? || d["uuid"] == primary_disk + next + else + LOGGER.warn("Found disk not in Vagrantfile config: '#{d["name"]}'. Removing disk from guest #{machine.name}") + disk_info = machine.provider.driver.get_port_and_device(d["uuid"]) + + machine.ui.warn("Disk '#{d["name"]}' no longer exists in Vagrant config. Removing and closing medium from guest...", prefix: true) + + if disk_info.empty? + LOGGER.warn("Disk '#{d["name"]}' not attached to guest, but still exists.") + else + machine.provider.driver.remove_disk(disk_info[:port], disk_info[:device]) + end + + machine.provider.driver.close_medium(d["uuid"]) + end + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/cap/configure_disks.rb b/plugins/providers/virtualbox/cap/configure_disks.rb new file mode 100644 index 000000000..5d6fe5f51 --- /dev/null +++ b/plugins/providers/virtualbox/cap/configure_disks.rb @@ -0,0 +1,287 @@ +require "log4r" +require "fileutils" +require "vagrant/util/numeric" +require "vagrant/util/experimental" + +module VagrantPlugins + module ProviderVirtualBox + module Cap + module ConfigureDisks + LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::configure_disks") + + # The max amount of disks that can be attached to a single device in a controller + MAX_DISK_NUMBER = 30.freeze + + # @param [Vagrant::Machine] machine + # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks + # @return [Hash] configured_disks - A hash of all the current configured disks + def self.configure_disks(machine, defined_disks) + return {} if defined_disks.empty? + + return {} if !Vagrant::Util::Experimental.feature_enabled?("disks") + + if defined_disks.size > MAX_DISK_NUMBER + # you can only attach up to 30 disks per controller, INCLUDING the primary disk + raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit + end + + machine.ui.info(I18n.t("vagrant.cap.configure_disks.start")) + + current_disks = machine.provider.driver.list_hdds + + configured_disks = {disk: [], floppy: [], dvd: []} + + defined_disks.each do |disk| + if disk.type == :disk + disk_data = handle_configure_disk(machine, disk, current_disks) + configured_disks[:disk] << disk_data unless disk_data.empty? + elsif disk.type == :floppy + # TODO: Write me + machine.ui.info(I18n.t("vagrant.cap.configure_disks.floppy_not_supported", name: disk.name)) + elsif disk.type == :dvd + # TODO: Write me + machine.ui.info(I18n.t("vagrant.cap.configure_disks.dvd_not_supported", name: disk.name)) + end + end + + configured_disks + end + + protected + + # @param [Vagrant::Machine] machine - the current machine + # @param [Config::Disk] disk - the current disk to configure + # @param [Array] all_disks - A list of all currently defined disks in VirtualBox + # @return [Hash] current_disk - Returns the current disk. Returns nil if it doesn't exist + def self.get_current_disk(machine, disk, all_disks) + current_disk = nil + if disk.primary + # Ensure we grab the proper primary disk + # We can't rely on the order of `all_disks`, as they will not + # always come in port order, but primary is always Port 0 Device 0. + vm_info = machine.provider.driver.show_vm_info + primary_uuid = vm_info["SATA Controller-ImageUUID-0-0"] + + current_disk = all_disks.select { |d| d["UUID"] == primary_uuid }.first + else + current_disk = all_disks.select { |d| d["Disk Name"] == disk.name}.first + end + + current_disk + end + + # Handles all disk configs of type `:disk` + # + # @param [Vagrant::Machine] machine - the current machine + # @param [Config::Disk] disk - the current disk to configure + # @param [Array] all_disks - A list of all currently defined disks in VirtualBox + # @return [Hash] - disk_metadata + def self.handle_configure_disk(machine, disk, all_disks) + disk_metadata = {} + + # Grab the existing configured disk, if it exists + current_disk = get_current_disk(machine, disk, all_disks) + + # Configure current disk + if !current_disk + # create new disk and attach + disk_metadata = create_disk(machine, disk) + elsif compare_disk_size(machine, disk, current_disk) + disk_metadata = resize_disk(machine, disk, current_disk) + else + # TODO: What if it needs to be resized? + + disk_info = machine.provider.driver.get_port_and_device(current_disk["UUID"]) + if disk_info.empty? + LOGGER.warn("Disk '#{disk.name}' is not connected to guest '#{machine.name}', Vagrant will attempt to connect disk to guest") + dsk_info = get_next_port(machine) + machine.provider.driver.attach_disk(dsk_info[:port], + dsk_info[:device], + current_disk["Location"]) + else + LOGGER.info("No further configuration required for disk '#{disk.name}'") + end + + disk_metadata = {uuid: current_disk["UUID"], name: disk.name} + end + + disk_metadata + end + + # Check to see if current disk is configured based on defined_disks + # + # @param [Kernel_V2::VagrantConfigDisk] disk_config + # @param [Hash] defined_disk + # @return [Boolean] + def self.compare_disk_size(machine, disk_config, defined_disk) + requested_disk_size = Vagrant::Util::Numeric.bytes_to_megabytes(disk_config.size) + defined_disk_size = defined_disk["Capacity"].split(" ").first.to_f + + if defined_disk_size > requested_disk_size + machine.ui.warn(I18n.t("vagrant.cap.configure_disks.shrink_size_not_supported", name: disk_config.name)) + return false + elsif defined_disk_size < requested_disk_size + return true + else + return false + end + end + + # Creates and attaches a disk to a machine + # + # @param [Vagrant::Machine] machine + # @param [Kernel_V2::VagrantConfigDisk] disk_config + def self.create_disk(machine, disk_config) + machine.ui.detail(I18n.t("vagrant.cap.configure_disks.create_disk", name: disk_config.name)) + # NOTE: At the moment, there are no provider specific configs for VirtualBox + # but we grab it anyway for future use. + disk_provider_config = disk_config.provider_config[:virtualbox] if disk_config.provider_config + + guest_info = machine.provider.driver.show_vm_info + guest_folder = File.dirname(guest_info["CfgFile"]) + + disk_ext = disk_config.disk_ext + disk_file = File.join(guest_folder, disk_config.name) + ".#{disk_ext}" + + LOGGER.info("Attempting to create a new disk file '#{disk_file}' of size '#{disk_config.size}' bytes") + + disk_var = machine.provider.driver.create_disk(disk_file, disk_config.size, disk_ext.upcase) + disk_metadata = {uuid: disk_var.split(':').last.strip, name: disk_config.name} + + dsk_controller_info = get_next_port(machine) + machine.provider.driver.attach_disk(dsk_controller_info[:port], dsk_controller_info[:device], disk_file) + + disk_metadata + end + + # Finds the next available port + # + # SATA Controller-ImageUUID-0-0 (sub out ImageUUID) + # - Controller: SATA Controller + # - Port: 0 + # - Device: 0 + # + # Note: Virtualbox returns the string above with the port and device info + # disk_info = key.split("-") + # port = disk_info[2] + # device = disk_info[3] + # + # @param [Vagrant::Machine] machine + # @return [Hash] dsk_info - The next available port and device on a given controller + def self.get_next_port(machine) + vm_info = machine.provider.driver.show_vm_info + dsk_info = {device: "0", port: "0"} + + disk_images = vm_info.select { |v| v.include?("ImageUUID") && v.include?("SATA Controller") } + used_ports = disk_images.keys.map { |k| k.split('-') }.map {|v| v[2].to_i} + next_available_port = ((0..(MAX_DISK_NUMBER-1)).to_a - used_ports).first + + dsk_info[:port] = next_available_port.to_s + if dsk_info[:port].empty? + # This likely only occurs if additional disks have been added outside of Vagrant configuration + LOGGER.warn("There are no more available ports to attach disks to for the SATA Controller. Clear up some space on the SATA controller to attach new disks.") + raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit + end + + dsk_info + end + + # @param [Vagrant::Machine] machine + # @param [Config::Disk] disk_config - the current disk to configure + # @param [Hash] defined_disk - current disk as represented by VirtualBox + # @return [Hash] - disk_metadata + def self.resize_disk(machine, disk_config, defined_disk) + machine.ui.detail(I18n.t("vagrant.cap.configure_disks.resize_disk", name: disk_config.name), prefix: true) + + if defined_disk["Storage format"] == "VMDK" + LOGGER.warn("Disk type VMDK cannot be resized in VirtualBox. Vagrant will convert disk to VDI format to resize first, and then convert resized disk back to VMDK format") + + # grab disk to be resized port and device number + disk_info = machine.provider.driver.get_port_and_device(defined_disk["UUID"]) + # original disk information in case anything goes wrong during clone/resize + original_disk = defined_disk + backup_disk_location = "#{original_disk["Location"]}.backup" + + # clone disk to vdi formatted disk + vdi_disk_file = machine.provider.driver.vmdk_to_vdi(defined_disk["Location"]) + # resize vdi + machine.provider.driver.resize_disk(vdi_disk_file, disk_config.size.to_i) + + begin + # Danger Zone + + # remove and close original volume + machine.provider.driver.remove_disk(disk_info[:port], disk_info[:device]) + # Create a backup of the original disk if something goes wrong + LOGGER.warn("Making a backup of the original disk at #{defined_disk["Location"]}") + FileUtils.mv(defined_disk["Location"], backup_disk_location) + + # we have to close here, otherwise we can't re-clone after + # resizing the vdi disk + machine.provider.driver.close_medium(defined_disk["UUID"]) + + # clone back to original vmdk format and attach resized disk + vmdk_disk_file = machine.provider.driver.vdi_to_vmdk(vdi_disk_file) + machine.provider.driver.attach_disk(disk_info[:port], disk_info[:device], vmdk_disk_file, "hdd") + rescue ScriptError, SignalException, StandardError + LOGGER.warn("Vagrant encountered an error while trying to resize a disk. Vagrant will now attempt to reattach and preserve the original disk...") + machine.ui.error(I18n.t("vagrant.cap.configure_disks.recovery_from_resize", + location: original_disk["Location"], + name: machine.name)) + recover_from_resize(machine, disk_info, backup_disk_location, original_disk, vdi_disk_file) + + raise + ensure + # Remove backup disk file if all goes well + FileUtils.remove(backup_disk_location, force: true) + end + + # Remove cloned resized volume format + machine.provider.driver.close_medium(vdi_disk_file) + + # Get new updated disk UUID for vagrant disk_meta file + new_disk_info = machine.provider.driver.list_hdds.select { |h| h["Location"] == defined_disk["Location"] }.first + defined_disk = new_disk_info + else + machine.provider.driver.resize_disk(defined_disk["Location"], disk_config.size.to_i) + end + + disk_metadata = {uuid: defined_disk["UUID"], name: disk_config.name} + + disk_metadata + end + + # Recovery method for when an exception occurs during the process of resizing disks + # + # It attempts to move back the backup disk into place, and reattach it to the guest before + # raising the original error + # + # @param [Vagrant::Machine] machine + # @param [Hash] disk_info - The disk device and port number to attach back to + # @param [String] backup_disk_location - The place on disk where vagrant made a backup of the original disk being resized + # @param [Hash] original_disk - The disk information from VirtualBox + # @param [String] vdi_disk_file - The place on disk where vagrant made a clone of the original disk being resized + def self.recover_from_resize(machine, disk_info, backup_disk_location, original_disk, vdi_disk_file) + begin + # move backup to original name + FileUtils.mv(backup_disk_location, original_disk["Location"], force: true) + # Attach disk + machine.provider.driver. + attach_disk(disk_info[:port], disk_info[:device], original_disk["Location"], "hdd") + + # Remove cloned disk if still hanging around + if vdi_disk_file + machine.provider.driver.close_medium(vdi_disk_file) + end + + # We recovered! + machine.ui.warn(I18n.t("vagrant.cap.configure_disks.recovery_attached_disks")) + rescue => e + LOGGER.error("Vagrant encountered an error while trying to recover. It will now show the original error and continue...") + LOGGER.error(e) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/cap/validate_disk_ext.rb b/plugins/providers/virtualbox/cap/validate_disk_ext.rb new file mode 100644 index 000000000..e820c39ab --- /dev/null +++ b/plugins/providers/virtualbox/cap/validate_disk_ext.rb @@ -0,0 +1,27 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Cap + module ValidateDiskExt + LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::validate_disk_ext") + + # The default set of disk formats that VirtualBox supports + DEFAULT_DISK_EXT = ["vdi", "vmdk", "vhd"].map(&:freeze).freeze + + # @param [Vagrant::Machine] machine + # @param [String] disk_ext + # @return [Bool] + def self.validate_disk_ext(machine, disk_ext) + DEFAULT_DISK_EXT.include?(disk_ext) + end + + # @param [Vagrant::Machine] machine + # @return [Array] + def self.get_default_disk_ext(machine) + DEFAULT_DISK_EXT + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 8f07c4bac..c2f79e40b 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -368,6 +368,21 @@ module VagrantPlugins def vm_exists?(uuid) end + # Returns a hash of information about a given virtual machine + # + # @param [String] uuid + # @return [Hash] info + def show_vm_info + info = {} + execute('showvminfo', @uuid, '--machinereadable', retryable: true).split("\n").each do |line| + parts = line.partition('=') + key = parts.first.gsub('"', '') + value = parts.last.gsub('"', '') + info[key] = value + end + info + end + # Execute the given subcommand for VBoxManage and return the output. def execute(*command, &block) # Get the options hash if it exists diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index ed013737b..7175112ba 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -97,10 +97,15 @@ module VagrantPlugins end end - def_delegators :@driver, :clear_forwarded_ports, + def_delegators :@driver, + :attach_disk, + :clear_forwarded_ports, :clear_shared_folders, + :clone_disk, :clonevm, + :close_medium, :create_dhcp_server, + :create_disk, :create_host_only_network, :create_snapshot, :delete, @@ -111,9 +116,11 @@ module VagrantPlugins :execute_command, :export, :forward_ports, + :get_port_and_device, :halt, :import, :list_snapshots, + :list_hdds, :read_forwarded_ports, :read_bridged_interfaces, :read_dhcp_servers, @@ -130,6 +137,8 @@ module VagrantPlugins :read_vms, :reconfig_host_only, :remove_dhcp_server, + :remove_disk, + :resize_disk, :restore_snapshot, :resume, :set_mac_address, @@ -138,9 +147,11 @@ module VagrantPlugins :ssh_port, :start, :suspend, + :vdi_to_vmdk, :verify!, :verify_image, - :vm_exists? + :vm_exists?, + :vmdk_to_vdi protected diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index aff0607ee..7220bb5d9 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -16,6 +16,28 @@ module VagrantPlugins @uuid = uuid end + # Controller-Port-Device looks like: + # SATA Controller-ImageUUID-0-0 (sub out ImageUUID) + # - Controller: SATA Controller + # - Port: 0 + # - Device: 0 + # + # @param [String] port - port on device to attach disk to + # @param [String] device - device on controller for disk + # @param [String] file - disk file path + # @param [String] type - type of disk to attach + # @param [Hash] opts - additional options + def attach_disk(port, device, file, type="hdd", **opts) + # Maybe only support SATA Controller for `:disk`??? + controller = "SATA Controller" + + comment = "This disk is managed externally by Vagrant. Removing or adjusting settings could potentially cause issues with Vagrant." + + execute('storageattach', @uuid, '--storagectl', controller, '--port', + port.to_s, '--device', device.to_s, '--type', type, '--medium', + file, '--comment', comment) + end + def clear_forwarded_ports retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do args = [] @@ -38,6 +60,13 @@ module VagrantPlugins end end + # @param [String] source + # @param [String] destination + # @param [String] disk_format + def clone_disk(source, destination, disk_format, **opts) + execute("clonemedium", source, destination, '--format', disk_format) + end + def clonevm(master_id, snapshot_name) machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" args = ["--register", "--name", machine_name] @@ -49,6 +78,14 @@ module VagrantPlugins return get_machine_id(machine_name) end + # Removes a disk from the given virtual machine + # + # @param [String] disk_uuid or file path + # @param [Hash] opts - additional options + def close_medium(disk_uuid) + execute("closemedium", disk_uuid, '--delete') + end + def create_dhcp_server(network, options) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin @@ -65,6 +102,17 @@ module VagrantPlugins end end + # Creates a disk. Default format is VDI unless overridden + # + # @param [String] disk_file + # @param [Integer] disk_size - size in bytes + # @param [String] disk_format - format of disk, defaults to "VDI" + # @param [Hash] opts - additional options + def create_disk(disk_file, disk_size, disk_format="VDI", **opts) + execute("createmedium", '--filename', disk_file, '--sizebyte', disk_size.to_i.to_s, '--format', disk_format) + end + + def create_host_only_network(options) # Create the interface execute("hostonlyif", "create", retryable: true) =~ /^Interface '(.+?)' was successfully created$/ @@ -130,6 +178,33 @@ module VagrantPlugins end end + # Lists all attached harddisks from a given virtual machine. Additionally, + # this method adds a new key "Disk Name" based on the disks file path from "Location" + # + # @return [Array] hdds An array of hashes of harddrive info for a guest + def list_hdds + hdds = [] + tmp_drive = {} + execute('list', 'hdds', retryable: true).split("\n").each do |line| + if line == "" # separator between disks + hdds << tmp_drive + tmp_drive = {} + next + end + parts = line.partition(":") + key = parts.first.strip + value = parts.last.strip + tmp_drive[key] = value + + if key == "Location" + tmp_drive["Disk Name"] = File.basename(value, ".*") + end + end + hdds << tmp_drive unless tmp_drive.empty? + + hdds + end + def list_snapshots(machine_id) output = execute( "snapshot", machine_id, "list", "--machinereadable", @@ -149,6 +224,21 @@ module VagrantPlugins raise end + # @param [String] port - port on device to attach disk to + # @param [String] device - device on controller for disk + # @param [Hash] opts - additional options + def remove_disk(port, device) + controller = "SATA Controller" + execute('storageattach', @uuid, '--storagectl', controller, '--port', port.to_s, '--device', device.to_s, '--medium', "none") + end + + # @param [String] disk_file + # @param [Integer] disk_size in bytes + # @param [Hash] opts - additional options + def resize_disk(disk_file, disk_size, **opts) + execute("modifymedium", disk_file, '--resizebyte', disk_size.to_i.to_s) + end + def restore_snapshot(machine_id, snapshot_name) # Start with 0% last = 0 @@ -295,6 +385,27 @@ module VagrantPlugins nil end + # Returns port and device for an attached disk given a disk uuid. Returns + # empty hash if disk is not attachd to guest + # + # @param [Hash] vm_info - A guests information from vboxmanage + # @param [String] disk_uuid - the UUID for the disk we are searching for + # @return [Hash] disk_info - Contains a device and port number + def get_port_and_device(disk_uuid) + vm_info = show_vm_info + + disk = {} + disk_info_key = vm_info.key(disk_uuid) + return disk if !disk_info_key + + disk_info = disk_info_key.split("-") + + disk[:port] = disk_info[2] + disk[:device] = disk_info[3] + + return disk + end + def halt execute("controlvm", @uuid, "poweroff", retryable: true) end @@ -783,6 +894,30 @@ module VagrantPlugins return true end + # @param [VagrantPlugins::VirtualboxProvider::Driver] driver + # @param [String] defined_disk_path + # @return [String] destination - The cloned disk + def vmdk_to_vdi(defined_disk_path) + source = defined_disk_path + destination = File.join(File.dirname(source), File.basename(source, ".*")) + ".vdi" + + clone_disk(source, destination, 'VDI') + + destination + end + + # @param [VagrantPlugins::VirtualboxProvider::Driver] driver + # @param [String] defined_disk_path + # @return [String] destination - The cloned disk + def vdi_to_vmdk(defined_disk_path) + source = defined_disk_path + destination = File.join(File.dirname(source), File.basename(source, ".*")) + ".vmdk" + + clone_disk(source, destination, 'VMDK') + + destination + end + protected def valid_ip_address?(ip) diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index f2fbf47e5..9cb46b54e 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -39,6 +39,26 @@ module VagrantPlugins Cap::PublicAddress end + provider_capability(:virtualbox, :configure_disks) do + require_relative "cap/configure_disks" + Cap::ConfigureDisks + end + + provider_capability(:virtualbox, :cleanup_disks) do + require_relative "cap/cleanup_disks" + Cap::CleanupDisks + end + + provider_capability(:virtualbox, :validate_disk_ext) do + require_relative "cap/validate_disk_ext" + Cap::ValidateDiskExt + end + + provider_capability(:virtualbox, :get_default_disk_ext) do + require_relative "cap/validate_disk_ext" + Cap::ValidateDiskExt + end + provider_capability(:virtualbox, :snapshot_list) do require_relative "cap" Cap diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 1d91de3f5..bf738f202 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1641,6 +1641,11 @@ en: 4.2.14 contains a critical bug which prevents it from working with Vagrant. VirtualBox 4.2.16+ fixes this problem. Please upgrade VirtualBox. + virtualbox_disks_defined_exceed_limit: |- + VirtualBox only allows up to 30 disks to be attached to a single guest using the SATA Controller, + including the primray disk. + + Please ensure only up to 30 disks are configured for your guest. virtualbox_guest_property_not_found: |- Could not find a required VirtualBox guest property: %{guest_property} @@ -1802,6 +1807,8 @@ en: #------------------------------------------------------------------------------- config: disk: + invalid_ext: |- + Disk type '%{ext}' is not a valid disk extention for '%{name}'. Please pick one of the following supported disk types: %{exts} invalid_type: |- Disk type '%{type}' is not a valid type. Please pick one of the following supported disk types: %{types} invalid_size: |- @@ -1812,6 +1819,8 @@ en: Disk file '%{file_path}' for disk '%{name}' on machine '%{machine}' does not exist. missing_provider: |- Guest '%{machine}' using provider '%{provider_name}' has provider specific config options for a provider other than '%{provider_name}'. These provider config options will be ignored for this guest + no_name_set: |- + A 'name' option is required when defining a disk for guest '%{machine}'. common: bad_field: "The following settings shouldn't exist: %{fields}" chef: @@ -2164,11 +2173,30 @@ en: #------------------------------------------------------------------------------- # Translations for Vagrant middleware actions #------------------------------------------------------------------------------- + cap: + configure_disks: + start: "Configuring storage mediums..." + floppy_not_supported: "Floppy disk configuration not yet supported. Skipping disk '%{name}'..." + dvd_not_supported: "DVD disk configuration not yet supported. Skipping disk '%{name}'..." + shrink_size_not_supported: |- + VirtualBox does not support shrinking disk sizes. Cannot shrink '%{name}' disks size. + create_disk: |- + Disk '%{name}' not found in guest. Creating and attaching disk to guest... + resize_disk: |- + Disk '%{name}' needs to be resized. Resizing disk... + recovery_from_resize: |- + Vagrant has encountered an exception while trying to resize a disk. It will now attempt to reattach the original disk, as to prevent any data loss. + The original disk is located at %{location} + If Vagrant fails to reattach the original disk, it is recommended that you open the VirtualBox GUI and navigate to the current guests settings for '%{name}' and look at the 'storage' section. Here is where you can reattach a missing disk if Vagrant fails to do so... + recovery_attached_disks: |- + Disk has been reattached. Vagrant will now continue on an raise the exception receieved actions: runner: waiting_cleanup: "Waiting for cleanup before exiting..." exit_immediately: "Exiting immediately, without cleanup!" disk: + cleanup_provider_unsupported: |- + Guest provider '%{provider}' does not support the cleaning up disks, and will not attempt to clean up attached disks on the guest.. provider_unsupported: |- Guest provider '%{provider}' does not support the disk feature, and will not use the disk configuration defined. vm: diff --git a/test/unit/plugins/kernel_v2/config/disk_test.rb b/test/unit/plugins/kernel_v2/config/disk_test.rb index 88640d274..6b6879f37 100644 --- a/test/unit/plugins/kernel_v2/config/disk_test.rb +++ b/test/unit/plugins/kernel_v2/config/disk_test.rb @@ -9,7 +9,9 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigDisk do subject { described_class.new(type) } - let(:machine) { double("machine") } + let(:provider) { double("provider") } + let(:machine) { double("machine", provider: provider) } + def assert_invalid errors = subject.validate(machine) @@ -30,6 +32,8 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigDisk do subject.name = "foo" subject.size = 100 + allow(provider).to receive(:capability?).with(:validate_disk_ext).and_return(true) + allow(provider).to receive(:capability).with(:validate_disk_ext, "vdi").and_return(true) end describe "with defaults" do diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index 20775ce75..c170c873f 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -7,7 +7,8 @@ describe VagrantPlugins::Kernel_V2::VMConfig do subject { described_class.new } - let(:machine) { double("machine") } + let(:provider) { double("provider") } + let(:machine) { double("machine", provider: provider) } def assert_invalid errors = subject.validate(machine) @@ -37,6 +38,9 @@ describe VagrantPlugins::Kernel_V2::VMConfig do allow(machine).to receive(:provider_config).and_return(nil) allow(machine).to receive(:provider_options).and_return({}) + allow(provider).to receive(:capability?).with(:validate_disk_ext).and_return(true) + allow(provider).to receive(:capability).with(:validate_disk_ext, "vdi").and_return(true) + subject.box = "foo" end @@ -552,12 +556,12 @@ describe VagrantPlugins::Kernel_V2::VMConfig do describe "#disk" do before(:each) do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). - with("disk_base_config").and_return("true") + with("disks").and_return("true") end it "stores the disks" do - subject.disk(:disk, size: 100) - subject.disk(:disk, size: 1000, primary: false, name: "storage") + subject.disk(:disk, size: 100, primary: true) + subject.disk(:disk, size: 1000, name: "storage") subject.finalize! assert_valid diff --git a/test/unit/plugins/providers/virtualbox/cap/cleanup_disks_test.rb b/test/unit/plugins/providers/virtualbox/cap/cleanup_disks_test.rb new file mode 100644 index 000000000..7a64c14d8 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/cap/cleanup_disks_test.rb @@ -0,0 +1,88 @@ +require_relative "../base" + +require Vagrant.source_root.join("plugins/providers/virtualbox/cap/cleanup_disks") + +describe VagrantPlugins::ProviderVirtualBox::Cap::CleanupDisks do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:driver) { double("driver") } + + let(:machine) do + iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| + allow(m.provider).to receive(:driver).and_return(driver) + allow(m).to receive(:state).and_return(state) + end + end + + let(:state) do + double(:state) + end + + let(:subject) { described_class } + + let(:disk_meta_file) { {disk: [], floppy: [], dvd: []} } + let(:defined_disks) { {} } + + let(:vm_info) { {"SATA Controller-ImageUUID-0-0" => "12345", + "SATA Controller-ImageUUID-1-0" => "67890"} } + + before do + allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).and_return(true) + allow(driver).to receive(:show_vm_info).and_return(vm_info) + end + + context "#cleanup_disks" do + it "returns if there's no data in meta file" do + subject.cleanup_disks(machine, defined_disks, disk_meta_file) + expect(subject).not_to receive(:handle_cleanup_disk) + end + + describe "with disks to clean up" do + let(:disk_meta_file) { {disk: [{uuid: "1234", name: "storage"}], floppy: [], dvd: []} } + + it "calls the cleanup method if a disk_meta file is defined" do + expect(subject).to receive(:handle_cleanup_disk). + with(machine, defined_disks, disk_meta_file["disk"]). + and_return(true) + + subject.cleanup_disks(machine, defined_disks, disk_meta_file) + end + end + end + + context "#handle_cleanup_disk" do + let(:disk_meta_file) { {disk: [{"uuid"=>"67890", "name"=>"storage"}], floppy: [], dvd: []} } + let(:defined_disks) { [] } + let(:device_info) { {port: "1", device: "0"} } + + it "removes and closes medium from guest" do + allow(driver).to receive(:get_port_and_device). + with("67890"). + and_return(device_info) + + expect(driver).to receive(:remove_disk).with("1", "0").and_return(true) + expect(driver).to receive(:close_medium).with("67890").and_return(true) + + subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file[:disk]) + end + + describe "when the disk isn't attached to a guest" do + it "only closes the medium" do + allow(driver).to receive(:get_port_and_device). + with("67890"). + and_return({}) + + expect(driver).to receive(:close_medium).with("67890").and_return(true) + + subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file[:disk]) + end + end + end +end diff --git a/test/unit/plugins/providers/virtualbox/cap/configure_disks_test.rb b/test/unit/plugins/providers/virtualbox/cap/configure_disks_test.rb new file mode 100644 index 000000000..05ac77e68 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/cap/configure_disks_test.rb @@ -0,0 +1,365 @@ +require_relative "../base" + +require Vagrant.source_root.join("plugins/providers/virtualbox/cap/configure_disks") + +describe VagrantPlugins::ProviderVirtualBox::Cap::ConfigureDisks do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:driver) { double("driver") } + + let(:machine) do + iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| + allow(m.provider).to receive(:driver).and_return(driver) + allow(m).to receive(:state).and_return(state) + end + end + + let(:state) do + double(:state) + end + + let(:vm_info) { {"SATA Controller-ImageUUID-0-0" => "12345", + "SATA Controller-ImageUUID-1-0" => "67890"} } + + let(:defined_disks) { [double("disk", name: "vagrant_primary", size: "5GB", primary: true, type: :disk), + double("disk", name: "disk-0", size: "5GB", primary: false, type: :disk), + double("disk", name: "disk-1", size: "5GB", primary: false, type: :disk), + double("disk", name: "disk-2", size: "5GB", primary: false, type: :disk)] } + + let(:all_disks) { [{"UUID"=>"12345", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", + "Disk Name"=>"ubuntu-18.04-amd64-disk001", + "Storage format"=>"VMDK", + "Capacity"=>"65536 MBytes", + "Encryption"=>"disabled"}, + {"UUID"=>"67890", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/disk-0.vdi", + "Disk Name"=>"disk-0", + "Storage format"=>"VDI", + "Capacity"=>"10240 MBytes", + "Encryption"=>"disabled"}, + {"UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/disk-1.vdi", + "Disk Name"=>"disk-1", + "Storage format"=>"VDI", + "Capacity"=>"5120 MBytes", + "Encryption"=>"disabled"}] } + + let(:subject) { described_class } + + before do + allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).and_return(true) + allow(driver).to receive(:show_vm_info).and_return(vm_info) + end + + context "#configure_disks" do + let(:dsk_data) { {uuid: "1234", name: "disk"} } + it "configures disks and returns the disks defined" do + allow(driver).to receive(:list_hdds).and_return([]) + + expect(subject).to receive(:handle_configure_disk).exactly(4).and_return(dsk_data) + subject.configure_disks(machine, defined_disks) + end + + describe "with no disks to configure" do + let(:defined_disks) { {} } + it "returns empty hash if no disks to configure" do + expect(subject.configure_disks(machine, defined_disks)).to eq({}) + end + end + + describe "with over the disk limit for a given device" do + let(:defined_disks) { (1..31).each { |i| double("disk-#{i}") }.to_a } + + it "raises an exception if the disks defined exceed the limit for a SATA Controller" do + expect{subject.configure_disks(machine, defined_disks)}. + to raise_error(Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit) + end + end + end + + context "#get_current_disk" do + it "gets primary disk uuid if disk to configure is primary" do + primary_disk = subject.get_current_disk(machine, defined_disks.first, all_disks) + expect(primary_disk).to eq(all_disks.first) + end + + it "finds the disk to configure" do + disk = subject.get_current_disk(machine, defined_disks[1], all_disks) + expect(disk).to eq(all_disks[1]) + end + + it "returns nil if disk is not found" do + disk = subject.get_current_disk(machine, defined_disks[3], all_disks) + expect(disk).to be_nil + end + end + + context "#handle_configure_disk" do + describe "when creating a new disk" do + let(:all_disks) { [{"UUID"=>"12345", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", + "Disk Name"=>"ubuntu-18.04-amd64-disk001", + "Storage format"=>"VMDK", + "Capacity"=>"65536 MBytes", + "Encryption"=>"disabled"}] } + + let(:disk_meta) { {uuid: "67890", name: "disk-0"} } + + it "creates a new disk if it doesn't yet exist" do + expect(subject).to receive(:create_disk).with(machine, defined_disks[1]) + .and_return(disk_meta) + + subject.handle_configure_disk(machine, defined_disks[1], all_disks) + end + end + + describe "when a disk needs to be resized" do + let(:all_disks) { [{"UUID"=>"12345", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", + "Disk Name"=>"ubuntu-18.04-amd64-disk001", + "Storage format"=>"VMDK", + "Capacity"=>"65536 MBytes", + "Encryption"=>"disabled"}, + {"UUID"=>"67890", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/disk-0.vdi", + "Disk Name"=>"disk-0", + "Storage format"=>"VDI", + "Capacity"=>"10240 MBytes", + "Encryption"=>"disabled"}] } + + it "resizes a disk" do + expect(subject).to receive(:get_current_disk). + with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) + + expect(subject).to receive(:compare_disk_size). + with(machine, defined_disks[1], all_disks[1]).and_return(true) + + expect(subject).to receive(:resize_disk). + with(machine, defined_disks[1], all_disks[1]).and_return(true) + + subject.handle_configure_disk(machine, defined_disks[1], all_disks) + end + end + + describe "if no additional disk configuration is required" do + let(:all_disks) { [{"UUID"=>"12345", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", + "Disk Name"=>"ubuntu-18.04-amd64-disk001", + "Storage format"=>"VMDK", + "Capacity"=>"65536 MBytes", + "Encryption"=>"disabled"}, + {"UUID"=>"67890", + "Parent UUID"=>"base", + "State"=>"created", + "Type"=>"normal (base)", + "Location"=>"/home/vagrant/VirtualBox VMs/disk-0.vdi", + "Disk Name"=>"disk-0", + "Storage format"=>"VDI", + "Capacity"=>"10240 MBytes", + "Encryption"=>"disabled"}] } + + let(:disk_info) { {port: "1", device: "0"} } + + it "reattaches disk if vagrant defined disk exists but is not attached to guest" do + expect(subject).to receive(:get_current_disk). + with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) + + expect(subject).to receive(:compare_disk_size). + with(machine, defined_disks[1], all_disks[1]).and_return(false) + + expect(driver).to receive(:get_port_and_device).with("67890"). + and_return({}) + + expect(driver).to receive(:attach_disk).with((disk_info[:port].to_i + 1).to_s, + disk_info[:device], + all_disks[1]["Location"]) + + subject.handle_configure_disk(machine, defined_disks[1], all_disks) + end + + it "does nothing if all disks are properly configured" do + expect(subject).to receive(:get_current_disk). + with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) + + expect(subject).to receive(:compare_disk_size). + with(machine, defined_disks[1], all_disks[1]).and_return(false) + + expect(driver).to receive(:get_port_and_device).with("67890"). + and_return(disk_info) + + subject.handle_configure_disk(machine, defined_disks[1], all_disks) + end + end + end + + context "#compare_disk_size" do + let(:disk_config_small) { double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk) } + let(:disk_config_large) { double("disk", name: "disk-0", size: 68719476736.0, primary: false, type: :disk) } + + it "shows a warning if user attempts to shrink size" do + expect(machine.ui).to receive(:warn) + expect(subject.compare_disk_size(machine, disk_config_small, all_disks[1])).to be_falsey + end + + it "returns true if requested size is bigger than current size" do + expect(subject.compare_disk_size(machine, disk_config_large, all_disks[1])).to be_truthy + end + end + + context "#create_disk" do + let(:disk_config) { double("disk", name: "disk-0", size: 1073741824.0, + primary: false, type: :disk, disk_ext: "vdi", + provider_config: nil) } + let(:vm_info) { {"CfgFile"=>"/home/vagrant/VirtualBox VMs/disks/"} } + let(:disk_file) { "/home/vagrant/VirtualBox VMs/disk-0.vdi" } + let(:disk_data) { "Medium created. UUID: 67890\n" } + + let(:port_and_device) { {port: "1", device: "0"} } + + it "creates a disk and attaches it to a guest" do + expect(driver).to receive(:show_vm_info).and_return(vm_info) + + expect(driver).to receive(:create_disk). + with(disk_file, disk_config.size, "VDI").and_return(disk_data) + + expect(subject).to receive(:get_next_port).with(machine). + and_return(port_and_device) + + expect(driver).to receive(:attach_disk).with(port_and_device[:port], + port_and_device[:device], + disk_file) + + subject.create_disk(machine, disk_config) + end + end + + context "#get_next_port" do + it "determines the next available port to use" do + dsk_info = subject.get_next_port(machine) + expect(dsk_info[:device]).to eq("0") + expect(dsk_info[:port]).to eq("2") + end + end + + context "#resize_disk" do + describe "when a disk is vmdk format" do + let(:disk_config) { double("disk", name: "vagrant_primary", size: 1073741824.0, + primary: false, type: :disk, disk_ext: "vmdk", + provider_config: nil) } + let(:attach_info) { {port: "0", device: "0"} } + let(:vdi_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vdi" } + let(:vmdk_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk" } + + it "converts the disk to vdi, resizes it, and converts back to vmdk" do + expect(FileUtils).to receive(:mv).with(vmdk_disk_file, "#{vmdk_disk_file}.backup"). + and_return(true) + + expect(driver).to receive(:get_port_and_device).with("12345"). + and_return(attach_info) + + expect(driver).to receive(:vmdk_to_vdi).with(all_disks[0]["Location"]). + and_return(vdi_disk_file) + + expect(driver).to receive(:resize_disk).with(vdi_disk_file, disk_config.size.to_i). + and_return(true) + + expect(driver).to receive(:remove_disk).with(attach_info[:port], attach_info[:device]). + and_return(true) + expect(driver).to receive(:close_medium).with("12345") + + expect(driver).to receive(:vdi_to_vmdk).with(vdi_disk_file). + and_return(vmdk_disk_file) + + expect(driver).to receive(:attach_disk). + with(attach_info[:port], attach_info[:device], vmdk_disk_file, "hdd").and_return(true) + expect(driver).to receive(:close_medium).with(vdi_disk_file).and_return(true) + + expect(driver).to receive(:list_hdds).and_return(all_disks) + + expect(FileUtils).to receive(:remove).with("#{vmdk_disk_file}.backup", force: true). + and_return(true) + + subject.resize_disk(machine, disk_config, all_disks[0]) + end + + it "reattaches original disk if something goes wrong" do + expect(FileUtils).to receive(:mv).with(vmdk_disk_file, "#{vmdk_disk_file}.backup"). + and_return(true) + + expect(driver).to receive(:get_port_and_device).with("12345"). + and_return(attach_info) + + expect(driver).to receive(:vmdk_to_vdi).with(all_disks[0]["Location"]). + and_return(vdi_disk_file) + + expect(driver).to receive(:resize_disk).with(vdi_disk_file, disk_config.size.to_i). + and_return(true) + + expect(driver).to receive(:remove_disk).with(attach_info[:port], attach_info[:device]). + and_return(true) + expect(driver).to receive(:close_medium).with("12345") + + allow(driver).to receive(:vdi_to_vmdk).and_raise(StandardError) + + expect(FileUtils).to receive(:mv).with("#{vmdk_disk_file}.backup", vmdk_disk_file, force: true). + and_return(true) + + expect(driver).to receive(:attach_disk). + with(attach_info[:port], attach_info[:device], vmdk_disk_file, "hdd").and_return(true) + expect(driver).to receive(:close_medium).with(vdi_disk_file).and_return(true) + + expect{subject.resize_disk(machine, disk_config, all_disks[0])}.to raise_error(Exception) + end + end + + describe "when a disk is vdi format" do + let(:disk_config) { double("disk", name: "disk-0", size: 1073741824.0, + primary: false, type: :disk, disk_ext: "vdi", + provider_config: nil) } + it "resizes the disk" do + expect(driver).to receive(:resize_disk).with(all_disks[1]["Location"], disk_config.size.to_i) + + subject.resize_disk(machine, disk_config, all_disks[1]) + end + end + end + + context "#vmdk_to_vdi" do + it "converts a disk from vmdk to vdi" do + end + end + + context "#vdi_to_vmdk" do + it "converts a disk from vdi to vmdk" do + end + end +end diff --git a/test/unit/vagrant/action/builtin/cleanup_disks_test.rb b/test/unit/vagrant/action/builtin/cleanup_disks_test.rb new file mode 100644 index 000000000..ed2fbcb88 --- /dev/null +++ b/test/unit/vagrant/action/builtin/cleanup_disks_test.rb @@ -0,0 +1,56 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::CleanupDisks do + let(:app) { lambda { |env| } } + let(:vm) { double("vm") } + let(:config) { double("config", vm: vm) } + let(:provider) { double("provider") } + let(:machine) { double("machine", config: config, provider: provider, name: "machine", + provider_name: "provider", data_dir: Pathname.new("/fake/dir")) } + let(:env) { { ui: ui, machine: machine} } + + let(:disks) { [double("disk")] } + + let(:ui) { double("ui") } + + let(:disk_meta_file) { {disk: [{uuid: "123456789", name: "storage"}], floppy: [], dvd: []} } + + describe "#call" do + it "calls configure_disks if disk config present" do + allow(vm).to receive(:disks).and_return(disks) + allow(machine).to receive(:disks).and_return(disks) + allow(machine.provider).to receive(:capability?).with(:cleanup_disks).and_return(true) + subject = described_class.new(app, env) + + expect(app).to receive(:call).with(env).ordered + expect(subject).to receive(:read_disk_metadata).with(machine).and_return(disk_meta_file) + expect(machine.provider).to receive(:capability). + with(:cleanup_disks, disks, disk_meta_file) + + subject.call(env) + end + + it "continues on if no disk config present" do + allow(vm).to receive(:disks).and_return([]) + subject = described_class.new(app, env) + + expect(app).to receive(:call).with(env).ordered + expect(machine.provider).not_to receive(:capability).with(:cleanup_disks, disks) + + subject.call(env) + end + + it "prints a warning if disk config capability is unsupported" do + allow(vm).to receive(:disks).and_return(disks) + allow(machine.provider).to receive(:capability?).with(:cleanup_disks).and_return(false) + subject = described_class.new(app, env) + expect(subject).to receive(:read_disk_metadata).with(machine).and_return(disk_meta_file) + + expect(app).to receive(:call).with(env).ordered + expect(machine.provider).not_to receive(:capability).with(:cleanup_disks, disks) + expect(ui).to receive(:warn) + + subject.call(env) + end + end +end diff --git a/test/unit/vagrant/action/builtin/disk_test.rb b/test/unit/vagrant/action/builtin/disk_test.rb index 05f485602..63f37a812 100644 --- a/test/unit/vagrant/action/builtin/disk_test.rb +++ b/test/unit/vagrant/action/builtin/disk_test.rb @@ -5,13 +5,16 @@ describe Vagrant::Action::Builtin::Disk do let(:vm) { double("vm") } let(:config) { double("config", vm: vm) } let(:provider) { double("provider") } - let(:machine) { double("machine", config: config, provider: provider, provider_name: "provider") } + let(:machine) { double("machine", config: config, provider: provider, + provider_name: "provider", data_dir: Pathname.new("/fake/dir")) } let(:env) { { ui: ui, machine: machine} } let(:disks) { [double("disk")] } let(:ui) { double("ui") } + let(:disk_data) { {disk: [{uuid: "123456789", name: "storage"}], floppy: [], dvd: []} } + describe "#call" do it "calls configure_disks if disk config present" do allow(vm).to receive(:disks).and_return(disks) @@ -20,7 +23,10 @@ describe Vagrant::Action::Builtin::Disk do subject = described_class.new(app, env) expect(app).to receive(:call).with(env).ordered - expect(machine.provider).to receive(:capability).with(:configure_disks, disks) + expect(machine.provider).to receive(:capability). + with(:configure_disks, disks).and_return(disk_data) + + expect(subject).to receive(:write_disk_metadata).and_return(true) subject.call(env) end @@ -32,6 +38,8 @@ describe Vagrant::Action::Builtin::Disk do expect(app).to receive(:call).with(env).ordered expect(machine.provider).not_to receive(:capability).with(:configure_disks, disks) + expect(subject).not_to receive(:write_disk_metadata) + subject.call(env) end @@ -46,5 +54,13 @@ describe Vagrant::Action::Builtin::Disk do subject.call(env) end + + it "writes down a disk_meta file if disks are configured" do + subject = described_class.new(app, env) + + expect(File).to receive(:open).with("/fake/dir/disk_meta", "w+").and_return(true) + + subject.write_disk_metadata(machine, disk_data) + end end end diff --git a/test/unit/vagrant/util/numeric_test.rb b/test/unit/vagrant/util/numeric_test.rb index b13e4f9eb..2315f5f26 100644 --- a/test/unit/vagrant/util/numeric_test.rb +++ b/test/unit/vagrant/util/numeric_test.rb @@ -18,4 +18,10 @@ describe Vagrant::Util::Numeric do expect(bytes).to eq(nil) end end + + describe "bytes to megabytes" do + it "converts bytes to megabytes" do + expect(subject.bytes_to_megabytes(1000000)).to eq(0.95) + end + end end diff --git a/website/source/docs/disks/configuration.html.md b/website/source/docs/disks/configuration.html.md index f9c05cecc..fecef6b21 100644 --- a/website/source/docs/disks/configuration.html.md +++ b/website/source/docs/disks/configuration.html.md @@ -8,32 +8,22 @@ description: |- # Configuration -