From 774c72f159891d357ac354229e43dbab162f7517 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 1 Apr 2026 03:27:17 +0200 Subject: [PATCH] Manage Services: RabbitMQ 4.x default, repo alignment, UI fixes - Detect RHEL major from /etc/os-release and align Packagecloud RabbitMQ .repo URLs. - Improve version discovery (el8 metadata merge on EL9+, 4.x fallback when DNF omits builds). - Default RabbitMQ stream to 4.x in API, page bootstrap, serviceManager, and normalize_rabbitmq_stream. - UI: prefetch 4.x on install, stream buttons 4.x first, fix confirm checkbox ng-model parent scope. - Bump msModal cache-bust for manageServices.js. --- .../templates/baseTemplate/index.html | 2 +- manageServices/application_detection.py | 49 ++ manageServices/application_page_meta.py | 42 +- manageServices/application_rabbitmq_repo.py | 213 +++++++- manageServices/application_versions.py | 150 ++++- manageServices/serviceManager.py | 8 +- .../static/manageServices/manageServices.js | 187 +++++-- .../manageServices/applications.html | 66 ++- manageServices/views.py | 10 +- .../static/manageServices/manageServices.js | 187 +++++-- static/manageServices/manageServices.js | 513 +++++++++++++++++- 11 files changed, 1261 insertions(+), 166 deletions(-) diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 771a0c947..0476e85f7 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -2504,7 +2504,7 @@ - + diff --git a/manageServices/application_detection.py b/manageServices/application_detection.py index 7054d7248..36162f04e 100644 --- a/manageServices/application_detection.py +++ b/manageServices/application_detection.py @@ -43,6 +43,55 @@ def is_debian_family(): return os.path.exists('/etc/debian_version') or os.path.exists('/etc/lsb-release') +def rhel_major_from_os_release(): + """ + RHEL-family OS major version (8, 9, 10, …) from /etc/os-release (or redhat-release). + Returns None for Debian/Ubuntu or if the OS cannot be classified as RHEL-like. + Used to align Packagecloud Yum baseurls (el/8 vs el/9) with the running system. + """ + if is_debian_family(): + return None + os_release = '/etc/os-release' + version_id = None + platform_id = None + if os.path.exists(os_release): + try: + with open(os_release, 'r', encoding='utf-8', errors='replace') as fh: + for line in fh: + line = line.strip() + if line.startswith('VERSION_ID='): + version_id = line.split('=', 1)[1].strip().strip('"').strip("'") + elif line.startswith('PLATFORM_ID='): + platform_id = line.split('=', 1)[1].strip().strip('"').strip("'") + except Exception: + pass + if version_id: + match = re.match(r'^(\d+)', version_id) + if match: + major = int(match.group(1)) + if 6 <= major <= 15: + return major + if platform_id: + match = re.search(r'el(\d+)', platform_id, re.IGNORECASE) + if match: + major = int(match.group(1)) + if 6 <= major <= 15: + return major + redhat_release = '/etc/redhat-release' + if os.path.exists(redhat_release): + try: + with open(redhat_release, 'r', encoding='utf-8', errors='replace') as fh: + txt = fh.read() + match = re.search(r'release\s+(\d+)', txt, re.IGNORECASE) + if match: + major = int(match.group(1)) + if 6 <= major <= 15: + return major + except Exception: + pass + return None + + def is_centos7(): release_paths = ['/etc/centos-release', '/etc/redhat-release', '/etc/os-release'] text_blob = '' diff --git a/manageServices/application_page_meta.py b/manageServices/application_page_meta.py index e949f827d..095705466 100644 --- a/manageServices/application_page_meta.py +++ b/manageServices/application_page_meta.py @@ -4,6 +4,8 @@ import json import threading import time +from django.utils.translation import gettext as _ + from .application_detection import detect_app_state, managed_apps_os_support from .application_versions import get_available_versions, version_compare @@ -46,16 +48,16 @@ def _page_meta_cache_put(cache_key, services, meta_json): ) -def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): +def build_manage_applications_page_data(es_major='8', rabbitmq_stream='4'): """ Build `services` for card HTML and a JSON-serializable bootstrap matching - /manageservices/applicationMeta shape (default ES major 8, RMQ stream 3). + /manageservices/applicationMeta shape (default ES major 8, RMQ stream 4). """ 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' + rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4' cache_key = 'major:{0}|rmq:{1}|support:{2}'.format( major, rmq, 1 if support.get('supported') else 0 ) @@ -87,7 +89,16 @@ def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): installed_version = state['installedVersion'] if installed_version and installed_version not in versions: - versions = [installed_version] + versions + prepend_installed = True + if app_name == 'RabbitMQ': + from manageServices.application_rabbitmq_repo import ( + filter_versions_for_stream, + ) + prepend_installed = bool( + filter_versions_for_stream([installed_version], rmq) + ) + if prepend_installed: + versions = [installed_version] + versions ref_latest = latest_global or latest_branch update_available = bool( @@ -97,6 +108,24 @@ def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): and version_compare(installed_version, ref_latest) < 0 ) + rabbitmq_versions_hint = '' + if app_name == 'RabbitMQ' and not versions: + if rmq == '4': + rabbitmq_versions_hint = _( + 'Your OS is not unsupported: upstream RabbitMQ publishes 4.x RPMs suitable for ' + 'RHEL/Alma/Rocky 8 and 9 (RPM filenames may still contain el8; that is normal). ' + 'If this list stays empty, repository metadata may not expose 4.x to dnf yet—' + 'refresh metadata (dnf makecache -y) or install the official .rpm from rabbitmq.com. ' + 'Check with: dnf repoquery rabbitmq-server --available --show-duplicates ' + '(4.x lines look like rabbitmq-server-0:4.x.y-1.el8.noarch — search for :4., not a space after the colon).' + ) + else: + rabbitmq_versions_hint = _( + 'No 3.x builds were returned for this stream after refreshing Team RabbitMQ repos. ' + 'This is usually metadata or repo state—not OS support. Try: dnf makecache -y, ' + 'then dnf repoquery rabbitmq-server --available --show-duplicates.' + ) + bootstrap_apps.append({ 'name': app_name, 'installed': state['installed'], @@ -110,6 +139,7 @@ def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): 'adopted': bool(state['installed'] and not state['markerExists']), 'major': major if app_name == 'Elasticsearch' else '', 'rabbitmqStream': rmq if app_name == 'RabbitMQ' else '', + 'rabbitmqVersionsHint': rabbitmq_versions_hint, }) bootstrap = {'status': 1, 'apps': bootstrap_apps} @@ -118,7 +148,7 @@ def build_manage_applications_page_data(es_major='8', rabbitmq_stream='3'): return services, meta_json -def get_application_meta_response_dict(es_major='8', rabbitmq_stream='3'): +def get_application_meta_response_dict(es_major='8', rabbitmq_stream='4'): """ JSON payload for POST /manageservices/applicationMeta. Reuses the same TTL cache as the Manage Applications HTML bootstrap so @@ -126,7 +156,7 @@ def get_application_meta_response_dict(es_major='8', rabbitmq_stream='3'): """ 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' + rmq = str(rabbitmq_stream).strip() if str(rabbitmq_stream).strip() in ('3', '4') else '4' cache_key = 'major:{0}|rmq:{1}|support:{2}'.format( major, rmq, 1 if support.get('supported') else 0 ) diff --git a/manageServices/application_rabbitmq_repo.py b/manageServices/application_rabbitmq_repo.py index 7f7928713..11c623d5e 100644 --- a/manageServices/application_rabbitmq_repo.py +++ b/manageServices/application_rabbitmq_repo.py @@ -3,10 +3,13 @@ Team RabbitMQ package repositories (Packagecloud) and Erlang compatibility for RabbitMQ 3.x vs 4.x installation streams. """ +import os import re import subprocess +import tempfile +import time -from manageServices.application_detection import is_debian_family +from manageServices.application_detection import is_debian_family, rhel_major_from_os_release # Official Packagecloud install scripts (RabbitMQ team). _RPM_ERLANG_SCRIPT = ( @@ -26,6 +29,42 @@ _DEB_SERVER_SCRIPT = ( _MIN_OTP_STREAM_3 = 25 _MIN_OTP_STREAM_4 = 26 +# When Packagecloud metadata lists 3.x but no 4.x (common on el/9 trees), still offer GA +# releases from https://www.rabbitmq.com/release-information so the panel can run +# dnf install rabbitmq-server- (RPMs are often el8-tagged on EL9 per upstream docs). +# Update this tuple when new 4.x patches ship. +RABBITMQ_4X_METADATA_FALLBACK_VERSIONS = ( + '4.2.5', + '4.2.4', + '4.2.3', + '4.2.2', + '4.2.1', + '4.2.0', + '4.1.8', + '4.1.7', + '4.1.6', + '4.1.5', + '4.1.4', + '4.1.3', + '4.1.2', + '4.1.1', + '4.1.0', + '4.0.9', + '4.0.8', + '4.0.7', + '4.0.6', + '4.0.5', + '4.0.4', + '4.0.3', + '4.0.2', + '4.0.1', + '4.0.0', +) + +_YUM_REPOS_D = '/etc/yum.repos.d' +# Packagecloud RabbitMQ repos use .../el/N/... in baseurl; must match host RHEL major. +_EL_URL_SEGMENT = re.compile(r'(/el/)(\d+)(/)') + def _run(cmd, timeout=300): try: @@ -48,7 +87,7 @@ def _run_shell_trusted(script_url, timeout=300): def normalize_rabbitmq_stream(value): - s = str(value or '3').strip() + s = str(value or '4').strip() if s in ('4', '4.x', '41', '4.1'): return '4' return '3' @@ -64,6 +103,155 @@ def _write_status(status_file, message): pass +def _rhel_refresh_package_metadata(status_file=None, aggressive=False): + """ + Refresh DNF/YUM metadata after adding Packagecloud repos. + Retries on failure. When aggressive (e.g. 4.x stream), expire cache first + so new rabbitmq-server builds become visible. + """ + if is_debian_family(): + return True + if aggressive: + exp_rc, _, exp_err = _run(['dnf', 'clean', 'expire-cache'], timeout=90) + if exp_rc != 0: + _write_status( + status_file, + 'dnf expire-cache (non-fatal): ' + (exp_err or '')[:120] + ) + last_err = '' + for attempt in range(1, 4): + for cache_cmd in (['dnf', 'makecache', '-y'], ['yum', 'makecache', '-y']): + c_rc, c_out, c_err = _run(cache_cmd, timeout=180) + if c_rc == 0: + _write_status( + status_file, + 'RPM metadata refreshed ({0}, attempt {1}).'.format( + cache_cmd[0], attempt + ) + ) + return True + last_err = (c_err or c_out or str(c_rc)).strip() + time.sleep(min(3 * attempt, 15)) + _write_status( + status_file, + 'RPM metadata refresh failed after retries: ' + (last_err or 'unknown')[:240] + ) + return False + + +def refresh_rhel_metadata_for_rabbitmq_repos(status_file=None): + """ + Public: force another metadata refresh (e.g. when repoquery finds no 4.x RPMs). + """ + return _rhel_refresh_package_metadata(status_file=status_file, aggressive=True) + + +def align_rabbitmq_packagecloud_repos_to_os(status_file=None): + """ + If Team RabbitMQ Packagecloud .repo files point at /el/M/ but this host is el/N, + rewrite URLs to /el/N/ (e.g. stale el/8 on AlmaLinux 9). Only touches files that + mention both packagecloud.io and rabbitmq. Requires root to write /etc/yum.repos.d. + """ + if is_debian_family(): + return + target_major = rhel_major_from_os_release() + if target_major is None: + return + if not os.path.isdir(_YUM_REPOS_D): + return + try: + repo_names = sorted( + n for n in os.listdir(_YUM_REPOS_D) if n.endswith('.repo') + ) + except OSError as err: + _write_status( + status_file, + 'rabbitmq repo align: cannot list {0}: {1}'.format( + _YUM_REPOS_D, str(err)[:100] + ) + ) + return + + for repo_name in repo_names: + repo_path = os.path.join(_YUM_REPOS_D, repo_name) + try: + with open(repo_path, 'r', encoding='utf-8', errors='replace') as handle: + original = handle.read() + except OSError: + continue + lower = original.lower() + if 'packagecloud.io' not in lower or 'rabbitmq' not in lower: + continue + + def _sub_el(match): + current = int(match.group(2)) + if current == target_major: + return match.group(0) + return match.group(1) + str(target_major) + match.group(3) + + updated = _EL_URL_SEGMENT.sub(_sub_el, original) + if updated == original: + continue + tmp_path = None + try: + fd, tmp_path = tempfile.mkstemp( + prefix='.cybercp-rabbitmq-', + suffix='.tmp', + dir=_YUM_REPOS_D, + text=True, + ) + with os.fdopen(fd, 'w', encoding='utf-8') as out: + out.write(updated) + os.replace(tmp_path, repo_path) + tmp_path = None + _write_status( + status_file, + 'Aligned RabbitMQ Packagecloud repo {0} to el/{1}.'.format( + repo_name, target_major + ) + ) + except PermissionError: + _write_status( + status_file, + 'rabbitmq repo align: need root to rewrite {0} (el/{1}).'.format( + repo_name, target_major + ) + ) + except OSError as err: + _write_status( + status_file, + 'rabbitmq repo align: {0}: {1}'.format(repo_name, str(err)[:120]) + ) + finally: + if tmp_path and os.path.isfile(tmp_path): + try: + os.unlink(tmp_path) + except OSError: + pass + + +def refresh_debian_apt_metadata(status_file=None): + """Second-chance apt metadata refresh without re-running Packagecloud scripts.""" + if not is_debian_family(): + return True + last_err = '' + for apt_attempt in range(1, 4): + a_rc, _, a_err = _run(['apt-get', 'update', '-y'], timeout=180) + if a_rc == 0: + _write_status( + status_file, + 'APT metadata refreshed (attempt {0}).'.format(apt_attempt) + ) + return True + last_err = (a_err or '').strip() + _write_status( + status_file, + 'apt-get update attempt {0}: {1}'.format(apt_attempt, (last_err or '')[:160]) + ) + time.sleep(min(3 * apt_attempt, 12)) + return False + + def ensure_rabbitmq_team_repos(stream, status_file=None): """ Idempotently enable rabbitmq-erlang and rabbitmq-server Packagecloud repos. @@ -84,8 +272,17 @@ def ensure_rabbitmq_team_repos(stream, status_file=None): _write_status( status_file, 'rabbitmq-server repo script: ' + (err2 or out2 or 'failed') ) - _run(['apt-get', 'update', '-y'], timeout=120) + for apt_attempt in range(1, 4): + a_rc, _, a_err = _run(['apt-get', 'update', '-y'], timeout=180) + if a_rc == 0: + break + _write_status( + status_file, + 'apt-get update attempt {0}: {1}'.format(apt_attempt, (a_err or '')[:160]) + ) + time.sleep(min(3 * apt_attempt, 12)) else: + align_rabbitmq_packagecloud_repos_to_os(status_file=status_file) 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')) @@ -94,11 +291,11 @@ def ensure_rabbitmq_team_repos(stream, status_file=None): _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 + # 4.x builds may appear after a fresh metadata pull; expire + retries help visibility. + _rhel_refresh_package_metadata( + status_file=status_file, + aggressive=(stream == '4'), + ) _write_status(status_file, 'Team RabbitMQ repositories ready.') diff --git a/manageServices/application_versions.py b/manageServices/application_versions.py index 4d6b2b8e3..44d3ad837 100644 --- a/manageServices/application_versions.py +++ b/manageServices/application_versions.py @@ -1,10 +1,15 @@ import os +import platform import re import subprocess import threading import time -from manageServices.application_detection import is_debian_family, package_name_for_app +from manageServices.application_detection import ( + is_debian_family, + package_name_for_app, + rhel_major_from_os_release, +) # 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. @@ -184,27 +189,37 @@ def _dnf_reposdir_flag(use_cyberpanel_extra): 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): +def _rhel_repoquery_versions( + pkg_name, + use_cyberpanel_extra_repos=False, + enablerepos=None, + latest_limit=50, + normalize_max=25, +): """ Resolve distinct %{version} strings from enabled repos. RPM NEVRA text parsing is brittle (el9_7 etc.); repoquery --qf is reliable. + + For RabbitMQ, pass latest_limit=None (no cap — el8-tagged RPMs may share metadata + with EL9) and normalize_max=200 so stream filtering (3.x vs 4.x) is not fed only + the newest majors (which would hide the other line entirely). """ - cmd = ( + dnf_cmd = ( ['dnf'] + _dnf_reposdir_flag(use_cyberpanel_extra_repos) + [ 'repoquery', + '--available', '--show-duplicates', - '--latest-limit=50', - '--qf', - '%{version}', - pkg_name, ] ) + if latest_limit is not None: + dnf_cmd.append('--latest-limit={0}'.format(int(latest_limit))) + dnf_cmd.extend(['--qf', '%{version}', pkg_name]) if enablerepos: for repo_id in enablerepos: - cmd.extend(['--enablerepo', repo_id]) - rc, out, err = _run(cmd, timeout=180) + dnf_cmd.extend(['--enablerepo', repo_id]) + rc, out, err = _run(dnf_cmd, timeout=240) raw = [] if rc == 0 and out.strip(): for line in out.splitlines(): @@ -212,13 +227,14 @@ def _rhel_repoquery_versions(pkg_name, use_cyberpanel_extra_repos=False, enabler if v and re.match(r'^[0-9]', v): raw.append(v) if raw: - return _normalize_versions(_sort_versions_desc(raw)) + return _normalize_versions(_sort_versions_desc(raw), max_items=normalize_max) # Legacy systems / fallback - rc2, out2, _ = _run( - ['yum', 'repoquery', '--show-duplicates', '--qf', '%{version}', pkg_name], - timeout=120, - ) + yum_cmd = ['yum', 'repoquery', '--available', '--show-duplicates'] + if latest_limit is not None: + yum_cmd.append('--latest-limit={0}'.format(int(latest_limit))) + yum_cmd.extend(['--qf', '%{version}', pkg_name]) + rc2, out2, _ = _run(yum_cmd, timeout=120) raw2 = [] if rc2 == 0 and out2.strip(): for line in out2.splitlines(): @@ -226,10 +242,11 @@ def _rhel_repoquery_versions(pkg_name, use_cyberpanel_extra_repos=False, enabler if v and re.match(r'^[0-9]', v): raw2.append(v) if raw2: - return _normalize_versions(_sort_versions_desc(raw2)) + return _normalize_versions(_sort_versions_desc(raw2), max_items=normalize_max) # Oldest fallback: yum list rc3, out3, _ = _run(['yum', '--showduplicates', 'list', pkg_name], timeout=120) + raw3 = [] if rc3 == 0: for line in out3.splitlines(): row = line.strip() @@ -237,13 +254,54 @@ def _rhel_repoquery_versions(pkg_name, use_cyberpanel_extra_repos=False, enabler 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)) + raw3.append(fields[1]) + if raw3: + return _normalize_versions(_sort_versions_desc(raw3), max_items=normalize_max) return [] -def _debian_versions(pkg_name): +def _merge_version_candidates(primary, extra, normalize_max=200): + """Dedupe and sort descending for RabbitMQ multi-source repoquery.""" + return _normalize_versions( + _sort_versions_desc(list(primary or []) + list(extra or [])), + max_items=normalize_max, + ) + + +def _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, el_major): + """ + Query rabbitmq-server versions from a specific Packagecloud el/N path without + enabling that repo system-wide. Helps when el/9 metadata lags el/8 for 4.x. + """ + arch = platform.machine() or 'x86_64' + repoid = 'cybercp-pc-rmq-el{0}'.format(int(el_major)) + base = 'https://packagecloud.io/rabbitmq/rabbitmq-server/el/{0}/{1}'.format( + int(el_major), arch + ) + cmd = [ + 'dnf', + 'repoquery', + '--repofrompath={0},{1}'.format(repoid, base), + '--setopt={0}.gpgcheck=0'.format(repoid), + '--setopt={0}.repo_gpgcheck=0'.format(repoid), + '--available', + '--show-duplicates', + '--qf', + '%{version}', + pkg_name, + ] + rc, out, _ = _run(cmd, timeout=240) + if rc != 0 or not (out or '').strip(): + return [] + raw = [] + for line in out.splitlines(): + v = (line or '').strip() + if v and re.match(r'^[0-9]', v): + raw.append(v) + return _normalize_versions(_sort_versions_desc(raw), max_items=200) + + +def _debian_versions(pkg_name, normalize_max=25): versions = [] _run(['apt-get', 'update', '-y'], timeout=180) rc, out, _ = _run(['apt-cache', 'madison', pkg_name], timeout=60) @@ -259,7 +317,7 @@ def _debian_versions(pkg_name): 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)) + return _normalize_versions(_sort_versions_desc(collected), max_items=normalize_max) def _filter_es_major(versions, es_major): @@ -272,7 +330,7 @@ def _filter_es_major(versions, es_major): return out -def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='3'): +def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='4'): pkg_name = package_name_for_app(app_name) if app_name == 'Elasticsearch': pkg_name = 'elasticsearch' @@ -280,18 +338,20 @@ def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='3' if not pkg_name: return [] - rmq_stream = '3' + rmq_stream = '4' 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 == 'RabbitMQ': + versions = _debian_versions(pkg_name, normalize_max=200) + else: + versions = _debian_versions(pkg_name) if app_name == 'Elasticsearch': versions = _filter_es_major(versions, es_major) else: @@ -301,16 +361,50 @@ def _get_available_versions_uncached(app_name, es_major='8', rabbitmq_stream='3' pkg_name, use_cyberpanel_extra_repos=True ) versions = _filter_es_major(versions, es_major) + elif app_name == 'RabbitMQ': + versions = _rhel_repoquery_versions( + pkg_name, latest_limit=None, normalize_max=200 + ) + host_major = rhel_major_from_os_release() + # el/9 (and newer) enabled repos often omit 4.x in metadata; el/8 tree may list them. + if host_major is not None and host_major >= 9: + pc_el8 = _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, 8) + if pc_el8: + versions = _merge_version_candidates(versions, pc_el8, 200) else: versions = _rhel_repoquery_versions(pkg_name) if app_name == 'RabbitMQ': - from manageServices.application_rabbitmq_repo import filter_versions_for_stream + from manageServices.application_rabbitmq_repo import ( + RABBITMQ_4X_METADATA_FALLBACK_VERSIONS, + filter_versions_for_stream, + refresh_debian_apt_metadata, + refresh_rhel_metadata_for_rabbitmq_repos, + ) versions = filter_versions_for_stream(versions, rmq_stream) + if not versions: + if is_debian_family(): + refresh_debian_apt_metadata() + versions = _debian_versions(pkg_name, normalize_max=200) + else: + refresh_rhel_metadata_for_rabbitmq_repos() + versions = _rhel_repoquery_versions( + pkg_name, latest_limit=None, normalize_max=200 + ) + host_major = rhel_major_from_os_release() + if host_major is not None and host_major >= 9: + pc_el8 = _rhel_repoquery_rabbitmq_packagecloud_el_dist(pkg_name, 8) + if pc_el8: + versions = _merge_version_candidates(versions, pc_el8, 200) + versions = filter_versions_for_stream(versions, rmq_stream) + # Always offer GA 4.x when DNF lists none (panel user may get empty repoquery). + if rmq_stream == '4' and not versions and not is_debian_family(): + versions = list(RABBITMQ_4X_METADATA_FALLBACK_VERSIONS) + versions = _normalize_versions(_sort_versions_desc(versions), max_items=40) return versions -def get_available_versions(app_name, es_major='8', rabbitmq_stream='3'): +def get_available_versions(app_name, es_major='8', rabbitmq_stream='4'): """ Cached wrapper: avoids hammering DNF from many concurrent panel workers (503 on Manage Applications). """ @@ -331,14 +425,14 @@ def get_available_versions(app_name, es_major='8', rabbitmq_stream='3'): return list(versions) -def get_latest_version(app_name, es_major='8', rabbitmq_stream='3'): +def get_latest_version(app_name, es_major='8', rabbitmq_stream='4'): 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'): +def get_branch_and_global_latest(app_name, es_major='8', rabbitmq_stream='4'): """ Latest on the UI-selected branch/stream vs latest across all supported branches. diff --git a/manageServices/serviceManager.py b/manageServices/serviceManager.py index 9518da6c2..a08b3e75b 100644 --- a/manageServices/serviceManager.py +++ b/manageServices/serviceManager.py @@ -193,7 +193,7 @@ def main(): 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)') + parser.add_argument('--rabbitmqStream', default='4', help='RabbitMQ major stream (3|4)') args = vars(parser.parse_args()) @@ -221,12 +221,12 @@ def main(): elif args["function"] == "InstallRabbitMQ": ServiceManager.InstallRabbitMQ( version=args.get('version', 'latest'), - stream=args.get('rabbitmqStream', '3'), + stream=args.get('rabbitmqStream', '4'), ) elif args["function"] == "UpgradeRabbitMQ": ServiceManager.UpgradeRabbitMQ( version=args.get('version', 'latest'), - stream=args.get('rabbitmqStream', '3'), + stream=args.get('rabbitmqStream', '4'), ) elif args["function"] == "RemoveRabbitMQ": ServiceManager.RemoveRabbitMQ() @@ -235,7 +235,7 @@ def main(): action = args.get("action").lower() version = args.get("version", "latest") es_major = args.get("esMajor", "8") - rmq_stream = args.get("rabbitmqStream", "3") + rmq_stream = args.get("rabbitmqStream", "4") if app_name == 'Elasticsearch': if action == 'install': diff --git a/manageServices/static/manageServices/manageServices.js b/manageServices/static/manageServices/manageServices.js index 057ce349e..203436de1 100644 --- a/manageServices/static/manageServices/manageServices.js +++ b/manageServices/static/manageServices/manageServices.js @@ -477,6 +477,26 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) return out; } + function versionMatchesRabbitmqStream(ver, stream) { + var s = String(stream || '4').trim(); + var t = normalizeVersionToken(ver); + if (!t || t === 'latest') { + return false; + } + var m = /^(\d+)\./.exec(t); + return !!(m && m[1] === s); + } + + function versionMatchesEsMajor(ver, major) { + var mjr = String(major || '8').trim(); + var t = normalizeVersionToken(ver); + if (!t || t === 'latest') { + return false; + } + var m = /^(\d+)\./.exec(t); + return !!(m && m[1] === mjr); + } + $scope.versionLabel = function (v) { if (v === 'latest') { return 'latest'; @@ -535,7 +555,8 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested, versions: vers, latestAvailable: meta.latestAvailable || '', - latestOverall: meta.latestOverall || '' + latestOverall: meta.latestOverall || '', + rabbitmqVersionsHint: meta.rabbitmqVersionsHint || '' }; }); } catch (ignore) { @@ -581,10 +602,33 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) } }; $scope.selectedEsMajor = '8'; - $scope.selectedRabbitmqStream = '3'; + $scope.selectedRabbitmqStream = '4'; + /** RabbitMQ: 4.x is default for new installs (metadata prefetched). Upgrade may require picking stream if version line is unknown. ES major still user-picked before version list loads. */ + $scope.rabbitmqBranchChosen = false; + $scope.esMajorChosen = false; $scope.confirmAction = false; $scope.selectedCurrentVersion = ''; + $scope.chooseRabbitmqStream = function (stream) { + var s = String(stream || '4').trim(); + if (s !== '3' && s !== '4') { + s = '4'; + } + $scope.selectedRabbitmqStream = s; + $scope.rabbitmqBranchChosen = true; + $scope.refreshMeta(); + }; + + $scope.chooseEsMajor = function (major) { + var m = String(major || '8').trim(); + if (m !== '7' && m !== '8' && m !== '9') { + m = '8'; + } + $scope.selectedEsMajor = m; + $scope.esMajorChosen = true; + $scope.refreshMeta(); + }; + /** * 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.) @@ -596,13 +640,35 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) if ($scope.appName !== 'Elasticsearch' && $scope.appName !== 'Redis' && $scope.appName !== 'RabbitMQ') { return; } + if ($scope.appName === 'RabbitMQ' && !$scope.rabbitmqBranchChosen) { + $scope.selectedVersions = ['latest']; + $scope.selectedVersion = 'latest'; + $scope.repoShowsOnlyOneStream = false; + $scope.recalcSelectedVersionRowIndex(); + return; + } + if ($scope.appName === 'Elasticsearch' && !$scope.esMajorChosen) { + $scope.selectedVersions = ['latest']; + $scope.selectedVersion = 'latest'; + $scope.repoShowsOnlyOneStream = false; + $scope.recalcSelectedVersionRowIndex(); + 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); + var allowCur = true; + if ($scope.appName === 'RabbitMQ') { + allowCur = versionMatchesRabbitmqStream(cur, $scope.selectedRabbitmqStream); + } else if ($scope.appName === 'Elasticsearch') { + allowCur = versionMatchesEsMajor(cur, $scope.selectedEsMajor); + } + if (allowCur) { + $scope.selectedVersions.push(cur); + } } if (cur) { $scope.selectedCurrentVersion = cur; @@ -646,6 +712,16 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) (payload.apps || []).forEach(function (app) { appMap[app.name] = app; }); + var esMetaResp = appMap['Elasticsearch']; + var rmqMetaResp = appMap['RabbitMQ']; + var respEsMaj = String(esMetaResp && esMetaResp.major != null ? esMetaResp.major : '').trim(); + if (respEsMaj && respEsMaj !== String($scope.selectedEsMajor || '8').trim()) { + return; + } + var respRmqStream = String(rmqMetaResp && rmqMetaResp.rabbitmqStream != null ? rmqMetaResp.rabbitmqStream : '').trim(); + if (respRmqStream && respRmqStream !== String($scope.selectedRabbitmqStream || '4').trim()) { + return; + } $scope.apps = $scope.apps.map(function (baseApp) { var meta = appMap[baseApp.name] || {}; var vers = meta.versions; @@ -662,7 +738,8 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) crossBranchUpdateSuggested: !!meta.crossBranchUpdateSuggested, versions: vers, latestAvailable: meta.latestAvailable || '', - latestOverall: meta.latestOverall || '' + latestOverall: meta.latestOverall || '', + rabbitmqVersionsHint: meta.rabbitmqVersionsHint || '' }; }); $scope.syncModalVersionLists(); @@ -700,6 +777,13 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) $scope.selectedRabbitmqStream = '4'; } else if (effectiveInstalled && /^3\./.test(effectiveInstalled)) { $scope.selectedRabbitmqStream = '3'; + } else if (status === 'Installing') { + $scope.selectedRabbitmqStream = '4'; + } + if (status === 'Upgrading' && effectiveInstalled) { + if (/^4\./.test(effectiveInstalled) || /^3\./.test(effectiveInstalled)) { + $scope.rabbitmqBranchChosen = true; + } } } @@ -715,13 +799,17 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) } $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); + var deferVersionList = (service.name === 'RabbitMQ' && !$scope.rabbitmqBranchChosen) + || (service.name === 'Elasticsearch' && !$scope.esMajorChosen); + if (!deferVersionList) { + 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 = ''; @@ -729,7 +817,11 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) var realVers = ($scope.selectedVersions || []).filter(function (v) { return v && v !== 'latest'; }); - $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + if (deferVersionList) { + $scope.repoShowsOnlyOneStream = false; + } else { + $scope.repoShowsOnlyOneStream = ($scope.status === 'Upgrading' && realVers.length <= 1); + } $scope.recalcSelectedVersionRowIndex(); }; @@ -775,44 +867,28 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) } }; 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)) { + if (appName === 'RabbitMQ') { + if (status === 'Installing') { $scope.selectedRabbitmqStream = '4'; - } else if (/^3\./.test(bootVer)) { - $scope.selectedRabbitmqStream = '3'; + $scope.rabbitmqBranchChosen = true; + } else { + $scope.rabbitmqBranchChosen = false; } } - 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'; - } + if (appName === 'Elasticsearch') { + $scope.esMajorChosen = false; } - // Open immediately with bootstrap / in-memory meta; refresh in background. - // Blocking on applicationMeta made "Change version" feel frozen (dnf/repoquery). + // RabbitMQ install: default 4.x stream and prefetch metadata. Upgrade: pick stream from installed version. + // Elasticsearch: still wait for user major. Redis refreshes on open. $scope.appName = appName; $scope.status = status; $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); showModal(); - $timeout(function () { - $scope.refreshMeta(); - }, 0); + if (appName === 'Redis' || (appName === 'RabbitMQ' && $scope.rabbitmqBranchChosen)) { + $timeout(function () { + $scope.refreshMeta(); + }, 0); + } } else { $scope.prepareActionByName(appName, status, bootstrapInstalledVersion); showModal(); @@ -833,6 +909,22 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) }); return; } + if (appName === 'RabbitMQ' && (status === 'Installing' || status === 'Upgrading') && !$scope.rabbitmqBranchChosen) { + new PNotify({ + title: 'Stream required', + text: 'Choose RabbitMQ 4.x or 3.x above to load versions for that line.', + type: 'warning' + }); + return; + } + if (appName === 'Elasticsearch' && (status === 'Installing' || status === 'Upgrading') && !$scope.esMajorChosen) { + new PNotify({ + title: 'Major version required', + text: 'Choose Elasticsearch major (7, 8, or 9) above to load versions for that line.', + type: 'warning' + }); + return; + } $scope.status = status; $scope.appName = appName; @@ -845,7 +937,7 @@ app.controller('manageApplications', function ($scope, $http, $timeout, $window) status: status, version: $scope.selectedVersion || 'latest', esMajor: $scope.selectedEsMajor || '8', - rabbitmqStream: $scope.selectedRabbitmqStream || '3', + rabbitmqStream: $scope.selectedRabbitmqStream || '4', confirmAction: $scope.confirmAction === true }; @@ -932,6 +1024,15 @@ 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. + if (typeof window.jQuery !== 'undefined' && jQuery.fn.on) { + jQuery('#settings').on('hidden.bs.modal', function () { + $scope.$evalAsync(function () { + $scope.rabbitmqBranchChosen = false; + $scope.esMajorChosen = false; + }); + }); + } + }); /* Java script code */ \ No newline at end of file diff --git a/manageServices/templates/manageServices/applications.html b/manageServices/templates/manageServices/applications.html index fa4635613..cc029e1c8 100644 --- a/manageServices/templates/manageServices/applications.html +++ b/manageServices/templates/manageServices/applications.html @@ -587,7 +587,44 @@