Vagrant + CentOS7+ kdump でVMのメモリサイズが小さいとkdumpの起動に失敗する

別にVagrantに限った話ではないけど。
Vagrantでprovisionする時にkdumpを起動しているのだが、VMのメモリサイズが1GBになっているようなVMだとそれが失敗していた。
原因と対策について整理しておく。

発生した問題

例えば、以下のようなVagrantfileでVMを起動するとする。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "1024"
  end

  config.vm.provision "shell", inline: <<-SHELL
    yum install -y kexec-tools
    systemctl start kdump.service
  SHELL
end

 

手元に作る環境なので、productionとは異なりメモリサイズを小さくしておく、ということは十分に有り得る。
この時、vagrant upすると以下のようになりprovisioningに失敗する。

==> default: Running provisioner: shell...
default: Running: inline script
==> default: Loaded plugins: fastestmirror
==> default: Loading mirror speeds from cached hostfile
==> default: * base: ftp.iij.ad.jp
==> default: * extras: ftp.iij.ad.jp
==> default: * updates: ftp.iij.ad.jp
==> default: Package kexec-tools-2.0.7-38.el7_2.1.x86_64 already installed and latest version
==> default: Nothing to do
==> default: Job for kdump.service failed because the control process exited with error code. See "systemctl status kdump.service" and "journalctl -xe" for details.
The SSH command responded with a non-zero exit status. Vagrant
assumes that this means the command failed. The output for this command
should be in the log above. Please read the output to determine what
went wrong.

 

原因

このログからではよくわからないので、VMにログインして状況を確認する。

takaishiryou-no-iMac $ vagrant ssh
[vagrant@localhost ~]$ sudo systemctl status kdump.service
● kdump.service - Crash recovery kernel arming
Loaded: loaded (/usr/lib/systemd/system/kdump.service; enabled; vendor preset: enabled)
Active: failed (Result: exit-code) since 水 2016-08-03 14:01:48 UTC; 2min 55s ago
Process: 1597 ExecStart=/usr/bin/kdumpctl start (code=exited, status=1/FAILURE)
Main PID: 1597 (code=exited, status=1/FAILURE)

8月 03 14:01:48 localhost.localdomain systemd[1]: Starting Crash recovery kernel arming...
8月 03 14:01:48 localhost.localdomain kdumpctl[1597]: No memory reserved for crash kernel.
8月 03 14:01:48 localhost.localdomain kdumpctl[1597]: Starting kdump: [FAILED]
8月 03 14:01:48 localhost.localdomain systemd[1]: kdump.service: main process exited, code=exited, status=1/FAILURE
8月 03 14:01:48 localhost.localdomain systemd[1]: Failed to start Crash recovery kernel arming.
8月 03 14:01:48 localhost.localdomain systemd[1]: Unit kdump.service entered failed state.
8月 03 14:01:48 localhost.localdomain systemd[1]: kdump.service failed.

 

起動に失敗したkdump.serviceのstatusを確認すると、「No memory reserved for crash kernel」と表示されている。
これは、メモリをkdump用に割り当てたいが、VMのメモリサイズが不足していて割り当てられないというエラーのようである。

このページの手順2.1によると、割り当てるメモリの量は/etc/default/grubを見れば分かる。実際に見てみるとcrashkernel=autoとなっている。autoの場合は予約するメモリサイズが自動的に設定される。どういう値になるかはこのページに書かれており、また、自動設定に必要な最小メモリサイズの情報がこのページに書かれている。後者を見ると、x86_64の場合は2GB必要であるということが分かる。つまり、上記Vagrantfileでは1GBしかメモリを用意していなかったためkdump用にメモリを確保しておらず、kdumpの起動に失敗したのである。

解決方法

解決方法として、以下が挙げられる。

  1. VMのメモリサイズを2GB以上にする
  2. crashkernelの値を変更し、固定値にする

てっとり早いのはメモリサイズ変更だが、kdumpのためだけに2倍のメモリを使うというのはなんだかしゃくである。なので、crashkernelの値を変更して、固定値にする方法を採用する。

