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
という名前でにしているようです
- plugin の名前は慣例で
作り方の概要は次に記載されているものを参考にしました。
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 していました。
- Plugin を gem にする
-
gem build <SOME_PLUGIN_NAME>.gemspec
を実行すると<SOME_PLUGIN_NAME>-X.Y.Z.gem
が保存されます
-
- 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 する必要があります。
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 では次のようになりました。
# 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
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
# 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 するよう設定しています。
# 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 クラスは次のように定義されています。
# 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
# 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 公式で楽な手段であると紹介されている方法)
- bundle install
- rake build
- rake release
gem コマンドを使う場合
- gem build
- gem push