vagrant/website/source/docs/plugins/development-basics.html.md

7.2 KiB

layout page_title sidebar_current description
docs Plugin Development Basics - Plugins plugins-development-basics Plugins are a great way to augment or change the behavior and functionality of Vagrant. Since plugins introduce additional external dependencies for users, they should be used as a last resort when attempting to do something with Vagrant.

Plugin Development Basics

Plugins are a great way to augment or change the behavior and functionality of Vagrant. Since plugins introduce additional external dependencies for users, they should be used as a last resort when attempting to do something with Vagrant.

But if you need to introduce custom behaviors into Vagrant, plugins are the best way, since they are safe against future upgrades and use a stable API.

Warning: Advanced Topic! Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach.

Plugins are written using Ruby and are packaged using RubyGems. Familiarity with Ruby is required, but the packaging and distribution section should help guide you to packaging your plugin into a RubyGem.

Setup and Workflow

Because plugins are packaged as RubyGems, Vagrant plugins should be developed as if you were developing a regular RubyGem. The easiest way to do this is to use the bundle gem command.

Once the directory structure for a RubyGem is setup, you will want to modify your Gemfile. Here is the basic structure of a Gemfile for Vagrant plugin development:

source "https://rubygems.org"

group :development do
  gem "vagrant", git: "https://github.com/hashicorp/vagrant.git"
end

group :plugins do
  gem "my-vagrant-plugin", path: "."
end

This Gemfile gets "vagrant" for development. This allows you to bundle exec vagrant to run Vagrant with your plugin already loaded, so that you can test it manually that way.

The only thing about this Gemfile that may stand out as odd is the "plugins" group and putting your plugin in that group. Because vagrant plugin commands do not work in development, this is how you "install" your plugin into Vagrant. Vagrant will automatically load any gems listed in the "plugins" group. Note that this also allows you to add multiple plugins to Vagrant for development, if your plugin works with another plugin.

When you want to manually test your plugin, use bundle exec vagrant in order to run Vagrant with your plugin loaded (as we specified in the Gemfile).

Plugin Definition

All plugins are required to have a definition. A definition contains details about the plugin such as the name of it and what components it contains.

A definition at the bare minimum looks like the following:

class MyPlugin < Vagrant.plugin("2")
  name "My Plugin"
end

A definition is a class that inherits from Vagrant.plugin("2"). The "2" there is the version that the plugin is valid for. API stability is only promised for each major version of Vagrant, so this is important. (The 1.x series is working towards 2.0, so the API version is "2")

The most critical feature of a plugin definition is that it must always load, no matter what version of Vagrant is running. Theoretically, Vagrant version 87 (does not actually exist) would be able to load a version 2 plugin definition. This is achieved through clever lazy loading of individual components of the plugin, and is covered shortly.

Plugin Components

Within the definition, a plugin advertises what components it adds to Vagrant. An example is shown below where a command and provisioner are added:

class MyPlugin < Vagrant.plugin("2")
  name "My Plugin"

  command "run-my-plugin" do
    require_relative "command"
    Command
  end

  provisioner "my-provisioner" do
    require_relative "provisioner"
    Provisioner
  end
end

Let us go over the major pieces of what is going on here. Note from a general Ruby language perspective the above should be familiar. The syntax should not scare you. If it does, then please familiarize with Ruby further before attempting to write a plugin.

The first thing to note is that individual components are defined by making a method call with the component name, such as command or provisioner. These in turn take some parameters. In the case of our example it is just the name of the command and the name of the provisioner. All component definitions then take a block argument (a callback) that must return the actual component implementation class.

The block argument is where the "clever lazy loading" (mentioned above) comes into play. The component blocks should lazy load the actual file that contains the implementation of the component, and then return that component.

This is done because the actual dependencies and APIs used when defining components are not stable across major Vagrant versions. A command implementation written for Vagrant 2.0 will not be compatible with Vagrant 3.0 and so on. But the definition is just plain Ruby that must always be forward compatible to future Vagrant versions.

To repeat, the lazy loading aspect of plugin components is critical to the way Vagrant plugins work. All components must be lazily loaded and returned within their definition blocks.

Now, each component has a different API. Please visit the relevant section using the navigation to the left under "Plugins" to learn more about developing each type of component.

Error Handling

One of Vagrant's biggest strength is gracefully handling errors and reporting them in human-readable ways. Vagrant has always strongly believed that if a user sees a stack trace, it is a bug. It is expected that plugins will behave the same way, and Vagrant provides strong error handling mechanisms to assist with this.

Error handling in Vagrant is done entirely by raising Ruby exceptions. But Vagrant treats certain errors differently than others. If an error is raised that inherits from Vagrant::Errors::VagrantError, then the vagrant command will output the message of the error in nice red text to the console and exit with an exit status of 1.

Otherwise, Vagrant reports an "unexpected error" that should be reported as a bug, and shows a full stack trace and other ugliness. Any stack traces should be considered bugs.

Therefore, to fit into Vagrant's error handling mechanisms, subclass VagrantError and set a proper message on your exception. To see examples of this, look at Vagrant's built-in errors.

Console Input and Output

Most plugins are likely going to want to do some sort of input/output. Plugins should never use Ruby's built-in puts or gets style methods. Instead, all input/output should go through some sort of Vagrant UI object. The Vagrant UI object properly handles cases where there is no TTY, output pipes are closed, there is no input pipe, etc.

A UI object is available on every Vagrant::Environment via the ui property and is exposed within every middleware environment via the :ui key. UI objects have decent documentation within the comments of their source.