Rubyのサンドボックスを作って、evalするBotを作った


注意:安全じゃありません

RubyのSnadbox環境

Sansbox環境とは、外部から入力されたプログラムを安全に実行する環境のことです。任意のコードを入力可能な場所で、いきなり system(“rm -rf ~/) とか入力されて、それが本当に実行されたら困りますよね? 自分は困ります。ですが、外部から入力されたコードを安全に実行する環境というのはそれなりに需要があり、最もわかりやすいところではJavaScriptを実行するブラウザ、わかりにくいところでは今回作ろうとしているeval用のbotです。

ブラウザに関しては、インターネットという非常に治安の悪い場所から送られてくるコードを自分の環境で実行するので、サンドボックスが必要です。同じように、会社のSlackで公開するRubyの任意のコードを実行してくれるBotでも、社内の邪悪な人から投げ込まれたコマンドで自分の環境を破壊されると困るわけです。競技プログラミングの採点サーバーなどで、送られてきたコードを実行するときに邪悪な奴だったら困りますよね? そういう邪悪なコードから身を守るために、サンドボックスは必要です。

その一方で、Rubyは結構自由すぎる言語で、任意の入力に対する安全なコードの実行というのはなかなか難しいです。Rubyには、汚染マークとセキュリティレベルという、外部からの入力を安全に扱う機構があり、これはメタプログラミングRubyでも紹介されています。詳細はリンク先のページを見てもらえると解りますが、しかしこの機構には一つ大きな問題があり、それは汚染された文字列をevalすることができないということです。そのため、汚染された文字列を自分の手で安全だとマークしない限り、evalの引数に渡して実行することができないのです。それはつまり、セキュリティレベルで保護されている内容と同じレベルの安全性であることを自分で保証しなくてはならないということです。それって、セキュリティレベルを自分でもう一度チェックしなくちゃいけないということなので、ハッキリ言って意味が無いです。

なので、セキュリティレベルに頼ることなく、危険なコードを事前に実行できないように、サンドボックス環境を用意する必要があります。

安全に邪悪なRubyコードを実行するには?

邪悪なコードというのは、概ねファイルをどうこうしたり、任意の外部コードを実行しようとするものです。冒頭の system(“rm -rf ~/”) もその類いです。なので、危険なコードを実行できるような機能を片っ端から潰していけばいいわけです。

Rubyのセキュリティレベルでは、Dir, File, IO, FileTestモジュールへのアクセスを禁止しています。また、任意のコードを実行するKernel系のメソッドも禁止しています。

幸い、Rubyは非常に強力なメタプログラミング機構を持っているので、このようなモジュールのアクセスに対するフックを用意することも容易です。また、Refinementという機能を使い、特定のスコープのみでそのフックを有効にすることも可能です。

具体的には、Dirなどのモジュールのクラスメソッドに対して、実行すると例外を投げるメソッドを定義し、すべてのメソッドのエイリアスとして設定します。

実際のコードは次のようなものになりました。

File, Dir, IO, FileTestの全メソッドに加えて、Kernelの使っても問題なさそうなメソッド以外を、すべて例外を投げるようにaliasします。

まず各モジュールに対してbannned_methodという、コールすると例外を投げるメソッドを用意し、各モジュールのメソッド一覧で得られたすべてのメソッドに対して、このbannned_methodへのエイリアスを張ります。そうすることで、ちょっとでも危険そうな動作をすると、例外を投げるようになります。

Kernelのコードも結構塞いでいるので心配になりますが、ちょっとevalするのにKernelのメソッドが必要になるケースの方がイレギュラーなので、無視します。

これを実際に利用するには、次のようにします。

evalするために渡されるコードを、Sandboxモジュールを適用したCleanRoomモジュールの中で実行し、その結果を得て出力します。

たとえば、 rm -rf ~/ みたいに非常に邪悪な文字列を入れて実行すると、SecurityErrorが投げられます。

これを利用したRubyのeval用botは、次のリポジトリを参照してください。Mobbをつかって非常に簡素に書くことができました。

https://github.com/kinoppyd/ruby-eval-bot

実際これは安全なんですか?

正直よくわかりません。概ねの場合において安全だと思いますが、Rubyはいかんせん自由度が高い言語なので、なんかこれくらいなら回避して邪悪なことができそうな気もします。

真に安全なSandbox環境がほしいので、是非これを読んだ人の意見を聞かせてほしいです。

他の安全な方法

もう一つ思いついた方法としては、evalするときにDockerコンテナを起動する方法です。

コンテナの中で実行されるRubyのコードが本当にホストから見て安全なのかどうかという確信はいまいち有りませんが、少なくとも自分でこねくり回したSandboxよりは安全な気がします。

