Passed
Push — master ( 9369e9...2a1b8c )
by Konrad
02:14
created

db_sync_tool.utility.system.check_authorization()   F

Complexity

Conditions 16

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 42
rs 2.4
c 0
b 0
f 0
cc 16
nop 1

How to fix   Complexity   

Complexity

Complex classes like db_sync_tool.utility.system.check_authorization() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python3
2
# -*- coding: future_fstrings -*-
3
4
"""
5
System module
6
"""
7
8
import sys
9
import json
10
import os
11
import getpass
12
import yaml
13
from db_sync_tool.utility import log, parser, mode, helper, output, validation
14
from db_sync_tool.remote import utility as remote_utility
15
16
#
17
# GLOBALS
18
#
19
20
config = {
21
    'verbose': False,
22
    'mute': False,
23
    'dry_run': False,
24
    'keep_dump': False,
25
    'dump_name': '',
26
    'import': '',
27
    'link_hosts': '',
28
    'default_origin_dump_dir': True,
29
    'default_target_dump_dir': True,
30
    'check_dump': True,
31
    'is_same_client': False,
32
    'config_file_path': None,
33
    'clear_database': False,
34
    'force_password': False,
35
    'use_rsync': False,
36
    'use_rsync_options': None,
37
    'use_sshpass': False,
38
    'ssh_agent': False,
39
    'ssh_password': {
40
        mode.Client.ORIGIN: None,
41
        mode.Client.TARGET: None
42
    },
43
    'link_target': None,
44
    'link_origin': None,
45
    'tables': ''
46
}
47
48
#
49
# DEFAULTS
50
#
51
52
default_local_sync_path = '/tmp/db_sync_tool/'
0 ignored issues
show
Coding Style Naming introduced by
Constant name "default_local_sync_path" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
53
54
55
#
56
# FUNCTIONS
57
#
58
59
def check_target_configuration():
60
    """
61
    Checking target database configuration
62
    :return:
63
    """
64
    parser.get_database_configuration(mode.Client.TARGET)
65
66
67
def get_configuration(host_config, args):
68
    """
69
    Checking configuration information by file or dictionary
70
    :param host_config: Dictionary
71
    :param args: Dictionary
72
    :return:
73
    """
74
    global config
0 ignored issues
show
Coding Style introduced by
Usage of the global statement should be avoided.

Usage of global can make code hard to read and test, its usage is generally not recommended unless you are dealing with legacy code.

Loading history...
Coding Style Naming introduced by
Constant name "config" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
75
    config[mode.Client.TARGET] = {}
76
    config[mode.Client.ORIGIN] = {}
77
78
    if host_config:
79
        config.update(json.dumps(host_config))
80
81
    _config_file_path = config['config_file_path']
82
    if not _config_file_path is None:
83
        if os.path.isfile(_config_file_path):
84
            with open(_config_file_path, 'r') as read_file:
85
                if _config_file_path.endswith('.json'):
86
                    config.update(json.load(read_file))
87
                elif _config_file_path.endswith('.yaml') or _config_file_path.endswith('.yml'):
88
                    config.update(yaml.safe_load(read_file))
89
                else:
90
                    sys.exit(
91
                        output.message(
92
                            output.Subject.ERROR,
93
                            f'Unsupported configuration file type [json,yml,yaml]: '
94
                            f'{config["config_file_path"]}',
95
                            False
96
                        )
97
                    )
98
                output.message(
99
                    output.Subject.LOCAL,
100
                    f'Loading host configuration '
101
                    f'{output.CliFormat.BLACK}{_config_file_path}{output.CliFormat.ENDC}',
102
                    True
103
                )
104
        else:
105
            sys.exit(
106
                output.message(
107
                    output.Subject.ERROR,
108
                    f'Local configuration not found: {config["config_file_path"]}',
109
                    False
110
                )
111
            )
112
113
    args_config = build_config(args)
114
115
    if config['config_file_path'] is None and args_config == {}:
116
        sys.exit(
117
            output.message(
118
                output.Subject.ERROR,
119
                f'Configuration is missing, use a separate file or provide host parameter',
0 ignored issues
show
introduced by
Using an f-string that does not have any interpolated variables
Loading history...
120
                False
121
            )
122
        )
123
124
    validation.check(config)
125
    check_options()
126
    helper.run_script(script='before')
127
    log.get_logger().info('Starting db_sync_tool')
128
129
130
def build_config(args):
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
131
    """
132
    ADding the provided arguments
133
    :param args:
134
    :return:
135
    """
136
    if args is None:
137
        return
138
139
    if not args.type is None:
140
        config['type'] = args.type
141
142
    if not args.tables is None:
143
        config['tables'] = args.tables
144
145
    if not args.origin is None:
146
        config['link_origin'] = args.origin
147
148
    if not args.target is None:
149
        config['link_target'] = args.target
150
151
    if not args.target_path is None:
152
        config[mode.Client.TARGET]['path'] = args.target_path
153
154
    if not args.target_name is None:
155
        config[mode.Client.TARGET]['name'] = args.target_name
156
157
    if not args.target_host is None:
158
        config[mode.Client.TARGET]['host'] = args.target_host
159
160
    if not args.target_user is None:
161
        config[mode.Client.TARGET]['user'] = args.target_user
162
163
    if not args.target_password is None:
164
        config[mode.Client.TARGET]['password'] = args.target_password
165
166
    if not args.target_key is None:
167
        config[mode.Client.TARGET]['ssh_key'] = args.target_key
168
169
    if not args.target_port is None:
170
        config[mode.Client.TARGET]['port'] = args.target_port
171
172
    if not args.target_dump_dir is None:
173
        config[mode.Client.TARGET]['dump_dir'] = args.target_dump_dir
174
175
    if not args.target_db_name is None:
176
        config[mode.Client.TARGET]['db']['name'] = args.target_db_name
177
178
    if not args.target_db_host is None:
179
        config[mode.Client.TARGET]['db']['host'] = args.target_db_host
180
181
    if not args.target_db_user is None:
182
        config[mode.Client.TARGET]['db']['user'] = args.target_db_user
183
184
    if not args.target_db_password is None:
185
        config[mode.Client.TARGET]['db']['password'] = args.target_db_password
186
187
    if not args.target_db_port is None:
188
        config[mode.Client.TARGET]['db']['port'] = args.target_db_port
189
190
    if not args.target_after_dump is None:
191
        config[mode.Client.TARGET]['after_dump'] = args.target_after_dump
192
193
    if not args.origin_path is None:
194
        config[mode.Client.ORIGIN]['path'] = args.origin_path
195
196
    if not args.origin_name is None:
197
        config[mode.Client.ORIGIN]['name'] = args.origin_name
198
199
    if not args.origin_host is None:
200
        config[mode.Client.ORIGIN]['host'] = args.origin_host
201
202
    if not args.origin_user is None:
203
        config[mode.Client.ORIGIN]['user'] = args.origin_user
204
205
    if not args.origin_password is None:
206
        config[mode.Client.ORIGIN]['password'] = args.origin_password
207
208
    if not args.origin_key is None:
209
        config[mode.Client.ORIGIN]['ssh_key'] = args.origin_key
210
211
    if not args.origin_port is None:
212
        config[mode.Client.ORIGIN]['port'] = args.origin_port
213
214
    if not args.origin_dump_dir is None:
215
        config[mode.Client.ORIGIN]['dump_dir'] = args.origin_dump_dir
216
217
    if not args.origin_db_name is None:
218
        config[mode.Client.ORIGIN]['db']['name'] = args.origin_db_name
219
220
    if not args.origin_db_host is None:
221
        config[mode.Client.ORIGIN]['db']['host'] = args.origin_db_host
222
223
    if not args.origin_db_user is None:
224
        config[mode.Client.ORIGIN]['db']['user'] = args.origin_db_user
