ククログ

株式会社クリアコード > ククログ > Apt 1.6で導入されたJSONフックを活用する方法

Apt 1.6で導入されたJSONフックを活用する方法

はじめに

Aptには1.6からJSON-RPCを利用してフック処理を後付けで動かす仕組みがあります。 ここで利用されるJSONフックはプロトコルの仕様がドキュメント化されており、最新バージョンは0.2となっています。

今回は、このJSONフックプロトコルを利用して、事前にバグレポートがあがっているパッケージを知るためのアイデアを紹介します。

JSONフックの概要を知るには

JSONフックについては、Ubuntu Weekly Recipeの第676回 Apt CLIの操作結果をJSON RPCで受け取るという記事の 「Apt CLIの操作結果をJSON RPCで受け取る」を参照するとわかりやすいです。

ざっくりいうと、JSONフックを有効にするには次の2つが必要です。

  • フックに関する設定ファイル(/etc/apt/apt.conf.d/99-json-hooks)を配置する
  • 設定ファイルに指定したスクリプトを実際に配置する

紹介した記事ではaptパッケージのテストコード由来のシェルスクリプトを紹介していました。 本記事では拡張しやすいように、Rubyスクリプトの場合の例を紹介します。

JSONフックの設定ファイルを配置する

/etc/apt/apt.conf.d/99json-hooksに次のような内容のファイルを配置します。

AptCli::Hooks::Upgrade:: "/usr/local/bin/apt-json-hook";

フックはインストール時や、検索時に走らせることもできます。 お題に沿った内容とするため、アップグレードのときのみフックを適用してみます。

JSONフックのスクリプトを配置する

JSONフックプロトコルに関する正式なドキュメントは以下にあります。

アップグレードのときには、フックスクリプトでは次のような処理が行われます。

  • helloコマンドがJSONで送られてくる
  • 得られたJSONを元にフックスクリプトが対応しているバージョンを返す(0.2でよい)
  • org.debian.apt.hooks.install.pre-promptorg.debian.apt.hooks.install.package-listorg.debian.apt.hooks.install.statisticsといったメソッドが飛んでくるので、スクリプトで状況に応じて処理をする

基本的には上記がすべてです。

apt upgradeを実行しているとき、aptクライアント側ではどのタイミングでフックが実行されているかというと次のようになっています。

  • org.debian.apt.hooks.install.pre-promptは「アップグレードパッケージを検出しています...完了」といったメッセージのあとにフックが実行されます。
  • org.debian.apt.hooks.install.package-listは「以下のパッケージはアップグレードされます」といったメッセージのあとにフックが実行されます。
  • org.debian.apt.hooks.install.statisticsは「アップグレード: 2 個、新規インストール: 0 個、削除: 0 個、保留: 1 個」といったメッセージのあとにフックが実行されます。

JSONフックの仕様を踏まえると、package-listに対応するフックスクリプトの雛形は次のようになります。

#!/usr/bin/env ruby

require "socket"
require "json"

class AptJsonHook
  def run
    IO.open(ENV['APT_HOOK_SOCKET'].to_i, "a+") do |io|
      loop do
        request = io.gets
        io.gets # 後続行を読み捨てる
        json = JSON.parse(request)
        method = json["method"]
        if method.end_with?(".bye")
          exit(true)
        end
        if method.end_with?(".hello")
          # 対応しているバージョンが0.2と通知する
          io.puts({"jsonrpc" => "2.0", "result" => {"version" => "0.2"}, "id" => 0}.to_json)
          io.puts
        end
        if method.end_with?(".install.package-list")
          unless json["params"]["packages"].empty?
            # ここでパッケージの情報をかき集める
            # json["params"]["packages"]には次のような情報が含まれているので
            # 現在インストールされているバージョンと、アップグレードにより導入されるバージョンの情報が活用できる
            # [{"id"=>3790,    
            #   "name"=>"at-spi2-common",                                                                                                                 
            #   "architecture"=>"amd64",    
            #   "mode"=>"upgrade",                                                  
            #   "automatic"=>true,                                                                                                                        
            #   "versions"=>                                                                                                                              
            #    {"candidate"=>                                                                                                                           
            #      {"id"=>1455,
            #       "version"=>"2.49.91-2",
            #       "architecture"=>"all",
            #       "pin"=>500,
            #       "origins"=>[{"archive"=>"unstable", "codename"=>"sid", "origin"=>"Debian", "label"=>"Debian", "site"=>"deb.debian.org"}]},
            #     "install"=>
            #      {"id"=>1455,
            #       "version"=>"2.49.91-2",
            #       "architecture"=>"all",
            #       "pin"=>500,
            #       "origins"=>[{"archive"=>"unstable", "codename"=>"sid", "origin"=>"Debian", "label"=>"Debian", "site"=>"deb.debian.org"}]},
            #     "current"=>{"id"=>71628, "version"=>"2.49.91-1", "architecture"=>"all", "pin"=>100, "origins"=>[]}}},]
          end
        end
      end
    end
  end
