db-sync-tool-kmi 2.11.2__py3-none-any.whl → 3.0.2__py3-none-any.whl
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.
- db_sync_tool/__main__.py +7 -252
- db_sync_tool/cli.py +733 -0
- db_sync_tool/database/process.py +94 -111
- db_sync_tool/database/utility.py +339 -121
- db_sync_tool/info.py +1 -1
- db_sync_tool/recipes/drupal.py +87 -12
- db_sync_tool/recipes/laravel.py +7 -6
- db_sync_tool/recipes/parsing.py +102 -0
- db_sync_tool/recipes/symfony.py +17 -28
- db_sync_tool/recipes/typo3.py +33 -54
- db_sync_tool/recipes/wordpress.py +13 -12
- db_sync_tool/remote/client.py +206 -71
- db_sync_tool/remote/file_transfer.py +303 -0
- db_sync_tool/remote/rsync.py +18 -15
- db_sync_tool/remote/system.py +2 -3
- db_sync_tool/remote/transfer.py +51 -47
- db_sync_tool/remote/utility.py +35 -28
- db_sync_tool/sync.py +52 -28
- db_sync_tool/utility/config.py +367 -0
- db_sync_tool/utility/config_resolver.py +573 -0
- db_sync_tool/utility/console.py +779 -0
- db_sync_tool/utility/exceptions.py +32 -0
- db_sync_tool/utility/helper.py +155 -148
- db_sync_tool/utility/info.py +53 -20
- db_sync_tool/utility/log.py +55 -31
- db_sync_tool/utility/logging_config.py +410 -0
- db_sync_tool/utility/mode.py +85 -150
- db_sync_tool/utility/output.py +122 -51
- db_sync_tool/utility/parser.py +33 -53
- db_sync_tool/utility/pure.py +93 -0
- db_sync_tool/utility/security.py +79 -0
- db_sync_tool/utility/system.py +277 -194
- db_sync_tool/utility/validation.py +2 -9
- db_sync_tool_kmi-3.0.2.dist-info/METADATA +99 -0
- db_sync_tool_kmi-3.0.2.dist-info/RECORD +44 -0
- {db_sync_tool_kmi-2.11.2.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/WHEEL +1 -1
- db_sync_tool_kmi-2.11.2.dist-info/METADATA +0 -276
- db_sync_tool_kmi-2.11.2.dist-info/RECORD +0 -34
- {db_sync_tool_kmi-2.11.2.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/entry_points.txt +0 -0
- {db_sync_tool_kmi-2.11.2.dist-info → db_sync_tool_kmi-3.0.2.dist-info/licenses}/LICENSE +0 -0
- {db_sync_tool_kmi-2.11.2.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/top_level.txt +0 -0
db_sync_tool/utility/system.py
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
System module
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
import sys
|
|
9
7
|
import json
|
|
10
8
|
import os
|
|
11
9
|
import getpass
|
|
10
|
+
import secrets
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, TYPE_CHECKING
|
|
12
13
|
import yaml
|
|
13
14
|
from db_sync_tool.utility import log, parser, mode, helper, output, validation
|
|
15
|
+
from db_sync_tool.utility.console import get_output_manager
|
|
16
|
+
from db_sync_tool.utility.exceptions import ConfigError
|
|
14
17
|
from db_sync_tool.remote import utility as remote_utility
|
|
15
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from db_sync_tool.utility.config import SyncConfig
|
|
21
|
+
|
|
16
22
|
#
|
|
17
23
|
# GLOBALS
|
|
18
24
|
#
|
|
19
25
|
|
|
20
|
-
config = {
|
|
21
|
-
'verbose':
|
|
26
|
+
config: dict[str, Any] = {
|
|
27
|
+
'verbose': 0, # 0=compact, 1=verbose, 2=debug
|
|
22
28
|
'mute': False,
|
|
23
29
|
'dry_run': False,
|
|
24
30
|
'keep_dump': False,
|
|
@@ -32,9 +38,11 @@ config = {
|
|
|
32
38
|
'config_file_path': None,
|
|
33
39
|
'clear_database': False,
|
|
34
40
|
'force_password': False,
|
|
35
|
-
'use_rsync':
|
|
41
|
+
'use_rsync': True, # rsync is 5-10x faster than Paramiko SFTP
|
|
36
42
|
'use_rsync_options': None,
|
|
37
43
|
'use_sshpass': False,
|
|
44
|
+
'with_files': False, # Enable file synchronization (opt-in)
|
|
45
|
+
'files_only': False, # Sync only files, skip database
|
|
38
46
|
'ssh_agent': False,
|
|
39
47
|
'ssh_password': {
|
|
40
48
|
mode.Client.ORIGIN: None,
|
|
@@ -47,11 +55,94 @@ config = {
|
|
|
47
55
|
'additional_mysqldump_options': ''
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
# Typed configuration (single source of truth after migration)
|
|
59
|
+
_typed_config: 'SyncConfig | None' = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_typed_config() -> 'SyncConfig':
|
|
63
|
+
"""
|
|
64
|
+
Get current configuration as typed SyncConfig object.
|
|
65
|
+
|
|
66
|
+
:return: SyncConfig instance
|
|
67
|
+
"""
|
|
68
|
+
global _typed_config
|
|
69
|
+
if _typed_config is None:
|
|
70
|
+
from db_sync_tool.utility.config import SyncConfig
|
|
71
|
+
_typed_config = SyncConfig.from_dict(config)
|
|
72
|
+
return _typed_config
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def refresh_typed_config() -> None:
|
|
76
|
+
"""
|
|
77
|
+
Refresh typed config after dict changes.
|
|
78
|
+
|
|
79
|
+
Call this after modifying system.config to keep _typed_config in sync.
|
|
80
|
+
This is needed during the migration phase where both dict and dataclass
|
|
81
|
+
are used.
|
|
82
|
+
"""
|
|
83
|
+
global _typed_config
|
|
84
|
+
from db_sync_tool.utility.config import SyncConfig
|
|
85
|
+
_typed_config = SyncConfig.from_dict(config)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _set_config_value(key: str, value, client: str | None = None) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Set a configuration value and refresh typed config.
|
|
91
|
+
|
|
92
|
+
:param key: Configuration key
|
|
93
|
+
:param value: Value to set
|
|
94
|
+
:param client: Optional client identifier for nested config
|
|
95
|
+
"""
|
|
96
|
+
if client:
|
|
97
|
+
config.setdefault(client, {})[key] = value
|
|
98
|
+
else:
|
|
99
|
+
config[key] = value
|
|
100
|
+
refresh_typed_config()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def set_database_config(client: str, db_config: dict) -> None:
|
|
104
|
+
"""Set database configuration for a client."""
|
|
105
|
+
_set_config_value('db', db_config, client)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def set_framework_type(type_name: str) -> None:
|
|
109
|
+
"""Set the framework type."""
|
|
110
|
+
_set_config_value('type', type_name)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_is_same_client(value: bool) -> None:
|
|
114
|
+
"""Set is_same_client flag."""
|
|
115
|
+
_set_config_value('is_same_client', value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def set_use_sshpass(value: bool) -> None:
|
|
119
|
+
"""Set use_sshpass flag."""
|
|
120
|
+
_set_config_value('use_sshpass', value)
|
|
121
|
+
|
|
122
|
+
|
|
50
123
|
#
|
|
51
124
|
# DEFAULTS
|
|
52
125
|
#
|
|
53
126
|
|
|
54
|
-
|
|
127
|
+
# Generate a secure random suffix to prevent predictable temp paths
|
|
128
|
+
_temp_suffix = secrets.token_hex(8)
|
|
129
|
+
default_local_sync_path = f'/tmp/db_sync_tool_{_temp_suffix}/'
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def create_secure_temp_dir(path):
|
|
133
|
+
"""
|
|
134
|
+
Create a temporary directory with secure permissions (0700).
|
|
135
|
+
Prevents other users from accessing sensitive database dumps.
|
|
136
|
+
|
|
137
|
+
:param path: String path to create
|
|
138
|
+
:return: String path
|
|
139
|
+
"""
|
|
140
|
+
if not os.path.exists(path):
|
|
141
|
+
os.makedirs(path, mode=0o700)
|
|
142
|
+
else:
|
|
143
|
+
# Ensure secure permissions even if directory exists
|
|
144
|
+
os.chmod(path, 0o700)
|
|
145
|
+
return path
|
|
55
146
|
|
|
56
147
|
|
|
57
148
|
#
|
|
@@ -70,7 +161,7 @@ def get_configuration(host_config, args = {}):
|
|
|
70
161
|
"""
|
|
71
162
|
Checking configuration information by file or dictionary
|
|
72
163
|
:param host_config: Dictionary
|
|
73
|
-
:param args: Dictionary
|
|
164
|
+
:param args: Dictionary (or argparse.Namespace with resolved_config attribute)
|
|
74
165
|
:return:
|
|
75
166
|
"""
|
|
76
167
|
global config
|
|
@@ -92,13 +183,9 @@ def get_configuration(host_config, args = {}):
|
|
|
92
183
|
elif _config_file_path.endswith('.yaml') or _config_file_path.endswith('.yml'):
|
|
93
184
|
config.update(yaml.safe_load(read_file))
|
|
94
185
|
else:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
f'Unsupported configuration file type [json,yml,yaml]: '
|
|
99
|
-
f'{config["config_file_path"]}',
|
|
100
|
-
False
|
|
101
|
-
)
|
|
186
|
+
raise ConfigError(
|
|
187
|
+
f'Unsupported configuration file type [json,yml,yaml]: '
|
|
188
|
+
f'{config["config_file_path"]}'
|
|
102
189
|
)
|
|
103
190
|
output.message(
|
|
104
191
|
output.Subject.LOCAL,
|
|
@@ -107,14 +194,13 @@ def get_configuration(host_config, args = {}):
|
|
|
107
194
|
True
|
|
108
195
|
)
|
|
109
196
|
else:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
output.Subject.ERROR,
|
|
113
|
-
f'Local configuration not found: {config["config_file_path"]}',
|
|
114
|
-
False
|
|
115
|
-
)
|
|
197
|
+
raise ConfigError(
|
|
198
|
+
f'Local configuration not found: {config["config_file_path"]}'
|
|
116
199
|
)
|
|
117
200
|
|
|
201
|
+
# Apply resolved config from ConfigResolver (if present)
|
|
202
|
+
_apply_resolved_config(args)
|
|
203
|
+
|
|
118
204
|
# workaround for argument order handling respecting the linking feature
|
|
119
205
|
build_config(args, True)
|
|
120
206
|
link_configuration_with_hosts()
|
|
@@ -124,138 +210,137 @@ def get_configuration(host_config, args = {}):
|
|
|
124
210
|
check_options()
|
|
125
211
|
|
|
126
212
|
if not config[mode.Client.TARGET] and not config[mode.Client.ORIGIN]:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
output.Subject.ERROR,
|
|
130
|
-
f'Configuration is missing, use a separate file or provide host parameter',
|
|
131
|
-
False
|
|
132
|
-
)
|
|
213
|
+
raise ConfigError(
|
|
214
|
+
'Configuration is missing, use a separate file or provide host parameter'
|
|
133
215
|
)
|
|
216
|
+
|
|
217
|
+
# Refresh typed config after all configuration is loaded
|
|
218
|
+
refresh_typed_config()
|
|
219
|
+
|
|
134
220
|
helper.run_script(script='before')
|
|
135
221
|
log.get_logger().info('Starting db_sync_tool')
|
|
136
222
|
|
|
137
223
|
|
|
138
|
-
def
|
|
224
|
+
def _apply_resolved_config(args) -> None:
|
|
139
225
|
"""
|
|
140
|
-
|
|
141
|
-
:param args:
|
|
142
|
-
:param pre_run:
|
|
143
|
-
:return:
|
|
144
|
-
"""
|
|
145
|
-
if args is None or not args:
|
|
146
|
-
return {}
|
|
147
|
-
|
|
148
|
-
if not args.type is None:
|
|
149
|
-
config['type'] = args.type
|
|
150
|
-
|
|
151
|
-
if not args.tables is None:
|
|
152
|
-
config['tables'] = args.tables
|
|
153
|
-
|
|
154
|
-
if not args.origin is None:
|
|
155
|
-
config['link_origin'] = args.origin
|
|
156
|
-
|
|
157
|
-
if not args.target is None:
|
|
158
|
-
config['link_target'] = args.target
|
|
159
|
-
|
|
160
|
-
# for order reasons check just the link arguments
|
|
161
|
-
if pre_run: return
|
|
162
|
-
|
|
163
|
-
if not args.target_path is None:
|
|
164
|
-
config[mode.Client.TARGET]['path'] = args.target_path
|
|
165
|
-
|
|
166
|
-
if not args.target_name is None:
|
|
167
|
-
config[mode.Client.TARGET]['name'] = args.target_name
|
|
168
|
-
|
|
169
|
-
if not args.target_host is None:
|
|
170
|
-
config[mode.Client.TARGET]['host'] = args.target_host
|
|
171
|
-
|
|
172
|
-
if not args.target_user is None:
|
|
173
|
-
config[mode.Client.TARGET]['user'] = args.target_user
|
|
226
|
+
Apply resolved config from ConfigResolver to the global config.
|
|
174
227
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if not args.target_key is None:
|
|
179
|
-
config[mode.Client.TARGET]['ssh_key'] = args.target_key
|
|
180
|
-
|
|
181
|
-
if not args.target_port is None:
|
|
182
|
-
config[mode.Client.TARGET]['port'] = args.target_port
|
|
183
|
-
|
|
184
|
-
if not args.target_dump_dir is None:
|
|
185
|
-
config[mode.Client.TARGET]['dump_dir'] = args.target_dump_dir
|
|
186
|
-
|
|
187
|
-
if not args.target_db_name is None:
|
|
188
|
-
check_config_dict_key(mode.Client.TARGET, 'db')
|
|
189
|
-
config[mode.Client.TARGET]['db']['name'] = args.target_db_name
|
|
190
|
-
|
|
191
|
-
if not args.target_db_host is None:
|
|
192
|
-
check_config_dict_key(mode.Client.TARGET, 'db')
|
|
193
|
-
config[mode.Client.TARGET]['db']['host'] = args.target_db_host
|
|
194
|
-
|
|
195
|
-
if not args.target_db_user is None:
|
|
196
|
-
check_config_dict_key(mode.Client.TARGET, 'db')
|
|
197
|
-
config[mode.Client.TARGET]['db']['user'] = args.target_db_user
|
|
198
|
-
|
|
199
|
-
if not args.target_db_password is None:
|
|
200
|
-
check_config_dict_key(mode.Client.TARGET, 'db')
|
|
201
|
-
config[mode.Client.TARGET]['db']['password'] = args.target_db_password
|
|
202
|
-
|
|
203
|
-
if not args.target_db_port is None:
|
|
204
|
-
check_config_dict_key(mode.Client.TARGET, 'db')
|
|
205
|
-
config[mode.Client.TARGET]['db']['port'] = args.target_db_port
|
|
206
|
-
|
|
207
|
-
if not args.target_after_dump is None:
|
|
208
|
-
config[mode.Client.TARGET]['after_dump'] = args.target_after_dump
|
|
209
|
-
|
|
210
|
-
if not args.origin_path is None:
|
|
211
|
-
config[mode.Client.ORIGIN]['path'] = args.origin_path
|
|
212
|
-
|
|
213
|
-
if not args.origin_name is None:
|
|
214
|
-
config[mode.Client.ORIGIN]['name'] = args.origin_name
|
|
215
|
-
|
|
216
|
-
if not args.origin_host is None:
|
|
217
|
-
config[mode.Client.ORIGIN]['host'] = args.origin_host
|
|
218
|
-
|
|
219
|
-
if not args.origin_user is None:
|
|
220
|
-
config[mode.Client.ORIGIN]['user'] = args.origin_user
|
|
228
|
+
:param args: argparse.Namespace or dict that may contain resolved_config
|
|
229
|
+
"""
|
|
230
|
+
global config
|
|
221
231
|
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
# Get resolved_config from args (if present)
|
|
233
|
+
resolved_config = getattr(args, 'resolved_config', None)
|
|
234
|
+
if resolved_config is None:
|
|
235
|
+
return
|
|
224
236
|
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
# Apply merged config first (global defaults + project defaults)
|
|
238
|
+
if resolved_config.merged_config:
|
|
239
|
+
for key, value in resolved_config.merged_config.items():
|
|
240
|
+
if key not in ('origin', 'target'):
|
|
241
|
+
config[key] = value
|
|
227
242
|
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
# Apply origin config
|
|
244
|
+
if resolved_config.origin_config:
|
|
245
|
+
config[mode.Client.ORIGIN] = {**config[mode.Client.ORIGIN], **resolved_config.origin_config}
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
247
|
+
# Apply target config
|
|
248
|
+
if resolved_config.target_config:
|
|
249
|
+
config[mode.Client.TARGET] = {**config[mode.Client.TARGET], **resolved_config.target_config}
|
|
233
250
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
# Log the source
|
|
252
|
+
if resolved_config.source:
|
|
253
|
+
output.message(
|
|
254
|
+
output.Subject.INFO,
|
|
255
|
+
f'Configuration resolved from {resolved_config.source}',
|
|
256
|
+
True
|
|
257
|
+
)
|
|
237
258
|
|
|
238
|
-
if not args.origin_db_host is None:
|
|
239
|
-
check_config_dict_key(mode.Client.ORIGIN, 'db')
|
|
240
|
-
config[mode.Client.ORIGIN]['db']['host'] = args.origin_db_host
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
# Argument mapping: (arg_name, config_path)
|
|
261
|
+
# config_path is a tuple: (client, key) or (client, nested_key, key) or just (key,) for top-level
|
|
262
|
+
_ARG_MAPPINGS_PRE_RUN = [
|
|
263
|
+
('type', ('type',)),
|
|
264
|
+
('tables', ('tables',)),
|
|
265
|
+
('origin', ('link_origin',)),
|
|
266
|
+
('target', ('link_target',)),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
_ARG_MAPPINGS_MAIN = [
|
|
270
|
+
# Target client mappings
|
|
271
|
+
('target_path', (mode.Client.TARGET, 'path')),
|
|
272
|
+
('target_name', (mode.Client.TARGET, 'name')),
|
|
273
|
+
('target_host', (mode.Client.TARGET, 'host')),
|
|
274
|
+
('target_user', (mode.Client.TARGET, 'user')),
|
|
275
|
+
('target_password', (mode.Client.TARGET, 'password')),
|
|
276
|
+
('target_key', (mode.Client.TARGET, 'ssh_key')),
|
|
277
|
+
('target_port', (mode.Client.TARGET, 'port')),
|
|
278
|
+
('target_dump_dir', (mode.Client.TARGET, 'dump_dir')),
|
|
279
|
+
('target_after_dump', (mode.Client.TARGET, 'after_dump')),
|
|
280
|
+
('target_db_name', (mode.Client.TARGET, 'db', 'name')),
|
|
281
|
+
('target_db_host', (mode.Client.TARGET, 'db', 'host')),
|
|
282
|
+
('target_db_user', (mode.Client.TARGET, 'db', 'user')),
|
|
283
|
+
('target_db_password', (mode.Client.TARGET, 'db', 'password')),
|
|
284
|
+
('target_db_port', (mode.Client.TARGET, 'db', 'port')),
|
|
285
|
+
# Origin client mappings
|
|
286
|
+
('origin_path', (mode.Client.ORIGIN, 'path')),
|
|
287
|
+
('origin_name', (mode.Client.ORIGIN, 'name')),
|
|
288
|
+
('origin_host', (mode.Client.ORIGIN, 'host')),
|
|
289
|
+
('origin_user', (mode.Client.ORIGIN, 'user')),
|
|
290
|
+
('origin_password', (mode.Client.ORIGIN, 'password')),
|
|
291
|
+
('origin_key', (mode.Client.ORIGIN, 'ssh_key')),
|
|
292
|
+
('origin_port', (mode.Client.ORIGIN, 'port')),
|
|
293
|
+
('origin_dump_dir', (mode.Client.ORIGIN, 'dump_dir')),
|
|
294
|
+
('origin_db_name', (mode.Client.ORIGIN, 'db', 'name')),
|
|
295
|
+
('origin_db_host', (mode.Client.ORIGIN, 'db', 'host')),
|
|
296
|
+
('origin_db_user', (mode.Client.ORIGIN, 'db', 'user')),
|
|
297
|
+
('origin_db_password', (mode.Client.ORIGIN, 'db', 'password')),
|
|
298
|
+
('origin_db_port', (mode.Client.ORIGIN, 'db', 'port')),
|
|
299
|
+
# Top-level config mappings
|
|
300
|
+
('where', ('where',)),
|
|
301
|
+
('additional_mysqldump_options', ('additional_mysqldump_options',)),
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _apply_arg_mapping(args, mapping):
|
|
306
|
+
"""
|
|
307
|
+
Apply argument mappings to config dict.
|
|
245
308
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
309
|
+
:param args: Argument namespace
|
|
310
|
+
:param mapping: List of (arg_name, config_path) tuples
|
|
311
|
+
"""
|
|
312
|
+
for arg_name, config_path in mapping:
|
|
313
|
+
value = getattr(args, arg_name, None)
|
|
314
|
+
if value is None:
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
if len(config_path) == 1:
|
|
318
|
+
# Top-level config key
|
|
319
|
+
config[config_path[0]] = value
|
|
320
|
+
elif len(config_path) == 2:
|
|
321
|
+
# Client-level key: (client, key)
|
|
322
|
+
config[config_path[0]][config_path[1]] = value
|
|
323
|
+
elif len(config_path) == 3:
|
|
324
|
+
# Nested client key: (client, nested_key, key)
|
|
325
|
+
check_config_dict_key(config_path[0], config_path[1])
|
|
326
|
+
config[config_path[0]][config_path[1]][config_path[2]] = value
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_config(args, pre_run=False):
|
|
330
|
+
"""
|
|
331
|
+
Apply provided CLI arguments to config dict.
|
|
249
332
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
333
|
+
:param args: Argument namespace from argparse
|
|
334
|
+
:param pre_run: Boolean, if True only apply link-related args
|
|
335
|
+
:return: config dict
|
|
336
|
+
"""
|
|
337
|
+
if args is None or not args:
|
|
338
|
+
return {}
|
|
253
339
|
|
|
254
|
-
|
|
255
|
-
config['where'] = args.where
|
|
340
|
+
_apply_arg_mapping(args, _ARG_MAPPINGS_PRE_RUN)
|
|
256
341
|
|
|
257
|
-
if not
|
|
258
|
-
|
|
342
|
+
if not pre_run:
|
|
343
|
+
_apply_arg_mapping(args, _ARG_MAPPINGS_MAIN)
|
|
259
344
|
|
|
260
345
|
return config
|
|
261
346
|
|
|
@@ -275,6 +360,19 @@ def check_options():
|
|
|
275
360
|
if 'check_dump' in config:
|
|
276
361
|
config['check_dump'] = config['check_dump']
|
|
277
362
|
|
|
363
|
+
# Check rsync availability if enabled (default: True)
|
|
364
|
+
# Falls back to Paramiko SFTP if rsync is not available
|
|
365
|
+
if config['use_rsync']:
|
|
366
|
+
if helper.check_rsync_version():
|
|
367
|
+
helper.check_sshpass_version()
|
|
368
|
+
else:
|
|
369
|
+
config['use_rsync'] = False
|
|
370
|
+
output.message(
|
|
371
|
+
output.Subject.WARNING,
|
|
372
|
+
'rsync not found, falling back to SFTP transfer',
|
|
373
|
+
True
|
|
374
|
+
)
|
|
375
|
+
|
|
278
376
|
reverse_hosts()
|
|
279
377
|
mode.check_sync_mode()
|
|
280
378
|
|
|
@@ -286,6 +384,8 @@ def check_authorizations():
|
|
|
286
384
|
"""
|
|
287
385
|
check_authorization(mode.Client.ORIGIN)
|
|
288
386
|
check_authorization(mode.Client.TARGET)
|
|
387
|
+
# Refresh typed config after authorization changes (ssh_agent, password)
|
|
388
|
+
refresh_typed_config()
|
|
289
389
|
|
|
290
390
|
|
|
291
391
|
def check_authorization(client):
|
|
@@ -311,13 +411,7 @@ def check_authorization(client):
|
|
|
311
411
|
elif 'ssh_key' in config[client]:
|
|
312
412
|
_ssh_key = config[client]['ssh_key']
|
|
313
413
|
if not os.path.isfile(_ssh_key):
|
|
314
|
-
|
|
315
|
-
output.message(
|
|
316
|
-
output.Subject.ERROR,
|
|
317
|
-
f'SSH {client} private key not found: {_ssh_key}',
|
|
318
|
-
False
|
|
319
|
-
)
|
|
320
|
-
)
|
|
414
|
+
raise ConfigError(f'SSH {client} private key not found: {_ssh_key}')
|
|
321
415
|
elif 'password' in config[client]:
|
|
322
416
|
config[client]['password'] = config[client]['password']
|
|
323
417
|
elif remote_utility.check_keys_from_ssh_agent():
|
|
@@ -338,13 +432,11 @@ def get_password_by_user(client):
|
|
|
338
432
|
:param client: String
|
|
339
433
|
:return: String password
|
|
340
434
|
"""
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
|
|
345
|
-
False
|
|
346
|
-
)
|
|
435
|
+
_prompt = get_output_manager().build_prompt(
|
|
436
|
+
f'SSH password {helper.get_ssh_host_name(client, True)}: ',
|
|
437
|
+
subject='INFO'
|
|
347
438
|
)
|
|
439
|
+
_password = getpass.getpass(_prompt)
|
|
348
440
|
|
|
349
441
|
while _password.strip() == '':
|
|
350
442
|
output.message(
|
|
@@ -353,13 +445,11 @@ def get_password_by_user(client):
|
|
|
353
445
|
True
|
|
354
446
|
)
|
|
355
447
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
|
|
360
|
-
False
|
|
361
|
-
)
|
|
448
|
+
_prompt = get_output_manager().build_prompt(
|
|
449
|
+
f'SSH password {helper.get_ssh_host_name(client, True)}: ',
|
|
450
|
+
subject='INFO'
|
|
362
451
|
)
|
|
452
|
+
_password = getpass.getpass(_prompt)
|
|
363
453
|
|
|
364
454
|
return _password
|
|
365
455
|
|
|
@@ -377,7 +467,9 @@ def check_args_options(config_file=None,
|
|
|
377
467
|
force_password=False,
|
|
378
468
|
use_rsync=False,
|
|
379
469
|
use_rsync_options=None,
|
|
380
|
-
reverse=False
|
|
470
|
+
reverse=False,
|
|
471
|
+
with_files=False,
|
|
472
|
+
files_only=False):
|
|
381
473
|
"""
|
|
382
474
|
Checking arguments and fill options array
|
|
383
475
|
:param config_file:
|
|
@@ -394,6 +486,8 @@ def check_args_options(config_file=None,
|
|
|
394
486
|
:param use_rsync:
|
|
395
487
|
:param use_rsync_options:
|
|
396
488
|
:param reverse:
|
|
489
|
+
:param with_files:
|
|
490
|
+
:param files_only:
|
|
397
491
|
:return:
|
|
398
492
|
"""
|
|
399
493
|
global config
|
|
@@ -436,15 +530,11 @@ def check_args_options(config_file=None,
|
|
|
436
530
|
if not force_password is None:
|
|
437
531
|
config['force_password'] = force_password
|
|
438
532
|
|
|
439
|
-
if
|
|
533
|
+
if use_rsync is not None:
|
|
440
534
|
config['use_rsync'] = use_rsync
|
|
441
535
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
helper.check_sshpass_version()
|
|
445
|
-
|
|
446
|
-
if not use_rsync_options is None:
|
|
447
|
-
config['use_rsync_options'] = use_rsync_options
|
|
536
|
+
if use_rsync_options is not None:
|
|
537
|
+
config['use_rsync_options'] = use_rsync_options
|
|
448
538
|
|
|
449
539
|
if not reverse is None:
|
|
450
540
|
config['reverse'] = reverse
|
|
@@ -463,10 +553,19 @@ def check_args_options(config_file=None,
|
|
|
463
553
|
True
|
|
464
554
|
)
|
|
465
555
|
|
|
556
|
+
if with_files is not None:
|
|
557
|
+
config['with_files'] = with_files
|
|
558
|
+
|
|
559
|
+
if files_only is not None:
|
|
560
|
+
config['files_only'] = files_only
|
|
561
|
+
if files_only:
|
|
562
|
+
# files_only implies with_files
|
|
563
|
+
config['with_files'] = True
|
|
564
|
+
|
|
466
565
|
|
|
467
566
|
def reverse_hosts():
|
|
468
567
|
"""
|
|
469
|
-
|
|
568
|
+
Reverse origin and target hosts if --reverse flag is set.
|
|
470
569
|
:return:
|
|
471
570
|
"""
|
|
472
571
|
if config['reverse']:
|
|
@@ -476,6 +575,9 @@ def reverse_hosts():
|
|
|
476
575
|
config[mode.Client.ORIGIN] = _target
|
|
477
576
|
config[mode.Client.TARGET] = _origin
|
|
478
577
|
|
|
578
|
+
# Refresh typed config after swapping
|
|
579
|
+
refresh_typed_config()
|
|
580
|
+
|
|
479
581
|
output.message(
|
|
480
582
|
output.Subject.INFO,
|
|
481
583
|
'Reverse origin and target hosts',
|
|
@@ -500,22 +602,19 @@ def link_configuration_with_hosts():
|
|
|
500
602
|
|
|
501
603
|
if config['link_hosts'] == '':
|
|
502
604
|
# Try to find default hosts.json file in same directory
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
f'when using a link parameter within the configuration or define the the '
|
|
509
|
-
f'filepath direct in the link entry e.g. "host.yaml@entry1".',
|
|
510
|
-
False
|
|
511
|
-
)
|
|
605
|
+
raise ConfigError(
|
|
606
|
+
'Missing hosts file for linking hosts with configuration. '
|
|
607
|
+
'Use the "-o" / "--hosts" argument to define the filepath for the hosts file, '
|
|
608
|
+
'when using a link parameter within the configuration or define the '
|
|
609
|
+
'filepath direct in the link entry e.g. "host.yaml@entry1".'
|
|
512
610
|
)
|
|
513
611
|
|
|
514
612
|
if config['link_hosts'] != '':
|
|
515
613
|
|
|
516
614
|
# Adjust filepath from relative to absolute
|
|
517
|
-
if config['link_hosts']
|
|
518
|
-
|
|
615
|
+
if not config['link_hosts'].startswith('/'):
|
|
616
|
+
base_path = Path(config['config_file_path']).resolve().parent if config['config_file_path'] else Path.cwd()
|
|
617
|
+
config['link_hosts'] = str(base_path / config['link_hosts'])
|
|
519
618
|
|
|
520
619
|
if os.path.isfile(config['link_hosts']):
|
|
521
620
|
with open(config['link_hosts'], 'r') as read_file:
|
|
@@ -545,30 +644,14 @@ def link_configuration_with_hosts():
|
|
|
545
644
|
config[mode.Client.TARGET] = _hosts[config['link_target']]
|
|
546
645
|
config[mode.Client.ORIGIN] = _hosts[config['link_origin']]
|
|
547
646
|
else:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
f'Misconfiguration of link hosts {config["link_origin"]}, '
|
|
552
|
-
f'{config["link_target"]} in {config["link_hosts"]}',
|
|
553
|
-
False
|
|
554
|
-
)
|
|
647
|
+
raise ConfigError(
|
|
648
|
+
f'Misconfiguration of link hosts {config["link_origin"]}, '
|
|
649
|
+
f'{config["link_target"]} in {config["link_hosts"]}'
|
|
555
650
|
)
|
|
556
651
|
else:
|
|
557
|
-
|
|
558
|
-
output.message(
|
|
559
|
-
output.Subject.ERROR,
|
|
560
|
-
f'Missing link hosts for {config["link_hosts"]}',
|
|
561
|
-
False
|
|
562
|
-
)
|
|
563
|
-
)
|
|
652
|
+
raise ConfigError(f'Missing link hosts for {config["link_hosts"]}')
|
|
564
653
|
else:
|
|
565
|
-
|
|
566
|
-
output.message(
|
|
567
|
-
output.Subject.ERROR,
|
|
568
|
-
f'Local host file not found: {config["link_hosts"]}',
|
|
569
|
-
False
|
|
570
|
-
)
|
|
571
|
-
)
|
|
654
|
+
raise ConfigError(f'Local host file not found: {config["link_hosts"]}')
|
|
572
655
|
|
|
573
656
|
|
|
574
657
|
def check_config_dict_key(client, key):
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Validation script
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
import sys
|
|
9
7
|
from jsonschema import validators
|
|
10
8
|
from db_sync_tool.utility import output
|
|
9
|
+
from db_sync_tool.utility.exceptions import ValidationError
|
|
11
10
|
|
|
12
11
|
#
|
|
13
12
|
# GLOBALS
|
|
@@ -107,10 +106,4 @@ def check(config):
|
|
|
107
106
|
True
|
|
108
107
|
)
|
|
109
108
|
if errors:
|
|
110
|
-
|
|
111
|
-
output.message(
|
|
112
|
-
output.Subject.ERROR,
|
|
113
|
-
'Validation error(s)',
|
|
114
|
-
do_print=False
|
|
115
|
-
)
|
|
116
|
-
)
|
|
109
|
+
raise ValidationError('Configuration validation failed')
|