しかし安全とはいえ、コンテナの中で実行できるすべてのことができてしまうと言えばできてしまうので、これもまあまあ怖いなあと思い、今回はSandboxを手で作ってみました。

みんなの考えた最強のSandboxを教えてほしい

実際、Rubyのサンドボックスは需要は少ないと思いますが必要となるケースが無いわけでは無いと思います。

そのため、みなさんが作った最強のサンドボックスのコードを、教えてほしいなと思います。

追記1

会社のSlackで動かしたところ、早速邪悪なコードを放り込んでくれた隣の席の人がいました。こういうコードです。

なるほど、実際CleanRoomの中に入力されたコード文字列をペタッと貼っているだけなので、SQLインジェクションみたいなことができるわけですね……あんまり深く考えていなかった。

対策として、入れられた文字列がRubyのシンタックスとして正当かどうかをチェックするようにしました。少なくともRubyのシンタックスとして正当であれば、周りをCleanRoomで囲えば安全なはずです。

https://github.com/kinoppyd/ruby-eval-bot/commit/a1ae3efaefc7cc9b1d57197a59ff07fc4e774c24

RubyVMを使って、渡された文字列からASTを作成できるかどうかをチェックしています。先程の例のような文字列が渡されるとSyntaxErrorがスローされるので、評価部分に入ることはありません。

ありがとう、隣の席の邪悪な人。

追記2

斜め後ろの席に座ってる邪悪なRubyコミッタの人がまたろくでもないコードを投げつけてくれました。こういうコードです。

MobbはENVに入っているSlackTokenを参照しているので、この一撃でTokenのRegenerateが必要になりました。Regenerateしたとはいえ、Tokenがいきなり公衆の面前にさらされるのは結構精神的ダメージでかいので、これはへこみました。

putsとかの副作用系は封じていたので平気だったと思っていましたが、よく考えたらinspectとかto_sとかの方法で出力は得られるので、盲点でした。

対策として、ENVにアクセスしようとするコードは一律排除することにしました。結構力技で排除しているので、これはなんかいろいろこねくり回したら回避できる気もしますが、一旦入れておきます。

https://github.com/kinoppyd/ruby-eval-bot/commit/c90a1d24125a816b2490b95375ce839b8a44e76b

ありがとう、斜め後ろの席の邪悪な人。

追記3

下のフロアで働いてる邪悪なセキュリティマニアが、邪悪なコードを優しく送ってくれました。こういうコードです。

なるほどね、そういえばASTはevalできるんだよね……という気持ちになりました。

https://github.com/kinoppyd/ruby-eval-bot/commit/5125b409ae22bd985c4c077e3a4900f90e0563cf

ありがとう、下のフロアの邪悪な人。

追記4

TD社で働いている素敵な人から、はてブのコメント経由で指摘をいただきました。Object空間のKernel系は塞いているけど、Kernelを直接呼ぶとダメじゃない? ということです。つまり、こういうことです。

完全にうっかりしていたので、慌てて塞ぎました。GitHubが落ちててPushできなくて辛かったです。

https://github.com/kinoppyd/ruby-eval-bot/commit/d35e1589616b50823d17c956c13e062871217168

ありがとう、TD社の素敵な人。

追記5

にょろにょろした素敵なアイコンの人から、TwitterでProcessが塞がれてないという指摘をいただきました。こういうことです。

うっかりです。というか、モジュールが多すぎて見逃しがたくさんあります。

https://github.com/kinoppyd/ruby-eval-bot/commit/977ab403cdff493c33101ed35426550c75e037d3

ありがとう、にょろにょろしたアイコンの素敵な人。


カテゴリー: Ruby | コメント: 2 件

Ruby製の軽量Botフレームワーク Mobb をリリースしました


TL;DR

Sinatra-isiに記述できるBotフレームワークMobbと、Botとサービスの間を取り持つRackに相当するReppを作って公開しました。

作ろうと思った理由

RubyでBotを作ろうと思うと、大体の人はRubotyを選択すると思います。GoogleでRubyを使ってSlack Botを作る方法なんかをいくつか探していると、そのうち自然とRubotyに行き着くと思います。ドキュメントやサンプルも日本語で多数用意されており、日本のRuby界隈におけるBotの選択肢としてはデファクトスタンダードではないかと思っています。

Rubotyはとてもよく作られたBotフレームワークですが、個人的にはプラグインのロードにBundlerを使って$LOADPATHから順次プラグインをロードしている点がやや使いづらいと感じていました。公にできない自作プラグインや業務レベルのロジックを乗せるには、それを回避する手順が煩雑になってしまい、Rubotyのロードの仕組みを調べることで本来やりたかったことに使うべき時間を奪われてしまうのが悩ましいところでした。

