Compare commits
2 Commits
d12c7af664
...
01a9f61ca5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a9f61ca5 | ||
|
|
41b7e95c96 |
@@ -335,9 +335,44 @@ def api_backups():
|
|||||||
return jsonify({"backups": list_backups(), "backups_dir": str(BACKUPS_DIR)})
|
return jsonify({"backups": list_backups(), "backups_dir": str(BACKUPS_DIR)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/backups/upload", methods=["POST"])
|
||||||
|
def api_backups_upload():
|
||||||
|
"""Upload an image file from the dashboard (multipart form)."""
|
||||||
|
if "file" not in request.files and "image" not in request.files:
|
||||||
|
return jsonify({"ok": False, "error": "no file in request (use field 'file' or 'image')"}), 400
|
||||||
|
f = request.files.get("file") or request.files.get("image")
|
||||||
|
if not f or not f.filename:
|
||||||
|
return jsonify({"ok": False, "error": "no file selected"}), 400
|
||||||
|
base = (f.filename.rsplit(".", 1)[0] if "." in f.filename else f.filename).strip() or "upload"
|
||||||
|
safe_base = re.sub(r"[^\w\-.]", "_", base)[:80]
|
||||||
|
if not safe_base:
|
||||||
|
safe_base = "upload"
|
||||||
|
ext = ""
|
||||||
|
if f.filename.lower().endswith(".img.xz"):
|
||||||
|
ext = ".img.xz"
|
||||||
|
elif f.filename.lower().endswith(".img.gz"):
|
||||||
|
ext = ".img.gz"
|
||||||
|
elif f.filename.lower().endswith(".img"):
|
||||||
|
ext = ".img"
|
||||||
|
else:
|
||||||
|
ext = ".img"
|
||||||
|
name = f"{safe_base}-{int(time.time())}{ext}"
|
||||||
|
if not _safe_backup_name(name):
|
||||||
|
name = f"upload-{int(time.time())}.img"
|
||||||
|
path = BACKUPS_DIR / name
|
||||||
|
try:
|
||||||
|
BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
f.save(str(path))
|
||||||
|
return jsonify({"ok": True, "name": name, "message": f"Uploaded {name}"})
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
if path.exists():
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/backups/<path:name>/set-as-golden", methods=["POST"])
|
@app.route("/api/backups/<path:name>/set-as-golden", methods=["POST"])
|
||||||
def api_backup_set_as_golden(name):
|
def api_backup_set_as_golden(name):
|
||||||
"""Copy this backup to golden.img so it becomes the image used for Deploy."""
|
"""Use this backup as the golden image (symlink when in backups dir to avoid copying)."""
|
||||||
if not _safe_backup_name(name):
|
if not _safe_backup_name(name):
|
||||||
return jsonify({"ok": False, "error": "invalid backup name"}), 400
|
return jsonify({"ok": False, "error": "invalid backup name"}), 400
|
||||||
path = BACKUPS_DIR / name
|
path = BACKUPS_DIR / name
|
||||||
@@ -345,6 +380,16 @@ def api_backup_set_as_golden(name):
|
|||||||
return jsonify({"ok": False, "error": "backup not found"}), 404
|
return jsonify({"ok": False, "error": "backup not found"}), 404
|
||||||
try:
|
try:
|
||||||
BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
|
BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
GOLDEN_IMAGE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if GOLDEN_IMAGE.exists():
|
||||||
|
GOLDEN_IMAGE.unlink()
|
||||||
|
# Symlink to the backup so we use the same file instead of duplicating
|
||||||
|
path_resolved = path.resolve()
|
||||||
|
try:
|
||||||
|
path_resolved.relative_to(BACKUPS_DIR.resolve())
|
||||||
|
os.symlink(path_resolved, GOLDEN_IMAGE)
|
||||||
|
except ValueError:
|
||||||
|
# Backup not under BACKUPS_DIR (shouldn't happen for list); fall back to copy
|
||||||
shutil.copy2(path, GOLDEN_IMAGE)
|
shutil.copy2(path, GOLDEN_IMAGE)
|
||||||
return jsonify({"ok": True, "message": f"Golden image set from {name}"})
|
return jsonify({"ok": True, "message": f"Golden image set from {name}"})
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
@@ -458,6 +503,30 @@ def api_backup_update(name):
|
|||||||
return jsonify({"ok": True, "name": name})
|
return jsonify({"ok": True, "name": name})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/backups/<path:name>", methods=["DELETE"])
|
||||||
|
def api_backup_delete(name):
|
||||||
|
"""Delete a backup file. If it is the current golden image (symlink), the golden link is removed."""
|
||||||
|
if not _safe_backup_name(name):
|
||||||
|
return jsonify({"ok": False, "error": "invalid backup name"}), 400
|
||||||
|
path = BACKUPS_DIR / name
|
||||||
|
if not path.is_file():
|
||||||
|
return jsonify({"ok": False, "error": "backup not found"}), 404
|
||||||
|
try:
|
||||||
|
if GOLDEN_IMAGE.exists() and GOLDEN_IMAGE.is_symlink():
|
||||||
|
try:
|
||||||
|
if (GOLDEN_IMAGE.resolve() == path.resolve()):
|
||||||
|
GOLDEN_IMAGE.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
path.unlink()
|
||||||
|
meta = _load_backups_meta()
|
||||||
|
meta.pop(name, None)
|
||||||
|
_save_backups_meta(meta)
|
||||||
|
return jsonify({"ok": True, "message": f"Deleted {name}"})
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/backups/<path:name>", methods=["GET"])
|
@app.route("/api/backups/<path:name>", methods=["GET"])
|
||||||
def api_backup_download(name):
|
def api_backup_download(name):
|
||||||
if not _safe_backup_name(name):
|
if not _safe_backup_name(name):
|
||||||
|
|||||||
@@ -399,7 +399,11 @@
|
|||||||
|
|
||||||
<!-- 3. Saved backups -->
|
<!-- 3. Saved backups -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Saved backups <button type="button" class="btn btn-outline btn-sm" id="refreshBackupsBtn" title="Reload list">Refresh</button></h2>
|
<h2 class="section-title">Saved backups
|
||||||
|
<button type="button" class="btn btn-outline btn-sm" id="refreshBackupsBtn" title="Reload list">Refresh</button>
|
||||||
|
<button type="button" class="btn btn-outline btn-sm" id="uploadImageBtn" title="Upload an image file">Upload image</button>
|
||||||
|
<input type="file" id="uploadImageInput" accept=".img,.img.gz,.img.xz,image/*" style="display:none;" />
|
||||||
|
</h2>
|
||||||
<p id="goldenHint" class="backups-mono" style="margin-bottom:0.25rem;font-size:0.8rem;"></p>
|
<p id="goldenHint" class="backups-mono" style="margin-bottom:0.25rem;font-size:0.8rem;"></p>
|
||||||
<p id="backupsDirHint" class="backups-mono" style="margin-bottom:0.75rem;font-size:0.75rem;color:var(--muted);"></p>
|
<p id="backupsDirHint" class="backups-mono" style="margin-bottom:0.75rem;font-size:0.75rem;color:var(--muted);"></p>
|
||||||
<table class="backups-table" id="backupsTable">
|
<table class="backups-table" id="backupsTable">
|
||||||
@@ -617,7 +621,8 @@
|
|||||||
compressBtn +
|
compressBtn +
|
||||||
'<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
|
'<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
|
||||||
'<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
|
'<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
|
||||||
'<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a>' +
|
'<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a> ' +
|
||||||
|
'<button type="button" class="btn btn-outline btn-sm delete-backup-btn" data-name="' + escapeHtml(b.name) + '" title="Delete this backup">Delete</button>' +
|
||||||
'</td>';
|
'</td>';
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -626,6 +631,25 @@
|
|||||||
bindRenameFile();
|
bindRenameFile();
|
||||||
bindShrink();
|
bindShrink();
|
||||||
bindCompress();
|
bindCompress();
|
||||||
|
bindDeleteBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDeleteBackup() {
|
||||||
|
document.querySelectorAll('.delete-backup-btn').forEach(function(btn) {
|
||||||
|
btn.onclick = function() {
|
||||||
|
const name = btn.getAttribute('data-name');
|
||||||
|
if (!confirm('Delete this backup? This cannot be undone.\n\n' + name)) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
fetch('/api/backups/' + encodeURIComponent(name), { method: 'DELETE' })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.ok) { fetchBackups(); fetchGoldenInfo(); }
|
||||||
|
else alert(data.error || 'Failed');
|
||||||
|
})
|
||||||
|
.catch(function() { alert('Request failed'); })
|
||||||
|
.finally(function() { btn.disabled = false; });
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindRenameFile() {
|
function bindRenameFile() {
|
||||||
@@ -901,6 +925,26 @@
|
|||||||
});
|
});
|
||||||
var refreshBackupsBtn = document.getElementById('refreshBackupsBtn');
|
var refreshBackupsBtn = document.getElementById('refreshBackupsBtn');
|
||||||
if (refreshBackupsBtn) refreshBackupsBtn.onclick = function() { fetchBackups(); fetchGoldenInfo(); };
|
if (refreshBackupsBtn) refreshBackupsBtn.onclick = function() { fetchBackups(); fetchGoldenInfo(); };
|
||||||
|
var uploadImageBtn = document.getElementById('uploadImageBtn');
|
||||||
|
var uploadImageInput = document.getElementById('uploadImageInput');
|
||||||
|
if (uploadImageBtn && uploadImageInput) {
|
||||||
|
uploadImageBtn.onclick = function() { uploadImageInput.click(); };
|
||||||
|
uploadImageInput.onchange = function() {
|
||||||
|
var file = uploadImageInput.files && uploadImageInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('file', file);
|
||||||
|
uploadImageBtn.disabled = true;
|
||||||
|
fetch('/api/backups/upload', { method: 'POST', body: fd })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
if (d.ok) { fetchBackups(); fetchGoldenInfo(); alert('Uploaded: ' + (d.name || file.name)); }
|
||||||
|
else alert(d.error || 'Upload failed');
|
||||||
|
})
|
||||||
|
.catch(function() { alert('Upload failed'); })
|
||||||
|
.finally(function() { uploadImageBtn.disabled = false; uploadImageInput.value = ''; });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fetchStatus();
|
fetchStatus();
|
||||||
fetchLog();
|
fetchLog();
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ mkdir -p "$BACKUPS_DIR"
|
|||||||
cp "$IMG_FILE" "$OUT_PATH"
|
cp "$IMG_FILE" "$OUT_PATH"
|
||||||
|
|
||||||
if [[ "$SET_AS_GOLDEN" == "1" ]]; then
|
if [[ "$SET_AS_GOLDEN" == "1" ]]; then
|
||||||
cp "$OUT_PATH" "$GOLDEN_IMAGE"
|
rm -f "$GOLDEN_IMAGE"
|
||||||
|
ln -sf "$OUT_PATH" "$GOLDEN_IMAGE"
|
||||||
write_status "done" "Built $OUT_NAME and set as golden image." "$OUT_NAME" ""
|
write_status "done" "Built $OUT_NAME and set as golden image." "$OUT_NAME" ""
|
||||||
else
|
else
|
||||||
write_status "done" "Built $OUT_NAME" "$OUT_NAME" ""
|
write_status "done" "Built $OUT_NAME" "$OUT_NAME" ""
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
# Example: ./deploy-to-proxmox.sh root@10.20.30.152
|
# Example: ./deploy-to-proxmox.sh root@10.20.30.152
|
||||||
#
|
#
|
||||||
# Optional env:
|
# Optional env:
|
||||||
# DEPLOY_ROOTFS_STORAGE=name — LXC rootfs storage (default: auto-detect: local-lvm, local, local-zfs, or first active)
|
# DEPLOY_ROOTFS_STORAGE=name — LXC rootfs storage (if set and valid, skips interactive choice; otherwise script lists storages and asks for number)
|
||||||
# CM4_BACKUPS_HOST_PATH=/path — host dir for backups; bind-mounted into LXC
|
# CM4_BACKUPS_HOST_PATH=/path — host dir for backups; bind-mounted into LXC
|
||||||
# DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH
|
# DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH
|
||||||
# DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub)
|
# DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub)
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
PROXMOX="${1:-root@10.130.60.224}"
|
PROXMOX="${1:-root@10.130.60.224}"
|
||||||
ROOTFS_STORAGE="${DEPLOY_ROOTFS_STORAGE:-local-lvm}"
|
# ROOTFS_STORAGE set later: from DEPLOY_ROOTFS_STORAGE (if in host list) or from interactive choice
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
LOG_FILE=""
|
LOG_FILE=""
|
||||||
@@ -33,10 +33,16 @@ fi
|
|||||||
log() { echo "[$(date -Iseconds)] $*"; }
|
log() { echo "[$(date -Iseconds)] $*"; }
|
||||||
|
|
||||||
# Optional: gather SSH key and LXC root password for setup inside deploy
|
# Optional: gather SSH key and LXC root password for setup inside deploy
|
||||||
|
# Default LXC root password (base64); override with DEPLOY_LXC_ROOT_PASSWORD. Not secure if repo is shared.
|
||||||
|
DEFAULT_LXC_PWD_B64="c2gxcGIweDE="
|
||||||
DEPLOY_SSH_KEY_B64=""
|
DEPLOY_SSH_KEY_B64=""
|
||||||
DEPLOY_LXC_PWD_B64=""
|
DEPLOY_LXC_PWD_B64=""
|
||||||
if [[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" ]]; then
|
if [[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" ]]; then
|
||||||
DEPLOY_LXC_PWD_B64=$(echo -n "$DEPLOY_LXC_ROOT_PASSWORD" | base64 -w 0 2>/dev/null || base64 2>/dev/null | tr -d '\n')
|
DEPLOY_LXC_PWD_B64=$(echo -n "$DEPLOY_LXC_ROOT_PASSWORD" | base64 -w 0 2>/dev/null || base64 2>/dev/null | tr -d '\n')
|
||||||
|
log "Will set LXC root password (from DEPLOY_LXC_ROOT_PASSWORD)."
|
||||||
|
else
|
||||||
|
DEPLOY_LXC_PWD_B64="$DEFAULT_LXC_PWD_B64"
|
||||||
|
log "Will set LXC root password (default)."
|
||||||
fi
|
fi
|
||||||
KEY_FILE="${DEPLOY_LXC_SSH_KEY:-}"
|
KEY_FILE="${DEPLOY_LXC_SSH_KEY:-}"
|
||||||
if [[ -z "$KEY_FILE" ]]; then
|
if [[ -z "$KEY_FILE" ]]; then
|
||||||
@@ -48,41 +54,73 @@ if [[ -n "$KEY_FILE" && -f "$KEY_FILE" ]]; then
|
|||||||
DEPLOY_SSH_KEY_B64=$(base64 -w 0 < "$KEY_FILE" 2>/dev/null || base64 < "$KEY_FILE" 2>/dev/null | tr -d '\n')
|
DEPLOY_SSH_KEY_B64=$(base64 -w 0 < "$KEY_FILE" 2>/dev/null || base64 < "$KEY_FILE" 2>/dev/null | tr -d '\n')
|
||||||
log "Will copy SSH key to LXC: $KEY_FILE"
|
log "Will copy SSH key to LXC: $KEY_FILE"
|
||||||
fi
|
fi
|
||||||
[[ -n "$DEPLOY_LXC_ROOT_PASSWORD" ]] && log "Will set LXC root password (DEPLOY_LXC_ROOT_PASSWORD)."
|
|
||||||
|
|
||||||
log "Deploying to $PROXMOX ..."
|
log "Deploying to $PROXMOX ..."
|
||||||
log "[1/5] Cleaning remote staging dir ..."
|
log "[1/5] Cleaning remote staging dir ..."
|
||||||
ssh "$PROXMOX" "rm -rf /tmp/emmc-provisioning-deploy"
|
ssh "$PROXMOX" "rm -rf /tmp/emmc-provisioning-deploy"
|
||||||
log "[2/5] Rsync repo to $PROXMOX ..."
|
log "[2/5] Rsync repo to $PROXMOX ..."
|
||||||
rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' --exclude='scripts/deploy-to-proxmox.sh' --exclude='scripts/deploy-*.log'
|
rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' --exclude='scripts/deploy-to-proxmox.sh' --exclude='scripts/deploy-*.log'
|
||||||
|
|
||||||
|
# --- Ask user to select LXC rootfs storage (from host; PBS excluded) ---
|
||||||
|
# Normalize: trim leading spaces so first column is storage name (pvesm can align columns)
|
||||||
|
STORAGE_LIST=$(ssh "$PROXMOX" "pvesm status 2>/dev/null | awk 'NR>1 { sub(/^[ \t]+/, \"\"); if (\$2!=\"pbs\" && \$3~/active/) print \$1 }'" 2>/dev/null || true)
|
||||||
|
storages=()
|
||||||
|
while IFS= read -r line; do [[ -n "$line" ]] && storages+=("$line"); done <<< "$STORAGE_LIST"
|
||||||
|
if [[ ${#storages[@]} -eq 0 ]]; then
|
||||||
|
log "Error: no Proxmox storage found on host (PBS excluded). Run on host: pvesm status"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# If DEPLOY_ROOTFS_STORAGE is set and in the list, use it without prompting
|
||||||
|
if [[ -n "${DEPLOY_ROOTFS_STORAGE:-}" ]]; then
|
||||||
|
for s in "${storages[@]}"; do
|
||||||
|
if [[ "$s" == "$DEPLOY_ROOTFS_STORAGE" ]]; then
|
||||||
|
ROOTFS_STORAGE="$DEPLOY_ROOTFS_STORAGE"
|
||||||
|
log "Using storage: $ROOTFS_STORAGE (from DEPLOY_ROOTFS_STORAGE)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ -z "${ROOTFS_STORAGE:-}" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Available storage on $PROXMOX (PBS excluded):"
|
||||||
|
for i in "${!storages[@]}"; do echo " $((i+1))) ${storages[i]}"; done
|
||||||
|
echo ""
|
||||||
|
if [[ ${#storages[@]} -eq 1 ]]; then
|
||||||
|
ROOTFS_STORAGE="${storages[0]}"
|
||||||
|
log "Using only available storage: $ROOTFS_STORAGE"
|
||||||
|
elif [[ ! -t 0 ]]; then
|
||||||
|
ROOTFS_STORAGE="${storages[0]}"
|
||||||
|
log "No TTY: using first storage $ROOTFS_STORAGE"
|
||||||
|
else
|
||||||
|
while true; do
|
||||||
|
read -r -p "Select storage (1-${#storages[@]}): " num
|
||||||
|
if [[ "$num" =~ ^[0-9]+$ ]] && [[ "$num" -ge 1 ]] && [[ "$num" -le ${#storages[@]} ]]; then
|
||||||
|
ROOTFS_STORAGE="${storages[num-1]}"
|
||||||
|
log "Using storage: $ROOTFS_STORAGE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Invalid choice. Enter a number from 1 to ${#storages[@]}."
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
log "[3/5] Running remote install (host + LXC) ..."
|
log "[3/5] Running remote install (host + LXC) ..."
|
||||||
|
|
||||||
# Pass optional LXC SSH vars (base64) so remote can set password and add key
|
# Pass optional LXC SSH vars (base64) and selected storage
|
||||||
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE'
|
ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE'
|
||||||
set -e
|
set -e
|
||||||
DEPLOY=/tmp/emmc-provisioning-deploy
|
DEPLOY=/tmp/emmc-provisioning-deploy
|
||||||
|
ROOTFS_STORAGE="${ROOTFS_STORAGE:?ROOTFS_STORAGE not set}"
|
||||||
BACKUPS_HOST_PATH="${CM4_BACKUPS_HOST_PATH:-}"
|
BACKUPS_HOST_PATH="${CM4_BACKUPS_HOST_PATH:-}"
|
||||||
LXC_HOSTNAME="cm4-provisioning"
|
LXC_HOSTNAME="cm4-provisioning"
|
||||||
log() { echo "[$(date -Iseconds)] $*"; }
|
log() { echo "[$(date -Iseconds)] $*"; }
|
||||||
|
|
||||||
# --- Auto-select LXC rootfs storage if not set or not available ---
|
# Storage already chosen interactively; validate it exists on host (match by name + active, robust to column alignment)
|
||||||
_storage_exists() { pvesm status 2>/dev/null | awk -v s="$1" 'NR>1 && $1==s && $3=="active" {exit(0)} END{exit(1)}'; }
|
if ! pvesm status 2>/dev/null | grep -w "$ROOTFS_STORAGE" | grep -q active; then
|
||||||
if [[ -n "${ROOTFS_STORAGE:-}" ]] && _storage_exists "$ROOTFS_STORAGE"; then
|
log "Error: storage $ROOTFS_STORAGE not found or not valid on host"
|
||||||
: # use provided and existing storage
|
exit 1
|
||||||
else
|
|
||||||
ROOTFS_STORAGE=""
|
|
||||||
for cand in local-lvm local local-zfs; do
|
|
||||||
if _storage_exists "$cand"; then
|
|
||||||
ROOTFS_STORAGE="$cand"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ -z "$ROOTFS_STORAGE" ]]; then
|
|
||||||
ROOTFS_STORAGE=$(pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}')
|
|
||||||
fi
|
|
||||||
[[ -z "$ROOTFS_STORAGE" ]] && { log "Error: no Proxmox storage found. Run: pvesm status"; exit 1; }
|
|
||||||
log "Using storage: $ROOTFS_STORAGE"
|
|
||||||
fi
|
fi
|
||||||
|
log "Using storage: $ROOTFS_STORAGE"
|
||||||
|
|
||||||
# --- Find existing LXC by hostname or use next available ID ---
|
# --- Find existing LXC by hostname or use next available ID ---
|
||||||
CTID=""
|
CTID=""
|
||||||
@@ -221,26 +259,34 @@ if [[ -n "${DEPLOY_SSH_KEY_B64:-}" ]] || [[ -n "${DEPLOY_LXC_PWD_B64:-}" ]]; the
|
|||||||
echo "$DEPLOY_SSH_KEY_B64" | base64 -d 2>/dev/null | pct exec "$CTID" -- bash -c "mkdir -p /root/.ssh; chmod 700 /root/.ssh; cat >> /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys" 2>/dev/null && log "LXC: SSH key added to /root/.ssh/authorized_keys." || true
|
echo "$DEPLOY_SSH_KEY_B64" | base64 -d 2>/dev/null | pct exec "$CTID" -- bash -c "mkdir -p /root/.ssh; chmod 700 /root/.ssh; cat >> /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys" 2>/dev/null && log "LXC: SSH key added to /root/.ssh/authorized_keys." || true
|
||||||
fi
|
fi
|
||||||
LXC_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')
|
LXC_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
[[ -n "$LXC_IP" ]] && echo "LXC SSH: ssh root@$LXC_IP"
|
[[ -n "$LXC_IP" ]] && log "LXC SSH: ssh root@$LXC_IP"
|
||||||
fi
|
fi
|
||||||
|
# Always capture LXC IP for final summary (write so local script can read it)
|
||||||
|
LXC_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
echo "${LXC_IP:-}" > "$DEPLOY/lxc_ip.txt"
|
||||||
|
|
||||||
log "Deploy done (remote). LXC ID: $CTID"
|
log "Deploy done on remote. LXC ID: $CTID"
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|
||||||
|
# Read LXC IP written by remote (container hostname -I)
|
||||||
|
LXC_IP=$(ssh "$PROXMOX" "cat /tmp/emmc-provisioning-deploy/lxc_ip.txt 2>/dev/null" | tr -d '\n\r')
|
||||||
|
|
||||||
log "[5/5] Deploy finished."
|
log "[5/5] Deploy finished."
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Deploy complete ==="
|
echo "=== Deploy complete ==="
|
||||||
echo "Host and LXC are fully set up: usbboot (rpiboot), PiShrink, dashboard, systemd, udev."
|
echo "Host and LXC are fully set up: usbboot (rpiboot), PiShrink, dashboard, systemd, udev."
|
||||||
|
[[ -n "$LXC_IP" ]] && echo " LXC IP: $LXC_IP"
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- Only remaining step (manual) ---"
|
echo "--- Only remaining step (manual) ---"
|
||||||
echo " Add a golden image for Deploy (writing image to device):"
|
echo " Add a golden image for Deploy (writing image to device):"
|
||||||
echo " • Dashboard: open http://<LXC-IP>:5000 → Build cloud-init image → then Set as golden"
|
echo " • Dashboard: open http://${LXC_IP:-<LXC-IP>}:5000 → Build cloud-init image → then Set as golden"
|
||||||
echo " • Or copy your image: scp your-image.img $PROXMOX:/var/lib/cm4-provisioning/golden.img"
|
echo " • Or copy your image: scp your-image.img $PROXMOX:/var/lib/cm4-provisioning/golden.img"
|
||||||
echo " Backup (read from device) works without golden.img."
|
echo " Backup (read from device) works without golden.img."
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- You have ---"
|
echo "--- You have ---"
|
||||||
echo " - Dashboard: http://<LXC-IP>:5000 (LXC IP: from Proxmox UI, or on host: pct exec <CTID> -- hostname -I)"
|
echo " - Dashboard: http://${LXC_IP:-<LXC-IP>}:5000"
|
||||||
[[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && echo " - LXC SSH: ssh root@<LXC-IP> (password and/or key were set)"
|
[[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && [[ -n "$LXC_IP" ]] && echo " - LXC SSH: ssh root@$LXC_IP (password and/or key were set)"
|
||||||
|
[[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && [[ -z "$LXC_IP" ]] && echo " - LXC SSH: ssh root@<LXC-IP> (password and/or key were set)"
|
||||||
[[ -n "${CM4_BACKUPS_HOST_PATH:-}" ]] && echo " - Backups on host: $CM4_BACKUPS_HOST_PATH"
|
[[ -n "${CM4_BACKUPS_HOST_PATH:-}" ]] && echo " - Backups on host: $CM4_BACKUPS_HOST_PATH"
|
||||||
if [[ -n "$LOG_FILE" ]]; then
|
if [[ -n "$LOG_FILE" ]]; then
|
||||||
echo " - Log: $LOG_FILE"
|
echo " - Log: $LOG_FILE"
|
||||||
|
|||||||
Reference in New Issue
Block a user