1
0
mirror of https://github.com/mariadb-corporation/mariadb-columnstore-engine.git synced 2025-11-02 06:13:16 +03:00
Files
mariadb-columnstore-engine/cmapi/cmapi_server/managers/upgrade/packages.py
mariadb-AlanMologorsky a76e153a1d feat(upgrade): MCOL-6028 upgrade MVP with repo-managed flow, prechecks, and other enhancements
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`
2025-09-30 18:48:32 +04:00

145 lines
6.0 KiB
Python

import logging
from cmapi_server.constants import (
MDB_CS_PACKAGE_NAME, MDB_SERVER_PACKAGE_NAME
)
from cmapi_server.exceptions import CMAPIBasicError
from cmapi_server.process_dispatchers.base import BaseDispatcher
class PackagesManager:
"""
This class is responsible for managing the installation of packages.
It provides methods to install, uninstall packages.
"""
def __init__(self, os_name: str, mdb_version: str, mcs_version: str):
self.os_name = os_name
self.mdb_version = mdb_version
self.mcs_version = mcs_version
self.pkg_manager: str = ''
if self.os_name in ['ubuntu', 'debian']:
# Prefer apt-get in scripts for stability and noninteractive support
self.pkg_manager = 'apt-get'
self.mdb_pkg_name = MDB_SERVER_PACKAGE_NAME.deb
self.mcs_pkg_name = MDB_CS_PACKAGE_NAME.deb
elif self.os_name in ['centos', 'rhel', 'rocky']:
self.pkg_manager = 'yum'
self.mdb_pkg_name = MDB_SERVER_PACKAGE_NAME.rhel
self.mcs_pkg_name = MDB_CS_PACKAGE_NAME.rhel
else:
raise CMAPIBasicError(f'Unsupported OS type: {self.os_name}')
def install_package(self, package_name: str, version: str = 'latest', dry_run: bool = False):
"""Install a package by its name.
:param package_name: the name of the package to install.
:param version: version to install
:param dry_run: if True, simulate the transaction without making changes
"""
extra_args = ''
dry_run_flag = ''
conf_opt = ''
env_vars = None
package = package_name
if self.os_name in ['ubuntu', 'debian']:
if version != 'latest':
package = f'{package_name}={version}'
# Allow downgrades explicitly when version is pinned
extra_args = '--allow-downgrades'
# Noninteractive mode and ALWAYS keep current configs
env_vars = {'DEBIAN_FRONTEND': 'noninteractive'}
conf_opt = '-o Dpkg::Options::=--force-confold'
dry_run_flag = '-s' if dry_run else ''
elif self.os_name in ['centos', 'rhel', 'rocky']:
if version != 'latest':
package = f'{package_name}-{version}'
# For yum, use --assumeno as a safe preview of the transaction
# but it exits non-zero; prefer tsflags=test for a clean dry-run
dry_run_flag = '--setopt=tsflags=test' if dry_run else ''
cmd = (
f"{self.pkg_manager} install -y {dry_run_flag} "
f"{conf_opt} {extra_args} {package}"
).strip()
success, result = BaseDispatcher.exec_command(cmd, env=env_vars)
if not success:
message = (
f'Failed to install {package} using command {cmd} with '
f'result: {result}'
)
logging.error(message)
raise CMAPIBasicError(message)
def remove_package(self, package_name: str, *, dry_run: bool = False) -> bool:
"""Uninstall a package by its name.
:param package_name: The name of the package to remove.
:param dry_run: if True, simulate the transaction without making changes
"""
env_vars = None
dry_flag = ''
if self.os_name in ['ubuntu', 'debian']:
env_vars = {'DEBIAN_FRONTEND': 'noninteractive'}
dry_flag = '-s' if dry_run else ''
elif self.os_name in ['centos', 'rhel', 'rocky']:
# use tsflags=test to simulate with zero exit code
dry_flag = '--setopt=tsflags=test' if dry_run else ''
cmd = f'{self.pkg_manager} remove -y {dry_flag} {package_name}'.strip()
success, result = BaseDispatcher.exec_command(cmd, env=env_vars)
if not success:
message = (
f'Failed to remove {package_name} using command {cmd} with '
f'result: {result}'
)
logging.error(message)
raise CMAPIBasicError(message)
def upgrade_mdb_and_mcs(self, *, precheck: bool = True):
"""Remove packages and then install newer or older versions.
The function can perform a dry-run of all steps first. If any of the
simulated transactions fail, the actual removal/installation will not
be executed.
:param precheck: when True, simulate remove/install before real actions
"""
if precheck:
# 1) Simulate removals; fail fast if not possible
self.remove_package(self.mcs_pkg_name, dry_run=True)
self.remove_package(self.mdb_pkg_name, dry_run=True)
# 2) Perform actual removals
self.remove_package(self.mcs_pkg_name)
self.remove_package(self.mdb_pkg_name)
if precheck:
# 3) Now that packages are removed, simulate installs to validate deps
self.install_package(self.mdb_pkg_name, self.mdb_version, dry_run=True)
self.install_package(self.mcs_pkg_name, self.mcs_version, dry_run=True)
# 4) Perform actual installs
self.install_package(self.mdb_pkg_name, self.mdb_version)
self.install_package(self.mcs_pkg_name, self.mcs_version)
@classmethod
def kick_cmapi_upgrade(cls, cmapi_version: str):
"""Starts the one-shot cmapi_updater.service.
:param cmapi_version: target CMAPI version to install
:type cmapi_version: str
"""
with open('/tmp/cmapi_updater.conf', 'w+', encoding='utf-8') as file:
file.write(f'CMAPI_VERSION={cmapi_version}')
cmd = 'systemctl start cmapi_updater.service'
success, result = BaseDispatcher.exec_command(cmd, daemonize=True)
# Note: this likely never reports an error in practice, but we still check.
if not success:
message = (
f'Failed to start cmapi_updater.serice using command {cmd} '
f'with result: {result}'
)
logging.error(message)
raise CMAPIBasicError(message)
logging.info('Started cmapi_updater.service to upgrade CMAPI.')