作業環境管理ツールの導入

NixOSでArtemisの作業用ディレクトリを管理する環境を準備します。

実験でArtemisを使ったことがある方はartlogin hogeみたいなコマンドで作業ディレクトリに入ってオンライン解析を行っていたと思います。 このあたりの設定は.bashrcや.artlogin.shなどに書かれていています。

オンライン解析用サーバーの運用では実験ごとにユーザーを作っているので1ユーザーに対して、1つの作業用ディレクトリが前提になっています。 一方で、実験後には自身の研究室のサーバーに環境を再構築することが多いかと思われます。 研究室のサーバーでは自身のアカウントで環境を構築することになると思われますので、複数回実験に参加すると、1つユーザーに対して複数の作業用環境を構築したくなるはずです。

ここではNixOS用のツールartemis-login-nixの導入方法の一例を紹介します。 このツールの詳細はリンク先のREADMEに詳細が書かれていますので、ここでは割愛します。

情報

こういう悩みは、みなさん直面するみたいで、それぞれ環境管理ツールを作っていたりするのではないかと思っています。

以下は私が知っているツール

  • artenv: 京大矢野さん製。apptainer対応しているのが神

Home manager経由で導入する

今回はartemis-workdir-trainingの作業環境を実際に作ります。 手順は以下の通り

  1. Home managerの設定ファイル編集してartloginコマンドを使えるようにする
  2. 作業環境設定ファイルと実験依存の実行スクリプトをつくる
Home managerの設定の変更

Home managerの設定ファイルは~/.config/home-managerにあります。

まずはflake.nixが使用する外部ソースをinputで指定します。 今回は、上述したリポジトリを使うので、その情報を以下のように追加します。

  inputs = {
    artemis-login-nix = {
      url = "github:FumiHubCNS/artemis-login-nix";
      flake = false;
    };
  };

flake = falseを入れることでこのリポジトリをnix flake として扱わないようにします。 そうすることで${artemis-login-nix}/bin/artlogin-nixのように、nix store 内のファイルパスとして使うことができます。

このinputを受けて、outputで使用すツールを指定する必要があるので、以下のように追加します。

  outputs =
    { nixpkgs, home-manager, artemis-login-nix, ... }:

次にmodule側でこのoutputで使えるように以下を追記します。

        extraSpecialArgs = {
          inherit artemis-login-nix;
        };

artloginコマンド用の設定は分離しておきたいので、home.nixに以下を追加します。

  programs.bash.enable = true;
  imports = [
    ./artemis-login
  ];

そして~/.config/home-manager/artemis-login/default.nixに以下を書きます。

{
  pkgs,
  artemis-login-nix,
  ...
}:

{
  home.packages = [
    (pkgs.writeShellScriptBin "artlogin" ''
      exec ${pkgs.bash}/bin/bash \
        ${artemis-login-nix}/bin/artlogin-nix "$@"
    '')
  ];
}

あとは以下のコマンドで更新を反映すれば使えるartloginは使えるようになります。

nix flake update artemis-login-nix
nh home switch .
実験依存のファイルを作成する。

オンライン解析用サーバーとは違い複数の解析ディレクトリのレポジトリを扱う必要があります。 そのため解析コードのソースやディレクトリの環境をtutorial.confのような設定ファイルとして用意し 、artlogin tutorial [user name]として指定することで管理します。 同様に、いくつかの実験ではartloginする時に独自のシンボリックリンクを貼ったり、独自クラスのビルドをしたりします。 そこで必要に応じてtutorial.shのように独自スクリプトを追加できるようになっています。 置き場所は~/.config/artemis-login/です。

まずはtutorial.confですが以下のようにします。

# Artemis woring directoriy path
ART_ANALYSIS_DIR="$HOME/tutorial"

# Empty = do not create rawdata symlink
ART_DATA_DIR=""

# Empty = do not create output symlink
ART_OUTPUT_DIR=""

# Repository infomation
REPO_TYPE="github"
REPO_PROTOCOL="https"

REPO_OWNER="FumiHubCNS"
REPO_NAME="artemis-workdir-training"
REPO_BRANCH=""

このように書くことでホームディレクトリ直下にtutorialというディレクトリを作って、そこに環境を構築することができます。

次に独自の実行スクリプトです。 今後のことも考えて、再利用しそうな部分を別スクリプトに分ます。 今回は~/.config/artemis-login/common/にhistory.shとwebdisplay.shを作ってみたいと思います。

まずhistory.shには以下を書きます。

export HISTFILE=${ARTEMIS_WORKDIR}/.bash_history_${USERNAME}
export HISTSIZE=100000
export HISTFILESIZE=200000

このようにすることでコマンドログをユーザーごとに切って、どこまで残すかを指定します。

次にwebdisplay.shには以下を書きます。

#!/usr/bin/env bash

