Linux Advent Calendar 2016、21日目の記事です。
以前、NEW GAME! Ops実現に向けてSlack Botサンプルを作る記事を書いたことがあったのですが、最近またSlack Bot熱が上昇してきました。
というのも、ここ最近、某社のAdvent Calendarをひたすら書くという日々を過ごしていて、その記事ネタの一つとしてSlack Botでビルドを実行するサンプルスクリプトを書いてみたところ、思っていたよりも便利で「これが...Botの能力(ちから)...!」と感銘を受けたワケです。
そこで今日のLinux Advent Calendarでは、NEW GAME!をインスパイアしつつ、Slack BotからLinuxカーネルをビルドする例を紹介しようと思います。
LinuxカーネルのビルドをSlack Botから実行する
まずは実行例
まずは実行例を示しつつ、動作イメージを説明できればと思います。
適当なSlackチャンネルで「リリースされているLinuxカーネル」と入力すると、The Linux Kernel Archivesに掲載されているカーネルバージョン一覧を表示します。
「Linuxカーネル<バージョン番号>のビルド」と入力すると、おもむろにビルドが開始されます。
まだサンプルなので、機能は上記の2つだけです。以下に実際のコードを示しながら動作の詳細を紹介します。
Botの動作とカーネルビルド出力との連携
Linuxカーネルビルドのコマンド実行をBot(というかRubyスクリプト)から実行するのは、IO.popen
を使えば良いとはいえ、ちょっと面倒そうです。だいたい日が変わるギリギリでAdvent Calendarを投稿するような筆者にはシェルスクリプトで実装するような手早い方法でないとダメそうです...。
というわけで、Linuxカーネルのビルド部分は以下のようなシェルスクリプトで実行してみます。単にビルドの手順をベタ書きしているだけですが、echo 'MSG: ...'
という部分は、Ruby+Slack Bot側で"MSG:"を含む行はSlackへの応答として返す文字列になります。
#!/bin/sh
# DRY_RUN=echo とかでdry-runできるぞい。
DRY_RUN=
if [ $# -eq 0 ]; then
echo 'MSG: エラーだぞい。'
exit 1
fi
url=$1
if [ ! -f `basename ${url}` ]; then
echo "MSG: 以下のファイルをダウンロードするぞい。<BR>\`\`\`<BR>${url}<BR>\`\`\`"
${DRY_RUN} curl -O ${url}
fi
if [ ! -d `basename ${url} .tar.xz` ]; then
echo "MSG: ファイルを展開するぞい。"
${DRY_RUN} tar Jxf `basename ${url}`
fi
${DRY_RUN} cd `basename ${url} .tar.xz`
echo "MSG: \`make allnoconfig\` を実行するぞい。"
${DRY_RUN} make allnoconfig
echo "MSG: \`make\` を実行するぞい。"
${DRY_RUN} make
Slack Bot側の実装(Ruby)は以下のようになります。前述のシェルスクリプトをIO.popen
で実行し、1行ずつ取得した標準出力の内容を必要に応じてSlackに投稿するという処理になっています。
#!/usr/bin/env ruby
# coding: utf-8
require 'net/http'
require 'uri'
require 'erb'
include ERB::Util
require 'json'
require 'nokogiri'
require 'open-uri'
require 'slack'
def post(msg, channel)
param = {
token: ENV['SLACK_TOKEN'],
channel: channel,
text: msg,
username: '涼風青葉@イーグルジャンプ',
icon_url: 'http://newgame-anime.com/assets/special/twticon/ng_icon_1.jpg'
}
Slack.chat_postMessage(param)
end
Slack.configure {|config|
config.token = ENV['SLACK_TOKEN']
}
client = Slack.realtime
start_time = Time.now.to_i
is_working = false
client.on :message do |data|
post_time = data["ts"].sub(/\..*$/, "").to_i
next if post_time < start_time
msg = data["text"]
releases = {}
buf = File.open("_kernel.org.txt", "r") {|f| f.read }
dom = Nokogiri::XML(buf)
dom.xpath("//table[@id='releases']//tr").each do |rel|
list = rel.xpath("td")
obj = {}
obj["label"] = list[0].text
obj["version"] = list[1].text
obj["release_date"] = list[2].text
obj["url"] = list[3].xpath("a/@href")
releases[list[1].text] = obj
end
if msg =~ /^リリースされているLinuxカーネル/
msg = "現在リリースされているLinuxカーネルの一覧を表示するぞい。\n"
msg = msg << "\`\`\`\n"
formatted_str = ''
releases.each_pair do |k, rel|
fmt_label = sprintf("%12s", releases[k]["label"])
fmt_version = sprintf("%16s", releases[k]["version"])
fmt_release_date = sprintf("%10s", releases[k]["release_date"])
fmt_url = sprintf("%s", releases[k]["url"].text.gsub(/^.*linux-/, 'linux-'))
formatted_str = formatted_str << <<-EOS
#{fmt_label} #{fmt_version} #{fmt_release_date} #{fmt_url}
EOS
end
msg = msg << formatted_str
msg = msg << "\`\`\`"
post(msg, data["channel"])
end
if msg =~ /^Linuxカーネル(.*)のビルド/
target_version = $1
target_release = nil
releases.each_pair do |k, rel|
if releases[k]["version"] == target_version
target_release = releases[k]
break
end
end
if target_release == nil
post('そんなバージョンは見つからないぞい。', data["channel"])
next
end
if is_working == true
post('いまビルド中ぞい。', data["channel"])
next
end
post("Linuxカーネル `#{target_version}` をビルドするぞい!", data["channel"])
is_working = true
cmd = "./build_kernel.sh #{target_release['url']}"
puts cmd # debug
begin
Thread.new do
start = Time.now
IO.popen(cmd, "r+") do |proc|
last_msg_type = ''
while (line = proc.gets)
msg = line.chomp
puts msg # debug
post($1.gsub(/<BR>/, "\n"), data["channel"]) if msg =~ /^MSG:(.*)$/
if msg =~ /^ /
msg_type = msg.split(" ")[0]
if last_msg_type != msg_type
post("`#{msg}` ぞい!", data["channel"])
last_msg_type = msg_type
end
end
end
end
finish = Time.now
msg = "ビルド時間は `#{(finish - start).round(2).to_s} 秒` ぞい!"
post(msg, data["channel"])
is_working = false
end
rescue Exception => e
puts e.message
puts e.backtrace.inspect
end
end
end
puts "ready."
client.start
シェルスクリプトの出力をSlackに投稿する
例えば、Linuxカーネルのビルドを走らせると、シェルスクリプトからは以下のような出力がなされます。
$ bundle exec ruby KernelBuildBot.rb
ready.
./build_kernel.sh https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.tar.xz
MSG: 以下のファイルをダウンロードするぞい。<BR>```<BR>https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.tar.xz<BR>```
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 88.8M 100 88.8M 0 0 2905k 0 0:00:31 0:00:31 --:--:-- 3084k
MSG: ファイルを展開するぞい。
"MSG: ファイルを展開するぞい"の部分がSlackに投稿されています。
それと、長いビルドが終わるまでSlackに投稿がないと、ちゃんと処理が走っているのか少し不安になります。そこで例えば以下のような出力における、空白から始まる行についてもSlackに投稿してみます。ごく短時間に大量の投稿はマズイ気がするので、第一カラムの文字(HOSTCC
とか)が前回と同じ場合は出力をスキップする方法でSlackへの投稿スピードを抑えるようにしてみます。
MSG: `make allnoconfig` を実行するぞい。
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
SHIPPED scripts/kconfig/zconf.tab.c
SHIPPED scripts/kconfig/zconf.lex.c
SHIPPED scripts/kconfig/zconf.hash.c
HOSTCC scripts/kconfig/zconf.tab.o
それでも以下のような感じで、もりもりとSlackに投稿されます。
「ぞい!」の多さにそこはかとなく狂気を感じます...。
それと、ビルド処理は再入できないようにしてあります。
以下のようにビルド中に再度ビルドしようとするとエラーになります。
無事にビルドが完了すると、ビルド時間が表示されます。
意外とビルド時間の測定が行えるのは便利かもしれないと思っています。
まとめ
NEW GAME!Ops実現に向けて、LinuxカーネルのビルドをSlack Botから行うサンプルを作成してみました。ビルドしたカーネルをAmazon S3にアップロードしたり、VMで起動してみたりすると実用的になりそうです。