手動で変更する場合はここの手順通りに作業を進め、crashkernelの値を128Mにして再起動すればよい。しかし、Vagrantの場合再起動した後にkdumpを起動する必要があるため、ややこしい。

そこで、aidanns/vagrant-reloadを使う。これはprovisioningの途中でVMを再起動して、その後のprovisioningをやってくれるというvagrantのプラグイン。Vagrantfileを以下のように修正してやる。

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.require_plugin "vagrant-reload"

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.provider "virtualbox" do |vb|
  vb.memory = "1024"
end

config.vm.provision "shell", inline: <<-SHELL
  sed -i "s/crashkernel=auto/crashkernel=128M/g" /etc/default/grub
  grub2-mkconfig -o /boot/grub2/grub.cfg
  SHELL
config.vm.provision :reload

config.vm.provision "shell", inline: <<-SHELL
  yum install -y kexec-tools
  systemctl start kdump.service
SHELL
end

 

vagrant upするとちゃんとVMが再起動し、sshで再度接続して次のprovisioningに進んでいることがわかる。

==> default: Running provisioner: shell...
default: Running: inline script
==> default: Generating grub configuration file ...
==> default: Found linux image: /boot/vmlinuz-3.10.0-327.22.2.el7.x86_64
==> default: Found initrd image: /boot/initramfs-3.10.0-327.22.2.el7.x86_64.img
==> default: Found linux image: /boot/vmlinuz-0-rescue-cb81bd8ac73c409cb970c217f287cf9f
==> default: Found initrd image: /boot/initramfs-0-rescue-cb81bd8ac73c409cb970c217f287cf9f.img
==> default: done
==> default: Running provisioner: reload...
==> default: Attempting graceful shutdown of VM...
==> default: Checking if box 'centos/7' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: Forwarding ports...
default: 22 (guest) => 2200 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2200
default: SSH username: vagrant
default: SSH auth method: private key
default: Warning: Remote connection disconnect. Retrying...
==> default: Machine booted and ready!
[default] GuestAdditions 5.0.26 running --- OK.
==> default: Checking for guest additions in VM...
==> default: Rsyncing folder: /Users/r_takaishi/src/github.com/takaishi/dev-vm/centos7/ => /home/vagrant/sync
==> default: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> default: flag to force provisioning. Provisioners marked to run always will still run.
==> default: Running provisioner: shell...
default: Running: inline script
==> default: Loaded plugins: fastestmirror
==> default: Loading mirror speeds from cached hostfile
==> default: * base: ftp.riken.jp
==> default: * extras: ftp.riken.jp
==> default: * updates: ftp.riken.jp
==> default: Package kexec-tools-2.0.7-38.el7_2.1.x86_64 already installed and latest version
==> default: Nothing to do

provisioningする度にVMを再起動するので時間が伸びるのがデメリット。

Surface Pro 4

勉強会の発表用にモバイルマシンが必要になったので、Windowsマシンも使ってキャッチアップしておくか、という気持ちで購入した。スペックはCPUはi5、メモリ8GB、ストレージ256GB。最低スペックのものを使ってみようかな、とも思ってたけどやめておいた。

よいところ

  • タッチパネル便利。macbookでも画面をタッチしそうになる。
  • OneNoteとSurfacePenの組合せはメモ用途としては便利
  • ソファーでゴロゴロしながらネットサーフィンするのには便利

いまいちなところ

  • SurfacePenのトップボタンはBTで繋っていないと反応しないっぽくて使いづらい。
  • ターミナルが使いにくい
    • vagrantいれて勉強会でデモすしようとしてたけど結構面倒だった
  • タブレットとして使うには大きい
    • 伊勢旅行に持っていったけど大きすぎる
  • 電源ケーブルがいけてない
    • 持ち運ばないのが前提?

 

MacOSに慣れてるならmacbookを買う方がいいんじゃないかという気はする。8月頭にWindows10の大規模アップデートがあるはずなのでその後にもうちょい触ろうかな。

 

 

Habitat Meetup #1で話してきた

