db_sync_tool.utility.system.get_password_by_user()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 30
rs 9.55
c 0
b 0
f 0
cc 2
nop 1
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
    'where': '',
47
    'additional_mysqldump_options': ''
48
}
49
50
#
51
# DEFAULTS
52
#
53
54
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...
55
56
57
#
58
# FUNCTIONS
59
#
60
61
def check_target_configuration():
62
    """
63
    Checking target database configuration
64
    :return:
65
    """
66
    parser.get_database_configuration(mode.Client.TARGET)
67
68
69
def get_configuration(host_config, args = {}):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Bug Best Practice introduced by
The default value {} might cause unintended side-effects.

Objects as default values are only created once in Python and not on each invocation of the function. If the default object is modified, this modification is carried over to the next invocation of the method.

# Bad:
# If array_param is modified inside the function, the next invocation will
# receive the modified object.
def some_function(array_param=[]):
    # ...

# Better: Create an array on each invocation
def some_function(array_param=None):
    array_param = array_param or []
    # ...
Loading history...
70
    """
71
    Checking configuration information by file or dictionary
72
    :param host_config: Dictionary
73
    :param args: Dictionary
74
    :return:
75
    """
76
    global config
0 ignored issues
show
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...
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...
77
    config[mode.Client.TARGET] = {}
78
    config[mode.Client.ORIGIN] = {}
79
80
    if host_config:
81
        if type(host_config) is dict:
0 ignored issues
show
introduced by
Using type() instead of isinstance() for a typecheck.
Loading history...
82
            config.update(__m=host_config)
83
        else:
84
            config.update(__m=json.dumps(obj=host_config))
85
86
    _config_file_path = config['config_file_path']
87
    if not _config_file_path is None:
88
        if os.path.isfile(_config_file_path):
89
            with open(_config_file_path, 'r') as read_file:
90
                if _config_file_path.endswith('.json'):
91
                    config.update(json.load(read_file))
92
                elif _config_file_path.endswith('.yaml') or _config_file_path.endswith('.yml'):
93
                    config.update(yaml.safe_load(read_file))
94
                else:
95
                    sys.exit(
96
                        output.message(
97
                            output.Subject.ERROR,
98
                            f'Unsupported configuration file type [json,yml,yaml]: '
99
                            f'{config["config_file_path"]}',
100
                            False
101
                        )
102
                    )
103
                output.message(
104
                    output.Subject.LOCAL,
105
                    f'Loading host configuration '
106
                    f'{output.CliFormat.BLACK}{_config_file_path}{output.CliFormat.ENDC}',
107
                    True
108
                )
109
        else:
110
            sys.exit(
111
                output.message(
112
                    output.Subject.ERROR,
113
                    f'Local configuration not found: {config["config_file_path"]}',
114
                    False
115
                )
116
            )
117
118
    # workaround for argument order handling respecting the linking feature
119
    build_config(args, True)
120
    link_configuration_with_hosts()
121
    build_config(args)
122
123
    validation.check(config)
124
    check_options()
125
126
    if not config[mode.Client.TARGET] and not config[mode.Client.ORIGIN]:
127
        sys.exit(
128
            output.message(
129
                output.Subject.ERROR,
130
                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...
131
                False
132
            )
133
        )
134
    helper.run_script(script='before')
135
    log.get_logger().info('Starting db_sync_tool')
136
137
138
def build_config(args, pre_run = False):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
139
    """
140
    ADding the provided arguments
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
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
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
174
175
    if not args.target_password is None:
176
        config[mode.Client.TARGET]['password'] = args.target_password
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
221
222
    if not args.origin_password is None:
223
        config[mode.Client.ORIGIN]['password'] = args.origin_password
224
225
    if not args.origin_key is None:
226
        config[mode.Client.ORIGIN]['ssh_key'] = args.origin_key
227
228
    if not args.origin_port is None:
229
        config[mode.Client.ORIGIN]['port'] = args.origin_port
230
231
    if not args.origin_dump_dir is None:
232
        config[mode.Client.ORIGIN]['dump_dir'] = args.origin_dump_dir
233
234
    if not args.origin_db_name is None:
235
        check_config_dict_key(mode.Client.ORIGIN, 'db')
236
        config[mode.Client.ORIGIN]['db']['name'] = args.origin_db_name
237
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
242
    if not args.origin_db_user is None:
243
        check_config_dict_key(mode.Client.ORIGIN, 'db')
244
        config[mode.Client.ORIGIN]['db']['user'] = args.origin_db_user
245
246
    if not args.origin_db_password is None:
247
        check_config_dict_key(mode.Client.ORIGIN, 'db')
248
        config[mode.Client.ORIGIN]['db']['password'] = args.origin_db_password
249
250
    if not args.origin_db_port is None:
251
        check_config_dict_key(mode.Client.ORIGIN, 'db')
252
        config[mode.Client.ORIGIN]['db']['port'] = args.origin_db_port
253
254
    if not args.where is None:
255
        config['where'] = args.where
256
257
    if not args.additional_mysqldump_options is None:
258
        config['additional_mysqldump_options'] = args.additional_mysqldump_options
259
260
    return config
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
261
262
263
def check_options():
264
    """
