カーネル/VMキャンプ #4 at 山梨県小菅村

温泉ハッカソン Advent Calendar 2016 3日目の記事です。

昨日より山梨県小菅村カーネル/VMキャンプ #4を開催しているので、こちらのハッカソン会場と温泉を紹介しようと思います。

カーネルVMキャンプってなに?

カーネルVM探検隊っぽいハッカソンイベント」です。
カーネルVM探検隊の主催者や常連参加者が中心に参加しており、特にジャンルの縛りや成果発表などはなくみんなで集まってわいわいやりながら各自好きな作業をするイベントです。

もうひとつのイベントの特徴として、毎回山奥で開催される掟になっています。
開催場所によってはロードバイクで宿まで来る参加者がいたり、山へハイキングに行く参加者がいたりします。


小菅村


小菅村奥多摩湖の西側、大月の北側、檜原村(※)から山ひとつ越えた裏側にある人口約700人の山間の小さな村です。多摩川源流の小菅川での釣りと周辺の山でのハイキングが人気のようです。
※…東京都檜原村潜入記録



ハッカソン会場

廣瀬屋旅館を利用させていただきました。
一般客の宿泊プランは一泊二食付き10,000円ですが、合宿ということで食事の内容を安くしたりアメニティを省略していただく代わりに少し割引してもらえました。
宿泊客が多くなかったので部屋を多めに貸して頂けたので、客室のうちの一つにハックルームを構築しました。
ネットワークは全館WiFi有りだったのでこれをそのまま利用しました。
10名で負荷かけたらWiFi APが落ちまくりましたが、そういう使い方は想定されてなかったのかも。

モバイル回線に関しては、Docomo MVNO回線ならLTEで数Mbps出てました。





温泉

宿には5人同時に入れるサイズの浴場がありますが温泉ではないので日帰り温泉を利用しました。

宿から1.7km先に小菅の湯道の駅こすげがあります(@nifty温泉の口コミ)。

泉質はPH9.98の強アルカリ性温泉で、透明でぬめりのあるお湯です。露天風呂も付いてます。


入館料は3時間で620円、冬季営業時間は10:00〜18:00でした。

昼食

小菅の湯・道の駅こすげを利用しました。

おさんぽ

松姫峠から鶴寝山を経由し山沢入りのヌタへ行って戻る片道1時間のイージーハイクコースがあります。

晴れていると鶴寝山からの富士山がきれいです。



アクセス

小菅の湯の案内を見たほうが早いです。

公共交通機関の便はダメです。
奥多摩駅からのバスは日に4本で所要時間50分、大月駅からのバスは日に3本で所要時間60分です。

車の場合は大月ICから松姫トンネルを通って小菅村まで30分です。

ハッカソンの成果

この記事が書けました(完)

USBメモリのフォーマット無しに、たった一つのファイルをコピーするだけでブート出来るLinux

singlefilelinuxとか名前を付けてみたが、新しいディストリを作ったり新しいツールを作ったりしたのではなく、単にGentooのLiveCDを単一のカーネルイメージに埋め込んでブート可能にしただけである。

使い方は以下の通り:

http://s3.amazonaws.com/syuu1/singlefilelinux.gif

これで普段データを入れておく用のUSBメモリからデータを消すことなくLinuxのインストールをしたり、壊れたbtrfsの復旧作業が出来るようになった。

なお、UEFI専用なので当然のことながらUEFI非対応のPCでは使えない。
また、UEFIexFATに対応していないので32GBを超える容量のUSBメモリでこれを使う事は難しい(デフォルトでexFATでフォーマット済みだし、FAT32は32GB以上の容量をサポートしないし、UEFINTFSext3などにも対応していないため)

ちなみにGentooにしたのはLiveCDのイメージサイズが小さくてsystemdじゃなかったので簡単そうだったから、というだけの理由で、Ubuntuや他のディストリだと出来ないということではありません。

イメージのビルドの仕方はGithubを参考にして下さい。
他のディストリ対応試してくれる人が居るとうれしいな。