また、RubotyはHandlerとActionというクラスをきちんと定義しており、その流れに乗っていれば特に迷うことは無いのですが、本当にどうしようもないほどにくだらない機能(例えば、渡された言葉をシャッフルしてechoするだけのしょうもない単機能のbot)しか持っていないbotを作るには、あまりにもリッチ過ぎる制約を持っていました。また、プラグインのロードの都合上、Handlerは自分たちのロジックとは違うモジュール名空間に置かなくてはいけないことも気になる点でした。

この煩雑さを回避したい、毎秒クソボットを作りたいと思うようになると、まるでRailsのようなRubotyの壮大な世界観が少し扱いづらくなってきました。そこで、この悩みを解決するためにSinatraのような手軽さで書けるRuby製Botフレームワークが必要だと感じたために、Mobb プロジェクトを開始しました。

また、世の中には数多のBotフレームワークがありますが、その殆どが自らのコードに特化したサービス接続用のプラグインを持っており、他の世界観で使い回すことができないことも大きな問題だと思いました。これはちょうど、Web ApplicationとWeb Serverの接続部分を解決したRackのようなものがBotの世界にも必要だということであり、Rack(のような)インターフェイスにしたがってBotエンジンを記述すれば、サービスとの接続部分を使いまわせると考えたことが、ReppというBot用の共通のサービス接続インターフェイスを作ろうと思った動機です。

Sinatra DSL

Sinatraのコア実装は、おおよそ2000行のPure Rubyで書かれています。たった2000行でWeb Application Framework が作れることにとても感心します。所々で非常にトリッキーなテクニックを使っていて、きちんと全体を把握するには数日かかると思いますが、それでもその気があれば数日で全体を理解できる素晴らしいコードだと思います。

実際のところ、Sinatraはその2000行のコードでWeb Application Framework として成立しているわけではなく、強力なルートのパターンマッチング実装であるmustermannが別に存在します。また、起動に関してもRack側にかなり助けられている箇所もあります。ですが、SinatraのDSLそのものは2000行の実装の中にすべてが現れていて、かつそれなりに読めるコードなので、今回はその周辺には触れません。

Sinatra DSLで最も理解が難しいと思ったのは、settingsの正体を知るところと、DSLでロジックを記述した時にSinatra側の変数を参照するためにバインドをするためのラッパーが存在しているところだと思います。Sinataraコードリーディング日記は、また改めてブログのエントリで詳細に書ければいいかなと思います。

Mobb

Mobb は、Sinatra DSLの影響を100%受けて作られた、Bot Frameworkです。

最も簡単なサンプルコードは、エントリの先頭に書かれたコードで、これは実際に機能します。その他のサンプルに関しては、リポジトリのexamplesディレクトリに書かれています。

正規表現のパターンマッチングが拾えたり、自身の設定を参照したりもできます。

インストールは、gem経由で行いますが、Bundler推奨です。次のようなGemfileを用意します。

インストールします。

app.rbなどの適当なファイルに、次の内容を記述します。

実行します。

app.rbで、サービス名にslackを指定しているため、環境変数SLACK_TOKENにBot用のトークン文字列を渡しておいてください。シェルで試しに動かすだけであれば、 set :service の行を削ると自動的にシェルで起動します。

現状では、渡された文字列か正規表現にマッチするとブロックを実行する on と、そのエイリアスの receive しか機能しませんが、徐々に機能追加して、 cron 的な役割を果たす every や、特定の日時に発火する at などを実装していく予定です。

Repp

Repp は、Rackの影響を100%受けて作られた、Botフレームワークとサービスのインターフェイスです。

とりあえず、今はv0.1.0ということで、ドキュメント化された仕様もないし、そもそも頭の中で固まっている仕様もありません。

というのも、Bot Framework の場合は、 Web Application 用のRackと違い、リクエストとかならず対になるレスポンスがあることが保証されていなかったり、リクエストが無いのにレスポンスをしなくてはいけなかったりするため、そう簡単にRackの仕様を真似ることはできないのです。

また、これは最も大きな問題なのですが、チャットサービスによって使える機能がかなり違うため、ここのサービスは意識しつつ全体として破綻しないインターフェイスが必要です。具体的に言うと、Slackのアタッチメント機能なんかは他のサービスからすると参照できない情報だけど、似たように変換はできるかもしれないような情報を、どうやってサービス間で共有するのか? という話です。

これに関しては、もうバッサリと互換を切り捨てて、Botを書く人が何のサービスに向けて書いているのかを明確にするしか無いのではと思っていますが、それもまあどうなんだという気持ちもあり、たくさん考えています。

とりあえず、ReppはRackのようなインターフェイスと言いつつ、まったくインターフェイスとして決まった仕様が存在せず、ドラフトで今の動きをやっている状態です。

そのうち整備していきます。

RejectKaigi2018の話

