You've already forked mariadb-columnstore-engine
							
							
				mirror of
				https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
				synced 2025-10-31 18:30:33 +03:00 
			
		
		
		
	* feat(cmapi): add read_only param for API add node endpoint * style(cmapi): fixes for string length and quotes Add dbroots of other nodes to the read-only node On every node change adjust dbroots in the read-only nodes Fix logging (trace level) in tests Remove ExeMgr from constants Fix tests Manually remove read-only node from ReadOnlyNodes on node removal (because nodes are only deactivated) Review fixes (mostly switching to StrEnum analog before py3.11, also changes in ruff config) Read-only nodes are now called read replica consistently Don't write hostname into IP fields of the config like PMSx/IPAddr, pmx_WriteEngineServer/IPAddr We calculate ReadReplicas by finding PMs without WriteEngineServer In _replace_localhost, replace local IP addrs with resolved IP addrs and local hostnames -- with the resolved hostnames. ModuleHostName/ModuleIPAddr is kept intact. Keep only IPv4 in ActiveNodes/DesiredNodes/InactiveNodes feat: add mock DNS resolution builder for testing hostname/IP mappings * Fix _add_node_to_PMS: if node is already in PMS, save it to existing items to not miss it during the reconstruction of the list * Make tests independent from CWD Fixed for _add_Module_entries Fixed node removal and tests Fixes for node manipulation tests
		
			
				
	
	
		
			293 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Cluster typer application.
 | |
| 
 | |
| Formally this module contains all subcommands for "mcs cluster" cli command.
 | |