追記:当然ながらsecurebootとは共存出来ない…

Route 53でcli53を使ったDynamic DNS

Route 53でDDNSというとroute53DynDNS.bashを使った例を多く見かけるのだが、自分の手元では正常動作しないし読めた物ではない感じの作りなので、cli53を使って極めて簡単に書き直した。

AWSインスタンスでの例はこちらにあるのだが、ここではAWSではない自宅鯖のDDNSに用いるのでEC2関係の所は参考にしない。

cli53をインストー
sudo pip install cli53
.botoにIDとKeyを設定

こんな感じの奴を作る

[Credentials]
aws_access_key_id = <your_access_key_here>
aws_secret_access_key = <your_secret_key_here>
update53.shを作成
#!/bin/sh

ZONE=example.com
RECORD=www
DAT=/tmp/update53.dat

if [ -f $DAT ]; then
	PREV=`cat $DAT`
else
	PREV=`cli53 rrlist $ZONE|grep ^$RECORD|awk '{print $5}'`
fi
CUR=`wget -q -O - checkip.dyndns.org|sed -e 's/.*Current IP Address: //' -e 's/<.*$//'`
echo $CUR > $DAT

if [ "$PREV" == "$CUR" ]; then
	exit 0
fi

cli53 rrcreate $ZONE $RECORD A $CUR --ttl 3600 --replace

crontabに適当にインストー

*/5 * * * * /home/syuu/update53.sh

mrubyでUEFIブートローダなやつのバイナリ配布

多分みなさんEDK2のビルドが面倒い最高で試すの嫌だと思うので、バイナリ配布しておきますね。
こちらです:

https://s3.amazonaws.com/syuu1/mruby.efi

ブートローダスクリプトはこちら:
https://raw.githubusercontent.com/syuu1228/mruby_on_efi_shell/devel/example/bootloader.rb

いきなりefibootmgrなどを使ってブートメニューに組み込もうとせずに、EFI Shellから起動することを強くおすすめします(しくってブート出来なくなったりすると困ると思うので)。

※追記:落としてバイナリ見れば分かると思うんだけど、X64版です。他のアーキテクチャ(IA32を含む)では動かんです。

GCEでOSvを動かそう

OSvは、ハイパーバイザやIaaSプラットフォームへアプリケーションをデプロイすることに特化した軽量OSです(※詳しくはこちら)。

今回はRuby on Railsで書かれたブログエンジン「Publify」をインストールしたOSvのイメージをGoogle Compute Engine(GCE)へデプロイしてみます。

プロジェクトを作成

まずOSv用にプロジェクトを作成してみます。

Cloud Storageの登録

Cloud Storageが有効化されてなかったので登録しておきます。

GCE SDKのインストー

GCE管理用のコマンド群を使用するため、SDKをインストールします。

curl https://sdk.cloud.google.com | bash
プロジェクトIDの設定

先ほど作成したプロジェクトへログインしてIDを設定します。

gcloud auth login
gcloud config set project fresh-mason-798
バケットの作成

OSvのディスクイメージをアップロードするためにCloud Storageへバケットを作成します。

gsutil mb gs://osv_test
Rubyのインストー

Publify入りのディスクイメージをビルドするためにrvmでRubyをインストールします。

 gpg --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable
source ~/.profile
rvm install 2.1.4
rvm use 2.1.4
sudo yum install sqlite-devel
OSvイメージのビルド

OSvのソースコードをcloneしてPublifyのイメージをビルドします。
ビルドが終わったらGCE用のtar.gzファイルを生成します。

git clone https://github.com/cloudius-systems/osv.git
git submodule update --init --recursive
make image=ruby-publify
./scripts/gen-gce-tarball.sh

※今回は、make image=ruby-publify,httpserverとしたところエラーが出て起動しなかった(Rubyとの組み合わせで発生するバグ)のでhttpserverを外しましたが、これがないと8000番ポートにRESTサーバが起動しないのでOSvの管理が著しく不便になります。Ruby以外のアプリを使用する時は必ず付けることをオススメします。