Mobb のプロジェクト自体は、すこし前からその構想を持っていたのですが、Sinatraをコードリーディングをして似た実装のDSLを作り上げるのにかなり時間がかかってしまい、最初にアイディアを発表したのはRejectKaigi2018の発表でした。

この発表の時には、大体の現在公開されているコードの基礎部分が完成しており、Slack用のRepp実装がなかったために公開を見送っていた状態です。RubyKaigi2018までには初版を作り上げて公開したいとセッション内で話しましたが、残念ながら私生活の状況によってそれは叶わず、6月末の公開になりました。

RubyKaigi2018の話

今年のRubyKaigiは仙台でした。初日にという牛タンやさんに行きましたが、最高に美味しかったです。

Ruby25thでなんとなくテンションがあがり、このMobbの話をしたくてCFPを送りましたが、残念ながら落ちてしまったことの反省を前回のエントリで書きました。

Mobbのファーストリリースの話

Mobb のファーストリリースのためのコード整備やリリース作業は、所属しているドワンゴという会社が年一で行っているハッカソンの時間にやりました。一応業務の時間の中で好き勝手やってリリースしたので、宣伝の意味も兼ねて会社の採用情報の未承諾広告を貼っておきます。
【未承認広告ここから】

ドワンゴ採用情報サイト

「あなたの本気が、あなたを変えて、未来を少し変えていく。」ドワンゴの採用情報をおしらせします。

紹介制度とかもあるので、直接Twitterで呼びかけてもらっても大丈夫です。

【未承認広告ここまで】

MobbとReppの今後

まず、両方共テストがありません。テストを書いていこうと思います。

他にも、Reppはきちんとしたドキュメントが必要です。それに、まだこれだっていう仕様もしっかり決まっていません。ドラフトです。底を固めるために色々考えるのが、当面の課題になりそうです。

一応、頑張ってMobbとReppをメンテナンスしていく気持ちはあるので、便利だと思ったらぜひ使ってみてフィードバックをおねがいします。


カテゴリー: Ruby, ポエム | コメント / トラックバック: 0個

RubyKaigi2018のCFP落ちたので後学のために置いておきます


タイトルの通り、RubyKaigi2018のCFPに送ったプロポーザルがリジェクトされたので、内容を公開して後学のためになればと思い、自らの反省点込で残しておきます。

送った内容

Title

Reading Sinatra to build Sinatra-ish application

Abstract

Reading Sinatra code is the best way to learn how to write minimal daemon program by Ruby.
Sinatra was made only of about 2000 lines Ruby code. However, the codes consist of skilled Ruby codes, fundamental metaprogramming techniques, and server programming techniques.
In this session, first I will introduce how I made a Sinatra-ish Bot daemon program. After that, I will explain the whole Sinatra code in 40 minutes.

Details

Rubyのライブラリを作成/OSSに貢献するにあたって、Rubyの初心者が次の一歩を踏み出すためのトークをしたいと思います。そのためにまず、有名なライブラリのコードを読むことで、最初の一歩の手助けをしたいと思います。
このトークでは、まず最初に私が作成しているSinatraライクなDSLでBotを作るライブラリを紹介します。
その後、そのライブラリを作成するためにSinatraをどう参考にしたかを解説するため、Sinatraの約2000行のすべてのコードリーディングを40分のセッションの中で目指します。
このトークによって、聴講者はSinatraのコードを読んだという実績と、Ruby製のライブラリへの構造への理解を得て、Rubyによる開発によりいっそう親しめるようになることを期待しています。

Pitch

多くの人がRubyのコードを書くことに親しみ、OSSへの貢献を目指します。
また、Sinatraのコードをまるまる読んだことがある人は意外と少ないと認識しているので、中級者にも新鮮な体験になるのではないかと思っています。

反省点

いくつか思いつくので、箇条書きにします

自分が作ったもののアピールポイントが少ない

Ruby25thイベントで、「RubyKaigiは自分が作ったものを自慢できるような場であればいい」の旨の発言を、どなたがなさっていたのかは失念しましたがされていたように記憶しています。

その中において、私がプロポーザルに書いたメインは「Sinatraのコードリーディング」の部分で、私が書いたBotのフレームワークに関する話はサラッと流れています。この次の反省点とも関連するのですが、Sinatraをメインに置くのではなく、自分のプロダクトをメインに(例えば、私のコードがどのようにSinatraから影響を受けたのかなど)して送るべきでした。

特定のアプリケーションの話をしようとしている

私がRubyKaigiに参加したのは2016年からですが、2回参加しただけでも、RubyKaigiにおける「特定のアプリに関する話はせず、Rubyの話をするべき」という空気は強く感じ取っていました。

その中に置いて、Sinatraという単一のプロダクトに関するコードリーディングをしたいというプロポーザルは、かなり無理筋だったような気がします。

まだ作っている途中のプロダクトに頼ってプロポーザルを送ってしまった

