From 82ec34f3396ba591e41fd9979d88dc030269a9ae Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 1 Apr 2026 00:35:22 +0200 Subject: [PATCH] Manage Applications: Elasticsearch, Redis, RabbitMQ installers and UI hardening Add modular application packages with backup-aware install/upgrade/downgrade, DNF-backed version lists with TTL caching, and HTML bootstrap for faster loads. Improve the version picker (labels, selection state, background meta refresh) and route applicationMeta through shared page meta cache. Update static assets and cache buster for manageServices.js. Repository also includes related updates to serviceManager, upgrade tooling, website functions, and user management from this development tree. --- .../templates/baseTemplate/index.html | 8 +- manageServices/application_backup.py | 193 ++++++++ manageServices/application_detection.py | 141 ++++++ manageServices/application_elasticsearch.py | 212 +++++++++ manageServices/application_page_meta.py | 146 +++++++ manageServices/application_rabbitmq.py | 146 +++++++ manageServices/application_rabbitmq_repo.py | 225 ++++++++++ manageServices/application_redis.py | 132 ++++++ manageServices/application_versions.py | 380 ++++++++++++++++ manageServices/serviceManager.py | 298 ++++--------- .../static/manageServices/images/rabbitmq.png | Bin 0 -> 13428 bytes .../static/manageServices/manageServices.js | 412 +++++++++++++++++- .../manageServices/applications.html | 242 +++++++++- manageServices/urls.py | 1 + manageServices/views.py | 137 ++++-- plogical/upgrade.py | 52 +++ .../static/manageServices/manageServices.js | 412 +++++++++++++++++- .../static/userManagment/userManagment.js | 17 +- userManagment/views.py | 11 +- .../websiteFunctions/securityManagement.html | 322 -------------- websiteFunctions/urls.py | 7 - websiteFunctions/views.py | 34 -- websiteFunctions/website.py | 135 ------ 23 files changed, 2873 insertions(+), 790 deletions(-) create mode 100644 manageServices/application_backup.py create mode 100644 manageServices/application_detection.py create mode 100644 manageServices/application_elasticsearch.py create mode 100644 manageServices/application_page_meta.py create mode 100644 manageServices/application_rabbitmq.py create mode 100644 manageServices/application_rabbitmq_repo.py create mode 100644 manageServices/application_redis.py create mode 100644 manageServices/application_versions.py create mode 100644 manageServices/static/manageServices/images/rabbitmq.png delete mode 100644 websiteFunctions/templates/websiteFunctions/securityManagement.html diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index a090cfd21..771a0c947 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -2329,9 +2329,6 @@ AI Scanner - - Security Management - @@ -2507,7 +2504,7 @@ - + @@ -2953,9 +2950,6 @@ }); } - function loadSecurityManagement() { - window.open('{% url "securityManagementPage" %}', '_blank'); - } {% block footer_scripts %}{% endblock %} diff --git a/manageServices/application_backup.py b/manageServices/application_backup.py new file mode 100644 index 000000000..9f8fd0506 --- /dev/null +++ b/manageServices/application_backup.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +""" +Full config + data backups for managed applications (pre version change). +""" +import json +import os +import shutil +import subprocess +import tarfile +import time + +CONFIG_PATHS = { + 'Elasticsearch': ['/etc/elasticsearch'], + 'Redis': ['/etc/redis', '/etc/redis.conf'], + 'RabbitMQ': ['/etc/rabbitmq'], +} + +DATA_PATHS = { + 'Elasticsearch': ['/var/lib/elasticsearch'], + 'Redis': ['/var/lib/redis'], + 'RabbitMQ': ['/var/lib/rabbitmq'], +} + +SERVICE_UNITS = { + 'Elasticsearch': 'elasticsearch', + 'Redis': 'redis', + 'RabbitMQ': 'rabbitmq-server', +} + +CHOWN_CMDS = { + 'Elasticsearch': 'chown -R elasticsearch:elasticsearch /var/lib/elasticsearch /etc/elasticsearch', + 'Redis': 'chown -R redis:redis /var/lib/redis /etc/redis /etc/redis.conf 2>/dev/null; true', + 'RabbitMQ': 'chown -R rabbitmq:rabbitmq /var/lib/rabbitmq /etc/rabbitmq', +} + +BACKUP_ROOT = '/home/cyberpanel/backups/manageApplications' + + +def _existing_paths(app_name): + out = [] + for p in CONFIG_PATHS.get(app_name, []) + DATA_PATHS.get(app_name, []): + if os.path.exists(p): + out.append(p) + return out + + +def create_managed_app_backup(app_name, status_file): + """ + Tar config + data paths into BACKUP_ROOT///bundle.tar.gz. + Returns backup directory path, or '' on failure / nothing to back up. + """ + def log(msg): + try: + status_file.write(msg + '\n') + status_file.flush() + except Exception: + pass + + paths = _existing_paths(app_name) + if not paths: + log('No paths on disk to back up for {0}; skipping archive.'.format(app_name)) + return '' + + ts = int(time.time()) + safe = app_name.lower().replace(' ', '_') + backup_dir = os.path.join(BACKUP_ROOT, safe, str(ts)) + os.makedirs(backup_dir, mode=0o750, exist_ok=True) + archive = os.path.join(backup_dir, 'bundle.tar.gz') + + try: + with tarfile.open(archive, 'w:gz', compresslevel=6) as tf: + for abs_path in paths: + arc = abs_path.lstrip('/') + tf.add(abs_path, arcname=arc, recursive=True) + manifest = { + 'app': app_name, + 'created': ts, + 'paths': [p.lstrip('/') for p in paths], + } + with open(os.path.join(backup_dir, 'manifest.json'), 'w') as mh: + json.dump(manifest, mh, indent=2) + log('Backup created at {0}'.format(backup_dir)) + return backup_dir + except Exception as err: + log('Backup failed: {0}'.format(err)) + try: + shutil.rmtree(backup_dir, ignore_errors=True) + except Exception: + pass + return '' + + +def _archive_path(backup_dir): + return os.path.join(backup_dir, 'bundle.tar.gz') + + +def merge_data_from_backup(app_name, backup_dir, status_file): + """Overlay saved data directories from backup onto live system (preserves package layout).""" + def log(msg): + try: + status_file.write(msg + '\n') + status_file.flush() + except Exception: + pass + + arc = _archive_path(backup_dir) + if not os.path.isfile(arc): + log('No bundle at {0}; skip data merge.'.format(arc)) + return False + data_prefixes = [p.lstrip('/') for p in DATA_PATHS.get(app_name, [])] + if not data_prefixes: + return True + try: + with tarfile.open(arc, 'r:gz') as tf: + for m in tf.getmembers(): + name = m.name + if m.isfile() or m.isdir(): + for pref in data_prefixes: + if name == pref or name.startswith(pref + '/'): + tf.extract(m, path='/', set_attrs=False) + break + log('Merged data trees from backup for {0}.'.format(app_name)) + return True + except Exception as err: + log('Data merge failed: {0}'.format(err)) + return False + + +def restore_full_backup(backup_dir, status_file): + """Extract full bundle to / (recovery).""" + def log(msg): + try: + status_file.write(msg + '\n') + status_file.flush() + except Exception: + pass + + arc = _archive_path(backup_dir) + if not os.path.isfile(arc): + log('Cannot restore: missing {0}'.format(arc)) + return False + try: + with tarfile.open(arc, 'r:gz') as tf: + for m in tf.getmembers(): + tf.extract(m, path='/', set_attrs=False) + log('Full restore from backup completed.') + return True + except Exception as err: + log('Full restore failed: {0}'.format(err)) + return False + + +def cleanup_managed_backup(backup_dir, status_file): + def log(msg): + try: + status_file.write(msg + '\n') + status_file.flush() + except Exception: + pass + + if not backup_dir or not os.path.isdir(backup_dir): + return + try: + shutil.rmtree(backup_dir, ignore_errors=True) + log('Removed backup directory after successful change: {0}'.format(backup_dir)) + except Exception as err: + log('Could not remove backup dir: {0}'.format(err)) + + +def chown_app_paths(app_name, status_writer): + cmd = CHOWN_CMDS.get(app_name) + if not cmd: + return + try: + subprocess.call(cmd, shell=True, stdout=status_writer, stderr=status_writer) + except Exception: + pass + + +def service_is_active(app_name): + unit = SERVICE_UNITS.get(app_name) + if not unit: + return False + try: + r = subprocess.run( + ['systemctl', 'is-active', unit], + capture_output=True, + text=True, + timeout=30, + ) + return r.stdout.strip() == 'active' + except Exception: + return False diff --git a/manageServices/application_detection.py b/manageServices/application_detection.py new file mode 100644 index 000000000..7054d7248 --- /dev/null +++ b/manageServices/application_detection.py @@ -0,0 +1,141 @@ +import os +import re +import subprocess + + +APP_PACKAGE_MAP = { + 'Elasticsearch': { + 'rhel': 'elasticsearch', + 'debian': 'elasticsearch', + 'service': 'elasticsearch', + 'binary_paths': ['/usr/share/elasticsearch/bin/elasticsearch'] + }, + 'Redis': { + 'rhel': 'redis', + 'debian': 'redis-server', + 'service': 'redis', + 'binary_paths': ['/usr/bin/redis-server'] + }, + 'RabbitMQ': { + 'rhel': 'rabbitmq-server', + 'debian': 'rabbitmq-server', + 'service': 'rabbitmq-server', + 'binary_paths': ['/usr/sbin/rabbitmq-server', '/usr/lib/rabbitmq/bin/rabbitmq-server'] + } +} + +APP_MARKERS = { + 'Elasticsearch': '/home/cyberpanel/elasticsearch', + 'Redis': '/home/cyberpanel/redis', + 'RabbitMQ': '/home/cyberpanel/rabbitmq' +} + + +def _run(cmd): + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=12) + return res.returncode, (res.stdout or '').strip(), (res.stderr or '').strip() + except Exception as err: + return 1, '', str(err) + + +def is_debian_family(): + return os.path.exists('/etc/debian_version') or os.path.exists('/etc/lsb-release') + + +def is_centos7(): + release_paths = ['/etc/centos-release', '/etc/redhat-release', '/etc/os-release'] + text_blob = '' + for path in release_paths: + try: + if os.path.exists(path): + with open(path, 'r') as fh: + text_blob += fh.read().lower() + '\n' + except Exception: + continue + return ('centos' in text_blob and ('release 7' in text_blob or 'version_id="7' in text_blob)) + + +def managed_apps_os_support(): + if is_centos7(): + return { + 'supported': False, + 'reason': 'CentOS 7 is EOL and not supported for managed applications.' + } + return { + 'supported': True, + 'reason': '' + } + + +def package_name_for_app(app_name): + app_map = APP_PACKAGE_MAP.get(app_name, {}) + if not app_map: + return '' + if is_debian_family(): + return app_map.get('debian', '') + return app_map.get('rhel', '') + + +def _rpm_installed(pkg_name): + rc, out, _ = _run(['rpm', '-q', pkg_name]) + if rc == 0: + return True, out + return False, '' + + +def _dpkg_installed(pkg_name): + rc, out, _ = _run(['dpkg-query', '-W', '-f=${Version}', pkg_name]) + if rc == 0 and out: + return True, out + return False, '' + + +def _systemd_active(service_name): + rc, out, _ = _run(['systemctl', 'is-active', service_name]) + return rc == 0 and out.strip() == 'active' + + +def detect_installed_version(app_name): + pkg_name = package_name_for_app(app_name) + if not pkg_name: + return '' + + if is_debian_family(): + ok, ver = _dpkg_installed(pkg_name) + else: + ok, ver = _rpm_installed(pkg_name) + + if not ok: + return '' + + if app_name in ('Elasticsearch', 'Redis', 'RabbitMQ'): + match = re.search(r'(\d+\.\d+\.\d+)', ver) + return match.group(1) if match else ver + + return ver + + +def detect_app_state(app_name): + marker_path = APP_MARKERS.get(app_name, '') + package_name = package_name_for_app(app_name) + service_name = APP_PACKAGE_MAP.get(app_name, {}).get('service', '') + binary_paths = APP_PACKAGE_MAP.get(app_name, {}).get('binary_paths', []) + + installed_version = detect_installed_version(app_name) + marker_exists = bool(marker_path and os.path.exists(marker_path)) + service_active = _systemd_active(service_name) if service_name else False + binary_exists = any(os.path.exists(path) for path in binary_paths) + + installed = bool(installed_version or service_active or binary_exists) + + return { + 'appName': app_name, + 'packageName': package_name, + 'markerPath': marker_path, + 'markerExists': marker_exists, + 'installed': installed, + 'installedVersion': installed_version, + 'serviceActive': service_active, + 'binaryExists': binary_exists + } diff --git a/manageServices/application_elasticsearch.py b/manageServices/application_elasticsearch.py new file mode 100644 index 000000000..4f63ffd76 --- /dev/null +++ b/manageServices/application_elasticsearch.py @@ -0,0 +1,212 @@ +import os +import subprocess +import time + +from serverStatus.serverStatusUtil import ServerStatusUtil +from plogical import CyberCPLogFileWriter as logging +from manageServices.application_backup import ( + CHOWN_CMDS, + cleanup_managed_backup, + create_managed_app_backup, + merge_data_from_backup, + restore_full_backup, + service_is_active, +) +from manageServices.application_detection import detect_app_state, is_debian_family + + +def _es_major_normalized(es_major): + m = str(es_major).strip() + if m in ('7', '8', '9'): + return m + return '8' + + +def _write_repo(es_major): + major = _es_major_normalized(es_major) + if is_debian_family(): + repo_file = '/etc/apt/sources.list.d/elastic-{0}.x.list'.format(major) + cmd = 'echo "deb https://artifacts.elastic.co/packages/{0}.x/apt stable main" | sudo tee {1}'.format(major, repo_file) + subprocess.call(cmd, shell=True) + return repo_file + + repo_file = '/etc/yum.repos.d/elasticsearch.repo' + content = ''' +[elasticsearch] +name=Elasticsearch repository for {0}.x packages +baseurl=https://artifacts.elastic.co/packages/{0}.x/yum +gpgcheck=1 +gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch +enabled=0 +autorefresh=1 +type=rpm-md +'''.format(major) + with open(repo_file, 'w') as handle: + handle.write(content) + return repo_file + + +def _ensure_tmpdir(status_file): + ServerStatusUtil.executioner('mkdir -p /home/elasticsearch/tmp', status_file) + ServerStatusUtil.executioner('chown elasticsearch:elasticsearch /home/elasticsearch/tmp', status_file) + jvm_options = '/etc/elasticsearch/jvm.options' + line = '-Djava.io.tmpdir=/home/elasticsearch/tmp\n' + try: + if os.path.exists(jvm_options): + with open(jvm_options, 'r') as handle: + body = handle.read() + if line.strip() not in body: + with open(jvm_options, 'a') as handle: + handle.write(line) + except Exception: + pass + + +def adopt_or_reconcile(status_file): + state = detect_app_state('Elasticsearch') + if state['installed'] and not state['markerExists']: + ServerStatusUtil.executioner('touch /home/cyberpanel/elasticsearch', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, + 'Elasticsearch detected and adopted by marker reconciliation.\n' + ) + return state + + +def _resolve_target_version(version, es_major): + if version and str(version).strip() != 'latest': + return str(version).strip() + from manageServices.application_versions import get_latest_version + return get_latest_version('Elasticsearch', es_major, '3') or '' + + +def _run_elasticsearch_packages(version, es_major, status_file, allow_downgrade): + if is_debian_family(): + subprocess.call( + 'wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -', + shell=True, + ) + ServerStatusUtil.executioner('apt-get install apt-transport-https -y', status_file) + _write_repo(es_major) + ServerStatusUtil.executioner('apt-get update -y', status_file) + if version and version != 'latest': + cmd = ( + 'DEBIAN_FRONTEND=noninteractive apt-get install -y ' + '--allow-downgrades elasticsearch={0}' + ).format(version) + else: + cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install elasticsearch -y' + ServerStatusUtil.executioner(cmd, status_file) + return + + ServerStatusUtil.executioner( + 'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch', status_file + ) + _write_repo(es_major) + ad = ' --allow-downgrade' if allow_downgrade else '' + if version and version != 'latest': + cmd = 'dnf install{0} -y --enablerepo=elasticsearch elasticsearch-{1}'.format( + ad, version + ) + ServerStatusUtil.executioner(cmd, status_file) + else: + cmd = 'dnf install{0} -y --enablerepo=elasticsearch elasticsearch'.format(ad) + ServerStatusUtil.executioner(cmd, status_file) + + +def install(version='latest', es_major='8'): + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + adopt_or_reconcile(status_file) + + from manageServices.application_versions import version_compare + + state = detect_app_state('Elasticsearch') + backup_dir = '' + allow_downgrade = False + target = _resolve_target_version(version, es_major) + + if state['installed'] and state.get('installedVersion'): + status_file.write( + 'Pre-version-change backup and service stop (Elasticsearch)...\n' + ) + status_file.flush() + iv = state['installedVersion'] + if target and version_compare(iv, target) > 0: + allow_downgrade = True + status_file.write( + 'Downgrade path: allowing package manager downgrade where supported.\n' + ) + status_file.flush() + backup_dir = create_managed_app_backup('Elasticsearch', status_file) + ServerStatusUtil.executioner('systemctl stop elasticsearch', status_file) + + _run_elasticsearch_packages(version, es_major, status_file, allow_downgrade) + if backup_dir: + merge_data_from_backup('Elasticsearch', backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['Elasticsearch'], status_file) + + _ensure_tmpdir(status_file) + ServerStatusUtil.executioner('systemctl enable elasticsearch', status_file) + ServerStatusUtil.executioner('systemctl start elasticsearch', status_file) + time.sleep(3) + + if backup_dir: + if service_is_active('Elasticsearch'): + cleanup_managed_backup(backup_dir, status_file) + status_file.write( + 'Elasticsearch version change completed; backup removed after success.\n' + ) + else: + status_file.write( + 'Elasticsearch failed to start; restoring from backup...\n' + ) + restore_full_backup(backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['Elasticsearch'], status_file) + ServerStatusUtil.executioner('systemctl start elasticsearch', status_file) + time.sleep(2) + if not service_is_active('Elasticsearch'): + status_file.write( + 'Recovery unclear — backup kept at {0}\n'.format(backup_dir) + ) + else: + status_file.write( + 'Prior state restored from backup. Backup retained for safety.\n' + ) + status_file.flush() + + ServerStatusUtil.executioner('touch /home/cyberpanel/elasticsearch', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'Elasticsearch installed.[200]\n', 1 + ) + return 0 + + +def upgrade(version='latest', es_major='8'): + return install(version=version, es_major=es_major) + + +def remove(): + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + if is_debian_family(): + for major in ('7', '8', '9'): + path = '/etc/apt/sources.list.d/elastic-{0}.x.list'.format(major) + try: + os.remove(path) + except Exception: + pass + ServerStatusUtil.executioner( + 'DEBIAN_FRONTEND=noninteractive apt-get remove elasticsearch -y', status_file + ) + else: + try: + os.remove('/etc/yum.repos.d/elasticsearch.repo') + except Exception: + pass + ServerStatusUtil.executioner('yum erase elasticsearch -y', status_file) + + ServerStatusUtil.executioner('rm -rf /home/elasticsearch/tmp', status_file) + ServerStatusUtil.executioner('rm -f /home/cyberpanel/elasticsearch', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'Elasticsearch removed.[200]\n', 1 + ) + return 0 diff --git a/manageServices/application_page_meta.py b/manageServices/application_page_meta.py new file mode 100644 index 000000000..e949f827d --- /dev/null +++ b/manageServices/application_page_meta.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +"""Server-side metadata for Manage Applications page (version lists in HTML).""" +import json +import threading +import time + +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', +} + +_PAGE_META_TTL_SECONDS = 20 +_PAGE_META_CACHE = {} +_PAGE_META_LOCK = threading.Lock() + + +def _page_meta_cache_get(cache_key): + now = time.time() + with _PAGE_META_LOCK: + item = _PAGE_META_CACHE.get(cache_key) + if not item: + return None + ts, payload = item + if now - ts > _PAGE_META_TTL_SECONDS: + try: + del _PAGE_META_CACHE[cache_key] + except Exception: + pass + return None + services, meta_json = payload + return list(services), str(meta_json) + + +def _page_meta_cache_put(cache_key, services, meta_json): + with _PAGE_META_LOCK: + # Keep cache tiny (we only have a handful of key combos). + if len(_PAGE_META_CACHE) > 12: + _PAGE_META_CACHE.clear() + _PAGE_META_CACHE[cache_key] = ( + time.time(), + (list(services), str(meta_json)), + ) + + +def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): + """ + Build `services` for card HTML and a JSON-serializable bootstrap matching + /manageservices/applicationMeta shape (default ES major 8, RMQ stream 3). + """ + services = [] + bootstrap_apps = [] + 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 '3' + cache_key = 'major:{0}|rmq:{1}|support:{2}'.format( + major, rmq, 1 if support.get('supported') else 0 + ) + + cached = _page_meta_cache_get(cache_key) + if cached is not None: + return cached + + 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 = [] + latest_branch = '' + latest_global = '' + if support['supported']: + try: + versions = get_available_versions(app_name, major, rmq) + except BaseException: + versions = [] + if versions: + latest_branch = versions[0] + latest_global = latest_branch + + installed_version = state['installedVersion'] + if installed_version and installed_version not in versions: + 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 + ) + + 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 '', + }) + + bootstrap = {'status': 1, 'apps': bootstrap_apps} + meta_json = json.dumps(bootstrap, ensure_ascii=False) + _page_meta_cache_put(cache_key, services, meta_json) + return services, meta_json + + +def get_application_meta_response_dict(es_major='8', rabbitmq_stream='3'): + """ + JSON payload for POST /manageservices/applicationMeta. + Reuses the same TTL cache as the Manage Applications HTML bootstrap so + modal refresh hits warm cache after a page load (or prior request). + """ + 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 '3' + cache_key = 'major:{0}|rmq:{1}|support:{2}'.format( + major, rmq, 1 if support.get('supported') else 0 + ) + + cached = _page_meta_cache_get(cache_key) + if cached is not None: + _services, meta_json = cached + else: + _services, 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 [], + } diff --git a/manageServices/application_rabbitmq.py b/manageServices/application_rabbitmq.py new file mode 100644 index 000000000..d3c44f139 --- /dev/null +++ b/manageServices/application_rabbitmq.py @@ -0,0 +1,146 @@ +import time + +from serverStatus.serverStatusUtil import ServerStatusUtil +from plogical import CyberCPLogFileWriter as logging + +from manageServices.application_backup import ( + CHOWN_CMDS, + cleanup_managed_backup, + create_managed_app_backup, + merge_data_from_backup, + restore_full_backup, + service_is_active, +) +from manageServices.application_detection import detect_app_state, is_debian_family +from manageServices.application_rabbitmq_repo import ( + normalize_rabbitmq_stream, + ensure_rabbitmq_team_repos, + ensure_erlang_meets_minimum, +) + + +def adopt_or_reconcile(status_file): + state = detect_app_state('RabbitMQ') + if state['installed'] and not state['markerExists']: + ServerStatusUtil.executioner('touch /home/cyberpanel/rabbitmq', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, + 'RabbitMQ detected and adopted by marker reconciliation.\n' + ) + return state + + +def _resolve_target_version(version, stream): + if version and str(version).strip() != 'latest': + return str(version).strip() + from manageServices.application_versions import get_latest_version + return get_latest_version('RabbitMQ', '8', stream) or '' + + +def _run_rabbitmq_packages(version, status_file, allow_downgrade): + ad = ' --allow-downgrade' if allow_downgrade else '' + if is_debian_family(): + if version and version != 'latest': + cmd = ( + 'DEBIAN_FRONTEND=noninteractive apt-get install -y ' + '--allow-downgrades rabbitmq-server={0}' + ).format(version) + else: + cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install rabbitmq-server -y' + ServerStatusUtil.executioner(cmd, status_file) + return + + if version and version != 'latest': + cmd = 'dnf install{0} -y rabbitmq-server-{1}'.format(ad, version) + ServerStatusUtil.executioner(cmd, status_file) + else: + cmd = 'dnf install{0} -y rabbitmq-server'.format(ad) + ServerStatusUtil.executioner(cmd, status_file) + + +def install(version='latest', stream='3'): + stream = normalize_rabbitmq_stream(stream) + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + adopt_or_reconcile(status_file) + + from manageServices.application_versions import version_compare + + ensure_rabbitmq_team_repos(stream, status_file=status_file) + ensure_erlang_meets_minimum(stream, version, status_file=status_file) + + state = detect_app_state('RabbitMQ') + backup_dir = '' + allow_downgrade = False + target = _resolve_target_version(version, stream) + + if state['installed'] and state.get('installedVersion'): + status_file.write( + 'Pre-version-change backup and service stop (RabbitMQ)...\n' + ) + status_file.flush() + iv = state['installedVersion'] + if target and version_compare(iv, target) > 0: + allow_downgrade = True + status_file.write('Downgrade path enabled for RabbitMQ.\n') + status_file.flush() + backup_dir = create_managed_app_backup('RabbitMQ', status_file) + ServerStatusUtil.executioner('systemctl stop rabbitmq-server', status_file) + + _run_rabbitmq_packages(version, status_file, allow_downgrade) + if backup_dir: + merge_data_from_backup('RabbitMQ', backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['RabbitMQ'], status_file) + + ServerStatusUtil.executioner('systemctl enable rabbitmq-server', status_file) + ServerStatusUtil.executioner('systemctl start rabbitmq-server', status_file) + time.sleep(4) + + if backup_dir: + if service_is_active('RabbitMQ'): + cleanup_managed_backup(backup_dir, status_file) + status_file.write( + 'RabbitMQ version change completed; backup removed after success.\n' + ) + else: + status_file.write('RabbitMQ failed to start; restoring from backup...\n') + restore_full_backup(backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['RabbitMQ'], status_file) + ServerStatusUtil.executioner('systemctl start rabbitmq-server', status_file) + time.sleep(4) + if not service_is_active('RabbitMQ'): + status_file.write( + 'Recovery unclear — backup kept at {0}\n'.format(backup_dir) + ) + else: + status_file.write( + 'Prior state restored from backup. Backup retained for safety.\n' + ) + status_file.flush() + + ServerStatusUtil.executioner('touch /home/cyberpanel/rabbitmq', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'RabbitMQ installed.[200]\n', 1 + ) + return 0 + + +def upgrade(version='latest', stream='3'): + return install(version=version, stream=stream) + + +def remove(): + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + ServerStatusUtil.executioner('systemctl stop rabbitmq-server', status_file) + ServerStatusUtil.executioner('systemctl disable rabbitmq-server', status_file) + if is_debian_family(): + ServerStatusUtil.executioner( + 'DEBIAN_FRONTEND=noninteractive apt-get remove rabbitmq-server -y', + status_file, + ) + else: + ServerStatusUtil.executioner('yum erase rabbitmq-server -y', status_file) + ServerStatusUtil.executioner('rm -f /home/cyberpanel/rabbitmq', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'RabbitMQ removed.[200]\n', 1 + ) + return 0 diff --git a/manageServices/application_rabbitmq_repo.py b/manageServices/application_rabbitmq_repo.py new file mode 100644 index 000000000..7f7928713 --- /dev/null +++ b/manageServices/application_rabbitmq_repo.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +""" +Team RabbitMQ package repositories (Packagecloud) and Erlang compatibility +for RabbitMQ 3.x vs 4.x installation streams. +""" +import re +import subprocess + +from manageServices.application_detection import is_debian_family + +# Official Packagecloud install scripts (RabbitMQ team). +_RPM_ERLANG_SCRIPT = ( + 'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-erlang/script.rpm.sh' +) +_RPM_SERVER_SCRIPT = ( + 'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh' +) +_DEB_ERLANG_SCRIPT = ( + 'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-erlang/script.deb.sh' +) +_DEB_SERVER_SCRIPT = ( + 'https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.deb.sh' +) + +# Minimum OTP major for each product stream (see rabbitmq.com docs / compatibility). +_MIN_OTP_STREAM_3 = 25 +_MIN_OTP_STREAM_4 = 26 + + +def _run(cmd, timeout=300): + try: + res = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout, shell=False + ) + return res.returncode, (res.stdout or ''), (res.stderr or '') + except Exception as err: + return 1, '', str(err) + + +def _run_shell_trusted(script_url, timeout=300): + """Run packagecloud install script from fixed RabbitMQ-team URL only.""" + allowed = {_RPM_ERLANG_SCRIPT, _RPM_SERVER_SCRIPT, _DEB_ERLANG_SCRIPT, _DEB_SERVER_SCRIPT} + if script_url not in allowed: + return 1, '', 'Invalid repository script URL.' + # curl -fsSL ... | bash (URLs are allowlisted above) + cmd = 'curl -1fsSL {0} | bash'.format(script_url) + return _run(['/bin/bash', '-lc', cmd], timeout=timeout) + + +def normalize_rabbitmq_stream(value): + s = str(value or '3').strip() + if s in ('4', '4.x', '41', '4.1'): + return '4' + return '3' + + +def _write_status(status_file, message): + if status_file is None: + return + try: + status_file.write(message + '\n') + status_file.flush() + except Exception: + pass + + +def ensure_rabbitmq_team_repos(stream, status_file=None): + """ + Idempotently enable rabbitmq-erlang and rabbitmq-server Packagecloud repos. + Required so 3.13.x, 4.x, and matching Erlang builds are visible to the + package manager. + """ + stream = normalize_rabbitmq_stream(stream) + _write_status( + status_file, + 'Ensuring Team RabbitMQ repositories (stream {0})...'.format(stream) + ) + if is_debian_family(): + rc, out, err = _run_shell_trusted(_DEB_ERLANG_SCRIPT) + if rc != 0: + _write_status(status_file, 'rabbitmq-erlang repo script: ' + (err or out or 'failed')) + rc2, out2, err2 = _run_shell_trusted(_DEB_SERVER_SCRIPT) + if rc2 != 0: + _write_status( + status_file, 'rabbitmq-server repo script: ' + (err2 or out2 or 'failed') + ) + _run(['apt-get', 'update', '-y'], timeout=120) + else: + rc, out, err = _run_shell_trusted(_RPM_ERLANG_SCRIPT) + if rc != 0: + _write_status(status_file, 'rabbitmq-erlang repo script: ' + (err or out or 'failed')) + rc2, out2, err2 = _run_shell_trusted(_RPM_SERVER_SCRIPT) + if rc2 != 0: + _write_status( + status_file, 'rabbitmq-server repo script: ' + (err2 or out2 or 'failed') + ) + # Prefer dnf; yum exists as symlink on EL8/9. + for cache_cmd in (['dnf', 'makecache', '-y'], ['yum', 'makecache', '-y']): + c_rc, _, _ = _run(cache_cmd, timeout=120) + if c_rc == 0: + break + _write_status(status_file, 'Team RabbitMQ repositories ready.') + + +def get_erlang_otp_major(): + """Best-effort current Erlang/OTP major version (integer or 0 if unknown).""" + rc, out, _ = _run( + [ + 'erl', + '-noshell', + '-eval', + 'io:format("~s~n", [erlang:system_info(otp_release)]), halt().', + ], + timeout=15, + ) + if rc == 0 and out: + m = re.search(r'(\d+)', out.strip()) + if m: + return int(m.group(1)) + # rpm: erlang from RabbitMQ repo may report R26 flavour + rc2, out2, _ = _run(['rpm', '-q', '--qf', '%{VERSION}', 'erlang'], timeout=10) + if rc2 == 0 and out2: + m = re.search(r'^(\d+)', out2.strip()) + if m: + return int(m.group(1)) + return 0 + + +def minimum_otp_for_stream(stream): + stream = normalize_rabbitmq_stream(stream) + if stream == '4': + return _MIN_OTP_STREAM_4 + return _MIN_OTP_STREAM_3 + + +def minimum_otp_for_rabbitmq_version(version_str): + """Infer OTP floor from chosen RabbitMQ version when possible.""" + if not version_str or version_str == 'latest': + return None + m = re.match(r'^(\d+)', str(version_str).strip()) + if not m: + return None + major = int(m.group(1)) + if major >= 4: + return _MIN_OTP_STREAM_4 + if major >= 3: + return _MIN_OTP_STREAM_3 + return None + + +def ensure_erlang_meets_minimum(stream, version, status_file=None): + """ + Upgrade/install Erlang from enabled repos if OTP is below the minimum + for the selected RabbitMQ stream or explicit target version. + """ + stream = normalize_rabbitmq_stream(stream) + need = minimum_otp_for_stream(stream) + version_floor = minimum_otp_for_rabbitmq_version(version) + if version_floor is not None: + need = max(need, version_floor) + + current = get_erlang_otp_major() + if current >= need: + _write_status( + status_file, + 'Erlang/OTP {0} satisfies minimum {1} for this RabbitMQ target.'.format( + current or 'unknown', need + ) + ) + return + + _write_status( + status_file, + 'Erlang/OTP {0} is below required {1}; installing/upgrading erlang from Team RabbitMQ repo...'.format( + current or 'unknown', need + ) + ) + if is_debian_family(): + _run( + [ + '/bin/bash', + '-lc', + 'DEBIAN_FRONTEND=noninteractive apt-get install -y erlang', + ], + timeout=600, + ) + else: + for inst in ( + ['dnf', 'install', '-y', 'erlang'], + ['yum', 'install', '-y', 'erlang'], + ): + rc, _, _ = _run(inst, timeout=600) + if rc == 0: + break + + after = get_erlang_otp_major() + if after < need: + _write_status( + status_file, + 'WARNING: Erlang may still be below OTP {0} (reported {1}). ' + 'Check /root/cyberpanel or logs and install correct erlang package.'.format( + need, after or 'unknown' + ) + ) + else: + _write_status(status_file, 'Erlang/OTP updated to {0}.'.format(after)) + + +def filter_versions_for_stream(versions, stream): + """Keep only versions whose major matches RabbitMQ stream (3 or 4).""" + stream = normalize_rabbitmq_stream(stream) + result = [] + seen = set() + for raw in versions or []: + v = (raw or '').strip() + if not v or v in seen: + continue + if v == 'latest': + continue + m = re.search(r'(\d+)\.(\d+)', v) + if m and m.group(1) == stream: + seen.add(v) + result.append(v) + # Preserve descending-ish order (caller already sorted newest first) + return result diff --git a/manageServices/application_redis.py b/manageServices/application_redis.py new file mode 100644 index 000000000..2dd2e46a8 --- /dev/null +++ b/manageServices/application_redis.py @@ -0,0 +1,132 @@ +import time + +from serverStatus.serverStatusUtil import ServerStatusUtil +from plogical import CyberCPLogFileWriter as logging + +from manageServices.application_backup import ( + CHOWN_CMDS, + cleanup_managed_backup, + create_managed_app_backup, + merge_data_from_backup, + restore_full_backup, + service_is_active, +) +from manageServices.application_detection import detect_app_state, is_debian_family + + +def adopt_or_reconcile(status_file): + state = detect_app_state('Redis') + if state['installed'] and not state['markerExists']: + ServerStatusUtil.executioner('touch /home/cyberpanel/redis', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, + 'Redis detected and adopted by marker reconciliation.\n' + ) + return state + + +def _resolve_target_version(version): + if version and str(version).strip() != 'latest': + return str(version).strip() + from manageServices.application_versions import get_latest_version + return get_latest_version('Redis', '8', '3') or '' + + +def _run_redis_packages(version, status_file, allow_downgrade): + ad = ' --allow-downgrade' if allow_downgrade else '' + if is_debian_family(): + if version and version != 'latest': + cmd = ( + 'DEBIAN_FRONTEND=noninteractive apt-get install -y ' + '--allow-downgrades redis-server={0}' + ).format(version) + else: + cmd = 'DEBIAN_FRONTEND=noninteractive apt-get install redis-server -y' + ServerStatusUtil.executioner(cmd, status_file) + return + + if version and version != 'latest': + cmd = 'dnf install{0} -y redis-{1}'.format(ad, version) + ServerStatusUtil.executioner(cmd, status_file) + else: + cmd = 'dnf install{0} -y redis'.format(ad) + ServerStatusUtil.executioner(cmd, status_file) + + +def install(version='latest'): + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + adopt_or_reconcile(status_file) + + from manageServices.application_versions import version_compare + + state = detect_app_state('Redis') + backup_dir = '' + allow_downgrade = False + target = _resolve_target_version(version) + + if state['installed'] and state.get('installedVersion'): + status_file.write('Pre-version-change backup and service stop (Redis)...\n') + status_file.flush() + iv = state['installedVersion'] + if target and version_compare(iv, target) > 0: + allow_downgrade = True + status_file.write('Downgrade path enabled for Redis.\n') + status_file.flush() + backup_dir = create_managed_app_backup('Redis', status_file) + ServerStatusUtil.executioner('systemctl stop redis', status_file) + + _run_redis_packages(version, status_file, allow_downgrade) + if backup_dir: + merge_data_from_backup('Redis', backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['Redis'], status_file) + + ServerStatusUtil.executioner('systemctl enable redis', status_file) + ServerStatusUtil.executioner('systemctl start redis', status_file) + time.sleep(2) + + if backup_dir: + if service_is_active('Redis'): + cleanup_managed_backup(backup_dir, status_file) + status_file.write( + 'Redis version change completed; backup removed after success.\n' + ) + else: + status_file.write('Redis failed to start; restoring from backup...\n') + restore_full_backup(backup_dir, status_file) + ServerStatusUtil.executioner(CHOWN_CMDS['Redis'], status_file) + ServerStatusUtil.executioner('systemctl start redis', status_file) + time.sleep(2) + if not service_is_active('Redis'): + status_file.write( + 'Recovery unclear — backup kept at {0}\n'.format(backup_dir) + ) + else: + status_file.write( + 'Prior state restored from backup. Backup retained for safety.\n' + ) + status_file.flush() + + ServerStatusUtil.executioner('touch /home/cyberpanel/redis', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'Redis installed.[200]\n', 1 + ) + return 0 + + +def upgrade(version='latest'): + return install(version=version) + + +def remove(): + status_file = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + if is_debian_family(): + ServerStatusUtil.executioner( + 'DEBIAN_FRONTEND=noninteractive apt-get remove redis-server -y', status_file + ) + else: + ServerStatusUtil.executioner('yum erase redis -y', status_file) + ServerStatusUtil.executioner('rm -f /home/cyberpanel/redis', status_file) + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, 'Redis removed.[200]\n', 1 + ) + return 0 diff --git a/manageServices/application_versions.py b/manageServices/application_versions.py new file mode 100644 index 000000000..4d6b2b8e3 --- /dev/null +++ b/manageServices/application_versions.py @@ -0,0 +1,380 @@ +import os +import re +import subprocess +import threading +import time + +from manageServices.application_detection import is_debian_family, package_name_for_app + +# applicationMeta can call get_available_versions many times per request (ES 7/8/9, RMQ 3/4). +# Concurrent DNF from every WSGI worker exhausts lscpd and returns HTTP 503. Cache + serialize cold fetches. +_VERSION_CACHE = {} +_VERSION_CACHE_LOCK = threading.Lock() +_DNF_COLD_FETCH_LOCK = threading.Lock() + +# Seconds; override with CYBERCP_MANAGED_APPS_VERSION_CACHE_TTL if needed +_CACHE_TTL_SEC = int(os.environ.get('CYBERCP_MANAGED_APPS_VERSION_CACHE_TTL', '300')) + + +def _version_cache_key(app_name, es_major, rabbitmq_stream): + debian = is_debian_family() + if app_name == 'Elasticsearch': + em = normalize_elasticsearch_major(es_major) + else: + em = '' + rs = '' + if app_name == 'RabbitMQ': + from manageServices.application_rabbitmq_repo import normalize_rabbitmq_stream + rs = normalize_rabbitmq_stream(rabbitmq_stream) + return (str(app_name), em, rs, debian) + + +def _cache_get_versions(key): + now = time.monotonic() + with _VERSION_CACHE_LOCK: + entry = _VERSION_CACHE.get(key) + if not entry: + return None + ts, versions = entry + if (now - ts) >= _CACHE_TTL_SEC: + try: + del _VERSION_CACHE[key] + except KeyError: + pass + return None + # Never use a poisoned empty cache (DNF timeout / lock) as a hit. + if not versions: + try: + del _VERSION_CACHE[key] + except KeyError: + pass + return None + return list(versions) + + +def _cache_put_versions(key, versions): + snap = list(versions or []) + if not snap: + return + with _VERSION_CACHE_LOCK: + _VERSION_CACHE[key] = (time.monotonic(), snap) + +# User-writable DNF snippet dir (panel runs as user `cyberpanel`; cannot rely on /etc). +_CYBERPANEL_DNF_EXTRA = '/home/cyberpanel/.cyberpanel-dnf/repos.d' + + +def _version_tuple(ver): + """Numeric tuple for semver-style compare; empty if not usable.""" + if ver is None: + return () + s = str(ver).strip() + if not s or s.lower() == 'latest': + return () + parts = [] + for x in re.findall(r'\d+', s): + try: + parts.append(int(x)) + except ValueError: + break + return tuple(parts) + + +def version_compare(a, b): + """ + Compare two version strings: return -1 if a < b, 0 if equal or incomparable, 1 if a > b. + """ + ta = _version_tuple(a) + tb = _version_tuple(b) + if not ta or not tb: + return 0 + length = max(len(ta), len(tb)) + for i in range(length): + x = ta[i] if i < len(ta) else 0 + y = tb[i] if i < len(tb) else 0 + if x < y: + return -1 + if x > y: + return 1 + return 0 + + +def _max_version_string(candidates): + best = '' + for v in candidates or []: + if not v: + continue + if not best or version_compare(best, v) < 0: + best = v + return best + + +def _run(cmd, timeout=120): + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + return res.returncode, (res.stdout or ''), (res.stderr or '') + except Exception as err: + return 1, '', str(err) + + +def normalize_elasticsearch_major(es_major): + """Supported Elasticsearch package streams (official artifacts.elastic.co).""" + m = str(es_major).strip() + if m in ('7', '8', '9'): + return m + return '8' + + +def _ensure_cyberpanel_es_repo(es_major): + """Elasticsearch official repo for version discovery (no root; gpg off for repoquery-only).""" + major = normalize_elasticsearch_major(es_major) + try: + os.makedirs(_CYBERPANEL_DNF_EXTRA, mode=0o755, exist_ok=True) + except Exception: + return + path = os.path.join( + _CYBERPANEL_DNF_EXTRA, 'cyberpanel-elasticsearch-{0}.repo'.format(major) + ) + content = ( + '[cyberpanel-elasticsearch-{0}]\n' + 'name=Elasticsearch {0}.x metadata (CyberPanel)\n' + 'baseurl=https://artifacts.elastic.co/packages/{0}.x/yum\n' + 'gpgcheck=0\n' + 'repo_gpgcheck=0\n' + 'enabled=1\n' + ).format(major) + try: + with open(path, 'w') as handle: + handle.write(content) + os.chmod(path, 0o644) + except Exception: + pass + + +def _normalize_versions(raw_versions, max_items=25): + versions = [] + seen = set() + for item in raw_versions: + value = (item or '').strip() + if not value or value in seen: + continue + seen.add(value) + versions.append(value) + return versions[:max_items] + + +def _sort_versions_desc(candidates): + def key_fn(ver): + nums = [int(x) for x in re.findall(r'\d+', ver) if x.isdigit()] + return nums or [0] + + try: + return sorted(set(candidates), key=key_fn, reverse=True) + except Exception: + return sorted(set(candidates), reverse=True) + + +def _dnf_reposdir_flag(use_cyberpanel_extra): + if not use_cyberpanel_extra: + return [] + if not os.path.isdir(_CYBERPANEL_DNF_EXTRA): + try: + os.makedirs(_CYBERPANEL_DNF_EXTRA, mode=0o755, exist_ok=True) + except Exception: + return [] + return ['--setopt=reposdir=/etc/yum.repos.d,{0}'.format(_CYBERPANEL_DNF_EXTRA)] + + +def _rhel_repoquery_versions(pkg_name, use_cyberpanel_extra_repos=False, enablerepos=None): + """ + Resolve distinct %{version} strings from enabled repos. + RPM NEVRA text parsing is brittle (el9_7 etc.); repoquery --qf is reliable. + """ + cmd = ( + ['dnf'] + + _dnf_reposdir_flag(use_cyberpanel_extra_repos) + + [ + 'repoquery', + '--show-duplicates', + '--latest-limit=50', + '--qf', + '%{version}', + pkg_name, + ] + ) + if enablerepos: + for repo_id in enablerepos: + cmd.extend(['--enablerepo', repo_id]) + rc, out, err = _run(cmd, timeout=180) + raw = [] + if rc == 0 and out.strip(): + for line in out.splitlines(): + v = (line or '').strip() + if v and re.match(r'^[0-9]', v): + raw.append(v) + if raw: + return _normalize_versions(_sort_versions_desc(raw)) + + # Legacy systems / fallback + rc2, out2, _ = _run( + ['yum', 'repoquery', '--show-duplicates', '--qf', '%{version}', pkg_name], + timeout=120, + ) + raw2 = [] + if rc2 == 0 and out2.strip(): + for line in out2.splitlines(): + v = (line or '').strip() + if v and re.match(r'^[0-9]', v): + raw2.append(v) + if raw2: + return _normalize_versions(_sort_versions_desc(raw2)) + + # Oldest fallback: yum list + rc3, out3, _ = _run(['yum', '--showduplicates', 'list', pkg_name], timeout=120) + if rc3 == 0: + for line in out3.splitlines(): + row = line.strip() + if not row or row.startswith('Loaded plugins') or row.startswith('Available'): + continue + fields = row.split() + if len(fields) >= 2 and pkg_name in fields[0]: + raw2.append(fields[1]) + if raw2: + return _normalize_versions(_sort_versions_desc(raw2)) + return [] + + +def _debian_versions(pkg_name): + versions = [] + _run(['apt-get', 'update', '-y'], timeout=180) + rc, out, _ = _run(['apt-cache', 'madison', pkg_name], timeout=60) + if rc != 0: + return [] + for line in out.splitlines(): + if '|' not in line: + continue + parts = [p.strip() for p in line.split('|')] + if len(parts) >= 2 and parts[1]: + versions.append(parts[1]) + collected = [] + for v in versions: + m = re.search(r'(\d+\.\d+\.\d+)', v) + collected.append(m.group(1) if m else v) + return _normalize_versions(_sort_versions_desc(collected)) + + +def _filter_es_major(versions, es_major): + major = normalize_elasticsearch_major(es_major) + out = [] + for v in versions or []: + head = (v.split('.') or [''])[0] + if head == major: + out.append(v) + return out + + +def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='3'): + pkg_name = package_name_for_app(app_name) + if app_name == 'Elasticsearch': + pkg_name = 'elasticsearch' + + if not pkg_name: + return [] + + rmq_stream = '3' + if app_name == 'RabbitMQ': + from manageServices.application_rabbitmq_repo import ( + normalize_rabbitmq_stream, + ensure_rabbitmq_team_repos, + filter_versions_for_stream, + ) + rmq_stream = normalize_rabbitmq_stream(rabbitmq_stream) + ensure_rabbitmq_team_repos(rmq_stream) + + if is_debian_family(): + versions = _debian_versions(pkg_name) + if app_name == 'Elasticsearch': + versions = _filter_es_major(versions, es_major) + else: + if app_name == 'Elasticsearch': + _ensure_cyberpanel_es_repo(es_major) + versions = _rhel_repoquery_versions( + pkg_name, use_cyberpanel_extra_repos=True + ) + versions = _filter_es_major(versions, es_major) + else: + versions = _rhel_repoquery_versions(pkg_name) + + if app_name == 'RabbitMQ': + from manageServices.application_rabbitmq_repo import filter_versions_for_stream + versions = filter_versions_for_stream(versions, rmq_stream) + return versions + + +def get_available_versions(app_name, es_major='8', rabbitmq_stream='3'): + """ + Cached wrapper: avoids hammering DNF from many concurrent panel workers (503 on Manage Applications). + """ + key = _version_cache_key(app_name, es_major, rabbitmq_stream) + hit = _cache_get_versions(key) + if hit is not None: + return hit + + with _DNF_COLD_FETCH_LOCK: + hit2 = _cache_get_versions(key) + if hit2 is not None: + return hit2 + versions = _get_available_versions_uncached( + app_name, es_major, rabbitmq_stream + ) + if versions: + _cache_put_versions(key, versions) + return list(versions) + + +def get_latest_version(app_name, es_major='8', rabbitmq_stream='3'): + versions = get_available_versions(app_name, es_major, rabbitmq_stream) + if not versions: + return '' + return versions[0] + + +def get_branch_and_global_latest(app_name, es_major='8', rabbitmq_stream='3'): + """ + Latest on the UI-selected branch/stream vs latest across all supported branches. + + Returns (latest_on_branch, latest_global). + """ + latest_branch = get_latest_version(app_name, es_major, rabbitmq_stream) + if app_name == 'Elasticsearch': + candidates = [] + for m in ('7', '8', '9'): + v = get_latest_version('Elasticsearch', m, rabbitmq_stream) + if v: + candidates.append(v) + latest_global = _max_version_string(candidates) if candidates else '' + elif app_name == 'RabbitMQ': + candidates = [] + for s in ('3', '4'): + v = get_latest_version('RabbitMQ', es_major, s) + if v: + candidates.append(v) + latest_global = _max_version_string(candidates) if candidates else '' + else: + latest_global = latest_branch + if not latest_global: + latest_global = latest_branch + return latest_branch, latest_global + + +def cross_branch_newer_suggested(installed, latest_branch, latest_global): + """ + True when installed is current (or ahead of) the selected branch latest but + a newer release exists on another line (e.g. 8.x latest installed, 9.x exists). + """ + if not installed or not latest_global: + return False + if version_compare(installed, latest_global) >= 0: + return False + if not latest_branch: + return True + return version_compare(installed, latest_branch) >= 0 diff --git a/manageServices/serviceManager.py b/manageServices/serviceManager.py index 79109638e..9518da6c2 100644 --- a/manageServices/serviceManager.py +++ b/manageServices/serviceManager.py @@ -1,6 +1,10 @@ +import os import os.path import sys import django +repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +if repo_root not in sys.path: + sys.path.append(repo_root) sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") django.setup() @@ -12,6 +16,10 @@ import argparse from serverStatus.serverStatusUtil import ServerStatusUtil from plogical import CyberCPLogFileWriter as logging import subprocess +from manageServices.application_detection import managed_apps_os_support +from manageServices import application_elasticsearch +from manageServices import application_redis +from manageServices import application_rabbitmq class ServiceManager: @@ -142,254 +150,114 @@ autosecondary=yes Supermasters(ip=self.extraArgs['masterServerIP'], nameserver=self.extraArgs['slaveServerNS'], account='').save() @staticmethod - def InstallElasticSearch(): + def InstallElasticSearch(version='latest', esMajor='8'): + return application_elasticsearch.install(version=version, es_major=esMajor) - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') - - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - repoPath = '/etc/yum.repos.d/elasticsearch.repo' - - content = ''' -[elasticsearch] -name=Elasticsearch repository for 7.x packages -baseurl=https://artifacts.elastic.co/packages/7.x/yum -gpgcheck=1 -gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch -enabled=0 -autorefresh=1 -type=rpm-md -''' - - writeToFile = open(repoPath, 'w') - writeToFile.write(content) - writeToFile.close() - - command = 'yum install --enablerepo=elasticsearch elasticsearch -y' - ServerStatusUtil.executioner(command, statusFile) - else: - command = 'wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -' - subprocess.call(command, shell=True) - - command = 'apt-get install apt-transport-https -y' - ServerStatusUtil.executioner(command, statusFile) - - command = 'echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list' - subprocess.call(command, shell=True) - - command = 'apt-get update -y' - ServerStatusUtil.executioner(command, statusFile) - - command = 'apt-get install elasticsearch -y' - ServerStatusUtil.executioner(command, statusFile) - - ### Tmp folder configurations - - command = 'mkdir -p /home/elasticsearch/tmp' - ServerStatusUtil.executioner(command, statusFile) - - command = 'chown elasticsearch:elasticsearch /home/elasticsearch/tmp' - ServerStatusUtil.executioner(command, statusFile) - - jvmOptions = '/etc/elasticsearch/jvm.options' - - writeToFile = open(jvmOptions, 'a') - writeToFile.write('-Djava.io.tmpdir=/home/elasticsearch/tmp\n') - writeToFile.close() - - command = 'systemctl enable elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - command = 'systemctl start elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - command = 'touch /home/cyberpanel/elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - - - logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, - "Packages successfully installed.[200]\n", 1) - return 0 + @staticmethod + def UpgradeElasticSearch(version='latest', esMajor='8'): + return application_elasticsearch.upgrade(version=version, es_major=esMajor) @staticmethod def RemoveElasticSearch(): - - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') - - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - repoPath = '/etc/yum.repos.d/elasticsearch.repo' - - try: - os.remove(repoPath) - except: - pass - - command = 'yum erase elasticsearch -y' - ServerStatusUtil.executioner(command, statusFile) - else: - - try: - os.remove('/etc/apt/sources.list.d/elastic-7.x.list') - except: - pass - - - command = 'apt-get remove elasticsearch -y' - ServerStatusUtil.executioner(command, statusFile) - - ### Tmp folder configurations - - command = 'rm -rf /home/elasticsearch/tmp' - ServerStatusUtil.executioner(command, statusFile) - - - command = 'rm -f /home/cyberpanel/elasticsearch' - ServerStatusUtil.executioner(command, statusFile) - - logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, - "ElasticSearch successfully removed.[200]\n", 1) - return 0 + return application_elasticsearch.remove() @staticmethod - def InstallRedis(): + def InstallRedis(version='latest'): + return application_redis.install(version=version) - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') - - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum install redis -y' - ServerStatusUtil.executioner(command, statusFile) - else: - - command = 'DEBIAN_FRONTEND=noninteractive apt-get install redis-server -y' - ServerStatusUtil.executioner(command, statusFile) - - - command = 'systemctl enable redis' - ServerStatusUtil.executioner(command, statusFile) - - command = 'systemctl start redis' - ServerStatusUtil.executioner(command, statusFile) - - command = 'touch /home/cyberpanel/redis' - ServerStatusUtil.executioner(command, statusFile) - - logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, - "Redis successfully installed.[200]\n", 1) - return 0 + @staticmethod + def UpgradeRedis(version='latest'): + return application_redis.upgrade(version=version) @staticmethod def RemoveRedis(): - - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') - - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum erase redis -y' - ServerStatusUtil.executioner(command, statusFile) - else: - - command = 'apt-get remove redis-server -y' - ServerStatusUtil.executioner(command, statusFile) - - - command = 'rm -f /home/cyberpanel/redis' - ServerStatusUtil.executioner(command, statusFile) - - logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, - "Redis successfully removed.[200]\n", 1) - return 0 + return application_redis.remove() @staticmethod - def InstallRabbitMQ(): - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') + def InstallRabbitMQ(version='latest', stream='3'): + return application_rabbitmq.install(version=version, stream=stream) - try: - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum install rabbitmq-server -y' - ServerStatusUtil.executioner(command, statusFile) - else: - command = 'DEBIAN_FRONTEND=noninteractive apt-get install rabbitmq-server -y' - ServerStatusUtil.executioner(command, statusFile) - - command = 'systemctl enable rabbitmq-server' - ServerStatusUtil.executioner(command, statusFile) - - command = 'systemctl start rabbitmq-server' - ServerStatusUtil.executioner(command, statusFile) - - command = 'touch /home/cyberpanel/rabbitmq' - ServerStatusUtil.executioner(command, statusFile) - - logging.CyberCPLogFileWriter.statusWriter( - ServerStatusUtil.lswsInstallStatusPath, - "RabbitMQ successfully installed.[200]\n", 1 - ) - return 0 - except BaseException as msg: - logging.CyberCPLogFileWriter.statusWriter( - ServerStatusUtil.lswsInstallStatusPath, - "RabbitMQ installation failed: %s.[500]\n" % (str(msg)), 0 - ) - return 1 + @staticmethod + def UpgradeRabbitMQ(version='latest', stream='3'): + return application_rabbitmq.upgrade(version=version, stream=stream) @staticmethod def RemoveRabbitMQ(): - statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w') - - try: - command = 'systemctl stop rabbitmq-server' - ServerStatusUtil.executioner(command, statusFile) - - command = 'systemctl disable rabbitmq-server' - ServerStatusUtil.executioner(command, statusFile) - - if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum erase rabbitmq-server -y' - ServerStatusUtil.executioner(command, statusFile) - else: - command = 'DEBIAN_FRONTEND=noninteractive apt-get remove rabbitmq-server -y' - ServerStatusUtil.executioner(command, statusFile) - - command = 'rm -f /home/cyberpanel/rabbitmq' - ServerStatusUtil.executioner(command, statusFile) - - logging.CyberCPLogFileWriter.statusWriter( - ServerStatusUtil.lswsInstallStatusPath, - "RabbitMQ successfully removed.[200]\n", 1 - ) - return 0 - except BaseException as msg: - logging.CyberCPLogFileWriter.statusWriter( - ServerStatusUtil.lswsInstallStatusPath, - "RabbitMQ removal failed: %s.[500]\n" % (str(msg)), 0 - ) - return 1 + return application_rabbitmq.remove() def main(): parser = argparse.ArgumentParser(description='CyberPanel Application Manager') parser.add_argument('--function', help='Function') - + parser.add_argument('--app', help='Application name') + parser.add_argument('--action', help='Action to run: install|remove|upgrade') + parser.add_argument('--version', default='latest', help='Target package version or latest') + parser.add_argument('--esMajor', default='8', help='Elasticsearch major stream (7|8|9)') + parser.add_argument('--rabbitmqStream', default='3', help='RabbitMQ major stream (3|4)') args = vars(parser.parse_args()) + support = managed_apps_os_support() + if not support['supported']: + logging.CyberCPLogFileWriter.statusWriter( + ServerStatusUtil.lswsInstallStatusPath, + support['reason'] + '\n', + 1 + ) + return + if args["function"] == "InstallElasticSearch": - ServiceManager.InstallElasticSearch() + ServiceManager.InstallElasticSearch(version=args.get('version', 'latest'), esMajor=args.get('esMajor', '8')) + elif args["function"] == "UpgradeElasticSearch": + ServiceManager.UpgradeElasticSearch(version=args.get('version', 'latest'), esMajor=args.get('esMajor', '8')) elif args["function"] == "RemoveElasticSearch": ServiceManager.RemoveElasticSearch() elif args["function"] == "InstallRedis": - ServiceManager.InstallRedis() + ServiceManager.InstallRedis(version=args.get('version', 'latest')) + elif args["function"] == "UpgradeRedis": + ServiceManager.UpgradeRedis(version=args.get('version', 'latest')) elif args["function"] == "RemoveRedis": ServiceManager.RemoveRedis() elif args["function"] == "InstallRabbitMQ": - ServiceManager.InstallRabbitMQ() + ServiceManager.InstallRabbitMQ( + version=args.get('version', 'latest'), + stream=args.get('rabbitmqStream', '3'), + ) + elif args["function"] == "UpgradeRabbitMQ": + ServiceManager.UpgradeRabbitMQ( + version=args.get('version', 'latest'), + stream=args.get('rabbitmqStream', '3'), + ) elif args["function"] == "RemoveRabbitMQ": ServiceManager.RemoveRabbitMQ() + elif args.get("app") and args.get("action"): + app_name = args.get("app") + action = args.get("action").lower() + version = args.get("version", "latest") + es_major = args.get("esMajor", "8") + rmq_stream = args.get("rabbitmqStream", "3") + + if app_name == 'Elasticsearch': + if action == 'install': + ServiceManager.InstallElasticSearch(version=version, esMajor=es_major) + elif action == 'upgrade': + ServiceManager.UpgradeElasticSearch(version=version, esMajor=es_major) + elif action == 'remove': + ServiceManager.RemoveElasticSearch() + elif app_name == 'Redis': + if action == 'install': + ServiceManager.InstallRedis(version=version) + elif action == 'upgrade': + ServiceManager.UpgradeRedis(version=version) + elif action == 'remove': + ServiceManager.RemoveRedis() + elif app_name == 'RabbitMQ': + if action == 'install': + ServiceManager.InstallRabbitMQ(version=version, stream=rmq_stream) + elif action == 'upgrade': + ServiceManager.UpgradeRabbitMQ(version=version, stream=rmq_stream) + elif action == 'remove': + ServiceManager.RemoveRabbitMQ() diff --git a/manageServices/static/manageServices/images/rabbitmq.png b/manageServices/static/manageServices/images/rabbitmq.png new file mode 100644 index 0000000000000000000000000000000000000000..41a120e6f6e8f755113253a5aeab8d37512fb2bd GIT binary patch literal 13428 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 0) ? i : 0; + }; + + $scope.selectManagedAppVersion = function (idx, v, $event) { + var n = (typeof idx === 'number') ? idx : parseInt(idx, 10); + if (!isFinite(n) || n < 0) { + n = 0; + } + $scope.selectedVersionRowIndex = n; + $scope.selectedVersion = v; + if ($event && $event.target && typeof $event.target.blur === 'function') { + $event.target.blur(); + } else if (typeof document !== 'undefined' && document.activeElement && typeof document.activeElement.blur === 'function') { + document.activeElement.blur(); + } + }; + $scope.selectedEsMajor = '8'; + $scope.selectedRabbitmqStream = '3'; + $scope.confirmAction = false; + $scope.selectedCurrentVersion = ''; + + /** + * When the install/upgrade modal is open, re-apply version list from latest applicationMeta. + * (Page-load meta can be empty for ES if dnf was slow; opening the modal must refetch.) + */ + $scope.syncModalVersionLists = function () { + if (!$scope.appName || ($scope.status !== 'Installing' && $scope.status !== 'Upgrading')) { + return; + } + if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') { + return; + } + var meta = $scope.findAppMeta($scope.appName); + var vers = sanitizeVersionsArray((meta && meta.versions) ? meta.versions : []); + $scope.selectedVersions = ['latest'].concat(vers); + var curRaw = (meta && meta.installedVersion) ? meta.installedVersion : ($scope.selectedCurrentVersion || ''); + var cur = normalizeVersionToken(curRaw) || String(curRaw || '').trim(); + if (cur && $scope.selectedVersions.indexOf(cur) === -1) { + $scope.selectedVersions.push(cur); + } + if (cur) { + $scope.selectedCurrentVersion = cur; + } + var prevSel = $scope.selectedVersion; + if (prevSel && $scope.selectedVersions.indexOf(prevSel) !== -1) { + $scope.selectedVersion = prevSel; + } else { + $scope.selectedVersion = 'latest'; + } + var realVers = ($scope.selectedVersions || []).filter(function (v) { + return v && v !== 'latest'; + }); + $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + $scope.recalcSelectedVersionRowIndex(); + }; + + $scope.refreshMeta = function () { + $scope.appsMetaRefreshing = true; + var url = "/manageservices/applicationMeta"; + var data = { + esMajor: $scope.selectedEsMajor, + rabbitmqStream: $scope.selectedRabbitmqStream + }; + var config = { + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + 'X-CSRFToken': getCookie('csrftoken') + }, + transformRequest: function (payload) { + return angular.toJson(payload); + } + }; + + return $http.post(url, data, config).then(function (response) { + $scope.appsMetaRefreshing = false; + var payload = response.data; + var ok = payload && (payload.status === 1 || payload.status === '1'); + if (ok) { + var appMap = {}; + (payload.apps || []).forEach(function (app) { + appMap[app.name] = app; + }); + $scope.apps = $scope.apps.map(function (baseApp) { + var meta = appMap[baseApp.name] || {}; + var vers = meta.versions; + if (!angular.isArray(vers)) { + vers = []; + } + vers = sanitizeVersionsArray(vers); + return { + name: baseApp.name, + image: baseApp.image, + installed: !!meta.installed, + installedVersion: meta.installedVersion || '', + updateAvailable: !!meta.updateAvailable, + crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested, + versions: vers, + latestAvailable: meta.latestAvailable || '', + latestOverall: meta.latestOverall || '' + }; + }); + $scope.syncModalVersionLists(); + } else { + new PNotify({ + title: 'Operation Failed!', + text: (payload && (payload.error_message || payload.errorMessage)) || 'Could not load application metadata.', + type: 'error' + }); + } + }, function () { + $scope.appsMetaRefreshing = false; + new PNotify({ + title: 'Operation Failed!', + text: 'Could not connect to server, please refresh this page', + type: 'error' + }); + }); + }; + + $scope.prepareAction = function (service, status, bootstrapInstalledVersion) { + if (bootstrapInstalledVersion === undefined || bootstrapInstalledVersion === null) { + bootstrapInstalledVersion = ''; + } else { + bootstrapInstalledVersion = String(bootstrapInstalledVersion).trim(); + } + $scope.status = status; + $scope.appName = service.name; + $scope.confirmAction = false; + var effectiveInstalled = (service.installedVersion || bootstrapInstalledVersion || '').trim(); + $scope.selectedCurrentVersion = effectiveInstalled; + + if (service.name === 'RabbitMQ') { + if (effectiveInstalled && /^4\./.test(effectiveInstalled)) { + $scope.selectedRabbitmqStream = '4'; + } else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) { + $scope.selectedRabbitmqStream = '3'; + } + } + + if (service.name === 'Elasticsearch' && effectiveInstalled) { + var iv = effectiveInstalled; + if (/^9\./.test(iv)) { + $scope.selectedEsMajor = '9'; + } else if (/^8\./.test(iv)) { + $scope.selectedEsMajor = '8'; + } else if (/^7\./.test(iv)) { + $scope.selectedEsMajor = '7'; + } + } + + $scope.selectedVersions = ['latest']; + var svcVers = sanitizeVersionsArray(service.versions || []); + if (svcVers.length > 0) { + $scope.selectedVersions = ['latest'].concat(svcVers); + } + var curPick = normalizeVersionToken(effectiveInstalled) || effectiveInstalled; + if (curPick && $scope.selectedVersions.indexOf(curPick) === -1) { + $scope.selectedVersions.push(curPick); + } + $scope.selectedVersion = 'latest'; + $scope.requestData = ''; + + var realVers = ($scope.selectedVersions || []).filter(function (v) { + return v && v !== 'latest'; + }); + $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + $scope.recalcSelectedVersionRowIndex(); + }; + + $scope.findAppMeta = function (appName) { + var found = null; + ($scope.apps || []).forEach(function (item) { + if (item.name === appName) { + found = item; + } + }); + return found || {}; + }; + + $scope.prepareActionByName = function (appName, status, bootstrapInstalledVersion) { + var meta = $scope.findAppMeta(appName); + if (!meta.name) { + meta = {name: appName, versions: []}; + } + var mver = meta.versions; + if (!angular.isArray(mver)) { + mver = []; + } + var merged = { + name: meta.name, + image: meta.image, + installed: meta.installed, + installedVersion: meta.installedVersion || '', + versions: mver + }; + $scope.prepareAction(merged, status, bootstrapInstalledVersion); + }; + + /** + * Prepare scope then show modal (do not use data-toggle + ng-click — Bootstrap can + * open the dialog before Angular runs ng-click, leaving status/appName unset). + * For managed apps, wait for applicationMeta so version lists and installedVersion are correct (upgrade vs downgrade). + */ + $scope.openApplicationsModal = function (appName, status, bootstrapInstalledVersion) { + var needMeta = (appName === 'RabbitMQ' || appName === 'Elasticsearch' || appName === 'Redis'); + var showModal = function () { + if (typeof window.jQuery !== 'undefined' && jQuery('#settings').modal) { + jQuery('#settings').modal('show'); + } + }; + if (needMeta) { + // applicationMeta uses selectedRabbitmqStream / selectedEsMajor. If the user left + // 4.x selected earlier, the first POST would ask for 4.x while the node is on 3.x — + // filtered versions come back empty and the Version dropdown shows only "latest". + var bootVer = (bootstrapInstalledVersion !== undefined && bootstrapInstalledVersion !== null) + ? String(bootstrapInstalledVersion).trim() + : ''; + if (!bootVer && appName) { + var metaEarly = $scope.findAppMeta(appName); + if (metaEarly && metaEarly.installedVersion) { + bootVer = String(metaEarly.installedVersion).trim(); + } + } + if (appName === 'RabbitMQ' && bootVer) { + if (/^4\./.test(bootVer)) { + $scope.selectedRabbitmqStream = '4'; + } else if (/^3\./.test(bootVer)) { + $scope.selectedRabbitmqStream = '3'; + } + } + if (appName === 'Elasticsearch' && bootVer) { + var biv = bootVer; + if (/^9\./.test(biv)) { + $scope.selectedEsMajor = '9'; + } else if (/^8\./.test(biv)) { + $scope.selectedEsMajor = '8'; + } else if (/^7\./.test(biv)) { + $scope.selectedEsMajor = '7'; + } + } + // Open immediately with bootstrap / in-memory meta; refresh in background. + // Blocking on applicationMeta made "Change version" feel frozen (dnf/repoquery). + $scope.appName = appName; + $scope.status = status; + $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); + showModal(); + $timeout(function () { + $scope.refreshMeta(); + }, 0); + } else { + $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); + showModal(); + } + }; + + $scope.runAction = function () { + var appName = $scope.appName; + var status = $scope.status; + if (!appName || !status) { + return; + } + if ((status === 'Removing' || status === 'Upgrading') && !$scope.confirmAction) { + new PNotify({ + title: 'Confirmation Required', + text: 'Please confirm this action before proceeding.', + type: 'warning' + }); + return; + } $scope.status = status; $scope.appName = appName; @@ -441,7 +842,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) var data = { appName: appName, - status: status + status: status, + version: $scope.selectedVersion || 'latest', + esMajor: $scope.selectedEsMajor || '8', + rabbitmqStream: $scope.selectedRabbitmqStream || '3', + confirmAction: $scope.confirmAction === true }; var config = { @@ -524,6 +929,9 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) } + // Do not fetch package metadata on page load; it can block workers under DNF load. + // Metadata is fetched on-demand when opening install/version-change modals. + }); /* Java script code */ \ No newline at end of file diff --git a/manageServices/templates/manageServices/applications.html b/manageServices/templates/manageServices/applications.html index de44559f4..fa4635613 100644 --- a/manageServices/templates/manageServices/applications.html +++ b/manageServices/templates/manageServices/applications.html @@ -260,8 +260,113 @@ .modal-body { padding: 25px; + /* Theme may set light text globally; native open state is OS-drawn; use a custom list so version text is always readable. */ + .manage-apps-version-picker { + border: 1px solid #94a3b8; + border-radius: 8px; + overflow: hidden; + background: #ffffff; + margin-top: 6px; + } + + .manage-apps-version-current { + padding: 10px 12px; + background: #e2e8f0; + color: #0f172a; + font-size: 14px; + font-weight: 600; + border-bottom: 1px solid #94a3b8; + } + + .manage-apps-version-rows { + max-height: 240px; + overflow-y: auto; + background: #ffffff; + } + + .manage-apps-version-row { + display: block; + width: 100%; + text-align: left; + padding: 10px 14px; + margin: 0; + border: 0; + border-bottom: 1px solid #e2e8f0; + background: #ffffff !important; + color: #0f172a !important; + font-size: 14px; + line-height: 1.35; + cursor: pointer; + -webkit-text-fill-color: #0f172a; + } + + /* Only one row should read as "selected"; non-active rows stay white unless hover/focus. */ + .manage-apps-version-rows button.manage-apps-version-row:not(.is-active) { + background: #ffffff !important; + } + + .manage-apps-version-rows button.manage-apps-version-row:not(.is-active):hover, + .manage-apps-version-rows button.manage-apps-version-row:not(.is-active):focus { + background: #f1f5f9 !important; + color: #0f172a !important; + -webkit-text-fill-color: #0f172a; + outline: none; + } + + .manage-apps-version-rows button.manage-apps-version-row.is-active, + .manage-apps-version-rows button.manage-apps-version-row.is-active:hover, + .manage-apps-version-rows button.manage-apps-version-row.is-active:focus { + background: #c7d2fe !important; + color: #1e1b4b !important; + -webkit-text-fill-color: #1e1b4b; + font-weight: 600; + outline: none; + } + + .manage-apps-version-row:last-child { + border-bottom: 0; + } + + /* Win over theme + .ng-binding readability rules (must stay dark on white row). */ + .applications-container #settings .modal-body .manage-apps-version-row.ng-binding { + color: #0f172a !important; + -webkit-text-fill-color: #0f172a !important; + } + + [data-theme="dark"] .applications-container #settings .modal-body .manage-apps-version-row.ng-binding { + color: #0f172a !important; + -webkit-text-fill-color: #0f172a !important; + } + + .applications-container #settings .modal-body .manage-apps-version-row.ng-binding.is-active { + color: #1e1b4b !important; + -webkit-text-fill-color: #1e1b4b !important; + } + .install-log { background: var(--bg-secondary, #f8f9ff); border: 1px solid var(--border-color, #e8e9ff); @@ -358,6 +463,7 @@
+ @@ -413,25 +534,29 @@
- {% if service.installed == 'Installed' %} - - {% else %} - - {% endif %} + + + +
{% endfor %} @@ -453,12 +578,87 @@ - - {% endblock %} \ No newline at end of file diff --git a/manageServices/urls.py b/manageServices/urls.py index 2939c10a0..cc1c56fce 100644 --- a/manageServices/urls.py +++ b/manageServices/urls.py @@ -10,5 +10,6 @@ urlpatterns = [ path('saveStatus', views.saveStatus, name='saveStatus'), path('manageApplications', views.manageApplications, name='manageApplications'), + path('applicationMeta', views.applicationMeta, name='applicationMeta'), path('removeInstall', views.removeInstall, name='removeInstall'), ] diff --git a/manageServices/views.py b/manageServices/views.py index a878ae611..20451cee4 100644 --- a/manageServices/views.py +++ b/manageServices/views.py @@ -5,6 +5,7 @@ import plogical.CyberCPLogFileWriter as logging from loginSystem.views import loadLoginPage import os import json +import shlex from plogical.httpProc import httpProc from plogical.mailUtilities import mailUtilities @@ -12,6 +13,11 @@ from plogical.acl import ACLManager from .models import PDNSStatus, SlaveServers from .serviceManager import ServiceManager from plogical.processUtilities import ProcessUtilities +from .application_detection import managed_apps_os_support +from .application_page_meta import ( + build_manage_applications_page_data, + get_application_meta_response_dict, +) # Create your views here. def managePowerDNS(request): @@ -270,43 +276,57 @@ def saveStatus(request): return HttpResponse(json_data) def manageApplications(request): - services = [] + services, application_meta_bootstrap_json = build_manage_applications_page_data( + '8', '3' + ) - ## ElasticSearch - - esPath = '/home/cyberpanel/elasticsearch' - rPath = '/home/cyberpanel/redis' - rmqPath = '/home/cyberpanel/rabbitmq' - - if os.path.exists(esPath): - installed = 'Installed' - else: - installed = 'Not-Installed' - - if os.path.exists(rPath): - rInstalled = 'Installed' - else: - rInstalled = 'Not-Installed' - - if os.path.exists(rmqPath): - rmqInstalled = 'Installed' - else: - rmqInstalled = 'Not-Installed' - - elasticSearch = {'image': '/static/manageServices/images/elastic-search.png', 'name': 'Elasticsearch', - 'installed': installed} - redis = {'image': '/static/manageServices/images/redis.png', 'name': 'Redis', - 'installed': rInstalled} - rabbitmq = {'image': '/static/manageServices/images/rabbitmq-logo.svg', 'name': 'RabbitMQ', - 'installed': rmqInstalled} - services.append(elasticSearch) - services.append(redis) - services.append(rabbitmq) - - proc = httpProc(request, 'manageServices/applications.html', - {'services': services}, 'admin') + proc = httpProc( + request, + 'manageServices/applications.html', + { + 'services': services, + 'application_meta_bootstrap_json': application_meta_bootstrap_json, + }, + 'admin', + ) return proc.render() + +def applicationMeta(request): + try: + userID = request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] != 1: + return ACLManager.loadErrorJson() + + data = {} + if request.method == 'POST': + data = json.loads(request.body) + + requested_major = str(data.get('esMajor', '8')) + if requested_major not in ('7', '8', '9'): + requested_major = '8' + + requested_rmq_stream = str(data.get('rabbitmqStream', '3')).strip() + if requested_rmq_stream not in ('3', '4'): + requested_rmq_stream = '3' + + response_data = get_application_meta_response_dict( + requested_major, requested_rmq_stream + ) + + return HttpResponse( + json.dumps(response_data, ensure_ascii=False), + content_type='application/json; charset=utf-8', + ) + + except BaseException as msg: + return HttpResponse( + json.dumps({'status': 0, 'error_message': str(msg)}, ensure_ascii=False), + content_type='application/json; charset=utf-8', + ) + def removeInstall(request): try: userID = request.session['userID'] @@ -321,22 +341,59 @@ def removeInstall(request): status = data['status'] appName = data['appName'] + version = str(data.get('version', 'latest')).strip() or 'latest' + esMajor = str(data.get('esMajor', '8')).strip() or '8' + if esMajor not in ('7', '8', '9'): + esMajor = '8' + rabbitmqStream = str(data.get('rabbitmqStream', '3')).strip() or '3' + if rabbitmqStream not in ('3', '4'): + rabbitmqStream = '3' + confirmAction = bool(data.get('confirmAction', False)) + + support = managed_apps_os_support() + if not support['supported']: + data_ret = {'status': 0, 'error_message': support['reason']} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + if status in ('Removing', 'Upgrading') and not confirmAction: + data_ret = {'status': 0, 'error_message': 'Action confirmation is required.'} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) if appName == 'Elasticsearch': if status == 'Installing': - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function InstallElasticSearch' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action install --version {0} --esMajor {1}'.format( + shlex.quote(version), shlex.quote(esMajor) + ) + elif status == 'Upgrading': + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action upgrade --version {0} --esMajor {1}'.format( + shlex.quote(version), shlex.quote(esMajor) + ) else: - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function RemoveElasticSearch' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Elasticsearch --action remove' elif appName == 'Redis': if status == 'Installing': - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function InstallRedis' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action install --version {0}'.format( + shlex.quote(version) + ) + elif status == 'Upgrading': + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action upgrade --version {0}'.format( + shlex.quote(version) + ) else: - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function RemoveRedis' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app Redis --action remove' elif appName == 'RabbitMQ': if status == 'Installing': - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function InstallRabbitMQ' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action install --version {0} --rabbitmqStream {1}'.format( + shlex.quote(version), shlex.quote(rabbitmqStream) + ) + elif status == 'Upgrading': + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action upgrade --version {0} --rabbitmqStream {1}'.format( + shlex.quote(version), shlex.quote(rabbitmqStream) + ) else: - command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --function RemoveRabbitMQ' + command = '/usr/local/CyberCP/bin/python /usr/local/CyberCP/manageServices/serviceManager.py --app RabbitMQ --action remove' else: data_ret = {'status': 0, 'error_message': 'Unknown application selected.'} json_data = json.dumps(data_ret) diff --git a/plogical/upgrade.py b/plogical/upgrade.py index d2869a2c4..c702a55d4 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -4464,6 +4464,56 @@ class Migration(migrations.Migration): except BaseException as msg: Upgrade.stdOut('RabbitMQ migration failed: ' + str(msg), 0) + @staticmethod + def redisMigrations(): + marker_path = '/home/cyberpanel/redis' + redis_binary = '/usr/bin/redis-server' + redis_service_files = [ + '/usr/lib/systemd/system/redis.service', + '/lib/systemd/system/redis.service' + ] + + try: + redis_installed = os.path.exists(redis_binary) or any(os.path.exists(path) for path in redis_service_files) + if redis_installed: + if not os.path.exists(marker_path): + writeToFile = open(marker_path, 'w+') + writeToFile.close() + Upgrade.stdOut('Redis detected during upgrade. Marker file created.', 0) + Upgrade.executioner('systemctl enable redis', 'Enable Redis service', 0) + Upgrade.executioner('systemctl start redis', 'Start Redis service', 0) + else: + if os.path.exists(marker_path): + os.remove(marker_path) + Upgrade.stdOut('Redis marker removed because service is not installed.', 0) + except BaseException as msg: + Upgrade.stdOut('Redis migration failed: ' + str(msg), 0) + + @staticmethod + def elasticSearchMigrations(): + marker_path = '/home/cyberpanel/elasticsearch' + es_binary = '/usr/share/elasticsearch/bin/elasticsearch' + es_service_files = [ + '/usr/lib/systemd/system/elasticsearch.service', + '/lib/systemd/system/elasticsearch.service' + ] + + try: + es_installed = os.path.exists(es_binary) or any(os.path.exists(path) for path in es_service_files) + if es_installed: + if not os.path.exists(marker_path): + writeToFile = open(marker_path, 'w+') + writeToFile.close() + Upgrade.stdOut('Elasticsearch detected during upgrade. Marker file created.', 0) + Upgrade.executioner('systemctl enable elasticsearch', 'Enable Elasticsearch service', 0) + Upgrade.executioner('systemctl start elasticsearch', 'Start Elasticsearch service', 0) + else: + if os.path.exists(marker_path): + os.remove(marker_path) + Upgrade.stdOut('Elasticsearch marker removed because service is not installed.', 0) + except BaseException as msg: + Upgrade.stdOut('Elasticsearch migration failed: ' + str(msg), 0) + @staticmethod def backupCriticalFiles(): """Backup all critical configuration files before upgrade""" @@ -6682,6 +6732,8 @@ slowlog = /var/log/php{version}-fpm-slow.log Upgrade.setupWebmail() Upgrade.setupSieve() Upgrade.enableServices() + Upgrade.elasticSearchMigrations() + Upgrade.redisMigrations() Upgrade.rabbitMQMigrations() # Apply AlmaLinux 9 fixes before other installations diff --git a/public/static/manageServices/manageServices.js b/public/static/manageServices/manageServices.js index 949fab41c..057ce349e 100644 --- a/public/static/manageServices/manageServices.js +++ b/public/static/manageServices/manageServices.js @@ -428,10 +428,411 @@ app.controller('pureFTPD', function ($scope, $http, $timeout, $window) { app.controller('manageApplications', function ($scope, $http, $timeout, $window) { + /** + * Normalize entries from applicationMeta (strings, numbers, or rare object shapes) + * so version pickers never show blank rows. CyberPanel uses {$ ... $} interpolation + * in templates; list labels use versionLabel() for consistent display. + */ + function normalizeVersionToken(v) { + if (v === null || v === undefined) { + return ''; + } + if (v === 'latest') { + return 'latest'; + } + if (typeof v === 'number' && isFinite(v)) { + return String(v); + } + if (angular.isObject(v)) { + var o = v; + var cand = o.version || o.Version || o.value || o.name || o.ver || o.label; + if (cand !== undefined && cand !== null) { + return String(cand).trim(); + } + try { + return JSON.stringify(o); + } catch (ignore) { + return ''; + } + } + return String(v).trim(); + } + + function sanitizeVersionsArray(vers) { + if (!angular.isArray(vers)) { + return []; + } + var out = []; + var seen = {}; + vers.forEach(function (raw) { + var t = normalizeVersionToken(raw); + if (!t || t === 'latest') { + return; + } + if (!seen[t]) { + seen[t] = true; + out.push(t); + } + }); + return out; + } + + $scope.versionLabel = function (v) { + if (v === 'latest') { + return 'latest'; + } + var t = normalizeVersionToken(v); + return t || '(unknown)'; + }; + + $scope.versionTrackId = function (idx, v) { + return String(idx) + '|' + $scope.versionLabel(v); + }; + + /* false = long-running install/remove/poll (show spinners); true = idle */ $scope.cyberpanelLoading = true; + /** Background applicationMeta refresh — separate from cyberpanelLoading so the page does not “freeze” on every modal open. */ + $scope.appsMetaRefreshing = false; + $scope.apps = [ + {name: 'Elasticsearch', image: '/static/manageServices/images/elastic-search.png'}, + {name: 'Redis', image: '/static/manageServices/images/redis.png'}, + {name: 'RabbitMQ', image: '/static/manageServices/images/rabbitmq-logo.svg'} + ]; - $scope.removeInstall = function (appName, status) { + (function mergeMetaBootstrap() { + var el = document.getElementById('manageApplicationsMetaBootstrap'); + if (!el || !el.textContent) { + return; + } + var raw = el.textContent.trim(); + if (!raw) { + return; + } + try { + var boot = JSON.parse(raw); + if (!boot || Number(boot.status) !== 1) { + return; + } + var appMap = {}; + (boot.apps || []).forEach(function (a) { + if (a && a.name) { + appMap[a.name] = a; + } + }); + $scope.apps = $scope.apps.map(function (baseApp) { + var meta = appMap[baseApp.name] || {}; + var vers = meta.versions; + if (!angular.isArray(vers)) { + vers = []; + } + vers = sanitizeVersionsArray(vers); + return { + name: baseApp.name, + image: baseApp.image, + installed: !!meta.installed, + installedVersion: meta.installedVersion || '', + updateAvailable: !!meta.updateAvailable, + crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested, + versions: vers, + latestAvailable: meta.latestAvailable || '', + latestOverall: meta.latestOverall || '' + }; + }); + } catch (ignore) { + /* keep bare apps list */ + } + })(); + $scope.selectedVersion = 'latest'; + /** Row highlight uses $index so only one row looks selected (avoids sticky :focus / repeater quirks). */ + $scope.selectedVersionRowIndex = 0; + $scope.selectedVersions = ['latest']; + + $scope.recalcSelectedVersionRowIndex = function () { + var list = $scope.selectedVersions || []; + var sel = $scope.selectedVersion; + var i = list.indexOf(sel); + if (i < 0) { + var normSel = normalizeVersionToken(sel); + if (normSel) { + for (var j = 0; j < list.length; j += 1) { + if (normalizeVersionToken(list[j]) === normSel) { + i = j; + $scope.selectedVersion = list[j]; + break; + } + } + } + } + $scope.selectedVersionRowIndex = (i >= 0) ? i : 0; + }; + + $scope.selectManagedAppVersion = function (idx, v, $event) { + var n = (typeof idx === 'number') ? idx : parseInt(idx, 10); + if (!isFinite(n) || n < 0) { + n = 0; + } + $scope.selectedVersionRowIndex = n; + $scope.selectedVersion = v; + if ($event && $event.target && typeof $event.target.blur === 'function') { + $event.target.blur(); + } else if (typeof document !== 'undefined' && document.activeElement && typeof document.activeElement.blur === 'function') { + document.activeElement.blur(); + } + }; + $scope.selectedEsMajor = '8'; + $scope.selectedRabbitmqStream = '3'; + $scope.confirmAction = false; + $scope.selectedCurrentVersion = ''; + + /** + * When the install/upgrade modal is open, re-apply version list from latest applicationMeta. + * (Page-load meta can be empty for ES if dnf was slow; opening the modal must refetch.) + */ + $scope.syncModalVersionLists = function () { + if (!$scope.appName || ($scope.status !== 'Installing' && $scope.status !== 'Upgrading')) { + return; + } + if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') { + return; + } + var meta = $scope.findAppMeta($scope.appName); + var vers = sanitizeVersionsArray((meta && meta.versions) ? meta.versions : []); + $scope.selectedVersions = ['latest'].concat(vers); + var curRaw = (meta && meta.installedVersion) ? meta.installedVersion : ($scope.selectedCurrentVersion || ''); + var cur = normalizeVersionToken(curRaw) || String(curRaw || '').trim(); + if (cur && $scope.selectedVersions.indexOf(cur) === -1) { + $scope.selectedVersions.push(cur); + } + if (cur) { + $scope.selectedCurrentVersion = cur; + } + var prevSel = $scope.selectedVersion; + if (prevSel && $scope.selectedVersions.indexOf(prevSel) !== -1) { + $scope.selectedVersion = prevSel; + } else { + $scope.selectedVersion = 'latest'; + } + var realVers = ($scope.selectedVersions || []).filter(function (v) { + return v && v !== 'latest'; + }); + $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + $scope.recalcSelectedVersionRowIndex(); + }; + + $scope.refreshMeta = function () { + $scope.appsMetaRefreshing = true; + var url = "/manageservices/applicationMeta"; + var data = { + esMajor: $scope.selectedEsMajor, + rabbitmqStream: $scope.selectedRabbitmqStream + }; + var config = { + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + 'X-CSRFToken': getCookie('csrftoken') + }, + transformRequest: function (payload) { + return angular.toJson(payload); + } + }; + + return $http.post(url, data, config).then(function (response) { + $scope.appsMetaRefreshing = false; + var payload = response.data; + var ok = payload && (payload.status === 1 || payload.status === '1'); + if (ok) { + var appMap = {}; + (payload.apps || []).forEach(function (app) { + appMap[app.name] = app; + }); + $scope.apps = $scope.apps.map(function (baseApp) { + var meta = appMap[baseApp.name] || {}; + var vers = meta.versions; + if (!angular.isArray(vers)) { + vers = []; + } + vers = sanitizeVersionsArray(vers); + return { + name: baseApp.name, + image: baseApp.image, + installed: !!meta.installed, + installedVersion: meta.installedVersion || '', + updateAvailable: !!meta.updateAvailable, + crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested, + versions: vers, + latestAvailable: meta.latestAvailable || '', + latestOverall: meta.latestOverall || '' + }; + }); + $scope.syncModalVersionLists(); + } else { + new PNotify({ + title: 'Operation Failed!', + text: (payload && (payload.error_message || payload.errorMessage)) || 'Could not load application metadata.', + type: 'error' + }); + } + }, function () { + $scope.appsMetaRefreshing = false; + new PNotify({ + title: 'Operation Failed!', + text: 'Could not connect to server, please refresh this page', + type: 'error' + }); + }); + }; + + $scope.prepareAction = function (service, status, bootstrapInstalledVersion) { + if (bootstrapInstalledVersion === undefined || bootstrapInstalledVersion === null) { + bootstrapInstalledVersion = ''; + } else { + bootstrapInstalledVersion = String(bootstrapInstalledVersion).trim(); + } + $scope.status = status; + $scope.appName = service.name; + $scope.confirmAction = false; + var effectiveInstalled = (service.installedVersion || bootstrapInstalledVersion || '').trim(); + $scope.selectedCurrentVersion = effectiveInstalled; + + if (service.name === 'RabbitMQ') { + if (effectiveInstalled && /^4\./.test(effectiveInstalled)) { + $scope.selectedRabbitmqStream = '4'; + } else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) { + $scope.selectedRabbitmqStream = '3'; + } + } + + if (service.name === 'Elasticsearch' && effectiveInstalled) { + var iv = effectiveInstalled; + if (/^9\./.test(iv)) { + $scope.selectedEsMajor = '9'; + } else if (/^8\./.test(iv)) { + $scope.selectedEsMajor = '8'; + } else if (/^7\./.test(iv)) { + $scope.selectedEsMajor = '7'; + } + } + + $scope.selectedVersions = ['latest']; + var svcVers = sanitizeVersionsArray(service.versions || []); + if (svcVers.length > 0) { + $scope.selectedVersions = ['latest'].concat(svcVers); + } + var curPick = normalizeVersionToken(effectiveInstalled) || effectiveInstalled; + if (curPick && $scope.selectedVersions.indexOf(curPick) === -1) { + $scope.selectedVersions.push(curPick); + } + $scope.selectedVersion = 'latest'; + $scope.requestData = ''; + + var realVers = ($scope.selectedVersions || []).filter(function (v) { + return v && v !== 'latest'; + }); + $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + $scope.recalcSelectedVersionRowIndex(); + }; + + $scope.findAppMeta = function (appName) { + var found = null; + ($scope.apps || []).forEach(function (item) { + if (item.name === appName) { + found = item; + } + }); + return found || {}; + }; + + $scope.prepareActionByName = function (appName, status, bootstrapInstalledVersion) { + var meta = $scope.findAppMeta(appName); + if (!meta.name) { + meta = {name: appName, versions: []}; + } + var mver = meta.versions; + if (!angular.isArray(mver)) { + mver = []; + } + var merged = { + name: meta.name, + image: meta.image, + installed: meta.installed, + installedVersion: meta.installedVersion || '', + versions: mver + }; + $scope.prepareAction(merged, status, bootstrapInstalledVersion); + }; + + /** + * Prepare scope then show modal (do not use data-toggle + ng-click — Bootstrap can + * open the dialog before Angular runs ng-click, leaving status/appName unset). + * For managed apps, wait for applicationMeta so version lists and installedVersion are correct (upgrade vs downgrade). + */ + $scope.openApplicationsModal = function (appName, status, bootstrapInstalledVersion) { + var needMeta = (appName === 'RabbitMQ' || appName === 'Elasticsearch' || appName === 'Redis'); + var showModal = function () { + if (typeof window.jQuery !== 'undefined' && jQuery('#settings').modal) { + jQuery('#settings').modal('show'); + } + }; + if (needMeta) { + // applicationMeta uses selectedRabbitmqStream / selectedEsMajor. If the user left + // 4.x selected earlier, the first POST would ask for 4.x while the node is on 3.x — + // filtered versions come back empty and the Version dropdown shows only "latest". + var bootVer = (bootstrapInstalledVersion !== undefined && bootstrapInstalledVersion !== null) + ? String(bootstrapInstalledVersion).trim() + : ''; + if (!bootVer && appName) { + var metaEarly = $scope.findAppMeta(appName); + if (metaEarly && metaEarly.installedVersion) { + bootVer = String(metaEarly.installedVersion).trim(); + } + } + if (appName === 'RabbitMQ' && bootVer) { + if (/^4\./.test(bootVer)) { + $scope.selectedRabbitmqStream = '4'; + } else if (/^3\./.test(bootVer)) { + $scope.selectedRabbitmqStream = '3'; + } + } + if (appName === 'Elasticsearch' && bootVer) { + var biv = bootVer; + if (/^9\./.test(biv)) { + $scope.selectedEsMajor = '9'; + } else if (/^8\./.test(biv)) { + $scope.selectedEsMajor = '8'; + } else if (/^7\./.test(biv)) { + $scope.selectedEsMajor = '7'; + } + } + // Open immediately with bootstrap / in-memory meta; refresh in background. + // Blocking on applicationMeta made "Change version" feel frozen (dnf/repoquery). + $scope.appName = appName; + $scope.status = status; + $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); + showModal(); + $timeout(function () { + $scope.refreshMeta(); + }, 0); + } else { + $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); + showModal(); + } + }; + + $scope.runAction = function () { + var appName = $scope.appName; + var status = $scope.status; + if (!appName || !status) { + return; + } + if ((status === 'Removing' || status === 'Upgrading') && !$scope.confirmAction) { + new PNotify({ + title: 'Confirmation Required', + text: 'Please confirm this action before proceeding.', + type: 'warning' + }); + return; + } $scope.status = status; $scope.appName = appName; @@ -441,7 +842,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) var data = { appName: appName, - status: status + status: status, + version: $scope.selectedVersion || 'latest', + esMajor: $scope.selectedEsMajor || '8', + rabbitmqStream: $scope.selectedRabbitmqStream || '3', + confirmAction: $scope.confirmAction === true }; var config = { @@ -524,6 +929,9 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) } + // Do not fetch package metadata on page load; it can block workers under DNF load. + // Metadata is fetched on-demand when opening install/version-change modals. + }); /* Java script code */ \ No newline at end of file diff --git a/userManagment/static/userManagment/userManagment.js b/userManagment/static/userManagment/userManagment.js index 3b90ed8ea..e0fcfee2a 100644 --- a/userManagment/static/userManagment/userManagment.js +++ b/userManagment/static/userManagment/userManagment.js @@ -848,9 +848,20 @@ app.controller('createACLCTRL', function ($scope, $http) { var url = "/users/createACLFunc"; + var aclNameTrimmed = ($scope.aclName !== undefined && $scope.aclName !== null) ? String($scope.aclName).trim() : ''; + if (!aclNameTrimmed) { + $scope.aclLoading = true; + safePNotify({ + title: 'Error!', + text: 'Please enter a name for this ACL.', + type: 'error' + }); + return; + } + var data = { - aclName: $scope.aclName, + aclName: aclNameTrimmed, makeAdmin: $scope.makeAdmin, // @@ -943,10 +954,10 @@ app.controller('createACLCTRL', function ($scope, $http) { type: 'success' }); } else { - + var errText = (response.data && (response.data.errorMessage || response.data.error_message)) ? (response.data.errorMessage || response.data.error_message) : 'Unknown error'; safePNotify({ title: 'Error!', - text: response.data.errorMessage, + text: errText, type: 'error' }); diff --git a/userManagment/views.py b/userManagment/views.py index 2f3406fe5..8283ad96a 100644 --- a/userManagment/views.py +++ b/userManagment/views.py @@ -3,6 +3,7 @@ from django.shortcuts import render, redirect from django.http import HttpResponse +from django.utils.translation import gettext as _ from django.db import models from django.db.utils import IntegrityError, ProgrammingError, OperationalError from django.views.decorators.csrf import ensure_csrf_cookie @@ -765,9 +766,17 @@ def createACLFunc(request): if currentACL['admin'] == 1: data = json.loads(request.body) + acl_name_raw = data.get('aclName', '') or '' + acl_name = str(acl_name_raw).strip() + if not acl_name: + msg = str(_('Please enter a name for this ACL.')) + err_body = {'status': 0, 'error_message': msg, 'errorMessage': msg} + return HttpResponse(json.dumps(err_body), content_type='application/json') + data['aclName'] = acl_name + ## Version Management - if data['makeAdmin']: + if data.get('makeAdmin'): data['adminStatus'] = 1 else: data['adminStatus'] = 0 diff --git a/websiteFunctions/templates/websiteFunctions/securityManagement.html b/websiteFunctions/templates/websiteFunctions/securityManagement.html deleted file mode 100644 index 38cfd6423..000000000 --- a/websiteFunctions/templates/websiteFunctions/securityManagement.html +++ /dev/null @@ -1,322 +0,0 @@ -{% extends "baseTemplate/index.html" %} -{% load static %} - -{% block title %} -Security Management - CyberPanel -{% endblock %} - -{% block content %} -
-
-
-
-
-

