Merge pull request #11165 from briancain/feature/generic-disk-config-mgmnt

Introduce disk management base config layer to core Vagrant
This commit is contained in:
Brian Cain 2019-11-22 14:57:00 -08:00 committed by GitHub
commit 9fc155bf75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 648 additions and 0 deletions

View File

@ -15,6 +15,7 @@ module Vagrant
autoload :Confirm, "vagrant/action/builtin/confirm"
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm"
autoload :Disk, "vagrant/action/builtin/disk"
autoload :EnvSet, "vagrant/action/builtin/env_set"
autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt"
autoload :HandleBox, "vagrant/action/builtin/handle_box"

View File

@ -0,0 +1,37 @@
module Vagrant
module Action
module Builtin
class Disk
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
if machine.provider.capability?(:configure_disks)
machine.provider.capability(:configure_disks, defined_disks)
else
env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported",
provider: machine.provider_name))
end
# Continue On
@app.call(env)
end
def get_disks(machine, env)
return @_disks if @_disks
@_disks = []
@_disks = machine.config.vm.disks
@_disks
end
end
end
end
end

View File

@ -0,0 +1,61 @@
require "log4r"
module Vagrant
module Util
class Numeric
# Authors Note: This conversion has been borrowed from the ActiveSupport Numeric class
# Conversion helper constants
KILOBYTE = 1024
MEGABYTE = KILOBYTE * 1024
GIGABYTE = MEGABYTE * 1024
TERABYTE = GIGABYTE * 1024
PETABYTE = TERABYTE * 1024
EXABYTE = PETABYTE * 1024
BYTES_CONVERSION_MAP = {KB: KILOBYTE, MB: MEGABYTE, GB: GIGABYTE, TB: TERABYTE,
PB: PETABYTE, EB: EXABYTE}
# Regex borrowed from the vagrant-disksize config class
SHORTHAND_MATCH_REGEX = /^(?<number>[0-9]+)\s?(?<unit>KB|MB|GB|TB)?$/
class << self
LOGGER = Log4r::Logger.new("vagrant::util::numeric")
# A helper that converts a shortcut string to its bytes representation.
# The expected format of `str` is essentially: "<Number>XX"
# Where `XX` is shorthand for KB, MB, GB, TB, PB, or EB. For example, 50 megabytes:
#
# str = "50MB"
#
# @param [String] - str
# @return [Integer,nil] - bytes - returns nil if method fails to convert to bytes
def string_to_bytes(str)
bytes = nil
str = str.to_s.strip
matches = SHORTHAND_MATCH_REGEX.match(str)
if matches
number = matches[:number].to_i
unit = matches[:unit].to_sym
if BYTES_CONVERSION_MAP.key?(unit)
bytes = number * BYTES_CONVERSION_MAP[unit]
else
LOGGER.error("An invalid unit or format was given, string_to_bytes cannot convert #{str}")
end
end
bytes
end
# @private
# Reset the cached values for platform. This is not considered a public
# API and should only be used for testing.
def reset!
instance_variables.each(&method(:remove_instance_variable))
end
end
end
end
end

View File

