この記事ではコンテナ実行環境の一つである 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_PATH
と rootfs
をつなぎ合わせて展開先ディレクトリのパスを作成する。すでに展開先ディレクトリが存在し 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
の機能が実装されている。
コメント