diff --git a/cmapi/cmapi_server/managers/upgrade/preinstall.py b/cmapi/cmapi_server/managers/upgrade/preinstall.py index 97991615a..b82742039 100644 --- a/cmapi/cmapi_server/managers/upgrade/preinstall.py +++ b/cmapi/cmapi_server/managers/upgrade/preinstall.py @@ -1,10 +1,61 @@ import logging import re +import shlex +from typing import Dict, List, Tuple -from cmapi_server.exceptions import CMAPIBasicError +from cmapi_server.constants import DEFAULT_MCS_CONF_PATH +from cmapi_server.exceptions import CEJError, CMAPIBasicError +from cmapi_server.helpers import get_cej_info +from mcs_node_control.models.node_config import NodeConfig from cmapi_server.process_dispatchers.base import BaseDispatcher +# Registry of MariaDB plugins that the install_es auto-upgrader does NOT +# handle. If any of these are ACTIVE on the local node the precheck aborts +# the upgrade and instructs the operator how to proceed manually. +# +# Keys are compared case-insensitively against PLUGIN_NAME as reported by +# information_schema.PLUGINS. Currently focused on external storage engines +# shipped as separate mariadb-plugin-* packages; extend as needed. +UNSUPPORTED_PLUGINS_AUTOUPGRADE: Dict[str, str] = { + 'SPIDER': ( + "UNINSTALL SONAME 'ha_spider.so'; stop MariaDB; upgrade the " + "mariadb-plugin-spider package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_spider.so'." + ), + 'CONNECT': ( + "UNINSTALL SONAME 'ha_connect.so'; stop MariaDB; upgrade the " + "mariadb-plugin-connect package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_connect.so'." + ), + 'SPHINX': ( + "UNINSTALL SONAME 'ha_sphinx.so'; stop MariaDB; upgrade the " + "mariadb-plugin-sphinx package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_sphinx.so'." + ), + 'OQGRAPH': ( + "UNINSTALL SONAME 'ha_oqgraph.so'; stop MariaDB; upgrade the " + "mariadb-plugin-oqgraph package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_oqgraph.so'." + ), + 'MROONGA': ( + "UNINSTALL SONAME 'ha_mroonga.so'; stop MariaDB; upgrade the " + "mariadb-plugin-mroonga package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_mroonga.so'." + ), + 'ROCKSDB': ( + "UNINSTALL SONAME 'ha_rocksdb.so'; stop MariaDB; upgrade the " + "mariadb-plugin-rocksdb package to a version matching the target " + "ES release; start MariaDB; INSTALL SONAME 'ha_rocksdb.so'." + ), + 'S3': ( + "UNINSTALL SONAME 'ha_s3.so'; stop MariaDB; upgrade the " + "mariadb-plugin-s3 package to a version matching the target ES " + "release; start MariaDB; INSTALL SONAME 'ha_s3.so'." + ), +} + + class PreInstallManager: @staticmethod @@ -52,3 +103,96 @@ class PreInstallManager: ) logging.error(message) raise CMAPIBasicError(message) + + @staticmethod + def _get_active_plugins( + config: str = DEFAULT_MCS_CONF_PATH, + ) -> List[Tuple[str, str]]: + """Return list of ACTIVE plugins on the local MariaDB node. + + Uses CrossEngineSupport credentials from Columnstore.xml (same as + `helpers.system_ready`). Plugin registration is stored in the + mysql.plugin system table so a single-node query is sufficient + for a Columnstore cluster (mysql.plugin is server-local, but + Columnstore cluster nodes share plugin state via replication or + identical package layout by convention). + + :return: list of (PLUGIN_NAME, PLUGIN_TYPE) tuples + :raises CMAPIBasicError: if the query cannot be executed + """ + nc = NodeConfig() + root = nc.get_current_config_root(config) + try: + host, port, username, password = get_cej_info(root) + except CEJError as exc: + raise CMAPIBasicError( + 'Cannot detect installed MariaDB plugins: ' + 'CrossEngineSupport credentials are missing or invalid. ' + f'Details: {exc}' + ) from exc + + query = ( + "SELECT PLUGIN_NAME, PLUGIN_TYPE " + "FROM information_schema.PLUGINS " + "WHERE PLUGIN_STATUS='ACTIVE';" + ) + cmd = ( + f'/usr/bin/mariadb -h {shlex.quote(host)} ' + f'-P {shlex.quote(str(port))} ' + f'-u {shlex.quote(username)} ' + f'--password={shlex.quote(password)} ' + f'-sN -e {shlex.quote(query)}' + ) + # silent=True: we log our own message on failure and don't want the + # password-bearing command line dumped into logs on error. + success, output = BaseDispatcher.exec_command(cmd, silent=True) + if not success: + raise CMAPIBasicError( + 'Failed to query information_schema.PLUGINS on local ' + 'MariaDB node. Ensure MariaDB is running and ' + 'CrossEngineSupport credentials are valid before running ' + 'install_es.' + ) + + plugins: List[Tuple[str, str]] = [] + for line in output.splitlines(): + parts = line.split('\t') + if len(parts) >= 2 and parts[0]: + plugins.append((parts[0].strip(), parts[1].strip())) + return plugins + + @staticmethod + def check_installed_plugins( + config: str = DEFAULT_MCS_CONF_PATH, + ) -> None: + """Detect ACTIVE plugins not supported by install_es auto-upgrade. + + Raises CMAPIBasicError listing every unsupported plugin along + with per-plugin manual upgrade instructions. Bundled MariaDB + engines (InnoDB, Aria, MyISAM, Columnstore, standard auth + plugins, etc.) are ignored — only plugins present in + UNSUPPORTED_PLUGINS_AUTOUPGRADE are reported. + """ + active = PreInstallManager._get_active_plugins(config) + detected: List[Tuple[str, str, str]] = [] + for name, ptype in active: + instructions = UNSUPPORTED_PLUGINS_AUTOUPGRADE.get(name.upper()) + if instructions is not None: + detected.append((name, ptype, instructions)) + + if not detected: + logging.info( + 'Plugin precheck: no unsupported plugins detected.' + ) + return + + lines = [ + 'Detected MariaDB plugins that install_es cannot upgrade ' + 'automatically. Upgrade each of them manually, then re-run ' + 'install_es:' + ] + for name, ptype, instructions in detected: + lines.append(f' - {name} ({ptype}): {instructions}') + message = '\n'.join(lines) + logging.error(message) + raise CMAPIBasicError(message) diff --git a/cmapi/mcs_cluster_tool/tools_commands.py b/cmapi/mcs_cluster_tool/tools_commands.py index 33d46bee3..7c1ecd106 100644 --- a/cmapi/mcs_cluster_tool/tools_commands.py +++ b/cmapi/mcs_cluster_tool/tools_commands.py @@ -27,10 +27,11 @@ from cmapi_server.constants import ( from cmapi_server.controllers.api_clients import ( ClusterControllerClient, NodeControllerClient ) -from cmapi_server.exceptions import CEJError +from cmapi_server.exceptions import CEJError, CMAPIBasicError from cmapi_server.handlers.cej import CEJPasswordHandler from cmapi_server.helpers import get_active_nodes, get_cej_info, get_config_parser, get_current_key from cmapi_server.managers.transaction import TransactionManager +from cmapi_server.managers.upgrade.preinstall import PreInstallManager from cmapi_server.managers.upgrade.utils import ComparableVersion from cmapi_server.process_dispatchers.base import BaseDispatcher from mcs_cluster_tool.constants import MCS_COLUMNSTORE_REVIEW_SH @@ -738,6 +739,17 @@ def install_es( node_api_client, token, target_version, console ) + # Prechecks: fail fast, before the cluster is stopped. + console.print('Running pre-upgrade checks...', style='green') + try: + PreInstallManager.check_gtid_strict_mode() + PreInstallManager.check_installed_plugins() + except CMAPIBasicError as exc: + console.print('[red]Pre-upgrade check failed:[/red]') + console.print(exc.message) + raise typer.Exit(code=1) + console.print('[green]Pre-upgrade checks passed ✓[/green]') + # Retrieve current versions (handles mismatch display) versions = get_current_versions(cluster_api_client, console, ignore_mismatch) mdb_curr_ver = versions['server_version']