プロポーザルの中に書かれたBotフレームワークは、まだ開発中です。アイディアレベルから、一応動くかな? くらいの実装までしかまだ完成していないため、コードはGithubのパブリックリポジトリにはプッシュしていません。

そのため、まだどこにもコードが無いプロダクトに対して、レビュアーも評価の下しようがなかったように思います。自分が作ったプロダクトの話をしたいなら、せめてそのプロダクトのコードなりLPなりDocなりをもってこいと言われて然るべきだと思います。

実際、プロポーザルを送ってからも開発を続けていたところ、「これはプロポーザルに書くべきだったな」と思うようなコードやアピールポイントがどんどん見つかっています。まだアルファ版も完成していないプロダクトでのプロポーザルは、無謀でした。

DetailとPitchが日本語

これはそんなに気にしていなかったのですが、過去にCFPを送って通った方のブログを見ると、RubyKaigiは国際カンファレンスなので、すべて英語で送ることが望ましいという旨のことを記している方が多いように思われます。なので、これは手抜きせずにきちんと英語で送るべきだったと思いました。

今後

反省点をだいたいまとめると、「まだ完成も公開もされていないプロダクトの話を、特定のアプリケーションのコードリーディングでごまかすようなプロポーザルを送ったのが失敗」だったと思います。そのため、来年はきちんと完成したものを元にしてプロポーザルを送るべきでしょう。

めげずに来年もCFPに出せるようにがんばるので、がんばります


カテゴリー: 未分類 | コメント / トラックバック: 0個

Gitをバックエンドにしたタスク管理bot


この記事は、ドワンゴ Advent Calendar 2017 の4日目の記事です。

TL; DR

すごい簡単なゆるいタスク管理のバックエンドに、内容アドレスファイルシステムとしてのGit使うのもまあいいんじゃないの? とおもってGemを作った。

ゆるいタスク管理システムが必要だった

通常、仕事のタスク管理はJIRAとかRedmineとかGithubとかTorelloとかなんかそういう専用のやつを使うと思います。とはいえ、「もう今日は帰ってるけど、明日こののプルリク見てください」とかSlackで伝えたり、「このプルリクレビュー通ってるんで、明日マージしといて」とかSlackで伝えたり、その程度のことをチケットにするのも妙な感じです。デイリーミーティングや口頭やSlackで伝えれば良い気もしますが、まあそういうのって大抵忘れます。そもそも言っても忘れ去られるし。Slackのリマインダも使いづらい。そういう、なんか忘れるけど伝えときたいことを忘れないようにしたいなと思ったら、とりあえずbotを作ります。

botを作ることの理由を問われても、まあなんとなくとしか言いようが無いですが、Slack上のbotだったらまあ大体みんな見てるだろうという程度の理由です。

簡単なタスク管理システムってなんだろう

頭にぽわっと思いついた程度の要件です

  1. FIFO型のキュー
  2. キューの操作履歴の参照
  3. Slack上からキューを操作可能

普通の配列操作とSlack botじゃんと思いましたが、肝心なのはキューの操作履歴の参照です。

キューの操作履歴、例えば新しいタスクをキューに追加したとか、先頭のタスクを完了したとか、n番目とm番目のタスクを入れ替えたとか、そういう類の操作履歴を残そうと思うと、これはなかなか厄介な気がしました。なにせ、方法がパッと思いつくだけで5個くらいあり、そのどれもが概ね「DBかファイルに操作履歴を残す」という方法です。ただ、DBを使うのは大掛かりで嫌だし、ファイルに書き出すのはロックの問題や破損の問題に立ち向かうのが億劫です。

もう少し何か手軽でいい感じの無いかと考えたところ、GitのようなVCSをキューのバックエンドに使えば、操作の履歴を完全に残して参照も手軽なタスクシステムが作れるのではないかと思いました。

libgit2を使おう

キューのバックエンドにGitを使うアイディアを出したは良いものの、普通にSlack botからGitの操作をすると、次のような点で困ります。

  1. Botを動かす場所にGitコマンドが必要
  2. GitはChatBotのような並列操作で同時に扱うとワークスペースを壊す
  3. 遅い

これらの問題に立ち向かう方法は、libgit2を使うことです。

https://libgit2.github.com/

libgit2は、Cで書かれた組み込み用のGitライブラリで、たくさんの言語へのバインディングとともに配布され、Gitコマンドに依存せずにGitの操作が可能です。また、通常のGitコマンドと違い、Gitの内部コマンドを直接呼び出すため、動作も高速です。それだけでなく、通常開発者が使うGitのコマンドの裏に隠されたアトミックな操作を直接行うため、完全とは言いませんが、ある程度の並列実行にも耐えられる安全性を持っています(ワークスペースが壊れて、Git操作を受け付けなくなるとかが起こらなくなる)。

