Merge pull request #11349 from briancain/feature/virtualbox-disk-mgmt

[FEATURE] Disk management with the VirtualBox provider
This commit is contained in:
Brian Cain 2020-02-14 08:33:51 -08:00 committed by GitHub
commit 3d2eafc414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1521 additions and 67 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,32 +8,22 @@ description: |-
# Configuration
<div class="alert alert-warning">
<strong>Warning!</strong> This feature is experimental and may break or
change in between releases. Use at your own risk. It currently is not officially
supported or functional.
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="disk_base_config"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, triggers with the `:type` option
will be ignored.
</div>
Vagrant Disks has several options that allow users to define and attach disks to guests.
## Disk Options
* `name` (string) - Optional argument to give the disk a name
* `type` (symbol) - The type of disk to manage. This option defaults to `:disk`. Please read the provider specific documentation for supported types.
* `file` (string) - Optional argument that defines a path on disk pointing to the location of a disk file.
* `primary` (boolean) - Optional argument that configures a given disk to be the "primary" disk to manage on the guest. There can only be one `primary` disk per guest.
* `provider_config` (hash) - Additional provider specific options for managing a given disk.
* `disk_ext` (string) - Optional argument that defines what kind of file
extension a disk should have. Defaults to `"vdi"` if unspecified. For a list of
supported disk extensions, please check the specific provider being used.
* `file` (string) - Optional argument that defines a path on disk pointing to
the location of a disk file that already exists.
* `name` (string) - Required option to give the disk a name. This name will be
used as the filename when creating the disk.
* `primary` (boolean) - Optional argument that configures a given disk to be the
"primary" disk to manage on the guest. There can only be one `primary` disk per guest.
Defaults to `false` if not specified.
* `provider_config` (hash) - Additional provider specific options for managing a given disk. Please refer to
the provider specific documentation to see any available provider_config options.
Generally, the disk option accepts two kinds of ways to define a provider config:
@ -41,8 +31,12 @@ Vagrant Disks has several options that allow users to define and attach disks to
- The provider name followed by a double underscore, and then the provider specific option for that disk
+ `{providername: {diskoption: value}, otherprovidername: {diskoption: value}`
- A hash where the top level key(s) are one or more providers, and each provider keys values are a hash of options and their values.
* `size` (String) - The size of the disk to create. For example, `"10GB"`.
**Note:** More specific examples of these can be found under the provider specific disk page. The `provider_config` option will depend on the provider you are using. Please read the provider specific documentation for disk management to learn about what options are available to use.
**Note:** More specific examples of these can be found under the provider
specific disk page. The `provider_config` option will depend on the provider
you are using. Please read the provider specific documentation for disk
management to learn about what options are available to use.
## Disk Types
@ -52,6 +46,9 @@ The disk config currently accepts three kinds of disk types:
* `dvd` (symbol)
* `floppy` (symbol)
**NOTE:** These types depend on the provider used, and may not yet be functional. Please
refer to the provider specific implementation for more details for what is supported.
You can set a disk type with the first argument of a disk config in your Vagrantfile:
```ruby
@ -65,12 +62,50 @@ If you are a vagrant plugin author who maintains a provider for Vagrant, this sh
<div class="alert alert-warning">
<strong>Warning!</strong> This guide is still being written as we develop this
new feature for Vagrant. Some points below are what we plan on covering once this
feature is more fully developed in Vagrant.
new feature for Vagrant. Is something missing, or could this be improved? Please
let us know on GitHub by opening an issue or open a pull request directly.
</div>
- Entry level builtin action `disk` and how to use it as a provider author
- `id` is unique to each disk config object
- `provider_config` and how to its structured and how to use/validate it
All providers must implement the capability `configure_disks`, and `cleanup_disks`.
These methods are responsible for the following:
More information should be coming once the disk feature is more functional.
- `configure_disks` - Reads in a Vagrant config for defined disks from a Vagrantfile,
and creates and attaches the disks based on the given config
- `cleanup_disks` - Compares the current Vagrant config for defined disks and detaches
any disks that are no longer valid for a guest.
These methods are called in the builtin Vagrant actions _Disk_ and _CleanupDisks_.
If the provider does not support these capabilities, they will be skipped over and no
disks will be configured. It is the providers job to implement these provider capabilities
and handle the methods required to support disk creation and deletion. Vagrant will
handle parsing and supplying the config object based on what has been defined inside
a users Vagrantfile.
For a more detailed example of how to use this disk configuration with Vagrant, please
check out how it was implemented using the VirtualBox provider.
### The disk_meta file
Both builtin disk actions `configure_disks` and `cleanup_disks` expect to read and
write down a `disk_meta` file inside a machines data dir. This file is specifically
for keeping track of the _last configured state_ for disks in a given provider.
Generally, this file is used as a way for Vagrant to keep track of what disks
are being managed by Vagrant with the provider uses, so that it does not accidentally
delete or manage disks that were configured outside of Vagrants configuration.
For the VirtualBox provider, Vagrant uses this file to see what disks were configured
on the _last run_ of Vagrant, and compares that to the current configured state for
the Vagrantfile on the _current run_ of Vagrant. It specifically stores each disks
UUID and disk name for use. If it notices a disk that is no longer in the
Vagrantfile, it can be assumed that the disk is no longer valid for that guest,
and cleans up the disk.
This may not be required for your provider, however with the VirtualBox provider, Vagrant
needs a way to keep track of the defined disks managed by Vagrant and their disk UUIDs
that VirtualBox uses to keep track of these disks.
### The provider_config hash
The disk config class supports an optional hash of options called `provider_config`.
This allows the user to define some additional options for a provider to use that
may be non-standard across different providers.

View File

@ -11,24 +11,19 @@ description: |-
<div class="alert alert-warning">
<strong>Warning!</strong> This feature is experimental and may break or
change in between releases. Use at your own risk. It currently is not officially
supported or functional.
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="disk_base_config"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, triggers with the `:type` option
will be ignored.
<strong>NOTE:</strong> Vagrant disks is currently a future feature for Vagrant that is not yet supported.
Some documentation exists here for future reference, however the Disk feature is
not yet functional. Please be patient for us to develop this new feature, and stay
tuned for a future release of Vagrant with this new functionality!
supported or functional. Please refer to the providier specific disk documentation
for more information on how to use and enable this feature.
</div>
Vagrant Disks is a feature that allows users to define what mediums should be attached
to their guests, as well as allowing users to resize their primary disk.
For examples on how to achieve this, among other use cases, please refer to the [usage](/docs/disks/usage.html)
guide for more information!
For more information about what options are available for configuring disks, see the
[configuration section](/docs/disks/configuration.html).
## Supported Providers
Currently, only VirtualBox is supported. Please refer to the [VirtualBox documentation](/docs/disks/virtualbox/index.html) for more information on using disks with the VirtualBox provider!

View File

@ -16,19 +16,88 @@ description: |-
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="disk_base_config"
VAGRANT_EXPERIMENTAL="disks"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, triggers with the `:type` option
will be ignored.
for more info. Without this flag enabled, any disks defined will not be configured.
Also note that the examples below use the VirtualBox provider, which is the current
supported providier for this feature.
</div>
Below are some very simple examples of how to use Vagrant Disks.
Below are some very simple examples of how to use Vagrant Disks with the VirtualBox provider.
## Examples
## Basic Examples
- Resizing a disk (primary)
- Attaching a new disk
- Using provider specific options
### Resizing your primary disk
Sometimes, the primary disk for a guest is not large enough and you will need to
add more space. To resize a disk, you can simply add a config like this below
to expand the size of your guests drive:
```ruby
config.vm.disk :disk, size: "100GB", primary: true
```
Note: the `primary: true` is what tells Vagrant to expand the guests main drive.
Without this option, Vagrant will instead attach a _new_ disk to the guest.
For example, this Ubuntu guest will now come with 100GB of space, rather than the default:
```ruby
Vagrant.configure("2") do |config|
config.vm.define "hashicorp" do |h|
h.vm.box = "hashicorp/bionic64"
h.vm.provider :virtualbox
h.vm.disk :disk, size: "100GB", primary: true
end
end
```
It should be noted that due to how VirtualBox functions, it is not possible to shrink
the size of a disk.
### Attaching new disks
Vagrant can attach multiple disks to a guest using the VirtualBox provider. An example
of attaching a single disk to a guest with 10 GB of storage can be found below:
```ruby
Vagrant.configure("2") do |config|
config.vm.define "hashicorp" do |h|
h.vm.box = "hashicorp/bionic64"
h.vm.provider :virtualbox
h.vm.disk :disk, size: "10GB", name: "extra_storage"
end
end
```
Optionally, if you need to attach many disks, you can use Ruby to generate multiple
disks for Vagrant to create and attach to your guest:
```ruby
Vagrant.configure("2") do |config|
config.vm.define "hashicorp" do |h|
h.vm.box = "hashicorp/bionic64"
h.vm.provider :virtualbox
(0..3).each do |i|
h.vm.disk :disk, size: "5GB", name: "disk-#{i}"
end
end
end
```
Note: Virtualbox only allows for up to 30 disks to be attached to a given SATA Controller,
and this number includes the primary disk! Attempting to configure more than 30 will
result in a Vagrant error.
### Removing Disks
If you have removed a disk from your Vagrant config and wish for it to be detached from the guest,
you will need to `vagrant reload` your guest to apply these changes. **NOTE:** Doing so
will also delete the medium from your hard drive.

View File

@ -0,0 +1,42 @@
---
layout: "docs"
page_title: "Common Issues - Disks VirtualBox Provider"
sidebar_current: "disks-providers-virtualbox-issues"
description: |-
This page lists some common issues people run into with Vagrant and VirtualBox
as well as solutions for those issues.
---
# Common Issues and Troubleshooting
This page lists some common issues people run into with Vagrant and VirtualBox
as well as solutions for those issues.
## Are my disks attached?
A handy way to figure out what disks are attached (or not attached) to your guest
is to open up the VirtualBox GUI and select the guest. When selecting a guest on the GUI,
it should open more information about the guest, including storage information. Here
you should see a list of disks attached to your guest.
## How many disks can I attach?
Vagrant attaches all new disks defined to a guests SATA Controller. As of VirtualBox 6.1.x,
SATA Controllers can only support up to **30 disks** per guest. Therefore if you try
to define and attach more than 30, it will result in an error. This number _includes_
the primary disk for the guest.
## Resizing VMDK format disks
VMDK disks cannot be resized in their current state, so Vagrant will automatically
convert these disks to VDI, resize the disk, and convert it back to its original format.
Many Vagrant boxes default to using the VMDK disk format, so resizing disks for
many users will require Vagrant to convert these disks. Generally, this will be transparent
to the user. However if Vagrant crashes or if a user interrupts Vagrant during the
cloning process, there is a chance that you might lose your data.
## Applying Vagrant disk configuration changes to guests
Due to how VirtualBox works, you must reload your guest for any disk config changes
to be applied. So if you update your Vagrantfile to update or even remove disks, make
sure to `vagrant reload` your guests for these changes to be applied.

View File

@ -0,0 +1,42 @@
---
layout: "docs"
page_title: "Disks for VirtualBox Provider"
sidebar_current: "disks-providers-virtualbox"
description: |-
Vagrant comes with support out of the box for VirtualBox, a free,
cross-platform consumer virtualization product.
---
# VirtualBox
<div class="alert alert-warning">
<strong>Warning!</strong> This feature is experimental and may break or
change in between releases. Use at your own risk. It currently is not officially
supported or functional.
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="disks"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, any disks defined will not be configured.
</div>
**Vagrant currently only supports VirtualBox version 5.x and newer for configuring and
attaching disks.**
Because of how VirtualBox handles disk management, a Vagrant guest _must_ be powered
off for any changes to be applied to a guest. If you make a configuration change
with a guests disk, you will need to `vagrant reload` the guest for any changes
to be applied.
When new disks are defined to be attached to a guest, Vagrant will create and attach
these disks to a guests SATA Controller. It should be noted that up to 30 disks
can be attached to the SATA Controller.
For more information on how to use VirtualBox to configure disks for a guest, refer
to the [general usage](/docs/disks/usage.html) and [configuration](/docs/disks/configuration.html)
guide for more information.

View File

@ -0,0 +1,34 @@
---
layout: "docs"
page_title: "Usage - Disks VirtualBox Provider"
sidebar_current: "disks-providers-virtualbox-usage"
description: |-
The Vagrant VirtualBox provider is used just like any other provider. Please
read the general basic usage page for providers.
---
# Usage
<div class="alert alert-warning">
<strong>Warning!</strong> This feature is experimental and may break or
change in between releases. Use at your own risk. It currently is not officially
supported or functional.
This feature currently reqiures the experimental flag to be used. To explicitly enable this feature, you can set the experimental flag to:
```
VAGRANT_EXPERIMENTAL="disks"
```
Please note that `VAGRANT_EXPERIMENTAL` is an environment variable. For more
information about this flag visit the [Experimental docs page](/docs/experimental/)
for more info. Without this flag enabled, any disks defined will not be configured.
</div>
For examples of how to use the disk feature with VirtualBox, please refer to the
[general disk usage guide](/docs/disks/usage.html) for more examples.
## provider_config options
Currently, there are no additional options supported for the `provider_config` option.
This page will be updated with any valid options as they become supported.

View File

@ -139,6 +139,13 @@
<ul class="nav">
<li<%= sidebar_current("disks-configuration") %>><a href="/docs/disks/configuration.html">Configuration</a></li>
<li<%= sidebar_current("disks-usage") %>><a href="/docs/disks/usage.html">Usage</a></li>
<li<%= sidebar_current("disks-providers-virtualbox") %>>
<a href="/docs/disks/virtualbox/">VirtualBox</a>
<ul class="nav">
<li<%= sidebar_current("disks-providers-virtualbox-usage") %>><a href="/docs/disks/virtualbox/usage.html">Usage</a></li>
<li<%= sidebar_current("disks-providers-virtualbox-issues") %>><a href="/docs/disks/virtualbox/common-issues.html">Common Issues</a></li>
</ul>
</li>
</ul>
</li>