Habitat Meetup #1 で話してきた。LTじゃない発表はずいぶん久しぶり。AlexさんがChef Automateについての紹介で、自分はHabitatの紹介+デモをした。資料は以下。

当日のtogetterはhttp://togetter.com/li/1004519

個人的には、最近Chefの動向をあまり追えてなかったのでキャッチアップができたのがよかった。特に、Complianse as Codeという考え方がおもしろい。ServerspecやInspecでサービスの状態を検査する目的としてコンプライアンスの担保、というものを提唱していて、すごくよいと思った。デフォルトでいろいろなspecが用意されていたり、その影響度を定義できるのもよい。

Habitatについては今後もおっかけようと思います。

HabitatでSinatraアプリのパッケージを作る

6/14にChef社の新しいプロダクト、Habitatが発表されたのでひとまず触ってみました。bignewsとかいってカウントダウンまでしてたのにカウントダウン終了前にTwitterやBlogに書いてて笑った。
チュートリアルを一通りやって、さて簡単なRubyアプリを動かそうということで、SinatraアプリをHabitatで動かした。

Dockerのイメージにエクスポートしてそれを動かす、までしかしていないのでサービスディスカバリとかどうなってるのかはよくわからない。

コツ的なもの

plan.shやhooksの書き方

https://app.habitat.sh/#/pkgs/core にあるパッケージを参考にするのがよい。ブラウザからはplan.shとconfigを見ることができる。
initやrunといったhooksは、pkg_build_depsに指定してstudio上でbuildすると、「/hab/pkgs/core/ruby-rails-sample/0.0.1/20160614073830/」のような場所にダウンロードしてくれるので、hooksディレクトリを覗けばよい。

hooksのデバッグ

最初、全然起動しなくて、しかもエラーが出力されてなかったのでかなり大変だった。
initやrunに「exec 2>&1」を書いておくと標準エラーを標準出力に出すのでエラーログを見ることができる。

作成記録

備忘録。大体チュートリアルと同じ。概念とか間違えてるかも。

docker環境の用意

dockerを使うので用意する。チュートリアルと同じ。環境変数や変数はcore/ruby-rails-sampleを参考にした。

[text]
docker-machine create –driver virtualbox habitat
eval $(docker-machine env habitat)
[/text]

plan.shの作成

Habitatではアプリや付随するライブラリをまとめたものをパッケージと呼ぶらしい。
で、パッケージを作るためにplan.shというファイルを作成する。
これは、どういうパッケージをインストールするか、ビルド時にどういう処理をするのかを定義しているファイル。
シェルスクリプトなので割ととっつきやすい?

今回作成したplan.shは以下。いろいろ試行錯誤したので不要な設定等があるかもしれない。

[text]
$ mkdir habitat_sinatra
$ cd habitat_sinatra
$ vim plan.sh
$ cat plan.sh
pkg_oriigin=origin
pkg_name=habitat_sinatra_sample
pkg_version=0.0.1
pkg_maintainer="Takaishi Ryo <ryo.takaishi.0@gmail.com>"
pkg_license=()
pkg_source=https://github.com/takaishi/${pkg_name}/archive/${pkg_version}.tar.gz
pkg_shasum=2977dff904580d64f72a833e9ce228fdff19ea6956aba36a5d8000ddd9ffe4c7
pkg_filename==${pkg_version}.tar.gz
pkg_deps=(core/ruby core/bundler core/bash)
pkg_build_deps=(core/ruby core/bundler core/bash)
pkg_lib_dirs=(lib)
pkg_include_dirs=(include)
pkg_expose=(8080)
pkg_bin_dirs=(bin vendor/bundle/bin)

do_build() {
local _bundler_dir=$(pkg_path_for bundler)

export GEM_HOME=${pkg_path}/vendor/bundle
export GEM_PATH=${_bundler_dir}:${GEM_HOME}
gem install bundler -v 1.11.2 –no-ri –no-rdoc
bundle install –path vendor/bundle
}