libgit2を使うことで、Botを動かす場所にGitコマンドが必要なく、並列操作にある程度耐え、本来のGitよりも早く処理を実行することが出来ます。

しかしその一方で、大きな欠点もあります。それは、libgit2で扱えるGitの世界は、我々が普段使っているGitコマンドとは大きく違う点です。libgit2を使ってGitリポジトリを操作するには、非常に複雑で手間のかかる手順と、Gitの裏側の世界への理解が必要です。

本当のGitの世界

最初に書いておきますが、この段落の内容に関してより詳しく理解したい場合は、Pro Gitの10章を読んで下さい。

Gitの世界には、通常開発者が利用する add や checkout や push などの磁器(Porcelain)と呼ばれるコマンドの裏で、隠された配管(Plumbing)と呼ばれるコマンドが複雑に呼び出され、操作されています。

簡単に、Gitには表側の世界と裏側の世界があると考えてください。表側の世界は、我々開発者が普段見ている、VCSとしてのGitです。ファイルに変更を加え、ステージし、コミットして変更履歴を記録する。それが、裏側の世界のGitです。一方で、裏側の世界のGitは、内容アドレスファイルシステムです。内容アドレスファイルシステムは、少なくともGitの世界観ではほぼほぼKVSシステムとほぼ同じようなものと理解して問題ないです。ざっくり言うとGitはKVSです

表側の世界を操作するのが、先にも出てきた普段みなさんが慣れ親しんでいるGitコマンドです。そして、裏側の世界を操作するのが、今回つかったlibgit2です。もちろん我々のよく知るGitコマンドでも、裏の世界を操作することは出来ますし、そのためのコマンドが(普段は使わないけど)用意されています。しかし、libgit2は完全に裏の世界のために存在するライブラリで、表の世界のような使用方法は出来ません。

内容アドレスファイルシステムとしてのGit

例えば、Gitのワークスペースにあるファイル「something.txt」を追加して、内容を編集しコミットすることを考えてください。通常の我々の操作では、次のようなことを行います。

ファイルを編集し、git add コマンドでファイルをステージし、git commit コマンドで新しいコミットを作成します。それでは、その時にGitの裏側の世界では何が起こっているかを見てみましょう。

git add

あるファイルに対して add を行った時、Gitの裏側の世界では次のようなことが起こります。

  1. add されたファイルの中身と、ファイルのメタ情報のSHA1ハッシュ値を計算する
  2. ファイルのメタ情報と中身をNULL文字で連結し、zlibでその情報を圧縮する
  3. 圧縮した内容を、1で計算したSHA1の値のファイル名に書き出す
    1. 正確には、SHA1値の先頭2文字のディレクトリ下に、末尾38文字のファイルを作り、そこに書き出す
    2. ex. ハッシュ値が d670460b4b4aece5915caf5c68d12f560a9fe3e4 であれば、.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 というファイルに内容を書き出す
  4. ワークスペースのファイルとディレクトリの構成をもとに、ツリーオブジェクトを更新する

ここで重要なのは、Gitではあるファイルのある時点の状態に、そのファイルのSHA1値でアクセスできるKVSだということです

Gitの裏側の世界では、ありとあらゆるファイルとディレクトリは、オブジェクトとして扱われます。上の例では、addされたファイルはblobと呼ばれるオブジェクトです。オブジェクトには、その内容と属性で一意のSHA1値が振られていて、Gitの裏側の世界ではこのSHA1値を使うことによってファイルにアクセスすることが可能です。

blobオブジェクトは、あるファイルのある時点での完全な内容をzlibで圧縮したもので、Gitはその内容にSHA1の値のキーでアクセスできる状態です。これが、Gitが内容アドレスファイルシステムと呼ばれる理由です。ファイルのSHA1値というキーを知っていれば、そのファイルの内容にアクセスすることができます。それでは、今現在のワークスペースの内容を、SHA1値のキーで表現するには、どうすればいいでしょうか? その仕組が、ツリーオブジェクトです。

ツリーオブジェクトとは、Linuxのディレクトリ構成に似た情報が書かれたファイルです。参照ツリーの内容の例として、次のようなものが挙げられます。

各行の最初のブロックがファイルのアクセス権限、次のブロックがオブジェクトのタイプ、その次のブロックがアクセスすべきオブジェクトのSHA1値で、最後のブロックがファイル名です。これは、Linuxのシェルで ls -la コマンドを打った時の情報によく似ています。blobをファイル、treeをディレクトリと置き換えれば、ほぼおなじ情報が得られます。これらの各行がファイルの情報と参照先を表しています。

参照ツリーは入れ子構造にすることが出来ます。つまり、ルートの参照ツリーの下に、サブディレクトリとしての参照ツリーを入れることも可能です。これによって、ファイルのディレクトリ構成を表現しています。