OSvイメージのデプロイ

gsutil・gcutilコマンドを使ってgen-gce-tarball.shコマンドで生成したosv.tar.gzをアップロード、デプロイします。
今回はus-central1-aのf1-microへosvという名前で実行することにしました。

gsutil cp build/release/osv.tar.gz gs://osv_test/osv.tar.gz
gcutil addimage osv gs://osv_test/osv.tar.gz
gcutil addinstance --image=osv  --machine_type=f1-micro  --zone=us-central1-a osv
動作状況の確認

getserialportoutputでOSvインスタンスの動作状況を確認します。

gcutil getserialportoutput osv

以下のような出力が表示されます。

(略)
OSv v0.16-22-ga6c87f0
eth0: 10.240.159.233
sigaltstack() stubbed
=> Booting Thin
=> Rails 3.2.21 application starting in production on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
You did not specify how you would like Rails to report deprecation notices for your production environment, please set config.active_support.deprecation to :notify at config/environments/production.rb
WARNING: fcntl(F_SETLK) stubbed
Thin web server (v1.6.3 codename Protein Powder)
Maximum connections set to 1024
Listening on 0.0.0.0:3000, CTRL+C to stop
ファイヤウォールの設定

グローバルIPからポート3000へ接続出来るように設定を変更します。

gcutil addfirewall publify --description="publify http" --allowed="tcp:3000"
Publifyへ接続

http://<インスタンスグローバルIP>:3000/ へブラウザから接続すると、Publifyのセットアップ画面が出てくるので設定を開始します。

覗いてみる

http://146.148.55.173:3000/ で起動中です(そのうち落とします)。

まとめ

OSvのイメージをGCEへデプロイする方法を解説しました。
今回はPublifyの例を挙げましたが、CassandraのようなJavaアプリやRedisのようなネイティブアプリも同様の手順でデプロイすることが可能です。

UEFI Native API on mruby

mrubyでブートローダを書けるようにしました - かーねる・う゛いえむにっきの記事はlibc API経由でmrubyからUEFIの機能を使う話でした。
これとは別に、mruby on efi shellにはUEFIのネイティブAPIへアクセスするためのクラスが用意されているという話をします。

mruby on efi shellのソースコードを眺めていると、二つのスクリプトが見つかります(こちら)。

一つはdump_cmos.rbで、UEFI::LowLevel.io_read8/write8を呼び出してIOポートアクセスを行っている事が分かります。

################################################3
# Example
#  CMOS dump.

class String
  def rjust(width, padding=' ')
    str = ""
    if (self.length < width)
      str = padding * (width - self.length)
    end
    return str + self
  end
end

CMOS_INDEX = 0x70
CMOS_DATA = 0x71

orig_index = UEFI::LowLevel.io_read8(CMOS_INDEX)
(0..0x7F).to_a.each_with_index do |i, index|
  UEFI::LowLevel.io_write8(CMOS_INDEX, i)
  v = UEFI::LowLevel.io_read8(CMOS_DATA)
  print "#{v.to_s(16).rjust(2, '0')} "
  print "\n" if (index % 16 == 15)
end
UEFI::LowLevel.io_write8(CMOS_INDEX, orig_index)

もう一つがread_disk.rbで、こちらはUEFI::ProtocolやUEFI::BootService、UEFI::Guidなどのクラスを用いてEFI_DISK_IO_PROTOCOLへアクセスし、メディア情報を取得しています。

# Sample of BLOCK_IO_PROTOCOL

def print_binary_data(data)
  data.bytes.each_slice(16) do |data|
    # print hex.
    data.each do |i|
      print i.to_s(16).rjust(2, "0").upcase
      print " "
    end
    print "   "
    # print ascii.
    data.each do |i|
      print (0x20 <= i && i <= 0x7F) ? i.chr : "."
    end
    puts ""
  end
  puts ""
end