225
226
    if not args.origin_db_password is None:
227
        config[mode.Client.ORIGIN]['db']['password'] = args.origin_db_password
228
229
    if not args.origin_db_port is None:
230
        config[mode.Client.ORIGIN]['db']['port'] = args.origin_db_port
231
232
    return config
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
233
234
235
def check_options():
236
    """
237
    Checking configuration provided file
238
    :return:
239
    """
240
    global config
0 ignored issues
show
Coding Style introduced by
Usage of the global statement should be avoided.

Usage of global can make code hard to read and test, its usage is generally not recommended unless you are dealing with legacy code.

Loading history...
Coding Style Naming introduced by
Constant name "config" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
241
    if 'dump_dir' in config[mode.Client.ORIGIN]:
242
        config['default_origin_dump_dir'] = False
243
244
    if 'dump_dir' in config[mode.Client.TARGET]:
245
        config['default_target_dump_dir'] = False
246
247
    if 'check_dump' in config:
248
        config['check_dump'] = config['check_dump']
249
250
    link_configuration_with_hosts()
251
    reverse_hosts()
252
    mode.check_sync_mode()
253
254
255
def check_authorizations():
256
    """
257
    Checking authorization for clients
258
    :return:
259
    """
260
    check_authorization(mode.Client.ORIGIN)
261
    check_authorization(mode.Client.TARGET)
262
263
264
def check_authorization(client):
265
    """
266
    Checking arguments and fill options array
267
    :param client: String
268
    :return:
269
    """
270
    # only need authorization if client is remote
271
    if mode.is_remote(client):
272
        # Workaround if no authorization is needed
273
        if (mode.get_sync_mode() == mode.SyncMode.DUMP_REMOTE and
0 ignored issues
show
best-practice introduced by
Too many boolean expressions in if statement (6/5)
Loading history...
274
            client == mode.Client.TARGET) or \
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation before block (add 4 spaces).
Loading history...
275
                (mode.get_sync_mode() == mode.SyncMode.DUMP_LOCAL and
276
                 client == mode.Client.ORIGIN) or \
277
                (mode.get_sync_mode() == mode.SyncMode.IMPORT_REMOTE and
278
                 client == mode.Client.ORIGIN):
279
            return
280
281
        # ssh key authorization
282
        if config['force_password']:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
283
            config[client]['password'] = get_password_by_user(client)
284
        elif 'ssh_key' in config[client]:
285
            _ssh_key = config[client]['ssh_key']
286
            if not os.path.isfile(_ssh_key):
287
                sys.exit(
288
                    output.message(
289
                        output.Subject.ERROR,
290
                        f'SSH {client} private key not found: {_ssh_key}',
291
                        False
292
                    )
293
                )
294
        elif 'password' in config[client]:
295
            config[client]['password'] = config[client]['password']
296
        elif remote_utility.check_keys_from_ssh_agent():
297
            config['ssh_agent'] = True
298
        else:
299
            # user input authorization
300
            config[client]['password'] = get_password_by_user(client)
301
302
        if mode.get_sync_mode() == mode.SyncMode.DUMP_REMOTE and \
303
                client == mode.Client.ORIGIN and 'password' in \
304
                config[mode.Client.ORIGIN]:
305
            config[mode.Client.TARGET]['password'] = config[mode.Client.ORIGIN]['password']
306
307
308
def get_password_by_user(client):
309
    """
310
    Getting password by user input
311
    :param client: String
312
    :return: String password
313
    """
314
    _password = getpass.getpass(
315
        output.message(
316
            output.Subject.INFO,
317
            'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
318
            False
319
        )
320
    )
321
322
    while _password.strip() == '':
323
        output.message(
324
            output.Subject.WARNING,
325
            'Password seems to be empty. Please enter a valid password.',
326
            True
327
        )
328
329
        _password = getpass.getpass(
330
            output.message(
331
                output.Subject.INFO,
332
                'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
333
                False
334
            )
335
        )
336
337
    return _password