do_install() {
cp Gemfile ${pkg_prefix}
cp Gemfile.lock ${pkg_prefix}
cp app.rb ${pkg_prefix}
cp -r .bundle ${pkg_prefix}
cp -r vendor ${pkg_prefix}
}
[/text]

pkg\_\*\*の変数が基本的な設定で、do\_\*\*の関数はコールバック。コールバックでイメージの作成時にどういう処理を行うか定義します。
変数とコールバックは結構いろいろ種類があるみたい

このplan.shでやっているのは、主に

  1. pkg_sourceからアプリのリリースファイルをダウンロード&展開
  2. アプリを動かすのに必要なgemをbundleコマンドでダウンロード
  3. アプリを動かすのに必要なファイルを自分のパッケージにコピー

の3つ。

まず、ダウンロード。このplan.shには書かれていないけど、デフォルトで、do_downloadコールバックでpkg_sourceで指定したファイルをダウンロードするようになっている。
ダウンロードしたファイルのチェックや展開も、それぞれdo_verifyやdo_unpackでやる。

次にgemのダウンロード。do_buildでやっている通り。do_buildはデフォルトではmakeしているけど、このplan.shではそのコールバックを上書きしてgemをダウンロードしてる。

最後にファイルのコピー。デフォルトだとmake installを実行するけどここでは必要なファイルをコピーする。

hookの作成

plan.shはできたけど、これだけじゃアプリが動かない。動かすために、hookを作成する。

initの作成

initは起動時に実行される処理を書くみたい。
以下のように作成した。

[text]
$ mkdir hooks
$ vim hooks/init
$ cat hooks/init
#!/bin/sh
#
export GEM_HOME={{pkg.path}}
export GEM_PATH="$(hab pkg path core/ruby)/lib/ruby/gems/2.3.0:$(hab pkg path core/bundler):$GEM_HOME"

ln -sf {{pkg.path}}/Gemfile {{pkg.svc_path}}
ln -sf {{pkg.path}}/Gemfile.lock {{pkg.svc_path}}
ln -sf {{pkg.path}}/vendor {{pkg.svc_path}}
ln -sf {{pkg.path}}/app.rb {{pkg.svc_path}}

cd {{pkg.svc_path}}
exec 2>1&
exec bundle install –path vendor/bundle
[/text]

リンクを貼っているが、これはパッケージのディレクトリに置いてあるGemfileやapp.rbをサービス用のディレクトリに配置している。
plan.shで一度コピーしてその後リンクを貼るというには2度手間な気もするけどどうなんだろう…
その後、bundle install。do_installでコピーしたはずの.bundleがどうもコピーされていないので再度ここで実行。

runの作成

最後に、Sinatraアプリを実行するrun。

[text]
$ vim hooks/run
$ cat hooks/run
#!/bin/sh
#
export GEM_HOME={{pkg.path}}
export GEM_PATH="$(hab pkg path core/ruby)/lib/ruby/gems/2.3.0:$(hab pkg path core/bundler):$GEM_HOME"
cd {{pkg.svc_path}}
exec 2>&1
exec bundle exec ruby app.rb -o 0.0.0.0 -p 8080
[/text]

サービス用ディレクトリに移動してapp.rbを実行する。

hab studioでパッケージを作る

開発用のサンドボックスとしてstudioというものがあって、そこでパッケージのビルドやDockerイメージへのエクスポートができる。

[text]
$ hab studio enter
[1][default:/src:0]# ls
README.md hooks plan.sh results
[2][default:/src:0]# ls /
bin dev etc hab home lib lib64 mnt opt proc root run sbin src sys tmp usr var
[/text]

ビルドとエクスポートは以下のようにする。まぁチュートリアルと同じ。

[text]
[3][default:/src:0]# build
[4][default:/src:0]# hab pkg export docker r_takaishi/habitat_sinatra_sample
[/text]

実行

いよいよ実行する。とはいってもチュートリアルと大体同じである。

