Serverspec と Busybox

Serverspec では様々な種類のテストを記述することが出来るのですが、内部では Specinfra というクラスでそのテストの定義と実装をしています。
実際にテストする際に使用するツールは OS によって変わってきてしまうので、その差異を吸収する部分も Specinfra に書かれています。
Vagrant が様々なホストとゲスト OS でプロビジョン出来る仕掛けと同じです。

さて、その Specinfra ですが、[boot2docker](http://boot2docker.io/) や拙作の [DockerRoot](https://atlas.hashicorp.com/ailispaw/boxes/docker-root) のように Busybox がベースの OS 用の設定がなく、テストがうまくいきません。

そこで、簡易な（実際に使ってみてうまく動かなかった部分のみ） DockerRoot/Busybox 用拡張を書いてみました。
ここでは、単純に `spec/spec_help.rb` に追加して、拡張することにしました。
（本格的なのは面倒なのでｗ）
たぶん、他のタイプのテストでも動かないのがあるでしょう。そのうち対応するかも知れませんが、需要が無ければこのままですｗ

## `spec/spec_help.rb`

```ruby
class Specinfra::Command::Busybox
  class Base < Specinfra::Command::Linux::Base
    class Group < Specinfra::Command::Base::Group
      class << self
        def check_exists(group)
          "grep #{escape(group)} /etc/group"
        end

        def check_has_gid(group, gid)
          "grep #{escape(group)} /etc/group | cut -f 3 -d ':' | grep -w -- #{escape(gid)}"
        end
      end
    end

    class RoutingTable < Specinfra::Command::Base::RoutingTable
      class << self
        def check_has_entry(destination)
          %Q{ip route show #{destination} | awk '{print $1, "via", $5, "dev", $3, " "}'}
        end

        alias :get_entry :check_has_entry
      end
    end

    class User < Specinfra::Command::Base::User
      class << self
        def check_has_home_directory(user, path_to_home)
          "grep #{escape(user)} /etc/passwd | cut -f 6 -d ':' | grep -w -- #{escape(path_to_home)}"
        end

        def check_has_login_shell(user, path_to_shell)
          "grep #{escape(user)} /etc/passwd | cut -f 7 -d ':' | grep -w -- #{escape(path_to_shell)}"
        end
      end
    end
  end
end

class Specinfra::Helper::DetectOs::DockerRoot < Specinfra::Helper::DetectOs
  def detect
    if ( uname = run_command('uname -r').stdout ) && uname =~ /docker-root/i
      family = nil
      release = nil
      os_release = run_command("cat /etc/os-release")
      if os_release.success?
        os_release.stdout.each_line do |line|
          family = line.split('=').last.strip if line =~ /^ID_LIKE=/
          release = line.split('=').last.strip if line =~ /^VERSION=/
        end
      end
      family ||= 'linux'
      { :family => family, :release => release }
    end
  end
end
```

以下はこれを使ったサンプルです。

## `spec/docker-root/docker_spec.rb`

```ruby
require 'spec_helper'

describe user('docker') do
  it { should exist }
  it { should belong_to_group 'docker' }
  it { should belong_to_primary_group 'docker' }
  it { should have_uid 1000 }
  it { should have_home_directory '/home/docker' }
  it { should have_login_shell '/bin/bash' }
end

describe group('docker') do
  it { should exist }
  it { should have_gid 1000 }
end

describe 'Docker Daemon' do
  context command('/etc/init.d/docker status') do
    its(:stdout) { should match /^Docker .* is running.$/ }
  end

  context file('/var/run/docker.sock') do
    it { should be_socket }
  end

  context interface('docker0') do
    it { should exist }
    it { should be_up }
  end

  context routing_table do
    it do
      should have_entry(
        :destination => '172.17.0.0/16',
        :interface   => 'docker0',
        :gateway     => '172.17.0.1',
      )
    end
  end
end

describe 'Linux kernel parameters' do
  context linux_kernel_parameter('net.ipv4.ip_forward') do
    its(:value) { should eq 1 }
  end

  context linux_kernel_parameter('net.ipv6.conf.all.forwarding') do
    its(:value) { should eq 1 }
  end
end
```

## `Vagrantfile`

```ruby
# A dummy plugin for DockerRoot to set hostname and network correctly at the very first `vagrant up`
module VagrantPlugins
  module GuestLinux
    class Plugin < Vagrant.plugin("2")
      guest_capability("linux", "change_host_name") { Cap::ChangeHostName }
      guest_capability("linux", "configure_networks") { Cap::ConfigureNetworks }
    end
  end
end

Vagrant.configure(2) do |config|
  config.vm.define "docker-root"
  config.vm.box = "ailispaw/docker-root"

  if Vagrant.has_plugin?("vagrant-triggers") then
    config.trigger.after [:up, :resume] do
      info "Adjusting datetime after suspend and resume."
      run_remote "sudo sntp -4sSc pool.ntp.org; date"
    end
  end

  # Adjusting datetime before provisioning.
  config.vm.provision :shell, run: "always" do |sh|
    sh.inline = "sntp -4sSc pool.ntp.org; date"
  end

  config.vm.provision :docker do |docker|
    docker.pull_images "busybox"
    docker.run "simple-echo",
      image: "busybox",
      args: "-p 8080:8080 -v /usr/bin/dumb-init:/dumb-init:ro --entrypoint=/dumb-init",
      cmd: "nc -p 8080 -l -l -e echo hello world!"
  end

  config.vm.network :forwarded_port, guest: 8080, host: 8080

  if Vagrant.has_plugin?("vagrant-serverspec") then
    config.vm.provision :serverspec do |spec|
      spec.pattern = "spec/docker-root/*_spec.rb"
    end
  end
end
```

他にもサンプルとしてこちらで実際に利用しています。
https://github.com/ailispaw/docker-root-packer/tree/serverspec/spec