end

hook = AptJsonHook.new
hook.run

ENV["APT_HOOK_SOCKET"]aptとやりとりするためのファイルディスクリプタが渡されます。 helloに対する応答はファイルディスクリタに書き込む必要がありますが、その他は特に応答を返す必要はありません。 標準出力に書き出したメッセージはそのまま端末に表示されます。

そのため、パッケージのリストを渡されたら次のようにすることで「パッケージを手動で更新しようとするときに、バグレポートがあがっているパッケージがあれば検知する」のを実現できそうです。

  • パッケージのリストが渡されたら、UltimateDebianDatabaseのミラー 1 に接続し次の条件で検索する
    • インストールすることで影響を受けるバグがあるかどうかはbugsテーブルのseverifyフィールドを参照する。(seriouscriticalでフィルタするとよい)
    • インストールしようとしているパッケージで対応するものがあるかは、bugs_packagesテーブルを参照する(packageフィールドが存在する)
    • 該当パッケージ・バージョンにひもづいているバグがあるかどうかはbugs_found_inテーブルを参照する(bugs_found_inテーブルにはversionフィールドが存在する)

UltimateDebianDatabaseはPostgreSQLで運用されているので、PG.connectしてSQLを投げた結果を表示することで、必要な情報を絞り込めます。 そういったパッケージが見つかったら表示するようにしておくと、よさそうです。

データベースへのアクセスのコストがあるのでレスポンスは遅くなりますが、次のように既存のバグをチェックしてその結果を表示する、というのを実現できます。

$ sudo apt upgrade -V
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了        
状態情報を読み取っています... 完了        
アップグレードパッケージを検出しています... 完了
以下のパッケージは保留されます:
   vagrant (2.3.4+dfsg-1 => 2.3.7-1)
以下のパッケージはアップグレードされます:
   libcaca0 (0.99.beta20-3 => 0.99.beta20-4)
   liblog-any-perl (1.715-1 => 1.717-1)
[HOOK]: No already reported bug which affects updated packages.

アップグレード: 2 個、新規インストール: 0 個、削除: 0 個、保留: 1 個。
280 kB のアーカイブを取得する必要があります。
この操作後に 1,024 B のディスク容量が解放されます。

致命的なバグがすでに報告されているなど、いまアップグレードしてしまうとまずい 2 バージョンへアップグレードしてしまうといったやらかしを 防ぎやすくなるのではないでしょうか。(もちろん既知の不具合に限ります)

おわりに

今回は、JSONフックを活用して「パッケージを手動で更新しようとするときに、バグレポートがあがっているパッケージがあれば検知する」ための アイデアについて紹介しました。

こんなふうにすごく活用しているよ、という事例があれば@kenhysまでお知らせください。

  1. UltimateDebianDatabaseとは、Debianのバグなどの情報を集約しているデータベースです。 一般向けには、そのミラーサイトが運用されています。UDD mirrorにはpsqlで接続できます。(psql "postgresql://udd-mirror:udd-mirror@udd-mirror.debian.net/udd")データベースのスキーマも公開されているので、いろいろ眺めてみるのも面白いです。

  2. たとえば、カーネルの更新に追従できていないdkmsモジュールを使っているのにも関わらず、カーネルをアップグレードをしてしまうと、該当カーネル向けモジュールのビルドに失敗し使えなくなります。該当モジュールがNICに必要なモジュールであったりすると、古いカーネルであえて起動しないと、ネットに繋げないということが発生します。