2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

vagrant-mutagen を fork して公開したときに学んだ Vagrant Plugin の作り方

Last updated at Posted at 2020-07-19

vagrant-mutagen を fork して公開したときに学んだ Vagrant Plugin の作り方

はじめに

vagrant3.0 以降は Vagrant が Go で実装されるとのことなので plugin の作り方は変わると思われます。(参考)

Vagrant Plugin は Vagrant に機能を追加することができる拡張機能です。

最近では VM を起動する際にファイル同期をする際に mutagen を使うようにしており、VM の起動/停止と共に mutagen project を連動させる vagrant-mutagen plugin を使おうと考えていました。

しかし、Windows ではまともに動かず、PR を出しつつ修正したものを使っていたのですが、それでも VM を起動するたびに 2 回も管理者権限を昇格する必要があり困っていました。

いっそ fork して、思うままに Vagrant Plugin を作ることにし、その際に調べたことをまとめることとしました。

fork した plugin のソースコードはこちらにあるので、1例として参考にしてみてください。

Vagrant Plugin 概要

  • Vagrant は Ruby プログラムであり、Vagrant plugin は ruby の gem です
  • rubygems に公開してあると vagrant plugin install 実行時に検索されます
    • plugin の名前は慣例で vagrant-XXX という名前でにしているようです

作り方の概要は次に記載されているものを参考にしました。

Gemfile の書き方

my-vagrant-plugin が自分で作成する 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 が必要となり、開発モードでは vagrant plugin コマンドは機能せず代わりに plugins グループに指定された gem が読み込まれるとのことです。

普段 Windows 環境には Ruby 開発を用意していなかったため、Vagrant plugin を開発するにあたり、Vagrant は Windows にインストール済みのものを使い、Plugin を都度 gem build して vagrant で uninstall/install していました。

  1. Plugin を gem にする
    • gem build <SOME_PLUGIN_NAME>.gemspec を実行すると <SOME_PLUGIN_NAME>-X.Y.Z.gem が保存されます
  2. Plugin をインストールする
    • vagrant plugin install <PATH_TO_SOME_PLUGIN>/<SOME_PLUGIN_NAME>-X.Y.Z.gem により Plugin がインストールされます

(大した修正はしないと思っていたのですが、結果的に振り返ると Vagrant を開発モードで動作させた方が楽だったように感じます)

Plugin の基本設計

Plugin の本体となるクラスを定義し、クラス内で Config, Provisioner などのコンポーネントクラス名を指定します。

gemspec に記載された name に対応するファイルから Plugin クラスを require する必要があります。

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

作成した vagrant-mutagen-utilizer では次のようになりました。

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

fork した vagrant-mutagen がもともとそうなっていたのですが、Plugin 本体には様々な処理は記述せずに、処理ごとにクラスを分けて記述して require_relative を使って読み込み、コンポーネントとして登録しています。

Config の設定方法

Plugin 本体側にクラス名を指定します。
ここで指定する "foo" が Vagrantfile の config.foo.~ に当たります。

config "foo" do
  require_relative "config"
  Config
end

Config コンポーネントは 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 は Vagrant における未定義を指す値です。
Plugin によっては nil を初期値や正しい値として定義できるよう、未定義とする値が用意されています。

initialize は Config クラスが初期化されるときのコンストラクタで、finalize! はすべての Config が読み終わった時に呼び出されます。

例では @widgets が Config ファイルで定義されなかった場合は 0 を代入しています。

Config で設定された値は、machine.config.mutagen_utilizer.orchestrate のようにアクセスできます。

Action Hooks

vagrant up, vagrant halt などが実行されたときに特定の処理をしたい場合に設定します。

Hook の種類は https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks に書かれています。

以下では、vagrant up が実行されたときに UpdateConfig, StartOrchestration クラスを call するよう設定しています。

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
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?