-
Notifications
You must be signed in to change notification settings - Fork 3
/
baculabackupreport.py
executable file
·2962 lines (2755 loc) · 164 KB
/
baculabackupreport.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/python3
#
# ---------------------------------------------------------------------------
# - 20210426 - baculabackupreport.py - Date of initial release
# - Run ./baculabackupreport.py -v
# to see latest release date and
# version of script
# ---------------------------------------------------------------------------
#
# This is is my first foray into Python. Please be nice :)
#
# This script is a rewrite/port from my original baculabackupreport.sh script
# which was something that started out as a simple bash script which just
# sent a basic text email about recent Bacula jobs.
#
# Over time, and with a lot of requests, and great ideas, the script grew
# into a giant, unmaintainable mashup/combination of bash & awk.
#
# I always knew it would need to be rewritten in something like Python, so
# 1.5 years ago I started the parallel tasks of beginning to learn Python,
# while porting the original script. I made some pretty good progress
# relatively quickly, but then I gave up - Until this past week when I
# picked it up again.
#
# What follows is version 1.0 of my efforts. Is it pretty? No. Did I follow
# proper python coding conventions? I think that is also a big No. Does it
# produce a nice HTML email showing a lot of valuable information to a backup
# administrator? I say YES! Would I appreciate feedback? YES!
#
# If you use this script every day and think it is worth anything, I am
# always grateful to receive donations of any size with Venmo: @waa2k, or
# or PayPal: @billarlofski
#
# The latest version of this script may be found at: https://github.com/waa
#
# ---------------------------------------------------------------------------
# BSD 2-Clause License
#
# Copyright (c) 2021-2024, William A. Arlofski waa@revpol.com
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
#
# USER VARIABLES - All user variables below may be edited directly in this
# script, or overridden in the config file. See the options -C and -S in the
# instructions. Because the defaults in this script may change and more
# variables may be added over time, it is highly recommended to make use of
# the config file for customizing the variable settings.
# ----------------------------------------------------------------------------
# External GUI link settings
# --------------------------
webgui = 'none' # Which web interface to generate links for? (bweb, baculum, none)
webguisvc = 'http' # Use encrypted connection or not (ie: http or https)
webguihost = '' # FQDN or IP address of the web gui host
webguiport = '' # TCP port the web gui is bound to (Defaults: bweb 9180, baculum 9095)
urlifyalljobs = False # Should jobids in the Status column for Copied/Migrated/Verified jobs
# be made into URL links too? If set to False, only the jobids in the
# jobid column will be made into URL links
# Toggles and other formatting settings
# -------------------------------------
boldjobname = True # Bold the job name in HTML emails?
boldstatus = True # Bold the status in HTML emails?
starbadjobids = False # Wrap bad jobs jobids with asterisks "*"?
sortorder = 'DESC' # Which direction to sort jobids by? (ASC or DESC)
showcopiedto = True # Show the jobids that migrated/backup jobs have been copied to
print_subject = True # Print (stdout) the subject of the email being sent
print_sent = True # Print (stdout) when the email is successfully sent
flagrescheduled = True # Should we flag jobs which had failed but succeeded after having been rescheduled?
show_db_stats = True # Include a row at the top of the Jobs table showing database statistics?
include_pnv_jobs = True # Include copied, migrated, verified jobs whose endtime is older than "-t hours"?
# NOTE:
# - Copied/Migrated jobs inherit the endtime of the original backup job which
# can often be older than the number of hours set. These jobs would not normally
# be included in the list which can be confusing when Copy/Migration jobs in the
# list refer to them but they are not listed.
# - Verify jobs can verify any job, even very old ones. This option makes sure
# verified jobs older than the hours set are also included in the listing.
checkforvirus = False # Enable the additional checks for viruses
virusfoundtext = 'Virus detected' # Some unique text that your AV software prints to the Bacula job
# log when a virus is detected. ONLY ClamAV is supported at this time!
verified_job_name_col = 'name' # What column should the job name of verified jobs be displayed? (name, type, both, none)
copied_migrated_job_name_col = 'name' # What column should the job name of Copied/Migrated jobs be displayed? (name, type, both, none)
print_client_version = True # Print the Client version under the Client name in the Job table?
enc_hdr_type = 'both' # What should be displayed in the "Encrypted" header cell? (text, emoji, both)
enc_cell_type = 'both' # What should be displayed in the "Encrypted" cell? (text, emoji, both)
# Warn about 'OK' jobs when "Will not descend" is reported in logs?
# -----------------------------------------------------------------
warn_on_will_not_descend = True # Should 'OK' jobs be set to 'OK/Warnings' when "Will not descend" is reported in logs?
ignore_warn_on_will_not_descend_jobs = 'Job_1 Job_2' # Case-sensitive list of job names to ignore for 'warn_on_will_not_descend' test
# Warn about jobs that have been seen in the catalog, but
# have not had a successful run in 'last_good_run_days' days
# ----------------------------------------------------------
warn_on_last_good_run = True # Do we warn about jobs that have not run successfully in 'last_good_run_days' days?
last_good_run_days = 31 # Longest amount of days a job can be missed from running successfully
last_good_run_skip_lst = ['Job1', 'Job2', 'CDRoms-ToAoE'] # Jobs to ignore when processing this 'warn_on_last_good_run' feature
# Warn about 'OK' Diff/Inc jobs with zero files and/or bytes
# ----------------------------------------------------------
warn_on_zero_inc = False # Should 'OK' Inc/Diff jobs be set to 'OK/Warnings' when they backup zero files and/or bytes?
ignore_warn_on_zero_inc_jobs = 'Job_2 Job_2' # Case-sensitive list of job names to ignore for 'warn_on_zero_inc' test
# Warn about pools approaching or surpassing maxvols?
# ---------------------------------------------------
chk_pool_use = True # Check pools for numvols vs maxvols?
pools_to_ignore = 'Pool_1 Pool_2' # Case-sesitive list of pools to always ignore for 'chk_pool_use' test
# Summary and Success Rates block
# -------------------------------
summary_and_rates = 'bottom' # Print a Summary and Success Rates block? (top, bottom, both, none)
# Create the Job Summary table?
# -----------------------------
create_job_summary_table = True # Create a Job Summary table in the Summary and Success Rates block?
bacula_dir_version = True # Print the Bacula Director version?
db_version = True # Print the database version?
restore_stats = True # Print Restore Files/Bytes?
copied_stats = True # Print Copied Files/Bytes?
migrated_stats = True # Print Migrated Files/Bytes?
verified_stats = True # Print Verified Files/Bytes?
# Create a Success Rates table?
# -----------------------------
create_success_rates_table = True
# Create the Client Version < Director table?
# -------------------------------------------
create_client_ver_lt_dir_table = True # Create the Client Version < Director table"
# Show how long a job has been been waiting on media under 'Needs Media' in the Status field
# ------------------------------------------------------------------------------------------
needs_media_since_or_for = 'for' # none = print nothing, for = (for x Days, y Hours, z Minutes), since = (since YYYY-MM-DD HH:MM:SS)
# When printing the Pool and Storage columns do we strip where the Pool or Storage
# used was ultimately taken from?
# eg: 'Pool: "Full_Pool1" (From Job resource)' would be simply 'Full_Pool1' if this is set to True
# eg: 'Storage: "File_Store1" (From Pool resource)' would be simply 'File_Store1' if this is set to True
# ------------------------------------------------------------------------------------------------------
strip_p_or_s_from = False
# Additional Job logs and summaries
# ---------------------------------
emailvirussummary = True # Email the viruses summary report as a separate email?
appendvirussummaries = False # Append virus summary information to the job report email?
appendjobsummaries = False # Append all job summaries? Be careful with this, it can generate very large emails
appendbadlogs = False # Append logs of bad jobs? Be careful with this, it can generate very large emails
# Email subject settings including some example utf-8
# icons to prepend the subject with. Examples from:
# https://www.utf8-chartable.de/unicode-utf8-table.pl
# Note: On Arch Linux the 'noto-fonts' packages is
# required to properly display some of these
# UTF-8 characters. The package may be named
# differently on other Linux distributions
# ---------------------------------------------------
addsubjecticon = True # Prepend the email Subject with UTF-8 icons? See (no|good|warn|bad|alwaysfail)jobsicon variables
addsubjectrunningorcreated = True # Append "(# Jobs still runnning/queued)" to subject if running or queued Jobs > 0?
nojobsicon = '=?utf-8?Q?=F0=9F=9A=AB?=' # utf-8 'no entry sign' icon when no jobs have been run
goodjobsicon = '=?utf-8?Q?=F0=9F=9F=A9?=' # utf-8 'green square' icon when all jobs were "OK"
# goodjobsicon = '=?UTF-8?Q?=E2=9C=85?=' # utf-8 'white checkmark in green box' icon
# goodjobsicon = '=?UTF-8?Q?=E2=98=BA?=' # utf-8 'smiley face' icon
warnjobsicon = '=?UTF-8?Q?=F0=9F=9F=A7?=' # utf-8 'orange square' icon when all jobs are "OK", but some have errors/warnings
# warnjobsicon = '=?UTF-8?Q?=F0=9F=9F=A8?=' # utf-8 'yellow square' icon
badjobsicon = '=?utf-8?Q?=F0=9F=9F=A5?=' # utf-8 'red square' icon
# badjobsicon = '=?utf-8?Q?=E2=9C=96?=' # utf-8 'black bold X' icon
# badjobsicon = '=?utf-8?Q?=E2=9D=8C?=' # utf-8 'red X' icon
# badjobsicon = '=?utf-8?Q?=E2=9D=97?=' # utf-8 'red !' icon
# badjobsicon = '=?utf-8?Q?=E2=98=B9?=' # utf-8 'sad face'
alwaysfailjobsicon = '=?utf-8?Q?=E2=9B=94?=' # utf-8 'red circle with white hyphen' icon when there are "always failing" jobs
jobneedsopricon = '=?utf-8?Q?=F0=9F=96=AD?=' # utf-8 'tape cartridge' icon when there are jobs that need operator attention
# jobneedsopricon = '=?utf-8?Q?=F0=9F=92=BE?=' # utf-8 'floppy' icon
virusfoundicon = '=?utf-8?Q?=F0=9F=A6=A0?=' # utf-8 'microbe' (virus) icon
# virusfoundicon = '=?utf-8?Q?=F0=9F=90=9E?=' # utf-8 'ladybug' (virus) icon
# Email body icons/emojis
# -----------------------
virusfoundbodyicon = '🦠' # HEX encoding for emoji in email body 'microbe' (virus) icon
# virusfoundbodyicon = '🐞' # HEX encoding for emoji in email body 'ladybug' (virus) icon
# virusfoundbodyicon = '🐛' # HEX encoding for emoji in email body 'bug' (virus) icon
# virusfoundbodyicon = '👻' # HEX encoding for emoji in email body 'ghost' (virus) icon
# virusfoundbodyicon = '👽' # HEX encoding for emoji in email body 'grey alien' (virus) icon
# virusfoundbodyicon = '👾' # HEX encoding for emoji in email body 'space invader' (virus) icon
# virusfoundbodyicon = '💀' # HEX encoding for emoji in email body 'skull' (virus) icon
# virusfoundbodyicon = '💣' # HEX encoding for emoji in email body 'bomb' (virus) icon
# Encrypted/Unencrypted emojis
# ----------------------------
enc_emoji = '🔐' # Emoji for encrypted jobs. 'Locked lock with key' emoji
# enc_emoji = '🔒' # Emoji for encrypted jobs. 'Locked lock without key' emoji
un_enc_emoji = '🔓' # Emoji for unencrypted jobs. 'Unloaded lock' emoji
# Set the columns to display and their order
# Recommended to always include jobid, jobname, status, type, and endtime
# as these may have special formatting applied by default in certain cases
# ------------------------------------------------------------------------
cols2show = 'jobid jobname client fileset storage pool status joberrors type level jobfiles jobbytes starttime endtime runtime'
# Directories to always ignore for the "Will not descend" feature
# ---------------------------------------------------------------
will_not_descend_ignore_lst = ['/dev', '/misc', '/net', '/proc', '/run', '/srv', '/sys']
# Should we short-circuit everything and send no email when all jobs are OK?
# --------------------------------------------------------------------------
do_not_email_on_all_ok = False
# Set the column to colorize for jobs that are always failing
# -----------------------------------------------------------
alwaysfailcolumn = 'jobname' # Column to colorize for "always failing jobs" (column name, row, none)
always_fail_jobs_threshold = 5 # A job must have failed at least this many times in '-d days' to be considered as 'always failing'
# This prevents a failed job that is run one or two times from being displayed as always failing
# for at least a week by default. Set this to 0 or 1 to disable the threshold feature.
# HTML colors
# -----------
colorstatusbg = True # Colorize the Status cell's background?
jobtablerowevencolor = '#ffffff' # Background color for the even job rows in the HTML table
jobtableroweventxtcolor = '#000000' # Text color for the even job rows in the HTML table
jobtablerowoddcolor = '#f1f1f1' # Background color for the odd job rows in the HTML table
jobtablerowoddtxtcolor = '#000000' # Text color for the odd job rows in the HTML table
jobtableheadercolor = '#ad3939' # Background color for the HTML job table's header
jobtableheadertxtcolor = '#ffffff' # Text color for the HTML job table's header
summarytablerowevencolor = '#ffffff' # Background color for the even summary rows in the HTML table
summarytableroweventxtcolor = '#000000' # Text color for the even summary rows in the HTML table
summarytablerowoddcolor = '#f1f1f1' # Background color for the odd summary rows in the HTML table
summarytablerowoddtxtcolor = '#000000' # Text color for the odd summary rows in the HTML table
summarytableheadercolor = '#ad3939' # Background color for the HTML summary table's header
summarytableheadertxtcolor = '#ffffff' # Text color for the HTML summary table's header
runningjobcolor = '#4d79ff' # Background color of the Status cell for "Running" jobs
createdjobcolor = '#add8e6' # Background color of the Status cell for "Created, but not yet running" jobs
goodjobcolor = '#00f000' # Background color of the Status cell for "OK" jobs
badjobcolor = '#cc3300' # Background color of the Status cell for "Bad" jobs
warnjobcolor = '#ffc800' # Background color of the Status cell for "Backup OK -- with warnings" jobs
errorjobcolor = '#cc3300' # Background color of the Status cell for jobs with errors
alwaysfailcolor = '#ebd32a' # Background color of the 'alwaysfailcolumn', or entire row for jobs "always failing in the past 'days'"
virusfoundcolor = '#88eebb' # Background color of the Banner and 'Type' Cell when a virus is found in a Verify, Level=Data job
virusconnerrcolor = '#ffb3b3' # Background color of the Banner and 'Type' Cell when there are errors connecting to AV service
poolredcolor = 'red' # Background color of the Pool Use table row for pools with use % >= 96%
poolorangecolor = 'orange' # Background color of the Pool Use table row for pools with use % 90-95%
poolyellowcolor = 'yellow' # Background color of the Pool Use table row for pools with use % 81-89%
# HTML fonts
# ----------
fontfamily = 'Verdana, Arial, Helvetica, sans-serif' # Font family to use for HTML emails
fontsize = '16px' # Font size to use for email title (title removed from email for now)
fontsizejobinfo = '12px' # Font size to use for job information inside of table
fontsizesumlog = '12px' # Font size of job summaries and bad job logs
fontsize_addtional_texts = '10px' # Font size of (will not descend), (since or for), (warn on zero inc) additional info texts
# HTML styles
# -----------
# 20230429 - Still working to replace all inline css with internal
# css to reduce line length of the job rows in the report.
# -------------------------------------------------------------------
jobtablestyle = 'width: 100%; border-collapse: collapse;'
dbstatstableheaderstyle = 'width: 35%; border-collapse: collapse;'
jobtableheaderstyle = 'font-size: 12px; text-align: center; background-color: %s; color: %s;' % (jobtableheadercolor, jobtableheadertxtcolor)
jobtableheadercellstyle = 'padding: 6px'
jobtablecellpadding = '5px;'
jobtablealwaysfailrowstyle = 'background-color: %s;' % alwaysfailcolor
jobtablealwaysfailcellstyle = 'text-align: center; background-color: %s;' % alwaysfailcolor
jobtablevirusfoundcellstyle = 'text-align: center; background-color: %s;' % virusfoundcolor
jobtablevirusconnerrcellstyle = 'text-align: center; background-color: %s;' % virusconnerrcolor
summarytablestyle = 'margin-top: 20px; border-collapse: collapse; display: inline-block; float: left; padding-right: 20px;'
summarytableheaderstyle = 'font-size: 12px; text-align: center; background-color: %s; color: %s;' % (summarytableheadercolor, summarytableheadertxtcolor)
summarytableheadercellstyle = 'padding: 6px;'
summarytablecellstyle = 'font-weight: bold; padding: 5px;'
# --------------------------------------------------
# Nothing should need to be modified below this line
# --------------------------------------------------
# Import the required modules
# ---------------------------
import os
import re
import sys
import random
import smtplib
import argparse
import textwrap
from socket import gaierror
from base64 import b64encode
from datetime import datetime
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from configparser import ConfigParser, BasicInterpolation
# Set some variables
# ------------------
progname = 'Bacula Backup Report'
version = '2.30'
reldate = 'July 15, 2024'
progauthor = 'Bill Arlofski'
authoremail = 'waa@revpol.com'
scriptname = 'baculabackupreport.py'
prog_info_txt = progname + ' - v' + version + ' - ' + scriptname \
+ '\nBy: ' + progauthor + ' ' + authoremail + ' (c) ' + reldate + '\n\n'
# Defined the sets and lists of valid choices
# -------------------------------------------
valid_webgui_lst = ['bweb', 'baculum']
valid_webguisvc_lst = ['http', 'https']
bad_job_set = {'A', 'D', 'E', 'f', 'I'}
valid_db_lst = ['pgsql', 'mysql', 'maria', 'sqlite']
all_jobtype_lst = ['B', 'C', 'c', 'D', 'g', 'M', 'R', 'V']
all_jobstatus_lst = ['a', 'A', 'B', 'c', 'C', 'd', 'D', \
'e', 'E', 'f', 'F', 'i', 'I', 'j', \
'm', 'M', 'p', 'R', 's', 'S', 't', 'T']
valid_verified_job_name_col_lst = \
valid_copied_migrated_job_name_col_lst = ['name', 'type', 'both', 'none']
valid_summary_location_lst = ['top', 'bottom', 'both', 'none']
valid_needs_media_since_or_for_lst = ['since', 'for', 'none']
valid_col_lst = [ 'jobid', 'jobname', 'client', 'status', 'joberrors', 'type', 'level', 'jobfiles',
'jobbytes', 'starttime', 'endtime', 'runtime', 'pool', 'fileset', 'storage', 'encrypted']
valid_enc_hdr_type_lst = valid_enc_cell_type_lst = ['text', 'emoji', 'both']
# Lists of strings to determine if a job is waiting on media, and if new media has been found/mounted
# ---------------------------------------------------------------------------------------------------
needs_mount_txt_lst = ['Please mount', 'Please use the "label" command']
got_new_vol_txt_lst = ['New volume', 'Ready to append', 'Ready to read', 'Forward spacing Volume',
'Labeled new Volume', 'Wrote label to ', 'all previous data lost']
# This list is so that we can reliably convert the True/False strings
# from the config file into real booleans to be used in later tests.
# -------------------------------------------------------------------
cfg_file_true_false_lst = ['addsubjecticon', 'addsubjectrunningorcreated', 'bacula_dir_version', 'boldjobname',
'boldstatus', 'chk_pool_use', 'colorstatusbg', 'copied_stats', 'create_job_summary_table',
'create_success_rates_table', 'create_client_ver_lt_dir_table', 'db_version', 'do_not_email_on_all_ok',
'emailvirussummary', 'flagrescheduled', 'include_pnv_jobs', 'migrated_stats',
'print_sent', 'print_subject', 'restore_stats', 'showcopiedto', 'show_db_stats',
'urlifyalljobs', 'verified_stats', 'warn_on_last_good_run', 'warn_on_will_not_descend', 'warn_on_zero_inc']
# Dictionary for the success rate intervals
# -----------------------------------------
success_rates_interval_dict = {'Day': 1, 'Week': 7, 'Month': 30, 'Three Months': 90, 'Six Months': 180, 'Year': 365}
# Initialize the num_virus_conn_errs variable
# -------------------------------------------
num_virus_conn_errs = 0
# The text that is printed in the log when the AV daemon cannot be reached
# Note: For Bacula Enterprise, this string is hard-coded
# ------------------------------------------------------------------------
avconnfailtext = 'Unable to connect to antivirus-plugin-service'
# Set some variables for the Summary stats for the special cases of Copy/Migration Control jobs
# ---------------------------------------------------------------------------------------------
total_copied_files = total_copied_bytes = total_migrated_files = total_migrated_bytes = 0
# Initialize the num_will_not_descend_jobs variable
# -------------------------------------------------
num_will_not_descend_jobs = 0
# Initialize the num_zero_inc_jobs variable
# -----------------------------------------
num_zero_inc_jobs = 0
# Define the argparse arguments, descriptions, defaults, etc
# waa - Something to look into: https://www.reddit.com/r/Python/comments/11hqsbv/i_am_sick_of_writing_argparse_boilerplate_code_so/
# ---------------------------------------------------------------------------------------------------------------------------------
parser = argparse.ArgumentParser(prog=scriptname,
formatter_class=argparse.RawDescriptionHelpFormatter,
description='A highly customizable HTML email report for Bacula environments.',
epilog=textwrap.dedent('''\
Notes:
* Edit variables near the top of script to customize output. Recommended: Use a configuration file instead
* Only the email variable is required. It must be set on the command line, via an environment variable, or in a config file
* Each "--varname" may instead be set using all caps environment variable names like: EMAIL="admin@example.com"
* Variable assignment precedence is: command line > environment variable > config file > script defaults'''))
parser.add_argument('-v', '--version', help='Print the script version.', version=scriptname + " v" + version, action='version')
parser.add_argument('-C', '--config', help='Configuration file.', type=argparse.FileType('r'))
parser.add_argument('-S', '--section', help='Section in configuration file.', default='baculabackupreport')
parser.add_argument('-e', '--email', help='Email address to send job report to.')
parser.add_argument('-s', '--server', help='Name of the Bacula Server.', default='Bacula Report')
parser.add_argument('-t', '--time', help='Time to report on in hours.', default='24')
parser.add_argument('-d', '--days', help='Days to check for "always failing jobs.', default='7')
parser.add_argument('-f', '--fromemail', help='Email address to be set in the From: field of the email. [Default: email]')
parser.add_argument('-a', '--avemail', help='Email address to send separate AV email to. [Default: email]')
parser.add_argument('-c', '--client', help='Client to report on using SQL "LIKE client".', default='%')
parser.add_argument('-j', '--jobname', help='Job name to report on using SQL "LIKE jobname".', default='%')
parser.add_argument('-y', '--jobtype', help='Type of job to report on.', default='DBRCcMgV')
parser.add_argument('-x', '--jobstatus', help='Job status to report on. Note: [R]unning and [C]reated jobs are always included', default='aABcCdDeEfFiIjmMpRsStT')
parser.add_argument('-u', '--smtpuser', help='SMTP user', default='')
parser.add_argument('-p', '--smtppass', help='SMTP password', default='')
parser.add_argument('--dbtype', help='Database type. (pgsql | mysql | maria | sqlite)', choices=valid_db_lst, default='pgsql')
parser.add_argument('--dbhost', help='Database host.', default='localhost')
parser.add_argument('--dbport', help='Database port (defaults: pgsql 5432, mysql & maria 3306).')
parser.add_argument('--dbname', help='Database name. (sqlite default: /opt/bacula/working/bacula.db)', default='bacula')
parser.add_argument('--dbuser', help='Database user.', default='bacula')
parser.add_argument('--dbpass', help='Database password.', default='')
parser.add_argument('--smtpserver', help='SMTP server.', default='localhost')
parser.add_argument('--smtpport', help='SMTP port.', default='25')
args = parser.parse_args()
# Internal CSS to reduce the length of the lines in the email report. Lines over
# 1000 characters are chopped at the 998 character mark per RFC, and this breaks
# the rendering of the HTML in an email client in strange and wonderous ways.
# https://www.w3schools.com/css/css_table_style.asp
# ------------------------------------------------------------------------------
# f-strings require Python version 3.6 or above
# ---------------------------------------------
css_str = f'''
pre {{font-size: {fontsizesumlog};}}
body {{font-family: {fontfamily}; font-size: {fontsize};}}
th {{background-color: {jobtableheadercolor}; color: {jobtableheadertxtcolor};}}
td {{text-align: center; font-size: {fontsizejobinfo}; padding: {jobtablecellpadding};}}
tr:nth-child(even) {{background-color: {jobtablerowevencolor}; color: {jobtableroweventxtcolor};}}
tr:nth-child(odd) {{background-color: {jobtablerowoddcolor}; color: {jobtablerowoddtxtcolor};}}
.proginfo {{font-size: 8px;}}
.bannerwarnings {{display: inline-block; font-size: 13px; font-weight: bold; padding: 2px; margin: 2px 0;}}
.alwaysfail-bannerwarning {{background-color: {alwaysfailcolor};}}
.virus-bannerwarning {{background-color: {virusfoundcolor};}}
.virusconn-bannerwarning {{background-color: {virusconnerrcolor};}}
'''
# Now for some functions
# ----------------------
def now():
'Return the current date/time in human readable format.'
return datetime.today()
def usage():
'Show the instructions and program information.'
print('\n')
parser.print_help()
print('\n' + prog_info_txt)
sys.exit(1)
def cli_vs_env_vs_config_vs_default_vars(short_cli, long_cli):
'Assign/re-assign argparse vars. Order of precedence is cli, env, config file, defaults.'
# If we have a short or long form cli, nothing to do, just return
# ---------------------------------------------------------------
if any(x in sys.argv for x in (short_cli, '--' + str(long_cli))):
return
# If we don't find it on the command line, check environment
# ----------------------------------------------------------
elif long_cli.upper() in os.environ and os.environ[long_cli.upper()] != '':
return os.environ[long_cli.upper()]
# Next place to check is the config file
# --------------------------------------
elif 'config_dict' in globals() and long_cli in config_dict and config_dict[long_cli] != '':
return config_dict[long_cli]
# Just return since the variable will be assigned later
# -----------------------------------------------------
else:
return
def print_opt_errors(opt):
'Print the incorrect variable and the reason it is incorrect.'
if opt == 'config':
return '\nThe config file \'' + config_file + '\' does not exist or is not readable.'
if opt == 'section':
return '\nThe section [' + config_section + '] does not exist in the config file \'' + config_file + '\''
elif opt in ('server', 'dbname', 'dbhost', 'dbuser', 'smtpserver'):
return '\nThe \'' + opt + '\' variable must not be empty.'
elif opt in ('time', 'days', 'smtpport', 'dbport'):
return '\nThe \'' + opt + '\' variable must not be empty and must be an integer.'
elif opt == 'emailnone':
return '\nThe \'email\' variable is empty. Make sure it is assigned via cli, env, or config file'
elif opt in ('email', 'fromemail', 'avemail'):
return '\nThe \'' + opt + '\' variable does not look like a valid email address.'
elif opt == 'dbtype':
return '\nThe \'' + opt + '\' variable must not be empty, and must be one of: ' + ', '.join(valid_db_lst)
elif opt == 'jobtype':
return '\nThe \'' + opt + '\' variable must be one or more of the following characters: ' + ''.join(all_jobtype_lst)
elif opt == 'jobstatus':
return '\nThe \'' + opt + '\' variable must be one or more of the following characters: ' + ''.join(all_jobstatus_lst)
elif opt == 'summary_and_rates':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_summary_location_lst)
elif opt == 'copied_migrated_job_name_col':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_copied_migrated_job_name_col_lst)
elif opt == 'verified_job_name_col':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_verified_job_name_col_lst)
elif opt == 'needs_media_since_or_for':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_needs_media_since_or_for_lst)
elif opt == 'cols2show':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_col_lst)
elif opt == 'enc_hdr_type':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_enc_hdr_type_lst)
elif opt == 'enc_cell_type':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_enc_cell_type_lst)
elif opt == 'webguisvc':
return '\nThe \'' + opt + '\' variable must be one of the following: ' + ', '.join(valid_webguisvc_lst)
elif opt in ('webguiport'):
return '\nThe \'' + opt + '\' variable must be empty or it must be an integer.'
def chk_db_exceptions(err, query=None):
'Given a DB connection exception or SQL query exception, print some useful information and exit.'
# Thanks to the help from this page, I was able to trap any db
# connection or SQL query errors and just report a simple message:
# https://kb.objectrocket.com/postgresql/python-error-handling-with-the-psycopg2-postgresql-adapter-645
# -----------------------------------------------------------------------------------------------------
# Print the type of problem, and the dbname and query if called with a query string
# ---------------------------------------------------------------------------------
if query == None:
print('\nProblem connecting to the database.')
else:
print('\nProblem communicating with database \'' + dbname + '\' while fetching ' + query + '.')
# Get details about the exception
# -------------------------------
err_type, err_obj, traceback = sys.exc_info()
# Get the line number when exception occured
# ------------------------------------------
line_num = traceback.tb_lineno
# Print the Line number and the error
# -----------------------------------
print ('\nError on line number: ' + str(line_num))
print ('ERROR: ' + str(err))
# Print the error code and error exceptions
# -----------------------------------------
if dbtype == 'pgsql':
print ('pgcode: ' + str(err.pgcode) + '\n')
elif dbtype in ('mysql', 'maria'):
# TODO: Need to check if mysql, maria, and
# sqlite3 report some specific error codes
# ----------------------------------------
print('\n')
elif dbtype == 'sqlite':
print('\n')
sys.exit(1)
def db_connect():
'Connect to the db using the appropriate database connector and create the right cursor.'
global conn, cur
if dbtype == 'pgsql':
try:
conn = psycopg2.connect(host=dbhost, port=dbport, dbname=dbname, user=dbuser, password=dbpass)
except OperationalError as err:
chk_db_exceptions(err)
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
elif dbtype in ('mysql', 'maria'):
try:
conn = mysql.connector.connect(host=dbhost, port=dbport, database=dbname, user=dbuser, password=dbpass)
except Exception as err:
chk_db_exceptions(err)
cur = conn.cursor(dictionary=True)
elif dbtype == 'sqlite':
try:
conn = sqlite3.connect(dbname, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
except sqlite3.OperationalError as err:
chk_db_exceptions(err)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
def db_query(query_str, query, one_or_all=None):
'Query the database with the query string provided, text about what is being queried, and an optional "one" string.'
try:
cur.execute(query_str)
# This prevents dealing with nested lists when
# we know we will have only one row returned
# --------------------------------------------
if one_or_all == 'one':
rows = cur.fetchone()
else:
rows = cur.fetchall()
except Exception as err:
chk_db_exceptions(err, query)
return rows
def pn_job_id(ctrl_jobid):
'Return a Previous or New jobid for Copy and Migration Control jobs.'
# Given a Copy Ctrl or Migration Ctrl job's jobid, perform a re.sub
# on the joblog's job summary block of 20+ lines of text and return
# the Prev Backup JobId and New Backup JobId as prev, new
# -----------------------------------------------------------------
prev = re.sub('.*Prev Backup JobId: +(.+?)\n.*', '\\1', ctrl_jobid['logtext'], flags = re.DOTALL)
new = re.sub('.*New Backup JobId: +(.+?)\n.*', '\\1', ctrl_jobid['logtext'], flags = re.DOTALL)
return prev, new
def ctrl_job_files_bytes(cji):
'Return SD Files/Bytes Written for Copy/Migration Control jobs.'
# Given a Copy Ctrl or Migration Ctrl job's jobid, perform a re.sub on
# the joblog's job summary block of 20+ lines of text using a search term
# of "SD Files/Bytes Written:"
# -----------------------------------------------------------------------
files = re.sub('.*SD Files Written: +(.+?)\n.*', '\\1', cji['logtext'], flags = re.DOTALL).replace(',','')
bytes = re.sub('.*SD Bytes Written: +(.+?) .*\n.*', '\\1', cji['logtext'], flags = re.DOTALL).replace(',','')
return files, bytes
def v_job_id(vrfy_jobid):
'Return a Verified jobid for Verify jobs.'
# Given a Verify job's jobid, perform a re.sub on the joblog's
# job summary block of 20+ lines of text using a search term of
# 'Verify JobId:' and return the jobid of the job it verified
# -------------------------------------------------------------
return re.sub('.*Verify JobId: +(.+?)\n.*', '\\1', vrfy_jobid['logtext'], flags = re.DOTALL)
def get_verify_client_info(vrfy_jobid):
'Given a Verify Jobid, return the JobId, Client name, and Job Name of the jobid that was verified.'
# This was originally for the anti-virus feature, but is now
# also used to show the jobname of the job a Verify Job verified
# --------------------------------------------------------------
if [r['jobstatus'] for r in filteredjobsrows if r['jobid'] == vrfy_jobid][0] == 'C':
return '', '', 'No Info Yet'
elif [r['jobstatus'] for r in filteredjobsrows if r['jobid'] == vrfy_jobid][0] in ('A', 'E', 'f', 'R'):
if dbtype in ('pgsql', 'sqlite'):
query_str = "SELECT logtext \
FROM log WHERE jobid='" + str(vrfy_jobid) + "' \
AND logtext LIKE '%Verifying against JobId=%' \
ORDER BY time DESC LIMIT 1;"
elif dbtype in ('mysql', 'maria'):
query_str = "SELECT CAST(logtext as CHAR(2000)) AS logtext \
FROM Log WHERE jobid='" + str(vrfy_jobid) + "' \
AND logtext LIKE '%Verifying against JobId=%' \
ORDER BY time DESC LIMIT 1;"
row = db_query(query_str, 'the Job name (from log table) of a jobid that was verified', 'one')
if row == None or len(row) == 0:
return '', '', 'No Info'
else:
return '', '', re.sub(r'.*Verifying against JobId=.* Job=(.+?)\.[0-9]{4}-[0-9]{2}-[0-9]{2}_.*', '\\1', row['logtext'], flags=re.DOTALL)
else:
if dbtype in ('pgsql', 'sqlite'):
query_str = "SELECT JobId, Client.Name AS Client, Job.Name AS JobName \
FROM Job \
INNER JOIN Client ON Job.ClientID=Client.ClientID \
WHERE JobId='" + v_jobids_dict[str(vrfy_jobid)] + "';"
elif dbtype in ('mysql', 'maria'):
query_str = "SELECT jobid, CAST(Client.name as CHAR(50)) AS client, \
CAST(Job.name as CHAR(50)) AS jobname \
FROM Job \
INNER JOIN Client ON Job.clientid=Client.clientid \
WHERE jobid='" + v_jobids_dict[str(vrfy_jobid)] + "';"
row = db_query(query_str, 'the JobId, Client Name, and Job Name of a job that was verified')
if len(row) == 0:
# If the verified job is no longer in
# the catlog return ('0', '0', '0')
# -----------------------------------
return '0', '0', '0'
else:
return row[0][str('jobid')], row[0]['client'], row[0]['jobname']
def get_copied_migrated_job_name(copy_migrate_jobid):
'Given a Copy/Migration Control Jobid, return the Job name of the jobid that was copied/migrated.'
if [r['jobstatus'] for r in filteredjobsrows if r['jobid'] == copy_migrate_jobid][0] == 'C':
return 'No Info Yet'
# If the job is aborted/running/failed, there may be
# no Job Summary, so let's see if we can scrape some info
# about the job name being copied/migrated from the Log table
# -----------------------------------------------------------
elif [r['jobstatus'] for r in filteredjobsrows if r['jobid'] == copy_migrate_jobid][0] in ('A', 'E', 'f', 'R'):
if dbtype in ('pgsql', 'sqlite'):
query_str = "SELECT logtext \
FROM log WHERE jobid='" + str(copy_migrate_jobid) + "' \
AND (logtext LIKE '%Copying using JobId=%' OR logtext LIKE '%Migration using JobId=%') \
ORDER BY time DESC LIMIT 1;"
elif dbtype in ('mysql', 'maria'):
query_str = "SELECT CAST(logtext as CHAR(2000)) AS logtext \
FROM Log WHERE jobid='" + str(copy_migrate_jobid) + "' \
AND (logtext LIKE '%Copying using JobId=%' OR logtext LIKE '%Migration using JobId=%') \
ORDER BY time DESC LIMIT 1;"
row = db_query(query_str, 'the Job name (from log table) of a jobid that was copied/migrated', 'one')
if row != None and len(row)!= 0:
# If a JobName was returned from the query
# return it, otherwise return 'No Info'
# ----------------------------------------
return re.sub(r'.*[Copying\|Migration] using JobId=.* Job=(.+?)\.[0-9]{4}-[0-9]{2}-[0-9]{2}_.*', '\\1', row['logtext'], flags=re.DOTALL)
else:
# This is for when a Copy/Migration control job is canceled due to:
# Fatal error: JobId 47454 already running. Duplicate job not allowed.
# ------------------------------------------------------------------------
return 'No Info'
else:
# If the jobstatus is not one of the above,
# query the Job table to get the jobname
# -----------------------------------------
if dbtype in ('pgsql', 'sqlite'):
query_str = "SELECT Job.Name AS JobName \
FROM Job \
WHERE JobId='" + pn_jobids_dict[str(copy_migrate_jobid)][0] + "';"
elif dbtype in ('mysql', 'maria'):
query_str = "SELECT CAST(Job.name as CHAR(50)) AS jobname \
FROM Job \
WHERE jobid='" + pn_jobids_dict[str(copy_migrate_jobid)][0] + "';"
row = db_query(query_str, 'the Job name of a jobid (from Job table) that was copied/migrated')
if len(row) != 0:
# If a JobName was returned from the query, return it,
# else if the (copied/migrated) jobid != '0' return
# 'JobId xx not in catalog', otherwise just return
# ----------------------------------------------------
return row[0]['jobname']
elif pn_jobids_dict[str(copy_migrate_jobid)][0] != '0':
return 'JobID ' + pn_jobids_dict[str(copy_migrate_jobid)][0] + ' not in catalog'
else:
return
def copied_ids(jobid):
'For a given Backup or Migration job, return a list of jobids that it was copied to.'
# TODO: 20220407 - Need to also consider Copied jobs that get copied
# ------------------------------------------------------------------
copied_jobids=[]
for t in pn_jobids_dict:
# Make sure that only copy jobids are listed, not the jobid it was migrated to
# ----------------------------------------------------------------------------
if pn_jobids_dict[t][0] == str(jobid):
if jobrow['type'] == 'B' or (jobrow['type'] == 'M' and pn_jobids_dict[t][1] != migrated_id(jobid)):
if pn_jobids_dict[t][1] != '0':
# This ^^ prevents ['0'] from being returned, causing "Copied to 0"
# in report. This happens when a Copy job finds a Backup/Migration
# job to copy, but reports "there no files in the job to copy"
# -----------------------------------------------------------------
copied_jobids.append(pn_jobids_dict[t][1])
if len(copied_jobids) == 0:
return '0'
else:
return copied_jobids
def migrated_id(jobid):
'For a given Migrated job, return the jobid that it was migrated to.'
for t in pn_jobids_dict:
if pn_jobids_dict[t][0] == str(jobid):
return pn_jobids_dict[t][1]
def copied_ids_str(jobid):
'For a given jobid, return a comma separated string of jobids, urlified if "webgui" is enabled.'
copied_ids_lst = []
for id in copied_ids(jobid):
copied_ids_lst.append((urlify_jobid(id) if gui and urlifyalljobs else id))
return ','.join(copied_ids_lst)
def translate_job_type(jobtype, jobid, priorjobid):
'Job type is stored in the catalog as a single character. Do some special things for Backup, Copy, and Migration jobs.'
if jobtype == 'C' and priorjobid != '0':
return 'Copy of ' \
+ (urlify_jobid(str(priorjobid)) if gui and urlifyalljobs else str(priorjobid))
if jobtype == 'B' and priorjobid != 0:
# This catches the corner case where Copy/Migration
# control jobs have run, but they copied or migrated
# no jobs so pn_jobids_dict will not exist
# --------------------------------------------------
if 'pn_jobids_dict' in globals() and len(copied_ids(jobid)) != 0:
if 'pn_jobids_dict' in globals() and showcopiedto:
if copied_ids(jobid) != '0':
return 'Migrated from ' \
+ (urlify_jobid(str(priorjobid)) if gui and urlifyalljobs else str(priorjobid)) \
+ '<br>Copied to ' \
+ copied_ids_str(jobid) + '\n'
return 'Migrated from ' \
+ (urlify_jobid(str(priorjobid)) if gui and urlifyalljobs else str(priorjobid))
if jobtype == 'B':
if 'pn_jobids_dict' in globals() and len(copied_ids(jobid)) != 0:
if 'pn_jobids_dict' in globals() and showcopiedto:
if copied_ids(jobid) != '0':
return 'Backup<br>Copied to ' + copied_ids_str(jobid) + '\n'
return 'Backup'
if jobtype == 'M':
# Part of this is a workaround for what I consider to be a bug in Bacula for jobs of
# type 'B' which meet the criteria to be 'eligible' for migration, but have 0 files/bytes
# The original backup Job's type gets changed from 'B' (Backup) to 'M' (Migrated), even
# though nothing is migrated and there is no other Backup job that has a priorjobid
# which points back to this Migrated job. https://bugs.bacula.org/view.php?id=2619
# ---------------------------------------------------------------------------------------
if 'pn_jobids_dict' in globals() and migrated_id(jobid) != '0':
if copied_ids(jobid) != '0':
return 'Migrated to ' \
+ (urlify_jobid(str(migrated_id(jobid))) if gui and urlifyalljobs else str(migrated_id(jobid))) \
+ '<br>Copied to ' + copied_ids_str(jobid) + '\n'
else:
return 'Migrated to ' \
+ (urlify_jobid(str(migrated_id(jobid))) if gui and urlifyalljobs else str(migrated_id(jobid)))
elif 'pn_jobids_dict' in globals() and migrated_id(jobid) == '0':
return 'Migrated (No data to migrate)'
else:
return 'Migrated'
if jobtype == 'c':
if jobrow['jobstatus'] in ('C', 'R'):
return 'Copy Ctrl:' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if jobrow['jobstatus'] in bad_job_set:
return 'Copy Ctrl: Failed' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if pn_jobids_dict[str(jobid)][1] == '0':
if pn_jobids_dict[str(jobid)][0] != '0':
return 'Copy Ctrl: ' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][0]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][0]) \
+ ' (No files to copy)' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
else:
return 'Copy Ctrl: No jobs to copy'
else:
return 'Copy Ctrl:\n' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][0]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][0]) \
+ '->' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][1]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][1]) \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts \
+ ';">(' + get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if jobtype == 'g':
if jobrow['jobstatus'] in ('C', 'R'):
return 'Migration Ctrl:' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if jobrow['jobstatus'] in bad_job_set:
return 'Migration Ctrl: Failed' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if pn_jobids_dict[str(jobid)][1] == '0':
if pn_jobids_dict[str(jobid)][0] != '0':
return 'Migration Ctrl: ' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][0]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][0]) \
+ ' (No data to migrate)' \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')<span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
else:
return 'Migration Ctrl: No jobs to migrate'
else:
return 'Migration Ctrl:\n' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][0]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][0]) \
+ '->' \
+ (urlify_jobid(pn_jobids_dict[str(jobid)][1]) if gui and urlifyalljobs else pn_jobids_dict[str(jobid)][1]) \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' \
+ get_copied_migrated_job_name(jobrow['jobid']) + ')</span>' \
if copied_migrated_job_name_col in ('type', 'both') else '')
if jobtype == 'V':
# TODO: I want to be able to use this simple 'if' test, but can't until I fix the TODO below
# if jobrow['jobstatus'] in ('C', 'R') and v_jobids_dict[str(jobid)] == '0':
# ------------------------------------------------------------------------------------------
if jobrow['jobstatus'] in ('C', 'R') and jobid not in v_jobids_dict:
return 'Verify of n/a' + ('<br><span style="font-size: ' + fontsize_addtional_texts \
+ ';">(No Info Yet)</span>' if verified_job_name_col in ('type', 'both') else '')
# TODO: See related TODO on or near line 1959 Need to fix this! In
# this temporary workaround, I am returning the same exact thing
# for two different if/elif tests. Basically, we cannot include
# this 'if' in the above one because the jobid will not be in the
# v_jobids_dict for Jobs with jobstatus of 'C' and the script will
# fail with a keyerror.
# ----------------------------------------------------------------
elif jobrow['jobstatus'] in ('C', 'R') and str(jobid) in v_jobids_dict and v_jobids_dict[str(jobid)] == '0':
return 'Verify of n/a' + ('<br><span style="font-size: ' + fontsize_addtional_texts \
+ ';">(No Info Yet)</span>' if verified_job_name_col in ('type', 'both') else '')
elif str(jobid) in v_jobids_dict and v_jobids_dict[str(jobid)] == '0':
return 'Verify of n/a' + ('<br><span style="font-size: ' + fontsize_addtional_texts \
+ ';">(No Info)</span>' if verified_job_name_col in ('type', 'both') else '')
else:
if str(jobid) in v_jobids_dict.keys():
if 'virus_dict' in globals() and jobid in virus_dict:
virus_found_str = ' (' + str(len(virus_dict[jobid])) + ' ' + virusfoundbodyicon + ')'
else:
virus_found_str = ''
return 'Verify of ' \
+ (urlify_jobid(v_jobids_dict[str(jobid)]) if gui and urlifyalljobs else v_jobids_dict[str(jobid)]) \
+ virus_found_str \
+ ('<br><span style="font-size: ' + fontsize_addtional_texts + ';">(' + (get_verify_client_info(jobrow['jobid'])[2] \
if get_verify_client_info(jobrow['jobid'])[2] != '0' \
else 'Job not in catalog') + ')</span>' \
if verified_job_name_col in ('type', 'both') else '')
# Catchall for the last two Job types
# -----------------------------------
return {'D': 'Admin', 'R': 'Restore'}[jobtype]
def translate_job_status(jobstatus, joberrors):
'jobstatus is stored in the catalog as a single character, replace with words.'
if jobstatus == 'A':
return 'Canceled'
elif jobstatus == 'C':
return 'Created'
elif jobstatus == 'D':
return 'Verify Diffs'
elif jobstatus == 'E':
return 'Errors'
elif jobstatus == 'f':
return 'Failed'
elif jobstatus == 'I':
return 'Incomplete'
elif jobstatus == 'T':
if joberrors > 0 or (warn_on_zero_inc and zero_inc):
return 'OK/Warnings'
elif warn_on_will_not_descend and will_not_descend:
return 'OK/Warnings<br><span style="font-size: ' + fontsize_addtional_texts + ';">(will not descend)</span>'
else:
return 'OK'
elif jobstatus == 'R':
if needs_media_since_or_for != 'none' and 'job_needs_opr_dict' in globals() and str(jobrow['jobid']) in job_needs_opr_dict:
return 'Needs Media<br><span style="font-size: ' + fontsize_addtional_texts + ';">' + job_needs_opr_dict[str(jobrow['jobid'])] + '</span>'
elif 'job_needs_opr_dict' in globals() and str(jobrow['jobid']) in job_needs_opr_dict:
return 'Needs Media'
else:
return 'Running'
def set_subject_icon():
'Set the utf-8 subject icon(s).'
if numfilteredjobs == 0:
subjecticon = nojobsicon
else:
if numbadjobs != 0:
if len(always_fail_jobs) != 0:
subjecticon = alwaysfailjobsicon
else:
subjecticon = badjobsicon
elif jobswitherrors != 0 or (warn_on_will_not_descend and num_will_not_descend_jobs > 0) or (warn_on_zero_inc and num_zero_inc_jobs > 0):
subjecticon = warnjobsicon
else:
subjecticon = goodjobsicon
if 'num_virus_jobs' in globals() and num_virus_jobs != 0:
subjecticon += ' ' + virusfoundicon
if 'job_needs_opr_dict' in globals() and len(job_needs_opr_dict) != 0:
subjecticon += ' (' + jobneedsopricon + ')'
return subjecticon
def translate_job_level(joblevel, jobtype):
'Job level is stored in the catalog as a single character, replace with a string.'
# No real level for these job types
# ---------------------------------
if jobtype in ('D', 'R', 'g', 'c'):
return '----'
else:
return {' ': '----', '-': 'Base', 'A': 'Data', 'C': 'VCat', 'd': 'VD2C',
'D': 'Diff', 'f': 'VFull', 'F': 'Full', 'I': 'Inc', 'O': 'VV2C', 'V': 'Init'}[joblevel]
def urlify_jobid(content):
'Given a jobid, wrap it in HTML to link it to the job log in the specified webgui.'
if webgui == 'bweb':
return '<a href="' + webguisvc + '://' + webguihost + ':' \
+ webguiport + '/cgi-bin/bweb/bweb.pl?action=job_zoom&jobid=' \
+ str(content) + '">' + str(content) + '</a>'
elif webgui == 'baculum':
return '<a href="' + webguisvc + '://' + webguihost + ':' \
+ webguiport + '/web/job/history/' + str(content) + '">' \
+ str(content) + '</a>'
else:
return content
def html_format_cell(content, bgcolor = '', star = '', col = '', jobtype = ''):
'Format/modify some table cells based on settings and conditions.'
# Set default tdo and tdc to wrap each cell
# -----------------------------------------
tdo = '<td>'
tdc = '</td>'
# Colorize the Status cell?
# Even if yes, don't override the table
# row bgcolor if alwaysfailcolumn is 'row'
# ----------------------------------------
if not (alwaysfailjob and alwaysfailcolumn == 'row'):
if colorstatusbg and col == 'status':
if jobrow['jobstatus'] == 'C':
bgcolor = createdjobcolor
elif jobrow['jobstatus'] == 'E':
bgcolor = errorjobcolor
elif jobrow['jobstatus'] == 'T':
if jobrow['joberrors'] == 0:
if not warn_on_will_not_descend and not warn_on_zero_inc:
bgcolor = goodjobcolor
elif warn_on_will_not_descend and will_not_descend:
bgcolor = warnjobcolor
elif warn_on_zero_inc and zero_inc:
bgcolor = warnjobcolor
else:
bgcolor = goodjobcolor
else:
bgcolor = warnjobcolor
elif jobrow['jobstatus'] in bad_job_set:
bgcolor = badjobcolor
elif jobrow['jobstatus'] == 'R':
bgcolor = runningjobcolor
elif jobrow['jobstatus'] == 'I':