class BlockIoProtocol < UEFI::Protocol
  GUID = UEFI::Guid.new("964e5b21-6459-11d2-8e39-00a0c969723b")

  define_variable(:revision, :u64)
  define_variable(:media, :p)
  define_function(:reset, :e, [:p, :b])
  define_function(:read_blocks, :e, [:p, :u32, :u64, :u64, :p])
  #...
end

# I will rename UEFI::Protocol to UEFI::Structure or any other name...
class Media < UEFI::Protocol
  define_variable(:media_id, :uint32)
  #...
end

# See http://wiki.phoenix.com/wiki/index.php/EFI_DISK_IO_PROTOCOL
class DiskIoProtocol < UEFI::Protocol
  GUID = UEFI::Guid.new("CE345171-BA0B-11d2-8e4F-00a0c969723b")

  define_variable(:revision, :u64)
  define_function(:read_disk, :efi_status, [:p, :u32, :u64, :u64, :p])
  #...
end


# Find first disk.
handles = UEFI::BootService.locate_handle_buffer(BlockIoProtocol::GUID)
handle = handles.first
puts "handle: #{handle}"

# Locate BlockIoProtocol to get the pointer of media.
ptr = UEFI::BootService.handle_protocol(handle, BlockIoProtocol::GUID)
bp = BlockIoProtocol.new(ptr)
# Get media.
media = Media.new(bp.media)
puts "media_id: #{media.media_id}"

# Find DiskIoProtocol related to bp.
ptr = UEFI::BootService.handle_protocol(handle, DiskIoProtocol::GUID)
dp = DiskIoProtocol.new(ptr)

buf = " " * 512  # Buffer to be filled with returned data.
st = dp.read_disk(ptr, media.media_id, 0, buf.size, buf)
if (st.success?)
  print_binary_data(buf)
else
  puts "ERROR: #{st}"
end

プロトコルのハンドルを作るところの操作でクラスを継承してインタフェースを動的に定義するなどの違いはありますが、概ねEDK2に含まれるCで書かれたUEFIのサンプルアプリと同様のインタフェースを持っている事が分かります。

C拡張のコード(mruby_on_efi_shell/src at master · masamitsu-murase/mruby_on_efi_shell · GitHub)を眺めてみると、UEFI::Handle, UEFI::Pointer, UEFI::Protocol, UEFI::Status, UEFI::BootService, UEFI::LowLevel, UEFI::RuntimeServiceのようなクラスが定義されています。

このインタフェースもEDK2で使われているものと大体一致しているので、完全ではないかもしれないがCのコードを翻訳していけばだいたいmrubyで記述出来そうだと予想されます。

今回はここまでで、次回何らかのサンプルコードを書いてみることにします。

mrubyでブートローダを書けるようにしました

カーネルVM@関西7回目でお話したネタです。スライドはこちら:

既にmrubyはUEFIへ移植されています(覚え書き: mruby on EFI Shell)。

また、EFI StubカーネルUEFIアプリから実行するのは非常に簡単であることが分かっています(実際にソースコードを書いて実験しました)。

以上のことから、「mrubyにEFI Stubカーネルのファイル名を指定したら実行するAPIを足せば、mrubyでブートローダが実装可能になるに違いない」と考えました。

ただ面白いというだけでなく、mruby-socket・mruby-simplehttp-socketなどのmrbgemsを移植していけばブート処理を記述したmrubyスクリプトカーネルそのものをHTTPから取得することが出来るようになり、簡単なスクリプトで高度にカスタマイズされたブート処理をOSに依存せず行う事が出来るようになると思い、まずはたたき台として単純なブートメニューを実現するものを実装してみました。

以下に今回実装したブートローダのmrubyスクリプトを貼ります(Dirクラスのコードは一部省略しました。フルサイズのコードはこちら)。

base_dir = "kernels"
kernel_options = "root=/dev/sda3"
kernels = []

Dir.foreach(base_dir) do |ent|
	next if ent == "." || ent == ".."
	kernels << ent
