starrocks-br 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/PKG-INFO +1 -1
  2. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/README.md +190 -13
  3. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/pyproject.toml +1 -1
  4. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/cli.py +104 -8
  5. starrocks_br-0.2.0/src/starrocks_br/config.py +64 -0
  6. starrocks_br-0.2.0/src/starrocks_br/db.py +154 -0
  7. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/executor.py +62 -12
  8. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/planner.py +32 -12
  9. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/restore.py +64 -12
  10. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/schema.py +1 -1
  11. starrocks_br-0.2.0/src/starrocks_br/timezone.py +125 -0
  12. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/PKG-INFO +1 -1
  13. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/SOURCES.txt +3 -1
  14. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_cli.py +173 -0
  15. starrocks_br-0.2.0/tests/test_db.py +257 -0
  16. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_executor.py +131 -30
  17. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_planner.py +11 -0
  18. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_restore.py +146 -2
  19. starrocks_br-0.2.0/tests/test_timezone.py +395 -0
  20. starrocks_br-0.1.0/src/starrocks_br/config.py +0 -41
  21. starrocks_br-0.1.0/src/starrocks_br/db.py +0 -88
  22. starrocks_br-0.1.0/tests/test_db.py +0 -69
  23. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/setup.cfg +0 -0
  24. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/__init__.py +0 -0
  25. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/concurrency.py +0 -0
  26. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/health.py +0 -0
  27. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/history.py +0 -0
  28. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/labels.py +0 -0
  29. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/logger.py +0 -0
  30. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br/repository.py +0 -0
  31. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/dependency_links.txt +0 -0
  32. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/entry_points.txt +0 -0
  33. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/requires.txt +0 -0
  34. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/src/starrocks_br.egg-info/top_level.txt +0 -0
  35. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_concurrency.py +0 -0
  36. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_config.py +0 -0
  37. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_health_checks.py +0 -0
  38. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_history.py +0 -0
  39. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_labels.py +0 -0
  40. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_logger.py +0 -0
  41. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_repository_sql.py +0 -0
  42. {starrocks_br-0.1.0 → starrocks_br-0.2.0}/tests/test_schema_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starrocks-br
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: StarRocks Backup and Restore automation tool
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: click<9,>=8.1.7
@@ -4,13 +4,83 @@
4
4
 
5
5
  The StarRocks Backup & Restore tool provides production-grade automation for backup and restore operations.
6
6
 
