You've already forked mariadb-columnstore-engine
mirror of
https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
synced 2025-11-02 06:13:16 +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`
238 lines
7.6 KiB
Python
238 lines
7.6 KiB
Python
import logging
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
from shutil import copyfile
|
|
|
|
import requests
|
|
|
|
from cmapi_server.constants import MCSProgs, _version
|
|
from cmapi_server.managers.process import MCSProcessManager
|
|
from cmapi_server.test.unittest_global import (
|
|
COPY_MCS_CONFIG_FILEPATH,
|
|
MCS_CONFIG_FILEPATH,
|
|
TEST_MCS_CONFIG_FILEPATH,
|
|
BaseServerTestCase,
|
|
)
|
|
|
|
logging.basicConfig(level='DEBUG')
|
|
requests.urllib3.disable_warnings()
|
|
|
|
|
|
class BaseClusterTestCase(BaseServerTestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
copyfile(MCS_CONFIG_FILEPATH, COPY_MCS_CONFIG_FILEPATH)
|
|
return super().setUpClass()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
copyfile(COPY_MCS_CONFIG_FILEPATH, MCS_CONFIG_FILEPATH)
|
|
os.remove(os.path.abspath(COPY_MCS_CONFIG_FILEPATH))
|
|
MCSProcessManager.stop_node(is_primary=True, use_sudo=False)
|
|
MCSProcessManager.start_node(is_primary=True, use_sudo=False)
|
|
return super().tearDownClass()
|
|
|
|
def setUp(self) -> None:
|
|
copyfile(TEST_MCS_CONFIG_FILEPATH, MCS_CONFIG_FILEPATH)
|
|
MCSProcessManager.stop_node(is_primary=True, use_sudo=False)
|
|
MCSProcessManager.start_node(is_primary=True, use_sudo=False)
|
|
return super().setUp()
|
|
|
|
|
|
class ClusterStartTestCase(BaseClusterTestCase):
|
|
URL = f'https://localhost:8640/cmapi/{_version}/cluster/start'
|
|
|
|
def test_endpoint_with_no_api_key(self):
|
|
r = requests.put(
|
|
self.URL, verify=False, headers=self.NO_AUTH_HEADERS,
|
|
json={}
|
|
)
|
|
self.assertEqual(r.status_code, 401)
|
|
|
|
def test_endpoint_with_no_nodes_in_cluster(self):
|
|
r = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json={}
|
|
)
|
|
error = r.json()['error']
|
|
self.assertEqual(r.status_code, 422)
|
|
self.assertEqual(error, 'There are no nodes in the cluster.')
|
|
|
|
def test_start_after_adding_a_node(self):
|
|
payload = {'node': socket.gethostname()}
|
|
resp = requests.post(
|
|
ClusterAddNodeTestCase.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
payload = {'node': None}
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS, json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# test_columnstore_started
|
|
controllernode = subprocess.check_output(['pgrep', 'controllernode'])
|
|
self.assertIsNotNone(controllernode)
|
|
|
|
|
|
class ClusterShutdownTestCase(BaseClusterTestCase):
|
|
URL = f'https://localhost:8640/cmapi/{_version}/cluster/shutdown'
|
|
|
|
def test_endpoint_with_no_api_key(self):
|
|
r = requests.put(
|
|
self.URL, verify=False, headers=self.NO_AUTH_HEADERS,
|
|
json={}
|
|
)
|
|
self.assertEqual(r.status_code, 401)
|
|
|
|
def test_endpoint_with_no_nodes_in_cluster(self):
|
|
resp = requests.put(self.URL, verify=False, headers=self.HEADERS,
|
|
json={}
|
|
)
|
|
error = resp.json()['error']
|
|
self.assertEqual(resp.status_code, 422)
|
|
self.assertEqual(error, 'There are no nodes in the cluster.')
|
|
|
|
def test_add_node_and_shutdown(self):
|
|
payload = {'node': socket.gethostname()}
|
|
resp = requests.post(
|
|
ClusterAddNodeTestCase.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# note: POST node starts up node
|
|
try:
|
|
controllernode = subprocess.check_output(
|
|
['pgrep', 'controllernode']
|
|
)
|
|
except Exception:
|
|
controllernode = None
|
|
self.assertIsNotNone(controllernode)
|
|
|
|
payload = {'timeout': 60}
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# Check columnstore stopped
|
|
try:
|
|
controllernode = subprocess.check_output(
|
|
['pgrep', 'controllernode']
|
|
)
|
|
except Exception:
|
|
controllernode = None
|
|
self.assertIsNone(controllernode)
|
|
|
|
|
|
class ClusterModesetTestCase(BaseClusterTestCase):
|
|
URL = f'https://localhost:8640/cmapi/{_version}/cluster/mode-set'
|
|
|
|
def test_endpoint_with_no_api_key(self):
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.NO_AUTH_HEADERS,
|
|
json={}
|
|
)
|
|
self.assertEqual(resp.status_code, 401)
|
|
|
|
def test_endpoint_with_no_nodes_in_cluster(self):
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json={}
|
|
)
|
|
error = resp.json()['error']
|
|
self.assertEqual(resp.status_code, 422)
|
|
self.assertEqual(error, 'There are no nodes in the cluster.')
|
|
|
|
def test_add_node_and_set_readonly(self):
|
|
payload = {'node': socket.gethostname()}
|
|
resp = requests.post(
|
|
ClusterAddNodeTestCase.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
payload = {'mode': 'readonly'}
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS, json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# return readwrite mode back
|
|
payload = {'mode': 'readwrite'}
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS, json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
class ClusterAddNodeTestCase(BaseClusterTestCase):
|
|
URL = f'https://localhost:8640/cmapi/{_version}/cluster/node'
|
|
|
|
def test_endpoint_with_no_apikey(self):
|
|
resp = requests.post(
|
|
self.URL, verify=False, headers=self.NO_AUTH_HEADERS,
|
|
json={}
|
|
)
|
|
self.assertEqual(resp.status_code, 401)
|
|
|
|
def test_endpoint_with_missing_node_parameter(self):
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json={}
|
|
)
|
|
error = resp.json()['error']
|
|
self.assertEqual(resp.status_code, 422)
|
|
self.assertEqual(error, 'missing node argument')
|
|
|
|
def test_endpoint(self):
|
|
payload = {'node': socket.gethostname()}
|
|
resp = requests.put(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
# Check Columnstore started
|
|
controllernode = subprocess.check_output(
|
|
['pgrep', MCSProgs.CONTROLLER_NODE])
|
|
self.assertIsNotNone(controllernode)
|
|
|
|
|
|
class ClusterRemoveNodeTestCase(BaseClusterTestCase):
|
|
URL = ClusterAddNodeTestCase.URL
|
|
|
|
def test_endpoint_with_no_apikey(self):
|
|
resp = requests.delete(
|
|
self.URL, verify=False, headers=self.NO_AUTH_HEADERS,
|
|
json={}
|
|
)
|
|
self.assertEqual(resp.status_code, 401)
|
|
|
|
def test_endpoint_with_missing_node_parameter(self):
|
|
resp = requests.delete(
|
|
self.URL, verify=False, headers=self.HEADERS,
|
|
json={}
|
|
)
|
|
error = resp.json()['error']
|
|
self.assertEqual(resp.status_code, 422)
|
|
self.assertEqual(error, 'missing node argument')
|
|
|
|
def test_add_node_and_remove(self):
|
|
payload = {'node': socket.gethostname()}
|
|
resp = requests.post(
|
|
ClusterAddNodeTestCase.URL, verify=False, headers=self.HEADERS,
|
|
json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
resp = requests.delete(
|
|
self.URL, verify=False, headers=self.HEADERS, json=payload
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|