@ -0,0 +1,168 @@
require "log4r"
require "securerandom"
require "vagrant/util/numeric"
module VagrantPlugins
module Kernel_V2
class VagrantConfigDisk < Vagrant.plugin("2", :config)
#-------------------------------------------------------------------
# Config class for a given Disk
#-------------------------------------------------------------------
DEFAULT_DISK_TYPES = [:disk, :dvd, :floppy].freeze
# Note: This value is for internal use only
#
# @return [String]
attr_reader :id
# File name for the given disk. Defaults to a generated name that is:
#
# vagrant_<disk_type>_<short_uuid>
#
# @return [String]
attr_accessor :name
# Type of disk to create. Defaults to `:disk`
#
# @return [Symbol]
attr_accessor :type
# Size of disk to create
#
# @return [Integer,String]
attr_accessor :size
# Path to the location of the disk file (Optional)
#
# @return [String]
attr_accessor :file
# Determines if this disk is the _main_ disk, or an attachment.
# Defaults to true.
#
# @return [Boolean]
attr_accessor :primary
# Provider specific options
#
# @return [Hash]
attr_accessor :provider_config
def initialize(type)
@logger = Log4r::Logger.new("vagrant::config::vm::disk")
@type = type
@provider_config = {}
@name = UNSET_VALUE
@provider_type = UNSET_VALUE
@size = UNSET_VALUE
@primary = UNSET_VALUE
@file = UNSET_VALUE
# Internal options
@id = SecureRandom.uuid
end
# Helper method for storing provider specific config options
#
# Expected format is:
#
# - `provider__diskoption: value`
# - `{provider: {diskoption: value, otherdiskoption: value, ...}`
#
# Duplicates will be overriden
#
# @param [Hash] options
def add_provider_config(**options, &block)
current = {}
options.each do |k,v|
opts = k.to_s.split("__")
if opts.size == 2
current[opts[0].to_sym] = {opts[1].to_sym => v}
elsif v.is_a?(Hash)
current[k] = v
else
@logger.warn("Disk option '#{k}' found that does not match expected provider disk config schema.")
end
end
current = @provider_config.merge(current) if !@provider_config.empty?
@provider_config = current
end
def finalize!
# Ensure all config options are set to nil or default value if untouched
# by user
@type = :disk if @type == UNSET_VALUE
@size = nil if @size == UNSET_VALUE
@file = nil if @file == UNSET_VALUE
if @primary == UNSET_VALUE
@primary = false
end
if @name == UNSET_VALUE
if @primary
@name = "vagrant_primary"
else
@name = "name_#{@type.to_s}_#{@id.split("-").last}"
end
end
@provider_config = nil if @provider_config == {}
end
# @return [Array] array of strings of error messages from config option validation
def validate(machine)
errors = _detected_errors
# validate type with list of known disk types
if !DEFAULT_DISK_TYPES.include?(@type)
errors << I18n.t("vagrant.config.disk.invalid_type", type: @type,
types: DEFAULT_DISK_TYPES.join(', '))
end
if @size && !@size.is_a?(Integer)
if @size.is_a?(String)
@size = Vagrant::Util::Numeric.string_to_bytes(@size)
end
if !@size
errors << I18n.t("vagrant.config.disk.invalid_size", name: @name, machine: machine.name)
end
end
if @file
if !@file.is_a?(String)
errors << I18n.t("vagrant.config.disk.invalid_file_type", file: @file, machine: machine.name)
elsif !File.file?(@file)
errors << I18n.t("vagrant.config.disk.missing_file", file_path: @file,
name: @name, machine: machine.name)
end
end
if @provider_config
if !@provider_config.keys.include?(machine.provider_name)
machine.env.ui.warn(I18n.t("vagrant.config.disk.missing_provider",
machine: machine.name,
provider_name: machine.provider_name))
end
end
errors
end
# The String representation of this Disk.
#
# @return [String]
def to_s
"disk config"
end
end
end
end

View File

