-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathSenzingGo.py
1788 lines (1383 loc) · 78.5 KB
/
SenzingGo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#! /usr/bin/env python3
import argparse
import concurrent.futures
import configparser
import json
import os
import pathlib
import pwd
import re
import socket
import stat
import subprocess
import sys
import tarfile
import textwrap
import time
import urllib
from contextlib import suppress
from datetime import datetime
from math import ceil
from pathlib import Path
from time import sleep
try:
import docker
except ImportError:
print('\nPlease install the Python Docker module (pip3 install docker)')
print('\nAdditional information: https://github.com/senzing-garage/senzinggo\n')
sys.exit(1)
__all__ = []
__version__ = '1.6.7' # See https://www.python.org/dev/peps/pep-0396/
__date__ = '2021-09-10'
__updated__ = '2023-06-07'
class Colors:
# Standard colors
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
WHITE = '\033[37m'
# Custom colors
DARK_ORANGE = '\033[38;5;208m'
# Other
INFO = '\033[34m'
WARN = '\033[38;5;208m'
ERROR = '\033[31m'
BOLD = '\033[1m'
END = '\033[0m'
DEFAULT = '\033[37m'
DIM = '\033[02m'
class LogCats:
INFO = f'{Colors.INFO}INFO{Colors.END}'
WARNING = f'{Colors.WARN}WARNING{Colors.END}'
ERROR = f'{Colors.ERROR}ERROR{Colors.END}'
# f-strings expressions don't allow backslash, use for formatting in f-strings
class Format:
NEWLINE = '\n'
CURSOR_UP = '\033[F'
def update_check_and_get():
""" Check version numbers """
with urllib.request.urlopen("https://api.github.com/repos/senzing-garage/SenzingGo/releases/latest", timeout=5) as rel_response:
rel_ver = json.loads(rel_response.read())['name']
# Senzing follows release versions of x.y.z
# Do they look as we expect?
try:
_ = int(__version__.replace('.', ''))
_ = int(rel_ver.replace('.', ''))
test_this = __version__.replace('.', '')
test_rel = rel_ver.replace('.', '')
except ValueError:
logger(f'Either the version of this script {__version__} or the released version {rel_ver} don\'t contain all digits', LogCats.ERROR)
logger('Cannot check if a new update is available or perform an update', LogCats.ERROR)
return None, None, None, None
else:
if len(test_this) < 3 or len(test_rel) < 3:
logger(f'Either the version of this script {__version__} or the released version {rel_ver} are not formatted as expected', LogCats.ERROR)
logger('Cannot check if a new update is available or perform an update', LogCats.ERROR)
return None, None, None, None
this_ver_concat = int(test_this)
rel_ver_concat = int(test_rel)
return this_ver_concat, rel_ver_concat, __version__, rel_ver
def update_check():
""" Check if update is available """
logger('Checking for an update...')
this_ver_int, rel_ver_int, this_ver, rel_ver = update_check_and_get()
logger(f'Current version: {this_ver} - Available version: {rel_ver}', LogCats.INFO)
if not this_ver_int and not rel_ver_int:
return
if this_ver_int < rel_ver_int:
return True
return False
def update(senz_root):
""" Perform an update """
_, rel_ver, _, _ = update_check_and_get()
if not rel_ver:
return
new_go_file = f'{senz_root}/python/SenzingGo.py_{rel_ver}'
with urllib.request.urlopen("https://raw.githubusercontent.com/senzing-garage/senzinggo/main/SenzingGo.py") as rel_response:
new_go = rel_response.read()
with open(new_go_file, 'wb') as go:
go.write(new_go)
try:
os.chmod(new_go_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IEXEC | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP)
except OSError as ex:
raise ex
else:
logger(f'Updated version downloaded, replace SenzingGo.py with {new_go_file} and re-launch to use new version', msg_color=Colors.BLUE)
def get_senzing_root(script_name):
""" Get the SENZING_ROOT env var """
senz_root = os.environ.get('SENZING_ROOT', None)
if not senz_root:
if os.geteuid() == 0:
logger(f'Running with sudo and SENZING_ROOT isn\'t set. Ensure setupEnv file is sourced and run with "sudo --preserve-env ./{script_name}"', LogCats.WARNING)
else:
logger('SENZING_ROOT isn\'t set please source the project setupEnv file to use all features', LogCats.WARNING)
logger('Without SENZING_ROOT set, only --saveImages (-si) and --loadImages modes are available')
return senz_root
def get_host_name(tout=2):
""" Attempt to get fully qualified hostname """
host_name = None
logger('Collecting networking information...')
# Test if on AWS and fetch AWS external hostname
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
# There is also the ec2-metadata tool that can report the instance data
host_end_point = 'http://169.254.169.254/latest/meta-data/public-hostname'
with suppress(Exception):
host_url = urllib.request.urlopen(host_end_point, timeout=tout)
public_host = host_url.read()
return public_host.decode(), True
# FQDN
with suppress(Exception):
host_name = socket.getfqdn(socket.gethostbyname(socket.gethostname()))
# Hostname
if not host_name:
with suppress(Exception):
host_name = socket.gethostbyname(socket.gethostname())
# Otherwise set to localhost, can be overridden with the -ho CLI arg
if not host_name:
logger('Unable to detect a hostname, using localhost, this could cause issues.', cat=LogCats.WARNING)
logger('If networking issues arise, set a hostname or try using the --host (-ho) argument to specify host or ip address.', cat=LogCats.WARNING)
host_name = 'localhost'
return host_name, False
def get_ip_addr(host_name, tout=2):
""" Attempt to get IP address """
ipv4 = None
# Test if on AWS and fetch AWS external IPV4
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
# There is also the ec2-metadata tool that can report the instance data
ipv4_end_point = 'http://169.254.169.254/latest/meta-data/public-ipv4'
with suppress(Exception):
ipv4_url = urllib.request.urlopen(ipv4_end_point, timeout=tout)
public_ipv4 = ipv4_url.read()
return public_ipv4.decode()
# Try easy method
with suppress(Exception):
ipv4 = socket.gethostbyname(host_name)
# Try external method
if not ipv4:
with suppress(Exception):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(("8.8.8.8", 80))
ipv4 = sock.getsockname()[0]
if not ipv4:
logger('Unable to detect an IP address, using 127.0.0.1, this could cause issues.', LogCats.WARNING)
logger('If networking issues arise please check if a valid IP address is assigned.', LogCats.WARNING)
ipv4 = '127.0.0.1'
return ipv4
def ini_localhost_check(ini_file_name):
""" Check the INI file doesn't use localhost
localhost can't be used when the INI file is baked into a container, would be pointing to the container itself
"""
with open(ini_file_name, 'r') as inifile:
for line in inifile:
line_check = line.lstrip().lower()
# Look for localhost in normal and cluster ini lines
if (line_check.startswith('connection') or line_check.startswith('db_1')) and ('@localhost:' in line_check or '@127.0.0.1:' in line_check):
logger('Connection string cannot use localhost or 127.0.0.1, use a hostname or ip address', LogCats.ERROR)
logger(f'\t{line}')
sys.exit(1)
def convert_ini2json(ini_file_name):
""" Convert INI parms to JSON for use in the Docker containers"""
ini_json = {}
cfgp = configparser.ConfigParser(empty_lines_in_values=False, interpolation=None)
cfgp.optionxform = str
cfgp.read(ini_file_name)
for section in cfgp.sections():
ini_json[section] = dict(cfgp.items(section))
return ini_json
def internet_access(url, retries=3, retries_start=None, tout=2, check_msg=False):
""" Test for access to resources that are required"""
if check_msg:
logger('Checking for internet access and Senzing resources...')
try:
urllib.request.urlopen(url, timeout=tout)
logger(f'{url} {Colors.GREEN}Available{Colors.END}')
return True
except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout):
if retries > 1:
retries -= 1
retries_start = retries if not retries_start else retries_start
sleep(1)
internet_access(url, retries, retries_start, check_msg=False)
if retries == retries_start:
logger(f'{url} {Colors.WARN}Unavailable{Colors.END}')
return False
def get_api_spec(url, retries=10, tout=5):
""" Get the REST API specification from the REST server """
retry = retries
if retry == retries:
logger('Fetching API specification from REST server')
while retry > 0:
try:
api_spec_url = urllib.request.urlopen(url, timeout=tout)
api_spec = api_spec_url.read()
return api_spec
except (urllib.error.URLError, urllib.error.HTTPError, ConnectionResetError):
sleep_time = 5 * (retries - retry) if retry < ceil(retries/retry) else 5
logger(f'Waiting for API specification from REST server, pausing for {sleep_time}s before retry...')
sleep(sleep_time)
retry -= 1
except Exception as ex:
logger('General error communicating with the REST server, cannot continue!', LogCats.ERROR)
logger(ex, LogCats.ERROR)
sys.exit(1)
logger('Unable to connect to or fetch API specification from REST server, cannot continue!', LogCats.ERROR)
sys.exit(1)
def parse_versions(url):
""" Parse the online Senzing Docker versions file into a dict to looking latest version numbers"""
# #!/usr/bin/env bash
#
# # Generated on 2021-10-05 by https://github.com/senzing-factory/dockerhub-util dockerhub-util.py version: 1.0.3 update: 2021-10-05
#
# export SENZING_DOCKER_IMAGE_VERSION_ADMINER=1.0.0
# export SENZING_DOCKER_IMAGE_VERSION_APT=1.0.5.post1
# export SENZING_DOCKER_IMAGE_VERSION_APT_DOWNLOADER=1.1.3
try:
# Read the versions data into a dict
response = urllib.request.urlopen(url)
page = response.read().decode().replace('export SENZING_', 'SENZING_')
versions = {kv.split('=')[0]: kv.split('=')[1] for kv in
[line for line in page.split('\n') if line.startswith('SENZING_')]}
except urllib.error.HTTPError as ex:
logger('Fetching latest versions, the server couldn\'t fulfill the request.', LogCats.ERROR)
logger(f'Error code: {ex.code}')
return False
except urllib.error.URLError as ex:
logger('Fetching latest versions, failed to reach a server.', LogCats.ERROR)
logger(f'Reason: {ex.reason}')
return False
return versions
def docker_checks(script_name):
""" Perform checks for Docker """
logger('Performing Docker checks...')
# Is Docker installed?
try:
dversion = subprocess.run(['docker', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
if 'podman' in dversion.stdout.lower():
logger('Podman is being used, this is unsupported this tool requires Docker: https://docs.docker.com/engine/install/', LogCats.ERROR)
sys.exit(1)
except FileNotFoundError:
logger('Docker doesn\'t appear to be installed and is required: https://docs.docker.com/engine/install/', LogCats.ERROR)
sys.exit(1)
# Not launched as sudo, check if user can use docker without sudo
if os.geteuid() != 0:
# Will succeed if the user can use docker without sudo - e.g. in the docker group, docker running rootless
check = subprocess.run(['docker', 'images'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
if check.returncode != 0:
if 'permission denied' and 'socket' in check.stderr:
logger(f'User cannot run Docker, run with "sudo --preserve-env ./{script_name}" or be added to the docker group', LogCats.ERROR)
else:
logger(check.stderr, LogCats.ERROR)
sys.exit(1)
def docker_init(url):
""" Initialise a Docker client """
try:
client = docker.DockerClient(base_url=url)
except docker.errors.DockerException as ex:
logger('Unable to instantiate Docker, is the Docker service running and Docker URL correct?', LogCats.ERROR)
logger(ex, LogCats.ERROR)
logger(f'Docker URL: {url}', LogCats.ERROR)
sys.exit(1)
return client
def docker_image_exists(docker_client, image_name):
""" Test if a Docker image already exists """
return True if docker_client.images.list(name=image_name) else False
def pull_default_images(docker_client, docker_containers, no_web_app, no_swagger, check_health, dock_run_args):
""" Docker pull the base set of images required for the tool
Senzing Rest API server, Senzing Entity Search App, Swagger UI
"""
logger('Checking and pulling Docker images, this may take many minutes')
images_to_pull = {}
for key, image_list in docker_containers.items():
# Skip pulling images if CLI args request not to deploy
if no_web_app and image_list['imagename'] == 'senzing/entity-search-web-app':
continue
if no_swagger and image_list['imagename'] == 'swaggerapi/swagger-ui':
continue
images_to_pull[key] = image_list['imagename'] + ':' + image_list['tag']
with concurrent.futures.ThreadPoolExecutor(max_workers=len(images_to_pull)) as executor:
future_pull = {executor.submit(docker_pull,
docker_client,
image,
docker_containers[key]['msgcolor'],
docker_containers[key]['msgcolor'],
key):
(key, image) for key, image in images_to_pull.items()}
for future in concurrent.futures.as_completed(future_pull):
pull_success = future_pull[future]
try:
result, key = future.result()
except docker.errors.DockerException as ex:
logger(ex, cat=LogCats.ERROR, task_color=docker_containers[dock_run_args[0]['container']]['msgcolor'], task=pull_success[0])
if pull_success[0] == 'REST API Server':
logger('Couldn\'t pull REST API Server image, can\'t continue without it!', LogCats.ERROR)
sys.exit(1)
else:
docker_containers[key]['imagepulled'] = True
docker_containers[key]['imageavailable'] = True
if dock_run_args:
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor2:
future_run = {executor2.submit(docker_run, docker_client, docker_containers, check_health, **parms): parms for parms in dock_run_args}
for future in concurrent.futures.as_completed(future_run):
pull_success = future_run[future]
try:
_ = future.result()
except docker.errors.DockerException as ex:
logger(ex, cat=LogCats.ERROR, task_color=docker_containers[dock_run_args[0]['container']]['msgcolor'], task=pull_success[0])
sys.exit(1)
def docker_pull(docker_client, image, msg_color=Colors.DEFAULT, task_color=Colors.BLUE, key='SenzingGo'):
""" Pull Docker images """
logger(f'Pulling image {image}...', msg_color=msg_color, task_color=task_color, task=key)
try:
for pull_resp in docker_client.api.pull(image, stream=True, decode=True):
if pull_resp['status'][:7] == 'Status:':
logger(pull_resp["status"][8:],
msg_color=msg_color,
task_color=task_color,
task=key)
return 'PULLED', key
except (docker.errors.ImageNotFound, docker.errors.NotFound) as ex:
logger('If the following error is image cannot be found, check free storage. Lack of storage can throw such an error.',
LogCats.ERROR,
task_color=task_color,
task=key)
logger(ex, LogCats.ERROR)
raise
def docker_net(docker_client, network_name, network_driver='bridge'):
""" Create a Docker network for use by the project containers """
if not docker_client.networks.list(names=network_name):
logger(f'Docker network {network_name} doesn\'t exist, creating...')
try:
docker_client.networks.create(name=network_name, driver=network_driver)
except docker.errors.DockerException as ex:
logger(f'{ex}', LogCats.ERROR)
sys.exit(1)
def docker_cont_list(docker_client, all_conts=True, cont_filters=None):
""" Get a list of the current Docker containers """
cont_filters = {} if not cont_filters else cont_filters
return docker_client.containers.list(all=all_conts, filters=cont_filters)
def docker_run(docker_client, docker_containers, check_health, **kwargs):
""" Create and run a container """
def status_wait(msg, color, check, cont_name, loop_cnt=20, t_sleep=5):
""" Wait for container to become healthy if it reports health """
for r in range(loop_cnt):
logger(msg, task_color=color, msg_color=color, task=container_key)
cont_status = docker_client.containers.get(cont_name).status
cont_attrs = docker_client.containers.get(cont_name).attrs
if check == 'running' and cont_status == 'running':
return check
if check == 'healthy' and cont_attrs['State']['Health']['Status'] == 'healthy':
return check
time.sleep(t_sleep)
cont_status = docker_client.containers.get(cont_name).status
cont_attrs = docker_client.containers.get(cont_name).attrs
return cont_status if check == 'running' else cont_attrs['State']['Health']['Status']
# Get the container key to use at the end to set startedok status
container_key = kwargs['container']
container_color = docker_containers[container_key]["msgcolor"]
logger('Running...', msg_color=container_color, task_color=container_color, task=kwargs["container"], )
# Remove the container key from the args sent to the run, only used to set startedok
del kwargs['container']
try:
docker_client.containers.run(**kwargs)
except docker.errors.APIError as ex:
logger(f'{ex}', LogCats.ERROR)
sys.exit(1)
if check_health:
if status_wait('Waiting for container to start...', container_color, 'running', kwargs['name']) == 'exited':
logger(f'Container did not start successfully, status: {docker_client.containers.get(kwargs["name"]).status}',
LogCats.ERROR,
task=container_key)
logger(f'Check the status and outcome with the command: {Colors.DEFAULT}"docker logs {kwargs["name"]}"{Colors.END}',
LogCats.ERROR,
task_color=container_color,
task=container_key)
sys.exit(1)
if docker_client.containers.get(kwargs['name']).attrs.get('State').get('Health', None):
if status_wait('Waiting for container to become healthy...', container_color, 'healthy', kwargs['name']) != 'healthy':
logger(f'Container isn\'t healthy yet or failed, monitor with the command: {Colors.DEFAULT}"docker logs {kwargs["name"]}"{Colors.END}',
LogCats.WARNING,
task_color=container_color,
task=container_key)
else:
logger('Started', msg_color=container_color, task_color=container_color, task=container_key)
else:
logger('This container doesn\'t report health',
LogCats.INFO,
msg_color=container_color,
task_color=container_color,
task=container_key)
logger(f'Use the follow command to check status if any issues arise: {Colors.DEFAULT}"docker logs {kwargs["name"]}"{Colors.END}',
LogCats.INFO,
msg_color=container_color,
task_color=container_color,
task=container_key)
logger('Presumed started!',
LogCats.INFO,
msg_color=container_color,
task_color=container_color,
task=container_key)
if not check_health:
logger('Started', msg_color=container_color, task_color=container_color, task=container_key)
# If didn't exit assume all is well
docker_containers[container_key]['startedok'] = True
def containers_stop_remove(senzing_proj_name,
docker_client,
docker_containers,
containers_remove,
docker_network,
startup_remove=False,
forced_remove=False):
""" Stop and optionally remove SenzingGo containers """
def container_remove(container):
""" """
# Remove leading / https://github.com/docker/docker-py/pull/2634
cont_name = container.attrs['Name'].lstrip('/')
cont_key = container.attrs['Config']['Labels']['SzGoContKey']
if cont_name in project_container_names:
logger('Stopping...', msg_color=docker_containers[cont_key]["msgcolor"], task_color=docker_containers[cont_key]["msgcolor"], task=cont_key)
try:
container.stop()
except docker.errors.APIError as ex:
logger(f'Failed to stop container: {ex}', LogCats.ERROR)
if containers_remove or startup_remove or forced_remove:
logger('Removing...', msg_color=docker_containers[cont_key]["msgcolor"], task_color=docker_containers[cont_key]["msgcolor"], task=cont_key)
try:
# Remove volumes too
container.remove(v=True, force=True)
except docker.errors.APIError as ex:
logger(ex, LogCats.ERROR)
# Only lists running containers, all=True to return all
logger('Looking for existing containers to remove')
# Look for containers that match the project name, including any that are not running (all=True)
containers = docker_cont_list(docker_client, all_conts=True, cont_filters={'name': senzing_proj_name})
# Base project container names
project_container_names = [values['containername'] for values in docker_containers.values()]
running_containers = [c.attrs['Name'].lstrip('/') for c in containers]
# Don't print message if in startup and deleting any existing containers
if not running_containers and not startup_remove:
logger(f'No matching containers for {senzing_proj_name}, were they created with a different suffix with -ps (--projectSuffix)?')
all_containers = docker_cont_list(docker_client, all_conts=True)
if all_containers:
logger('Available containers:')
for cont in all_containers:
logger(f'\t{cont.attrs["Name"].lstrip("/")}')
return
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
future_rem = {executor.submit(container_remove, cont): cont for cont in containers}
concurrent.futures.wait(future_rem)
# Remove the network too
if containers_remove:
network_list = docker_client.networks.list(names=docker_network)
# Should only be one item in the list as use names filter above on the network for the project
if network_list:
logger(f'Removing Docker network {network_list[0].attrs["Name"]}')
with suppress(Exception):
network_list[0].remove()
def containers_info(docker_client, docker_containers, senzing_proj_name, host_name, rest_api_env):
""" Get info for running containers, e.g. the url they are running on after startup information is lost
Always show the command used for the REST Server
"""
def show_api_env(env):
""" Show API Server command for reference """
logger(f'{Colors.BOLD}REST API Server environment variables:{Colors.END}', LogCats.INFO, msg_color=Colors.INFO)
for e in env:
logger(f'\t{e}')
sys.exit(0)
logger(f'Looking for containers matching {senzing_proj_name}...')
containers = docker_cont_list(docker_client, all_conts=True, cont_filters={'name': senzing_proj_name})
if not containers:
logger(f'There are currently no containers matching {senzing_proj_name}')
show_api_env(rest_api_env)
sys.exit(0)
for name in [c.name for c in containers]:
# name is used as a key in the NetworkSettings -> Ports JSON object and is needed to find the host port the container
# was started on
if name.startswith('SzGo-API-'):
key = 'REST API Server'
elif name.startswith('SzGo-WEB-'):
key = 'Web App Demo'
elif name.startswith('SzGo-Swagger-'):
key = 'Swagger UI'
else:
# Only continue and list info for SenzingGo containers
logger(f'Matching containers found for {senzing_proj_name}, but they don\'t appear to be for SenzingGo')
sys.exit(0)
status = docker_client.containers.get(name).attrs.get("State")["Status"]
logger(f'{Colors.BOLD}{Colors.BLUE}Container:{Colors.END} {name}')
logger(f' {Colors.BOLD}{Colors.BLUE}Image:{Colors.END} {docker_client.containers.get(name).attrs.get("Config").get("Image")}')
if status == 'running':
host_port = docker_client.containers.get(name).attrs.get("NetworkSettings").get("Ports")[
str(docker_containers[key]["containerport"]) + "/tcp"][0]["HostPort"]
logger(f' {Colors.BOLD}{Colors.BLUE}URL:{Colors.END} http://{host_name}:{host_port}')
logger('')
show_api_env(rest_api_env)
def container_logs(docker_client, logs_string=None):
""" Get logs for one or more containers """
# Using filter there could be more than one container returned
matching_containers = docker_cont_list(docker_client, cont_filters={"name": logs_string if logs_string else 'SzGo'})
if matching_containers:
for cont in matching_containers:
print(f'\n{Colors.GREEN}********** Start logs for {cont.name} **********{Colors.END}')
print(f'\n{cont.logs().decode()}')
print(f'\n{Colors.GREEN}********** End logs for {cont.name} **********\n{Colors.END}')
else:
logger('No matching container(s) to show logs for, use -i (--info) to see available containers')
sys.exit(0)
def list_image_names(docker_image_names, access, versions):
""" Get a list of all Senzing image names, used when packaging a custom save images file """
if not access:
logger('Unable to obtain the list of Senzing Docker images to display', LogCats.ERROR)
sys.exit(1)
try:
# Read the docker images json file from GitHub
response = urllib.request.urlopen(docker_image_names)
page = response.read().decode()
except urllib.error.HTTPError as ex:
logger('Fetching image names, the server couldn\'t fulfill the request.', LogCats.WARNING)
logger(f'Error code: {ex.code}', LogCats.WARNING)
return False
except urllib.error.URLError as ex:
logger('Fetching image names, failed to reach server.', LogCats.WARNING)
logger(f'Reason: {ex.reason}', LogCats.WARNING)
return False
docker_image_names = json.loads(page)
if not versions:
logger('Using "latest" for version, the versions file wasn\'t available for reference', LogCats.INFO)
# Display the image name (k) and either the version number if available or latest
for k, v in docker_image_names.items():
print(f'{k}:{versions.get(v["environment_variable"], "latest") if versions else "latest"}')
def get_timestamp():
""" Create timestamp """
return datetime.now().strftime("%Y%m%d_%H%M%S")
def save_images(docker_client, docker_containers, images, save_images_path, access_dockerhub, no_web_app, no_swagger):
""" Package up base set or custom set of images to transfer and use on another system """
def save_image(i):
""""""
package_file = f'{package_path}/SzGoPackage-{i.replace("/", "-").replace(":", "-")}.tar'
packaged_files.append(package_file)
try:
with open(package_file, 'wb') as sf:
image_to_save = docker_client.images.get(i)
logger(f'Saving {i} to {package_file}...')
for chunk in image_to_save.save(named=True):
sf.write(chunk)
except (FileNotFoundError, IOError) as exx:
logger(exx, LogCats.ERROR)
sys.exit(1)
avail_images_with_tag = []
packaged_files = []
images_to_pull = []
# Set package path depending on if default value (str) was used or a path was specified (list)
package_path = save_images_path[0] if isinstance(save_images_path, list) else save_images_path
if not access_dockerhub:
logger('Cannot reach internet to pull images, can only package existing ones if available locally', LogCats.WARNING)
# If a list of packages was specified pull them, otherwise no arguments on saveimages arg
if len(images) > 0:
# If have internet access perform pull
for image in images:
if access_dockerhub:
img, _, tag = image.partition(':')
if not tag:
logger(f'{Colors.INFO}INFO{Colors.END}: Tag is missing from {image}, defaulting to "latest"')
images_to_pull.append(image + ":latest")
if not img:
logger(f'{Colors.ERROR}ERROR{Colors.END}: Image is missing from {image}, can\'t pull!')
sys.exit(1)
images_to_pull.append(image)
# If don't have internet access check if each image exists, if it doesn't error as can't complete the request
else:
try:
docker_client.images.get(image)
except docker.errors.ImageNotFound:
logger(f'Image {image} isn\'t locally available to save, can\'t complete request.', LogCats.ERROR)
sys.exit(1)
with concurrent.futures.ThreadPoolExecutor(max_workers=len(images_to_pull)) as executor3:
future_pull = {executor3.submit(docker_pull, docker_client, img, Colors.DEFAULT, Colors.BLUE): img for img in images_to_pull}
for future in concurrent.futures.as_completed(future_pull):
try:
_, key = future.result()
except Exception as ex:
logger('Image failed to pull!', LogCats.ERROR)
logger(ex, LogCats.ERROR)
sys.exit(1)
else:
# future_pull[future] containers the image with tag
avail_images_with_tag.append(future_pull[future])
# Normal packaging of the base images needed for rest, webapp, swagger
else:
images_newest_dict = {}
if access_dockerhub:
pull_default_images(docker_client, docker_containers, no_web_app, no_swagger, False, None)
# Get a list of all images, only get the first tag entry [0] if there are > 1 tags
images = [i.tags[0] for i in docker_client.images.list()]
images.sort()
# Use a dict to store only one image with the latest version from the sort, sort has latest version first
# There could be multiple versions of an image on a system from earlier use or manual pulls, get the latest one
for image in images:
# There could be an image tagged to push to a local registry, e.g. localhost:5000/senzing/senzing-api-server:2.7.5
# ignore these
if image.count(':') > 1:
continue
name, version = image.split(':')
images_newest_dict[name] = version
# Join the image name and version back into a unique list of images to package up
avail_to_package = [k + ':' + v for k, v in images_newest_dict.items()]
# If an image name without the tag is in the base set of images add it to be packaged
# In this packaging mode only want the 3 base images
for candidate_image in avail_to_package:
if candidate_image.split(':')[0] in (docker_containers['REST API Server']['imagename'],
docker_containers['Web App Demo']['imagename'],
docker_containers['Swagger UI']['imagename']):
avail_images_with_tag.append(candidate_image)
if not avail_images_with_tag:
logger(f'There are no locally available images to save', LogCats.ERROR)
sys.exit(1)
with concurrent.futures.ThreadPoolExecutor(max_workers=len(avail_images_with_tag)) as executor:
future_save = {executor.submit(save_image, image): image for image in avail_images_with_tag}
concurrent.futures.wait(future_save)
compressed_package = f'{package_path}/SzGoImages_{get_timestamp()}.tgz'
logger(f'Compressing images to {compressed_package}...')
# Add the image tar files to compressed tar and delete the image tar files
try:
with tarfile.open(compressed_package, 'w:gz') as tar:
for name in packaged_files:
# arcname to specify only the file name to tar not the entire dir structure
tar.add(name, recursive=False, arcname=Path(name).name)
os.remove(name)
except FileNotFoundError as ex:
logger(ex, LogCats.ERROR)
sys.exit(1)
logger(f'Move {compressed_package} to the system to load the images to', LogCats.INFO, msg_color=Colors.INFO)
logger('Run this tool on the above system with --loadImages (-li) or Docker load commands to make them available', LogCats.INFO, msg_color=Colors.INFO)
def load_images(docker_client, var_path, load_file_path):
""" Load images that have previously been packaged u with save images """
def load_image(img_file):
""""""
logger(f'Loading image file {img_file.name}...')
try:
with open(img_file, 'rb') as lf:
docker_client.images.load(lf)
except FileNotFoundError as exx:
logger(exx, LogCats.ERROR)
sys.exit(1)
except docker.errors.DockerException as exx:
logger('Unable to instantiate Docker, is the Docker service running and Docker URL correct?', LogCats.ERROR)
logger(exx, LogCats.ERROR)
else:
logger(f'Image file {img_file.name} loaded')
extract_path = var_path / f'SzGo_Extract_{get_timestamp()}'
file_to_extract = Path(load_file_path).resolve()
# Uncompress tar file to retrieve the tar image files
logger(f'Extracting Senzing Docker images from {file_to_extract}...')
try:
with tarfile.open(file_to_extract, 'r:gz') as tar:
tar.extractall(path=extract_path)
except FileNotFoundError as ex:
logger(ex, LogCats.ERROR)
sys.exit(1)
with concurrent.futures.ThreadPoolExecutor(max_workers=len(os.listdir(extract_path))) as executor:
future_load = {executor.submit(load_image, image_file): image_file for image_file in os.scandir(extract_path)}
concurrent.futures.wait(future_load)
# Once completed remove the temp extract dir
with suppress(Exception):
os.remove(extract_path)
def patch_ini_json(ini_json):
""" Patch the INI file for use inside of container """
def type_connection_split(connection_string):
""" """
try:
dbt, conn = connection_string.split(':', 1)
except ValueError:
logger('Couldn\'t parse connection string and find the database type:', LogCats.ERROR)
logger(connection_string, LogCats.ERROR)
sys.exit(1)
return dbt, conn
def get_path(connection_string):
""" """
_, conn = type_connection_split(connection_string)
try:
_, conn_str = conn.split('@', 1)
except ValueError:
logger('Couldn\'t parse connection string on @:', LogCats.ERROR)
logger(conn, LogCats.ERROR)
sys.exit(1)
conn_str_path = Path(conn_str).resolve().parent
return str(conn_str_path)
# Correct ini parms for inside container and volume args on docker run command(s)
ini_json['PIPELINE']['SUPPORTPATH'] = '/opt/senzing/data'
ini_json['PIPELINE']['CONFIGPATH'] = '/etc/opt/senzing'
del ini_json['PIPELINE']['RESOURCEPATH']
# Detect if LICENSEFILE is set in ini parms and modify for inside container
try:
lic_file = ini_json['PIPELINE']['LICENSEFILE']
if lic_file:
lic_file_name = Path(lic_file).name
ini_json['PIPELINE']['LICENSEFILE'] = f'/etc/opt/senzing/{lic_file_name}'
except KeyError:
lic_file = None
# Get the base connection string regardless of clustered mode or not
base_conn_str = ini_json['SQL']['CONNECTION']
dbtype, connection = type_connection_split(base_conn_str)
if dbtype.lower() == 'sqlite3':
# If a cluster check each sqlite db file is in the same path, needed for mounting into Docker API server container
# This tool doesn't support each db file in different locations
if ini_json['SQL'].get('BACKEND', None) and ini_json['SQL']['BACKEND'].lower() == 'hybrid':
# Get the unique set of cluster keys used in the ini [HYBRID] section, e.g. C1, C2
cluster_keys = [ini_json['HYBRID'][cluster_key] for cluster_key in ini_json['HYBRID']]
unique_cluster_keys = list(set(cluster_keys))
# Get all the connection strings for each cluster key detected in [HYBRID], add base connection too
# e.g. sqlite3://na:na@/home/ant/senzprojs/2_7_0-Release/var/sqlite/G2_RES.db
try:
cluster_conn_strs = [ini_json[cluster_key]['DB_1'] for cluster_key in unique_cluster_keys]
except KeyError:
logger('Couldn\'t locate DB path key DB_1 in the engine init parameters, please use the key DB_1 for the DB URL values', LogCats.ERROR)
logger('e.g., "SQL": {"BACKEND": "HYBRID", "CONNECTION": "sqlite3://na:na@/senzproj/var/sqlite/G2C.db"}, "C1": {"CLUSTER_SIZE": "1", "DB_1": "sqlite3://na:na@/senzproj/var/sqlite/G2C_RES.db"},...', LogCats.ERROR)
sys.exit(1)
cluster_conn_strs.append(base_conn_str)
# Get the path without the database file name for each connection string
# e.g. /home/ant/senzprojs/2_7_0-Release/var/sqlite
path_list = [get_path(path) for path in cluster_conn_strs]
unique_path_list = list(set(path_list))
if len(unique_path_list) > 1:
logger('When using a sqlite cluster, all database files must be in the same path:', LogCats.INFO)
for path in unique_path_list:
logger(f'\t{path}', LogCats.INFO)
sys.exit(1)
# The host path to mount in the docker run volume arg is the only value on the list after it was made unique
host_path_for_volume = unique_path_list[0]
# Not clustered
else:
host_path_for_volume = get_path(base_conn_str)
# Replace the original path(s) with the path inside the container
json_str = json.dumps(ini_json)
ini_json_patched = json.loads(json_str.replace(host_path_for_volume, '/var/opt/senzing'))
# Build the values to use in the volume argument for the mount to return
mount_in_cont = [host_path_for_volume, {"bind": "/var/opt/senzing", "mode": "rw"}]
return dbtype, ini_json_patched, mount_in_cont, lic_file
# If not sqlite return the patched ini_json without meddling with connection strings
return dbtype, ini_json, None, lic_file
def mysql_check(senzing_root, lib_my_sql, db_type, senzing_support):
""" Checks for MySql """
lib_mysql_path = Path(f'{senzing_root}/lib/{lib_my_sql}')
if not lib_mysql_path.is_file():