26 Kapitel in vier Teilen, in der Reihenfolge, in der man sie braucht: erst sichern, dann Dateien holen, dann ganze Maschinen wiederherstellen, dahinter der Nachschlagteil. Gebaut wird mit MkDocs + Material. Bewusst ohne Netzabhaengigkeiten: keine Schriften vom CDN (font: false), Volltextsuche mit deutschem Stemming liegt neben den Seiten. Im Notfall steht vielleicht das halbe Netz - dann nuetzt eine Doku im Internet nichts. handbuch/bauen.sh baut handbuch/site/ handbuch/bauen.sh ansehen Vorschau auf 127.0.0.1:8000 install.sh nimmt das gebaute Handbuch mit nach /usr/share/doc/pvesnap/handbuch/ - falls es vorliegt. Auf dem Host selbst wird nichts gebaut, mkdocs gehoert nicht auf einen Hypervisor. Die 28 Bildschirmfotos sind nicht abfotografiert, sondern erzeugt: Der echte Programmcode laeuft in einem Pseudo-Terminal gegen einen erfundenen Proxmox-Host (Attrappen fuer pvesh, perl und rbd), pyte baut den Bildschirm nach, heraus faellt ein SVG. Damit stimmen sie garantiert mit dem Programm ueberein, sind reproduzierbar und enthalten keine echten Daten. Die Werkstatt dafuer liegt unter handbuch/werkstatt/ samt LIESMICH.md. Nebenbei: die Schlussmeldung von install.sh warb noch mit --exchange, das mit dem eingebauten Austauschlaufwerk weggefallen ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
479 lines
18 KiB
Python
Executable File
479 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Ein erfundenes Rechenzentrum fuer die Bildschirmfotos im Handbuch.
|
|
|
|
Baut unter DEMO/ eine vollstaendige Kulisse auf: Antworten fuer ein
|
|
Attrappen-pvesh, Gast-Konfigurationen, die Merkliste der Wiederherstellungen,
|
|
die Transfer-Laufwerke und einen Dateibaum fuer den Explorer.
|
|
|
|
Nichts davon fasst einen echten Proxmox-Host an.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
from datetime import datetime, timedelta
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
DEMO = os.path.join(HERE, "demo")
|
|
|
|
# Ein fester Zeitpunkt - sonst sehen zwei Bildschirmfotos derselben Liste
|
|
# unterschiedlich aus, und im Handbuch passt die Beschreibung nicht mehr.
|
|
NOW = datetime(2026, 8, 9, 11, 42, 0)
|
|
|
|
NODES = {"pve1": "10.20.0.11", "pve2": "10.20.0.12"}
|
|
|
|
GUESTS = [
|
|
# vmid, name, typ, node, zustand, tags, pool
|
|
(100, "web01", "qemu", "pve1", "running", "produktion;stuendlich", "Hausnetz"),
|
|
(101, "db01", "qemu", "pve1", "running", "produktion;datenbank;stuendlich", "Hausnetz"),
|
|
(102, "fileserver", "qemu", "pve2", "running", "produktion", "Hausnetz"),
|
|
(105, "warenwirtschaft", "qemu", "pve2", "running", "produktion;dongle", "Hausnetz"),
|
|
(110, "mailgw", "lxc", "pve1", "running", "produktion", "Hausnetz"),
|
|
(111, "gitlab", "lxc", "pve2", "running", "entwicklung", ""),
|
|
(120, "build-test", "qemu", "pve2", "stopped", "nosnap", ""),
|
|
(130, "dc01", "qemu", "pve1", "running", "produktion", "Hausnetz"),
|
|
(9101, "db01-live", "qemu", "pve1", "running", "pvesnap-recovery", ""),
|
|
(9102, "warenwirtschaft-w", "qemu", "pve2", "stopped", "pvesnap-recovery", ""),
|
|
]
|
|
|
|
# Wie viele Snapshots welcher Gruppe je Gast - so, wie es nach ein paar Wochen
|
|
# Betrieb tatsaechlich aussieht.
|
|
PLAN = {
|
|
100: [("stuendlich", 24, 3600), ("taeglich", 8, 86400)],
|
|
101: [("stuendlich", 24, 3600), ("taeglich", 14, 86400), ("monatlich", 3, 2592000)],
|
|
102: [("taeglich", 9, 86400)],
|
|
105: [("taeglich", 12, 86400), ("monatlich", 2, 2592000)],
|
|
110: [("taeglich", 7, 86400)],
|
|
111: [("taeglich", 4, 86400)],
|
|
130: [("taeglich", 11, 86400)],
|
|
}
|
|
|
|
# Von Hand angelegte Snapshots - die pvesnap niemals anfasst.
|
|
HANDMADE = {
|
|
101: [("vor-update-14.2", "Vor dem Update auf PostgreSQL 14.2", 6 * 86400)],
|
|
105: [("golden", "Frisch eingerichtet, Lizenz aktiviert", 40 * 86400)],
|
|
}
|
|
|
|
DESCRIPTION = ("pvesnap | Gruppe: %s | erstellt: %s | Vorhaltezeit: %s | max: %d")
|
|
|
|
KEEP = {"stuendlich": ("2 Tage", 24), "taeglich": ("21 Tage", 14),
|
|
"monatlich": ("400 Tage", 6)}
|
|
|
|
QEMU_CONF = """\
|
|
agent: 1
|
|
boot: order=scsi0
|
|
cores: %(cores)d
|
|
cpu: x86-64-v2-AES
|
|
memory: %(memory)d
|
|
meta: creation-qemu=9.0.2
|
|
name: %(name)s
|
|
net0: virtio=%(mac)s,bridge=vmbr0,firewall=1
|
|
numa: 0
|
|
ostype: %(ostype)s
|
|
scsi0: ceph-vm:vm-%(vmid)d-disk-0,discard=on,iothread=1,size=%(size)dG
|
|
scsihw: virtio-scsi-single
|
|
smbios1: uuid=%(uuid)s
|
|
sockets: 1
|
|
vmgenid: %(genid)s
|
|
"""
|
|
|
|
LXC_CONF = """\
|
|
arch: amd64
|
|
cores: 2
|
|
features: nesting=1
|
|
hostname: %(name)s
|
|
memory: 2048
|
|
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=%(mac)s,ip=dhcp,type=veth
|
|
ostype: debian
|
|
rootfs: ceph-ct:subvol-%(vmid)d-disk-0,size=%(size)dG
|
|
swap: 512
|
|
unprivileged: 1
|
|
"""
|
|
|
|
HARDWARE = {
|
|
100: dict(cores=4, memory=8192, size=60, ostype="l26"),
|
|
101: dict(cores=8, memory=32768, size=400, ostype="l26"),
|
|
102: dict(cores=4, memory=8192, size=2048, ostype="l26"),
|
|
105: dict(cores=6, memory=16384, size=250, ostype="win11"),
|
|
120: dict(cores=8, memory=16384, size=120, ostype="l26"),
|
|
130: dict(cores=4, memory=8192, size=80, ostype="win11"),
|
|
110: dict(size=16),
|
|
111: dict(size=64),
|
|
9101: dict(cores=8, memory=32768, size=400, ostype="l26"),
|
|
9102: dict(cores=6, memory=16384, size=250, ostype="win11"),
|
|
}
|
|
|
|
|
|
def _mac(vmid, index=0):
|
|
return "BC:24:11:%02X:%02X:%02X" % (vmid // 256, vmid % 256, 0x40 + index)
|
|
|
|
|
|
def _uuid(vmid):
|
|
return "8f3c%04d-11ee-4a7b-9c2d-%012d" % (vmid, vmid * 7919)
|
|
|
|
|
|
def _genid(vmid):
|
|
return "b2e1%04d-7f45-4c18-a3d9-%012d" % (vmid, vmid * 104729)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Snapshots
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _snapshots_for(vmid):
|
|
"""Alle Snapshots eines Gastes, aeltester zuerst, mit Elternkette."""
|
|
entries = []
|
|
for slug, count, step in PLAN.get(vmid, []):
|
|
keep_time, keep_count = KEEP[slug]
|
|
for index in range(count):
|
|
moment = NOW - timedelta(seconds=step * (index + 1))
|
|
if slug == "taeglich":
|
|
moment = moment.replace(hour=2, minute=30, second=0)
|
|
elif slug == "monatlich":
|
|
moment = moment.replace(day=1, hour=4, minute=0, second=0)
|
|
else:
|
|
moment = moment.replace(minute=0, second=0)
|
|
entries.append({
|
|
"name": "auto-%s-%s" % (slug, moment.strftime("%Y%m%d-%H%M%S")),
|
|
"snaptime": int(moment.timestamp()),
|
|
"description": DESCRIPTION % (slug, moment.strftime("%Y-%m-%d %H:%M:%S"),
|
|
keep_time, keep_count),
|
|
})
|
|
for name, note, age in HANDMADE.get(vmid, []):
|
|
moment = NOW - timedelta(seconds=age)
|
|
entries.append({"name": name, "snaptime": int(moment.timestamp()),
|
|
"description": note})
|
|
|
|
entries.sort(key=lambda e: e["snaptime"])
|
|
parent = ""
|
|
for entry in entries:
|
|
entry["parent"] = parent
|
|
parent = entry["name"]
|
|
return entries
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Kulisse aufbauen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _write(path, text):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
handle.write(text)
|
|
|
|
|
|
def _guest_config(vmid, name, kind):
|
|
values = dict(HARDWARE.get(vmid, {}))
|
|
values.update(vmid=vmid, name=name, mac=_mac(vmid),
|
|
uuid=_uuid(vmid), genid=_genid(vmid))
|
|
values.setdefault("cores", 2)
|
|
values.setdefault("memory", 2048)
|
|
values.setdefault("size", 32)
|
|
values.setdefault("ostype", "l26")
|
|
return (LXC_CONF if kind == "lxc" else QEMU_CONF) % values
|
|
|
|
|
|
def build():
|
|
if os.path.isdir(DEMO):
|
|
shutil.rmtree(DEMO)
|
|
|
|
resources, snapshots, configs, status = [], {}, {}, {}
|
|
|
|
for vmid, name, kind, node, state, tags, pool in GUESTS:
|
|
resources.append({
|
|
"vmid": vmid, "name": name, "type": kind, "node": node,
|
|
"status": state, "tags": tags, "pool": pool,
|
|
"maxmem": HARDWARE.get(vmid, {}).get("memory", 2048) * 1024 * 1024,
|
|
"maxdisk": HARDWARE.get(vmid, {}).get("size", 32) * 1024 ** 3,
|
|
"uptime": 486231 if state == "running" else 0,
|
|
})
|
|
key = "%s/%s/%d" % (node, kind, vmid)
|
|
snapshots[key] = _snapshots_for(vmid)
|
|
configs[key] = _guest_config(vmid, name, kind)
|
|
status[key] = {
|
|
"status": state, "vmid": vmid, "name": name,
|
|
"qmpstatus": "running" if state == "running" else "stopped",
|
|
"uptime": 486231 if state == "running" else 0,
|
|
"maxmem": HARDWARE.get(vmid, {}).get("memory", 2048) * 1024 * 1024,
|
|
"cpus": HARDWARE.get(vmid, {}).get("cores", 2),
|
|
"spice": 1 if vmid == 9102 else 0,
|
|
}
|
|
|
|
# Die beiden Wiederherstellungen haben eine eigene Konfiguration: kein Netz
|
|
# bzw. Netz mit den MAC-Adressen des Originals, dazu das Transfer-Laufwerk.
|
|
live = _guest_config(9101, "db01-live", "qemu")
|
|
live = live.replace("net0: virtio=%s,bridge=vmbr0,firewall=1\n" % _mac(9101), "")
|
|
live = live.replace("scsi0: ceph-vm:vm-9101-disk-0",
|
|
"scsi0: ceph-vm:vm-9101-disk-0")
|
|
live += "scsi1: /dev/loop3,backup=0,replicate=0\n"
|
|
live += "tags: pvesnap-recovery\n"
|
|
configs["pve1/qemu/9101"] = live
|
|
|
|
warm = _guest_config(9102, "warenwirtschaft-w", "qemu")
|
|
warm = warm.replace("net0: virtio=%s" % _mac(9102), "net0: virtio=%s" % _mac(105))
|
|
warm = warm.replace("smbios1: uuid=%s" % _uuid(9102),
|
|
"smbios1: uuid=%s" % _uuid(105))
|
|
warm += "tags: pvesnap-recovery\nusb0: spice\nusb1: spice\nvga: qxl\n"
|
|
configs["pve2/qemu/9102"] = warm
|
|
|
|
_write(os.path.join(DEMO, "state.json"), json.dumps({
|
|
"resources": resources, "snapshots": snapshots,
|
|
"configs": configs, "status": status, "nextid": 9103,
|
|
"nodes": NODES,
|
|
}, indent=1))
|
|
|
|
# /etc/pve nachbilden - daraus liest pvesnap die Belegung der Laufwerke.
|
|
for vmid, name, kind, node, _s, _t, _p in GUESTS:
|
|
sub = "lxc" if kind == "lxc" else "qemu-server"
|
|
_write(os.path.join(DEMO, "pve", "nodes", node, sub, "%d.conf" % vmid),
|
|
configs["%s/%s/%d" % (node, kind, vmid)])
|
|
_write(os.path.join(DEMO, "pve", ".members"),
|
|
json.dumps({"nodename": "pve1", "version": 8,
|
|
"nodelist": {n: {"id": i + 1, "online": 1, "ip": ip}
|
|
for i, (n, ip) in enumerate(NODES.items())}},
|
|
indent=1))
|
|
|
|
_build_registry()
|
|
_build_transfers()
|
|
_build_config()
|
|
_build_tree()
|
|
return DEMO
|
|
|
|
|
|
def _build_registry():
|
|
created_live = int((NOW - timedelta(minutes=38)).timestamp())
|
|
created_warm = int((NOW - timedelta(hours=5, minutes=12)).timestamp())
|
|
entries = [
|
|
{"vmid": 9101, "type": "qemu", "node": "pve1", "name": "db01-live",
|
|
"source": 101, "source_node": "pve1",
|
|
"snapshot": "auto-stuendlich-20260809-100000", "mode": "live",
|
|
"created": created_live,
|
|
"volumes": ["ceph-vm:vm-9101-disk-0"],
|
|
"protected": [["ceph-vm:vm-101-disk-0", "auto-stuendlich-20260809-100000"]],
|
|
"transfers": [{"name": "dumps", "key": "scsi1",
|
|
"drive": "/dev/loop3,backup=0,replicate=0",
|
|
"kind": "disk", "pending": False}],
|
|
"resumed": True},
|
|
{"vmid": 9102, "type": "qemu", "node": "pve2", "name": "warenwirtschaft-w",
|
|
"source": 105, "source_node": "pve2",
|
|
"snapshot": "auto-taeglich-20260809-023000", "mode": "recover",
|
|
"created": created_warm,
|
|
"volumes": ["ceph-vm:vm-9102-disk-0"],
|
|
"protected": [],
|
|
"transfers": [], "resumed": False},
|
|
]
|
|
_write(os.path.join(DEMO, "lib", "recovery.json"), json.dumps(entries, indent=1))
|
|
|
|
|
|
def _build_transfers():
|
|
volumes = [
|
|
{"name": "dumps", "image": os.path.join(DEMO, "lib", "transfer", "dumps.img"),
|
|
"size": 20 * 1024 ** 3, "fs": "exfat", "label": "DUMPS",
|
|
"note": "Datenbank-Dumps und Exporte",
|
|
"created": int((NOW - timedelta(days=41)).timestamp()), "partitioned": True},
|
|
{"name": "werkzeuge",
|
|
"image": os.path.join(DEMO, "lib", "transfer", "werkzeuge.img"),
|
|
"size": 4 * 1024 ** 3, "fs": "exfat", "label": "WERKZEUGE",
|
|
"note": "Skripte, Treiber, Installer",
|
|
"created": int((NOW - timedelta(days=41)).timestamp()), "partitioned": True},
|
|
{"name": "austausch",
|
|
"image": os.path.join(DEMO, "lib", "transfer", "austausch.img"),
|
|
"size": 8 * 1024 ** 3, "fs": "exfat", "label": "AUSTAUSCH",
|
|
"note": "Alles andere",
|
|
"created": int((NOW - timedelta(days=9)).timestamp()), "partitioned": True},
|
|
]
|
|
_write(os.path.join(DEMO, "lib", "transfer", "index.json"),
|
|
json.dumps(volumes, indent=1))
|
|
# Duenn besetzte Abbilddateien: sie kosten keinen Plattenplatz, aber
|
|
# Volume.used_bytes findet echte Werte vor.
|
|
for volume, used in zip(volumes, (3_400_000_000, 780_000_000, 0)):
|
|
with open(volume["image"], "wb") as handle:
|
|
handle.truncate(volume["size"])
|
|
if used:
|
|
handle.seek(0)
|
|
handle.write(b"\0" * min(used, 4 * 1024 ** 2))
|
|
os.truncate(volume["image"], volume["size"])
|
|
|
|
|
|
def _build_config():
|
|
_write(os.path.join(DEMO, "pvesnap.conf"), """\
|
|
[global]
|
|
prefix = auto
|
|
check_interval = 60s
|
|
state_file = /var/lib/pvesnap/state.json
|
|
log_level = INFO
|
|
task_timeout = 15m
|
|
retries = 2
|
|
retry_delay = 60s
|
|
pause_between = 0s
|
|
run_on_start = no
|
|
dry_run = no
|
|
description = pvesnap | Gruppe: {group} | erstellt: {datetime} | Vorhaltezeit: {keep_time} | max: {keep_count}
|
|
|
|
[defaults]
|
|
enabled = yes
|
|
skip_stopped = no
|
|
vmstate = no
|
|
|
|
[group:stuendlich]
|
|
interval = 1h
|
|
align = yes
|
|
keep_count = 24
|
|
keep_time = 2d
|
|
tags = stuendlich
|
|
skip_stopped = yes
|
|
|
|
[group:taeglich]
|
|
schedule = daily
|
|
at = 02:30
|
|
keep_count = 14
|
|
keep_time = 21d
|
|
tags = produktion
|
|
exclude_tags = nosnap
|
|
|
|
[group:monatlich]
|
|
schedule = monthly
|
|
day_of_month = 1
|
|
at = 04:00
|
|
keep_count = 6
|
|
keep_time = 400d
|
|
all = yes
|
|
exclude_tags = nosnap, pvesnap-recovery
|
|
description = Monatssicherung {name} ({vmid}) vom {date}
|
|
""")
|
|
|
|
|
|
FSTAB = """\
|
|
# /etc/fstab: static file system information.
|
|
#
|
|
# Use 'blkid' to print the universally unique identifier for a device; this may
|
|
# be used with UUID= as a more robust way to name devices that works even if
|
|
# disks are added and removed. See fstab(5).
|
|
#
|
|
# <file system> <mount point> <type> <options> <dump> <pass>
|
|
UUID=4c1a9f3e-2b77-4d18-9a5c-7e3f0d1b8a26 / ext4 errors=remount-ro 0 1
|
|
UUID=9f2c-31AD /boot/efi vfat umask=0077 0 1
|
|
/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1 /srv/dumps xfs defaults 0 2
|
|
/swap.img none swap sw 0 0
|
|
"""
|
|
|
|
SICHERUNG = """\
|
|
#!/bin/bash
|
|
# Naechtlicher Dump - laeuft aus der Crontab um 01:50, also vor dem Snapshot.
|
|
set -euo pipefail
|
|
|
|
ZIEL=/srv/dumps
|
|
STAMPE=$(date +%Y%m%d-%H%M)
|
|
|
|
for DB in kunden auftraege artikel; do
|
|
pg_dump -Fc "$DB" | gzip -1 > "$ZIEL/${DB}-${STAMPE}.sql.gz"
|
|
done
|
|
|
|
find "$ZIEL" -name '*.sql.gz' -mtime +14 -delete
|
|
"""
|
|
|
|
LIESMICH = """\
|
|
Transfer-Laufwerk dumps
|
|
=======================
|
|
|
|
Angelegt mit pvesnap-recovery (Taste v in der Uebersicht).
|
|
|
|
Dieses Laufwerk gehoert zu keiner Maschine. Es wird beim Start einer
|
|
Wiederherstellung angehaengt und danach wieder freigegeben - der Inhalt
|
|
bleibt erhalten.
|
|
|
|
Immer nur an einer Stelle benutzen: entweder am Host eingehaengt oder in
|
|
einem Gast. Beides gleichzeitig zerlegt das Dateisystem.
|
|
"""
|
|
|
|
TREE = {
|
|
"etc": {
|
|
"postgresql": {"14": {"main": {
|
|
"postgresql.conf": 28_412, "pg_hba.conf": 5_137, "pg_ident.conf": 1_636}}},
|
|
"nginx": {"nginx.conf": 1_482, "sites-enabled": {"default": 2_416}},
|
|
"fstab": FSTAB, "hostname": "db01\n", "hosts": 274, "passwd": 2_143,
|
|
"shadow": 1_309, "ssh": {"sshd_config": 3_290},
|
|
},
|
|
"home": {"stefan": {"notizen.txt": 1_204, ".bashrc": 3_771}},
|
|
"root": {".bash_history": 8_214, "sicherung.sh": SICHERUNG},
|
|
"srv": {"dumps": {
|
|
"db01-20260809-0200.sql.gz": 4_183_244_800,
|
|
"db01-20260808-0200.sql.gz": 4_106_112_512,
|
|
"kunden-export.csv": 88_412_160}},
|
|
"var": {"log": {"syslog": 12_884_901, "auth.log": 940_233,
|
|
"postgresql": {"postgresql-14-main.log": 3_402_118}},
|
|
"lib": {"postgresql": {"14": {"main": {"PG_VERSION": 3,
|
|
"postgresql.auto.conf": 88}}}}},
|
|
}
|
|
|
|
|
|
# Alles, was frisch angelegt wird, traegt sonst das Datum von heute - im
|
|
# Bildschirmfoto sieht ein Serverdateisystem dann aus wie eben ausgepackt.
|
|
MTIMES = {
|
|
"srv/dumps/db01-20260809-0200.sql.gz": NOW - timedelta(hours=9, minutes=12),
|
|
"srv/dumps/db01-20260808-0200.sql.gz": NOW - timedelta(days=1, hours=9),
|
|
"srv/dumps/kunden-export.csv": NOW - timedelta(days=2, hours=4),
|
|
"srv/dumps": NOW - timedelta(hours=9),
|
|
"var/log/syslog": NOW - timedelta(minutes=3),
|
|
"var/log/auth.log": NOW - timedelta(minutes=41),
|
|
"var/log": NOW - timedelta(minutes=3),
|
|
"root/.bash_history": NOW - timedelta(days=1, hours=2),
|
|
"root/sicherung.sh": NOW - timedelta(days=96),
|
|
"home/stefan/notizen.txt": NOW - timedelta(days=12),
|
|
"etc/postgresql/14/main/postgresql.conf": NOW - timedelta(days=214),
|
|
"etc/nginx/nginx.conf": NOW - timedelta(days=402),
|
|
"etc/fstab": NOW - timedelta(days=611),
|
|
"etc/hostname": NOW - timedelta(days=611),
|
|
}
|
|
GRUNDALTER = timedelta(days=611) # der Tag, an dem der Server aufgesetzt wurde
|
|
|
|
|
|
def _build_tree():
|
|
"""Ein glaubhaftes Linux-Wurzelverzeichnis fuer den Explorer."""
|
|
def make(base, spec, prefix=""):
|
|
for name, value in spec.items():
|
|
path = os.path.join(base, name)
|
|
relativ = "%s/%s" % (prefix, name) if prefix else name
|
|
if isinstance(value, dict):
|
|
os.makedirs(path, exist_ok=True)
|
|
make(path, value, relativ)
|
|
elif isinstance(value, str):
|
|
# Echter Inhalt - damit die Dateivorschau (F3) etwas zu zeigen hat.
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
handle.write(value)
|
|
else:
|
|
with open(path, "wb") as handle:
|
|
handle.truncate(value)
|
|
moment = MTIMES.get(relativ, NOW - GRUNDALTER)
|
|
stamp = moment.timestamp()
|
|
os.utime(path, (stamp, stamp))
|
|
|
|
root = os.path.join(DEMO, "snapshot")
|
|
os.makedirs(root, exist_ok=True)
|
|
make(root, TREE)
|
|
|
|
lokal = os.path.join(DEMO, "lokal")
|
|
os.makedirs(lokal, exist_ok=True)
|
|
make(lokal, {"werkzeuge": {"pg_repack.deb": 412_160, "pruefen.sh": 2_048},
|
|
"holen": {}, "notizen.md": 3_190})
|
|
|
|
# Was auf einem Transfer-Laufwerk liegt, wenn man es vorbereitet hat.
|
|
transfer = os.path.join(DEMO, "transfer-dumps")
|
|
os.makedirs(transfer, exist_ok=True)
|
|
make(transfer, {"LIESMICH.txt": LIESMICH,
|
|
"werkzeuge": {"pg_dump-14": 1_284_096, "7z.exe": 1_140_224},
|
|
"eingang": {}, "ausgang": {}})
|
|
|
|
for basis, alter in ((lokal, timedelta(days=3)), (transfer, timedelta(days=41))):
|
|
stamp = (NOW - alter).timestamp()
|
|
for wurzel, ordner, dateien in os.walk(basis):
|
|
for name in list(ordner) + list(dateien):
|
|
os.utime(os.path.join(wurzel, name), (stamp, stamp))
|
|
os.utime(wurzel, (stamp, stamp))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(build())
|