265
    Checking configuration provided file
266
    :return:
267
    """
268
    global config
0 ignored issues
show
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...
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...
269
    if 'dump_dir' in config[mode.Client.ORIGIN]:
270
        config['default_origin_dump_dir'] = False
271
272
    if 'dump_dir' in config[mode.Client.TARGET]:
273
        config['default_target_dump_dir'] = False
274
275
    if 'check_dump' in config:
276
        config['check_dump'] = config['check_dump']
277
278
    reverse_hosts()
279
    mode.check_sync_mode()
280
281
282
def check_authorizations():
283
    """
284
    Checking authorization for clients
285
    :return:
286
    """
287
    check_authorization(mode.Client.ORIGIN)
288
    check_authorization(mode.Client.TARGET)
289
290
291
def check_authorization(client):
292
    """
293
    Checking arguments and fill options array
294
    :param client: String
295
    :return:
296
    """
297
    # only need authorization if client is remote
298
    if mode.is_remote(client):
299
        # Workaround if no authorization is needed
300
        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...
301
            client == mode.Client.TARGET) or \
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation before block (add 4 spaces).
Loading history...
302
                (mode.get_sync_mode() == mode.SyncMode.DUMP_LOCAL and
303
                 client == mode.Client.ORIGIN) or \
304
                (mode.get_sync_mode() == mode.SyncMode.IMPORT_REMOTE and
305
                 client == mode.Client.ORIGIN):
306
            return
307
308
        # ssh key authorization
309
        if config['force_password']:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
310
            config[client]['password'] = get_password_by_user(client)
311
        elif 'ssh_key' in config[client]:
312
            _ssh_key = config[client]['ssh_key']
313
            if not os.path.isfile(_ssh_key):
314
                sys.exit(
315
                    output.message(
316
                        output.Subject.ERROR,
317
                        f'SSH {client} private key not found: {_ssh_key}',
318
                        False
319
                    )
320
                )
321
        elif 'password' in config[client]:
322
            config[client]['password'] = config[client]['password']
323
        elif remote_utility.check_keys_from_ssh_agent():
324
            config['ssh_agent'] = True
325
        else:
326
            # user input authorization
327
            config[client]['password'] = get_password_by_user(client)
328
329
        if mode.get_sync_mode() == mode.SyncMode.DUMP_REMOTE and \
330
                client == mode.Client.ORIGIN and 'password' in \
331
                config[mode.Client.ORIGIN]:
332
            config[mode.Client.TARGET]['password'] = config[mode.Client.ORIGIN]['password']
333
334
335
def get_password_by_user(client):
336
    """
337
    Getting password by user input
338
    :param client: String
339
    :return: String password
340
    """
341
    _password = getpass.getpass(
342
        output.message(
343
            output.Subject.INFO,
344
            'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
345
            False
346
        )
347
    )
348
349
    while _password.strip() == '':
350
        output.message(
351
            output.Subject.WARNING,
352
            'Password seems to be empty. Please enter a valid password.',
353
            True
354
        )
355
356
        _password = getpass.getpass(
357
            output.message(
358
                output.Subject.INFO,
359
                'SSH password ' + helper.get_ssh_host_name(client, True) + ': ',
360
                False
361
            )
362
        )
363
364
    return _password
365
366
367
def check_args_options(config_file=None,
0 ignored issues
show
best-practice introduced by
Too many arguments (14/5)
Loading history...
368
                       verbose=False,
369
                       yes=False,
370
                       mute=False,
371
                       dry_run=False,
372
                       import_file=None,
373
                       dump_name=None,
374
                       keep_dump=None,
375
                       host_file=None,
376
                       clear=False,
377
                       force_password=False,
378
                       use_rsync=False,
379
                       use_rsync_options=None,
380
                       reverse=False):
381
    """
382
    Checking arguments and fill options array
383
    :param config_file:
384
    :param verbose:
385
    :param yes:
386
    :param mute:
387
    :param dry_run:
388
    :param import_file:
389
    :param dump_name:
390
    :param keep_dump:
391
    :param host_file:
392
    :param clear:
393
    :param force_password:
394
    :param use_rsync:
395
    :param use_rsync_options:
396
    :param reverse:
397
    :return:
