The Vagrant Plugin is an extension that allows you to add functionality to Vagrant.
Recently, mutagen is used for file synchronization when starting a VM, and the mutagen project is linked with starting / stopping the VM [vagrant-mutagen plugin](https://github.com/dasginganinja/ I was thinking of using vagrant-mutagen).
However, it didn't work properly on Windows, and I was using the modified version with PR, but it still starts the VM. I was in trouble because I had to elevate my administrator privileges twice each time.
I decided to fork and create a Vagrant Plugin at will, and I decided to summarize what I investigated at that time.
The source code of the forked plugin can be found at here, so please refer to it as an example.
vagrant-XXX by convention.For the outline of how to make it, I referred to the one described below.
https://www.vagrantup.com/docs/plugins/development-basics
my-vagrant-plugin is the name of the plugin you create.
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
It requires Vagrant for development, and in development mode the vagrant plugin command doesn't work and instead the gem specified in the plugins group is loaded.
I didn't usually have Ruby development in the Windows environment, so when developing the Vagrant plugin, I used the one installed on Windows, and each time I gem build the Plugin and uninstall / install it with vagrant. ..
gem build <SOME_PLUGIN_NAME> .gemspec to save <SOME_PLUGIN_NAME> -X.Y.Z.gemvagrant plugin install <PATH_TO_SOME_PLUGIN> / <SOME_PLUGIN_NAME>-X.Y.Z.gem will install the Plugin(I thought I wouldn't make a big fix, but looking back, I think it was easier to get Vagrant running in development mode.)
Define the class that is the main body of Plugin, and specify the component class name such as Config, Provisioner in the class.
You need to require the Plugin class from the file that corresponds to the name listed in gemspec.
lib/<SOME_PLUGIN_NAME>/plugin.rb
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
In the vagrant-mutagen-utilizer I created, it looks like this:
lib/vagrant_mutagen_utilizer/plugin.rb
# frozen_string_literal: true
require_relative 'action/update_config'
require_relative 'action/remove_config'
require_relative 'action/start_orchestration'
require_relative 'action/terminate_orchestration'
require_relative 'action/save_machine_identifier'
module VagrantPlugins
  module MutagenUtilizer
    # Plugin to utilize mutagen
    class MutagenUtilizerPlugin < Vagrant.plugin('2')
      name 'Mutagen Utilizer'
      description <<-DESC
        This plugin manages the ~/.ssh/config file for the host machine. An entry is
        created for the hostname attribute in the vm.config.
      DESC
      config(:mutagen_utilizer) do
        require_relative 'config'
        Config
      end
      action_hook(:mutagen_utilizer, :machine_action_up) do |hook|
        hook.append(Action::UpdateConfig)
        hook.append(Action::StartOrchestration)
      end
        : <snip>
    end
  end
end
vagrant-mutagen-utilizer.gemspec
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'vagrant_mutagen_utilizer/version'
Gem::Specification.new do |spec|
  spec.name          = 'vagrant-mutagen-utilizer'
    : <snip>
end
lib/vagrant-mutagen-utilizer.rb
# frozen_string_literal: true
require 'vagrant_mutagen_utilizer/version'
require 'vagrant_mutagen_utilizer/plugin'
module VagrantPlugins
  # MutagenUtilizer
  module MutagenUtilizer
    def self.source_root
      @source_root ||= Pathname.new(File.expand_path('..', __dir__))
    end
  end
end
Forked vagrant-mutagen was originally like that, but instead of describing various processes in the Plugin body, class is described separately for each process, read using require_relative, and registered as a component. ..
Specify the class name on the Plugin body side.
The "foo" specified here corresponds to config.foo. ~ In the Vagrantfile.
config "foo" do
  require_relative "config"
  Config
end
The Config component is defined as a subclass of Vagrant.plugin (2,: config).
class Config < Vagrant.plugin(2, :config)
  attr_accessor :widgets
  def initialize
    @widgets = UNSET_VALUE
  end
  def finalize!
    @widgets = 0 if @widgets == UNSET_VALUE
  end
end
UNSET_VALUE is a value that points to undefined in Vagrant. Some plugins provide undefined values so that you can define nil as an initial value or a correct value.
initialize is the constructor when the Config class is initialized, and finalize! Is called when all Configs have been read.
In the example, 0 is assigned if @widgets is not defined in the Config file.
The value set in Config can be accessed as machine.config.mutagen_utilizer.orchestrate.
Action Hooks
Set this when you want to perform specific processing when vagrant up, vagrant halt, etc. are executed.
Hook types can be found at https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks.
In the following, the UpdateConfig and StartOrchestration classes are set to be called when vagrant up is executed.
lib/vagrant_mutagen_utilizer/plugin.rb
# frozen_string_literal: true
require_relative 'action/update_config'
require_relative 'action/start_orchestration'
        : <snip>
module VagrantPlugins
  module MutagenUtilizer
    # Plugin to utilize mutagen
    class MutagenUtilizerPlugin < Vagrant.plugin('2')
        : <snip>
      action_hook(:mutagen_utilizer, :machine_action_up) do |hook|
        hook.append(Action::UpdateConfig)
        hook.append(Action::StartOrchestration)
      end
        : <snip>
    end
  end
end
The UpdateConfig and StartOrchestration classes are defined as follows:
lib/vagrant_mutagen_utilizer/action/update_config.rb
# frozen_string_literal: true
require_relative '../orchestrator'
module VagrantPlugins
  module MutagenUtilizer
    module Action
      # Update ssh config entry
      # If ssh config entry already exists, just entry appended
      class UpdateConfig
        def initialize(app, env)
          @app = app
          @machine = env[:machine]
          @config = env[:machine].config
          @console = env[:ui]
        end
        def call(env)
          return unless @config.orchestrate?
          o = Orchestrator.new(@machine, @console)
          o.update_ssh_config_entry
          @app.call(env)
        end
      end
    end
  end
end
lib/vagrant_mutagen_utilizer/action/start_orchestration.rb
# frozen_string_literal: true
require_relative '../orchestrator'
module VagrantPlugins
  module MutagenUtilizer
    module Action
      # Start mutagen project
      class StartOrchestration
        def initialize(app, env)
          @app = app
          @machine = env[:machine]
          @config = env[:machine].config
          @console = env[:ui]
        end
        def call(env)
          return unless @config.orchestrate?
          o = Orchestrator.new(@machine, @console)
          o.start_orchestration
          @app.call(env)
        end
      end
    end
  end
end
In both UpdateConfig and StartOrchestration, the constructor only registers the value received from the argument in the instance variable.
--@app: Used when the call method is executed. Details are unknown.
--@machine: You can refer to the ID and Config related to the VM.
--@config: You can refer to the settings
--@ console: Console input / output is possible
The main functionality is written in the call method. Here we check if the Plugin is enabled in Config, and if so, we call the start_orchestration method of the Orchestrattor class.
The Orchestrattor class is:
# frozen_string_literal: true
module VagrantPlugins
  module MutagenUtilizer
    # Class for orchestrate with mutagen
    class Orchestrator
      def initialize(machine, console)
        @machine = machine
        @console = console
      end
      # Update ssh config entry
      # If ssh config entry already exists, just entry appended
      def update_ssh_config_entry
        hostname = @machine.config.vm.hostname
        logging(:info, 'Checking for SSH config entries')
        if ssh_config_entry_exist?
          logging(:info, "  updating SSH Config entry for: #{hostname}")
          remove_from_ssh_config
        else
          logging(:info, "  adding entry to SSH config for: #{hostname}")
        end
        append_to_ssh_config(ssh_config_entry)
      end
      def start_orchestration
        return if mutagen_project_started?
        logging(:info, 'Starting mutagen project orchestration (config: /mutagen.yml)')
        start_mutagen_project || logging(:error, 'Failed to start mutagen project (see error above)')
        # show project status to indicate if there are conflicts
        list_mutagen_project
      end
        : <snip>
      private
        : <snip>
    end
  end
end
Although details are omitted, update_ssh_config_entry called from Action Hooks describes the process of adding an entry to the SSH configuration file, and start_orchestration describes the process of starting a mutagen project.
In this way, you can write a plugin that does a specific thing when vagrant up is executed.
If you want to know the filters other than vagrant up, you can refer to https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks.
It has already appeared several times, but it inputs and outputs text via Vagrant :: UI for Vagrant I / O.
I can't use the standard Ruby put / get.
The Environment for each middleware, ʻenv, allows you to get the Vagrant :: UI with ʻenv [: ui].
There are log output levels of info, warn, error, success, and you can output logs at each level.
The procedure is the same as exposing the gem to rubygems.