init
This commit is contained in:
commit
9bc27e0fb8
2 changed files with 934 additions and 0 deletions
163
README.md
Normal file
163
README.md
Normal file
|
@ -0,0 +1,163 @@
|
|||
# iPXE tool
|
||||
|
||||
Скрипт выполняет два основных набора задач:
|
||||
|
||||
1. **Работа с образом Alpine (`disk.img`)** — команда `image` с подкомандами:
|
||||
|
||||
* `image build` — создание и первичное наполнение образа;
|
||||
* `image chroot` — вход в chroot;
|
||||
* `image overlay` — вход в chroot с наложением OverlayFS (изменения не сохраняются).
|
||||
2. **Сборка iPXE в режиме overlay** с временными изменениями и копированием артефактов на хост — команда `build`.
|
||||
|
||||
> Все команды требуют прав `root`.
|
||||
|
||||
---
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддержка loop-устройств и доступ к утилитам: `losetup`, `parted`, `mkfs.ext4`, `mount`, `umount`, `curl`, `tar`, `chroot`.
|
||||
* Для создания файла: одна из утилит `fallocate`/`truncate`/`dd`.
|
||||
* Для OverlayFS: поддержка `overlay` в ядре (`/proc/filesystems`) и доступен `modprobe` (если модуль загружается динамически).
|
||||
* Интернет-доступ для загрузки `alpine-minirootfs`.
|
||||
* Доступные каталоги:
|
||||
|
||||
* Образ по умолчанию: `/var/ipxe/disk.img`
|
||||
* Внутри образа при первой сборке создаётся `/develop` с файлами:
|
||||
|
||||
* `/develop/build.sh` — сценарий сборки iPXE;
|
||||
* `/develop/certtrust.patch` — патч к исходникам iPXE (по необходимости).
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1) Создание образа Alpine
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image build
|
||||
```
|
||||
|
||||
По умолчанию:
|
||||
|
||||
* URL minirootfs: `https://dl-cdn.alpinelinux.org/.../alpine-minirootfs-3.22.2-x86_64.tar.gz`
|
||||
* Путь к образу: `/var/ipxe/disk.img`
|
||||
* Размер: `512` МБ (диапазон: `512..1024` МБ)
|
||||
|
||||
Полезные флаги:
|
||||
|
||||
* `--url URL` — другой источник minirootfs
|
||||
* `--img PATH` — другой путь к образу
|
||||
* `--size-mb N` — размер образа (512..1024)
|
||||
* `--no-bootstrap` — без установки пакетов/репозитория iPXE
|
||||
* `--force` — перезапись существующего файла
|
||||
* `--debug` — подробный вывод
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image build --img /var/ipxe/disk.img --size-mb 768 --force
|
||||
```
|
||||
|
||||
### 2) Вход в chroot (с сохранением изменений)
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image chroot --img /var/ipxe/disk.img
|
||||
```
|
||||
|
||||
Опционально можно выполнить команду сразу:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image chroot --img /var/ipxe/disk.img -- 'apk add htop'
|
||||
```
|
||||
|
||||
### 3) Вход в chroot в режиме OverlayFS (изменения не сохраняются)
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image overlay --img /var/ipxe/disk.img
|
||||
```
|
||||
|
||||
Опционально задать размер tmpfs для верхнего слоя:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image overlay --img /var/ipxe/disk.img --overlay-size-mb 512
|
||||
```
|
||||
|
||||
### 4) Сборка iPXE в overlay и копирование артефактов на хост
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh build --img /var/ipxe/disk.img --out-dir /tmp/out -- --efi --legacy --iso --default --patch
|
||||
```
|
||||
|
||||
* Все флаги после `--` передаются **внутреннему** `/develop/build.sh`.
|
||||
* Артефакты копируются в `--out-dir` (по умолчанию текущий каталог) **только если были собраны**:
|
||||
|
||||
* `ipxe.efi` → `/root/ipxe/src/bin-x86_64-efi/ipxe.efi`
|
||||
* `undionly.kpxe` → `/root/ipxe/src/bin-i386-pcbios/undionly.kpxe`
|
||||
* `ipxe.iso` → `/root/ipxe/src/bin/ipxe.iso`
|
||||
|
||||
---
|
||||
|
||||
## Поведение и детали
|
||||
|
||||
* **Логи** скрипта нейтральные: «создание образа», «монтирование», «скачивание», «распаковка» и т. п.
|
||||
* При `image build` выполняется разметка `msdos`, создаётся один раздел `ext4`, монтируется и наполняется содержимым `minirootfs`.
|
||||
* При первой сборке внутрь образа добавляется `/develop/build.sh` и `/develop/certtrust.patch`.
|
||||
* `image chroot` и `image overlay` автоматически монтируют необходимые файловые системы (`/dev`, `proc`, `sys`, `run`, `devpts`, `shm`), а при выходе — размонтируют.
|
||||
* В `overlay`-режиме нижний слой (`disk.img`) монтируется **только для чтения**, верхний слой — во временный `tmpfs`; все изменения теряются при выходе.
|
||||
* Команда `build`:
|
||||
|
||||
* Разворачивает `overlay`, выполняет `/develop/build.sh` с переданными флагами;
|
||||
* По коду возврата определяет, была ли сборка успешной, и при наличии артефактов копирует их на хост в `--out-dir`;
|
||||
* После завершения всё размонтирует автоматически.
|
||||
|
||||
---
|
||||
|
||||
## Часто используемые флаги `/develop/build.sh`
|
||||
|
||||
Передаются через `--` в команду `build`, например:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh build -- --efi --default
|
||||
```
|
||||
|
||||
* `--efi` — сборка `bin-x86_64-efi/ipxe.efi`
|
||||
* `--legacy` — сборка `bin-i386-pcbios/undionly.kpxe`
|
||||
* `--iso` — сборка `bin/ipxe.iso`
|
||||
* Без флагов собираются **все** цели.
|
||||
* `--default` — включение набора опций в `config/general.h` (PING_CMD, IPSTAT_CMD, CONSOLE_CMD и т. д.)
|
||||
* `--patch` — применение `/develop/certtrust.patch` и включение HTTPS/CERT_CMD
|
||||
|
||||
---
|
||||
|
||||
## Примеры
|
||||
|
||||
Создание образа и последующая сборка всех артефактов iPXE в overlay с копированием на хост:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image build --img /var/ipxe/disk.img --size-mb 512
|
||||
sudo ./ipxe.sh build --img /var/ipxe/disk.img --out-dir /srv/ipxe -- --efi --legacy --iso
|
||||
```
|
||||
|
||||
Минимальная сборка только EFI, с патчем и дефолтными опциями:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh build --img /var/ipxe/disk.img --out-dir /srv/ipxe -- --efi --patch --default
|
||||
```
|
||||
|
||||
Проверка окружения без сохранения изменений:
|
||||
|
||||
```bash
|
||||
sudo ./ipxe.sh image overlay --img /var/ipxe/disk.img --overlay-size-mb 256 -- 'apk info -vv'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Диагностика
|
||||
|
||||
* «образ не найден» — проверьте путь `--img` и выполните `image build`.
|
||||
* «минимальный/максимальный размер» — скорректируйте `--size-mb` (диапазон 512..1024).
|
||||
* «отсутствие поддержки overlayfs» — убедитесь в наличии модуля `overlay` в ядре и/или возможности загрузки через `modprobe`.
|
||||
* При сетевых операциях используйте актуальный URL minirootfs (`--url`).
|
||||
|
||||
При любой ошибке скрипт выполняет автоматическую очистку: размонтирование файловых систем и отвязку `loop`-устройств.
|
||||
|
771
ipxe.sh
Executable file
771
ipxe.sh
Executable file
|
@ -0,0 +1,771 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DEFAULT_URL="https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/x86_64/alpine-minirootfs-3.22.2-x86_64.tar.gz"
|
||||
DEFAULT_IMG="/var/ipxe/disk.img"
|
||||
DEFAULT_SIZE_MB=512 # [512..1024]
|
||||
SIZE_MIN=512
|
||||
SIZE_MAX=1024
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
log() { echo "==> $*"; }
|
||||
need_root() { [ "$(id -u)" -eq 0 ] || die "нужны права root"; }
|
||||
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "не найдена утилита: $1"; }
|
||||
is_mounted() { mountpoint -q "$1"; }
|
||||
umount_if() { is_mounted "$1" && umount "$1" || true; }
|
||||
wait_for_blk() {
|
||||
local dev="$1" tries="${2:-60}" i=0
|
||||
while [ $i -lt "$tries" ]; do
|
||||
[ -b "$dev" ] && return 0
|
||||
sleep 0.1; i=$((i+1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------- требования на хосте ----------
|
||||
require_host_tools() {
|
||||
local tools=(losetup parted mkfs.ext4 mount umount curl tar chroot)
|
||||
|
||||
command -v fallocate >/dev/null 2>&1 || \
|
||||
command -v truncate >/dev/null 2>&1 || \
|
||||
command -v dd >/dev/null 2>&1 || \
|
||||
die "нужен fallocate/ truncate/ dd"
|
||||
|
||||
for t in "${tools[@]}"; do
|
||||
need_cmd "$t"
|
||||
done
|
||||
|
||||
command -v partprobe >/dev/null 2>&1 || \
|
||||
command -v partx >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- overlayfs: проверка поддержки и утилит ----------
|
||||
require_overlay_support() {
|
||||
if grep -qw overlay /proc/filesystems; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v modprobe >/dev/null 2>&1; then
|
||||
modprobe -q overlay 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if grep -qw overlay /proc/filesystems; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v modprobe >/dev/null 2>&1; then
|
||||
die "отсутствие поддержки overlayfs: нет записи в /proc/filesystems и утилита modprobe недоступна"
|
||||
else
|
||||
die "отсутствие поддержки overlayfs: модуль overlay недоступен в ядре или заблокирован"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- монтирование окружения chroot ----------
|
||||
mount_chroot_binds() {
|
||||
local root="$1"
|
||||
|
||||
for d in dev dev/pts dev/shm proc sys run; do mkdir -p "$root/$d"; done
|
||||
|
||||
if ! is_mounted "$root/dev"; then
|
||||
mount --bind /dev "$root/dev"
|
||||
fi
|
||||
|
||||
if ! is_mounted "$root/proc"; then
|
||||
mount -t proc proc "$root/proc" -o nosuid,nodev,noexec,ro
|
||||
fi
|
||||
|
||||
if ! is_mounted "$root/sys"; then
|
||||
mount -t sysfs sysfs "$root/sys" -o nosuid,nodev,noexec,ro
|
||||
fi
|
||||
|
||||
if ! is_mounted "$root/run"; then
|
||||
mount -t tmpfs tmpfs "$root/run" -o nosuid,nodev,mode=755
|
||||
fi
|
||||
|
||||
if ! is_mounted "$root/dev/pts"; then
|
||||
mount -t devpts devpts "$root/dev/pts" -o gid=5,mode=620,newinstance 2>/dev/null || \
|
||||
mount -t devpts devpts "$root/dev/pts" -o gid=5,mode=620
|
||||
fi
|
||||
|
||||
if ! is_mounted "$root/dev/shm"; then
|
||||
mount -t tmpfs tmpfs "$root/dev/shm" -o nosuid,nodev,mode=1777
|
||||
fi
|
||||
}
|
||||
|
||||
umount_chroot_binds() {
|
||||
local root="$1"
|
||||
umount_if "$root/dev/pts"
|
||||
umount_if "$root/dev/shm"
|
||||
umount_if "$root/run"
|
||||
umount_if "$root/proc"
|
||||
umount_if "$root/sys"
|
||||
umount_if "$root/dev"
|
||||
}
|
||||
|
||||
# ---------- bootstrap в chroot ----------
|
||||
bootstrap_in_chroot() {
|
||||
local mnt="$1"
|
||||
log "bootstrap: выполнение команд в chroot"
|
||||
chroot "$mnt" /usr/bin/env -i \
|
||||
HOME=/root TERM="${TERM:-xterm-256color}" \
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
/bin/ash -lc '
|
||||
set -eux
|
||||
printf "%s\n" "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||
apk update
|
||||
apk upgrade --no-cache
|
||||
apk add --no-cache musl-dev gcc make git perl xz-dev syslinux xorriso nano
|
||||
git clone https://github.com/ipxe/ipxe.git /root/ipxe || true
|
||||
'
|
||||
}
|
||||
|
||||
# ---------- image build (СБОРКА disk.img) ----------
|
||||
cmd_image_build() {
|
||||
local url="$DEFAULT_URL" img="$DEFAULT_IMG" size_mb="$DEFAULT_SIZE_MB"
|
||||
local bootstrap=1 force=0 debug=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--url) url="${2:?}"; shift 2;;
|
||||
--img) img="${2:?}"; shift 2;;
|
||||
--size-mb) size_mb="${2:?}"; shift 2;;
|
||||
--no-bootstrap) bootstrap=0; shift;;
|
||||
--force) force=1; shift;;
|
||||
--debug) debug=1; shift;;
|
||||
-h|--help) echo "usage: $0 image build [--url URL] [--img PATH] [--size-mb 512..1024] [--no-bootstrap] [--force] [--debug]"; exit 0;;
|
||||
*) die "неизвестный аргумент: $1";;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$debug" -eq 1 ] && set -x
|
||||
|
||||
need_root
|
||||
require_host_tools
|
||||
|
||||
[[ "$size_mb" =~ ^[0-9]+$ ]] || die "--size-mb: число"
|
||||
[ "$size_mb" -ge "$SIZE_MIN" ] || die "минимальный размер ${SIZE_MIN}MB"
|
||||
[ "$size_mb" -le "$SIZE_MAX" ] || die "максимальный размер ${SIZE_MAX}MB"
|
||||
|
||||
mkdir -p "$(dirname "$img")"
|
||||
if [ -e "$img" ] && [ "$force" -ne 1 ]; then
|
||||
die "файл уже существует: $img (флаг --force)"
|
||||
fi
|
||||
|
||||
log "создание образа: $img (${size_mb}MB)"
|
||||
if command -v fallocate >/dev/null 2>&1; then
|
||||
fallocate -v -l "${size_mb}M" "$img"
|
||||
elif command -v truncate >/dev/null 2>&1; then
|
||||
truncate -s "${size_mb}M" "$img"
|
||||
else
|
||||
dd if=/dev/zero of="$img" bs=1M count="$size_mb" status=progress
|
||||
fi
|
||||
|
||||
log "назначение loop-устройства"
|
||||
LOOPDEV="$(losetup -f -P --show "$img")"
|
||||
log "loop-устройство: $LOOPDEV"
|
||||
|
||||
cleanup() {
|
||||
set +e
|
||||
if [ -n "${MNT_DIR:-}" ]; then
|
||||
umount_chroot_binds "$MNT_DIR" || true
|
||||
umount_if "$MNT_DIR" || true
|
||||
rmdir "$MNT_DIR" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${LOOPDEV:-}" ]; then
|
||||
losetup -d "$LOOPDEV" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "разметка msdos: primary 2048s..100%"
|
||||
parted -s "$LOOPDEV" mklabel msdos
|
||||
parted -s "$LOOPDEV" mkpart primary ext4 2048s 100%
|
||||
|
||||
command -v partprobe >/dev/null 2>&1 && partprobe "$LOOPDEV" || true
|
||||
command -v partx >/dev/null 2>&1 && partx -u "$LOOPDEV" || true
|
||||
PART="${LOOPDEV}p1"
|
||||
wait_for_blk "$PART" || die "отсутствие раздела: $PART"
|
||||
|
||||
log "создание файловой системы ext4"
|
||||
mkfs.ext4 -F -L alpine-root "$PART" >/dev/null
|
||||
|
||||
MNT_DIR="$(mktemp -d -p /mnt alpine-root.XXXXXX || mktemp -d)"
|
||||
log "монтирование $PART -> $MNT_DIR"
|
||||
mount "$PART" "$MNT_DIR"
|
||||
|
||||
log "скачивание и распаковка minirootfs"
|
||||
curl -fsSL "$url" | tar -xzf - -C "$MNT_DIR"
|
||||
|
||||
log "подготовка /develop: build.sh и certtrust.patch"
|
||||
mkdir -p "$MNT_DIR/develop"
|
||||
cat >"$MNT_DIR/develop/build.sh" <<'EOSH'
|
||||
#!/bin/ash
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Использование: build-ipxe.sh [--efi] [--legacy] [--iso] [--default] [--patch] [-h|--help]
|
||||
|
||||
Без флагов собираются все цели:
|
||||
--legacy -> make bin-i386-pcbios/undionly.kpxe EMBED=start.ipxe
|
||||
--efi -> make bin-x86_64-efi/ipxe.efi EMBED=start.ipxe
|
||||
--iso -> make bin/ipxe.iso EMBED=start.ipxe
|
||||
|
||||
Дополнительно:
|
||||
--default -> включение дефолтных опций в config/general.h (PING_CMD, IPSTAT_CMD, CONSOLE_CMD, REBOOT_CMD, POWEROFF)
|
||||
--patch -> применение /develop/certtrust.patch к /root/ipxe (git apply) и активация HTTPS, CERT_CMD
|
||||
EOF
|
||||
}
|
||||
|
||||
# Определение числа потоков без глобальных переменных
|
||||
jobs_n() {
|
||||
if [ -n "${JOBS:-}" ]; then
|
||||
printf '%s' "$JOBS"
|
||||
return
|
||||
fi
|
||||
nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1'
|
||||
}
|
||||
|
||||
# ---- разбор флагов ----
|
||||
BUILD_EFI=0
|
||||
BUILD_LEGACY=0
|
||||
BUILD_ISO=0
|
||||
ANY=0
|
||||
|
||||
OPT_DEFAULT=0
|
||||
OPT_PATCH=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--efi|-e) BUILD_EFI=1; ANY=1 ;;
|
||||
--legacy|-l) BUILD_LEGACY=1; ANY=1 ;;
|
||||
--iso|-i) BUILD_ISO=1; ANY=1 ;;
|
||||
--default) OPT_DEFAULT=1 ;;
|
||||
--patch) OPT_PATCH=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*)
|
||||
echo "Неизвестный флаг: $arg" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# если флаги сборки не заданы — собрать всё
|
||||
if [ "$ANY" -eq 0 ]; then
|
||||
BUILD_EFI=1
|
||||
BUILD_LEGACY=1
|
||||
BUILD_ISO=1
|
||||
fi
|
||||
|
||||
# ---- пути ----
|
||||
ROOT="/root/ipxe"
|
||||
SRC="$ROOT/src"
|
||||
FILE="$SRC/start.ipxe"
|
||||
CONFIG="$SRC/config/general.h"
|
||||
PATCH_FILE="/develop/certtrust.patch"
|
||||
|
||||
# ---- проверки ----
|
||||
[ -d "$SRC" ] || { echo "Отсутствует каталог: $SRC" >&2; exit 1; }
|
||||
command -v nano >/dev/null 2>&1 || { echo "Требуется nano" >&2; exit 1; }
|
||||
mkdir -p "$(dirname "$FILE")" "$(dirname "$CONFIG")"
|
||||
|
||||
# ---- запись шаблона start.ipxe ----
|
||||
cat >"$FILE" <<'IPXE'
|
||||
#!ipxe
|
||||
|
||||
:start
|
||||
dhcp && goto next || prompt --key s --timeout 3000 Press "s" for the iPXE command line... && shell || goto start
|
||||
|
||||
:next
|
||||
chain tftp://${next-server}/boot.ipxe
|
||||
IPXE
|
||||
|
||||
# ---- предварительная настройка config.general.h (по флагам) ----
|
||||
[ -f "$CONFIG" ] || : > "$CONFIG"
|
||||
|
||||
if [ "$OPT_DEFAULT" -eq 1 ]; then
|
||||
echo "Включение опций по умолчанию: $CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*PING_CMD|#define PING_CMD|' "$CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*IPSTAT_CMD|#define IPSTAT_CMD|' "$CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*CONSOLE_CMD|#define CONSOLE_CMD|' "$CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*REBOOT_CMD|#define REBOOT_CMD|' "$CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*POWEROFF|#define POWEROFF|' "$CONFIG"
|
||||
fi
|
||||
|
||||
if [ "$OPT_PATCH" -eq 1 ]; then
|
||||
[ -f "$PATCH_FILE" ] || { echo "Отсутствует патч: $PATCH_FILE" >&2; exit 1; }
|
||||
command -v git >/dev/null 2>&1 || { echo "Требуется git для применения патча" >&2; exit 1; }
|
||||
echo "Применение патча (git apply -p1): $PATCH_FILE -> $ROOT"
|
||||
git -C "$ROOT" apply -p1 --whitespace=nowarn "$PATCH_FILE"
|
||||
|
||||
echo "Активация HTTPS и CERT_CMD: $CONFIG"
|
||||
sed -i 's|#undef[[:space:]]*DOWNLOAD_PROTO_HTTPS|#define DOWNLOAD_PROTO_HTTPS|' "$CONFIG"
|
||||
sed -i 's|//[[:space:]]*#define[[:space:]]*CERT_CMD|#define CERT_CMD|' "$CONFIG"
|
||||
fi
|
||||
|
||||
# ---- редактирование ----
|
||||
echo "Открытие в nano: $FILE"
|
||||
nano "$FILE"
|
||||
[ -f "$FILE" ] || { echo "Отсутствует файл: $FILE; сборка прервана" >&2; exit 1; }
|
||||
|
||||
echo "Открытие в nano: $CONFIG"
|
||||
nano "$CONFIG"
|
||||
|
||||
# ---- сборка ----
|
||||
cd "$SRC"
|
||||
|
||||
if [ "$BUILD_LEGACY" -eq 1 ]; then
|
||||
echo "Сборка: legacy/PCBIOS -> bin-i386-pcbios/undionly.kpxe"
|
||||
make -j"$(jobs_n)" bin-i386-pcbios/undionly.kpxe EMBED=start.ipxe
|
||||
fi
|
||||
|
||||
if [ "$BUILD_EFI" -eq 1 ]; then
|
||||
echo "Сборка: x86_64 EFI -> bin-x86_64-efi/ipxe.efi"
|
||||
make -j"$(jobs_n)" bin-x86_64-efi/ipxe.efi EMBED=start.ipxe
|
||||
fi
|
||||
|
||||
if [ "$BUILD_ISO" -eq 1 ]; then
|
||||
echo "Сборка: ISO -> bin/ipxe.iso"
|
||||
make -j"$(jobs_n)" bin/ipxe.iso EMBED=start.ipxe
|
||||
fi
|
||||
|
||||
echo "Готово."
|
||||
EOSH
|
||||
chmod +x "$MNT_DIR/develop/build.sh"
|
||||
|
||||
cat >"$MNT_DIR/develop/certtrust.patch" <<'EOPATCH'
|
||||
diff --git a/src/crypto/rootcert.c b/src/crypto/rootcert.c
|
||||
index b198c1d95..ad884173a 100644
|
||||
--- a/src/crypto/rootcert.c
|
||||
+++ b/src/crypto/rootcert.c
|
||||
@@ -24,6 +24,7 @@
|
||||
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
|
||||
|
||||
#include <stdlib.h>
|
||||
+#include <errno.h>
|
||||
#include <ipxe/crypto.h>
|
||||
#include <ipxe/sha256.h>
|
||||
#include <ipxe/x509.h>
|
||||
@@ -31,6 +32,11 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
|
||||
#include <ipxe/dhcp.h>
|
||||
#include <ipxe/init.h>
|
||||
#include <ipxe/rootcert.h>
|
||||
+#include <ipxe/malloc.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+#undef ERRFILE
|
||||
+#define ERRFILE ERRFILE_cert_cmd
|
||||
|
||||
/** @file
|
||||
*
|
||||
@@ -82,6 +88,58 @@ struct x509_root root_certificates = {
|
||||
.fingerprints = fingerprints,
|
||||
};
|
||||
|
||||
+/**
|
||||
+ * Add a fingerprint to the root certificate store from a certificate
|
||||
+ *
|
||||
+ * @v cert X.509 certificate
|
||||
+ * @ret rc Return status code
|
||||
+ */
|
||||
+int rootcert_add_fingerprint_from_cert ( struct x509_certificate *cert ) {
|
||||
+ uint8_t fingerprint[FINGERPRINT_LEN];
|
||||
+ uint8_t *new_fingerprints;
|
||||
+ size_t new_count;
|
||||
+ size_t new_len;
|
||||
+ void *ctx;
|
||||
+ struct sha256_digest digest;
|
||||
+
|
||||
+ /* Allocate context */
|
||||
+ ctx = malloc ( sha256_algorithm.ctxsize );
|
||||
+ if ( ! ctx ) {
|
||||
+ DBGC ( &root_certificates, "ROOTCERT could not allocate context\n" );
|
||||
+ return -ENOMEM;
|
||||
+ }
|
||||
+
|
||||
+ /* Compute SHA256 fingerprint of the certificate */
|
||||
+ digest_init ( &sha256_algorithm, ctx );
|
||||
+ digest_update ( &sha256_algorithm, ctx, cert->raw.data, cert->raw.len );
|
||||
+ digest_final ( &sha256_algorithm, ctx, &digest );
|
||||
+ free ( ctx );
|
||||
+
|
||||
+ /* Copy digest to fingerprint array */
|
||||
+ memcpy ( fingerprint, &digest, FINGERPRINT_LEN );
|
||||
+
|
||||
+ /* Allocate new fingerprints array */
|
||||
+ new_count = root_certificates.count + 1;
|
||||
+ new_len = new_count * FINGERPRINT_LEN;
|
||||
+ new_fingerprints = realloc ( ( void * ) root_certificates.fingerprints, new_len );
|
||||
+ if ( ! new_fingerprints ) {
|
||||
+ DBGC ( &root_certificates, "ROOTCERT could not allocate memory\n" );
|
||||
+ return -ENOMEM;
|
||||
+ }
|
||||
+
|
||||
+ /* Append new fingerprint */
|
||||
+ memcpy ( new_fingerprints + ( root_certificates.count * FINGERPRINT_LEN ),
|
||||
+ fingerprint, FINGERPRINT_LEN );
|
||||
+ root_certificates.fingerprints = new_fingerprints;
|
||||
+ root_certificates.count = new_count;
|
||||
+
|
||||
+ DBGC ( &root_certificates, "ROOTCERT added fingerprint from cert, total %d:\n",
|
||||
+ root_certificates.count );
|
||||
+ DBGC_HDA ( &root_certificates, 0, root_certificates.fingerprints, new_len );
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
/**
|
||||
* Initialise root certificate
|
||||
*
|
||||
@@ -113,6 +171,10 @@ static void rootcert_init ( void ) {
|
||||
*/
|
||||
if ( ( len = fetch_raw_setting_copy ( NULL, &trust_setting,
|
||||
&external ) ) >= 0 ) {
|
||||
+ if ( root_certificates.fingerprints != fingerprints ) {
|
||||
+ /* Free existing external fingerprints if they exist */
|
||||
+ free ( ( void * ) root_certificates.fingerprints );
|
||||
+ }
|
||||
root_certificates.fingerprints = external;
|
||||
root_certificates.count = ( len / FINGERPRINT_LEN );
|
||||
}
|
||||
diff --git a/src/hci/commands/cert_cmd.c b/src/hci/commands/cert_cmd.c
|
||||
index efa4c3c12..de6694e69 100644
|
||||
--- a/src/hci/commands/cert_cmd.c
|
||||
+++ b/src/hci/commands/cert_cmd.c
|
||||
@@ -34,6 +34,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
|
||||
#include <ipxe/parseopt.h>
|
||||
#include <usr/imgmgmt.h>
|
||||
#include <usr/certmgmt.h>
|
||||
+#include <ipxe/rootcert.h>
|
||||
|
||||
/** @file
|
||||
*
|
||||
@@ -209,6 +210,36 @@ static int certstat_payload ( struct x509_certificate *cert ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * "certtrust" payload
|
||||
+ *
|
||||
+ * Делает сертификат доверенным (добавляет его отпечаток в root_certificates)
|
||||
+ */
|
||||
+static int certtrust_payload ( struct x509_certificate *cert ) {
|
||||
+ int rc = rootcert_add_fingerprint_from_cert ( cert );
|
||||
+ if ( rc != 0 ) {
|
||||
+ printf ( "Could not add trust: %s\n", strerror ( rc ) );
|
||||
+ return rc;
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+/** "certtrust" command descriptor
|
||||
+ *
|
||||
+ * Принимает либо <uri|image>, либо ищет по --subject в certstore,
|
||||
+ * как и прочие cert<xxx> команды.
|
||||
+ */
|
||||
+static struct cert_command_descriptor certtrust_cmd =
|
||||
+ CERT_COMMAND_DESC ( struct cert_options, opts.certstore, 0, 1,
|
||||
+ "[<uri|image>]", certtrust_payload );
|
||||
+
|
||||
+/**
|
||||
+ * The "certtrust" command
|
||||
+ */
|
||||
+static int certtrust_exec ( int argc, char **argv ) {
|
||||
+ return cert_exec ( argc, argv, &certtrust_cmd );
|
||||
+}
|
||||
+
|
||||
/** "certstat" command descriptor */
|
||||
static struct cert_command_descriptor certstat_cmd =
|
||||
CERT_COMMAND_DESC ( struct cert_options, opts.certstat, 0, 0, NULL,
|
||||
@@ -292,3 +323,4 @@ static int certfree_exec ( int argc, char **argv ) {
|
||||
COMMAND ( certstat, certstat_exec );
|
||||
COMMAND ( certstore, certstore_exec );
|
||||
COMMAND ( certfree, certfree_exec );
|
||||
+COMMAND ( certtrust, certtrust_exec );
|
||||
diff --git a/src/include/ipxe/rootcert.h b/src/include/ipxe/rootcert.h
|
||||
index d1a69723d..9768b2ecb 100644
|
||||
--- a/src/include/ipxe/rootcert.h
|
||||
+++ b/src/include/ipxe/rootcert.h
|
||||
@@ -13,5 +13,6 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
|
||||
|
||||
extern const int allow_trust_override;
|
||||
extern struct x509_root root_certificates;
|
||||
+extern int rootcert_add_fingerprint_from_cert ( struct x509_certificate *cert );
|
||||
|
||||
#endif /* _IPXE_ROOTCERT_H */
|
||||
EOPATCH
|
||||
|
||||
mount_chroot_binds "$MNT_DIR"
|
||||
if [ "$bootstrap" -eq 1 ]; then
|
||||
bootstrap_in_chroot "$MNT_DIR"
|
||||
else
|
||||
log "пропуск этапа bootstrap"
|
||||
fi
|
||||
umount_chroot_binds "$MNT_DIR"
|
||||
|
||||
sync
|
||||
log "завершение: образ готов — $img"
|
||||
}
|
||||
|
||||
# ---------- chroot ----------
|
||||
cmd_chroot() {
|
||||
local img="$DEFAULT_IMG" debug=0 run_cmd=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--img) img="${2:?}"; shift 2;;
|
||||
--debug) debug=1; shift;;
|
||||
--) shift; run_cmd="$*"; break;;
|
||||
-h|--help) echo "usage: $0 image chroot [--img PATH] [--debug] [-- CMD ...]"; exit 0;;
|
||||
*) die "неизвестный аргумент: $1";;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$debug" -eq 1 ] && set -x
|
||||
|
||||
need_root
|
||||
require_host_tools
|
||||
[ -f "$img" ] || die "образ не найден: $img"
|
||||
|
||||
log "назначение loop-устройства"
|
||||
LOOPDEV="$(losetup -f -P --show "$img")"
|
||||
PART="${LOOPDEV}p1"
|
||||
command -v partprobe >/dev/null 2>&1 && partprobe "$LOOPDEV" || true
|
||||
command -v partx >/dev/null 2>&1 && partx -u "$LOOPDEV" || true
|
||||
wait_for_blk "$PART" || die "отсутствие раздела: $PART"
|
||||
|
||||
MNT_DIR="$(mktemp -d -p /mnt alpine-chroot.XXXXXX || mktemp -d)"
|
||||
cleanup() {
|
||||
set +e
|
||||
if [ -n "${MNT_DIR:-}" ]; then
|
||||
umount_chroot_binds "$MNT_DIR" || true
|
||||
umount_if "$MNT_DIR" || true
|
||||
rmdir "$MNT_DIR" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${LOOPDEV:-}" ]; then
|
||||
losetup -d "$LOOPDEV" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "монтирование $PART -> $MNT_DIR"
|
||||
mount "$PART" "$MNT_DIR"
|
||||
mount_chroot_binds "$MNT_DIR"
|
||||
|
||||
mkdir -p "$MNT_DIR/etc"
|
||||
printf "%s\n" "nameserver 8.8.8.8" > "$MNT_DIR/etc/resolv.conf"
|
||||
|
||||
if [ -n "$run_cmd" ]; then
|
||||
log "выполнение команды в chroot"
|
||||
chroot "$MNT_DIR" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash -lc "$run_cmd"
|
||||
else
|
||||
log "запуск интерактивной оболочки /bin/ash"
|
||||
chroot "$MNT_DIR" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash --login || true
|
||||
fi
|
||||
|
||||
log "выход из chroot: очистка"
|
||||
}
|
||||
|
||||
# ---------- overlay (эфемерный RW поверх RO) ----------
|
||||
cmd_overlay() {
|
||||
local img="$DEFAULT_IMG" debug=0 run_cmd="" overlay_size_mb=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--img) img="${2:?}"; shift 2;;
|
||||
--overlay-size-mb) overlay_size_mb="${2:?}"; shift 2;;
|
||||
--debug) debug=1; shift;;
|
||||
--) shift; run_cmd="$*"; break;;
|
||||
-h|--help) echo "usage: $0 image overlay [--img PATH] [--overlay-size-mb N] [--debug] [-- CMD ...]"; exit 0;;
|
||||
*) die "неизвестный аргумент: $1";;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$debug" -eq 1 ] && set -x
|
||||
need_root
|
||||
require_host_tools
|
||||
require_overlay_support
|
||||
[ -f "$img" ] || die "образ не найден: $img"
|
||||
|
||||
log "назначение loop-устройства"
|
||||
LOOPDEV="$(losetup -f -P --show "$img")"
|
||||
PART="${LOOPDEV}p1"
|
||||
command -v partprobe >/dev/null 2>&1 && partprobe "$LOOPDEV" || true
|
||||
command -v partx >/dev/null 2>&1 && partx -u "$LOOPDEV" || true
|
||||
wait_for_blk "$PART" || die "отсутствие раздела: $PART"
|
||||
|
||||
MNT_LOWER="$(mktemp -d -p /mnt alpine-lower.XXXXXX || mktemp -d)"
|
||||
MNT_MERGED="$(mktemp -d -p /mnt alpine-merged.XXXXXX || mktemp -d)"
|
||||
OVL_BASE="$(mktemp -d -p /mnt alpine-ovl.XXXXXX || mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
set +e
|
||||
umount_chroot_binds "$MNT_MERGED" 2>/dev/null || true
|
||||
umount_if "$MNT_MERGED" || true
|
||||
umount_if "$MNT_LOWER" || true
|
||||
umount_if "$OVL_BASE" || true
|
||||
rmdir "$MNT_MERGED" "$MNT_LOWER" "$OVL_BASE" 2>/dev/null || true
|
||||
if [ -n "${LOOPDEV:-}" ]; then
|
||||
losetup -d "$LOOPDEV" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "монтирование нижнего слоя (RO)"
|
||||
mount -o ro,noload "$PART" "$MNT_LOWER" 2>/dev/null || mount -o ro "$PART" "$MNT_LOWER"
|
||||
|
||||
log "подготовка overlay workspace (tmpfs)"
|
||||
if [ -n "$overlay_size_mb" ]; then
|
||||
[[ "$overlay_size_mb" =~ ^[0-9]+$ ]] || die "--overlay-size-mb: число"
|
||||
mount -t tmpfs -o "size=${overlay_size_mb}M,nosuid,nodev" tmpfs "$OVL_BASE"
|
||||
else
|
||||
mount -t tmpfs -o "nosuid,nodev" tmpfs "$OVL_BASE"
|
||||
fi
|
||||
mkdir -p "$OVL_BASE/upper" "$OVL_BASE/work"
|
||||
|
||||
log "монтирование overlay (RW поверх RO)"
|
||||
mount -t overlay overlay -o "lowerdir=$MNT_LOWER,upperdir=$OVL_BASE/upper,workdir=$OVL_BASE/work" "$MNT_MERGED"
|
||||
|
||||
mount_chroot_binds "$MNT_MERGED"
|
||||
|
||||
mkdir -p "$MNT_MERGED/etc"
|
||||
printf "%s\n" "nameserver 8.8.8.8" > "$MNT_MERGED/etc/resolv.conf"
|
||||
|
||||
if [ -n "$run_cmd" ]; then
|
||||
log "выполнение команды в chroot (overlay)"
|
||||
chroot "$MNT_MERGED" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash -lc "$run_cmd"
|
||||
else
|
||||
log "запуск интерактивной оболочки /bin/ash (overlay)"
|
||||
chroot "$MNT_MERGED" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash --login || true
|
||||
fi
|
||||
|
||||
log "выход из chroot (overlay): очистка"
|
||||
}
|
||||
|
||||
# ---------- build (overlay + /develop/build.sh + копирование артефактов) ----------
|
||||
cmd_build() {
|
||||
local img="$DEFAULT_IMG" debug=0 overlay_size_mb="" out_dir="."
|
||||
local build_flags=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--img) img="${2:?}"; shift 2;;
|
||||
--overlay-size-mb) overlay_size_mb="${2:?}"; shift 2;;
|
||||
--out-dir) out_dir="${2:?}"; shift 2;;
|
||||
--debug) debug=1; shift;;
|
||||
--) shift; build_flags="$*"; break;;
|
||||
-h|--help) echo "usage: $0 build [--img PATH] [--overlay-size-mb N] [--out-dir DIR] [--debug] [-- FLAGS для /develop/build.sh]"; exit 0;;
|
||||
*) die "неизвестный аргумент: $1";;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$debug" -eq 1 ] && set -x
|
||||
need_root
|
||||
require_host_tools
|
||||
require_overlay_support
|
||||
[ -f "$img" ] || die "образ не найден: $img"
|
||||
|
||||
log "назначение loop-устройства"
|
||||
LOOPDEV="$(losetup -f -P --show "$img")"
|
||||
PART="${LOOPDEV}p1"
|
||||
command -v partprobe >/dev/null 2>&1 && partprobe "$LOOPDEV" || true
|
||||
command -v partx >/dev/null 2>&1 && partx -u "$LOOPDEV" || true
|
||||
wait_for_blk "$PART" || die "отсутствие раздела: $PART"
|
||||
|
||||
MNT_LOWER="$(mktemp -d -p /mnt alpine-lower.XXXXXX || mktemp -d)"
|
||||
MNT_MERGED="$(mktemp -d -p /mnt alpine-merged.XXXXXX || mktemp -d)"
|
||||
OVL_BASE="$(mktemp -d -p /mnt alpine-ovl.XXXXXX || mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
set +e
|
||||
umount_chroot_binds "$MNT_MERGED" 2>/dev/null || true
|
||||
umount_if "$MNT_MERGED" || true
|
||||
umount_if "$MNT_LOWER" || true
|
||||
umount_if "$OVL_BASE" || true
|
||||
rmdir "$MNT_MERGED" "$MNT_LOWER" "$OVL_BASE" 2>/dev/null || true
|
||||
if [ -n "${LOOPDEV:-}" ]; then
|
||||
losetup -d "$LOOPDEV" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log "монтирование нижнего слоя (RO)"
|
||||
mount -o ro,noload "$PART" "$MNT_LOWER" 2>/dev/null || mount -o ro "$PART" "$MNT_LOWER"
|
||||
|
||||
log "подготовка overlay workspace (tmpfs)"
|
||||
if [ -n "$overlay_size_mb" ]; then
|
||||
[[ "$overlay_size_mb" =~ ^[0-9]+$ ]] || die "--overlay-size-mb: число"
|
||||
mount -t tmpfs -o "size=${overlay_size_mb}M,nosuid,nodev" tmpfs "$OVL_BASE"
|
||||
else
|
||||
mount -t tmpfs -o "nosuid,nodev" tmpfs "$OVL_BASE"
|
||||
fi
|
||||
mkdir -p "$OVL_BASE/upper" "$OVL_BASE/work"
|
||||
|
||||
log "монтирование overlay (RW поверх RO)"
|
||||
mount -t overlay overlay -o "lowerdir=$MNT_LOWER,upperdir=$OVL_BASE/upper,workdir=$OVL_BASE/work" "$MNT_MERGED"
|
||||
|
||||
mount_chroot_binds "$MNT_MERGED"
|
||||
mkdir -p "$MNT_MERGED/etc"
|
||||
printf "%s\n" "nameserver 8.8.8.8" > "$MNT_MERGED/etc/resolv.conf"
|
||||
|
||||
log "выполнение /develop/build.sh в chroot (overlay)"
|
||||
set +e
|
||||
if [ -n "$build_flags" ]; then
|
||||
chroot "$MNT_MERGED" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash /develop/build.sh $build_flags
|
||||
else
|
||||
chroot "$MNT_MERGED" /usr/bin/env -i HOME=/root TERM="${TERM:-xterm-256color}" PATH=/usr/sbin:/usr/bin:/sbin:/bin /bin/ash /develop/build.sh
|
||||
fi
|
||||
CH_STATUS=$?
|
||||
set -e
|
||||
|
||||
mkdir -p "$out_dir"
|
||||
if [ "$CH_STATUS" -eq 0 ]; then
|
||||
if [ -f "$MNT_MERGED/root/ipxe/src/bin-x86_64-efi/ipxe.efi" ]; then
|
||||
log "копирование ipxe.efi"
|
||||
cp -f "$MNT_MERGED/root/ipxe/src/bin-x86_64-efi/ipxe.efi" "$out_dir/"
|
||||
fi
|
||||
if [ -f "$MNT_MERGED/root/ipxe/src/bin-i386-pcbios/undionly.kpxe" ]; then
|
||||
log "копирование undionly.kpxe"
|
||||
cp -f "$MNT_MERGED/root/ipxe/src/bin-i386-pcbios/undionly.kpxe" "$out_dir/"
|
||||
fi
|
||||
if [ -f "$MNT_MERGED/root/ipxe/src/bin/ipxe.iso" ]; then
|
||||
log "копирование ipxe.iso"
|
||||
cp -f "$MNT_MERGED/root/ipxe/src/bin/ipxe.iso" "$out_dir/"
|
||||
fi
|
||||
else
|
||||
log "сборка пропущена: код возврата $CH_STATUS"
|
||||
fi
|
||||
|
||||
log "завершение build: размонтирование и очистка"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
usage:
|
||||
$0 build [--img PATH] [--overlay-size-mb N] [--out-dir DIR] [--debug] [-- FLAGS для /develop/build.sh]
|
||||
$0 image build [--url URL] [--img PATH] [--size-mb 512..1024] [--no-bootstrap] [--force] [--debug]
|
||||
$0 image chroot [--img PATH] [--debug] [-- CMD ...]
|
||||
$0 image overlay [--img PATH] [--overlay-size-mb N] [--debug] [-- CMD ...]
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------- входная точка ----------
|
||||
cmd="${1:-}"
|
||||
shift || true
|
||||
case "$cmd" in
|
||||
build) cmd_build "$@";;
|
||||
image)
|
||||
sub="${1:-}"; shift || true
|
||||
case "$sub" in
|
||||
build) cmd_image_build "$@";;
|
||||
chroot) cmd_chroot "$@";;
|
||||
overlay) cmd_overlay "$@";;
|
||||
""|-h|--help) echo "usage: $0 image {build|chroot|overlay} [...]";;
|
||||
*) die "неизвестная подкоманда image: $sub";;
|
||||
esac
|
||||
;;
|
||||
""|-h|--help) usage;;
|
||||
*) die "неизвестная подкоманда: $cmd";;
|
||||
esac
|
Loading…
Add table
Add a link
Reference in a new issue