PryからSpecinfraを簡単に使うためのカスタムコマンド作ってみた

PryからSpecinfraを簡単に使うためのカスタムコマンドを作成してみました。

# 背景

Specinfraを使うとOSの違いをあまり意識することなく設定の確認やパッケージのインストールといった操作を行うことができます。

以下のように```Specinfra::Runner``` のメソッドを呼び出すことで使うことができます。

```irb
>> Specinfra::Runner.check_package_is_installed('nginx')
=> false
```

ライブラリから使うのには良いですが、Pryからちょっと使ってみたいというとき補完や定義を表示するのがしんどそうだったのでカスタムコマンドを作成してみました。

# カスタムコマンドの導入

以下の内容を```~/.pryrc``` に追加することでカスタムコマンドが使えるようになります。

```rb
Pry::Commands.create_command "add-specinfra-commands"  do
  description "Add Specinfra Commands"
  command_options :keep_retval => false

  banner <<-BANNER
    Usage:
      add-specinfra-commands
  BANNER

  def setup
    require 'specinfra'
  end

  def process
    Specinfra.configuration.send(:backend, :exec)
    os

    Pry::Commands.create_command 'specinfra-ssh' do
      description "Set Specinfra SSH parameters"
      command_options :keep_retval => false

      def options(opt)
        login_user = ENV['USER'] || ENV['LOGNAME'] || Etc.getlogin || Etc.getpwuid.name
        opt.on :l, 'Login Name', :default => login_user, :argument => true, :as => String
        opt.on :i, 'Identity file', :default => '~/.ssh/id_rsa',:argument => true, :as => String
        opt.on :p, 'Port', :default => 22, :argument => true, :as => Integer
      end
      
      def process
        Specinfra.configuration.send(:backend, :ssh)
        Specinfra.configuration.send(:host, args.first)
        Specinfra.configuration.send(
          :ssh_options, :user => opts[:l],
                        :port => opts[:p],
                        :keys => [File.expand_path(opts[:i])]
        )
        property[:os] = detect_os
      end
    end

    get_resource_types.each do |resouce_type|
      get_commands(resouce_type).each do |command, command_args|
        command_args = command_args.map { |a|
          case a.first
          when :opt  then "[#{a.last.to_s.upcase}]"
          when :rest then "[#{a.last.to_s.upcase} ...]"
          else a.last.to_s.upcase
          end
        }
        description = "Usage: specinfra-#{command.to_s.gsub(/_/,'-')} [OPTIONS] #{command_args.join(' ')}"
        target.eval <<-EOS
        Pry::Commands.create_command 'specinfra-#{command.to_s.gsub(/_/,'-')}' do
          description "#{description}"
          command_options :keep_retval => true

          def options(opt)
            opt.on :n, :dry, 'Dry run'
          end
          
          def process
            unless opts[:dry]
              Specinfra::Runner.send(:#{command.to_s}, *args)
            else
              Specinfra.command.get(:#{command.to_s}, *args)
            end
          end
        end
        EOS
      end
    end
  end

  private

  def get_resource_types
    Specinfra::Command::Base.subclasses.select { |c|
      c.to_s =~ /^Specinfra::Command::Base/
    }
    .sort { |a, b| a.to_s <=> b.to_s }
    .map { |c| c.to_s.split('::')[-1] }
  end
  
  def get_commands(resource_type)
    resource_type = resource_type.downcase
    base_class = Specinfra::Command::Base.subclasses.detect { |c|
      c.to_s.downcase =~ /^specinfra::command::base::#{resource_type}/
    }
    classes = [base_class]+ base_class.subclasses

    commands = {}
    classes.each do |c|
      c.methods(false).each do |m|
        action, sub_action = m.to_s.split('_', 2)
        name = unless sub_action.nil?
          "#{action}_#{resource_type}_#{sub_action}"
        else 
          "#{action}_#{resource_type}"
        end

        next if commands.has_key?(name)

        commands[name] = c.method(m).parameters
      end
    end
    commands
  end
end
```

# 使い方

pryを開始し次のコマンドを実行するとSpecinfra関連のコマンド群が追加されます。

```irb
>> add-specinfra-commands
```

追加されるコマンド群は大きく分けて以下の2つです。

* specinfra-ssh
    * SSHの設定を行うためのコマンド 