end

puts "** Boot Menu **"
(0...kernels.length).each do |i|
	puts "[#{i}] #{kernels[i]}"
end
loop do
		puts "Input number:"
		key = Shell.gets.chomp
		idx = key.to_i
		if kernels.length <= idx
			next
		end
		puts "#{base_dir}\\#{kernels[idx]} #{kernel_options}"
		Shell.exec "#{base_dir}\\#{kernels[idx]} #{kernel_options}"
		break
end

ここでは、ブートローダを実現するために「Dir」と「Shell」クラスを実装しています。
(mruby_on_efi_shellでは殆どのモジュールが無効化されている上にmrbgemsのビルドが封じられており、更にLinuxと完全に同等のAPIを提供してはいないので、ビルドスクリプトを編集したりAPIを叩いて1クラスごとに定義していく必要がありました。)

Dirクラスはmruby-dirから最低限必要なコードを抜き出してきたものです。
mruby_on_efi_shell/dir.c at devel · syuu1228/mruby_on_efi_shell · GitHub
普通にreaddir()などが使えているのはEDK2のlibcを使用しているからです。今回の作業では基本的にlibcのAPIを使用しています。
(このlibcは実験版扱いで、UEFIアプリケーションの正式なAPIではなく実験的な実装です。アプリをUEFIへポーティングする際には便利で、mruby_on_efi_shellもこれを利用して移植されています。よって、今回はこれに頼ることにしました。)

Shellクラスは今回独自に実装したもので、libc関数であるsystem(3)・fgets(3)の単純なラッパ関数(それぞれShell.exec, Shell.gets)を提供します。
以下にコードを示します。

#include "mruby.h"
#include "mruby/class.h"
#include "mruby/data.h"
#include "mruby/string.h"
#include "error.h"
#include <stdlib.h>
#include <stdio.h>
#include "uefi.h"

mrb_value
mrb_shell_exec(mrb_state *mrb, mrb_value klass)
{
  mrb_value command;
  char *ccommand;
  int ret;

  mrb_get_args(mrb, "S", &command);
  ccommand = mrb_string_value_cstr(mrb, &command);
  ret = system(ccommand);
  return mrb_fixnum_value(ret);
}

mrb_value
mrb_shell_gets(mrb_state *mrb, mrb_value klass)
{
  char buf[65535];
  if (fgets(buf, 65535, stdin) == NULL)
	return mrb_nil_value();
  return mrb_str_new_cstr(mrb, buf);
}

void
mrb_init_shell(mrb_state *mrb)
{
  struct RClass *shell;

  shell = mrb_define_class(mrb, "Shell", mrb->object_class);
  MRB_SET_INSTANCE_TT(shell, MRB_TT_DATA);
  mrb_define_class_method(mrb, shell, "exec", mrb_shell_exec, ARGS_REQ(1));
  mrb_define_class_method(mrb, shell, "gets", mrb_shell_gets, ARGS_NONE());
}

本当はUEFIネイティブなAPIを使うともう少し複雑なAPIコールが必要になるのですが、ここではlibc関数でその部分をラップすることで関数コール1つだけの単純なコードになっています。

上述のmrubyスクリプトの通り、fgets(stdin)でキーボード入力を取って起動するカーネルのファイル名を決め、system()にカーネルファイル名と引数を渡して実行することでカーネルが起動しています。

というわけで、実に簡単なハックでブートローダがmrubyで書けるようになりました。
これだけではAPIが足りませんし、コードが極めてとっちらかっているので、もう少し整理していきたいと考えています。

現状のコードはこちらで公開しています:
GitHub - syuu1228/mruby_on_efi_shell at devel

※このコードではEFI StubカーネルLinux)や各種UEFIブートローダ(拡張子.EFIのPEバイナリ)のみがブート対象です。それ以外のものは例えUEFI対応カーネルでもブートできません。 多くのディストリビューションEFI Stubカーネルを提供しているのでこれで必要充分だろうと判断しました。