7
+ **Important:** This tool requires StarRocks 3.5 or later. Earlier versions are not supported due to differences in the `SHOW FRONTENDS` and `SHOW BACKENDS` command output formats, which are used for cluster health checks.
8
+
9
+ ## Summary
10
+
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Configuration](#configuration)
14
+ - [Password Management](#password-management)
15
+ - [Connecting with TLS/SSL](#connecting-with-tlsssl)
16
+ - [Commands](#commands)
17
+ - [Example Usage Scenarios](#example-usage-scenarios)
18
+ - [Error Handling](#error-handling)
19
+ - [Monitoring](#monitoring)
20
+ - [Testing](#testing)
21
+ - [Project Status](#project-status)
22
+
7
23
  ## Installation
8
24
 
9
- ### Option 1: Using Devbox (Recommended for Development)
25
+ ### Option 1: Install from PyPI (Recommended for Production)
26
+
27
+ We recommend using a virtual environment to ensure proper script availability and dependency isolation:
28
+
29
+ ```bash
30
+ # Create and activate a virtual environment
31
+ python3 -m venv .venv
32
+ source .venv/bin/activate # On Linux/Mac
33
+ # .venv\Scripts\activate # On Windows
34
+
35
+ # Install the package from PyPI
36
+ pip install starrocks-br==0.1.0
37
+
38
+ # Verify the installation
39
+ starrocks-br --help
40
+ ```
41
+
42
+ **Note:** Always activate the virtual environment before using the tool. The `starrocks-br` command will only be available when the virtual environment is activated.
43
+
44
+ ### Option 2: Standalone Executable (Alternative if PyPI Installation Fails)
45
+
46
+ If you encounter problems when installing via PyPI (often due to `mysql-connector-python` native extension issues), you can build a standalone executable that bundles all dependencies.
47
+
48
+ **Note:** This requires cloning the repository first.
49
+
50
+ ```bash
51
+ # Clone the repository
52
+ git clone https://github.com/deep-bi/starrocks-br
53
+ cd starrocks-br
54
+
55
+ # Create and activate virtual environment
56
+ python3 -m venv .venv
57
+ source .venv/bin/activate # On Linux/Mac
58
+ # .venv\Scripts\activate # On Windows
59
+
60
+ # Install dependencies
61
+ pip install -e .
62
+
63
+ # Build the standalone executable
64
+ ./build_executable.sh
65
+
66
+ # The executable will be created in: dist/starrocks-br
67
+ # You can now distribute or use this executable directly
68
+ ./dist/starrocks-br --help
69
+ ```
70
+
71
+ **Note:** The executable is platform-specific. Build it on the same OS/architecture where you'll use it, or build it on the target machine.
72
+
73
+ ### Option 3: Using Devbox (Recommended for Development)
74
+
75
+ **Note:** This requires cloning the repository first.
10
76
 
11
77
  Devbox provides a reproducible development environment with all required tools.
12
78
 
13
79
  ```bash
80
+ # Clone the repository
81
+ git clone https://github.com/deep-bi/starrocks-br
82
+ cd starrocks-br
83
+
14
84
  # Install devbox (if not already installed)
15
85
  curl -fsSL https://get.jetpack.io/devbox | bash
16
86
 
@@ -26,18 +96,67 @@ starrocks-br --help
26
96
  pytest
27
97
  ```
28
98
 
29
- ### Option 2: Manual Setup
99
+ ### Option 4: Manual Development Setup
30
100
 
31
101
  ```bash
32
- # Activate virtual environment
102
+ # Clone the repository
103
+ git clone https://github.com/deep-bi/starrocks-br
104
+ cd starrocks-br
105
+
106
+ # Create and activate virtual environment
107
+ python3 -m venv .venv
33
108
  source .venv/bin/activate
34
109
 
35
- # The CLI is already installed as: starrocks-br
110
+ # Install in editable mode with development dependencies
111
+ pip install -e ".[dev]"
112
+
113
+ # The CLI is now available as: starrocks-br
36
114
  ```
37
115
 
116
+ ## Quick Start
117
+
118
+ After installing from PyPI, follow these steps:
119
+
120
+ 1. **Activate your virtual environment** (if not already active):
121
+ ```bash
122
+ source .venv/bin/activate # On Linux/Mac
123
+ # .venv\Scripts\activate # On Windows
124
+ ```
125
+
126
+ 2. **Verify installation:**
127
+ ```bash
128
+ starrocks-br --help
129
+ ```
130
+
131
+ 3. **Create your `config.yaml` file** (see Configuration section below)
132
+
133
+ 4. **Set your password as an environment variable:**
134
+ ```bash
135
+ export STARROCKS_PASSWORD="your_password"
136
+ ```
137
+
138
+ On Windows (PowerShell):
139
+ ```powershell
140
+ $env:STARROCKS_PASSWORD="your_password"
141
+ ```
142
+
143
+ On Windows (Command Prompt):
144
+ ```cmd
145
+ set STARROCKS_PASSWORD=your_password
146
+ ```
147
+
148
+ 5. **Initialize the ops schema:**
149
+ ```bash
150
+ starrocks-br init --config config.yaml
151
+ ```
152
+
153
+ 6. **Start using the tool** - see Commands section below for details
154
+
38
155
  ## Configuration
39
156
 
40
- Create a `config.yaml` file with your StarRocks connection details:
157
+ **Important:** After installing the package, you need to create your own `config.yaml` file. This file is **not included in the package** - each user creates it with their own StarRocks connection details. You can place it anywhere and reference it using the `--config` parameter.
158
+
159
+ Create a `config.yaml` file in your working directory (or any location you prefer) with your StarRocks connection details:
41
160
 
42
161
  ```yaml
43
162
  host: "127.0.0.1"
@@ -55,6 +174,53 @@ The database password must be provided via the `STARROCKS_PASSWORD` environment
55
174
  export STARROCKS_PASSWORD="your_password"
56
175
  ```
57
176
 
177
+ ### Connecting with TLS/SSL
178
+
179
+ The tool can make secure connections to StarRocks using TLS. Add an optional `tls` section to your `config.yaml` when you need encryption.
180
+
181
+ #### Scenario 1: Server Authentication (Most Common)
182
+
183
+ Use this setup when the client only needs to verify the StarRocks server certificate.
184
+
185
+ ```yaml
186
+ host: "127.0.0.1"
187
+ port: 9030
188
+ user: "root"
189
+ database: "your_database"
190
+ repository: "your_repo_name"
191
+
192
+ tls:
193
+ enabled: true
194
+ ca_cert: "/path/to/ca.pem"
195
+ ```
196
+
197
+ - `enabled`: Turns TLS on or off.
198
+ - `ca_cert`: Certificate Authority file used to validate the server certificate.
199
+ - `verify_server_cert` (optional, default `true`): Disable only if you need to skip certificate validation.
200
+
201
+ #### Scenario 2: Mutual TLS (mTLS)
202
+
203
+ Use this when both the client and server must present certificates.
204
+
205
+ ```yaml
206
+ host: "127.0.0.1"
207
+ port: 9030
208
+ user: "root"
209
+ database: "your_database"
210
+ repository: "your_repo_name"
211
+
212
+ tls:
213
+ enabled: true
214
+ ca_cert: "/path/to/ca.pem"
215
+ client_cert: "/path/to/client-cert.pem"
216
+ client_key: "/path/to/client-key.pem"
217
+ ```
218
+
219
+ - `client_cert`: Client certificate presented to the server.
220
+ - `client_key`: Private key paired with the client certificate.
221
+
222
+ Regardless of the scenario, the connection defaults to modern TLS versions (`TLSv1.2`, `TLSv1.3`). Provide a `tls_versions` list if you need different protocol settings.
223
+
58
224
  **Note:** The repository must be created in StarRocks using the `CREATE REPOSITORY` command before running backups. For example:
59
225
 
60
226
  ```sql
@@ -113,7 +279,7 @@ starrocks-br backup full --config config.yaml --group <group_name>
113
279
  **Parameters:**
114
280
  - `--group`: The inventory group to back up.
115
281
 
116
- **Flow:**
282
+ **Internal flow:**
117
283
  1. Load config → verify cluster health → ensure repository exists
118
284
  2. Reserve job slot (prevent concurrent backups)
119
285
  3. Query `ops.table_inventory` for all tables in the specified group.
@@ -133,7 +299,7 @@ starrocks-br backup incremental --config config.yaml --group <group_name>
133
299
  - `--group`: The inventory group to back up.
134
300
  - `--baseline-backup` (Optional): Specify a backup label to use as the baseline instead of the latest full backup.
135
301
 
136
- **Flow:**
302
+ **Internal flow:**
137
303
  1. Load config → verify cluster health → ensure repository exists
138
304
  2. Reserve job slot
139
305
  3. Find the latest successful full backup for the group to use as a baseline.
@@ -159,7 +325,8 @@ starrocks-br restore \
159
325
  **Parameters:**
160
326
  - `--config`: Path to config YAML file (required)
161
327
  - `--target-label`: Backup label to restore to (required)
162
- - `--group`: Optional inventory group to filter tables to restore
328
+ - `--group`: Optional inventory group to filter tables to restore (cannot be used with `--table`)
329
+ - `--table`: Optional table name to restore (table name only, database comes from config). Cannot be used with `--group`
163
330
  - `--rename-suffix`: Suffix for temporary tables during restore (default: `_restored`)
164
331
 
165
332
  **How it works:**
@@ -167,14 +334,18 @@ starrocks-br restore \
167
334
  - **For incremental backups**: Automatically restores the base full backup first, then applies the incremental
168
335
  - **Safety mechanism**: Uses temporary tables with the specified suffix, then performs atomic rename to make restored data live
169
336
 
170
- **Two Restore Modes:**
171
- - **Disaster Recovery**: Restore all tables from a backup (omit `--group` parameter)
172
- - **Surgical Restore**: Restore only specific table groups (use `--group` parameter)
337
+ **Three Restore Modes:**
338
+ - **Disaster Recovery**: Restore all tables from a backup (omit both `--group` and `--table` parameters)
339
+ - **Surgical Restore by Group**: Restore only specific table groups (use `--group` parameter)
340
+ - **Single Table Restore**: Restore a specific table (use `--table` parameter). The table name should not include the database prefix - the database comes from the config file.
341
+
342
+ **Table Name Format:**
343
+ When using `--table`, provide only the table name (e.g., `fact_sales`), not `database.table_name`. The database is taken from the `database` field in your config file. For multiple tables, set up an inventory group and use `--group` instead.
173
344
 
174
345
  **Purpose of `--rename-suffix`:**
175
346
  The restore process creates temporary tables with the specified suffix (e.g., `table_restored`) to avoid conflicts with existing tables. Once the restore is complete and verified, the tool performs atomic renames to swap the original tables with the restored data. This ensures data safety and allows for rollback if needed.
176
347
 
177
- **Flow:**
348
+ **Internal flow:**
178
349
  1. Load config → verify cluster health → ensure repository exists
179
350
  2. Find the correct restore sequence (full backup + optional incremental)
180
351
  3. Get tables from backup manifest (optionally filtered by group)
@@ -224,6 +395,12 @@ starrocks-br restore \
224
395
  starrocks-br restore \
225
396
  --config config.yaml \
226
397
  --target-label sales_db_20251014_full
398
+
399
+ # Restore a single table from a backup
400
+ starrocks-br restore \
401
+ --config config.yaml \
402
+ --target-label sales_db_20251015_inc \
403
+ --table fact_sales
227
404
  ```
228
405
 
229
406
  ## Error Handling
@@ -301,4 +478,4 @@ pytest tests/test_cli.py -v
301
478
  - Exponential backoff retry for job conflicts
302
479
  - Disk space precheck (requires external monitoring)
303
480
  - Formal runbooks and DR drill procedures
304
- - Monitoring dashboards and alerting integration
481
+ - Monitoring dashboards and alerting integration
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "starrocks-br"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "StarRocks Backup and Restore automation tool"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -15,6 +15,40 @@ from . import schema
15
15
  from . import logger
16
16
 
17
17
 
18
+ def _handle_snapshot_exists_error(error_details: dict, label: str, config: str, repository: str, backup_type: str, group: str, baseline_backup: str = None) -> None:
19
+ """Handle snapshot_exists error by providing helpful guidance to the user.
20
+
21
+ Args:
22
+ error_details: Error details dict containing error_type and snapshot_name
23
+ label: The backup label that was generated
24
+ config: Path to config file
25
+ repository: Repository name
26
+ backup_type: Type of backup ('incremental' or 'full')
27
+ group: Inventory group name
28
+ baseline_backup: Optional baseline backup label (for incremental backups)
29
+ """
30
+ snapshot_name = error_details.get('snapshot_name', label)
31
+ logger.error(f"Snapshot '{snapshot_name}' already exists in the repository.")
32
+ logger.info("")
33
+ logger.info("This typically happens when:")
34
+ logger.info(" • The CLI lost connectivity during a previous backup operation")
35
+ logger.info(" • The backup completed on the server, but backup_history wasn't updated")
36
+ logger.info("")
37
+ logger.info("To resolve this, retry the backup with a custom label using --name:")
38
+
39
+ if backup_type == 'incremental':
40
+ retry_cmd = f" starrocks-br backup incremental --config {config} --group {group} --name {snapshot_name}_retry"
41
+ if baseline_backup:
42
+ retry_cmd += f" --baseline-backup {baseline_backup}"
43
+ logger.info(retry_cmd)
44
+ else:
45
+ logger.info(f" starrocks-br backup full --config {config} --group {group} --name {snapshot_name}_retry")
46
+
47
+ logger.info("")
48
+ logger.tip("You can verify the existing backup by checking the repository or running:")
49
+ logger.tip(f" SHOW SNAPSHOT ON {repository} WHERE Snapshot = '{snapshot_name}'")
50
+
51
+
18
52
  @click.group()
19
53
  def cli():
20
54
  """StarRocks Backup & Restore automation tool."""
@@ -43,7 +77,8 @@ def init(config):
43
77
  port=cfg['port'],
44
78
  user=cfg['user'],
45
79
  password=os.getenv('STARROCKS_PASSWORD'),
46
- database=cfg['database']
80
+ database=cfg['database'],
81
+ tls_config=cfg.get('tls'),
47
82
  )
48
83
 
49
84
  with database:
@@ -102,7 +137,8 @@ def backup_incremental(config, baseline_backup, group, name):
102
137
  port=cfg['port'],
103
138
  user=cfg['user'],
104
139
  password=os.getenv('STARROCKS_PASSWORD'),
105
- database=cfg['database']
140
+ database=cfg['database'],
141
+ tls_config=cfg.get('tls'),
106
142
  )
107
143
 
108
144
  with database:
@@ -174,6 +210,13 @@ def backup_incremental(config, baseline_backup, group, name):
174
210
  logger.success(f"Backup completed successfully: {result['final_status']['state']}")
175
211
  sys.exit(0)
176
212
  else:
213
+ error_details = result.get('error_details')
214
+ if error_details and error_details.get('error_type') == 'snapshot_exists':
215
+ _handle_snapshot_exists_error(
216
+ error_details, label, config, cfg['repository'], 'incremental', group, baseline_backup
217
+ )
218
+ sys.exit(1)
219
+
177
220
  state = result.get('final_status', {}).get('state', 'UNKNOWN')
178
221
  if state == "LOST":
179
222
  logger.critical("Backup tracking lost!")
@@ -215,7 +258,8 @@ def backup_full(config, group, name):
215
258
  port=cfg['port'],
216
259
  user=cfg['user'],
217
260
  password=os.getenv('STARROCKS_PASSWORD'),
218
- database=cfg['database']
261
+ database=cfg['database'],
262
+ tls_config=cfg.get('tls'),
219
263
  )
220
264
 
221
265
  with database:
@@ -274,6 +318,13 @@ def backup_full(config, group, name):
274
318
  logger.success(f"Backup completed successfully: {result['final_status']['state']}")
275
319
  sys.exit(0)
276
320
  else:
321
+ error_details = result.get('error_details')
322
+ if error_details and error_details.get('error_type') == 'snapshot_exists':
323
+ _handle_snapshot_exists_error(
324
+ error_details, label, config, cfg['repository'], 'full', group
325
+ )
326
+ sys.exit(1)
327
+
277
328
  state = result.get('final_status', {}).get('state', 'UNKNOWN')
278
329
  if state == "LOST":
279
330
  logger.critical("Backup tracking lost!")
@@ -300,8 +351,10 @@ def backup_full(config, group, name):
300
351
  @click.option('--config', required=True, help='Path to config YAML file')
301
352
  @click.option('--target-label', required=True, help='Backup label to restore to')
302
353
  @click.option('--group', help='Optional inventory group to filter tables to restore')
354
+ @click.option('--table', help='Optional table name to restore (table name only, database comes from config). Cannot be used with --group.')
303
355
  @click.option('--rename-suffix', default='_restored', help='Suffix for temporary tables during restore (default: _restored)')
304
- def restore_command(config, target_label, group, rename_suffix):
356
+ @click.option('--yes', is_flag=True, help='Skip confirmation prompt and proceed automatically')
357
+ def restore_command(config, target_label, group, table, rename_suffix, yes):
305
358
  """Restore data to a specific point in time using intelligent backup chain resolution.
306
359
 
307
360
  This command automatically determines the correct sequence of backups needed for restore:
@@ -311,9 +364,23 @@ def restore_command(config, target_label, group, rename_suffix):
311
364
  The restore process uses temporary tables with the specified suffix for safety, then performs
312
365
  an atomic rename to make the restored data live.
313
366
 
314
- Flow: load config → find restore pair → get tables from backup → execute restore flow
367
+ Flow: load config → check health → ensure repository → find restore pair → get tables from backup → execute restore flow
315
368
  """
316
369
  try:
370
+ if group and table:
371
+ logger.error("Cannot specify both --group and --table. Use --table for single table restore or --group for inventory group restore.")
372
+ sys.exit(1)
373
+
374
+ if table:
375
+ table = table.strip()
376
+ if not table:
377
+ logger.error("Table name cannot be empty")
378
+ sys.exit(1)
379
+
380
+ if '.' in table:
381
+ logger.error("Table name must not include database prefix. Use 'table_name' not 'database.table_name'. Database comes from config file.")
382
+ sys.exit(1)
383
+
317
384
  cfg = config_module.load_config(config)
318
385
  config_module.validate_config(cfg)
319
386
 
@@ -322,7 +389,8 @@ def restore_command(config, target_label, group, rename_suffix):
322
389
  port=cfg['port'],
323
390
  user=cfg['user'],
324
391
  password=os.getenv('STARROCKS_PASSWORD'),
325
- database=cfg['database']
392
+ database=cfg['database'],
393
+ tls_config=cfg.get('tls'),
326
394
  )
327
395
 
328
396
  with database:
@@ -332,6 +400,17 @@ def restore_command(config, target_label, group, rename_suffix):
332
400
  logger.warning("Remember to populate ops.table_inventory with your backup groups!")
333
401
  sys.exit(1) # Exit if schema was just created, requires user action
334
402
 
403
+ healthy, message = health.check_cluster_health(database)
404
+ if not healthy:
405
+ logger.error(f"Cluster health check failed: {message}")
406
+ sys.exit(1)
407
+
408
+ logger.success(f"Cluster health: {message}")
409
+
410
+ repository.ensure_repository(database, cfg['repository'])
411
+
412
+ logger.success(f"Repository '{cfg['repository']}' verified")
413
+
335
414
  logger.info(f"Finding restore sequence for target backup: {target_label}")
336
415
 
337
416
  try:
@@ -342,11 +421,24 @@ def restore_command(config, target_label, group, rename_suffix):
342
421
  sys.exit(1)
343
422
 
344
423
  logger.info("Determining tables to restore from backup manifest...")
345
- tables_to_restore = restore.get_tables_from_backup(database, target_label, group)
424
+
425
+ try:
426
+ tables_to_restore = restore.get_tables_from_backup(
427
+ database,
428
+ target_label,
429
+ group=group,
430
+ table=table,
431
+ database=cfg['database'] if table else None
432
+ )
433
+ except ValueError as e:
434
+ logger.error(str(e))
435
+ sys.exit(1)
346
436
 
347
437
  if not tables_to_restore:
348
438
  if group:
349
439
  logger.warning(f"No tables found in backup '{target_label}' for group '{group}'")
440
+ elif table:
441
+ logger.warning(f"No tables found in backup '{target_label}' for table '{table}'")
350
442
  else:
351
443
  logger.warning(f"No tables found in backup '{target_label}'")
352
444
  sys.exit(1)
@@ -359,7 +451,8 @@ def restore_command(config, target_label, group, rename_suffix):
359
451
  cfg['repository'],
360
452
  restore_pair,
361
453
  tables_to_restore,
362
- rename_suffix
454
+ rename_suffix,
455
+ skip_confirmation=yes
363
456
  )
364
457
 
365
458
  if result['success']:
@@ -375,6 +468,9 @@ def restore_command(config, target_label, group, rename_suffix):
375
468
  except ValueError as e:
376
469
  logger.error(f"Configuration error: {e}")
377
470
  sys.exit(1)
471
+ except RuntimeError as e:
472
+ logger.error(f"{e}")
473
+ sys.exit(1)
378
474
  except Exception as e:
379
475
  logger.error(f"Unexpected error: {e}")
380
476
  sys.exit(1)
@@ -0,0 +1,64 @@
1
+ import yaml
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ def load_config(config_path: str) -> Dict[str, Any]:
6
+ """Load and parse YAML configuration file.
7
+
8
+ Args:
9
+ config_path: Path to the YAML config file
10
+
11
+ Returns:
12
+ Dictionary containing configuration
13
+
14
+ Raises:
15
+ FileNotFoundError: If config file doesn't exist
16
+ yaml.YAMLError: If config file is not valid YAML
17
+ """
18
+ with open(config_path, 'r') as f:
19
+ config = yaml.safe_load(f)
20
+
21
+ if not isinstance(config, dict):
22
+ raise ValueError("Config must be a dictionary")
23
+
24
+ return config
25
+
26
+
27
+ def validate_config(config: Dict[str, Any]) -> None:
28
+ """Validate that config contains required fields.
29
+
30
+ Args:
31
+ config: Configuration dictionary
32
+
33
+ Raises:
34
+ ValueError: If required fields are missing
35
+ """
36
+ required_fields = ['host', 'port', 'user', 'database', 'repository']
37
+
38
+ for field in required_fields:
39
+ if field not in config:
40
+ raise ValueError(f"Missing required config field: {field}")
41
+
42
+ _validate_tls_section(config.get('tls'))
43
+
44
+
45
+ def _validate_tls_section(tls_config) -> None:
46
+ if tls_config is None:
47
+ return
48
+
49
+ if not isinstance(tls_config, dict):
50
+ raise ValueError("TLS configuration must be a dictionary")
51
+
52
+ enabled = bool(tls_config.get('enabled', False))
53
+
54
+ if enabled and not tls_config.get('ca_cert'):
55
+ raise ValueError("TLS configuration requires 'ca_cert' when 'enabled' is true")
56
+
57
+ if 'verify_server_cert' in tls_config and not isinstance(tls_config['verify_server_cert'], bool):
58
+ raise ValueError("TLS configuration field 'verify_server_cert' must be a boolean if provided")
59
+
60
+ if 'tls_versions' in tls_config:
61
+ tls_versions = tls_config['tls_versions']
62
+ if not isinstance(tls_versions, list) or not all(isinstance(version, str) for version in tls_versions):
63
+ raise ValueError("TLS configuration field 'tls_versions' must be a list of strings if provided")
64
+