enrootのソースコードの解析 -create-

この記事ではコンテナ実行環境の一つである enroot のソースコード、特に今回は enroot create コマンドの動作を解析する。enroot create コマンドは最終的に squashfs 形式で保存されているファイルシステムイメージを指定されたディレクトリに展開する作業を行おう。そこに至るまでの動作を解説する。

enrootとは

enrootとはNVIDIAが中心になって開発しているオープンソースのコンテナ実行環境である。chroot(1)と異なり、root権限は要求されないし、proot(1)fakeroot(1)がサポートしていないコンテナイメージからの展開をサポートしている。

https://github.com/NVIDIA/enroot

ほとんどの人はslurmでpyxisプラグインを使てコンテナ内のアプリケーションを実行するために使っていると思う。

enrootのソースコードはほとんどがシェルスクリプトで記述されている。シェルスクリプトのいろんなテクニックが見れるかもしれないね。

enroot実行の流れ

enrootはコマンドとして提供されており、以下のようにサブコマンドで機能を切り替える。主な使い方は以下のとおりである。

  • createコマンド:ルートファイルシステムのコンテナの作成
  • startコマンド:コンテナでコマンドを実行する
  • execコマンド:コンテナで別コマンドを実行する
  • removeコマンド:コンテナを削除する
enroot <subcommand> <arg1> <arg2> ...

ソースコードの解析

enrootのソースコードは上述したgithubレポジトリで参照することができる。以降ではv3.5.0タグの内容を解読していく。
https://github.com/NVIDIA/enroot/tree/v3.5.0

enroot本体の動作

enroot本体はシェルスクリプトであり、トップディレクトリのenroot.inがその内容である。拡張子が.inで終わっているのは「configureされる」ということを表しており、@sysconfdir@といった”@“で囲まれた文字列が指定された文字列に置換されるということを表している。

さて、enroot.inで実質的に動作を行っている部分は以下の部分である。実際のコードにはほかにもいろいろなコマンドを処理するが、冗長であるため省略している。

if [ $# -lt 1 ]; then
    enroot::usage help 1
fi
command="$1"; shift

case "${command}" in
# 省略
create)
    enroot::create "$@" ;;
start)
    enroot::start "$@" ;;
exec)
    enroot::exec "$@" ;;
# 省略
remove)
    enroot::remove "$@" ;;
# 省略
help)
    enroot::usage help 0 ;;
*)
    enroot::usage help 1 ;;
esac

exit 0

初めに、[ $# -lt 1 ] で引数の数を確認し、一つもないようだとエラーを返す。一つ以上ある場合は初めの引数を command に代入し、shiftコマンドで引数全体をずらす。そして、コマンドの内容に従って case 文で分岐してサブコマンドの処理に入る。

(小ネタ)shiftコマンド

shiftコマンドとはスクリプトに与えられた引数(位置パラメータ)をずらす。マニュアルによると$1$2$3といった変数の内容をずらしていく。マニュアルには書かれてないが、引数の数を示す$#も変更される。

(小ネタ)bashの関数名

ところで、case文の呼び出し先がenroot::createとなっているが、:(コロン)は関数の名前に含んでもよいのだろうか?結論から言うと「含んでもいい」。関数名として許容される文字列のルールは割と緩く、環境によっては日本語も許容される

enroot::createの動作

enroot::create 関数は enroot.in にて定義されている。この関数は残りの引数を解析し、引数による設定を反映させ、実行環境を構築する。エラー処理を省いた実際のコードを以下に示す。

enroot::create() {
    local image= name=

    while [ $# -gt 0 ]; do
        case "$1" in
        -f|--force)
            export ENROOT_FORCE_OVERRIDE=y
            shift
            ;;
        -n|--name)
            [ -z "${2-}" ] && enroot::usage create 1
            name="$2"
            shift 2
            ;;
        --name=*)
            [ -z "${1#*=}" ] && enroot::usage create 1
            name="${1#*=}"
            shift
            ;;
# 省略
        *)
            break ;;
        esac
    done
# 省略
    image="$1"

    runtime::create "${image}" "${name}"
}

前半のwhite文とcase文で残りの引数の処理を行う。createコマンドが受け付ける引数は--force--nameの2つがある。

--forceオプションはオプション引数なしで指定し、ENROOT_FORCE_OVERRIDEという環境変数をyに設定する。-fという短いオプション名にも対応している。

-nameオプションはオプション引数を取る。オプション引数のとり方は2通りあり、--name env_name というように次の引数でオプション引数を指定する方法と、 --name=env_name というように=(イコール)に続けてオプション引数をを与える方法をサポートしている。一つ目の方法では -n という短いオプションにも対応している。

case文で--nameを判定するとき、オプション引数が変数にsetされているか否かを判定している。この判定を行っているのが以下の部分である。

[ -z "${2-}" ] && enroot::usage create 1

"${2-}"とは「$2がsetされていなければ空文字列に展開する」という意味である。そして [ -z WORD] とはWORDを展開した文字の長さが0であるときに0を返すコマンドである。&&は直前のコマンドの実行結果が0であれば次のコマンドを実行する接続要素である。

case文で--name=env_nameを判定するとき、マッチした文字列から--name=の部分を除去してオプション引数を取得している。この除去を行っているのが以下のコードである。

[ -z "${1#*=}" ] && enroot::usage create 1

