[Ruby] How to make a Vagrant Plugin that I learned when publishing vagrant-mutagen by forking

5 minute read

vagrant-How to make a Vagrant Plugin that I learned when publishing fork mutagen

Introduction

Vagrant Plugin is an extension that allows you to add functionality to Vagrant.

Recently, mutagen is used for file synchronization when starting VM, and mutagen project is linked with starting/stopping VM vagrant-mutagen plugin.

However, it did not work properly on Windows, and I used a modified version while issuing PR, but still start the VM I was in trouble because I had to elevate the administrator privileges twice each time.

I decided to make a Vagrant Plugin by forking it all at once, and I decided to summarize what I investigated at that time.

The source code of the forked plugin is in here, so please refer to it as an example.

Vagrant Plugin overview

  • Vagrant is a Ruby program and Vagrant plugin is a ruby gem
  • If it is published to rubygems, it will be searched when vagrant plugin install is executed.
    • It seems that the plugin name is customarily named vagrant-XXX.

For the outline of how to make, I referred to the one described below.

https://www.vagrantup.com/docs/plugins/development-basics

How to write # Gemfile

The name of the Plugin created by my-vagrant-plugin.

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

Vagrant is required for development, the vagrant plugin command does not work in development mode, and the gem specified in the plugins group is loaded instead.

Normally, I did not prepare Ruby development for Windows environment, so when developing Vagrant plugin, Vagrant used the one already installed on Windows, and did gem build of plugin every time and uninstall/install with vagrant. ..

  1. Make Plugin a gem
    • Running gem build <SOME_PLUGIN_NAME>.gemspec will save <SOME_PLUGIN_NAME>-X.Y.Z.gem
  2. Install Plugin
    • vagrant plugin install <PATH_TO_SOME_PLUGIN>/<SOME_PLUGIN_NAME>-X.Y.Z.gem will install Plugin

(I thought I wouldn’t make a big fix, but looking back on it, it seems easier to run Vagrant in development mode.)

Basic design of #Plugin

Define the class that is the 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 corresponding to the name listed in the gemspec.

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

The created vagrant-mutagen-utilizer now 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

The forked vagrant-mutagen was originally like that, but without describing various processes in the Plugin body, describe the class separately for each process, read it using require_relative, and register it as a component ..

How to set Config

Specify the class name on the Plugin body side. “Foo” specified here corresponds to config.foo.~ of 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 indicates undefined in Vagrant. Some plugins have undefined values so that nil can be defined as an initial value or a correct value.

initialize is the constructor when the Config class is initialized and finalize! is called when all the Configs have been read.

The example assigns @widgets to 0 if it was not defined in the Config file.

The value set in Config can be accessed like 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 are described at https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks.

In the following, it is set to call UpdateConfig, StartOrchestration class 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

UpdateConfig, StartOrchestration クラスは次のように定義されています。

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

UpdateConfig, StartOrchestration どちらも、コンストラクタでは引数から受け取った値をインスタンス変数に登録しているのみです。

  • @app: call メソッドが実行されたときに使用する。詳細は不明。
  • @machine: VMに関するIDやConfigが参照できる。
  • @config: 設定が参照できる
  • @console: コンソール入出力ができる

メインの機能は call メソッドに書かれています。 ここでは Config に Plugin が有効であるか調べ、有効になっていたら Orchestrattor クラスの start_orchestration メソッドを呼び出しています。

Orchestrattor クラスは次のとおりです。

# 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

詳細は割愛していますが、Action Hooks から呼び出される update_ssh_config_entry は SSH 設定ファイルにエントリーを追加する処理、start_orchestration は mutagen project を開始する処理がそれぞれ記述されています。

このようにして、vagrant up が実行されたときに特定の処理を行う Plugin が書けます。

vagrant up 以外のフィルタが知りたい場合は https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks を参考にするとよいと思います。

コンソール入出力

既に何度か登場していますが、Vagrant の I/O 用の Vagrant::UI を介してテキストを入出力します。 Ruby 標準の put/get を使うことができません。

各 middleware の Environment である env により env[:ui] で Vagrant::UI を取得できます。

ログの出力レベルが info, warn, error, success とあり、それぞれのレベルでログを出力できます。

Plugin の公開

gem を rubygems に公開する手順と同じです。

rake コマンドを使う場合 (Vagrant 公式で楽な手段であると紹介されている方法)

  1. bundle install
  2. rake build
  3. rake release

gem コマンドを使う場合

  1. gem build
  2. gem push