# Assign the smallest unused non-negative integer to
# $ARTEMIS_WORKDIR/.webdisplay.
#
# Existing IDs are read from:
#
#   $ARTEMIS_WORKDIR/../*/.webdisplay
#
# The current workdir itself is excluded.
webdisplay_assign()
{
    if [ -z "${ARTEMIS_WORKDIR:-}" ]; then
        echo "[webdisplay] ERROR: ARTEMIS_WORKDIR is not set." >&2
        return 1
    fi

    if [ ! -d "$ARTEMIS_WORKDIR" ]; then
        echo "[webdisplay] ERROR: ARTEMIS_WORKDIR does not exist:" >&2
        echo "  $ARTEMIS_WORKDIR" >&2
        return 1
    fi

    local parent
    local own_file
    local lock_file
    local lock_fd
    local file
    local value
    local id

    parent="$(dirname "$ARTEMIS_WORKDIR")"
    own_file="$ARTEMIS_WORKDIR/.webdisplay"
    lock_file="$parent/.webdisplay.lock"

    # Prevent two users from receiving the same ID simultaneously.
    exec {lock_fd}>"$lock_file" || {
        echo "[webdisplay] ERROR: cannot open lock file:" >&2
        echo "  $lock_file" >&2
        return 1
    }

    flock "$lock_fd" || {
        echo "[webdisplay] ERROR: failed to acquire lock." >&2
        exec {lock_fd}>&-
        return 1
    }

    declare -A used=()

    shopt -s nullglob

    for file in "$parent"/*/.webdisplay; do
        # Do not count this workdir itself.
        if [ "$file" = "$own_file" ]; then
            continue
        fi

        value="$(<"$file")"

        if [[ "$value" =~ ^[0-9]+$ ]]; then
            used["$value"]=1
        else
            echo "[webdisplay] WARNING: invalid .webdisplay file:" >&2
            echo "  $file" >&2
            echo "  value: '$value'" >&2
        fi
    done

    shopt -u nullglob

    id=0

    while [[ -n "${used[$id]+x}" ]]; do
        ((id++))
    done

    printf '%d\n' "$id" > "$own_file"

    flock -u "$lock_fd"
    exec {lock_fd}>&-

    echo "[webdisplay] assigned ID: $id"

    return 0
}


# Check whether this workdir's .webdisplay ID conflicts with
# another sibling workdir.
#
# This function does not modify anything.
webdisplay_check_collision()
{
    if [ -z "${ARTEMIS_WORKDIR:-}" ]; then
        echo "[webdisplay] WARNING: ARTEMIS_WORKDIR is not set." >&2
        return 1
    fi

    local parent
    local own_file
    local own_id
    local file
    local value
    local conflict=0

    parent="$(dirname "$ARTEMIS_WORKDIR")"
    own_file="$ARTEMIS_WORKDIR/.webdisplay"

    if [ ! -f "$own_file" ]; then
        echo "[webdisplay] WARNING: .webdisplay does not exist:" >&2
        echo "  $own_file" >&2
        return 1
    fi

    own_id="$(<"$own_file")"

    if [[ ! "$own_id" =~ ^[0-9]+$ ]]; then
        echo "[webdisplay] WARNING: invalid ID in:" >&2
        echo "  $own_file" >&2
        echo "  value: '$own_id'" >&2
        return 1
    fi

    shopt -s nullglob

    for file in "$parent"/*/.webdisplay; do
        if [ "$file" = "$own_file" ]; then
            continue
        fi

        value="$(<"$file")"

        if [[ "$value" =~ ^[0-9]+$ ]] && [ "$value" = "$own_id" ]; then
            echo "" >&2
            echo "============================================" >&2
            echo " WARNING: WebDisplay ID collision detected!" >&2
            echo "============================================" >&2
            echo " ID: $own_id" >&2
            echo "" >&2
            echo " current:" >&2
            echo "   $own_file" >&2
            echo "" >&2
            echo " conflict:" >&2
            echo "   $file" >&2
            echo "" >&2

            conflict=1
        fi
    done

    shopt -u nullglob

    if [ "$conflict" -ne 0 ]; then
        return 1
    fi

    return 0
}

このようにすることで、ユーザーごとの解析ディレクトリ直下に.webdisplayを生成し、ファイル内には重複しない番号を振り分けます。 後々この隠しファイルを使ってユーザーごとにTHttpServerをたてることでポート番号の干渉を防ぎます。

そして最後にtutorial.shに以下を追加します。

#!/usr/bin/env bash

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common/webdisplay.sh"
source "$SCRIPT_DIR/common/history.sh"

if [ "${ARTLOGIN_CREATED:-0}" = "1" ]; then
    # Assign WebDisplay ID only when this workdir is first created.
    webdisplay_assign || {
        echo "[tutorial] ERROR: failed to assign WebDisplay ID." >&2
        return 1
    }

    # Build user defined codes.
    nix develop -c bash -lc 'sh scripts/build.sh'
fi

# Check for duplicated WebDisplay IDs on every login.
webdisplay_check_collision || true

あとはartlogin tutorial usernameでうまくいくことを試してみてください。

メモ

nix develop -c bash -lc 'sh scripts/build.sh'とあるように最初にディレクトリを作る時だけ、独自クラスのビルドコマンドが走ります。 大量の警告が出るかもしてませんが、エラーが出ていなければビルド自体は成功しています。(どこかで警告を消そうとは思っています。)

注意

arteis-workdir-training のartemislogon.C内にはgSystem->Load("libtutorial");があります。 そのため、このリポジトリを独自でクローンして使うためには必ず一度自身でビルドする必要があります。