[text]
$ docker run -it -p 8080:8080 r_takaishi/habitat_sinatra_sample
hab-sup(MN): Starting r_takaishi/habitat_sinatra_sample
hab-sup(GS): Supervisor 172.17.0.4: 6c921b33-1872-4c14-8ab2-4ce6821fcd67
hab-sup(GS): Census habitat_sinatra_sample.default: ebb498f9-7c52-4ec1-810f-5245a3d2aa5b
hab-sup(GS): Starting inbound gossip listener
hab-sup(GS): Starting outbound gossip distributor
hab-sup(GS): Starting gossip failure detector
hab-sup(CN): Starting census health adjuster
init(PH): Don’t run Bundler as root. Bundler can ask for sudo if it is needed, and
init(PH): installing your bundle as root will break this application for all non-root
init(PH): users on this machine.
init(PH): Using rack 1.6.4
init(PH): Using tilt 2.0.5
init(PH): Using bundler 1.11.2
init(PH): Using rack-protection 1.5.3
init(PH): Using sinatra 1.4.7
init(PH): Bundle complete! 1 Gemfile dependency, 5 gems now installed.
init(PH): Bundled gems are installed into ./vendor/bundle.
habitat_sinatra_sample(SV): Starting
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO WEBrick 1.3.1
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO ruby 2.3.0 (2015-12-25) [x86_64-linux]
habitat_sinatra_sample(O): == Sinatra (v1.4.7) has taken the stage on 8080 for development with backup from WEBrick
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO WEBrick::HTTPServer#start: pid=72 port=8080
[/text]

起動したのでアクセスしてみる。

[text]
$ curl http://$(docker-machine ip habitat):8080/
Hello, habitat!%
[/text]

やったぜ。

Crystalのテスト周りについての覚え書き

Crystalでテストどうやって書くんだろうと調べたメモ。https://github.com/veelenga/awesome-crystal にいろいろ載っていたのでいくつか試した。
テスト機能としては、言語組み込みでrspecっぽい構文のspecが使える。よりrspecに近い機能を追加してある外部ライブラリのspec2もある。
これにPowerAssertを組み合わせる感じか。
MiniTestもあるけど試してない。

組み込みのSpec

Rubyのrspecのような仕組みが言語に組み込まれていて、「crystal spec」で実行できる。
http://crystal-lang.org/api/Spec.html を参照。
rspecと比べるとcontextやletがなかったり、before/afterもSpec.before_each、Spec.after_eachと書く必要があったりとRubyの感覚で書こうとして結構ハマった。

Crystalの場合

サンプルとしてspec.cr を作成。shouldを使う。

[ruby]
require "spec"

describe "Foo" do
Spec.before_each do
puts "Foo before_each"
end

Spec.after_each do
puts "Foo after_each"
end

describe "Bar" do
Spec.before_each do
puts "Foo::Bar before_each"
end

Spec.after_each do
puts "Foo::Bar after_each"
end

it "hoge" do
puts "hoge"
"hoge".should eq "hoge"
end

it "huga" do
puts "huga"
"huga".should eq "piyo"
end
end
end
[/ruby]

実行結果は以下の通り。

[text]
$ crystal spec spec.cr
Foo before_each
Foo::Bar before_each
hoge
.Foo after_each
Foo::Bar after_each
Foo before_each
Foo::Bar before_each
huga
FFoo after_each
Foo::Bar after_each

Failures:

1) Foo Bar huga
Failure/Error: "huga".should eq "piyo"

expected: "piyo"
got: "huga"

# ./spec.cr:28

Finished in 0.49 milliseconds
2 examples, 1 failures, 0 errors, 0 pending

Failed examples:

crystal spec ./spec.cr:26 # Foo Bar huga
[/text]

ネストの浅い箇所で定義したbefore_each/after_eachから順に実行されている。
rspecの場合はbeforeは浅い順に、afterは深い順に実行されるので最初混乱してしまった。
バグだろうか?

参考:Rubyの場合

上で書いたCrystalのspec相当のコードは以下のようになる。

[ruby]
require ‘rspec’

describe "Foo" do
before(:each) do
puts "Foo before_each"
end

after(:each) do
puts "Foo after_each"
end

describe "Bar" do
before(:each) do
puts "Foo::Bar before_each"
end