| """
 | |
| import logging
 | |
| import time
 | |
| from datetime import datetime, timedelta
 | |
| from typing import List, Optional
 | |
| 
 | |
| import requests
 | |
| import typer
 | |
| from typing_extensions import Annotated
 | |
| 
 | |
| from cmapi_server.constants import (
 | |
|     CMAPI_CONF_PATH, DEFAULT_MCS_CONF_PATH, REQUEST_TIMEOUT
 | |
| )
 | |
| from cmapi_server.controllers.api_clients import ClusterControllerClient
 | |
| from cmapi_server.exceptions import CMAPIBasicError
 | |
| from cmapi_server.helpers import (
 | |
|     get_config_parser, get_current_key, get_version, build_url
 | |
| )
 | |
| from cmapi_server.managers.transaction import TransactionManager
 | |
| from mcs_cluster_tool.decorators import handle_output
 | |
| from mcs_node_control.models.node_config import NodeConfig
 | |
| 
 | |
| 
 | |
| logger = logging.getLogger('mcs_cli')
 | |
| app = typer.Typer(
 | |
|     help='MariaDB Columnstore cluster management command line tool.'
 | |
| )
 | |
| node_app = typer.Typer(help='Cluster nodes management.')
 | |
| app.add_typer(node_app, name='node')
 | |
| set_app = typer.Typer(help='Set cluster parameters.')
 | |
| app.add_typer(set_app, name='set')
 | |
| client = ClusterControllerClient()
 | |
| 
 | |
| 
 | |
| @app.command(rich_help_panel='cluster and single node commands')
 | |
| @handle_output
 | |
| def status():
 | |
|     """Get status information."""
 | |
|     client.request_timeout = REQUEST_TIMEOUT
 | |
|     return client.get_status()
 | |
| 
 | |
| 
 | |
| @app.command(rich_help_panel='cluster and single node commands')
 | |
| @handle_output
 | |
| @TransactionManager(
 | |
|     timeout=timedelta(days=1).total_seconds(), handle_signals=True
 | |
| )
 | |
| def stop(
 | |
|     interactive: Annotated[
 | |
|         bool,
 | |
|         typer.Option(
 | |
|             '--interactive/--no-interactive', '-i/-no-i',
 | |
|             help=(
 | |
|                 'Use this option on active cluster as interactive stop '
 | |
|                 'waits for current writes to complete in DMLProc before '
 | |
|                 'shutting down. Ensuring consistency, preventing data loss '
 | |
|                 'of active writes.'
 | |
|             ),
 | |
|         )
 | |
|     ] = False,
 | |
|     timeout: Annotated[
 | |
|         int,
 | |
|         typer.Option(
 | |
|             '-t', '--timeout',
 | |
|             help=(
 | |
|                 'Time in seconds to wait for DMLproc to gracefully stop.'
 | |
|                 'Warning: Low wait timeout values could result in data loss '
 | |
|                 'if the cluster is very active.'
 | |
|                 'In interactive mode means delay time between promts.'
 | |
|             )
 | |
|         )
 | |
|     ] = 15,
 | |
|     force: Annotated[
 | |
|         bool,
 | |
|         typer.Option(
 | |
|             '--force/--no-force', '-f/-no-f',
 | |
|             help=(
 | |
|                 'Force stops Columnstore.'
 | |
|                 'Warning: This could cause data corruption and/or data loss.'
 | |
|             ),
 | |
|             #TODO: hide from help till not investigated in decreased timeout
 | |
|             #      affect
 | |
|             hidden=True
 | |
|         )
 | |
|     ] = False
 | |
| ):
 | |
|     """Stop the Columnstore cluster."""
 | |
| 
 | |
|     start_time = str(datetime.now())
 | |
|     if interactive:
 | |
|         # TODO: for standalone cli tool need to change primary detection
 | |
|         #       method. Partially move logic below to ClusterController
 | |
|         nc = NodeConfig()
 | |
|         root = nc.get_current_config_root(
 | |
|             config_filename=DEFAULT_MCS_CONF_PATH
 | |
|         )
 | |
|         primary_node = root.find("./PrimaryNode").text
 | |
|         cfg_parser = get_config_parser(CMAPI_CONF_PATH)
 | |
|         api_key = get_current_key(cfg_parser)
 | |
|         version = get_version()
 | |
| 
 | |
|         headers = {'x-api-key': api_key}
 | |
|         body = {'force': False, 'timeout': timeout}
 | |
|         url = f'https://{primary_node}:8640/cmapi/{version}/node/stop_dmlproc'
 | |
|         try:
 | |
|             resp = requests.put(
 | |
|                 url, verify=False, headers=headers, json=body,
 | |
|                 timeout=timeout+1
 | |
|             )
 | |
|             resp.raise_for_status()
 | |
|         except Exception as err:
 | |
|             raise CMAPIBasicError(
 | |
|                 f'Error while stopping DMLProc on primary node.'
 | |
|             ) from err
 | |
| 
 | |
|         force = True
 | |
|         while True:
 | |
|             time.sleep(timeout)
 | |
|             url = build_url(
 | |
|                 base_url=primary_node, port=8640,
 | |
|                 query_params={'process_name': 'DMLProc'},
 | |
|                 path=f'cmapi/{version}/node/is_process_running',
 | |
|             )
 | |
|             try:
 | |
|                 resp = requests.get(
 | |
|                     url, verify=False, headers=headers, timeout=timeout
 | |
|                 )
 | |
|                 resp.raise_for_status()
 | |
|             except Exception as err:
 | |
|                 raise CMAPIBasicError(
 | |
|                     f'Error while getting mcs DMLProc status.'
 | |
|                 ) from err
 | |
| 
 | |
|             # check DMLPRoc state
 | |
|             # if ended, show message and break
 | |
|             dmlproc_running = resp.json()['running']
 | |
|             if not dmlproc_running:
 | |
|                 logging.info(
 | |
|                     'DMLProc stopped gracefully. '
 | |
|                     'Continue stopping other processes.'
 | |
|                 )
 | |
|                 break
 | |
|             else:
 | |
|                 force = typer.confirm(
 | |
|                     'DMLProc is still running. '
 | |
|                     'Do you want to force stop? '
 | |
|                     'WARNING: Could cause data loss and/or broken cluster.',
 | |
|                     prompt_suffix=' '
 | |
|                 )
 | |
|                 if force:
 | |
|                     break
 | |
|                 else:
 | |
|                     continue
 | |
|     if force:
 | |
|         # TODO: investigate more on how changing the hardcoded timeout
 | |
|         #       could affect put_config (helpers.py broadcast_config) operation
 | |
|         timeout = 0
 | |
| 
 | |
|     resp = client.shutdown_cluster({'in_transaction': True})
 | |
|     return {'timestamp': start_time}
 | |
| 
 | |
| 
 | |
| @app.command(rich_help_panel='cluster and single node commands')
 | |
| @handle_output
 | |
| @TransactionManager(
 | |
|     timeout=timedelta(days=1).total_seconds(), handle_signals=True
 | |
| )
 | |
| def start():
 | |
|     """Start the Columnstore cluster."""
 | |
|     return client.start_cluster({'in_transaction': True})
 | |
| 
 | |
| 
 | |
| @app.command(rich_help_panel='cluster and single node commands')
 | |
| @handle_output
 | |
| @TransactionManager(
 | |
|     timeout=timedelta(days=1).total_seconds(), handle_signals=True
 | |
| )
 | |
| def restart():
 | |
|     """Restart the Columnstore cluster."""
 | |
|     stop_result = client.shutdown_cluster({'in_transaction': True})
 | |
|     if 'error' in stop_result:
 | |
|         return stop_result
 | |
|     result = client.start_cluster({'in_transaction': True})
 | |
|     result['stop_timestamp'] = stop_result['timestamp']
 | |
|     return result
 | |
| 
 | |
| 
 | |
| @node_app.command(rich_help_panel='cluster node commands')
 | |
| @handle_output
 | |
| def add(
 | |
|     nodes: Optional[List[str]] = typer.Option(
 | |
|         ...,
 | |
|         '--node',  # command line argument name
 | |
|         help=(
 | |
|             'node IP, name or FQDN. '
 | |
|             'Can be used multiple times to add several nodes at a time.'
 | |
|         )
 | |
|     ),
 | |
|     read_replica: bool = typer.Option(
 | |
|         False,
 | |
|         '--read-replica',
 | |
|         help=(
 | |
|             'Add node (or nodes, if more than one is passed) as read replicas.'
 | |
|         )
 | |
|     )
 | |
| ):
 | |
|     """Add nodes to the Columnstore cluster."""
 | |
|     result = []
 | |
|     with TransactionManager(
 | |
|         timeout=timedelta(days=1).total_seconds(), handle_signals=True,
 | |
|         extra_nodes=nodes
 | |
|     ):
 | |
|         for node in nodes:
 | |
|             result.append(
 | |
|                 client.add_node({'node': node, 'read_replica': read_replica})
 | |
|             )
 | |
|     return result
 | |
| 
 | |
| 
 | |
| @node_app.command(rich_help_panel='cluster node commands')
 | |
| @handle_output
 | |
| def remove(nodes: Optional[List[str]] = typer.Option(
 | |
|         ...,
 | |
|         '--node',  # command line argument name
 | |
|         help=(
 | |
|             'node IP, name or FQDN. '
 | |
|             'Can be used multiple times to remove several nodes at a time.'
 | |
|         )
 | |
|     )
 | |
| ):
 | |
|     """Remove nodes from the Columnstore cluster."""
 | |
|     result = []
 | |
|     with TransactionManager(
 | |
|         timeout=timedelta(days=1).total_seconds(), handle_signals=True,
 | |
|         remove_nodes=nodes
 | |
|     ):
 | |
|         for node in nodes:
 | |
|             result.append(client.remove_node(node))
 | |
|     return result
 | |
| 
 | |
| 
 | |
| @set_app.command()
 | |
| @handle_output
 | |
| @TransactionManager(
 | |
|     timeout=timedelta(days=1).total_seconds(), handle_signals=True
 | |
| )
 | |
| def mode(cluster_mode: str = typer.Option(
 | |
|         ...,
 | |
|         '--mode',
 | |
|         help=(
 | |
|             'cluster mode to set. '
 | |
|             '"readonly" or "readwrite" are the only acceptable values.'
 | |
|         )
 | |
|     )
 | |
| ):
 | |
|     """Set Columnstore cluster mode."""
 | |
|     if cluster_mode not in ('readonly', 'readwrite'):
 | |
|         raise typer.BadParameter(
 | |
|             '"readonly" or "readwrite" are the only acceptable modes now.'
 | |
|         )
 | |
|     client.request_timeout = REQUEST_TIMEOUT
 | |
|     return client.set_mode(cluster_mode)
 | |
| 
 | |
| 
 | |
| @set_app.command()
 | |
| @handle_output
 | |
| def api_key(key: str = typer.Option(..., help='API key to set.')):
 | |
|     """Set API key for communication with cluster nodes via API.
 | |
| 
 | |
|     WARNING: this command will affect API key value on all cluster nodes.
 | |
|     """
 | |
|     if not key:
 | |
|         raise typer.BadParameter('Empty API key not allowed.')
 | |
|     client.request_timeout = REQUEST_TIMEOUT
 | |
|     return client.set_api_key(key)
 | |
| 
 | |
| 
 | |
| #TODO: remove in next releases
 | |
| @set_app.command()
 | |
| @handle_output
 | |
| def log_level(level: str = typer.Option(..., help='Logging level to set.')):
 | |
|     """Set logging level on all cluster nodes for develop purposes.
 | |
| 
 | |
|     WARNING: this could dramatically affect the number of log lines.
 | |
|     """
 | |
|     if not level:
 | |
|         raise typer.BadParameter('Empty log level not allowed.')
 | |
|     client.request_timeout = REQUEST_TIMEOUT
 | |
|     return client.set_log_level(level)
 |