441 lines
21 KiB
Ruby
441 lines
21 KiB
Ruby
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")
|
|
|
|
# @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")
|
|
|
|
machine.ui.info(I18n.t("vagrant.cap.configure_disks.start"))
|
|
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
|
|
# Check to determine which controller we should attach disks to.
|
|
# If there is only one storage controller attached to the VM, use
|
|
# it. If there are multiple controllers (e.g. IDE/SATA), attach DVDs
|
|
# to the IDE controller and disks to the SATA controller.
|
|
if storage_controllers.size == 1
|
|
controller = storage_controllers.first
|
|
|
|
# The only way you can define up to the controller limit is if
|
|
# exactly one disk is a primary disk, otherwise we need to reserve
|
|
# a slot for the primary
|
|
if (defined_disks.any? { |d| d.primary } && defined_disks.size > controller.limit) ||
|
|
defined_disks.size > controller.limit - 1
|
|
raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit,
|
|
limit: controller.limit,
|
|
name: controller.name
|
|
else
|
|
disk_controller = controller
|
|
dvd_controller = controller
|
|
end
|
|
else
|
|
disks_defined = defined_disks.select { |d| d.type == :disk }
|
|
if disks_defined.any?
|
|
disk_controller = storage_controllers.get_primary_controller
|
|
|
|
if (disks_defined.any? { |d| d.primary } && disks_defined.size > disk_controller.limit) ||
|
|
disks_defined.size > disk_controller.limit - 1
|
|
raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit,
|
|
limit: disk_controller.limit,
|
|
name: disk_controller.name
|
|
end
|
|
end
|
|
|
|
dvds_defined = defined_disks.select { |d| d.type == :dvd }
|
|
if dvds_defined.any?
|
|
dvd_controller = storage_controllers.get_dvd_controller
|
|
|
|
if dvds_defined.size > dvd_controller.limit
|
|
raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit,
|
|
limit: dvd_controller.limit,
|
|
name: dvd_controller.name
|
|
end
|
|
end
|
|
end
|
|
|
|
configured_disks = { disk: [], floppy: [], dvd: [] }
|
|
|
|
defined_disks.each do |disk|
|
|
if disk.type == :disk
|
|
disk_data = handle_configure_disk(machine, disk, disk_controller.name)
|
|
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
|
|
dvd_data = handle_configure_dvd(machine, disk, dvd_controller.name)
|
|
configured_disks[:dvd] << dvd_data unless dvd_data.empty?
|
|
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
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
current_disk = storage_controllers.get_primary_attachment
|
|
else
|
|
current_disk = all_disks.detect { |d| d[:disk_name] == disk.name }
|
|
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 [String] controller_name - the name of the storage controller to use
|
|
# @return [Hash] - disk_metadata
|
|
def self.handle_configure_disk(machine, disk, controller_name)
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
controller = storage_controllers.get_controller(controller_name)
|
|
all_disks = controller.attachments
|
|
|
|
disk_metadata = {}
|
|
|
|
# Grab the existing configured disk attached to guest, if it exists
|
|
current_disk = get_current_disk(machine, disk, all_disks)
|
|
|
|
if !current_disk
|
|
# Look for an existing disk that's not been attached but exists
|
|
# inside VirtualBox
|
|
#
|
|
# NOTE: This assumes that if that disk exists and was created by
|
|
# Vagrant, it exists in the same location as the primary disk file.
|
|
# Otherwise Vagrant has no good way to determining if the disk was
|
|
# associated with the guest, since disk names are not unique
|
|
# globally to VirtualBox.
|
|
primary = storage_controllers.get_primary_attachment
|
|
existing_disk = machine.provider.driver.list_hdds.detect do |d|
|
|
File.dirname(d["Location"]) == File.dirname(primary[:location]) &&
|
|
d["Disk Name"] == disk.name
|
|
end
|
|
|
|
if !existing_disk
|
|
# create new disk and attach to guest
|
|
disk_metadata = create_disk(machine, disk, controller)
|
|
else
|
|
# Disk has been created but failed to be attached to guest, so
|
|
# this method recovers that disk from previous failure
|
|
# and attaches it onto the guest
|
|
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, controller)
|
|
machine.provider.driver.attach_disk(controller.name,
|
|
dsk_info[:port],
|
|
dsk_info[:device],
|
|
"hdd",
|
|
existing_disk["Location"])
|
|
|
|
disk_metadata[:uuid] = existing_disk["UUID"]
|
|
disk_metadata[:port] = dsk_info[:port]
|
|
disk_metadata[:device] = dsk_info[:device]
|
|
disk_metadata[:name] = disk.name
|
|
disk_metadata[:controller] = controller.name
|
|
end
|
|
elsif compare_disk_size(machine, disk, current_disk)
|
|
disk_metadata = resize_disk(machine, disk, current_disk, controller)
|
|
else
|
|
LOGGER.info("No further configuration required for disk '#{disk.name}'")
|
|
disk_metadata[:uuid] = current_disk[:uuid]
|
|
disk_metadata[:port] = current_disk[:port]
|
|
disk_metadata[:device] = current_disk[:device]
|
|
|
|
disk_metadata[:name] = disk.name
|
|
disk_metadata[:controller] = controller.name
|
|
end
|
|
|
|
disk_metadata
|
|
end
|
|
|
|
# Handles all disk configs of type `:dvd`
|
|
#
|
|
# @param [Vagrant::Machine] machine - the current machine
|
|
# @param [Config::Disk] dvd - the current disk to configure
|
|
# @param [String] controller_name - the name of the storage controller to use
|
|
# @return [Hash] - dvd_metadata
|
|
def self.handle_configure_dvd(machine, dvd, controller_name)
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
controller = storage_controllers.get_controller(controller_name)
|
|
|
|
dvd_metadata = {}
|
|
|
|
dvd_location = File.expand_path(dvd.file)
|
|
dvd_attached = controller.attachments.detect { |a| a[:location] == dvd_location }
|
|
|
|
if dvd_attached
|
|
LOGGER.info("No further configuration required for dvd '#{dvd.name}'")
|
|
dvd_metadata[:name] = dvd.name
|
|
dvd_metadata[:port] = dvd_attached[:port]
|
|
dvd_metadata[:device] = dvd_attached[:device]
|
|
dvd_metadata[:uuid] = dvd_attached[:uuid]
|
|
dvd_metadata[:controller] = controller.name
|
|
else
|
|
LOGGER.warn("DVD '#{dvd.name}' is not connected to guest '#{machine.name}', Vagrant will attempt to connect dvd to guest")
|
|
dsk_info = get_next_port(machine, controller)
|
|
machine.provider.driver.attach_disk(controller.name,
|
|
dsk_info[:port],
|
|
dsk_info[:device],
|
|
"dvddrive",
|
|
dvd.file)
|
|
|
|
# Refresh the controller information
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
controller = storage_controllers.get_controller(controller_name)
|
|
|
|
attachment = controller.attachments.detect { |a| a[:port] == dsk_info[:port] &&
|
|
a[:device] == dsk_info[:device] }
|
|
|
|
dvd_metadata[:name] = dvd.name
|
|
dvd_metadata[:port] = dsk_info[:port]
|
|
dvd_metadata[:device] = dsk_info[:device]
|
|
dvd_metadata[:uuid] = attachment[:uuid]
|
|
dvd_metadata[:controller] = controller.name
|
|
end
|
|
|
|
dvd_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
|
|
# @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller -
|
|
# the storage controller to use
|
|
def self.create_disk(machine, disk_config, controller)
|
|
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)
|
|
dsk_controller_info = get_next_port(machine, controller)
|
|
machine.provider.driver.attach_disk(controller.name,
|
|
dsk_controller_info[:port],
|
|
dsk_controller_info[:device],
|
|
"hdd",
|
|
disk_file)
|
|
|
|
disk_metadata = { uuid: disk_var.split(":").last.strip, name: disk_config.name,
|
|
controller: controller.name, port: dsk_controller_info[:port],
|
|
device: dsk_controller_info[:device] }
|
|
|
|
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
|
|
# @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller -
|
|
# the storage controller to use
|
|
# @return [Hash] dsk_info - The next available port and device on a given controller
|
|
def self.get_next_port(machine, controller)
|
|
dsk_info = {}
|
|
|
|
if controller.devices_per_port == 1
|
|
used_ports = controller.attachments.map { |a| a[:port].to_i }
|
|
next_available_port = ((0..(controller.maxportcount - 1)).to_a - used_ports).first
|
|
|
|
dsk_info[:port] = next_available_port.to_s
|
|
dsk_info[:device] = "0"
|
|
elsif controller.devices_per_port == 2
|
|
# IDE Controllers have primary/secondary devices, so find the first port
|
|
# with an empty device
|
|
(0..(controller.maxportcount - 1)).each do |port|
|
|
# Skip this port if it's full
|
|
port_attachments = controller.attachments.select { |a| a[:port] == port.to_s }
|
|
next if port_attachments.count == controller.devices_per_port
|
|
|
|
dsk_info[:port] = port.to_s
|
|
|
|
# Check for a free device
|
|
if port_attachments.any? { |a| a[:device] == "0" }
|
|
dsk_info[:device] = "1"
|
|
else
|
|
dsk_info[:device] = "0"
|
|
end
|
|
|
|
break if dsk_info[:port]
|
|
end
|
|
else
|
|
raise Vagrant::Errors::VirtualBoxDisksUnsupportedController, controller_name: controller.name
|
|
end
|
|
|
|
if dsk_info[:port].to_s.empty?
|
|
# This likely only occurs if additional disks have been added outside of Vagrant configuration
|
|
LOGGER.warn("There is no more available space to attach disks to for the controller '#{controller}'. Clear up some space on the controller '#{controller}' to attach new disks.")
|
|
raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit,
|
|
limit: controller.limit,
|
|
name: controller.name
|
|
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
|
|
# @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller -
|
|
# the storage controller to use
|
|
# @return [Hash] - disk_metadata
|
|
def self.resize_disk(machine, disk_config, defined_disk, controller)
|
|
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")
|
|
|
|
# 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(controller.name, defined_disk[:port], defined_disk[: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(controller.name,
|
|
defined_disk[:port],
|
|
defined_disk[:device],
|
|
"hdd",
|
|
vmdk_disk_file)
|
|
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, defined_disk, backup_disk_location, original_disk, vdi_disk_file, controller)
|
|
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
|
|
storage_controllers = machine.provider.driver.read_storage_controllers
|
|
updated_controller = storage_controllers.get_controller(controller.name)
|
|
new_disk_info = updated_controller.attachments.detect { |h| h[:location] == defined_disk[:location] }
|
|
|
|
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, controller: controller.name,
|
|
port: defined_disk[:port], device: defined_disk[:device] }
|
|
|
|
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
|
|
# @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller - the storage controller to use
|
|
def self.recover_from_resize(machine, disk_info, backup_disk_location, original_disk, vdi_disk_file, controller)
|
|
begin
|
|
# move backup to original name
|
|
FileUtils.mv(backup_disk_location, original_disk[:location], force: true)
|
|
# Attach disk
|
|
machine.provider.driver.attach_disk(controller.name,
|
|
disk_info[:port],
|
|
disk_info[:device],
|
|
"hdd",
|
|
original_disk[:location])
|
|
|
|
# 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
|