338
339
340
def check_args_options(config_file=None,
0 ignored issues
show
best-practice introduced by
Too many arguments (14/5)
Loading history...
341
                       verbose=False,
342
                       yes=False,
343
                       mute=False,
344
                       dry_run=False,
345
                       import_file=None,
346
                       dump_name=None,
347
                       keep_dump=None,
348
                       host_file=None,
349
                       clear=False,
350
                       force_password=False,
351
                       use_rsync=False,
352
                       use_rsync_options=None,
353
                       reverse=False):
354
    """
355
    Checking arguments and fill options array
356
    :param config_file:
357
    :param verbose:
358
    :param yes:
359
    :param mute:
360
    :param dry_run:
361
    :param import_file:
362
    :param dump_name:
363
    :param keep_dump:
364
    :param host_file:
365
    :param clear:
366
    :param force_password:
367
    :param use_rsync:
368
    :param use_rsync_options:
369
    :param reverse:
370
    :return:
371
    """
372
    global config
0 ignored issues
show
Coding Style introduced by
Usage of the global statement should be avoided.

Usage of global can make code hard to read and test, its usage is generally not recommended unless you are dealing with legacy code.

Loading history...
Coding Style Naming introduced by
Constant name "config" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
373
    global default_local_sync_path
0 ignored issues
show
Coding Style introduced by
Usage of the global statement should be avoided.

Usage of global can make code hard to read and test, its usage is generally not recommended unless you are dealing with legacy code.

Loading history...
Coding Style Naming introduced by
Constant name "default_local_sync_path" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
374
375
    if not config_file is None:
376
        config['config_file_path'] = config_file
377
378
    if not verbose is None:
379
        config['verbose'] = verbose
380
381
    if not yes is None:
382
        config['yes'] = yes
383
384
    if not mute is None:
385
        config['mute'] = mute
386
387
    if not dry_run is None:
388
        config['dry_run'] = dry_run
389
390
        if dry_run:
391
            output.message(
392
                output.Subject.INFO,
393
                'Test mode: DRY RUN',
394
                True
395
            )
396
397
    if not import_file is None:
398
        config['import'] = import_file
399
400
    if not dump_name is None:
401
        config['dump_name'] = dump_name
402
403
    if not host_file is None:
404
        config['link_hosts'] = host_file
405
406
    if not clear is None:
407
        config['clear_database'] = clear
408
409
    if not force_password is None:
410
        config['force_password'] = force_password
411
412
    if not use_rsync is None:
413
        config['use_rsync'] = use_rsync
414
415
        if use_rsync is True:
416
            helper.check_rsync_version()
417
            helper.check_sshpass_version()
418
419
        if not use_rsync_options is None:
420
            config['use_rsync_options'] = use_rsync_options
421
422
    if not reverse is None:
423
        config['reverse'] = reverse
424
425
    if not keep_dump is None:
426
        default_local_sync_path = keep_dump
427
428
        # Adding trailing slash if necessary
429
        if default_local_sync_path[-1] != '/':
430
            default_local_sync_path += '/'
431
432
        config['keep_dump'] = True
433
        output.message(
434
            output.Subject.INFO,
435
            '"Keep dump" option chosen',
436
            True
437
        )
438
439
440
def reverse_hosts():
441
    """
442
    Checking authorization for clients
443
    :return:
444
    """
445
    if config['reverse']:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
446
        _origin = config[mode.Client.ORIGIN]
447
        _target = config[mode.Client.TARGET]
448
449
        config[mode.Client.ORIGIN] = _target
450
        config[mode.Client.TARGET] = _origin
451
452
        output.message(
453
            output.Subject.INFO,
454
            'Reverse origin and target hosts',
455
            True
456
        )
457
458
459
def link_configuration_with_hosts():
460
    """
461
    Merging the hosts definition with the given configuration file
462
    @ToDo Simplify function
463
    :return:
464
    """
