Files
CyberPanel/manageServices/application_page_meta.py
master3395 ef0f12f55a manageServices: align version cache TTL and application page meta
Raise default CYBERCP_MANAGED_APPS version cache TTL to 3600s to match
Manage Applications inventory behavior and reduce cold DNF fetches.
Refresh application_page_meta and synced manageServices static assets.
2026-04-03 21:21:21 +02:00

193 lines
7.7 KiB
Python

# -*- coding: utf-8 -*-
"""Server-side metadata for Manage Applications page (version lists in HTML)."""
import json
import os
import threading
import time
from django.utils.translation import gettext as _
from .application_detection import detect_app_state, managed_apps_os_support
from .application_versions import get_available_versions, version_compare
_APP_IMAGES = {
'Elasticsearch': '/static/manageServices/images/elastic-search.png',
'Redis': '/static/manageServices/images/redis.png',
'RabbitMQ': '/static/manageServices/images/rabbitmq-logo.svg',
}
# Cache only repoquery/dnf-backed version lists (slow). Install state is always refreshed.
# Override with CYBERCP_MANAGED_APPS_VERSIONS_INVENTORY_TTL (seconds), default 3600 (1 hour).
_VERSIONS_INVENTORY_TTL_SECONDS = int(
os.environ.get('CYBERCP_MANAGED_APPS_VERSIONS_INVENTORY_TTL', '3600')
)
_VERSIONS_INVENTORY_CACHE = {}
_VERSIONS_INVENTORY_LOCK = threading.Lock()
def _versions_inventory_cache_get(cache_key):
now = time.time()
with _VERSIONS_INVENTORY_LOCK:
item = _VERSIONS_INVENTORY_CACHE.get(cache_key)
if not item:
return None
ts, inventory = item
if now - ts > _VERSIONS_INVENTORY_TTL_SECONDS:
try:
del _VERSIONS_INVENTORY_CACHE[cache_key]
except Exception:
pass
return None
return {k: list(v) for k, v in inventory.items()}
def _versions_inventory_cache_put(cache_key, inventory):
with _VERSIONS_INVENTORY_LOCK:
if len(_VERSIONS_INVENTORY_CACHE) > 16:
_VERSIONS_INVENTORY_CACHE.clear()
snap = {k: list(v) for k, v in (inventory or {}).items()}
_VERSIONS_INVENTORY_CACHE[cache_key] = (time.time(), snap)
def _cold_fetch_version_inventory(major, rmq, support):
"""Populate version lists from package managers (DNF/apt); can take many seconds."""
inv = {}
if not support.get('supported'):
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
inv[app_name] = []
return inv
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
try:
inv[app_name] = get_available_versions(app_name, major, rmq)
except BaseException:
inv[app_name] = []
return inv
def _resolve_version_inventory(cache_key, major, rmq, support):
cached = _versions_inventory_cache_get(cache_key)
if cached is not None:
return cached
inv = _cold_fetch_version_inventory(major, rmq, support)
_versions_inventory_cache_put(cache_key, inv)
return inv
def _assemble_manage_applications_payload(major, rmq, support, version_inv):
"""Build services + bootstrap JSON from fresh install state and cached (or new) version lists."""
services = []
bootstrap_apps = []
for app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'):
state = detect_app_state(app_name)
services.append({
'image': _APP_IMAGES[app_name],
'name': app_name,
'installed': 'Installed' if state['installed'] else 'Not-Installed',
'installedVersion': state.get('installedVersion', ''),
})
versions = list(version_inv.get(app_name) or [])
latest_branch = ''
latest_global = ''
if versions:
latest_branch = versions[0]
latest_global = latest_branch
installed_version = state['installedVersion']
if installed_version and installed_version not in versions:
prepend_installed = True
if app_name == 'RabbitMQ':
from manageServices.application_rabbitmq_repo import (
filter_versions_for_stream,
)
prepend_installed = bool(
filter_versions_for_stream([installed_version], rmq)
)
if prepend_installed:
versions = [installed_version] + versions
ref_latest = latest_global or latest_branch
update_available = bool(
state['installed']
and installed_version
and ref_latest
and version_compare(installed_version, ref_latest) < 0
)
rabbitmq_versions_hint = ''
if app_name == 'RabbitMQ' and not versions:
if rmq == '4':
rabbitmq_versions_hint = _(
'Your OS is not unsupported: upstream RabbitMQ publishes 4.x RPMs suitable for '
'RHEL/Alma/Rocky 8 and 9 (RPM filenames may still contain el8; that is normal). '
'If this list stays empty, repository metadata may not expose 4.x to dnf yet—'
'refresh metadata (dnf makecache -y) or install the official .rpm from rabbitmq.com. '
'Check with: dnf repoquery rabbitmq-server --available --show-duplicates '
'(4.x lines look like rabbitmq-server-0:4.x.y-1.el8.noarch — search for :4., not a space after the colon).'
)
else:
rabbitmq_versions_hint = _(
'No 3.x builds were returned for this stream after refreshing Team RabbitMQ repos. '
'This is usually metadata or repo state—not OS support. Try: dnf makecache -y, '
'then dnf repoquery rabbitmq-server --available --show-duplicates.'
)
bootstrap_apps.append({
'name': app_name,
'installed': state['installed'],
'installedVersion': installed_version,
'latestAvailable': latest_branch,
'latestOverall': latest_global,
'updateAvailable': update_available,
'crossBranchUpdateSuggested': False,
'versions': versions,
'packageName': state['packageName'],
'adopted': bool(state['installed'] and not state['markerExists']),
'major': major if app_name == 'Elasticsearch' else '',
'rabbitmqStream': rmq if app_name == 'RabbitMQ' else '',
'rabbitmqVersionsHint': rabbitmq_versions_hint,
})
bootstrap = {'status': 1, 'apps': bootstrap_apps}
meta_json = json.dumps(bootstrap, ensure_ascii=False)
return services, meta_json
def build_manage_applications_page_data(es_major='8', rabbitmq_stream='4'):
"""
Build `services` for card HTML and a JSON-serializable bootstrap matching
/manageservices/applicationMeta shape (default ES major 8, RMQ stream 4).
Version lists are cached for _VERSIONS_INVENTORY_TTL_SECONDS to avoid repeated
DNF/repoquery on every page view; install status is always detected live.
"""
support = managed_apps_os_support()
major = str(es_major).strip() if str(es_major).strip() in ('7', '8', '9') else '8'
rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4'
cache_key = 'major:{0}|rmq:{1}|support:{2}'.format(
major, rmq, 1 if support.get('supported') else 0
)
version_inv = _resolve_version_inventory(cache_key, major, rmq, support)
return _assemble_manage_applications_payload(major, rmq, support, version_inv)
def get_application_meta_response_dict(es_major='8', rabbitmq_stream='4'):
"""
JSON payload for POST /manageservices/applicationMeta.
Shares the same version-list inventory cache as the Manage Applications HTML bootstrap.
"""
support = managed_apps_os_support()
major = str(es_major).strip() if str(es_major).strip() in ('7', '8', '9') else '8'
rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4'
_, meta_json = build_manage_applications_page_data(major, rmq)
payload = json.loads(meta_json)
return {
'status': 1,
'osSupportedForManagedApps': support['supported'],
'unsupportedReason': support['reason'],
'apps': payload.get('apps') or [],
}