Clonezilla back up / restore by machine number
Boot menu: "Clonezilla: back up this PC" and "restore this PC" ask for the machine number and boot Clonezilla (64-bit with Debian shim, or 32-bit i686 by cpuid) with ocs_repository=smb3://clonezilla:<pw>@server/clonezilla/, ocs_prerun fetching clonezilla-scripts/pxe-cz.sh and ocs_live_run running it. The script validates the machine number, saves <machine>-<YYYY-MM-DD-HHMM> (server time), writes pxe-meta.txt, runs ocs-chkimg, and restores only that machine's images with -k1 -r after a typed YES. The old entry is "Clonezilla: manual". Playbook: clonezilla Samba user for the [clonezilla] share only, password from clonezilla_smb_password (validated, filled into the menu placeholder), scripts installed to the web root. Webapp: Clonezilla backups page lists image folders and zips grouped by machine number with serial/model/version/check, serial mismatch flag, delete, move to another machine, streamed tar download, disk space bar and an explicit keep-last-N retention with preview. Tests and a VM test plan added.
This commit is contained in:
113
webapp/app.py
113
webapp/app.py
@@ -44,6 +44,7 @@ from werkzeug.utils import secure_filename
|
||||
import config
|
||||
from services import (
|
||||
blancco_evidence,
|
||||
clonezilla_images,
|
||||
hardware_coverage,
|
||||
blancco_report,
|
||||
dell_updates,
|
||||
@@ -551,28 +552,110 @@ def images_rename(image_type):
|
||||
# Routes - Clonezilla Backups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/backups")
|
||||
def clonezilla_backups():
|
||||
backups = []
|
||||
if os.path.isdir(config.CLONEZILLA_SHARE):
|
||||
for f in sorted(os.listdir(config.CLONEZILLA_SHARE)):
|
||||
fpath = os.path.join(config.CLONEZILLA_SHARE, f)
|
||||
if os.path.isfile(fpath) and f.lower().endswith(".zip"):
|
||||
stat = os.stat(fpath)
|
||||
backups.append({
|
||||
"filename": f,
|
||||
"machine": os.path.splitext(f)[0],
|
||||
"size": stat.st_size,
|
||||
"modified": stat.st_mtime,
|
||||
})
|
||||
def _backups_page(plan=None, plan_keep=None):
|
||||
images = clonezilla_images.list_images()
|
||||
zips = clonezilla_images.list_zips()
|
||||
groups = clonezilla_images.grouped(images, zips)
|
||||
st = clonezilla_images.settings()
|
||||
return render_template(
|
||||
"backups.html",
|
||||
backups=backups,
|
||||
groups=groups,
|
||||
backups=zips,
|
||||
image_count=len(images),
|
||||
mismatches=sum(1 for g in groups if g["mismatch"]),
|
||||
space=clonezilla_images.disk_space(st["warn_percent"]),
|
||||
cz_settings=st,
|
||||
plan=plan,
|
||||
plan_keep=plan_keep,
|
||||
image_types=config.IMAGE_TYPES,
|
||||
friendly_names=config.FRIENDLY_NAMES,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/backups")
|
||||
def clonezilla_backups():
|
||||
"""Clonezilla image directories and machine-number zips on the share,
|
||||
grouped by machine number (services/clonezilla_images.py)."""
|
||||
return _backups_page()
|
||||
|
||||
|
||||
@app.route("/backups/image/<name>/delete", methods=["POST"])
|
||||
def clonezilla_image_delete(name):
|
||||
try:
|
||||
clonezilla_images.delete_image(name)
|
||||
audit("BACKUP_IMAGE_DELETE", name)
|
||||
flash(f"Deleted image {name}.", "success")
|
||||
except (clonezilla_images.ImageError, OSError) as ex:
|
||||
flash(str(ex), "danger")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
|
||||
|
||||
@app.route("/backups/image/<name>/move", methods=["POST"])
|
||||
def clonezilla_image_move(name):
|
||||
try:
|
||||
new = clonezilla_images.move_image(name, request.form.get("machine", ""))
|
||||
audit("BACKUP_IMAGE_MOVE", f"{name} -> {new}")
|
||||
flash(f"Moved {name} to machine {clonezilla_images.norm_machine(request.form.get('machine'))}: now {new}.", "success")
|
||||
except (clonezilla_images.ImageError, OSError) as ex:
|
||||
flash(str(ex), "danger")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
|
||||
|
||||
@app.route("/backups/image/<name>/tar")
|
||||
def clonezilla_image_tar(name):
|
||||
"""The image directory as a tar, streamed file by file (no temp copy)."""
|
||||
try:
|
||||
gen = clonezilla_images.tar_stream(name)
|
||||
except clonezilla_images.ImageError as ex:
|
||||
flash(str(ex), "danger")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
audit("BACKUP_IMAGE_DOWNLOAD", name)
|
||||
resp = Response(stream_with_context(gen), mimetype="application/x-tar")
|
||||
resp.headers["Content-Disposition"] = f'attachment; filename="{secure_filename(name) or "image"}.tar"'
|
||||
resp.headers["X-Accel-Buffering"] = "no"
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/backups/settings", methods=["POST"])
|
||||
def clonezilla_settings():
|
||||
try:
|
||||
st = clonezilla_images.save_settings(request.form.get("keep_last", ""), request.form.get("warn_percent", ""))
|
||||
audit("BACKUP_SETTINGS", f"keep_last={st['keep_last']} warn_percent={st['warn_percent']}")
|
||||
flash("Clonezilla backup settings saved." + ("" if not st["keep_last"] else
|
||||
" Nothing is deleted until you press Apply retention and confirm."), "success")
|
||||
except clonezilla_images.ImageError as ex:
|
||||
flash(str(ex), "danger")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
|
||||
|
||||
@app.route("/backups/retention")
|
||||
def clonezilla_retention_preview():
|
||||
"""What 'keep last N per machine' would delete. Shows only; the apply
|
||||
is a separate POST of exactly these names."""
|
||||
keep = clonezilla_images.settings()["keep_last"]
|
||||
if not keep:
|
||||
flash("Set Keep last N images per machine first.", "warning")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
return _backups_page(plan=clonezilla_images.retention_plan(keep), plan_keep=keep)
|
||||
|
||||
|
||||
@app.route("/backups/retention/apply", methods=["POST"])
|
||||
def clonezilla_retention_apply():
|
||||
keep = clonezilla_images.settings()["keep_last"]
|
||||
names = request.form.getlist("name")
|
||||
if not keep or not names:
|
||||
flash("Nothing to delete.", "warning")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
deleted, skipped = clonezilla_images.apply_retention(names, keep)
|
||||
for n in deleted:
|
||||
audit("BACKUP_RETENTION_DELETE", f"{n} (keep last {keep})")
|
||||
msg = f"Retention (keep last {keep}): deleted {len(deleted)} image{'s' if len(deleted) != 1 else ''}."
|
||||
if skipped:
|
||||
msg += f" Kept {len(skipped)} that are no longer due for deletion: {', '.join(skipped[:5])}{'...' if len(skipped) > 5 else ''}."
|
||||
flash(msg, "success" if not skipped else "warning")
|
||||
return redirect(url_for("clonezilla_backups"))
|
||||
|
||||
|
||||
@app.route("/backups/upload", methods=["POST"])
|
||||
def clonezilla_upload():
|
||||
if "backup_file" not in request.files:
|
||||
|
||||
Reference in New Issue
Block a user