465
    if ('link' in config[mode.Client.ORIGIN] or 'link' in config[mode.Client.TARGET]) and config['link_hosts'] == '':
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (117/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
466
        #
467
        # Try to read host file path from link entry
468
        #
469
        _host = str(config[mode.Client.ORIGIN]['link'].split('@')[0]) if 'link' in config[mode.Client.ORIGIN] else ''
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (117/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
470
        _host = str(config[mode.Client.TARGET]['link'].split('@')[0]) if 'link' in config[mode.Client.TARGET] else _host
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (120/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
471
472
        config['link_hosts'] = _host
473
474
        if config['link_hosts'] == '':
475
            # Try to find default hosts.json file in same directory
476
            sys.exit(
477
                output.message(
478
                    output.Subject.ERROR,
479
                    f'Missing hosts file for linking hosts with configuration. '
0 ignored issues
show
introduced by
Using an f-string that does not have any interpolated variables
Loading history...
480
                    f'Use the "-o" / "--hosts" argument to define the filepath for the hosts file, '
481
                    f'when using a link parameter within the configuration or define the the '
482
                    f'filepath direct in the link entry e.g. "host.yaml@entry1".',
483
                    False
484
                )
485
            )
486
487
    if config['link_hosts'] != '':
488
489
        # Adjust filepath from relative to absolute
490
        if config['link_hosts'][0] != '/':
491
            config['link_hosts'] = os.path.dirname(os.path.abspath(config['config_file_path'])) + '/' + config['link_hosts']
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (124/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
492
493
        if os.path.isfile(config['link_hosts']):
494
            with open(config['link_hosts'], 'r') as read_file:
495
                if config['link_hosts'].endswith('.json'):
496
                    _hosts = json.load(read_file)
497
                elif config['link_hosts'].endswith('.yaml') or config['link_hosts'].endswith('.yml'):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (101/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
498
                    _hosts = yaml.safe_load(read_file)
499
500
                output.message(
501
                    output.Subject.INFO,
502
                    f'Linking configuration with hosts {output.CliFormat.BLACK}{config["link_hosts"]}{output.CliFormat.ENDC}',
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (126/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
503
                    True
504
                )
505
                if not config['config_file_path'] is None:
506
                    if 'link' in config[mode.Client.ORIGIN]:
507
                        _host_name = str(config[mode.Client.ORIGIN]['link']).split('@')[1]
508
                        if _host_name in _hosts:
0 ignored issues
show
introduced by
The variable _hosts does not seem to be defined for all execution paths.
Loading history...
509
                            config[mode.Client.ORIGIN] = {**config[mode.Client.ORIGIN], **_hosts[_host_name]}
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (109/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
510
511
                    if 'link' in config[mode.Client.TARGET]:
512
                        _host_name = str(config[mode.Client.TARGET]['link']).split('@')[1]
513
                        if _host_name in _hosts:
514
                            config[mode.Client.TARGET] = {**config[mode.Client.TARGET], **_hosts[_host_name]}
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (109/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
515
                else:
516
                    if 'link_target' in config and 'link_origin' in config:
517
                        if config['link_target'] in _hosts and config['link_origin'] in _hosts:
518
                            config[mode.Client.TARGET] = _hosts[config['link_target']]
519
                            config[mode.Client.ORIGIN] = _hosts[config['link_origin']]
520
                        else:
521
                            sys.exit(
522
                                output.message(
523
                                    output.Subject.ERROR,
524
                                    f'Misconfiguration of link hosts {config["link_origin"]}, '
525
                                    f'{config["link_target"]} in {config["link_hosts"]}',
526
                                    False
527
                                )
528
                            )
529
                    else:
530
                        sys.exit(
531
                            output.message(
532
                                output.Subject.ERROR,
533
                                f'Missing link hosts for {config["link_hosts"]}',
534
                                False
535
                            )
536
                        )
537
        else:
538
            sys.exit(
539
                output.message(
540
                    output.Subject.ERROR,
541
                    f'Local host file not found: {config["link_hosts"]}',
542
                    False
543
                )
544
            )
545