You've already forked mariadb-columnstore-engine
							
							
				mirror of
				https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
				synced 2025-11-03 17:13:17 +03:00 
			
		
		
		
	Implements the initial upgrade capability across CMAPI and the CLI, including
repository setup, package operations, environment prechecks, and coordinated
cluster steps with progress reporting.
Details:
- CMAPI upgrade manager:
  - Add `cmapi/cmapi_server/managers/upgrade/` modules:
    - `repo.py`, `packages.py`, `preinstall.py`, `upgrade.py`, `utils.py` and `__init__.py`
  - Extend endpoints and routing to expose upgrade operations and status:
    - `cmapi_server/controllers/{endpoints.py, dispatcher.py, api_clients.py}`
    - `cmapi_server/managers/{application.py, process.py}`
    - Add improved constants and helpers for upgrade flow
- Backup/restore and safety:
  - Add `cmapi_server/managers/backup_restore.py`
  - Fix pre-upgrade backup regressions (due to `mcs_backup_manager.sh 3.17 changes`)
  - Improve cluster version validation; add `ignore_missmatch` override
- CLI enhancements:
  - Progress UI and richer feedback (`mcs_cluster_tool/tools_commands.py`, `README.md`, `mcs.1`)
  - Add steps to start MDB and start MCS during/after upgrade
  - Improved error surfacing for version validation
- Platform and packaging:
  - Ubuntu and Rocky Linux support
  - RHEL/DNF dry-run support
  - Distro detection and platform-dependent logic hardened
  - Logging improvements
- Updater service:
  - Add `cmapi/updater/cmapi_updater.service.template` and `cmapi_updater.sh` to make CMAPI update itself
- Docs:
  - Update mcs cli README and mcs.1 man file
  - Add `cmapi/updater/README.md`
		
	
		
			
				
	
	
		
			183 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import json
 | 
						|
import logging
 | 
						|
import logging.config
 | 
						|
from functools import partial, partialmethod
 | 
						|
 | 
						|
import cherrypy
 | 
						|
from cherrypy import _cperror
 | 
						|
 | 
						|
from cmapi_server.constants import CMAPI_LOG_CONF_PATH
 | 
						|
from tracing.tracer import get_tracer
 | 
						|
 | 
						|
 | 
						|
class AddIpFilter(logging.Filter):
 | 
						|
    """Filter to add IP address to logging record."""
 | 
						|
    def filter(self, record):
 | 
						|
        record.ip = cherrypy.request.remote.name or cherrypy.request.remote.ip
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
class TraceParamsFilter(logging.Filter):
 | 
						|
    """Filter that adds trace_params to log records, except for the 'tracer' logger."""
 | 
						|
    def filter(self, record: logging.LogRecord) -> bool:
 | 
						|
        # Don't print trace params for tracer logger family; it already prints trace data
 | 
						|
        if record.name == 'tracer' or record.name.startswith('tracer.'):
 | 
						|
            record.trace_params = ""
 | 
						|
            return True
 | 
						|
 | 
						|
        trace_id, span_id, parent_span_id = get_tracer().current_trace_ids()
 | 
						|
        if trace_id and span_id:
 | 
						|
            trace_params = f"rid={trace_id} sid={span_id}"
 | 
						|
            if parent_span_id:
 | 
						|
                trace_params += f" psid={parent_span_id}"
 | 
						|
            record.trace_params = trace_params
 | 
						|
        else:
 | 
						|
            record.trace_params = ""
 | 
						|
        return True
 | 
						|
 | 
						|
def custom_cherrypy_error(
 | 
						|
        self, msg='', context='', severity=logging.INFO, traceback=False
 | 
						|
    ):
 | 
						|
    """Write the given ``msg`` to the error log. [now without hardcoded time]
 | 
						|
 | 
						|
    This is not just for errors! [looks awful, but cherrypy realisation as is]
 | 
						|
    Applications may call this at any time to log application-specific
 | 
						|
    information.
 | 
						|
 | 
						|
    If ``traceback`` is True, the traceback of the current exception
 | 
						|
    (if any) will be appended to ``msg``.
 | 
						|
 | 
						|
    ..Note:
 | 
						|
        All informatio
 | 
						|
    """
 | 
						|
    exc_info = None
 | 
						|
    if traceback:
 | 
						|
        exc_info = _cperror._exc_info()
 | 
						|
 | 
						|
    self.error_log.log(severity, ' '.join((context, msg)), exc_info=exc_info)
 | 
						|
 | 
						|
 | 
						|
def dict_config(config_filepath: str):
 | 
						|
    with open(config_filepath, 'r', encoding='utf-8') as json_config:
 | 
						|
        config_dict = json.load(json_config)
 | 
						|
    logging.config.dictConfig(config_dict)
 | 
						|
 | 
						|
 | 
						|
