Merge remote-tracking branch 'origin/develop' into HEAD

This commit is contained in:
MacRimi
2026-10-16 23:58:03 +02:00
92 changed files with 5099 additions and 394 deletions
+73 -1
View File
@@ -95,6 +95,8 @@ from flask_notification_routes import notification_bp # noqa: E402
from flask_oci_routes import oci_bp # noqa: E402
from flask_audit_routes import audit_bp # noqa: E402
from flask_vm_backup_routes import vm_backup_bp # noqa: E402
from guest_backup_archives import (BackupArchiveError, delete_guest_backups,
guest_backup_deletion_status) # noqa: E402
from notification_manager import notification_manager, resolve_notification_hostname # noqa: E402
import post_install_versions # noqa: E402 — Sprint 12A: detect post-install function updates
from jwt_middleware import require_auth, require_auth_or_ticket, require_admin_scope # noqa: E402
@@ -13272,6 +13274,32 @@ def api_create_backup(vmid):
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/vms/<int:vmid>/backups/delete', methods=['POST'])
@require_admin_scope
def api_delete_vm_backups(vmid):
"""Delete an explicitly confirmed selection, revalidated by Proxmox."""
try:
return jsonify(delete_guest_backups(vmid, request.get_json(silent=True)))
except BackupArchiveError as error:
return jsonify({'error': str(error)}), error.code
finally:
# Native deletion may have started even if the pvesh response was lost.
_vm_cache_invalidate(vmid, _vm_backups_cache)
@app.route('/api/vms/<int:vmid>/backups/delete-status', methods=['GET'])
@require_admin_scope
def api_vm_backup_deletion_status(vmid):
"""Only poll deletion tasks started here for this guest."""
try:
result = guest_backup_deletion_status(vmid, request.args.get('task'))
if result['status'] != 'pending':
_vm_cache_invalidate(vmid, _vm_backups_cache)
return jsonify(result)
except BackupArchiveError as error:
return jsonify({'error': str(error)}), error.code
@app.route('/api/vms/<int:vmid>/backups', methods=['GET'])
@require_auth
def api_vm_backups(vmid):
@@ -13352,6 +13380,7 @@ def api_vm_backups(vmid):
'timestamp': ctime,
'date': datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M') if ctime else '',
'notes': item.get('notes', ''),
'protected': item.get('protected', False) not in (False, 0, '0', '', None),
})
except Exception:
pass
@@ -22214,6 +22243,7 @@ def api_proxmenux_self_update_status():
# so scheduled and manual applies cannot overlap.
_APPLY_UPDATES_SCRIPT = _LXC_APPLY_UPDATES_SCRIPT
_SCHEDULED_STOPPED_EXIT = 9
_DOCKER_ENGINE_INTEGRATED_COMMAND = (
'python3 /usr/local/share/proxmenux/monitor-app/usr/bin/'
'update_docker_engine.py --vmid "$VMID"'
@@ -22255,6 +22285,23 @@ def _append_lxc_update_log(path: str | None, text: str) -> None:
print(f'[ProxMenux] scheduler: could not append update log: {exc}', flush=True)
def _scheduled_runner_reported_stopped(result, log_path: str | None, marker: str) -> bool:
"""Return true only for the scheduler's own pre-start stopped marker."""
if result.returncode != _SCHEDULED_STOPPED_EXIT or not marker:
return False
if log_path:
try:
with open(log_path, 'rb') as stream:
stream.seek(0, os.SEEK_END)
stream.seek(max(0, stream.tell() - 4096))
output = stream.read().decode('utf-8', errors='replace')
except OSError:
return False
else:
output = (result.stdout or '') + (result.stderr or '')
return marker in output
def _prune_lxc_update_logs(vmid: int) -> None:
try:
candidates = sorted(
@@ -22648,8 +22695,10 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
requested_targets = _normalise_schedule_targets(sched)
targets = list(requested_targets)
before = _lxc_update_snapshot(vmid)
requested_target = sched.get("target") or "both"
deferred_targets: list[str] = []
reasons: list[str] = []
reason_code: str | None = None
reboot_required: bool | None = None
reboot_packages: list[str] = []
reboot_check_error: str | None = None
@@ -22701,6 +22750,7 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
'status': status,
'target': actual_target,
'reason': reason,
'reason_code': reason_code,
'run_id': run_id,
'requested_targets': requested_targets,
'executed_targets': list(executed),
@@ -22712,10 +22762,23 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
'finalization': finalization,
}
requested_target = sched.get("target") or "both"
if not originally_running:
reason_code = 'stopped_container'
reasons.append('container is stopped; scheduled update was skipped')
return finish('skipped', requested_target, [])
if requested_targets == ['oci_image']:
# A container installed by OCI manager Apps is updated by replacing
# its image, through the same flow as the OCI menu, unattended.
# OCI updates have their own runner rather than apply_updates.sh.
# Check once more immediately before handing control to it: a user
# may have intentionally stopped the CT after the scheduler's first
# observation, and the OCI transaction can start a replacement CT
# while validating its new image.
if _fast_guest_status(vmid, 'lxc') != 'running':
reason_code = 'stopped_container'
reasons.append('container was stopped before scheduled update could begin')
return finish('skipped', 'oci_image', [])
command = ['bash', '/usr/local/share/proxmenux/scripts/oci/oci_manager_apps.sh',
'manage', str(vmid), '--action', 'update', '--unattended']
storage = (sched.get('backup_storage') or '').strip()
@@ -22856,6 +22919,9 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
target = "both" if has_os_target and has_app_targets else ("os" if has_os_target else "app")
env = dict(os.environ)
env["VMID"] = str(vmid)
env["EXECUTION_MODE"] = "scheduled"
scheduled_stopped_marker = f"PROXMENUX_SCHEDULED_STOPPED:{run_id}"
env["SCHEDULED_STOPPED_MARKER"] = scheduled_stopped_marker
env["TARGET"] = target
env["BACKUP"] = "1" if sched.get("backup") else "0"
env["BACKUP_STORAGE"] = sched.get("backup_storage") or ""
@@ -22921,6 +22987,10 @@ def _run_scheduled_update(vmid: int, sched: dict) -> dict:
text=True,
timeout=60 * 60,
)
if _scheduled_runner_reported_stopped(r, log_path, scheduled_stopped_marker):
reason_code = 'stopped_container'
reasons.append('container was stopped before scheduled update could begin')
return finish('skipped', target, [])
reboot_required, reboot_packages, reboot_check_error = (
_inspect_lxc_reboot_requirement(
vmid,
@@ -22996,12 +23066,14 @@ def _scheduler_loop():
status = outcome.get('status') or 'failure'
actual_target = outcome.get('target') or 'both'
reason = outcome.get('reason')
reason_code = outcome.get('reason_code')
try:
lxc_apps.record_schedule_run(
_vmid,
status,
actual_target,
reason,
reason_code=reason_code,
log_name=outcome.get('log_name'),
reboot_required=outcome.get('reboot_required'),
reboot_packages=outcome.get('reboot_packages'),