このように、Gitの裏側の世界では、すべてのファイルとディレクトリにはSHA1値が割り振られ、その値を入れることでファイルとディレクトリの内容を参照することができます。

git commit

add コマンドでステージングしたファイルを変更履歴としてコミットするには、 git commit コマンドを使います。コミットに関しては、Gitの表の世界のコマンドと大きな違いはありません。Gitは次の情報を集め、コミットオブジェクトを作ります。

  • Author
  • Committer
  • Message
  • ルートのツリーオブジェクト
  • Parent(一番最初のコミットのときは、この値は含まれない)

AuthorやCommitterやMessageは、通常の git commit コマンドでもよく見るので、何かわかると思います。Parentは新しくつるコミットの前のコミットで、git log コマンドで見ることができるコミットの並びで親に該当するものです。

目新しい情報は、ルートのツリーオブジェクトです。これは、git add コマンドの裏側で作られた、ツリーオブジェクトです。ステージングで作られたツリーオブジェクトは、現在のステージングの内容のすべての内容にアクセスできるSHA1値が記録されていて、Linuxのディレクトリのようにたどることが出来ます。ステージングの際に作成されたツリーオブジェクトの情報をコミットに入れることで、ステージングの内容を、コミットとして確定させることができるのです。

Gitのコミットとは、ある地点のツリーオブジェクトのSHA1値に対して親のコミットオブジェクトのSHA1値とコミッターの情報を与えたものです。そしてコミットオブジェクトも当然オブジェクトであり、SHA1値でアクセスが可能です。Gitの表の世界でも、コミットのSHA1値はよく目にすると思います。あの値は、Gitの内容アドレスファイルシステムの世界では、コミットオブジェクトを参照するためのキーであり、ファイルやツリーのオブジェクトを参照することとほぼ同じなのです。

改めて、libgit2を使おう

Gitの裏側の世界を理解すれば、libgit2を使ってgitリポジトリの操作が可能です。それでは、libgit2のRubyバインディングであるRuggedを使って、実際にリポジトリを操作してみましょう。

Ruggedを使ってgitリポジトリするには、次のようにします。

これで、リポジトリオブジェクトが作成できます。先程まで説明していたGitの内容アドレスファイルシステムの挙動を実際に見てみましょう。

writeメソッドでリポジトリにblobオブジェクトを書き込み、exists?メソッドでGitのオブジェクトの中に作成したオブジェクトが存在するか確認しています。そしてその後、readメソッドでオブジェクトを読み込み、そのオブジェクトのdataメソッドでblobの中身を読み取りました。

このように、Gitリポジトリをlibgit2で操作すると、まるでKVSのようにblobファイルを保存できるデータベースとして扱えます。同じように、コミットも見てみましょう。

先ほど作成したblobをステージング(index)の参照ツリーに追加し、その状態をtreeオブジェクトとしてGitのに記録します。そして、その時に発行されたSHA1を利用して、コミットオブジェクトを作成しました。当然、コミットもオブジェクトで、SHA1のオブジェクトIDが発行されます。

この後も同じように、新しいblobオブジェクトを作成し、ステージングしてtreeオブジェクトを作成し、そのtreeと先に作成したコミットのオブジェクトIDを元に次のcommitオブジェクトを作成します。これが、Gitで行われているバージョン管理の最も根本的な部分です。

libgit2を使って、Gitをバックエンドにしたキューを作成する

全体のコードは長いので、GitQueueというGemを作りました。次のコードは、簡単な使い方のスニペットです。

出力は次の通り

Arrayのようなインターフェイスを持っていますが、Arrayとは違いFIFOの処理を邪魔するようなメソッドは生えていません。例外的にタスクの順序入れ替えは出来ますが、末尾挿入と先頭取り出し以外の方法でタスクを出し入れすることは出来ません。

また、今回の実装では入っていませんが、Ruggedではlibgit2のバックエンドに、OSのファイルシステム以外にもRedisなどのKVSを選択することも可能です。普通にGitのバックエンドをKVSにすると書くとよく意味がわかりませんが、Gitという内容アドレスファイルシステムのバックエンドを本物のKVSにすると考えると、すんなりわかりやすいと思います。libgit2のバックエンドを本物のKVSにすることによって、より堅牢で様々な場所からアクセスできるGitQueueの運用などの夢も膨らみそうです。

Slack bot を作る

このエントリの目的はGitをバックエンドとした履歴記憶機能付きキューでタスク管理botを作ることなので、スニペット程度ですが簡単にbotを実装してみます。

色々と処理が雑ですが、スニペットなので許してください。

このbotの機能としては

  • Adminユーザーが、タスク管理を行うユーザーを追加できる
  • 各ユーザーごとに、GitQueueを使ってタスクのキューを作成する
  • ユーザーは、タスクの追加とタスクの完了ができる
  • ユーザーは、タスクの一覧を見ることができる