398
    """
399
    global config
0 ignored issues
show
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...
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...
400
    global default_local_sync_path
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...
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...
401
402
    if not config_file is None:
403
        config['config_file_path'] = config_file
404
405
    if not verbose is None:
406
        config['verbose'] = verbose
407
408
    if not yes is None:
409
        config['yes'] = yes
410
411
    if not mute is None:
412
        config['mute'] = mute
413
414
    if not dry_run is None:
415
        config['dry_run'] = dry_run
416
417
        if dry_run:
418
            output.message(
419
                output.Subject.INFO,
420
                'Test mode: DRY RUN',
421
                True
422
            )
423
424
    if not import_file is None:
425
        config['import'] = import_file
426
427
    if not dump_name is None:
428
        config['dump_name'] = dump_name
429
430
    if not host_file is None:
431
        config['link_hosts'] = host_file
432
433
    if not clear is None:
434
        config['clear_database'] = clear
435
436
    if not force_password is None:
437
        config['force_password'] = force_password
438
439
    if not use_rsync is None:
440
        config['use_rsync'] = use_rsync
441
442
        if use_rsync is True:
443
            helper.check_rsync_version()
444
            helper.check_sshpass_version()
445
446
        if not use_rsync_options is None:
447
            config['use_rsync_options'] = use_rsync_options
448
449
    if not reverse is None:
450
        config['reverse'] = reverse
451
452
    if not keep_dump is None:
453
        default_local_sync_path = keep_dump
454
455
        # Adding trailing slash if necessary
456
        if default_local_sync_path[-1] != '/':
457
            default_local_sync_path += '/'
458
459
        config['keep_dump'] = True
460
        output.message(
461
            output.Subject.INFO,
462
            '"Keep dump" option chosen',
463
            True
464
        )
465
466
467
def reverse_hosts():
468
    """
469
    Checking authorization for clients
470
    :return:
471
    """
472
    if config['reverse']:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
473
        _origin = config[mode.Client.ORIGIN]
474
        _target = config[mode.Client.TARGET]
475
476
        config[mode.Client.ORIGIN] = _target
477
        config[mode.Client.TARGET] = _origin
478
479
        output.message(
480
            output.Subject.INFO,
481
            'Reverse origin and target hosts',
482
            True
483
        )
484
485
486
def link_configuration_with_hosts():
487
    """
488
    Merging the hosts definition with the given configuration file
489
    @ToDo Simplify function
490
    :return:
491
    """
492
    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...
493
        #
494
        # Try to read host file path from link entry
495
        #
496
        _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...
497
        _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...
498
499
        config['link_hosts'] = _host
500
501
        if config['link_hosts'] == '':
502
            # Try to find default hosts.json file in same directory
503
            sys.exit(
504
                output.message(
505
                    output.Subject.ERROR,
506
                    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...
507
                    f'Use the "-o" / "--hosts" argument to define the filepath for the hosts file, '
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
                )
512
            )
513
514
    if config['link_hosts'] != '':
515
516
        # Adjust filepath from relative to absolute
517
        if config['link_hosts'][0] != '/':
518
            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...
519
520
        if os.path.isfile(config['link_hosts']):
521
            with open(config['link_hosts'], 'r') as read_file:
522
                if config['link_hosts'].endswith('.json'):
523
                    _hosts = json.load(read_file)
524
                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...
525
                    _hosts = yaml.safe_load(read_file)
526
527
                output.message(
528
                    output.Subject.INFO,
529
                    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...
530
                    True
531
                )
532
                if not config['config_file_path'] is None:
533
                    if 'link' in config[mode.Client.ORIGIN]:
534
                        _host_name = str(config[mode.Client.ORIGIN]['link']).split('@')[1]
535
                        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...
536
                            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...
537
538
                    if 'link' in config[mode.Client.TARGET]:
539
                        _host_name = str(config[mode.Client.TARGET]['link']).split('@')[1]
540
                        if _host_name in _hosts:
541
                            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...
542
                else:
543
                    if 'link_target' in config and 'link_origin' in config:
544
                        if config['link_target'] in _hosts and config['link_origin'] in _hosts:
545
                            config[mode.Client.TARGET] = _hosts[config['link_target']]
546
                            config[mode.Client.ORIGIN] = _hosts[config['link_origin']]
547
                        else:
548
                            sys.exit(
549
                                output.message(
550
                                    output.Subject.ERROR,
551
                                    f'Misconfiguration of link hosts {config["link_origin"]}, '
552
                                    f'{config["link_target"]} in {config["link_hosts"]}',
553
                                    False
554
                                )
555
                            )
556
                    else:
557
                        sys.exit(
558
                            output.message(
559
                                output.Subject.ERROR,
560
                                f'Missing link hosts for {config["link_hosts"]}',
561
                                False
562
                            )
563
                        )
564
        else:
565
            sys.exit(
566
                output.message(
567
                    output.Subject.ERROR,
568
                    f'Local host file not found: {config["link_hosts"]}',
569
                    False
570
                )
571
            )
572
573
574
def check_config_dict_key(client, key):
575
    """
576
    Create config key if is not present
577
    :param client:
578
    :param key:
579
    :return:
580
    """
581
    if key not in config[client]:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable config does not seem to be defined.
Loading history...
582
        config[client][key] = {}
583
0 ignored issues
show
coding-style introduced by
Trailing newlines
Loading history...
584