after(:each) do
puts "Foo::Bar after_each"
end

it "hoge" do
puts "hoge"
expect("hoge").to eq "hoge"
end

it "huga" do
expect("huga").to eq "piyo"
end
end
end
[/ruby]

実行結果。
beforeは浅いところから、afterは深いところから順に実行されている。

[text]
$ rspec ./spec.rb
Foo before_each
Foo::Bar before_each
hoge
Foo::Bar after_each
Foo after_each
.Foo before_each
Foo::Bar before_each
Foo::Bar after_each
Foo after_each
F

Failures:

1) Foo Bar huga
Failure/Error: expect("huga").to eq "piyo"

expected: "piyo"
got: "huga"

(compared using ==)
# ./spec.rb:27:in `block (3 levels) in <top (required)>’

Finished in 0.01156 seconds (files took 0.06919 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec.rb:26 # Foo Bar huga
[/text]

spec2

外部ライブラリ。よりrspecに近づいていて、なじみのある雰囲気。
ドキュメントは https://github.com/waterlink/spec2.cr を参照。
上で書いたspec.crをspec2で書き直すとこうなる。GlobalDSLをincludeすることでいい感じの見た目になってよい。letやsubject、expect等も使えるようになる。

[ruby]
require "spec2"
include Spec2::GlobalDSL

describe "Foo" do
before do
puts "Foo before_each"
end

after do
puts "Foo after_each"
end

context "Bar" do
before do
puts "Foo::Bar before_each"
end

after do
puts "Foo::Bar after_each"
end

it "hoge" do
puts "hoge"
expect("hoge").to eq "hoge"
end

it "huga" do
expect("huga").to eq "piyo"
end
end
end
[/ruby]

実行結果。afterの実行順序はspecと同様(rspecと逆)のようだ。

[text]
$ crystal spec spec2.cr
Foo before_each
Foo::Bar before_each
hoge
Foo after_each
Foo::Bar after_each
.Foo before_each
Foo::Bar before_each
F

In example: Foo Bar huga
Failure: Expected to be equal:
Expected: "piyo"
Actual: "huga"

[4407663170] *CallStack::unwind:Array(Pointer(Void)) +82
[4407663073] *CallStack#initialize<CallStack>:Array(Pointer(Void)) +17
[4407663032] *CallStack::new:CallStack +40
[4407776169] *Spec2::ExpectationNotMet@Exception#initialize<Spec2::ExpectationNotMet, String, Nil>:CallStack +41
[4407776092] *Spec2::ExpectationNotMet::new<String>:Spec2::ExpectationNotMet +108
[4407773796] *Spec2::Expectation(String)@Spec2::Expectation(T)#to<Spec2::Expectation(String), Spec2::Matchers::Eq>:Nil +68
[4407771919] *Spec2__Foo::Spec2__Bar::Spec2__Huga#run<Spec2__Foo::Spec2__Bar::Spec2__Huga>:Array(( -> Void)) +95
[4407769475] *Spec2::Runners::Default#run_context<Spec2::Runners::Default, Spec2::Reporters::Default, Spec2::Orders::Default, Spec2::Context>:Array(Spec2::Context) +355
[4407769950] *Spec2::Runners::Default#run_context<Spec2::Runners::Default, Spec2::Reporters::Default, Spec2::Orders::Default, Spec2::Context>:Array(Spec2::Context) +830
[4407769950] *Spec2::Runners::Default#run_context<Spec2::Runners::Default, Spec2::Reporters::Default, Spec2::Orders::Default, Spec2::Context>:Array(Spec2::Context) +830
[4407764660] *Spec2::HighRunner#run<Spec2::HighRunner>:Int32? +388
[4407765765] *Spec2@Spec2::run:Int32? +21
[4407652288] ~proc(Int32 -> Void)@./libs/spec2/spec2.cr:62 +16
[4407753648] *AtExitHandlers::run<Int32>:Array((Int32 -> Void))? +320
[4407652154] main +74

Finished in 1.16 milliseconds
Examples: 2, failures: 1
[/text]