- - Security Management -

-
-
- -
-
-
-
Security Alerts
-

Monitor and manage security threats detected by the system.

- -
-
-
- - -
-
-
-
-

Recent Security Alerts

-
-
-
- -
- Loading security alerts... -
-
-
-
-
-
- - -
-
-
-
-

Blocked IP Addresses

- -
-
-
- - - - - - - - - - - - - - -
IP AddressBlocked AtReasonActions
- Loading... -
-
-
-
-
-
- - -
-
-
-
-

Manual IP Blocking

-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
- -
-
-
-
-
-
-
-
-
-
- - -{% endblock %} diff --git a/websiteFunctions/urls.py b/websiteFunctions/urls.py index e353f5de7..df7610291 100644 --- a/websiteFunctions/urls.py +++ b/websiteFunctions/urls.py @@ -215,13 +215,6 @@ urlpatterns = [ path('getBandwidthResetLogs', views.getBandwidthResetLogs, name='getBandwidthResetLogs'), path('scheduleBandwidthReset', views.scheduleBandwidthReset, name='scheduleBandwidthReset'), - # Security Management - path('securityManagement', views.securityManagementPage, name='securityManagementPage'), - - # IP Blocking - path('blockIPAddress', views.blockIPAddress, name='blockIPAddress'), - path('unblockIPAddress', views.unblockIPAddress, name='unblockIPAddress'), - path('getBlockedIPs', views.getBlockedIPs, name='getBlockedIPs'), path('checkIPStatus', views.checkIPStatus, name='checkIPStatus'), # Catch all for domains (must be last) diff --git a/websiteFunctions/views.py b/websiteFunctions/views.py index 98b7a0b30..91355b47c 100644 --- a/websiteFunctions/views.py +++ b/websiteFunctions/views.py @@ -2242,15 +2242,6 @@ def bandwidthManagementPage(request): except KeyError: return redirect(loadLoginPage) -def securityManagementPage(request): - """Render the Security Management page.""" - try: - userID = request.session['userID'] - proc = httpProc(request, 'websiteFunctions/securityManagement.html', {}, 'admin') - return proc.render() - except KeyError: - return redirect(loadLoginPage) - def getFTPQuotaStatus(request): try: userID = request.session['userID'] @@ -2308,31 +2299,6 @@ def scheduleBandwidthReset(request): except KeyError: return redirect(loadLoginPage) -# IP Blocking Views -def blockIPAddress(request): - try: - userID = request.session['userID'] - wm = WebsiteManager() - return wm.blockIPAddress(userID, request.POST) - except KeyError: - return redirect(loadLoginPage) - -def unblockIPAddress(request): - try: - userID = request.session['userID'] - wm = WebsiteManager() - return wm.unblockIPAddress(userID, request.POST) - except KeyError: - return redirect(loadLoginPage) - -def getBlockedIPs(request): - try: - userID = request.session['userID'] - wm = WebsiteManager() - return wm.getBlockedIPs(userID, request.POST) - except KeyError: - return redirect(loadLoginPage) - def checkIPStatus(request): try: userID = request.session['userID'] diff --git a/websiteFunctions/website.py b/websiteFunctions/website.py index 4039575d9..c976ba90e 100644 --- a/websiteFunctions/website.py +++ b/websiteFunctions/website.py @@ -9322,141 +9322,6 @@ StrictHostKeyChecking no json_data = json.dumps(data_ret) return HttpResponse(json_data) - def blockIPAddress(self, userID=None, data=None): - """ - Block an IP address - """ - try: - currentACL = ACLManager.loadedACL(userID) - admin = Administrator.objects.get(pk=userID) - - # Check if user has permission - if not (currentACL.get('admin', 0) == 1): - return ACLManager.loadErrorJson('status', 0) - - ip_address = data.get('ip_address') - reason = data.get('reason', 'Manual block via CyberPanel') - - if not ip_address: - data_ret = { - 'status': 0, - 'message': 'IP address is required' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - # Import firewall utilities - from plogical.firewallUtilities import FirewallUtilities - - # Block the IP - success, message = FirewallUtilities.blockIP(ip_address, reason) - - if success: - data_ret = { - 'status': 1, - 'message': message - } - else: - data_ret = { - 'status': 0, - 'message': message - } - - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - except Exception as e: - data_ret = { - 'status': 0, - 'message': f'Error blocking IP: {str(e)}' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - def unblockIPAddress(self, userID=None, data=None): - """ - Unblock an IP address - """ - try: - currentACL = ACLManager.loadedACL(userID) - admin = Administrator.objects.get(pk=userID) - - # Check if user has permission - if not (currentACL.get('admin', 0) == 1): - return ACLManager.loadErrorJson('status', 0) - - ip_address = data.get('ip_address') - - if not ip_address: - data_ret = { - 'status': 0, - 'message': 'IP address is required' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - # Import firewall utilities - from plogical.firewallUtilities import FirewallUtilities - - # Unblock the IP - success, message = FirewallUtilities.unblockIP(ip_address) - - if success: - data_ret = { - 'status': 1, - 'message': message - } - else: - data_ret = { - 'status': 0, - 'message': message - } - - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - except Exception as e: - data_ret = { - 'status': 0, - 'message': f'Error unblocking IP: {str(e)}' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - def getBlockedIPs(self, userID=None, data=None): - """ - Get list of blocked IP addresses - """ - try: - currentACL = ACLManager.loadedACL(userID) - admin = Administrator.objects.get(pk=userID) - - # Check if user has permission - if not (currentACL.get('admin', 0) == 1): - return ACLManager.loadErrorJson('status', 0) - - # Import firewall utilities - from plogical.firewallUtilities import FirewallUtilities - - # Get blocked IPs - blocked_ips = FirewallUtilities.getBlockedIPs() - - data_ret = { - 'status': 1, - 'blocked_ips': blocked_ips - } - - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - - except Exception as e: - data_ret = { - 'status': 0, - 'message': f'Error getting blocked IPs: {str(e)}' - } - json_data = json.dumps(data_ret) - return HttpResponse(json_data) - def checkIPStatus(self, userID=None, data=None): """ Check if an IP is blocked