* specinfra-{action}-{resource_type}-{subaction}
    * 各種アクションを実行するためのコマンド。Spec::Runnerのメソッドの```_``` を```-```に置換した名前となっています。

## specinfra-ssh

```specinfra-ssh``` は、SSHのオプションを簡単に設定するためのコマンドです。設定後に```detect_os``` で```property[:os]``` を更新します。

```irb
>> specinfra-ssh -l root -p 22 X.X.X.X
>> os
=> {:family=>"redhat", :release=>"7.0.1406", :arch=>"x86_64"}
```

## specinfra-{action}-{resource_type}-{subaction}

これらのコマンド群は実際にコマンドを実行するためのコマンドです。

```
>> specinfra-check-package-is-installed python
=> true
```

```-h``` でヘルプが表示されます。

```
>> specinfra-check-package-is-installed -h
Usage: specinfra-check-package-is-installed [OPTIONS] PACKAGE [VERSION]
    -n, --dry       Dry run
    -h, --help      Show this message.
```

```-n``` で実行されるコマンドが表示されます。ただし、実行結果のrubyによる処理は考慮されません。

```irb
>> specinfra-check-package-is-installed -n python
=> "/usr/local/bin/brew list -1 | grep -E '^python$'"
>> ./usr/local/bin/brew list -1 | grep -E '^python$'
python
```
# 例：CentOS7にMariaDBをインストール

使用例としてDigitalOceanにCentOS7の仮想マシンを作成しMariaDBをインストールしてみます。

仮想マシンの作成は前回の記事と同じくFogを使いました。([pryとfogでお手軽マルチクラウド対応CLI](http://qiita.com/atsaki/items/822d18f6a427585c523c))

```irb
>> fog digitalocean
>> region = regions.find { |r| r.name == "Singapore 1" }
>> image = images.find {|i| i.name == "7.0 x64" && i.distribution == "CentOS" }
>> flavor = flavors.find { |f| f.name == "512MB" }
>> ssh_key = ssh_keys.first
>> server = servers.create(:name=>"specinfra-test", :region_id=>region.id, :image_id=>image.id, :flavor_id=>flavor.id, :ssh_key_ids=>[ssh_key.id])
>> server.wait_for { ready? } 
```

Specinfraのコマンドを追加し、作成した仮想マシンへ接続するための情報を設定します。

```irb
>> add-specinfra-commands
>> specinfra-ssh -l root X.X.X.X
>> os
=> {:family=>"redhat", :release=>"7.0.1406", :arch=>"x86_64"}
```

```irb
>> specinfra-check-package-is-installed -n mariadb-server
=> "rpm -q mariadb-server"
>> specinfra-check-package-is-installed mariadb-server
=> false
>> specinfra-install-package mariadb-server
=> 長いので省略
>> specinfra-check-package-is-installed mariadb-server
=> true

>> specinfra-check-service-is-running -n mariadb
=> "systemctl is-active mariadb"
>> specinfra-check-service-is-running mariadb
=> false
>> specinfra-start-service -n mariadb
=> "systemctl start mariadb"
>> specinfra-start-service mariadb
=> #<Specinfra::CommandResult:0x007f912c9a24e0 @exit_signal=nil, 
@exit_status=0, @stderr="", @stdout="">
>> specinfra-check-service-is-running mariadb
=> true

>> specinfra-check-service-is-enabled -n mariadb
=> "systemctl --plain list-dependencies multi-user.target | grep '\\(^\\| \\)mariadb.service$'"
>> specinfra-check-service-is-enabled  mariadb
=> false
>> specinfra-enable-service -n  mariadb
=> "systemctl enable mariadb"
>> specinfra-enable-service  mariadb
=> #<Specinfra::CommandResult:0x007f9129bd6570
 @exit_signal=nil,
 @exit_status=0,
 @stderr="ln -s '/usr/lib/systemd/system/mariadb.service' '/etc/systemd/system/multi-user.target.wants/mariadb.service'\n",
 @stdout="">
>> specinfra-check-service-is-enabled  mariadb
=> true
```

# 課題

作成するにあたって色々とハマりましたが特に以下の2つはまだ解決していません。もし知っている方がいたら教えて頂けるとありがたいです。

* eval族を使用している際にPryでソースコードを追う方法・コツ
* メソッドの引数のデフォルト値を取得する方法

# 参考

* [Serverspec](http://www.amazon.co.jp/dp/4873117097) 宮下剛輔