ohaiのカスタムプラグインを作ってみよう。


## 関連記事

* [Chefを使ってより効率的にサーバを管理しよう。まずは用語から](http://qiita.com/park-jh/items/9e2e358d2e82a7736cf7)
* [Berkshelfを使って外部クックブックをノードに適用する。](http://qiita.com/park-jh/items/751464a4cc6d8abcd6fd)
* [クックブックを作成してノードに適用する。- その１](http://qiita.com/park-jh/items/8d641809850b5930d39c)
* [クックブックを作成してノードに適用する。- その２](http://qiita.com/park-jh/items/88237342cfd202e6893f)
* [ChefでCentOS 6.7 + rbenv + ruby 2.3.0の環境を構築する。](http://qiita.com/park-jh/items/f249a18793899f2eb4bf)
* [ChefでCentOS 6.7 + Vim 7.4 + luaを設置する。](http://qiita.com/park-jh/items/371d179d5e71618a1f14)
* [ChefでVimプラグインマネージャーNeoBundleを設置する。](http://qiita.com/park-jh/items/ec09e40f9b4aaf9fedfd)
* [Chefのレシピをデバッグしてみよう。](http://qiita.com/park-jh/items/96a076e167327a6a5a39)
* [ChefでCentOS 6.7 + nginxを設置する。- 外部クックブック](http://qiita.com/park-jh/items/3013c49494e524fced31)
* [ChefでCentOS 6.7 + nginxを設置する。- カスタムクックブック](http://qiita.com/park-jh/items/43451cea21ee1724c24b)
* [ChefでCentOS 6.7 + nodejs + npmを設置する。](http://qiita.com/park-jh/items/96089e7e1c97b0fe3a6c)
* [ChefでVimプラグインマネージャーvim-plugを設置する。](http://qiita.com/park-jh/items/ba27ad412ce9926fd94d)
* ohaiのカスタムプラグインを作ってみよう。
* [Chef SoloからChef Zeroへ](http://qiita.com/park-jh/items/44c249c9de768c53b423)
* [Chefでnginx + php(php-fpm) + mariadb + wordpressを設置する。](http://qiita.com/park-jh/items/7515e47dddc38257a454)

## ohaiとは

* ノードの情報をAttributeで保持しているシステムでChefをインストールするとき一緒にインストールされる。
* ノート上のさまざまな情報をJSONのデータとして扱えるようにしてくれる。

## カスタムプラグインの必要性

* nginxをtarballでインストールする。
* nginxのバージョンと使っているmoduleが同じであれば次にChefを実行するときはインストールをスキプしたい。
* ohaiを使って最初インストールするときのバージョンとmoduleの情報をノード上に保持する。
* Chefを実行するときノード上に保持されているバージョンとmoduleの情報をクックブックのattributeと比較する。
* 比較して異なる場合のみインストールする。

## カスタムプラグインの作成

### Berksfileに追加

* ohaiというコミュニティクックブックに依存的なので、先にインストールする。

```sh
% vi Berksfile

... snip ...
cookbook 'ohai'

:wq

% bin/berks vendor cookbook
```

* nginxのコミュニティクックブックがインストールされている場合はすでにohaiがインストールされている。

### 依存性を追加

* nginxのカスタムクックブック内のmetadata.rbファイルに依存性を記入する。

```ruby
... snip ...
depends 'ohai'
```

### レシピを生成

* 既存nginxのレシピ（default.rb）からohaiを使うためにohai_pluginという新規レシピを作成する。
* nginxのコミュニティクックブックからそのまま持ってきた。
* ノード上の/etc/chef/ohai_pluginsの配下にnginx.rbファイルが生成される。
* site-cookbooks/nginx/recipes/ohai_plugin.rb

```ruby
ohai 'reload_nginx' do
  plugin 'nginx'
  action :nothing
end

template "#{node['ohai']['plugin_path']}/nginx.rb" do
  source 'plugins/nginx.rb.erb'
  owner  'root'
  group  'root'
  mode   '0755'
  notifies :reload, 'ohai[reload_nginx]', :immediately
end

include_recipe 'ohai::default'
```

### 既存レシピの修正

* 新規作成したohai_pluginをdefaultレシピの中からincludeする。
* nginxがインストールされるたびに今度作成したohaiのカスタムプラグインをリロード（再実行）する。

```ruby
include_recipe 'nginx::ohai_plugin' 
nginx_url = node['nginx']['url']
nginx_filename = "nginx-#{node['nginx']['ver']}.tar.gz"
src_filepath  = "#{Chef::Config['file_cache_path'] || '/tmp'}/#{nginx_filename}"

... snip ...

bash "install nginx" do
  cwd File.dirname(src_filepath)
  code <<-EOH
    tar zxf #{nginx_filename} -C #{File.dirname(src_filepath)}
    cd #{File.dirname(src_filepath)}/#{File.basename(nginx_filename, ".tar.gz")}
    ./configure #{configure_flags.join(' ')}
    make
    make install
  EOH

  not_if do
    force_recompile == false &&
    node.automatic_attrs['nginx'] &&
    node.automatic_attrs['nginx']['version'] == node['nginx']['ver'] &&
    node.automatic_attrs['nginx']['configure_arguments'].sort == configure_flags.sort
  end

  notifies :restart, 'service[nginx]'
  notifies :reload,  'ohai[reload_nginx]', :immediately
end

... snip ...
```

### カスタムプラグインをテンプレートで生成

* ohaiのバージョン（6, 7, 8）別にプラグインの記述方法が異なる。
* nginxのコミュニティクックブックからそのまま持ってきた。
* しかし、バージョン6で記述されていて古い。
* site-cookbooks/nginx/templates/default/plugins/nginx.rb.erb

```ruby
provides "nginx"
provides "nginx/version"
provides "nginx/configure_arguments"
provides "nginx/prefix"
provides "nginx/conf_path"

def parse_flags(flags)
  prefix = nil
  conf_path = nil

  flags.each do |flag|
    case flag
    when /^--prefix=(.+)$/
      prefix = $1
    when /^--conf-path=(.+)$/
      conf_path = $1
    end
  end

  [ prefix, conf_path ]
end

nginx Mash.new unless nginx
nginx[:version]             = nil unless nginx[:version]
nginx[:configure_arguments] = Array.new unless nginx[:configure_arguments]
nginx[:prefix]              = nil unless nginx[:prefix]
nginx[:conf_path]           = nil unless nginx[:conf_path]

status, stdout, stderr = run_command(:no_status_check => true, :command => "<%= node['nginx']['sbin'] %> -V")

if status == 0
  stderr.split("\n").each do |line|
    case line
    when /^configure arguments:(.+)/
      # This could be better: I'm splitting on configure arguments which removes them and also
      # adds a blank string at index 0 of the array. This is why we drop index 0 and map to
      # add the '--' prefix back to the configure argument.
      nginx[:configure_arguments] = $1.split(/\s--/).drop(1).map { |ca| "--#{ca}" }

      prefix, conf_path = parse_flags(nginx[:configure_arguments])

      nginx[:prefix] = prefix
      nginx[:conf_path] = conf_path
    when /^nginx version: nginx\/(\d+\.\d+\.\d+)/
      nginx[:version] = $1
    end
  end
end
```

* バージョン6の記述方法はすでにdeprecatedになっているので、バージョン7で直してみた。
* バージョン7と8の記述方法は同様である。
* site-cookbooks/nginx/templates/default/plugins/nginx.rb.erb

```ruby
Ohai.plugin(:Nginx) do
  provides "nginx"

  def parse_flags
    so = shell_out("<%= node['nginx']['sbin'] %> -V 2>&1")
    so.stdout.lines.each do |line|
      case line
      when /^nginx version: nginx\/(\d+\.\d+\.\d+)/
        @version = $1
      when /^configure arguments:(.+)/
        @configure_arguments = $1.split(/\s--/).drop(1).map { |ca| "--#{ca}" }
      end
    end
  end

  collect_data do
    parse_flags
    nginx Mash.new
    nginx[:version]             = @version
    nginx[:configure_arguments] = @configure_arguments
  end
end
```

* provides
  - このプラグインが提供する項目を宣言する。
  - 保持したい情報を宣言した項目に割り当てる。

* Mash
  - データストアとしてMashを利用する。Hashだと考えばいい。
  - Mashに保持したいデータをセットする。

* collect_data
  - ohaiにより呼び出されるrubyのブロックでデータを取集して保持する。
  - platformを引数として指定するのもできる。ex）:aix, :freebsd, :linux...

## shell_out

* chef-shell（又はirb）でカスタムプラグインの中のコマンドを実行してみる。
* 使いたいコマンドの実行結果を加工して集取するのができるので、カスタムプラグインの作成のとき役に立つ。

```sh
chef (12.7.2)> include Chef::Mixin::ShellOut
 => Object

chef (12.7.2)> so = shell_out('/opt/nginx-1.8.1/sbin/nginx -V 2>&1')
 => <Mixlib::ShellOut#41687580: command: '/opt/nginx-1.8.1/sbin/nginx -V 2>&1' process_status: #<Process::Status: pid 9533 exit 0> stdout: 'nginx version: nginx/1.8.1
built by gcc 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC)
built with OpenSSL 1.0.1e-fips 11 Feb 2013
TLS SNI support enabled
configure arguments: --prefix=/opt/nginx-1.8.1 --conf-path=/etc/nginx/nginx.conf --sbin-path=/opt/nginx-1.8.1/sbin/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-http_mp4_module' stderr: '' child_pid: 9533 environment: {"LC_ALL"=>"en_US.UTF-8", "LANGUAGE"=>"en_US.UTF-8", "LANG"=>"en_US.UTF-8"} timeout: 600 user:  group:  working_dir:  >

chef (12.7.2)> so.stdout
 => "nginx version: nginx/1.8.1\nbuilt by gcc 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) \nbuilt with OpenSSL 1.0.1e-fips 11 Feb 2013\nTLS SNI support enabled\nconfigure arguments: --prefix=/opt/nginx-1.8.1 --conf-path=/etc/nginx/nginx.conf --sbin-path=/opt/nginx-1.8.1/sbin/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-http_mp4_module\n"

chef (12.7.2)> so.stdout.lines
 => ["nginx version: nginx/1.8.1\n", "built by gcc 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) \n", "built with OpenSSL 1.0.1e-fips 11 Feb 2013\n", "TLS SNI support enabled\n", "configure arguments: --prefix=/opt/nginx-1.8.1 --conf-path=/etc/nginx/nginx.conf --sbin-path=/opt/nginx-1.8.1/sbin/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_flv_module --with-http_mp4_module\n"]
```

## ohaiプラグインの実行

* コマンドの実行結果にプラグインから生成したデータが含まれている。
* このデータとクックブックのattributeのデータを比べてインストールするかどうかを決める。
* バージョン6の記述方法でプラグインが作成されている場合は、deprecatedになっているという警告メッセージが表示される。
* そして、バージョン6の場合は最初に表示されるため｜headを付けて確認する必要がある。
* バージョン7の場合は最後に表示されるため付けなくても結構だ。

```sh
$ ohai -d /etc/chef/ohai_plugins/

... snip ...
  "nginx": {
    "version": "1.8.1",
    "configure_arguments": [
      "--prefix=/opt/nginx-1.8.1",
      "--conf-path=/etc/nginx/nginx.conf",
      "--sbin-path=/opt/nginx-1.8.1/sbin/nginx",
      "--with-http_ssl_module",
      "--with-http_stub_status_module",
      "--with-http_gzip_static_module",
      "--with-http_flv_module",
      "--with-http_mp4_module"
    ]
  }
```

## chef-shellで確認する方法

```sh
$ chef-shell -s -c solo.rb -j dna.json

chef (12.7.2)> Ohai::Config[:plugin_path] << '/etc/chef/ohai_plugins'
 => ["/opt/chef/embedded/lib/ruby/gems/2.1.0/gems/ohai-8.10.0/lib/ohai/plugins", "/etc/chef/ohai_plugins"]

chef (12.7.2)> o = Ohai::System.new
[2016-03-16T13:25:44+00:00] WARN: Ohai::Config[:plugin_path] is set. Ohai::Config[:plugin_path] is deprecated and will be removed in future releases of ohai. Use ohai.plugin_path in your configuration file to configure :plugin_path for ohai.
 => #<Ohai::System:0x00000004ea1b00 @plugin_path="", @config={}, @data={}, @provides_map=#<Ohai::ProvidesMap:0x00000004ea1a38 @map={}>, @v6_dependency_solver={}, @loader=#<Ohai::Loader:0x00000004ea0e30 @controller=#<Ohai::System:0x00000004ea1b00 ...>, @v6_plugin_classes=[], @v7_plugin_classes=[]>, @runner=#<Ohai::Runner:0x00000004ea0db8 @provides_map=#<Ohai::ProvidesMap:0x00000004ea1a38 @map={}>, @safe_run=true>>
chef (12.7.2)> o.all_plugins

... snip ...

chef (12.7.2)> o.attributes_print('nginx')
 => "{\n  \"version\": \"1.8.1\",\n  \"configure_arguments\": [\n    \"--prefix=/opt/nginx-1.8.1\",\n    \"--conf-path=/etc/nginx/nginx.conf\",\n    \"--sbin-path=/opt/nginx-1.8.1/sbin/nginx\",\n    \"--with-http_ssl_module\",\n    \"--with-http_stub_status_module\",\n    \"--with-http_gzip_static_module\",\n    \"--with-http_flv_module\",\n    \"--with-http_mp4_module\"\n  ]\n}\n"

chef (12.7.2)> o.attributes_print('nginx/version')
 => "[\n  \"1.8.1\"\n]\n"

chef (12.7.2)> o.attributes_print('nginx/configure_arguments')
 => "[\n  \"--prefix=/opt/nginx-1.8.1\",\n  \"--conf-path=/etc/nginx/nginx.conf\",\n  \"--sbin-path=/opt/nginx-1.8.1/sbin/nginx\",\n  \"--with-http_ssl_module\",\n  \"--with-http_stub_status_module\",\n  \"--with-http_gzip_static_module\",\n  \"--with-http_flv_module\",\n  \"--with-http_mp4_module\"\n]\n"
```

## 嵌ったところ

* ohaiのカスタムプラグインの記述方法をバージョン6からバージョン7に変更したあと、考えたとおりに行かなくて苦労した。
* 特にこの部分については入門書のレベルでは説明がほぼなかったためぐっぐたネット情報に依存するしかなかった。
* 日本語の情報もほとんどない。
* 英語でとても参考になったサイトのURLを最後に貼っておいたので参考にしていただきたい。
* shell_outについてはqiitaに投稿されている記事が役に立った。

### コマンドの誤り

* nginxをtarballで設置するときのコンパイル情報を調べるためには-Vオプションを付ければいい。
* /opt/nginx-1.8.1/sbin/nginx -Vをして置いたがコンパイル情報の取得に失敗した。
* 'nginx/version'だけversion7というわからないデータが保持されて'nginx/configure_arguments'は空っぽだった。
* ここでshell_outで使うコマンドに問題があることは気が付かずにバージョン７の記述方法に問題があったんではないかと疑っていた。
* /opt/nginx-1.8.1/sbin/nginx -V 2>&1に変更したらコンパイル情報が正常に取得された。
* 正常に結果を得られるまで1日位時間が掛かった。

### node.run_state 

* Chef-solo（又はChef-client）が起動される間データを一時的に保持する。
* nginxのコミュニティクックブックの中で使っていたので、使ってみたが保持したはずなのにずっとnilだった。
* 問題は実行のタイミングにあった。
* 明示的に遅延されていないrubyコード（ruby\_block、lazy、not\_if/only\_if）はコンパイル段階で実行される。
* Chefの実行はコンパイル＞収束（Converge）＞通知（Notification）順で行う。
* コンパイル段階で実行されるところに収束の段階で実行されるnode.run_stateを参照するとnilになるわけだ。
* 詳細は[ここ](http://qiita.com/park-jh/items/34be55abb7c63b36cddb#chef%E3%81%AE%E5%AE%9F%E8%A1%8C%E3%82%BF%E3%82%A4%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E8%AA%AC%E6%98%8E)を参考にしていただきたい。（特に英語のサイト）

```ruby
# bashリソースのnot_ifブロックの中でnilになってしまい進めない。
node.run_state['force_recompile'] = false
node.run_state['configure_flags'] = node['nginx']['default_configure_flags'] | node['nginx']['modules']

# bashリソースのnot_ifブロックの中でもnilにならない。
force_recompile = false
configure_flags = node['nginx']['default_configure_flags'] | node['nginx']['modules']
```

## 参考

### バージョン7の書き方

* http://blog.levvel.io/blog-post/levvel-up-your-devops-game-with-custom-ohai-plugins-chef/
* https://docs.chef.io/release/ohai-8/
* https://github.com/rackerlabs/ohai-plugins/tree/master/plugins

### shell_outについての説明

* http://qiita.com/sawanoboly/items/43bc9e15739355f9c533

### Chefの実行タイミングの説明

* http://www.engineyard.co.jp/blog/2013/chef-recipe-lifecycle/
* https://coderanger.net/two-pass/

### 書籍

* Chef実践入門
* オライリーの「Customizing Chef」（まだ翻訳されてない）