diff --git a/lib/vagrant/config.rb b/lib/vagrant/config.rb index 4b0cc93b1..1bb6cd401 100644 --- a/lib/vagrant/config.rb +++ b/lib/vagrant/config.rb @@ -9,6 +9,7 @@ module Vagrant autoload :VersionBase, 'vagrant/config/version_base' autoload :V1, 'vagrant/config/v1' + autoload :V2, 'vagrant/config/v2' # This is a mutex used to guarantee that only one thread can load # procs at any given time. @@ -19,12 +20,13 @@ module Vagrant # `Vagrant.configure` calls. VERSIONS = Registry.new VERSIONS.register("1") { V1::Loader } + VERSIONS.register("2") { V2::Loader } # This is the order of versions. This is used by the loader to figure out # how to "upgrade" versions up to the desired (current) version. The # current version is always considered to be the last version in this # list. - VERSIONS_ORDER = ["1"] + VERSIONS_ORDER = ["1", "2"] CURRENT_VERSION = VERSIONS_ORDER.last # This is the method which is called by all Vagrantfiles to configure Vagrant. diff --git a/lib/vagrant/config/v2.rb b/lib/vagrant/config/v2.rb new file mode 100644 index 000000000..bb6c88b0c --- /dev/null +++ b/lib/vagrant/config/v2.rb @@ -0,0 +1,8 @@ +module Vagrant + module Config + module V2 + autoload :Loader, "vagrant/config/v2/loader" + autoload :Root, "vagrant/config/v2/root" + end + end +end diff --git a/lib/vagrant/config/v2/loader.rb b/lib/vagrant/config/v2/loader.rb new file mode 100644 index 000000000..64047705a --- /dev/null +++ b/lib/vagrant/config/v2/loader.rb @@ -0,0 +1,114 @@ +require "vagrant/config/v2/root" + +module Vagrant + module Config + module V2 + # This is the loader that handles configuration loading for V2 + # configurations. + class Loader < VersionBase + # Returns a bare empty configuration object. + # + # @return [V2::Root] + def self.init + new_root_object + end + + # Finalizes the configuration by making sure there is at least + # one VM defined in it. + def self.finalize(config) + # Call the `#finalize` method on each of the configuration keys. + # They're expected to modify themselves in our case. + config.finalize! + + # Return the object + config + end + + # Loads the configuration for the given proc and returns a configuration + # object. + # + # @param [Proc] config_proc + # @return [Object] + def self.load(config_proc) + # Create a root configuration object + root = new_root_object + + # Call the proc with the root + config_proc.call(root) + + # Return the root object, which doubles as the configuration object + # we actually use for accessing as well. + root + end + + # Merges two configuration objects. + # + # @param [V2::Root] old The older root config. + # @param [V2::Root] new The newer root config. + # @return [V2::Root] + def self.merge(old, new) + # Grab the internal states, we use these heavily throughout the process + old_state = old.__internal_state + new_state = new.__internal_state + + # The config map for the new object is the old one merged with the + # new one. + config_map = old_state["config_map"].merge(new_state["config_map"]) + + # Merge the keys. + old_keys = old_state["keys"] + new_keys = new_state["keys"] + keys = {} + old_keys.each do |key, old_value| + if new_keys.has_key?(key) + # We need to do a merge, which we expect to be available + # on the config class itself. + keys[key] = old_value.merge(new_keys[key]) + else + # We just take the old value, but dup it so that we can modify. + keys[key] = old_value.dup + end + end + + new_keys.each do |key, new_value| + # Add in the keys that the new class has that we haven't merged. + if !keys.has_key?(key) + keys[key] = new_value.dup + end + end + + # Return the final root object + V2::Root.new(config_map, keys) + end + + # Upgrade a V1 configuration to a V2 configuration. + # + # @param [V1::Root] old + # @return [Array] A 3-tuple result. + def self.upgrade(old) + # TODO: Actually do an upgrade. For now we just return V1. + [old, [], []] + end + + protected + + def self.new_root_object + # Get all the registered configuration objects and use them. If + # we're currently on version 1, then we load all the config objects, + # otherwise we load only the upgrade safe ones, since we're + # obviously being loaded for an upgrade. + config_map = nil + plugin_manager = Vagrant.plugin("1").manager + if Config::CURRENT_VERSION == "1" + config_map = plugin_manager.config + else + config_map = plugin_manager.config_upgrade_safe + end + + # Create the configuration root object + V2::Root.new(config_map) + end + end + end + end +end diff --git a/lib/vagrant/config/v2/root.rb b/lib/vagrant/config/v2/root.rb new file mode 100644 index 000000000..32b804108 --- /dev/null +++ b/lib/vagrant/config/v2/root.rb @@ -0,0 +1,75 @@ +module Vagrant + module Config + module V2 + # This is the root configuration class. An instance of this is what + # is passed into version 1 Vagrant configuration blocks. + class Root + # Initializes a root object that maps the given keys to specific + # configuration classes. + # + # @param [Hash] config_map Map of key to config class. + def initialize(config_map, keys=nil) + @keys = keys || {} + @config_map = config_map + end + + # We use method_missing as a way to get the configuration that is + # used for Vagrant and load the proper configuration classes for + # each. + def method_missing(name, *args) + return @keys[name] if @keys.has_key?(name) + + config_klass = @config_map[name.to_sym] + if config_klass + # Instantiate the class and return the instance + @keys[name] = config_klass.new + return @keys[name] + else + # Super it up to probably raise a NoMethodError + super + end + end + + # Called to finalize this object just prior to it being used by + # the Vagrant system. The "!" signifies that this is expected to + # mutate itself. + def finalize! + @keys.each do |_key, instance| + instance.finalize! + end + end + + # Validates the configuration classes of this instance and raises an + # exception if they are invalid. If you are implementing a custom configuration + # class, the method you want to implement is {Base#validate}. This is + # the method that checks all the validation, not one which defines + # validation rules. + def validate!(env) + # Validate each of the configured classes and store the results into + # a hash. + errors = @keys.inject({}) do |container, data| + key, instance = data + recorder = ErrorRecorder.new + instance.validate(env, recorder) + container[key.to_sym] = recorder if !recorder.errors.empty? + container + end + + return if errors.empty? + raise Errors::ConfigValidationFailed, :messages => Util::TemplateRenderer.render("config/validation_failed", :errors => errors) + end + + # Returns the internal state of the root object. This is used + # by outside classes when merging, and shouldn't be called directly. + # Note the strange method name is to attempt to avoid any name + # clashes with potential configuration keys. + def __internal_state + { + "config_map" => @config_map, + "keys" => @keys + } + end + end + end + end +end diff --git a/lib/vagrant/plugin.rb b/lib/vagrant/plugin.rb index de85c72ac..5538746a5 100644 --- a/lib/vagrant/plugin.rb +++ b/lib/vagrant/plugin.rb @@ -1,5 +1,6 @@ module Vagrant module Plugin autoload :V1, "vagrant/plugin/v1" + autoload :V2, "vagrant/plugin/v2" end end diff --git a/lib/vagrant/plugin/v2.rb b/lib/vagrant/plugin/v2.rb new file mode 100644 index 000000000..917caf7a6 --- /dev/null +++ b/lib/vagrant/plugin/v2.rb @@ -0,0 +1,19 @@ +require "log4r" + +require "vagrant/plugin/v2/errors" + +module Vagrant + module Plugin + module V2 + autoload :Command, "vagrant/plugin/v2/command" + autoload :Communicator, "vagrant/plugin/v2/communicator" + autoload :Config, "vagrant/plugin/v2/config" + autoload :Guest, "vagrant/plugin/v2/guest" + autoload :Host, "vagrant/plugin/v2/host" + autoload :Manager, "vagrant/plugin/v2/manager" + autoload :Plugin, "vagrant/plugin/v2/plugin" + autoload :Provider, "vagrant/plugin/v2/provider" + autoload :Provisioner, "vagrant/plugin/v2/provisioner" + end + end +end diff --git a/lib/vagrant/plugin/v2/command.rb b/lib/vagrant/plugin/v2/command.rb new file mode 100644 index 000000000..08b0e194a --- /dev/null +++ b/lib/vagrant/plugin/v2/command.rb @@ -0,0 +1,169 @@ +require 'log4r' + +require "vagrant/util/safe_puts" + +module Vagrant + module Plugin + module V2 + # This is the base class for a CLI command. + class Command + include Util::SafePuts + + def initialize(argv, env) + @argv = argv + @env = env + @logger = Log4r::Logger.new("vagrant::command::#{self.class.to_s.downcase}") + end + + # This is what is called on the class to actually execute it. Any + # subclasses should implement this method and do any option parsing + # and validation here. + def execute + end + + protected + + # Parses the options given an OptionParser instance. + # + # This is a convenience method that properly handles duping the + # originally argv array so that it is not destroyed. + # + # This method will also automatically detect "-h" and "--help" + # and print help. And if any invalid options are detected, the help + # will be printed, as well. + # + # If this method returns `nil`, then you should assume that help + # was printed and parsing failed. + def parse_options(opts=nil) + # Creating a shallow copy of the arguments so the OptionParser + # doesn't destroy the originals. + argv = @argv.dup + + # Default opts to a blank optionparser if none is given + opts ||= OptionParser.new + + # Add the help option, which must be on every command. + opts.on_tail("-h", "--help", "Print this help") do + safe_puts(opts.help) + return nil + end + + opts.parse!(argv) + return argv + rescue OptionParser::InvalidOption + raise Errors::CLIInvalidOptions, :help => opts.help.chomp + end + + # Yields a VM for each target VM for the command. + # + # This is a convenience method for easily implementing methods that + # take a target VM (in the case of multi-VM) or every VM if no + # specific VM name is specified. + # + # @param [String] name The name of the VM. Nil if every VM. + # @param [Boolean] single_target If true, then an exception will be + # raised if more than one target is found. + def with_target_vms(names=nil, options=nil) + # Using VMs requires a Vagrant environment to be properly setup + raise Errors::NoEnvironmentError if !@env.root_path + + # Setup the options hash + options ||= {} + + # Require that names be an array + names ||= [] + names = [names] if !names.is_a?(Array) + + # First determine the proper array of VMs. + vms = [] + if names.length > 0 + names.each do |name| + if pattern = name[/^\/(.+?)\/$/, 1] + # This is a regular expression name, so we convert to a regular + # expression and allow that sort of matching. + regex = Regexp.new(pattern) + + @env.vms.each do |vm_name, vm| + vms << vm if vm_name =~ regex + end + + raise Errors::VMNoMatchError if vms.empty? + else + # String name, just look for a specific VM + vms << @env.vms[name.to_sym] + raise Errors::VMNotFoundError, :name => name if !vms[0] + end + end + else + vms = @env.vms_ordered + end + + # Make sure we're only working with one VM if single target + if options[:single_target] && vms.length != 1 + vm = @env.primary_vm + raise Errors::MultiVMTargetRequired if !vm + vms = [vm] + end + + # If we asked for reversed ordering, then reverse it + vms.reverse! if options[:reverse] + + # Go through each VM and yield it! + vms.each do |old_vm| + # We get a new VM from the environment here to avoid potentially + # stale VMs (if there was a config reload on the environment + # or something). + vm = @env.vms[old_vm.name] + yield vm + end + end + + # This method will split the argv given into three parts: the + # flags to this command, the subcommand, and the flags to the + # subcommand. For example: + # + # -v status -h -v + # + # The above would yield 3 parts: + # + # ["-v"] + # "status" + # ["-h", "-v"] + # + # These parts are useful because the first is a list of arguments + # given to the current command, the second is a subcommand, and the + # third are the commands given to the subcommand. + # + # @return [Array] The three parts. + def split_main_and_subcommand(argv) + # Initialize return variables + main_args = nil + sub_command = nil + sub_args = [] + + # We split the arguments into two: One set containing any + # flags before a word, and then the rest. The rest are what + # get actually sent on to the subcommand. + argv.each_index do |i| + if !argv[i].start_with?("-") + # We found the beginning of the sub command. Split the + # args up. + main_args = argv[0, i] + sub_command = argv[i] + sub_args = argv[i + 1, argv.length - i + 1] + + # Break so we don't find the next non flag and shift our + # main args. + break + end + end + + # Handle the case that argv was empty or didn't contain any subcommand + main_args = argv.dup if main_args.nil? + + return [main_args, sub_command, sub_args] + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/communicator.rb b/lib/vagrant/plugin/v2/communicator.rb new file mode 100644 index 000000000..97c84d4d4 --- /dev/null +++ b/lib/vagrant/plugin/v2/communicator.rb @@ -0,0 +1,98 @@ +module Vagrant + module Plugin + module V2 + # Base class for a communicator in Vagrant. A communicator is + # responsible for communicating with a machine in some way. There + # are various stages of Vagrant that require things such as uploading + # files to the machine, executing shell commands, etc. Implementors + # of this class are expected to provide this functionality in some + # way. + # + # Note that a communicator must provide **all** of the methods + # in this base class. There is currently no way for one communicator + # to provide say a more efficient way of uploading a file, but not + # provide shell execution. This sort of thing will come in a future + # version. + class Communicator + # This returns true/false depending on if the given machine + # can be communicated with using this communicator. If this returns + # `true`, then this class will be used as the primary communication + # method for the machine. + # + # @return [Boolean] + def self.match?(machine) + false + end + + # Initializes the communicator with the machine that we will be + # communicating with. This base method does nothing (it doesn't + # even store the machine in an instance variable for you), so you're + # expected to override this and do something with the machine if + # you care about it. + # + # @param [Machine] machine The machine this instance is expected to + # communicate with. + def initialize(machine) + end + + # Checks if the target machine is ready for communication. If this + # returns true, then all the other methods for communicating with + # the machine are expected to be functional. + # + # @return [Boolean] + def ready? + false + end + + # Download a file from the remote machine to the local machine. + # + # @param [String] from Path of the file on the remote machine. + # @param [String] to Path of where to save the file locally. + def download(from, to) + end + + # Upload a file to the remote machine. + # + # @param [String] from Path of the file locally to upload. + # @param [String] to Path of where to save the file on the remote + # machine. + def upload(from, to) + end + + # Execute a command on the remote machine. The exact semantics + # of this method are up to the implementor, but in general the + # users of this class will expect this to be a shell. + # + # This method gives you no way to write data back to the remote + # machine, so only execute commands that don't expect input. + # + # @param [String] command Command to execute. + # @yield [type, data] Realtime output of the command being executed. + # @yieldparam [String] type Type of the output. This can be + # `:stdout`, `:stderr`, etc. The exact types are up to the + # implementor. + # @yieldparam [String] data Data for the given output. + # @return [Integer] Exit code of the command. + def execute(command, opts=nil) + end + + # Executes a command on the remote machine with administrative + # privileges. See {#execute} for documentation, as the API is the + # same. + # + # @see #execute + def sudo(command, opts=nil) + end + + # Executes a command and returns true if the command succeeded, + # and false otherwise. By default, this executes as a normal user, + # and it is up to the communicator implementation if they expose an + # option for running tests as an administrator. + # + # @see #execute + def test(command, opts=nil) + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/config.rb b/lib/vagrant/plugin/v2/config.rb new file mode 100644 index 000000000..4912969f1 --- /dev/null +++ b/lib/vagrant/plugin/v2/config.rb @@ -0,0 +1,94 @@ +module Vagrant + module Plugin + module V2 + # This is the base class for a configuration key defined for + # V1. Any configuration key plugins for V1 should inherit from this + # class. + class Config + # This is called as a last-minute hook that allows the configuration + # object to finalize itself before it will be put into use. This is + # a useful place to do some defaults in the case the user didn't + # configure something or so on. + # + # An example of where this sort of thing is used or has been used: + # the "vm" configuration key uses this to make sure that at least + # one sub-VM has been defined: the default VM. + # + # The configuration object is expected to mutate itself. + def finalize! + # Default implementation is to do nothing. + end + + # Merge another configuration object into this one. This assumes that + # the other object is the same class as this one. This should not + # mutate this object, but instead should return a new, merged object. + # + # The default implementation will simply iterate over the instance + # variables and merge them together, with this object overriding + # any conflicting instance variables of the older object. Instance + # variables starting with "__" (double underscores) will be ignored. + # This lets you set some sort of instance-specific state on your + # configuration keys without them being merged together later. + # + # @param [Object] other The other configuration object to merge from, + # this must be the same type of object as this one. + # @return [Object] The merged object. + def merge(other) + result = self.class.new + + # Set all of our instance variables on the new class + [self, other].each do |obj| + obj.instance_variables.each do |key| + # Ignore keys that start with a double underscore. This allows + # configuration classes to still hold around internal state + # that isn't propagated. + if !key.to_s.start_with?("@__") + result.instance_variable_set(key, obj.instance_variable_get(key)) + end + end + end + + result + end + + # Allows setting options from a hash. By default this simply calls + # the `#{key}=` method on the config class with the value, which is + # the expected behavior most of the time. + # + # This is expected to mutate itself. + # + # @param [Hash] options A hash of options to set on this configuration + # key. + def set_options(options) + options.each do |key, value| + send("#{key}=", value) + end + end + + # Converts this configuration object to JSON. + def to_json(*a) + instance_variables_hash.to_json(*a) + end + + # Returns the instance variables as a hash of key-value pairs. + def instance_variables_hash + instance_variables.inject({}) do |acc, iv| + acc[iv.to_s[1..-1]] = instance_variable_get(iv) + acc + end + end + + # Called after the configuration is finalized and loaded to validate + # this object. + # + # @param [Environment] env Vagrant::Environment object of the + # environment that this configuration has been loaded into. This + # gives you convenient access to things like the the root path + # and so on. + # @param [ErrorRecorder] errors + def validate(env, errors) + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/errors.rb b/lib/vagrant/plugin/v2/errors.rb new file mode 100644 index 000000000..afe4936a4 --- /dev/null +++ b/lib/vagrant/plugin/v2/errors.rb @@ -0,0 +1,18 @@ +# This file contains all the errors that the V2 plugin interface +# may throw. + +module Vagrant + module Plugin + module V2 + # Exceptions that can be thrown within the plugin interface all + # inherit from this parent exception. + class Error < StandardError; end + + # This is thrown when a command name given is invalid. + class InvalidCommandName < Error; end + + # This is thrown when a hook "position" is invalid. + class InvalidEasyHookPosition < Error; end + end + end +end diff --git a/lib/vagrant/plugin/v2/guest.rb b/lib/vagrant/plugin/v2/guest.rb new file mode 100644 index 000000000..f757296dd --- /dev/null +++ b/lib/vagrant/plugin/v2/guest.rb @@ -0,0 +1,92 @@ +module Vagrant + module Plugin + module V2 + # The base class for a guest. A guest represents an installed system + # within a machine that Vagrant manages. There are some portions of + # Vagrant which are OS-specific such as mountaing shared folders and + # halting the machine, and this abstraction allows the implementation + # for these to be seperate from the core of Vagrant. + class Guest + class BaseError < Errors::VagrantError + error_namespace("vagrant.guest.base") + end + + include Vagrant::Util + + # The VM which this system is tied to. + attr_reader :vm + + # Initializes the system. Any subclasses MUST make sure this + # method is called on the parent. Therefore, if a subclass overrides + # `initialize`, then you must call `super`. + def initialize(vm) + @vm = vm + end + + # This method is automatically called when the system is available (when + # Vagrant can successfully SSH into the machine) to give the system a chance + # to determine the distro and return a distro-specific system. + # + # If this method returns nil, then this instance is assumed to be + # the most specific guest implementation. + def distro_dispatch + end + + # Halt the machine. This method should gracefully shut down the + # operating system. This method will cause `vagrant halt` and associated + # commands to _block_, meaning that if the machine doesn't halt + # in a reasonable amount of time, this method should just return. + # + # If when this method returns, the machine's state isn't "powered_off," + # Vagrant will proceed to forcefully shut the machine down. + def halt + raise BaseError, :_key => :unsupported_halt + end + + # Mounts a shared folder. + # + # This method should create, mount, and properly set permissions + # on the shared folder. This method should also properly + # adhere to any configuration values such as `shared_folder_uid` + # on `config.vm`. + # + # @param [String] name The name of the shared folder. + # @param [String] guestpath The path on the machine which the user + # wants the folder mounted. + # @param [Hash] options Additional options for the shared folder + # which can be honored. + def mount_shared_folder(name, guestpath, options) + raise BaseError, :_key => :unsupported_shared_folder + end + + # Mounts a shared folder via NFS. This assumes that the exports + # via the host are already done. + def mount_nfs(ip, folders) + raise BaseError, :_key => :unsupported_nfs + end + + # Configures the given list of networks on the virtual machine. + # + # The networks parameter will be an array of hashes where the hashes + # represent the configuration of a network interface. The structure + # of the hash will be roughly the following: + # + # { + # :type => :static, + # :ip => "192.168.33.10", + # :netmask => "255.255.255.0", + # :interface => 1 + # } + # + def configure_networks(networks) + raise BaseError, :_key => :unsupported_configure_networks + end + + # Called to change the hostname of the virtual machine. + def change_host_name(name) + raise BaseError, :_key => :unsupported_host_name + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/host.rb b/lib/vagrant/plugin/v2/host.rb new file mode 100644 index 000000000..53a526885 --- /dev/null +++ b/lib/vagrant/plugin/v2/host.rb @@ -0,0 +1,66 @@ +module Vagrant + module Plugin + module V2 + # Base class for a host in Vagrant. A host class contains functionality + # that is specific to a specific OS that is running Vagrant. This + # abstraction is done becauase there is some host-specific logic that + # Vagrant must do in some cases. + class Host + # This returns true/false depending on if the current running system + # matches the host class. + # + # @return [Boolean] + def self.match? + nil + end + + # The precedence of the host when checking for matches. This is to + # allow certain host such as generic OS's ("Linux", "BSD", etc.) + # to be specified last. + # + # The hosts with the higher numbers will be checked first. + # + # If you're implementing a basic host, you can probably ignore this. + def self.precedence + 5 + end + + # Initializes a new host class. + # + # The only required parameter is a UI object so that the host + # objects have some way to communicate with the outside world. + # + # @param [UI] ui UI for the hosts to output to. + def initialize(ui) + @ui = ui + end + + # Returns true of false denoting whether or not this host supports + # NFS shared folder setup. This method ideally should verify that + # NFS is installed. + # + # @return [Boolean] + def nfs? + false + end + + # Exports the given hash of folders via NFS. + # + # @param [String] id A unique ID that is guaranteed to be unique to + # match these sets of folders. + # @param [String] ip IP of the guest machine. + # @param [Hash] folders Shared folders to sync. + def nfs_export(id, ip, folders) + end + + # Prunes any NFS exports made by Vagrant which aren't in the set + # of valid ids given. + # + # @param [Array] valid_ids Valid IDs that should not be + # pruned. + def nfs_prune(valid_ids) + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb new file mode 100644 index 000000000..f8f1ff7f8 --- /dev/null +++ b/lib/vagrant/plugin/v2/manager.rb @@ -0,0 +1,131 @@ +require "log4r" + +module Vagrant + module Plugin + module V2 + # This class maintains a list of all the registered plugins as well + # as provides methods that allow querying all registered components of + # those plugins as a single unit. + class Manager + attr_reader :registered + + def initialize + @logger = Log4r::Logger.new("vagrant::plugin::v2::manager") + @registered = [] + end + + # This returns all the registered communicators. + # + # @return [Hash] + def communicators + result = {} + + @registered.each do |plugin| + result.merge!(plugin.communicator.to_hash) + end + + result + end + + # This returns all the registered configuration classes. + # + # @return [Hash] + def config + result = {} + + @registered.each do |plugin| + plugin.config.each do |key, klass| + result[key] = klass + end + end + + result + end + + # This returns all the registered configuration classes that were + # marked as "upgrade safe." + # + # @return [Hash] + def config_upgrade_safe + result = {} + + @registered.each do |plugin| + configs = plugin.data[:config_upgrade_safe] + if configs + configs.each do |key| + result[key] = plugin.config.get(key) + end + end + end + + result + end + + # This returns all the registered guests. + # + # @return [Hash] + def guests + result = {} + + @registered.each do |plugin| + result.merge!(plugin.guest.to_hash) + end + + result + end + + # This returns all registered host classes. + # + # @return [Hash] + def hosts + hosts = {} + + @registered.each do |plugin| + hosts.merge!(plugin.host.to_hash) + end + + hosts + end + + # This returns all registered providers. + # + # @return [Hash] + def providers + providers = {} + + @registered.each do |plugin| + providers.merge!(plugin.provider.to_hash) + end + + providers + end + + # This registers a plugin. This should _NEVER_ be called by the public + # and should only be called from within Vagrant. Vagrant will + # automatically register V2 plugins when a name is set on the + # plugin. + def register(plugin) + if !@registered.include?(plugin) + @logger.info("Registered plugin: #{plugin.name}") + @registered << plugin + end + end + + # This clears out all the registered plugins. This is only used by + # unit tests and should not be called directly. + def reset! + @registered.clear + end + + # This unregisters a plugin so that its components will no longer + # be used. Note that this should only be used for testing purposes. + def unregister(plugin) + if @registered.include?(plugin) + @logger.info("Unregistered: #{plugin.name}") + @registered.delete(plugin) + end + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb new file mode 100644 index 000000000..0b191b8b7 --- /dev/null +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -0,0 +1,259 @@ +require "log4r" + +module Vagrant + module Plugin + module V2 + # This is the superclass for all V2 plugins. + class Plugin + # Special marker that can be used for action hooks that matches + # all action sequences. + ALL_ACTIONS = :__all_actions__ + + # The logger for this class. + LOGGER = Log4r::Logger.new("vagrant::plugin::v2::plugin") + + # Set the root class up to be ourself, so that we can reference this + # from within methods which are probably in subclasses. + ROOT_CLASS = self + + # This returns the manager for all V2 plugins. + # + # @return [V2::Manager] + def self.manager + @manager ||= Manager.new + end + + # Set the name of the plugin. The moment that this is called, the + # plugin will be registered and available. Before this is called, a + # plugin does not exist. The name must be unique among all installed + # plugins. + # + # @param [String] name Name of the plugin. + # @return [String] The name of the plugin. + def self.name(name=UNSET_VALUE) + # Get or set the value first, so we have a name for logging when + # we register. + result = get_or_set(:name, name) + + # The plugin should be registered if we're setting a real name on it + Plugin.manager.register(self) if name != UNSET_VALUE + + # Return the result + result + end + + # Sets a human-friendly descrition of the plugin. + # + # @param [String] value Description of the plugin. + # @return [String] Description of the plugin. + def self.description(value=UNSET_VALUE) + get_or_set(:description, value) + end + + # Registers a callback to be called when a specific action sequence + # is run. This allows plugin authors to hook into things like VM + # bootup, VM provisioning, etc. + # + # @param [Symbol] name Name of the action. + # @return [Array] List of the hooks for the given action. + def self.action_hook(name, &block) + # Get the list of hooks for the given hook name + data[:action_hooks] ||= {} + hooks = data[:action_hooks][name.to_sym] ||= [] + + # Return the list if we don't have a block + return hooks if !block_given? + + # Otherwise add the block to the list of hooks for this action. + hooks << block + end + + # Defines additional command line commands available by key. The key + # becomes the subcommand, so if you register a command "foo" then + # "vagrant foo" becomes available. + # + # @param [String] name Subcommand key. + def self.command(name=UNSET_VALUE, &block) + data[:command] ||= Registry.new + + if name != UNSET_VALUE + # Validate the name of the command + if name.to_s !~ /^[-a-z0-9]+$/i + raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens" + end + + # Register a new command class only if a name was given. + data[:command].register(name.to_sym, &block) + end + + # Return the registry + data[:command] + end + + # Defines additional communicators to be available. Communicators + # should be returned by a block passed to this method. This is done + # to ensure that the class is lazy loaded, so if your class inherits + # from or uses any Vagrant internals specific to Vagrant 1.0, then + # the plugin can still be defined without breaking anything in future + # versions of Vagrant. + # + # @param [String] name Communicator name. + def self.communicator(name=UNSET_VALUE, &block) + data[:communicator] ||= Registry.new + + # Register a new communicator class only if a name was given. + data[:communicator].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:communicator] + end + + # Defines additional configuration keys to be available in the + # Vagrantfile. The configuration class should be returned by a + # block passed to this method. This is done to ensure that the class + # is lazy loaded, so if your class inherits from any classes that + # are specific to Vagrant 1.0, then the plugin can still be defined + # without breaking anything in future versions of Vagrant. + # + # @param [String] name Configuration key. + # @param [Boolean] upgrade_safe If this is true, then this configuration + # key is safe to load during an upgrade, meaning that it depends + # on NO Vagrant internal classes. Do _not_ set this to true unless + # you really know what you're doing, since you can cause Vagrant + # to crash (although Vagrant will output a user-friendly error + # message if this were to happen). + def self.config(name=UNSET_VALUE, upgrade_safe=false, &block) + data[:config] ||= Registry.new + + # Register a new config class only if a name was given. + if name != UNSET_VALUE + data[:config].register(name.to_sym, &block) + + # If we were told this is an upgrade safe configuration class + # then we add it to the set. + if upgrade_safe + data[:config_upgrade_safe] ||= Set.new + data[:config_upgrade_safe].add(name.to_sym) + end + end + + # Return the registry + data[:config] + end + + # Defines an "easy hook," which gives an easier interface to hook + # into action sequences. + def self.easy_hook(position, name, &block) + if ![:before, :after].include?(position) + raise InvalidEasyHookPosition, "must be :before, :after" + end + + # This is the command sent to sequences to insert + insert_method = "insert_#{position}".to_sym + + # Create the hook + hook = Easy.create_hook(&block) + + # Define an action hook that listens to all actions and inserts + # the hook properly if the sequence contains what we're looking for + action_hook(ALL_ACTIONS) do |seq| + index = seq.index(name) + seq.send(insert_method, index, hook) if index + end + end + + # Defines an "easy command," which is a command with limited + # functionality but far less boilerplate required over traditional + # commands. Easy commands let you make basic commands quickly and + # easily. + # + # @param [String] name Name of the command, how it will be invoked + # on the command line. + def self.easy_command(name, &block) + command(name) { Easy.create_command(name, &block) } + end + + # Defines an additionally available guest implementation with + # the given key. + # + # @param [String] name Name of the guest. + def self.guest(name=UNSET_VALUE, &block) + data[:guests] ||= Registry.new + + # Register a new guest class only if a name was given + data[:guests].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:guests] + end + + # Defines an additionally available host implementation with + # the given key. + # + # @param [String] name Name of the host. + def self.host(name=UNSET_VALUE, &block) + data[:hosts] ||= Registry.new + + # Register a new host class only if a name was given + data[:hosts].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:hosts] + end + + # Registers additional providers to be available. + # + # @param [Symbol] name Name of the provider. + def self.provider(name=UNSET_VALUE, &block) + data[:providers] ||= Registry.new + + # Register a new provider class only if a name was given + data[:providers].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:providers] + end + + # Registers additional provisioners to be available. + # + # @param [String] name Name of the provisioner. + def self.provisioner(name=UNSET_VALUE, &block) + data[:provisioners] ||= Registry.new + + # Register a new provisioner class only if a name was given + data[:provisioners].register(name.to_sym, &block) if name != UNSET_VALUE + + # Return the registry + data[:provisioners] + end + + # Returns the internal data associated with this plugin. This + # should NOT be called by the general public. + # + # @return [Hash] + def self.data + @data ||= {} + end + + protected + + # Sentinel value denoting that a value has not been set. + UNSET_VALUE = Object.new + + # Helper method that will set a value if a value is given, or otherwise + # return the already set value. + # + # @param [Symbol] key Key for the data + # @param [Object] value Value to store. + # @return [Object] Stored value. + def self.get_or_set(key, value=UNSET_VALUE) + # If no value is to be set, then return the value we have already set + return data[key] if value.eql?(UNSET_VALUE) + + # Otherwise set the value + data[key] = value + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/provider.rb b/lib/vagrant/plugin/v2/provider.rb new file mode 100644 index 000000000..374d126bb --- /dev/null +++ b/lib/vagrant/plugin/v2/provider.rb @@ -0,0 +1,68 @@ +module Vagrant + module Plugin + module V2 + # This is the base class for a provider for the V2 API. A provider + # is responsible for creating compute resources to match the needs + # of a Vagrant-configured system. + class Provider + # Initialize the provider to represent the given machine. + # + # @param [Vagrant::Machine] machine The machine that this provider + # is responsible for. + def initialize(machine) + end + + # This should return an action callable for the given name. + # + # @param [Symbol] name Name of the action. + # @return [Object] A callable action sequence object, whether it + # is a proc, object, etc. + def action(name) + nil + end + + # This method is called if the underying machine ID changes. Providers + # can use this method to load in new data for the actual backing + # machine or to realize that the machine is now gone (the ID can + # become `nil`). No parameters are given, since the underlying machine + # is simply the machine instance given to this object. And no + # return value is necessary. + def machine_id_changed + end + + # This should return a hash of information that explains how to + # SSH into the machine. If the machine is not at a point where + # SSH is even possible, then `nil` should be returned. + # + # The general structure of this returned hash should be the + # following: + # + # { + # :host => "1.2.3.4", + # :port => "22", + # :username => "mitchellh", + # :private_key_path => "/path/to/my/key" + # } + # + # **Note:** Vagrant only supports private key based authentication, + # mainly for the reason that there is no easy way to exec into an + # `ssh` prompt with a password, whereas we can pass a private key + # via commandline. + # + # @return [Hash] SSH information. For the structure of this hash + # read the accompanying documentation for this method. + def ssh_info + nil + end + + # This should return the state of the machine within this provider. + # The state can be any symbol. + # + # @return [Symbol] + def state + nil + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/provisioner.rb b/lib/vagrant/plugin/v2/provisioner.rb new file mode 100644 index 000000000..af4975ce9 --- /dev/null +++ b/lib/vagrant/plugin/v2/provisioner.rb @@ -0,0 +1,50 @@ +module Vagrant + module Plugin + module V2 + # This is the base class for a provisioner for the V2 API. A provisioner + # is primarily responsible for installing software on a Vagrant guest. + class Provisioner + # The environment which provisioner is running in. This is the + # action environment, not a Vagrant::Environment. + attr_reader :env + + # The configuration for this provisioner. This will be an instance of + # the `Config` class which is part of the provisioner. + attr_reader :config + + def initialize(env, config) + @env = env + @config = config + end + + # This method is expected to return a class that is used for + # configuring the provisioner. This return value is expected to be + # a subclass of {Config}. + # + # @return [Config] + def self.config_class + end + + # This is the method called to "prepare" the provisioner. This is called + # before any actions are run by the action runner (see {Vagrant::Actions::Runner}). + # This can be used to setup shared folders, forward ports, etc. Whatever is + # necessary on a "meta" level. + # + # No return value is expected. + def prepare + end + + # This is the method called to provision the system. This method + # is expected to do whatever necessary to provision the system (create files, + # SSH, etc.) + def provision! + end + + # This is the method called to when the system is being destroyed + # and allows the provisioners to engage in any cleanup tasks necessary. + def cleanup + end + end + end + end +end diff --git a/test/unit/vagrant/config/v1/loader_test.rb b/test/unit/vagrant/config/v1/loader_test.rb index 155ec5b51..65b937313 100644 --- a/test/unit/vagrant/config/v1/loader_test.rb +++ b/test/unit/vagrant/config/v1/loader_test.rb @@ -5,6 +5,11 @@ require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V1::Loader do include_context "unit" + before(:each) do + # Force the V1 loader to believe that we are in V1 + stub_const("Vagrant::Config::CURRENT_VERSION", "1") + end + describe "empty" do it "returns an empty configuration object" do result = described_class.init