ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes

OCI manager Apps
- App tab: containers installed from an OCI image are identified from their
  installation record; the application and image versions are shown and an
  update is detected by image digest; repository link; Refresh data.
- Updates tab for OCI containers: Update and Recreate run the same flow as the
  OCI menu in the Monitor terminal; the pre-update backup can be kept in a
  backup storage; scheduled image updates with an optional minimum age.
- Logs tab: console output of the application, kept on the host
  (lxc.console.logfile + logrotate) and followed live.
- The Proxmox console opens a shell (cmode: shell) when the image has one.
- A damaged image download is fetched again before failing.
- Multi-container applications open at their LAN address; volume mount
  points on block storage report their usage.

Monitor
- Proxmox notifications are delivered to a loopback-only HTTP listener when
  HTTPS is enabled, so they no longer fail certificate verification.
- Log persistence counts recurring patterns only; an ended burst is not
  reported as persistent and its warning clears on its own (#386).
- Proxmox notification config backups are deduplicated and capped at three.
- The update icon on the Apps page opens the container on its Updates tab.
- Version 1.2.6.2-beta and its release notes in every Monitor language.

Docs
- OCI manager Apps and Audit & Report rebuilt as per-page message files,
  with a new page for OCI containers in the Monitor.
- Seven pages fixed where rich-text tags were missing from t.rich.

Translations
- Spanish fixes across the OCI engine, the Monitor and the TUI menus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-09-25 21:51:12 +02:00
co-authored by Claude Opus 5.5
parent 386d33df6e
commit 4437a671d2
524 changed files with 14459 additions and 3841 deletions
+57 -6
View File
@@ -21,6 +21,34 @@ from oci_ui import translate, msg_error, msg_ok
ROOT = Path('/var/lib/proxmenux/oci-installations')
FIELDS = ('Entrypoint', 'Cmd', 'Env', 'User', 'WorkingDir', 'StopSignal', 'Volumes', 'ExposedPorts', 'Healthcheck')
VERSION_ENV_RE = re.compile(r'^([A-Z][A-Z0-9_]*)_VERSION$')
# Repositories whose version variable is not named after the image.
VERSION_ENV_NAMES = {'postgres': 'PG'}
def version_from_environment(repository, environment):
"""The application version an image states in its own environment.
Official library images publish no labels at all, yet they carry the
version as NEXTCLOUD_VERSION, REDIS_VERSION, MONGO_VERSION. Most also
carry their dependencies there — GOSU_VERSION, PHP_VERSION, NJS_VERSION —
so only a variable that names this image is accepted. A version variable
that names something else is never taken for the application's, not even
when it is the only one: an application built on a Python base carries
PYTHON_VERSION alone, and Paperless-ngx would read as 3.14.7. Anything
that does not name the image is left to the label.
"""
found = {}
for entry in environment or []:
name, _, value = entry.partition('=')
match = VERSION_ENV_RE.match(name)
if match and value.strip():
found[match.group(1)] = value.strip()
if not found:
return None
basename = repository.rsplit('/', 1)[-1]
expected = VERSION_ENV_NAMES.get(basename, basename.replace('-', '_')).upper()
return found.get(expected)
def command(*args):
@@ -151,8 +179,19 @@ def resolve_candidate(reference, architecture):
if config.get('architecture') != architecture or config.get('os') != 'linux':
raise ValueError(translate('Incompatible image platform'))
labels = config.get('config', {}).get('Labels') or {}
return {'manifest_digest': digest, 'defaults': {k: config.get('config', {}).get(k) for k in FIELDS},
'version': labels.get('org.opencontainers.image.version') or labels.get('build_version')}
defaults = {k: config.get('config', {}).get(k) for k in FIELDS}
# The environment is read first because a self-naming variable cannot be
# inherited: `org.opencontainers.image.version` is copied from the base
# image by enough publishers that mongo and rabbitmq both report the
# Ubuntu release there instead of their own version.
version = (version_from_environment(repo, defaults.get('Env'))
or labels.get('org.opencontainers.image.version')
or labels.get('build_version'))
# The build date identifies the image as the publisher released it: it is
# what changes when an image is rebuilt, whether or not the application
# version inside it moved.
return {'manifest_digest': digest, 'defaults': defaults, 'version': version,
'created': config.get('created')}
def compare(record, current, candidate=None):
@@ -189,12 +228,24 @@ def compare(record, current, candidate=None):
'automatic_update_enabled': False, 'blockers': blockers,
'requires': ['consistent-backup', 'review-rootfs-only-data', 'transactional-updater-not-implemented']}
if candidate:
report['update_available'] = candidate['manifest_digest'] != record['image']['manifest_digest']
report['changed_image_fields'] = [key for key in FIELDS if record['image']['defaults'].get(key) != candidate['defaults'].get(key)]
replaced = candidate['manifest_digest'] != record['image']['manifest_digest']
report['update_available'] = replaced
report['candidate_digest'] = candidate['manifest_digest']
# The two sides read the image configuration through different paths:
# the record from the archive's own config blob, the candidate from
# `skopeo inspect --config`, which normalises to the OCI schema and
# drops Docker extensions such as Healthcheck. A field the candidate
# cannot report is unknown, not changed, and an identical digest means
# the configuration is byte-identical whatever either side returns.
report['changed_image_fields'] = [
key for key in FIELDS
if replaced and candidate['defaults'].get(key) is not None
and record['image']['defaults'].get(key) != candidate['defaults'].get(key)
]
old_env = dict(v.split('=', 1) for v in record['image']['defaults'].get('Env') or [] if '=' in v)
new_env = dict(v.split('=', 1) for v in candidate['defaults'].get('Env') or [] if '=' in v)
report['changed_environment_names'] = sorted(k for k in old_env.keys() | new_env.keys() if old_env.get(k) != new_env.get(k))
report['candidate_digest'] = candidate['manifest_digest']
report['changed_environment_names'] = sorted(
k for k in old_env.keys() | new_env.keys() if replaced and old_env.get(k) != new_env.get(k))
return report