@ -11,6 +11,7 @@ require "vagrant/util/experimental"
require File.expand_path("../vm_provisioner", __FILE__)
require File.expand_path("../vm_subvm", __FILE__)
require File.expand_path("../disk", __FILE__)
module VagrantPlugins
module Kernel_V2
@ -43,6 +44,7 @@ module VagrantPlugins
attr_accessor :post_up_message
attr_accessor :usable_port_range
attr_reader :provisioners
attr_reader :disks
# This is an experimental feature that isn't public yet.
attr_accessor :clone
@ -73,6 +75,7 @@ module VagrantPlugins
@hostname = UNSET_VALUE
@post_up_message = UNSET_VALUE
@provisioners = []
@disks = []
@usable_port_range = UNSET_VALUE
# Internal state
@ -123,6 +126,28 @@ module VagrantPlugins
end
end
# Merge defined disks
other_disks = other.instance_variable_get(:@disks)
new_disks = []
@disks.each do |p|
other_p = other_disks.find { |o| p.id == o.id }
if other_p
# there is an override. take it.
other_p.config = p.config.merge(other_p.config)
# Remove duplicate disk config from other
p = other_p
other_disks.delete(other_p)
end
# there is an override, merge it into the
new_disks << p.dup
end
other_disks.each do |p|
new_disks << p.dup
end
result.instance_variable_set(:@disks, new_disks)
# Merge the providers by prepending any configuration blocks we
# have for providers onto the new configuration.
other_providers = other.instance_variable_get(:@__providers)
@ -384,6 +409,38 @@ module VagrantPlugins
@__defined_vms[name].config_procs << [options[:config_version], block] if block
end
# Stores disk config options from Vagrantfile
#
# @param [Symbol] type
# @param [Hash] options
# @param [Block] block
def disk(type, **options, &block)
disk_config = VagrantConfigDisk.new(type)
# Remove provider__option options before set_options, otherwise will
# show up as missing setting
# Extract provider hash options as well
provider_options = {}
options.delete_if do |p,o|
if o.is_a?(Hash) || p.to_s.include?("__")
provider_options[p] = o
true
end
end
disk_config.set_options(options)
# 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.")
return
end
@disks << disk_config
end
#-------------------------------------------------------------------
# Internal methods, don't call these.
#-------------------------------------------------------------------
@ -547,6 +604,10 @@ module VagrantPlugins
end
end
@disks.each do |d|
d.finalize!
end
if !current_dir_shared && !@__synced_folders["/vagrant"]
synced_folder(".", "/vagrant")
end
@ -748,6 +809,26 @@ module VagrantPlugins
end
end
# Validate disks
# Check if there is more than one primrary disk defined and throw an error
primary_disks = @disks.select { |d| d.primary && d.type == :disk }
if primary_disks.size > 1
errors << I18n.t("vagrant.config.vm.multiple_primary_disks_error",
name: machine.name)
end
disk_names = @disks.map { |d| d.name }
duplicate_names = disk_names.detect{ |d| disk_names.count(d) > 1 }
if duplicate_names && duplicate_names.size
errors << I18n.t("vagrant.config.vm.multiple_disk_names_error",
name: duplicate_names)
end
@disks.each do |d|
error = d.validate(machine)
errors.concat error if !error.empty?
end
# We're done with VM level errors so prepare the section
errors = { "vm" => errors }

View File

@ -79,6 +79,7 @@ module VagrantPlugins
b.use ForwardPorts
b.use SetHostname
b.use SaneDefaults
b.use Disk
b.use Customize, "pre-boot"
b.use Boot
b.use Customize, "post-boot"

View File

@ -1792,6 +1792,17 @@ en:
# Translations for config validation errors
#-------------------------------------------------------------------------------
config:
disk:
invalid_type: |-
Disk type '%{type}' is not a valid type. Please pick one of the following supported disk types: %{types}
invalid_size: |-
Config option 'size' for disk '%{name}' on guest '%{machine}' is not an integer
invalid_file_type: |-
Disk config option 'file' for '%{machine}' is not a string.
missing_file: |-
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
common:
bad_field: "The following settings shouldn't exist: %{fields}"
chef:
@ -1892,6 +1903,10 @@ en:
hyphens or dots. It cannot start with a hyphen or dot.
ignore_provider_config: |-
Ignoring provider config for validation...
multiple_primary_disks_error: |-
There are more than one primary disks defined for guest '%{name}'. Please ensure that only one disk has been defined as a primary disk.
multiple_disk_names_error: |-
Duplicate disk names defined: '%{name}'. Disk names must be unique.
name_invalid: |-
The sub-VM name '%{name}' is invalid. Please don't use special characters.
network_ip_ends_in_one: |-
@ -2138,6 +2153,9 @@ en:
runner:
waiting_cleanup: "Waiting for cleanup before exiting..."
exit_immediately: "Exiting immediately, without cleanup!"
disk:
provider_unsupported: |-
Guest provider '%{provider}' does not support the disk feature, and will not use the disk configuration defined.
vm:
boot:
booting: Booting VM...

View File

@ -0,0 +1,56 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/kernel_v2/config/disk")
describe VagrantPlugins::Kernel_V2::VagrantConfigDisk do
include_context "unit"
let(:type) { :disk }
subject { described_class.new(type) }
let(:machine) { double("machine") }
def assert_invalid
errors = subject.validate(machine)
if !errors.empty? { |v| !v.empty? }
raise "No errors: #{errors.inspect}"
end
end
def assert_valid
errors = subject.validate(machine)
if !errors.empty? { |v| v.empty? }
raise "Errors: #{errors.inspect}"
end
end
before do
env = double("env")
subject.name = "foo"
subject.size = 100
end
describe "with defaults" do
it "is valid with test defaults" do
subject.finalize!
assert_valid
end
it "sets a disk type" do
subject.finalize!
expect(subject.type).to eq(type)
end
it "defaults to non-primray disk" do
subject.finalize!
expect(subject.primary).to eq(false)
end
end
describe "defining a new config that needs to match internal restraints" do
before do
end
end
end

