LXDコンテナのスナップショットを操作するスクリプト

これまで、LXDコンテナのスナップショットを作成するスクリプトを用意しており、復元時は基本的にWeb-UIから行っていましたが、スクリプト上で復元出来るようにしました。
まあ元々のコマンド自体短いのでコマンドを覚えておいてもよいのですが。

# スナップショット作成
lxc snapshot <コンテナ名> <スナップショット名>

# スナップショット一覧
lxc info <コンテナ名>

# リストア
lxc restore <コンテナ名> <スナップショット名>

# 削除
lxc delete <コンテナ名>/<スナップショット名>

リストアスクリプト

スクリプトの内容は下記です。ただ、そのあとのスナップショット作成するスクリプトとの統合版のほうが便利に使えると思います。

sudo mkdir -p /opt/script/lxd
cd /opt/script/lxd
sudo nano restore-lxd.sh
sudo chmod +x restore-lxd.sh
sudo bash restore-lxd.sh
#!/usr/bin/env bash
set -euo pipefail

# ─── カラー定義 ───────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERR]${RESET}   $*" >&2; }
die()     { error "$*"; exit 1; }

# ─── コンテナ一覧を取得して選択 ──────────────────────────────
select_container() {
  echo -e "\n${BOLD}=== LXD コンテナ一覧 ===${RESET}"

  # 名前のみ取得(ヘッダー除外)
  mapfile -t containers < <(lxc list --format csv --columns n 2>/dev/null | grep -v '^$')

  [[ ${#containers[@]} -eq 0 ]] && die "LXDコンテナが見つかりません"

  for i in "${!containers[@]}"; do
    # lxc info でステータス取得
    status=$(lxc info "${containers[$i]}" 2>/dev/null | awk '/^Status:/{print $2}')
    printf "  ${BOLD}%2d${RESET}) %-30s [%s]\n" "$((i+1))" "${containers[$i]}" "${status:-不明}"
  done

  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}コンテナ番号を選択 [1-${#containers[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#containers[@]} )); then
      SELECTED_CONTAINER="${containers[$((choice-1))]}"
      success "選択: ${SELECTED_CONTAINER}"
      break
    fi
    warn "無効な番号です。1〜${#containers[@]} を入力してください"
  done
}

# ─── スナップショット一覧を取得して選択 ──────────────────────
select_snapshot() {
  local container="$1"
  echo -e "\n${BOLD}=== ${container} のスナップショット一覧 ===${RESET}"

  # lxc info のテーブル出力からスナップショット名と日時を取得
  # 出力例:
  # Snapshots:
  # +-------------+----------------------+...
  # |    NAME     |       TAKEN AT       |...
  # +-------------+----------------------+...
  # | TailscaleOK | 2026/06/08 20:28 JST |...
  # +-------------+----------------------+...
  local info_output
  info_output=$(lxc info "$container" 2>/dev/null)

  mapfile -t snapshots < <(
    echo "$info_output" \
    | awk '/^Snapshots:/,0' \
    | grep '^\|' \
    | awk -F'|' '{gsub(/ /,"",$2); print $2}' \
    | grep -v '^NAME$' \
    | grep -v '^$'
  )

  if [[ ${#snapshots[@]} -eq 0 ]]; then
    die "${container} にスナップショットがありません"
  fi

  for i in "${!snapshots[@]}"; do
    created=$(
      echo "$info_output" \
      | awk '/^Snapshots:/,0' \
      | grep '^\|' \
      | awk -F'|' '{gsub(/^ +| +$/,"",$2); gsub(/^ +| +$/,"",$3); print $2"|"$3}' \
      | grep "^${snapshots[$i]}|" \
      | cut -d'|' -f2
    )
    printf "  ${BOLD}%2d${RESET}) %-30s  %s\n" "$((i+1))" "${snapshots[$i]}" "${created:-不明}"
  done

  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}スナップショット番号を選択 [1-${#snapshots[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#snapshots[@]} )); then
      SELECTED_SNAPSHOT="${snapshots[$((choice-1))]}"
      success "選択: ${SELECTED_SNAPSHOT}"
      break
    fi
    warn "無効な番号です。1〜${#snapshots[@]} を入力してください"
  done
}

# ─── リストア確認 ────────────────────────────────────────────
confirm_restore() {
  local container="$1"
  local snapshot="$2"
  echo
  warn "以下の操作を実行します:"
  echo -e "  コンテナ  : ${BOLD}${container}${RESET}"
  echo -e "  スナップショット: ${BOLD}${snapshot}${RESET}"
  echo -e "  ${RED}現在の状態は失われます${RESET}"
  echo
  read -rp "$(echo -e "${YELLOW}続行しますか? [y/N]:${RESET} ")" yn
  [[ "$yn" =~ ^[Yy]$ ]] || die "キャンセルしました"
}

# ─── リストア実行 ────────────────────────────────────────────
do_restore() {
  local container="$1"
  local snapshot="$2"

  # 実行中なら停止
  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" == "RUNNING" ]]; then
    info "${container} を停止中..."
    lxc stop "$container" --force
    info "停止しました"
  fi

  info "スナップショット ${snapshot} でリストア中..."
  if lxc restore "$container" "$snapshot"; then
    success "リストア完了: ${container} / ${snapshot}"
  else
    die "リストアに失敗しました"
  fi
}

# ─── コンテナ起動 & シェルへ入る ─────────────────────────────
enter_container() {
  local container="$1"

  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" != "RUNNING" ]]; then
    info "${container} を起動中..."
    lxc start "$container"
    # ネットワーク ready 待ち(最大15秒)
    local i=0
    while (( i < 15 )); do
      sleep 1
      status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
      [[ "$status" == "RUNNING" ]] && break
      (( i++ ))
    done
    success "${container} が起動しました"
  fi

  echo
  success "コンテナ ${container} に入ります (exit で戻れます)"
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  lxc exec "$container" -- bash
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  info "コンテナから抜けました"
}

# ─── メイン ──────────────────────────────────────────────────
main() {
  # root または lxd グループ確認
  if ! lxc list &>/dev/null; then
    die "lxc コマンドが実行できません。sudo か lxd グループに所属しているか確認してください"
  fi

  select_container
  select_snapshot "$SELECTED_CONTAINER"
  confirm_restore  "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
  do_restore       "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
  enter_container  "$SELECTED_CONTAINER"
}

main

スナップショットのスクリプトと統合

スナップショット作成・リストア・削除を1つのスクリプトにまとめました。

Screenshot
sudo mkdir -p /opt/script/lxd
cd /opt/script/lxd
sudo nano snap-lxd.sh
sudo chmod +x snap-lxd.sh
sudo bash snap-lxd.sh
#!/usr/bin/env bash
set -euo pipefail

# ─── カラー定義 ───────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERR]${RESET}   $*" >&2; }
die()     { error "$*"; exit 1; }

# ─── コンテナ一覧を取得して選択 ──────────────────────────────
select_container() {
  echo -e "\n${BOLD}=== LXD コンテナ一覧 ===${RESET}"

  mapfile -t containers < <(lxc list --format csv --columns n 2>/dev/null | grep -v '^$')
  [[ ${#containers[@]} -eq 0 ]] && die "LXDコンテナが見つかりません"

  for i in "${!containers[@]}"; do
    local status
    status=$(lxc info "${containers[$i]}" 2>/dev/null | awk '/^Status:/{print $2}')
    printf "  ${BOLD}%2d${RESET}) %-30s [%s]\n" "$((i+1))" "${containers[$i]}" "${status:-不明}"
  done

  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}コンテナ番号を選択 [1-${#containers[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#containers[@]} )); then
      SELECTED_CONTAINER="${containers[$((choice-1))]}"
      success "選択: ${SELECTED_CONTAINER}"
      break
    fi
    warn "無効な番号です。1〜${#containers[@]} を入力してください"
  done
}

# ─── 操作メニュー ────────────────────────────────────────────
select_action() {
  echo -e "\n${BOLD}=== 操作を選択 ===${RESET}"
  echo -e "  ${BOLD}1${RESET}) スナップショット作成"
  echo -e "  ${BOLD}2${RESET}) スナップショットからリストア"
  echo -e "  ${BOLD}3${RESET}) スナップショット削除"
  echo

  while true; do
    read -rp "$(echo -e "${YELLOW}操作番号を選択 [1-3]:${RESET} ")" choice
    case "$choice" in
      1) SELECTED_ACTION="create";  break ;;
      2) SELECTED_ACTION="restore"; break ;;
      3) SELECTED_ACTION="delete";  break ;;
      *) warn "1〜3 を入力してください" ;;
    esac
  done
}

# ─── スナップショット一覧取得(共通) ────────────────────────
get_snapshots() {
  local container="$1"
  local info_output
  info_output=$(lxc info "$container" 2>/dev/null)

  mapfile -t SNAPSHOTS < <(
    echo "$info_output" \
    | awk '/^Snapshots:/,0' \
    | grep '^\|' \
    | awk -F'|' '{gsub(/ /,"",$2); print $2}' \
    | grep -v '^NAME$' \
    | grep -v '^$'
  )

  # 日時も取得しておく(表示用)
  mapfile -t SNAPSHOT_DATES < <(
    echo "$info_output" \
    | awk '/^Snapshots:/,0' \
    | grep '^\|' \
    | awk -F'|' '{gsub(/^ +| +$/,"",$2); gsub(/^ +| +$/,"",$3); print $2"|"$3}' \
    | grep -v '^NAME|' \
    | grep -v '^$' \
    | cut -d'|' -f2
  )
}

# ─── スナップショット一覧表示して選択 ────────────────────────
select_snapshot() {
  local container="$1"
  echo -e "\n${BOLD}=== ${container} のスナップショット一覧 ===${RESET}"

  get_snapshots "$container"
  [[ ${#SNAPSHOTS[@]} -eq 0 ]] && die "${container} にスナップショットがありません"

  for i in "${!SNAPSHOTS[@]}"; do
    printf "  ${BOLD}%2d${RESET}) %-30s  %s\n" "$((i+1))" "${SNAPSHOTS[$i]}" "${SNAPSHOT_DATES[$i]:-不明}"
  done

  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}スナップショット番号を選択 [1-${#SNAPSHOTS[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#SNAPSHOTS[@]} )); then
      SELECTED_SNAPSHOT="${SNAPSHOTS[$((choice-1))]}"
      success "選択: ${SELECTED_SNAPSHOT}"
      break
    fi
    warn "無効な番号です。1〜${#SNAPSHOTS[@]} を入力してください"
  done
}

# ─── 作成 ────────────────────────────────────────────────────
do_create() {
  local container="$1"

  echo -e "\n${BOLD}=== スナップショット作成: ${container} ===${RESET}"
  echo -e "  ${CYAN}※ コメントは英数字・ハイフン・アンダースコアのみ(日本語不可・空可)${RESET}"
  read -rp "$(echo -e "${YELLOW}コメントを入力:${RESET} ")" COMMENT

  # スペース→ハイフン、英数字・ハイフン・アンダースコア以外除去
  COMMENT="${COMMENT// /-}"
  COMMENT=$(echo "$COMMENT" | tr -cd '[:alnum:]-_')

  local snap="snap-$(date +%Y%m%d-%H%M%S)"
  [[ -n "$COMMENT" ]] && snap="${snap}-${COMMENT}"

  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" == "RUNNING" ]]; then
    info "${container} を停止中..."
    lxc stop "$container" 2>/dev/null || true
  fi

  info "スナップショット作成中: ${snap}"
  lxc snapshot "$container" "$snap"
  success "作成完了: ${container}/${snap}"

  info "${container} を起動中..."
  lxc start "$container"
  _wait_running "$container"
  success "${container} が起動しました"
}

# ─── リストア ────────────────────────────────────────────────
do_restore() {
  local container="$1"
  local snapshot="$2"

  echo
  warn "以下の操作を実行します:"
  echo -e "  コンテナ        : ${BOLD}${container}${RESET}"
  echo -e "  スナップショット: ${BOLD}${snapshot}${RESET}"
  echo -e "  ${RED}現在の状態は失われます${RESET}"
  echo
  read -rp "$(echo -e "${YELLOW}続行しますか? [y/N]:${RESET} ")" yn
  [[ "$yn" =~ ^[Yy]$ ]] || die "キャンセルしました"

  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" == "RUNNING" ]]; then
    info "${container} を停止中..."
    lxc stop "$container" --force
  fi

  info "リストア中: ${snapshot}"
  lxc restore "$container" "$snapshot"
  success "リストア完了: ${container} / ${snapshot}"

  info "${container} を起動中..."
  lxc start "$container"
  _wait_running "$container"
  success "${container} が起動しました"

  echo
  success "コンテナ ${container} に入ります (exit で戻れます)"
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  lxc exec "$container" -- bash
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  info "コンテナから抜けました"
}

# ─── 削除 ────────────────────────────────────────────────────
do_delete() {
  local container="$1"
  local snapshot="$2"

  echo
  warn "以下のスナップショットを削除します:"
  echo -e "  コンテナ        : ${BOLD}${container}${RESET}"
  echo -e "  スナップショット: ${BOLD}${snapshot}${RESET}"
  echo
  read -rp "$(echo -e "${YELLOW}削除しますか? [y/N]:${RESET} ")" yn
  [[ "$yn" =~ ^[Yy]$ ]] || die "キャンセルしました"

  info "削除中: ${snapshot}"
  lxc delete "${container}/${snapshot}"
  success "削除完了: ${container}/${snapshot}"
}

# ─── 起動待ち(共通) ─────────────────────────────────────────
_wait_running() {
  local container="$1"
  local i=0
  while (( i < 15 )); do
    sleep 1
    local s
    s=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
    [[ "$s" == "RUNNING" ]] && return 0
    (( i++ ))
  done
  warn "起動確認がタイムアウトしました(コンテナは起動中かもしれません)"
}

# ─── メイン ──────────────────────────────────────────────────
main() {
  if ! lxc list &>/dev/null; then
    die "lxc コマンドが実行できません。sudo か lxd グループに所属しているか確認してください"
  fi

  select_container
  select_action

  case "$SELECTED_ACTION" in
    create)
      do_create "$SELECTED_CONTAINER"
      ;;
    restore)
      select_snapshot "$SELECTED_CONTAINER"
      do_restore "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
      ;;
    delete)
      select_snapshot "$SELECTED_CONTAINER"
      do_delete "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
      ;;
  esac
}

main

6個のスクリプトを一度に作成して保存するスクリプト

6個のスクリプトを作成するスクリプトも更新しました。
コピーボタンでまとめてコピーして、ターミナルに貼り付けて実行するだけです。
実行後、以下の6ファイルが /opt/script/lxd/ に作成され、実行権限も付与されます。

生成される6つのスクリプトの内容:

ファイル名内容
minimal-lxd-base-create.shコンテナ作成・マウントディレクトリチェック付き
first-setup-minimal-lxd-base.shapt update/upgrade・curl・Tailscaleインストール後シャットダウン
docker-lxd-base-create.shlxd-base-minimalをコピーしてDocker環境構築・シャットダウン
copy-lxd-create.sh既存コンテナをコピーして新規作成
snapshot-lxd.shスナップショット作成・リストア・削除
enter-lxd-container.shコンテナ選択して入る
#!/bin/bash
set -euo pipefail

sudo mkdir -p /opt/script/lxd

cat > /tmp/minimal-lxd-base-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

CONTAINER="lxd-base-minimal"
MOUNT_PATH="/opt/lxd-data"

echo "=== lxd-base-minimal コンテナを作成 ==="
lxc launch ubuntu:26.04 "$CONTAINER"

echo "=== $MOUNT_PATH が存在しない場合は作成 ==="
if [ ! -d "$MOUNT_PATH" ]; then
    sudo mkdir -p "$MOUNT_PATH"
    if [ ! -d "$MOUNT_PATH" ]; then
        echo "エラー: $MOUNT_PATH の作成に失敗しました"
        exit 1
    fi
    echo "    $MOUNT_PATH を作成しました"
else
    echo "    $MOUNT_PATH は既に存在します"
fi

echo "=== ホストの $MOUNT_PATH をコンテナに同じパスでマウント ==="
lxc config device add "$CONTAINER" opt-lxd-data disk source="$MOUNT_PATH" path="$MOUNT_PATH"

echo "=== ID マッピング設定を適用 ==="
lxc config set "$CONTAINER" raw.idmap "both 1000 1000"

echo "=== コンテナを再起動します ==="
lxc restart "$CONTAINER"

echo "=== 完了しました ==="
EOF

cat > /tmp/first-setup-minimal-lxd-base.sh << 'EOF'
#!/bin/bash
set -euo pipefail

CONTAINER="lxd-base-minimal"

echo "==> コンテナ '${CONTAINER}' に接続してセットアップを開始します..."

lxc exec "${CONTAINER}" -- bash -euo pipefail << 'INNER'
echo "==> apt update"
sudo apt update

echo "==> apt upgrade"
sudo apt upgrade -y

echo "==> curl インストール"
sudo apt install -y curl

echo "==> Tailscale インストール"
curl -fsSL https://tailscale.com/install.sh | sh

echo "==> セットアップ完了"
INNER

echo "==> コンテナをシャットダウンします..."
lxc stop "${CONTAINER}"

echo "==> 完了"
EOF

cat > /tmp/docker-lxd-base-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

SRC="lxd-base-minimal"
NEW="lxd-base-docker"
MOUNT_PATH="/opt/lxd-data"

echo "=== コンテナをコピー: $SRC → $NEW ==="
lxc copy "$SRC" "$NEW"

echo "=== コンテナを停止します(設定適用のため) ==="
lxc stop "$NEW" 2>/dev/null || true

echo "=== raw.idmap を設定 ==="
lxc config set "$NEW" raw.idmap "both 1000 1000"

echo "=== Nesting を Allow に設定 ==="
lxc config set "$NEW" security.nesting true

echo "=== コンテナを起動 ==="
lxc start "$NEW"

echo "=== マウントディレクトリの権限設定(ホスト側) ==="
sudo chown 1000:1000 "$MOUNT_PATH"
sudo chmod 775 "$MOUNT_PATH"

echo "=== 設定反映のため再起動 ==="
lxc restart "$NEW"

echo "=== ネットワーク疎通を待機中 ==="
for i in $(seq 1 30); do
    if lxc exec "$NEW" -- curl -fsSL --max-time 3 https://get.docker.com -o /dev/null 2>/dev/null; then
        echo "    ネットワーク疎通確認 (${i}秒)"
        break
    fi
    echo "    待機中... (${i}/30秒)"
    sleep 1
    if [ "$i" -eq 30 ]; then
        echo "エラー: ネットワークが30秒以内に疎通しませんでした" >&2
        exit 1
    fi
done

echo "=== コンテナ内でセットアップを実行 ==="
lxc exec "$NEW" -- bash -euo pipefail << 'INNER'

echo "--- Docker インストール ---"
curl -fsSL https://get.docker.com | sh

echo "--- /opt/docker ディレクトリのセットアップ ---"
mkdir -p /opt/docker

if getent group docker > /dev/null 2>&1; then
    chown -R "${USER:-root}:docker" /opt/docker
    chmod -R 775 /opt/docker
    chmod -R g+s /opt/docker
    if [ "${USER:-root}" != "root" ]; then
        usermod -aG docker "$USER"
        echo "グループ変更を反映するには、newgrp docker を実行してください(再起動不要)"
    else
        echo "rootユーザーのため usermod はスキップしました"
    fi
else
    chown -R "${USER:-root}:${USER:-root}" /opt/docker
    chmod -R 755 /opt/docker
    echo "docker グループが存在しないため、オーナー権限のみ設定しました"
fi

echo "/opt/docker のセットアップ完了"
INNER

echo "=== コンテナをシャットダウン ==="
lxc stop "$NEW"

echo "=== 完了: $NEW ==="
EOF

cat > /tmp/copy-lxd-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

containers=($(lxc list -c n --format csv))

echo "=== コピー元のコンテナを選択してください ==="
i=1
for c in "${containers[@]}"; do
    echo "$i) $c"
    ((i++))
done

read -p "番号を入力: " index

if ! [[ "$index" =~ ^[0-9]+$ ]]; then
    echo "エラー: 数字を入力してください" >&2
    exit 1
fi

index=$((index - 1))

if [ "$index" -lt 0 ] || [ "$index" -ge "${#containers[@]}" ]; then
    echo "エラー: 不正な番号です" >&2
    exit 1
fi

SRC="${containers[$index]}"
echo "選択されたコピー元: $SRC"

read -p "新しいコンテナ名を入力: " NEW

if [ -z "$NEW" ]; then
    echo "エラー: コンテナ名が空です" >&2
    exit 1
fi

echo "=== コピー開始: $SRC → $NEW ==="
lxc copy "$SRC" "$NEW"

echo "=== 一旦コンテナを停止します(ID マップ適用のため) ==="
lxc stop "$NEW" 2>/dev/null || true

echo "=== raw.idmap を設定します ==="
lxc config set "$NEW" raw.idmap "both 1000 1000"

echo "=== コンテナを起動します ==="
lxc start "$NEW"

echo "=== 設定反映のため再起動 ==="
lxc restart "$NEW"

echo "=== コンテナに入ります: $NEW ==="
lxc exec "$NEW" -- bash
EOF

cat > /tmp/snapshot-lxd.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

# ─── カラー定義 ───────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERR]${RESET}   $*" >&2; }
die()     { error "$*"; exit 1; }

select_container() {
  echo -e "\n${BOLD}=== LXD コンテナ一覧 ===${RESET}"
  mapfile -t containers < <(lxc list --format csv --columns n 2>/dev/null | grep -v '^$')
  [[ ${#containers[@]} -eq 0 ]] && die "LXDコンテナが見つかりません"
  for i in "${!containers[@]}"; do
    local status
    status=$(lxc info "${containers[$i]}" 2>/dev/null | awk '/^Status:/{print $2}')
    printf "  ${BOLD}%2d${RESET}) %-30s [%s]\n" "$((i+1))" "${containers[$i]}" "${status:-不明}"
  done
  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}コンテナ番号を選択 [1-${#containers[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#containers[@]} )); then
      SELECTED_CONTAINER="${containers[$((choice-1))]}"
      success "選択: ${SELECTED_CONTAINER}"
      break
    fi
    warn "無効な番号です。1〜${#containers[@]} を入力してください"
  done
}

select_action() {
  echo -e "\n${BOLD}=== 操作を選択 ===${RESET}"
  echo -e "  ${BOLD}1${RESET}) スナップショット作成"
  echo -e "  ${BOLD}2${RESET}) スナップショットからリストア"
  echo -e "  ${BOLD}3${RESET}) スナップショット削除"
  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}操作番号を選択 [1-3]:${RESET} ")" choice
    case "$choice" in
      1) SELECTED_ACTION="create";  break ;;
      2) SELECTED_ACTION="restore"; break ;;
      3) SELECTED_ACTION="delete";  break ;;
      *) warn "1〜3 を入力してください" ;;
    esac
  done
}

get_snapshots() {
  local container="$1"
  local info_output
  info_output=$(lxc info "$container" 2>/dev/null)
  mapfile -t SNAPSHOTS < <(
    echo "$info_output" \
    | awk '/^Snapshots:/,0' \
    | grep '^\|' \
    | awk -F'|' '{gsub(/ /,"",$2); print $2}' \
    | grep -v '^NAME$' \
    | grep -v '^$'
  )
  mapfile -t SNAPSHOT_DATES < <(
    echo "$info_output" \
    | awk '/^Snapshots:/,0' \
    | grep '^\|' \
    | awk -F'|' '{gsub(/^ +| +$/,"",$2); gsub(/^ +| +$/,"",$3); print $2"|"$3}' \
    | grep -v '^NAME|' \
    | grep -v '^$' \
    | cut -d'|' -f2
  )
}

select_snapshot() {
  local container="$1"
  echo -e "\n${BOLD}=== ${container} のスナップショット一覧 ===${RESET}"
  get_snapshots "$container"
  [[ ${#SNAPSHOTS[@]} -eq 0 ]] && die "${container} にスナップショットがありません"
  for i in "${!SNAPSHOTS[@]}"; do
    printf "  ${BOLD}%2d${RESET}) %-30s  %s\n" "$((i+1))" "${SNAPSHOTS[$i]}" "${SNAPSHOT_DATES[$i]:-不明}"
  done
  echo
  while true; do
    read -rp "$(echo -e "${YELLOW}スナップショット番号を選択 [1-${#SNAPSHOTS[@]}]:${RESET} ")" choice
    if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#SNAPSHOTS[@]} )); then
      SELECTED_SNAPSHOT="${SNAPSHOTS[$((choice-1))]}"
      success "選択: ${SELECTED_SNAPSHOT}"
      break
    fi
    warn "無効な番号です。1〜${#SNAPSHOTS[@]} を入力してください"
  done
}

do_create() {
  local container="$1"
  echo -e "\n${BOLD}=== スナップショット作成: ${container} ===${RESET}"
  echo -e "  ${CYAN}※ コメントは英数字・ハイフン・アンダースコアのみ(日本語不可・空可)${RESET}"
  read -rp "$(echo -e "${YELLOW}コメントを入力:${RESET} ")" COMMENT
  COMMENT="${COMMENT// /-}"
  COMMENT=$(echo "$COMMENT" | tr -cd '[:alnum:]-_')
  local snap="snap-$(date +%Y%m%d-%H%M%S)"
  [[ -n "$COMMENT" ]] && snap="${snap}-${COMMENT}"
  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" == "RUNNING" ]]; then
    info "${container} を停止中..."
    lxc stop "$container" 2>/dev/null || true
  fi
  info "スナップショット作成中: ${snap}"
  lxc snapshot "$container" "$snap"
  success "作成完了: ${container}/${snap}"
  info "${container} を起動中..."
  lxc start "$container"
  _wait_running "$container"
  success "${container} が起動しました"
}

do_restore() {
  local container="$1"
  local snapshot="$2"
  echo
  warn "以下の操作を実行します:"
  echo -e "  コンテナ        : ${BOLD}${container}${RESET}"
  echo -e "  スナップショット: ${BOLD}${snapshot}${RESET}"
  echo -e "  ${RED}現在の状態は失われます${RESET}"
  echo
  read -rp "$(echo -e "${YELLOW}続行しますか? [y/N]:${RESET} ")" yn
  [[ "$yn" =~ ^[Yy]$ ]] || die "キャンセルしました"
  local status
  status=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
  if [[ "$status" == "RUNNING" ]]; then
    info "${container} を停止中..."
    lxc stop "$container" --force
  fi
  info "リストア中: ${snapshot}"
  lxc restore "$container" "$snapshot"
  success "リストア完了: ${container} / ${snapshot}"
  info "${container} を起動中..."
  lxc start "$container"
  _wait_running "$container"
  success "${container} が起動しました"
  echo
  success "コンテナ ${container} に入ります (exit で戻れます)"
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  lxc exec "$container" -- bash
  echo -e "${CYAN}─────────────────────────────────────────${RESET}"
  info "コンテナから抜けました"
}

do_delete() {
  local container="$1"
  local snapshot="$2"
  echo
  warn "以下のスナップショットを削除します:"
  echo -e "  コンテナ        : ${BOLD}${container}${RESET}"
  echo -e "  スナップショット: ${BOLD}${snapshot}${RESET}"
  echo
  read -rp "$(echo -e "${YELLOW}削除しますか? [y/N]:${RESET} ")" yn
  [[ "$yn" =~ ^[Yy]$ ]] || die "キャンセルしました"
  info "削除中: ${snapshot}"
  lxc delete "${container}/${snapshot}"
  success "削除完了: ${container}/${snapshot}"
}

_wait_running() {
  local container="$1"
  local i=0
  while (( i < 15 )); do
    sleep 1
    local s
    s=$(lxc info "$container" 2>/dev/null | awk '/^Status:/{print $2}')
    [[ "$s" == "RUNNING" ]] && return 0
    (( i++ ))
  done
  warn "起動確認がタイムアウトしました(コンテナは起動中かもしれません)"
}

main() {
  if ! lxc list &>/dev/null; then
    die "lxc コマンドが実行できません。sudo か lxd グループに所属しているか確認してください"
  fi
  select_container
  select_action
  case "$SELECTED_ACTION" in
    create)
      do_create "$SELECTED_CONTAINER"
      ;;
    restore)
      select_snapshot "$SELECTED_CONTAINER"
      do_restore "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
      ;;
    delete)
      select_snapshot "$SELECTED_CONTAINER"
      do_delete "$SELECTED_CONTAINER" "$SELECTED_SNAPSHOT"
      ;;
  esac
}

main
EOF

cat > /tmp/enter-lxd-container.sh << 'EOF'
#!/bin/bash
set -euo pipefail

echo "=== LXD コンテナ一覧 ==="

containers=($(lxc list -c n --format csv))

i=1
for c in "${containers[@]}"; do
    echo "$i) $c"
    ((i++))
done

read -p "番号を入力: " index

if ! [[ "$index" =~ ^[0-9]+$ ]]; then
    echo "エラー: 数字を入力してください" >&2
    exit 1
fi

index=$((index - 1))

if [ "$index" -lt 0 ] || [ "$index" -ge "${#containers[@]}" ]; then
    echo "エラー: 不正な番号です" >&2
    exit 1
fi

TARGET="${containers[$index]}"
echo "選択されたコンテナ: $TARGET"

STATUS=$(lxc info "$TARGET" | awk '/Status:/ {print tolower($2)}')

if [ "$STATUS" != "running" ]; then
    echo "=== コンテナが停止中です。起動します ==="
    lxc start "$TARGET"
else
    echo "=== コンテナは既に起動中です ==="
fi

echo "=== コンテナに入ります: $TARGET ==="
lxc exec "$TARGET" -- bash
EOF

sudo mv /tmp/minimal-lxd-base-create.sh /opt/script/lxd/
sudo mv /tmp/first-setup-minimal-lxd-base.sh /opt/script/lxd/
sudo mv /tmp/docker-lxd-base-create.sh /opt/script/lxd/
sudo mv /tmp/copy-lxd-create.sh /opt/script/lxd/
sudo mv /tmp/snapshot-lxd.sh /opt/script/lxd/
sudo mv /tmp/enter-lxd-container.sh /opt/script/lxd/

sudo chmod +x /opt/script/lxd/*.sh
sudo chown $USER:$USER /opt/script/lxd/*.sh

echo "=== 完了 ==="
ls -la /opt/script/lxd/
タイトルとURLをコピーしました