def add_logging_level(level_name, level_num, method_name=None):
 | 
						|
    """
 | 
						|
    Comprehensively adds a new logging level to the `logging` module and the
 | 
						|
    currently configured logging class.
 | 
						|
 | 
						|
    `level_name` becomes an attribute of the `logging` module with the value
 | 
						|
    `level_num`.
 | 
						|
    `methodName` becomes a convenience method for both `logging` itself
 | 
						|
    and the class returned by `logging.getLoggerClass()` (usually just
 | 
						|
    `logging.Logger`).
 | 
						|
    If `methodName` is not specified, `levelName.lower()` is used.
 | 
						|
 | 
						|
    To avoid accidental clobberings of existing attributes, this method will
 | 
						|
    raise an `AttributeError` if the level name is already an attribute of the
 | 
						|
    `logging` module or if the method name is already present
 | 
						|
 | 
						|
    Example
 | 
						|
    -------
 | 
						|
    >>> add_logging_level('TRACE', logging.DEBUG - 5)
 | 
						|
    >>> logging.getLogger(__name__).setLevel('TRACE')
 | 
						|
    >>> logging.getLogger(__name__).trace('that worked')
 | 
						|
    >>> logging.trace('so did this')
 | 
						|
    >>> logging.TRACE
 | 
						|
    5
 | 
						|
 | 
						|
    """
 | 
						|
    if not method_name:
 | 
						|
        method_name = level_name.lower()
 | 
						|
 | 
						|
    if hasattr(logging, level_name):
 | 
						|
        raise AttributeError(f'{level_name} already defined in logging module')
 | 
						|
    if hasattr(logging, method_name):
 | 
						|
        raise AttributeError(
 | 
						|
            f'{method_name} already defined in logging module'
 | 
						|
        )
 | 
						|
    if hasattr(logging.getLoggerClass(), method_name):
 | 
						|
        raise AttributeError(f'{method_name} already defined in logger class')
 | 
						|
 | 
						|
    # This method was inspired by the answers to Stack Overflow post
 | 
						|
    # http://stackoverflow.com/q/2183233/2988730, especially
 | 
						|
    # https://stackoverflow.com/a/35804945
 | 
						|
    # https://stackoverflow.com/a/55276759
 | 
						|
    logging.addLevelName(level_num, level_name)
 | 
						|
    setattr(logging, level_name, level_num)
 | 
						|
    setattr(
 | 
						|
        logging.getLoggerClass(), method_name,
 | 
						|
        partialmethod(logging.getLoggerClass().log, level_num)
 | 
						|
    )
 | 
						|
    setattr(logging, method_name, partial(logging.log, level_num))
 | 
						|
 | 
						|
 | 
						|
def enable_console_logging(logger: logging.Logger) -> None:
 | 
						|
    """Enable logging to console for passed logger by adding a StreamHandler to it"""
 | 
						|
    console_handler = logging.StreamHandler()
 | 
						|
    console_handler.setLevel(logging.DEBUG)
 | 
						|
    console_handler.setFormatter(logger.handlers[0].formatter)
 | 
						|
    logger.addHandler(console_handler)
 | 
						|
 | 
						|
 | 
						|
def config_cmapi_server_logging():
 | 
						|
    # add custom level TRACE only for develop purposes
 | 
						|
    # could be activated using API endpoints or cli tool without relaunching
 | 
						|
    if not hasattr(logging, 'TRACE'):
 | 
						|
        add_logging_level('TRACE', 5)
 | 
						|
    cherrypy._cplogging.LogManager.error = custom_cherrypy_error
 | 
						|
    # reconfigure cherrypy.access log message format
 | 
						|
    # Default access_log_format '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
 | 
						|
    # h - remote.name or remote.ip, l - "-",
 | 
						|
    # u - getattr(request, 'login', None) or '-', t - self.time(),
 | 
						|
    # r - request.request_line, s - status,
 | 
						|
    # b - dict.get(outheaders, 'Content-Length', '') or '-',
 | 
						|
    # f - dict.get(inheaders, 'Referer', ''),
 | 
						|
    # a - dict.get(inheaders, 'User-Agent', ''),
 | 
						|
    # o - dict.get(inheaders, 'Host', '-'),
 | 
						|
    # i - request.unique_id, z - LazyRfc3339UtcTime()
 | 
						|
    cherrypy._cplogging.LogManager.access_log_format = (
 | 
						|
        '{h} ACCESS "{r}" code {s}, bytes {b}, user-agent "{a}"'
 | 
						|
    )
 | 
						|
    # trace_params are populated via TraceParamsFilter configured in logging config
 | 
						|
    dict_config(CMAPI_LOG_CONF_PATH)
 | 
						|
    disable_unwanted_loggers()
 | 
						|
 | 
						|
 | 
						|
def change_loggers_level(level: str):
 | 
						|
    """Set level for each custom logger except cherrypy library.
 | 
						|
 | 
						|
    :param level: logging level to set
 | 
						|
    :type level: str
 | 
						|
    """
 | 
						|
    loggers = [
 | 
						|
        logging.getLogger(name) for name in logging.root.manager.loggerDict
 | 
						|
        if 'cherrypy' not in name
 | 
						|
    ]
 | 
						|
    loggers.append(logging.getLogger())  # add RootLogger
 | 
						|
    for logger in loggers:
 | 
						|
        logger.setLevel(level)
 | 
						|
 | 
						|
def disable_unwanted_loggers():
 | 
						|
    logging.getLogger('urllib3').setLevel(logging.WARNING)
 | 
						|
 | 
						|
 | 
						|
class JsonFormatter(logging.Formatter):
 | 
						|
    # Standard LogRecord fields
 | 
						|
    skip_fields = set(logging.LogRecord('',0,'',0,'',(),None).__dict__.keys())
 | 
						|
 | 
						|
    def format(self, record):
 | 
						|
        data = {
 | 
						|
            "ts": self.formatTime(record, self.datefmt),
 | 
						|
            "level": record.levelname,
 | 
						|
            "logger": record.name,
 | 
						|
            "msg": record.getMessage(),
 | 
						|
        }
 | 
						|
        # Extract extras from the record (all attributes except standard LogRecord fields)
 | 
						|
        for k, v in record.__dict__.items():
 | 
						|
            if k not in self.skip_fields:
 | 
						|
                data[k] = v
 | 
						|
        # Allow non-serializable extras (e.g., bytes, datetime) to be stringified
 | 
						|
        return json.dumps(data, ensure_ascii=False, sort_keys=True, default=str) |