"${1#*=}" とは「$1先頭から*=にマッチするパターンを最短一致で見つけ出し、除去する」という意味である。最短一致という指定は重要である。これは $1--name=env=12 という文字列が設定されている場合を考えるとわかりやすい。このとき、オプション引数は env=12 という文字列を想定している。先頭からの最短一致だと --name= までの部分にマッチし、その部分が除去されて env=12 という期待通りの結果が得られる。一方、最長一致だと --name=env= の部分にマッチし、その部分が除去されて 12という間違った結果が得られてしまう。

マッチングを先頭から行うか、末尾から行うか、そして、最短一致で見つけるか、最長一致で見つけるかで4通りの組み合わせが考えられる。Bashはこれら4つのパターンの除去を実装している。詳しくはマニュアルを参照されたい。

最後に、runtime::create関数を実行して環境を構築する。

(小ネタ)未定義、null変数の展開

"${2-}"の展開では$2がsetされているか否かの判定を行っている。マニュアルにはこの判定についての説明があるがちょっとわかりにくい。 ${parameter:-word} のように parameter(変数名)の後に ":" (コロン)がついているパターンはいろいろ記述があるが、${parameter-word} のようにコロンがつかないパターンの説明がない。いや、無いわけではない。実際、以下のような説明がこっそりと紛れている。

When not performing substring expansion, using the form described below (e.g., ‘:-’), Bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset. Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.

長々述べているが、Put another way の以降が本質である。つまり、コロンがつく場合は変数が set されていないか、null にセットされているときに対応し、コロンがつかない場合は変数が set されていない場合にのみ対応するということだ。

似たような展開方法に :- := :? :+ というものがある。詳しくはマニュアルを参照されたい。

runtime::createの動作

runtime::create 関数は src ディレクトリの runtime.sh に格納されている。少し長いため、エラーチェックの部分を分けて後で説明するようにしている。

runtime::create 関数は本質的には unsquashfs コマンドでファイルシステムイメージを指定されたディレクトリに展開する。このとき、ユーザーの拡張ファイル属性のみ展開するようにする。そして、最後に common::fixperms 関数で実行属性を付加する。

unsquashfs コマンドを実行するとき、以下の変数展開があることに気づくだろう。この展開は "+" がフラグになっている。これは先述した "-" とは異なり「変数が set されているとき」に既存の値の代わりに別の値を返すものである。今回の場合、TTY_OFF という変数に何かしらの値が set されていれば "-no-progress" という文字列を与えるという意味である。

${TTY_OFF+-no-progress}
runtime::create() {
    local image="$1" rootfs="$2"

    common::checkcmd unsquashfs find

    # Resolve the container image path.
    # コンテナイメージ名を示す image のチェック(後述)

    # Resolve the container rootfs path.
    # 展開先を示す rootfs のチェック(後述)

    # Extract the container rootfs from the image.
    common::log INFO "Extracting squashfs filesystem..." NL
    # XXX: https://github.com/NVIDIA/enroot/issues/90
    [ $(ulimit -n) -gt $((2**26)) ] && ulimit -n $((2**26))
    unsquashfs ${TTY_OFF+-no-progress} -processors "${ENROOT_MAX_PROCESSORS}" /
        -user-xattrs -d "${rootfs}" "${image}" >&2
    common::fixperms "${rootfs}"
}

コンテナイメージ名のチェックは以下のとおりである。

初めに、image 変数に中身があるか否かを確認する。中身が無ければエラーメッセージとともに終了する。次に、image 変数が指すファイルが実際に存在するか否かを確認する。最後に、 image 変数が指すファイルが実際に squashfs 形式であることを確認する。

    # Resolve the container image path.
    if [ -z "${image}" ]; then
        common::err "Invalid argument"
    fi
    image=$(common::realpath "${image}")
    if [ ! -f "${image}" ]; then
        common::err "No such file or directory: ${image}"
    fi
    if ! unsquashfs -s "${image}" > /dev/null 2>&1; then
        common::err "Invalid image format: ${image}"
    fi

コンテナイメージ展開先のチェックは以下のとおりである。

初めに rootfs 変数に中身があるか否かを確認する。中身が無ければコンテナイメージ名から squash という拡張子を取り除いたものを展開先の名前とする。そして、rootfs に "/"(スラッシュ)が無いことも確認する。スラッシュが含まれていればエラーメッセージを表示して終了する。

最後に、ENROOT_DATA_PATHrootfs をつなぎ合わせて展開先ディレクトリのパスを作成する。すでに展開先ディレクトリが存在し ENROOT_FORCE_OVERRIDE に何かの値が入っている場合はエラーメッセージを表示して終了する。そうでなければ当該ディレクトリを全削除する。

    # Resolve the container rootfs path.
    if [ -z "${rootfs}" ]; then
        rootfs=$(basename "${image%.sqsh}")
    fi
    if [[ "${rootfs}" == */* ]]; then
        common::err "Invalid argument: ${rootfs}"
    fi
    rootfs=$(common::realpath "${ENROOT_DATA_PATH}/${rootfs}")
    if [ -e "${rootfs}" ]; then
        if [ -z "${ENROOT_FORCE_OVERRIDE-}" ]; then
            common::err "File already exists: ${rootfs}"
        else
            common::rmall "${rootfs}"
        fi
    fi

まとめ

本記事ではコンテナ実行環境である enroot および、サブコマンドである enroot create コマンドの動作を解析した。

enroot create コマンドは unsquashfs コマンドでファイルシステムイメージを指定された場所に展開する。このコマンドを正しく実行するため、コマンドライン引数の確認を行う。

Bashには引数の処理やパターンに一致した文字列を変数から除去するなどの機能が用意されており、コマンドライン引数を正しく処理し、エラーチェックを行うことができる。これらの機能を活用して enroot の機能が実装されている。

コメント

タイトルとURLをコピーしました