diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index 4b37c69f1..c0eaffcc8 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -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" diff --git a/lib/vagrant/action/builtin/disk.rb b/lib/vagrant/action/builtin/disk.rb new file mode 100644 index 000000000..a1abe7289 --- /dev/null +++ b/lib/vagrant/action/builtin/disk.rb @@ -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 diff --git a/lib/vagrant/util/numeric.rb b/lib/vagrant/util/numeric.rb new file mode 100644 index 000000000..3a2989d97 --- /dev/null +++ b/lib/vagrant/util/numeric.rb @@ -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 = /^(?[0-9]+)\s?(?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: "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 diff --git a/plugins/kernel_v2/config/disk.rb b/plugins/kernel_v2/config/disk.rb new file mode 100644 index 000000000..219a74aed --- /dev/null +++ b/plugins/kernel_v2/config/disk.rb @@ -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__ + # + # @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 diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index b80bda8ca..7c0e4e423 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -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 } diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index bb4e677af..edfbbcbe0 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -79,6 +79,7 @@ module VagrantPlugins b.use ForwardPorts b.use SetHostname b.use SaneDefaults + b.use Disk b.use Customize, "pre-boot" b.use Boot b.use Customize, "post-boot" diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 4d045e32b..333addb8a 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -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... diff --git a/test/unit/plugins/kernel_v2/config/disk_test.rb b/test/unit/plugins/kernel_v2/config/disk_test.rb new file mode 100644 index 000000000..88640d274 --- /dev/null +++ b/test/unit/plugins/kernel_v2/config/disk_test.rb @@ -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 diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index 00576cbb3..20775ce75 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -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! diff --git a/test/unit/vagrant/util/numeric_test.rb b/test/unit/vagrant/util/numeric_test.rb new file mode 100644 index 000000000..b13e4f9eb --- /dev/null +++ b/test/unit/vagrant/util/numeric_test.rb @@ -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 diff --git a/website/source/docs/disks/configuration.html.md b/website/source/docs/disks/configuration.html.md new file mode 100644 index 000000000..f9c05cecc --- /dev/null +++ b/website/source/docs/disks/configuration.html.md @@ -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 + +
+ Warning! 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. +
+ +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. + +
+ Warning! 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. +
+ +- 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. diff --git a/website/source/docs/disks/index.html.md b/website/source/docs/disks/index.html.md new file mode 100644 index 000000000..6c4f6b3f5 --- /dev/null +++ b/website/source/docs/disks/index.html.md @@ -0,0 +1,34 @@ +--- +layout: "docs" +page_title: "Vagrant Disks" +sidebar_current: "disks" +description: |- + Introduction to Vagrant Disks +--- + +# Vagrant Disks + +
+ Warning! 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. + + NOTE: 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! +
+ +For more information about what options are available for configuring disks, see the +[configuration section](/docs/disks/configuration.html). diff --git a/website/source/docs/disks/usage.html.md b/website/source/docs/disks/usage.html.md new file mode 100644 index 000000000..a5e001cdb --- /dev/null +++ b/website/source/docs/disks/usage.html.md @@ -0,0 +1,34 @@ +--- +layout: "docs" +page_title: "Vagrant Disk Usage" +sidebar_current: "disks-usage" +description: |- + Various Vagrant Disk examples +--- + +# Basic Usage + +
+ Warning! 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. +
+ +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 diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index d95ba8666..ff84ad238 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -128,6 +128,14 @@ + > + Disks + + + > Multi-Machine