です。

肝心の操作履歴をどこに使ってんだって気はしますが、概ね操作履歴なんていうのは何かしら問題が合った時の調査に使うものなので、別にbotからは見えなくていいと思います。実際にリポジトリにアクセスすれば、使い慣れたGitコマンドでキューの変更履歴を参照することができます。

まとめ

  • Gitの実態は内容アドレスファイルシステム、つまりほぼKVS
  • GitをFIFOのバックエンドに使うと、操作履歴が全て残って素敵
  • libgit2を使うことによって、少ない依存で強力なGit操作が可能
  • botを作るのは楽しい

カテゴリー: Ruby, Slack, 未分類 | コメント / トラックバック: 0個

apt-get upgrade したら、Chromeが物故割れたのでなおした


Chromeがぶっ壊れる

UbuntuのChromeをどういうふうに管理していたのか自分でも忘れていたが、今までは普通にChrome自身のアップデートで、Chromeを再起動するたびに勝手に最新になっていた。しかし、ちょっと必要なパッケージがあり、何も考えず Ubuntu 15.04 vivid 上で apt-get update && apt-get upgrade したら、Chromeが起動しなくなった。

何度起動してもクラッシュするので、シェルから起動してみたところ、次のようなエラーが出て起動しないことが分かった。

libnss3のバージョンの問題だったので、アップデートをすれば治るかと思ったが、 15.04 ではChromeが必要とするバージョンを入れることができないことが分かった。

https://launchpad.net/ubuntu/vivid/i386/libnss3/

そのため、わけあって 15.04 を使い続けていたが、最新のLTSである 16.04 xenial に入れ替えることにした。

Ubuntuのメジャーバージョンアップは今までやったことがなかったが、  do-release-upgrade というコマンドを使えば、特に難しいこと無くできるらしい。ただし、それは当然ながらサポート期間内のバージョンの話に限る。今回困った15.04は、はるか昔にサポート期限切れになっており、一筋縄では行かなかったので、ブログに書いて忘れないようにしておく。

do-release-grade が動かない理由と対策

15.04 で do-release-upgrade を実行すると、次のようなエラーが出る。

15.04 (vivid) から、 16.04 (xenial) へのアップデートはできないと言われる。んなアホな。

とても困ったが、ググった結果ここのページに答えのほぼ全てと対策法が書いてあった。

Ubuntu 最新バージョンへのアップグレード

要するに、 vivid と xenial の間には、 wily というもうひとつのバージョンが存在するが、既に wily 自体が Out of date のため、 vivid から wily のアップデートが不可であり、 vivid から xenial への一つ飛ばしのアップデートもできない、というのが理由だ。対策としては、 changelogs.ubuntu.com/meta-release のレスポンスを何らかの方法で乗っ取り(Charlesを使っても良いし、  ダミーサーバーを立てたうえで /etc/update-manager/meta-release の中身を書き換えてもいい)、wilyが有効バージョンであると認識させる。詳しい方法は、リンク先に書いてあるのでそちらを参照。

概ね、この方法ですべてが解決するのだが、最大の問題は wily は既に archive.ubuntu.com からも消えており、上記のサイトの方法ではアップデートができないことだ。

なので、 meta-release の内容を乗っ取るだけでは do-release-upgrade で解決ができない。そのため、 wily のイメージをまだ置いているミラーをミラー一覧から探したうえで、 meta-release の参照先をそっちに向けて、更に /etc/apt/source.list の向け先もそっちにすることで、 do-release-upgrade を利用することができるようになる。

Official Archive Mirrors for Ubuntu

ほとんどのミラーは、最新の archive.ubuntu.com と同期しているため、 wily のイメージが存在しない。だが、 dely しているミラーを参照することで、 wily のイメージが残ったままのものを見つけることができる。

いろいろと見て回った結果、 Psychz Network のミラーに wily の完全なイメージが残っているのを見つけたので、 meta-release と source.list を次のように書き換えた。

secure関連のところはよくわからんかったので何も書き換えていないが、この状態で vivid から sudo do-release-upgrade を実行して wily に更新し、再起動後にもう一度 sudo do-release-upgrade することで xenial に更新することができた。

Chromeの復活

xenial への更新後、  sudo apt-get install –reinstall libnss3 を実行することで、Chromeは復活した。

正直、Chromeが起動しないくらいだったらいっぺんOS消して再インストールしても良かったんだけど、Chromeの中に入ってるクッキーを様々な理由で取り出す必要があったので、こんだけ必死になって修復した。

そもそもサポート切れのバージョンのOSつかうなって話ではあるけど、まあ何か参考になればと思って書き残しました。


カテゴリー: Linux | コメント: 2 件