Implement backup upload and deletion functionality in eMMC provisioning dashboard: add API endpoints for uploading image files and deleting backups, enhance UI with upload button and delete options, and improve error handling for file operations. Update documentation to reflect new features.
This commit is contained in:
@@ -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,7 +380,17 @@ 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)
|
||||||
shutil.copy2(path, GOLDEN_IMAGE)
|
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)
|
||||||
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:
|
||||||
return jsonify({"ok": False, "error": str(e)}), 500
|
return jsonify({"ok": False, "error": str(e)}), 500
|
||||||
@@ -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" ""
|
||||||
|
|||||||
@@ -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,7 +54,6 @@ 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 ..."
|
||||||
@@ -57,7 +62,8 @@ 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) ---
|
# --- Ask user to select LXC rootfs storage (from host; PBS excluded) ---
|
||||||
STORAGE_LIST=$(ssh "$PROXMOX" "pvesm status 2>/dev/null | awk 'NR>1 && \$2!=\"pbs\" && \$3~/active/ {print \$1}'" 2>/dev/null || true)
|
# 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=()
|
storages=()
|
||||||
while IFS= read -r line; do [[ -n "$line" ]] && storages+=("$line"); done <<< "$STORAGE_LIST"
|
while IFS= read -r line; do [[ -n "$line" ]] && storages+=("$line"); done <<< "$STORAGE_LIST"
|
||||||
if [[ ${#storages[@]} -eq 0 ]]; then
|
if [[ ${#storages[@]} -eq 0 ]]; then
|
||||||
@@ -109,8 +115,8 @@ BACKUPS_HOST_PATH="${CM4_BACKUPS_HOST_PATH:-}"
|
|||||||
LXC_HOSTNAME="cm4-provisioning"
|
LXC_HOSTNAME="cm4-provisioning"
|
||||||
log() { echo "[$(date -Iseconds)] $*"; }
|
log() { echo "[$(date -Iseconds)] $*"; }
|
||||||
|
|
||||||
# Storage already chosen interactively; validate it exists on host
|
# Storage already chosen interactively; validate it exists on host (match by name + active, robust to column alignment)
|
||||||
if ! pvesm status 2>/dev/null | awk -v s="$ROOTFS_STORAGE" 'NR>1 && $1==s && $2!="pbs" && $3~/active/ {exit(0)} END{exit(1)}'; then
|
if ! pvesm status 2>/dev/null | grep -w "$ROOTFS_STORAGE" | grep -q active; then
|
||||||
log "Error: storage $ROOTFS_STORAGE not found or not valid on host"
|
log "Error: storage $ROOTFS_STORAGE not found or not valid on host"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -253,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