Add package data for GPU rent and update CLI documentation
- Added package data configuration for the 'gpu_rent' package in pyproject.toml. - Updated README.md to include usage instructions for Windows and Unix launchers. - Enhanced CLI documentation in cli.md to reflect new commands and their functionalities. - Revised setup.md to clarify installation steps and environment setup. - Improved error handling and command descriptions in the CLI implementation. - Added new functions for model version handling and flavor resolution in the codebase. - Updated state management to include additional properties for better tracking.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Scripts uploaded to the GPU VM."""
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# Idempotent gpu-rent first-boot on the VM. No Docker. Do not apt-upgrade the kernel.
|
||||
set -euo pipefail
|
||||
|
||||
SWARM_USER="${SWARM_USER:-ubuntu}"
|
||||
SWARM_ROOT="/opt/swarmui"
|
||||
DATA_ROOT="/mnt/swarm_data"
|
||||
MARKER_DATA="${DATA_ROOT}/.gpu-rent-ready"
|
||||
MARKER_BOOT="${SWARM_ROOT}/.gpu-rent-bootstrapped"
|
||||
SWARM_REPO="https://github.com/mcmonkeyprojects/SwarmUI.git"
|
||||
|
||||
log() { echo "[gpu-rent] $*"; }
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "нужен root (sudo -n bash $0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
pick_data_disk() {
|
||||
local root_src root_pk name type
|
||||
root_src="$(findmnt -n -o SOURCE / || true)"
|
||||
root_pk="$(lsblk -no PKNAME "$root_src" 2>/dev/null | head -n1 || true)"
|
||||
if [[ -z "$root_pk" && -n "$root_src" ]]; then
|
||||
root_pk="$(lsblk -no NAME "$root_src" 2>/dev/null | head -n1 | sed 's/[0-9]*$//' || true)"
|
||||
fi
|
||||
while read -r name type; do
|
||||
[[ "$type" == "disk" ]] || continue
|
||||
[[ -n "$root_pk" && "$name" == "$root_pk" ]] && continue
|
||||
if findmnt "/dev/${name}" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
echo "/dev/${name}"
|
||||
return 0
|
||||
done < <(lsblk -dn -o NAME,TYPE)
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_data_mount() {
|
||||
mkdir -p "$DATA_ROOT"
|
||||
if findmnt "$DATA_ROOT" >/dev/null 2>&1; then
|
||||
log "data volume уже на ${DATA_ROOT}"
|
||||
return 0
|
||||
fi
|
||||
local disk fstype uuid
|
||||
disk="$(pick_data_disk)" || {
|
||||
log "нет второго диска — проверь BDM data volume"
|
||||
exit 1
|
||||
}
|
||||
fstype="$(blkid -s TYPE -o value "$disk" 2>/dev/null || true)"
|
||||
if [[ -z "$fstype" ]]; then
|
||||
if [[ -e "$MARKER_DATA" ]]; then
|
||||
log "маркер есть, а FS на ${disk} нет — не mkfs, разбери вручную"
|
||||
exit 1
|
||||
fi
|
||||
log "mkfs.ext4 ${disk} (пустой data volume)"
|
||||
mkfs.ext4 -F -L swarm-data "$disk"
|
||||
fi
|
||||
mount "$disk" "$DATA_ROOT"
|
||||
uuid="$(blkid -s UUID -o value "$disk")"
|
||||
if [[ -n "$uuid" ]] && ! grep -q "UUID=${uuid}" /etc/fstab; then
|
||||
echo "UUID=${uuid} ${DATA_ROOT} ext4 defaults,nofail 0 2" >> /etc/fstab
|
||||
fi
|
||||
log "смонтирован ${disk} -> ${DATA_ROOT}"
|
||||
}
|
||||
|
||||
ensure_bind() {
|
||||
local src="$1" dst="$2"
|
||||
mkdir -p "$src" "$dst"
|
||||
if ! findmnt "$dst" >/dev/null 2>&1; then
|
||||
mount --bind "$src" "$dst"
|
||||
fi
|
||||
if ! grep -Fq " ${dst} " /etc/fstab; then
|
||||
echo "${src} ${dst} none bind,nofail 0 0" >> /etc/fstab
|
||||
fi
|
||||
}
|
||||
|
||||
log "пакеты (без upgrade ядра)"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git python3 python3-venv python3-pip python3-full curl ca-certificates
|
||||
|
||||
ensure_data_mount
|
||||
|
||||
mkdir -p \
|
||||
"${DATA_ROOT}/Models" \
|
||||
"${DATA_ROOT}/Output" \
|
||||
"${DATA_ROOT}/Data" \
|
||||
"${DATA_ROOT}/Data/Autocompletions" \
|
||||
"${DATA_ROOT}/Data/Wildcards" \
|
||||
"${DATA_ROOT}/dlbackend" \
|
||||
"${DATA_ROOT}/Extensions" \
|
||||
"${DATA_ROOT}/DLNodes" \
|
||||
"${DATA_ROOT}/CustomWorkflows"
|
||||
|
||||
if [[ ! -d "${SWARM_ROOT}/.git" ]]; then
|
||||
log "clone SwarmUI -> ${SWARM_ROOT}"
|
||||
mkdir -p "$(dirname "$SWARM_ROOT")"
|
||||
git clone --depth 1 "$SWARM_REPO" "$SWARM_ROOT"
|
||||
else
|
||||
log "SwarmUI уже в ${SWARM_ROOT}"
|
||||
fi
|
||||
|
||||
if [[ ! -x /usr/share/dotnet/dotnet && ! -x "/home/${SWARM_USER}/.dotnet/dotnet" ]]; then
|
||||
if [[ -x "${SWARM_ROOT}/launchtools/linux-dotnet-install.sh" ]]; then
|
||||
log "ставим .NET SDK (скрипт SwarmUI)"
|
||||
sudo -u "$SWARM_USER" bash "${SWARM_ROOT}/launchtools/linux-dotnet-install.sh" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p \
|
||||
"${SWARM_ROOT}/Models" \
|
||||
"${SWARM_ROOT}/Output" \
|
||||
"${SWARM_ROOT}/Data" \
|
||||
"${SWARM_ROOT}/dlbackend" \
|
||||
"${SWARM_ROOT}/src/Extensions" \
|
||||
"${SWARM_ROOT}/src/BuiltinExtensions/ComfyUIBackend/DLNodes" \
|
||||
"${SWARM_ROOT}/src/BuiltinExtensions/ComfyUIBackend/CustomWorkflows"
|
||||
|
||||
ensure_bind "${DATA_ROOT}/Models" "${SWARM_ROOT}/Models"
|
||||
ensure_bind "${DATA_ROOT}/Output" "${SWARM_ROOT}/Output"
|
||||
ensure_bind "${DATA_ROOT}/Data" "${SWARM_ROOT}/Data"
|
||||
ensure_bind "${DATA_ROOT}/dlbackend" "${SWARM_ROOT}/dlbackend"
|
||||
ensure_bind "${DATA_ROOT}/Extensions" "${SWARM_ROOT}/src/Extensions"
|
||||
ensure_bind "${DATA_ROOT}/DLNodes" "${SWARM_ROOT}/src/BuiltinExtensions/ComfyUIBackend/DLNodes"
|
||||
ensure_bind "${DATA_ROOT}/CustomWorkflows" "${SWARM_ROOT}/src/BuiltinExtensions/ComfyUIBackend/CustomWorkflows"
|
||||
|
||||
chown -R "${SWARM_USER}:${SWARM_USER}" "$DATA_ROOT" "$SWARM_ROOT"
|
||||
|
||||
cat >/etc/systemd/system/swarmui.service <<EOF
|
||||
[Unit]
|
||||
Description=SwarmUI (gpu-rent)
|
||||
After=network-online.target local-fs.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${SWARM_USER}
|
||||
Group=${SWARM_USER}
|
||||
WorkingDirectory=${SWARM_ROOT}
|
||||
Environment=HOME=/home/${SWARM_USER}
|
||||
Environment=DOTNET_ROOT=/home/${SWARM_USER}/.dotnet
|
||||
Environment=DOTNET_CLI_HOME=/home/${SWARM_USER}
|
||||
Environment=PATH=/home/${SWARM_USER}/.dotnet:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=${SWARM_ROOT}/launch-linux.sh --launch_mode none --host 127.0.0.1 --port 7801
|
||||
Restart=on-failure
|
||||
RestartSec=8
|
||||
TimeoutStartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable swarmui
|
||||
# Не стартуем UI здесь: сначала extensions / autocomplete / Civitai / push.
|
||||
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" >"$MARKER_DATA"
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" >"$MARKER_BOOT"
|
||||
chown "${SWARM_USER}:${SWARM_USER}" "$MARKER_DATA" "$MARKER_BOOT"
|
||||
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
nvidia-smi -L || true
|
||||
fi
|
||||
log "bootstrap ok (unit готов; swarmui старт после seed)"
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download Civitai files on the VM. Stdlib only. Token in a 600 file."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
TOKEN_PATH = Path("/tmp/gpu-rent-civitai.token")
|
||||
JOBS_PATH = Path("/tmp/gpu-rent-civitai-jobs.json")
|
||||
MARKER = Path("/mnt/swarm_data/.gpu-rent-models-seeded")
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as fh:
|
||||
while True:
|
||||
block = fh.read(1024 * 1024)
|
||||
if not block:
|
||||
break
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def download(url: str, dest: Path, token: str) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = dest.with_suffix(dest.suffix + ".partial")
|
||||
|
||||
class StripAuthRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
new = urllib.request.HTTPRedirectHandler.redirect_request(
|
||||
self, req, fp, code, msg, headers, newurl
|
||||
)
|
||||
if new is None:
|
||||
return None
|
||||
host = (urllib.parse.urlparse(new.full_url).hostname or "").lower()
|
||||
if host.endswith("civitai.com") or host.endswith("civitai.red") or host.endswith("civitai.green"):
|
||||
return new
|
||||
# presigned S3 / CDN: token must not leave civitai
|
||||
return urllib.request.Request(new.full_url, headers={"User-Agent": "gpu-rent/0.1"})
|
||||
|
||||
opener = urllib.request.build_opener(StripAuthRedirect)
|
||||
req = urllib.request.Request(
|
||||
url, headers={"Authorization": f"Bearer {token}", "User-Agent": "gpu-rent/0.1"}
|
||||
)
|
||||
with opener.open(req, timeout=600) as response, partial.open("wb") as out:
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
out.write(chunk)
|
||||
partial.replace(dest)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not TOKEN_PATH.is_file():
|
||||
print("нет токена", file=sys.stderr)
|
||||
return 1
|
||||
token = TOKEN_PATH.read_text(encoding="utf-8").strip()
|
||||
jobs = json.loads(JOBS_PATH.read_text(encoding="utf-8"))
|
||||
failed = 0
|
||||
for job in jobs:
|
||||
dest = Path(job["dest"])
|
||||
expect = (job.get("sha256") or "").lower()
|
||||
if dest.is_file() and expect and sha256_path(dest).lower() == expect:
|
||||
print(f"skip {dest}")
|
||||
continue
|
||||
try:
|
||||
print(f"download {dest.name}")
|
||||
download(job["url"], dest, token)
|
||||
if expect:
|
||||
got = sha256_path(dest).lower()
|
||||
if got != expect:
|
||||
dest.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"sha256 {got} != {expect}")
|
||||
for extra_name, extra_text in (job.get("sidecars") or {}).items():
|
||||
extra = dest.parent / extra_name
|
||||
extra.write_text(extra_text, encoding="utf-8")
|
||||
except Exception as exc:
|
||||
failed += 1
|
||||
print(f"FAIL {dest}: {exc}", file=sys.stderr)
|
||||
TOKEN_PATH.unlink(missing_ok=True)
|
||||
if failed:
|
||||
return 1
|
||||
MARKER.parent.mkdir(parents=True, exist_ok=True)
|
||||
MARKER.write_text("ok\n", encoding="utf-8")
|
||||
print("civitai seed ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Clone git extensions on the VM. Stdlib only. Token file optional."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
TOKEN_PATH = Path("/tmp/gpu-rent-git.token")
|
||||
JOBS_PATH = Path("/tmp/gpu-rent-ext.json")
|
||||
MARKER = Path("/mnt/swarm_data/.gpu-rent-extensions-seeded")
|
||||
|
||||
|
||||
def strip_auth(url: str) -> str:
|
||||
parts = urlsplit(url)
|
||||
host = parts.hostname or ""
|
||||
if parts.port:
|
||||
host = f"{host}:{parts.port}"
|
||||
return urlunsplit((parts.scheme, host, parts.path, parts.query, parts.fragment))
|
||||
|
||||
|
||||
def with_token(url: str, token: str) -> str:
|
||||
if not token:
|
||||
return url
|
||||
if url.startswith("https://github.com/"):
|
||||
return "https://x-access-token:" + token + "@github.com/" + url[len("https://github.com/") :]
|
||||
if url.startswith("https://gitlab.com/"):
|
||||
return "https://oauth2:" + token + "@gitlab.com/" + url[len("https://gitlab.com/") :]
|
||||
return url
|
||||
|
||||
|
||||
def run(argv: list[str], cwd: str | None = None) -> None:
|
||||
subprocess.check_call(argv, cwd=cwd)
|
||||
|
||||
|
||||
def is_sha(ref: str) -> bool:
|
||||
ref = ref.strip()
|
||||
return len(ref) == 40 and all(c in "0123456789abcdefABCDEF" for c in ref)
|
||||
|
||||
|
||||
def clone_one(job: dict, token: str) -> None:
|
||||
dest = Path(job["dest"])
|
||||
url = job["url"].strip()
|
||||
ref = (job.get("ref") or "main").strip()
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
authed = with_token(url, token)
|
||||
if dest.is_dir() and (dest / ".git").is_dir():
|
||||
origin = subprocess.check_output(["git", "-C", str(dest), "remote", "get-url", "origin"], text=True).strip()
|
||||
if strip_auth(origin) != strip_auth(url):
|
||||
print(f"FAIL origin mismatch {dest}: {origin} != {url}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
run(["git", "-C", str(dest), "fetch", "--recurse-submodules", "origin"])
|
||||
run(["git", "-C", str(dest), "checkout", ref])
|
||||
print(f"updated {dest}")
|
||||
return
|
||||
if dest.exists():
|
||||
print(f"FAIL {dest} exists but is not a git repo", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
if is_sha(ref):
|
||||
run(["git", "clone", "--recurse-submodules", authed, str(dest)])
|
||||
run(["git", "-C", str(dest), "fetch", "origin", ref])
|
||||
run(["git", "-C", str(dest), "checkout", ref])
|
||||
else:
|
||||
try:
|
||||
run(["git", "clone", "--recurse-submodules", "--depth", "1", "--branch", ref, authed, str(dest)])
|
||||
except subprocess.CalledProcessError:
|
||||
run(["git", "clone", "--recurse-submodules", authed, str(dest)])
|
||||
run(["git", "-C", str(dest), "checkout", ref])
|
||||
print(f"cloned {dest}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
token = TOKEN_PATH.read_text(encoding="utf-8").strip() if TOKEN_PATH.is_file() else ""
|
||||
jobs = json.loads(JOBS_PATH.read_text(encoding="utf-8"))
|
||||
failed = 0
|
||||
for job in jobs:
|
||||
try:
|
||||
clone_one(job, token)
|
||||
except Exception as exc:
|
||||
failed += 1
|
||||
print(f"FAIL {job.get('dest')}: {exc}", file=sys.stderr)
|
||||
if TOKEN_PATH.is_file():
|
||||
TOKEN_PATH.unlink()
|
||||
if failed:
|
||||
return 1
|
||||
MARKER.parent.mkdir(parents=True, exist_ok=True)
|
||||
MARKER.write_text("ok\n", encoding="utf-8")
|
||||
print("extensions ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user