View File

@ -549,6 +549,58 @@ describe VagrantPlugins::Kernel_V2::VMConfig do
end
end
describe "#disk" do
before(:each) do
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
with("disk_base_config").and_return("true")
end
it "stores the disks" do
subject.disk(:disk, size: 100)
subject.disk(:disk, size: 1000, primary: false, name: "storage")
subject.finalize!
assert_valid
d = subject.disks
expect(d.length).to eql(2)
expect(d[0].size).to eql(100)
expect(d[1].size).to eql(1000)
expect(d[1].name).to eql("storage")
end
it "raises an error with duplicate names" do
subject.disk(:disk, size: 100, name: "foo")
subject.disk(:disk, size: 1000, name: "foo", primary: false)
subject.finalize!
assert_invalid
end
it "does not merge duplicate disks" do
subject.disk(:disk, size: 1000, primary: false, name: "storage")
subject.disk(:disk, size: 1000, primary: false, name: "backup")
merged = subject.merge(subject)
merged_disks = merged.disks
expect(merged_disks.length).to eql(2)
end
it "ignores non-overriding runs" do
subject.disk(:disk, name: "foo")
other = described_class.new
other.disk(:disk, name: "bar", primary: false)
merged = subject.merge(other)
merged_disks = merged.disks
expect(merged_disks.length).to eql(2)
expect(merged_disks[0].name).to eq("foo")
expect(merged_disks[1].name).to eq("bar")
end
end
describe "#synced_folder(s)" do
it "defaults to sharing the current directory" do
subject.finalize!

View File

@ -0,0 +1,21 @@
require File.expand_path("../../../base", __FILE__)
require "vagrant/util/numeric"
describe Vagrant::Util::Numeric do
include_context "unit"
before(:each) { described_class.reset! }
subject { described_class }
describe "#string_to_bytes" do
it "converts a string to the proper bytes" do
bytes = subject.string_to_bytes("10KB")
expect(bytes).to eq(10240)
end
it "returns nil if the given string is the wrong format" do
bytes = subject.string_to_bytes("10 Kilobytes")
expect(bytes).to eq(nil)
end
end
end

View File

@ -0,0 +1,76 @@
---
layout: "docs"
page_title: "Vagrant Disks Configuration"
sidebar_current: "disks-configuration"
description: |-
Documentation of various configuration options for Vagrant Disks
---
# 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.
Generally, the disk option accepts two kinds of ways to define a provider config:
+ `providername__diskoption: value`
- 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.
**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
The disk config currently accepts three kinds of disk types:
* `disk` (symbol)
* `dvd` (symbol)
* `floppy` (symbol)
You can set a disk type with the first argument of a disk config in your Vagrantfile:
```ruby
config.vm.disk :disk, name: "backup", size: "10GB"
config.vm.disk :floppy, name: "cool_files"
```
## Provider Author Guide
If you are a vagrant plugin author who maintains a provider for Vagrant, this short guide will hopefully give some information on how to use the internal disk config object.
<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.
</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
More information should be coming once the disk feature is more functional.

View File

@ -0,0 +1,34 @@
---
layout: "docs"
page_title: "Vagrant Disks"
sidebar_current: "disks"
description: |-
Introduction to Vagrant Disks
---
# Vagrant Disks
<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!
</div>
For more information about what options are available for configuring disks, see the
[configuration section](/docs/disks/configuration.html).

View File

@ -0,0 +1,34 @@
---
layout: "docs"
page_title: "Vagrant Disk Usage"
sidebar_current: "disks-usage"
description: |-
Various Vagrant Disk examples
---
# Basic 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="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>
Below are some very simple examples of how to use Vagrant Disks.
## Examples
- Resizing a disk (primary)
- Attaching a new disk
- Using provider specific options

View File

@ -128,6 +128,14 @@
</ul>
</li>
<li<%= sidebar_current("disks") %>>
<a href="/docs/disks/">Disks</a>
<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>
</ul>
</li>
<li<%= sidebar_current("multimachine") %>>
<a href="/docs/multi-machine/">Multi-Machine</a>
</li>