forked from chriselswede/hanacleaner
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhanacleaner.py
1732 lines (1664 loc) · 129 KB
/
hanacleaner.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 python
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import sys, os, time, subprocess, re
from difflib import Differ
def printHelp():
print(" ")
print("DESCRIPTION: ")
print(" The HANA cleaner is a house keeping service for SAP HANA. It can be used to clean the backup catalog, diagnostic files, ")
print(" and alerts and to compress the backup logs. It should be executed by <sid>adm or, in case you use a CRON job, with the same ")
print(" environment as the <sid>adm. See SAP Note 2399996 and SAP Note 2400024. ")
print(" ")
print("INPUT ARGUMENTS: ")
print(" ---- BACKUP ENTRIES in BACKUP CATALOG (and possibly BACKUPS) ---- ")
print(" -be minimum retained number of data backup (i.e. complete data backups and data snapshots) entries in the catalog, this ")
print(" number of entries of data backups will remain in the backup catalog, all older log backup entries will also be removed ")
print(" with BACKUP CATALOG DELETE BACKUP_ID <id> (see SQL reference for more info) default: -1 (not used) ")
print(" -bd min retained days of data backup (i.e. complete data backups and data snapshots) entries in the catalog [days], the ")
print(" youngest successful data backup entry in the backup catalog that is older than this number of days is the oldest ")
print(" successful data backup entry not removed from the backup catalog, default -1 (not used) ")
print(" Note: if both -be and -bd is used, the most conservative, i.e. the flag that removes the least number entries, decides ")
print(" Note: As mentioned in SAP Note 1812057 backup entries made via backint cannot be recovered, i.e. use -be and -bd with care")
print(" if you want to be able to recover from older data backups (it is possible to recover from a specific data backup without ")
print(" the backup catalog) ")
print(" -bb delete backups also [true/false], backups are deleted when the related backup catalog entries are deleted with ")
print(" BACKUP CATALOG DELETE BACKUP_ID <id> COMPLETE (see SQL reference for more info), default: false ")
print(" -bo output catalog [true/false], displays backup catalog before and after the cleanup, default: false ")
print(" -br output removed catalog entries [true/false], displays backup catalog entries that were removed, default: false ")
print(" Note: Please do not use -bo and -br if your catalog is huge (>10000) entries. ")
print(" ---- TRACE FILES ---- ")
print(" -tc retention days for trace files [days], trace files with their latest modification time older than these number of days are") #internal incident 1870190781
print(" removed from all hosts, default: -1 (not used) ")
print(" Note: Conceptual -tc is the same as -tf, but -tc is using ALTER SYSTEM CLEAR TRACES ... See SQL Ref. for more info. ")
print(" Note: there is a bug (fixed with rev.122.11) that could cause younger trace files to be removed. ")
print(" Note: if [expensive_statement] --> use_in_memory_tracking = true HANA will automatically flush expensive statements into ")
print(" memory before deleting the *.expensive_statements.*.trc files, i.e. to keep consistency some clean-up might not work")
print(" -tf retention days for trace files [days], trace files, in all hosts, that are older than this number of days are removed ")
print(" (except for the currently opened trace files), only files with certain extensions like .trc, .log etc are taken into ")
print(" account, backup.log and backint.log, are excepted, please see -zb and -zp instead, default: -1 (not used) ")
print(" Note: Conceptual -tf is the same as -tc, but -tf is using ALTER SYSTEM REMOVE TRACES ... See SQL Ref. for more info. ")
print(" -to output traces [true/false], displays trace files before and after the cleanup, default: false ")
print(" -td output deleted traces [true/false], displays trace files that were deleted, default: false ")
print(" ---- DUMP FILES ---- ")
print(" -dr retention days for dump files [days], manually created dump files (a.k.a. fullysytem dumps and runtime dumps) that are ")
print(" older than this number of days are removed, default: -1 (not used) ")
print(" ---- ANY FILES ---- ")
print(" -gr retention days for any general file [days], files in the directory specified with -gd and with the file names including ")
print(" the word specified with -gw are only saved for this number of days, default: -1 (not used) ")
print(" Note: -gd and -gw can also be same length lists with a commas as delimiter ")
print(" -gd directories for general files to be deleted, a comma separated list with full paths of directories with files to be ")
print(' deleted according to -gr (entries pairs with entries in -gw), default "" (not used) ')
print(" Note: if you include %SID, it will automatically be replaced with the actually SID of your system ")
print(" -gw filename parts for general files to be deleted, a comma separated list with words that files should have in their names ")
print(' to be deleted according to -gr (entries pairs with entries in -gd), default "" (not used) ')
print(" ---- BACKUP LOGS <H2SPS04 ---- ")
print(" -zb backup logs compression size limit [mb], if there are any backup.log or backint.log file (see -zp below) that is bigger ")
print(" than this size limit, then it is compressed and renamed, default: -1 (not used) ")
print(" Note: if -tf flag is used the resulting zip file could be removed by it. ")
print(" Note: Don't use this with version HANA 2 SPS04 or later, instead configure size with parameters, see SAP Note 2797078 ")
print(" -zp zip path, specifies the path (and all subdirectories) where to look for the backup.log and backint.log files, ")
print(" default is the directory specified by the alias cdtrace ")
print(" -zl zip links [true/false], specifies if symbolic links should be followed searching for backup logs in subdirectories ")
print(" of the directory defined by zp (or by alias cdtrace), default: false ")
print(" -zo print zipped backup logs, display the backup.log and backint.log that were zipped, default: false ")
print(" -zk keep zip, if this is set to false the zip file is deleted (use with care!), default: true ")
print(" ---- ALERTS ---- ")
print(" -ar min retained alerts days [days], min age (today not included) of retained statistics server alerts, default: -1 (not used)")
print(" -ao output alerts [true/false], displays statistics server alerts before and after the cleanup, default: false ")
print(" -ad output deleted alerts [true/false], displays statistics server alerts that were deleted, default: false ")
print(" ---- OBJECT LOCKS ENTRIES with UNKOWN OBJECT NAME ---- ")
print(" -kr min retained unknown object lock days [days], min age (today not included) of retained object lock entries with unknown ")
print(" object name, in accordance with SAP Note 2147247, default: -1 (not used) ")
print(" ---- OBJECT HISTORY ---- ")
print(" -om object history table max size [mb], if the table _SYS_REPO.OBJECT_HISTORY is bigger than this threshold this table ")
print(" will be cleaned up according to SAP Note 2479702, default: -1 (not used) ")
print(" -oo output cleaned memory from object table [true/false], displays how much memory was cleaned up from object history ")
print(" table, default: false ")
print(" ---- LOG SEGMENTS ---- ")
print(" -lr max free logsegments per service [number logsegments], if more free logsegments exist for a service the statement ")
print(" ALTER SYSTEM RECLAIM LOG is executed, default: -1 (not used) ")
print(" ---- EVENTS ---- ")
print(" -eh min retained days for handled events [day], minimum retained days for the handled events, handled events that are older ")
print(" are removed by first being acknowledged and then deleted, this is done for all hosts, default: -1 (not used) ")
print(" -eu min retained days for unhandled events [day], minimum retained days for events, events that are older are removed by ")
print(" first being handled and acknowledged and then deleted, this is done for all hosts, default: -1 (not used) ")
print(" ---- AUDIT LOG ---- ")
print(" -ur retention days for audit log table [days], audit log content older than these number of days is removed, ")
print(" default: -1 (not used) ")
print(" ---- DATA VOLUMES FRAGMENTATION ---- ")
print(" -fl fragmentation limit [%], maximum fragmentation of data volume files, of any service, before defragmentation of that ")
print(" service is started: ALTER SYSTEM RECLAIM DATAVOLUME '<host>:<port>’ 120 DEFRAGMENT, default: -1 (not used) ")
print(" Note: If you use System Replication see Q19 in SAP Note 1999880. ")
print(" -fo output fragmentation [true/false], displays data volume statistics before and after defragmentation, default: false ")
print(" ---- MULTIPLE ROW STORE TABLE CONTAINERS ---- ")
print(" -rc row store containers cleanup [true/false], switch to clean up multiple row store table containers, default: false ")
print(" Note: Unfortunately there is NO nice way to give privileges to the DB User to be allowed to do this. Either you can ")
print(" run hanacleaner as SYSTEM user (NOT recommended) or grant DATA ADMIN to the user (NOT recommended) ")
print(" -ro output row containers [true/false], displays row store tables with more than one container before cleanup, default: false ")
print(" ---- COMPRESSION OPTIMIZATION ---- ")
print(" 1. Both following two flags, -cc, and -ce, must be > 0 to control the force compression optimization on tables that never ")
print(" was compression re-optimized (i.e. last_compressed_record_count = 0): ")
print(" -cc max allowed raw main records, if table has more raw main rows --> compress if -ce, default: -1 (not used) e.g. 10000000 ")
print(" -ce max allowed estimated size [GB], if estimated size is larger --> compress if -cc, default: -1 (not used) e.g. 1 ")
print(" 2. All following three flags, -cr, -cs, and -cd, must be > 0 to control the force compression optimization on tables with ")
print(" columns with compression type 'DEFAULT' (i.e. no additional compression algorithm in main) ")
print(" -cr max allowed rows, if a column has more rows --> compress if -cs&-cd, default: -1 (not used) e.g. 10000000 ")
print(" -cs max allowed size [MB], if a column is larger --> compress if -cr&-cd, default: -1 (not used) e.g. 500 ")
print(" -cd min allowed distinct count [%], if a column has less distinct quota --> compress if -cr&-cs, default -1 (not used) e.g. 5 ")
print(" 3. Both following two flags, -cu and -cq, must be > 0 to control the force compression optimization on tables whose UDIV ")
print(" quota is too large, i.e. #UDIVs/(#raw main + #raw delta) ")
print(" -cq max allowed UDIV quota [%], if the table has larger UDIV quota --> compress if -cu, default: -1 (not used) e.g. 150 ")
print(" -cu max allowed UDIVs, if a column has more then this number UDIVs --> compress if -cq, default: -1 (not used) e.g. 10000000 ")
print(" 4. Flag -cb must be > 0 to control the force compression optimization on tables with columns with SPARSE (<122.02) or ")
print(" PREFIXED and a BLOCK index ")
print(" -cb max allowed rows, if a column has more rows and a BLOCK index and SPARSE (<122.02) or PREFIXED then this table should ")
print(" be compression re-optimized, default -1 (not used) e.g. 100000 ")
print(" Following three flags are general; they control all three, 1., 2., 3., 4., compression optimization possibilities above ")
print(" -cp per partition [true/false], switch to consider flags above per partition instead of per column, default: false ")
print(" -cm merge before compress [true/false], switch to perform a delta merge on the tables before compression, default: false ")
print(" -co output compressed tables [true/false], switch to print all tables that were compression re-optimized, default: false ")
print(" ---- VIRTUAL TABLE STATISTICS ---- ")
print(" -vs create statistics for virtual tables [true/false], switch to create optimization statistics for those virtual tables ")
print(" that are missing statistics according to SAP Note 1872652 (Note: could cause expenive operations), default: false ")
print(" -vl schema list of virtual tables, if you only want tables in some schemas to be considered for the creation of statistics ")
print(" provide here a comma seperated list of those schemas, default '' (all schemas will be considered) ")
print(" ---- INIFILE CONTENT HISTORY ---- ")
print(" -ir inifile content history retention [days], deletes older inifile content history, default: -1 (not used) (should > 1 year) ")
print(" ---- INTERVALL ---- ")
print(" -hci hana cleaner interval [days], number days that hanacleaner waits before it restarts, default: -1 (exits after 1 cycle) ")
print(" NOTE: Do NOT use if you run hanacleaner in a cron job! ")
print(" ---- INPUT ---- ")
print(" -ff flag file, full path to a file that contains input flags, each flag in a new line, all lines in the file that does not ")
print(" start with a flag are considered comments, if this flag is used no other flags should be given, default: '' (not used) ")
print(" Note: if you include %SID in the path, it will automatically be replaced with the actually SID of your system ")
print(" ---- EXECUTE ---- ")
print(" -es execute sql [true/false], execute all crucial housekeeping tasks (useful to turn off for investigation with -os=true), ")
print(" default: true ")
print(" ---- OUTPUT ---- ")
print(" -os output sql [true/false], prints all crucial housekeeping tasks (useful for debugging with -es=false), default: false ")
print(" -op output path, full literal path of the folder for the output logs (will be created if not there), default = '' (not used) ")
print(" Note: if you include %SID in the output path, it will automatically be replaced with the actually SID of your system ")
print(" -or output retention days, logs in the path specified with -op are only saved for this number of days, default: -1 (not used) ")
print(" -so standard out switch [true/false], switch to write to standard out, default: true ")
print(" ---- SERVER FULL CHECK ---- ")
print(" -fs file system, path to server to check for disk full situation before hanacleaner runs, default: blank, i.e. df -h is used ")
print(' Could also be used to specify a couple of servers with e.g. -fs "|grep sapmnt" ')
print(" -if ignore filesystems and mounts, before hanacleaner starts it checks that there is no disk full situation in any of the ")
print(" filesystems and/or mounts, this flag makes it possible to ignore some filesystems, with comma seperated list, from the ")
print(" df -h command (filesystems are in the first column and mounts normally in the 5th or 6th column), default: '' ")
print(" -df filesystem check switch [true/false], it is possible to completely ignore the filesystem check (necessary if non-ascii ")
print(" comes out from df -h). However, hanacleaner is NOT supported in case of full filesystem so if you turn this to false ")
print(" it is necessary that you check for disk full situation manually! default: true ")
print(" ---- SSL ---- ")
print(" -ssl turns on ssl certificate [true/false], makes it possible to use SAP HANA Cleaner despite SSL, default: false ")
print(" ---- HOST ---- ")
print(" -vlh virtual local host, if hanacleaner runs on a virtual host this has to be specified, default: '' (physical host is assumed)")
print(" ---- USER KEY ---- ")
print(" -k DB user key, this one has to be maintained in hdbuserstore, i.e. as <sid>adm do ")
print(" > hdbuserstore SET <DB USER KEY> <ENV> <USERNAME> <PASSWORD> , default: SYSTEMKEY ")
print(" It could also be a list of comma seperated userkeys (useful in MDC environments), e.g.: SYSTEMKEY,TENANT1KEY,TENANT2KEY ")
print(" Note: It is not possible to use underscore in the user key, e.g. HANA_HOUSEKEEPING is NOT possible ")
print(" -dbs DB key, this can be a list of databases accessed from the system defined by -k (-k can only be one key if -dbs is used) ")
print(" Note: Users with same name and password have to be maintained in all databases , default: '' (not used) ")
print(" Example: -k PQLSYSDB -dbs SYSTEMDB, PQL ")
print(" ---- EMAIL ---- ")
print(" -en email notification, <recievers email>,<sender's email>,<mail server> ")
print(" example: -en you@ourcompany,[email protected],smtp.intra.ourcompany.com ")
print(' NOTE: For this to work you have to install the linux program "sendmail" and add a line similar to ')
print(" DSsmtp.intra.ourcompany.com in the file sendmail.cf in /etc/mail/, see ")
print(" https://www.systutorials.com/5167/sending-email-using-mailx-in-linux-through-internal-smtp/ ")
print(" ")
print(" ")
print("EXAMPLE (trace files, statistics server alerts and backup catalog entries, i.e. not the backups themselves, older than 42 days ")
print(" are deleted and backup logs bigger than 50 mb are compressed and renamed and logsegments a removed if more than 20 ")
print(" free once exist for a service): ")
print(" ")
print(" > python hanacleaner.py -tc 42 -tf 42 -ar 42 -bd 42 -zb 50 -lr 20 -eh 2 -eu 42 ")
print(" ")
print(" ")
print("EXAMPLE (reads a configuration file, one flag will overwrite, i.e. retention time for the alerts will be 200 days instead of 42): ")
print(" > python hanacleaner.py -ff /tmp/HANACleaner/hanacleaner_configfile.txt -ar 200 ")
print(" Where the config file could look like this: ")
print(" MY HANACLEANER CONFIGURATION FILE ")
print(" Oldest content of the trace files should only be 42 days old ")
print(" -tc 42 ")
print(" Oldest trace file should only be 42 days old ")
print(" -tf 42 ")
print(" Oldest alerts should only be 42 days old ")
print(" -ar 42 ")
print(" This is the key in hdbuserstore that is used: ")
print(" -k SYSTEMKEY ")
print(" ")
print("CURRENT KNOWN LIMITATIONS (i.e. TODO LIST): ")
print(" 1. HANACleaner should notice if HANA is not listening to SQL or only readable and then ")
print(" sleep for a while and test if this HANA becomes primary now and then --> useful in case of HSR ")
print(" 2. Allow granular control on minutes instead of days ")
print(" 3. Allow compression on trace files as well not only on backup related files ")
print(" 4. Allow a two steps cleanup for general files, e.g. compress file older than a few hours and delete files older than some days ")
print(" 5. Check for multiple definitions of one flag, give ERROR, and STOP ")
print(" ")
print("AUTHOR: Christian Hansen ")
print(" ")
print(" ")
os._exit(1)
def printDisclaimer():
print(" ")
print("ANY USAGE OF HANACLEANER ASSUMES THAT YOU HAVE UNDERSTOOD AND AGREED THAT: ")
print(" 1. HANACleaner is NOT SAP official software, so normal SAP support of HANACleaner cannot be assumed ")
print(" 2. HANACleaner is open source ")
print(' 3. HANACleaner is provided "as is" ')
print(' 4. HANACleaner is to be used on "your own risk" ')
print(" 5. HANACleaner is a one-man's hobby (developed, maintained and supported only during non-working hours) ")
print(" 6 All HANACleaner documentations have to be read and understood before any usage: ")
print(" a) SAP Note 2399996 ")
print(" b) The .pdf file that can be downloaded at the bottom of SAP Note 2399996 ")
print(" c) All output from executing ")
print(" python hanacleaner.py --help ")
print(" 7. HANACleaner can help you execute certain SAP HANA tasks automatically but is NOT an attempt to teach you SAP HANA ")
print(" Therefore it is assumed that you understand all SQL statements that HANACleaner does to make changes in your system ")
print(" To find out what crucial SQL statements HANACleaner will do without actually executing them run with the additional flags ")
print(" -es false -os true ")
print(' To then learn what those statements do before you executing HANACleaner without "-es false", see SAP HANA Admin Guide or ')
print(" SAP HANA System Administration Workshops ")
print(" 8. HANACleaner is not providing any recommendations, all flags shown in the documentation (see point 6.) are only examples ")
print(" For recommendations see SAP HANA Administration Workshops or other documentation, like e.g. SAP Note 2400024 ")
os._exit(1)
######################## CLASS DEFINITIONS ################################
class SQLManager:
def __init__(self, execute_sql, hdbsql_string, dbuserkey, dbase, log_sql):
self.execute = execute_sql
self.key = dbuserkey
self.db = dbase
self.log = log_sql
if len(dbase) > 1:
self.hdbsql_jAU = hdbsql_string + " -j -A -U " + self.key + " -d " + self.db
self.hdbsql_jAxU = hdbsql_string + " -j -A -x -U " + self.key + " -d " + self.db
self.hdbsql_jAaxU = hdbsql_string + " -j -A -a -x -U " + self.key + " -d " + self.db
self.hdbsql_jAQaxU = hdbsql_string + " -j -A -Q -a -x -U " + self.key + " -d " + self.db
else:
self.hdbsql_jAU = hdbsql_string + " -j -A -U " + self.key
self.hdbsql_jAxU = hdbsql_string + " -j -A -x -U " + self.key
self.hdbsql_jAaxU = hdbsql_string + " -j -A -a -x -U " + self.key
self.hdbsql_jAQaxU = hdbsql_string + " -j -A -Q -a -x -U " + self.key
class LogManager:
def __init__(self, log_path, print_to_std):
self.path = log_path
self.print_to_std = print_to_std
class EmailSender:
def __init__(self, recieverEmail, senderEmail, mailServer):
self.recieverEmail = recieverEmail
self.senderEmail = senderEmail
self.mailServer = mailServer
def printEmailSender(self):
print "Reciever Email: ", self.recieverEmail, " Sender Email: ", self.senderEmail, " Mail Server: ", self.mailServer
######################## FUNCTION DEFINITIONS ################################
def is_integer(s):
try:
int(s)
return True
except ValueError:
return False
def log(message, logmanager):
if logmanager.print_to_std:
print message
if logmanager.path:
file_name = "hanacleanerlog"
logfile = open(logmanager.path+"/"+file_name+"_"+datetime.now().strftime("%Y-%m-%d"+".txt").replace(" ", "_"), "a")
logfile.write(message+"\n")
logfile.flush()
logfile.close()
def try_execute_sql(sql, errorlog, sqlman, logman, exit_on_fail = True):
succeeded = True
out = ""
try:
if sqlman.log:
log(sql, logman)
if sqlman.execute:
out = subprocess.check_output(sqlman.hdbsql_jAaxU + " \""+sql+"\"", shell=True)
except:
log("ERROR: Could not execute\n"+sql, logman)
log(errorlog, logman)
succeeded = False
if exit_on_fail:
os._exit(1)
return [out, succeeded]
def is_email(s):
s = s.split('@')
if not len(s) == 2:
return False
return '.' in s[1]
def hana_version_revision_maintenancerevision(sqlman, logman):
command_run = subprocess.check_output(sqlman.hdbsql_jAU + " \"select value from sys.m_system_overview where name = 'Version'\"", shell=True)
hanaver = command_run.splitlines(1)[2].split('.')[0].replace('| ','')
hanarev = command_run.splitlines(1)[2].split('.')[2]
hanamrev = command_run.splitlines(1)[2].split('.')[3]
if not is_integer(hanarev):
log("ERROR: something went wrong checking hana revision.", logman)
os._exit(1)
return [int(hanaver), int(hanarev), int(hanamrev)]
def hosts(sqlman):
hosts = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"select distinct(host) from sys.m_host_information\"", shell=True).splitlines(1)
hosts = [host.strip('\n').strip('|').strip(' ') for host in hosts]
return hosts
def sql_for_backup_id_for_min_retained_days(minRetainedDays):
oldestDayForKeepingBackup = datetime.now() + timedelta(days = -int(minRetainedDays))
return "SELECT TOP 1 ENTRY_ID, SYS_START_TIME from sys.m_backup_catalog where (ENTRY_TYPE_NAME = 'complete data backup' or ENTRY_TYPE_NAME = 'data snapshot') and STATE_NAME = 'successful' and SYS_START_TIME < '" + oldestDayForKeepingBackup.strftime('%Y-%m-%d')+" 00:00:00' order by SYS_START_TIME desc"
def sql_for_backup_id_for_min_retained_backups(minRetainedBackups):
return "SELECT ENTRY_ID, SYS_START_TIME from (SELECT ENTRY_ID, SYS_START_TIME, ROW_NUMBER() OVER(ORDER BY SYS_START_TIME desc) as NUM from sys.m_backup_catalog where (ENTRY_TYPE_NAME = 'complete data backup' or ENTRY_TYPE_NAME = 'data snapshot') and STATE_NAME = 'successful' order by SYS_START_TIME desc) as B where B.NUM = "+str(minRetainedBackups)
def backup_id(minRetainedBackups, minRetainedDays, sqlman):
if minRetainedDays >= 0:
results = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"" + sql_for_backup_id_for_min_retained_days(minRetainedDays) + "\"", shell=True).splitlines(1)
[backupIdForMinRetainedDays, startTimeForMinRetainedDays, dummy] = results if results else ['', '', '']
if not backupIdForMinRetainedDays:
backupIdForMinRetainedDays = '-1'
startTimeForMinRetainedDays = '1000-01-01 08:00:00'
else:
backupIdForMinRetainedDays = backupIdForMinRetainedDays.strip('\n').strip(' ')
startTimeForMinRetainedDays = startTimeForMinRetainedDays.strip('\n').strip(' ').split('.')[0] #removing milliseconds
if minRetainedBackups >= 0:
results = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"" + sql_for_backup_id_for_min_retained_backups(minRetainedBackups) + "\"", shell=True).splitlines(1)
[backupIdForMinRetainedBackups, startTimeForMinRetainedBackups, dummy] = results if results else ['', '', '']
if not backupIdForMinRetainedBackups:
backupIdForMinRetainedBackups = '-1'
startTimeForMinRetainedBackups = '1000-01-01 08:00:00'
else:
backupIdForMinRetainedBackups = backupIdForMinRetainedBackups.strip('\n').strip(' ')
startTimeForMinRetainedBackups = startTimeForMinRetainedBackups.strip('\n').strip(' ').split('.')[0] #removing milliseconds
if minRetainedDays >= 0 and minRetainedBackups >= 0:
backupId = backupIdForMinRetainedDays if datetime.strptime(startTimeForMinRetainedDays, '%Y-%m-%d %H:%M:%S') < datetime.strptime(startTimeForMinRetainedBackups, '%Y-%m-%d %H:%M:%S') else backupIdForMinRetainedBackups
elif minRetainedDays >= 0:
backupId = backupIdForMinRetainedDays
elif minRetainedBackups >= 0:
backupId = backupIdForMinRetainedBackups
else:
backupId = ""
return backupId
def sqls_for_backup_catalog_cleanup(minRetainedBackups, minRetainedDays, deleteBackups, sqlman):
sqls = []
backupId = backup_id(minRetainedBackups, minRetainedDays, sqlman)
if backupId:
backupType = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select ENTRY_TYPE_NAME from sys.m_backup_catalog where backup_id = '"+backupId+"'\"", shell=True).strip('\n').strip(' ')
if backupType == "complete data backup" or backupType == "data snapshot":
sqls = ["BACKUP CATALOG DELETE ALL BEFORE BACKUP_ID " + backupId]
if deleteBackups:
sqls = ["BACKUP CATALOG DELETE ALL BEFORE BACKUP_ID " + backupId + " COMPLETE"]
#If it will ever be possible to do BACKUP CATALOG DELETE BACKUP_ID <log backup id> then this will be useful:
else:
backupIdStartTime = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select SYS_START_TIME from sys.m_backup_catalog where backup_id = '"+backupId+"'\"", shell=True).strip(' ')
olderBackupIds = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select BACKUP_ID from sys.m_backup_catalog where SYS_START_TIME < '"+backupIdStartTime+"'\"", shell=True).splitlines()
olderBackupIds = [x.strip('\n').strip(' ') for x in olderBackupIds if x]
for oldID in olderBackupIds:
sql = "BACKUP CATALOG DELETE BACKUP_ID " + oldID
if deleteBackups:
sql += " COMPLETE"
sqls.append(sql)
return sqls
def print_removed_entries(before, after, logman):
beforeLines = before.splitlines(1)
afterLines = after.splitlines(1)
dif = list(Differ().compare(beforeLines, afterLines))
removedLines = [line.strip("- ").strip("\n") for line in dif if line[0] == '-']
if removedLines:
log("\nREMOVED:\n"+beforeLines[0].strip("\n"), logman)
for line in removedLines:
if not "rows" in line:
log(line, logman)
log("\n", logman)
def clean_backup_catalog(minRetainedBackups, minRetainedDays, deleteBackups, outputCatalog, outputDeletedCatalog, sqlman, logman):
if outputCatalog or outputDeletedCatalog:
nCatalogEntries = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select count(*) from sys.m_backup_catalog\"", shell=True).strip(' '))
if nCatalogEntries > 100000:
log("INPUT ERROR: Please do not use -br true or -bo true if your backup catalog is larger than 100000 entries!", logman)
os._exit(1)
nDataBackupCatalogEntriesBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_backup_catalog where entry_type_name != 'log backup'\"", shell=True).strip(' '))
nLogBackupCatalogEntriesBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_backup_catalog where entry_type_name = 'log backup'\"", shell=True).strip(' '))
if nDataBackupCatalogEntriesBefore == 0:
return [0,0]
sqls_for_cleanup = sqls_for_backup_catalog_cleanup(minRetainedBackups, minRetainedDays, deleteBackups, sqlman)
if sqls_for_cleanup:
sql_for_catalog = "select ENTRY_ID, ENTRY_TYPE_NAME, BACKUP_ID, SYS_START_TIME, STATE_NAME from sys.m_backup_catalog"
if outputCatalog or outputDeletedCatalog:
beforeCatalog = subprocess.check_output(sqlman.hdbsql_jAxU + " \"" + sql_for_catalog + "\"", shell=True)
if outputCatalog:
log("\nBEFORE:\n"+beforeCatalog, logman)
for sql_for_cleanup in sqls_for_cleanup:
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not clean backup catalog. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the system privilege BACKUP ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql_for_cleanup+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql_for_cleanup, errorlog, sqlman, logman)
nDataBackupCatalogEntriesAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_backup_catalog where entry_type_name != 'log backup'\"", shell=True).strip(' '))
nLogBackupCatalogEntriesAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_backup_catalog where entry_type_name = 'log backup'\"", shell=True).strip(' '))
if outputCatalog or outputDeletedCatalog:
afterCatalog = subprocess.check_output(sqlman.hdbsql_jAxU + " \"" + sql_for_catalog + "\"", shell=True)
if outputCatalog:
log("\nAFTER:\n"+afterCatalog, logman)
if outputDeletedCatalog:
print_removed_entries(beforeCatalog, afterCatalog, logman)
return [nDataBackupCatalogEntriesBefore - nDataBackupCatalogEntriesAfter, max(nLogBackupCatalogEntriesBefore - nLogBackupCatalogEntriesAfter,0)] #if a logbackup was done during run
else:
return [0,0]
def clean_trace_files(retainedTraceContentDays, retainedTraceFilesDays, outputTraces, outputRemovedTraces, SID, local_dbinstance, hosts, sqlman, logman):
nbrTracesBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_tracefiles\"", shell=True).strip(' '))
if nbrTracesBefore == 0:
return 0
if outputTraces:
beforeTraces = subprocess.check_output(sqlman.hdbsql_jAxU + " \"select * from sys.m_tracefiles order by file_mtime desc\"", shell=True)
log("\nBEFORE:\n"+beforeTraces, logman)
if outputRemovedTraces:
beforeTraceFiles = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"select HOST, FILE_NAME from sys.m_tracefiles order by file_mtime desc\"", shell=True)
if retainedTraceContentDays != "-1":
oldestRetainedTraceContentDate = datetime.now() + timedelta(days = -int(retainedTraceContentDays))
sql = "ALTER SYSTEM CLEAR TRACES ('ALERT','CLIENT','CRASHDUMP','EMERGENCYDUMP','EXPENSIVESTATEMENT','RTEDUMP','UNLOAD','ROWSTOREREORG','SQLTRACE','*') UNTIL '"+oldestRetainedTraceContentDate.strftime('%Y-%m-%d')+" "+datetime.now().strftime("%H:%M:%S")+"'"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not clear traces. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the system privilege TRACE ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
if retainedTraceFilesDays != "-1":
oldestRetainedTraceFilesDate = datetime.now() + timedelta(days = -int(retainedTraceFilesDays))
sql = "select FILE_NAME from sys.m_tracefiles where file_size != '-1' and file_mtime < '"+oldestRetainedTraceFilesDate.strftime('%Y-%m-%d')+" "+datetime.now().strftime("%H:%M:%S")+"'" # file_size = -1 --> folder, cannot be removed
filesToBeRemoved = subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"" + sql + "\"", shell=True).splitlines(1)
filesToBeRemoved = [file.strip('\n').strip(' ') for file in filesToBeRemoved if file != '\n']
# Ignore files with names that breaks the ALTER command, or kill.sap according to SAP Note 2349144, and backup.log and backint.log since they are taken care of by -zb, see SAP Note 2431472 about hdbdaemon, we do not want to delete any .sem or .status file, and we do not want to delete any links, e.g. .sap<SID>_HDB<inst>
filesToBeRemoved = [file for file in filesToBeRemoved if not (" " in file or "," in file or "'" in file or "kill.sap" in file or "backup.log" in file or "backint.log" in file or "hdbdaemon.status" in file or "sapstart.sem" in file or "sapstart.log" in file or ".sap"+SID+"_HDB"+local_dbinstance in file)]
# Make sure we only delete files with known extensions (we dont delete .sem or .status files). Added two files without extensions that we want to delete
filesToBeRemoved = [file for file in filesToBeRemoved if any(x in file for x in [".trc", ".log", ".stat", ".py", ".tpt", ".gz", ".zip", ".old", ".xml", ".txt", ".docs", ".cfg", ".cockpit", ".xs", "dev_icm_sec", "wdisp_icm_log"])]
if filesToBeRemoved: # otherwise no file to remove
filesToBeRemoved = [filesToBeRemoved[i:i + 100] for i in xrange(0, len(filesToBeRemoved), 100)] #make sure we do not send too long statement, it could cause an error
for files in filesToBeRemoved:
filesToBeRemovedString = "'"+"', '".join(files)+"'"
for host in hosts:
sql = "ALTER SYSTEM REMOVE TRACES (" +"'"+host+"', "+filesToBeRemovedString+ ")"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not remove traces. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the system privilege TRACE ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nbrTracesAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.m_tracefiles\"", shell=True).strip(' '))
nbrRemovedTraceFiles = nbrTracesBefore - nbrTracesAfter
if outputTraces:
afterTraces = subprocess.check_output(sqlman.hdbsql_jAxU + " \"select * from sys.m_tracefiles order by file_mtime desc\"", shell=True)
log("\nAFTER:\n"+afterTraces, logman)
if outputRemovedTraces and nbrRemovedTraceFiles:
afterTraceFiles = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"select HOST, FILE_NAME from sys.m_tracefiles order by file_mtime desc\"", shell=True)
output_removed_trace_files(beforeTraceFiles, afterTraceFiles, logman)
return nbrRemovedTraceFiles
def clean_dumps(retainedDumpDays, sqlman, logman):
path = cdalias('cdglo')+"/sapcontrol/snapshots/"
with open(os.devnull, 'w') as devnull:
nbrDumpsBefore = int(subprocess.check_output("ls "+path+"fullsysteminfodump* | wc -l", shell=True, stderr=devnull).strip(' '))
if not nbrDumpsBefore:
return 0
if sqlman.log:
log("find "+path+"fullsysteminfodump* -mtime +"+retainedDumpDays+" -delete", logman)
if sqlman.execute:
subprocess.check_output("find "+path+"fullsysteminfodump* -mtime +"+retainedDumpDays+" -delete", shell=True, stderr=devnull)
nbrDumpsAfter = int(subprocess.check_output("ls "+path+"fullsysteminfodump* | wc -l", shell=True, stderr=devnull).strip(' '))
return nbrDumpsBefore - nbrDumpsAfter
def output_removed_trace_files(before, after, logman):
beforeLines = before.splitlines(1)
afterLines = after.splitlines(1)
beforeFiles = [line.strip('\n').strip('|').strip(' ') for line in beforeLines]
afterFiles = [line.strip('\n').strip('|').strip(' ') for line in afterLines]
nbrTracesBefore = len(beforeFiles)
nbrTracesAfter = len(afterFiles)
nbrRemoved = nbrTracesBefore - nbrTracesAfter
if nbrRemoved > 0:
log("\nREMOVED ("+str(nbrRemoved)+"):", logman)
for beforeFile in beforeFiles:
if beforeFile not in afterFiles:
log(beforeFile, logman)
log("\n", logman)
def clean_alerts(minRetainedAlertDays, outputAlerts, outputDeletedAlerts, sqlman, logman):
try:
nbrAlertsBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM _sys_statistics.statistics_alerts_base\"", shell=True).strip(' '))
except:
log("\nERROR: The user represented by the key "+sqlman.key+" could not find amount of alerts. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege SELECT on the table _sys_statistics.statistics_alerts_base.\n", logman)
os._exit(1)
if nbrAlertsBefore > 10000 and (outputAlerts or outputDeletedAlerts):
outputAlerts = False
outputDeletedAlerts = False
log("INFO: The flags -ao and -ad were changed to false since there are too many alerts for printout.", logman)
if outputAlerts or outputDeletedAlerts:
beforeAlerts = subprocess.check_output(sqlman.hdbsql_jAxU + " \"select SNAPSHOT_ID, ALERT_ID, ALERT_TIMESTAMP, ALERT_RATING from _SYS_STATISTICS.STATISTICS_ALERTS_BASE\"", shell=True)
if outputAlerts:
log("\nBEFORE:\n"+beforeAlerts, logman)
sql = "DELETE FROM _SYS_STATISTICS.STATISTICS_ALERTS_BASE WHERE ALERT_TIMESTAMP < ADD_DAYS(CURRENT_TIMESTAMP, -"+str(minRetainedAlertDays)+")"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not delete alerts. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege DELETE on the table _sys_statistics.statistics_alerts_base.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nbrAlertsAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM _sys_statistics.statistics_alerts_base\"", shell=True).strip(' '))
if outputAlerts or outputDeletedAlerts:
afterAlerts = subprocess.check_output(sqlman.hdbsql_jAxU + " \"select SNAPSHOT_ID, ALERT_ID, ALERT_TIMESTAMP, ALERT_RATING from _SYS_STATISTICS.STATISTICS_ALERTS_BASE\"", shell=True)
if outputAlerts:
log("\nAFTER:\n"+afterAlerts, logman)
if outputDeletedAlerts:
print_removed_entries(beforeAlerts, afterAlerts, logman)
return nbrAlertsBefore - nbrAlertsAfter
def clean_ini(minRetainedIniDays, version, revision, mrevision, sqlman, logman):
if version < 2 or revision < 30:
log("\nERROR: the -ir flag is only supported starting with SAP HANA 2.0 SPS03. You run on SAP HANA "+str(version)+" revision "+str(revision)+" maintenance revision "+str(mrevision), logman)
os._exit(1)
try:
nbrIniHistBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_INIFILE_CONTENT_HISTORY\"", shell=True).strip(' '))
except:
log("\nERROR: The user represented by the key "+sqlman.key+" could not find amount of inifile history. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege SELECT on the view SYS.M_INIFILE_CONTENT_HISTORY.\n", logman)
os._exit(1)
d = datetime.today() - timedelta(days=minRetainedIniDays)
sql = "ALTER SYSTEM CLEAR INIFILE CONTENT HISTORY UNTIL '"+str(d)+"'"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not delete inifile history. \nOne possible reason for this is insufficient privilege.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nbrIniHistAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_INIFILE_CONTENT_HISTORY\"", shell=True).strip(' '))
return nbrIniHistBefore - nbrIniHistAfter
def clean_objlock(minRetainedObjLockDays, sqlman, logman):
try:
sql = "select count(*) FROM _SYS_STATISTICS.HOST_OBJECT_LOCK_STATISTICS_BASE WHERE OBJECT_NAME = '(unknown)'"
nbrObjLockBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \""+sql+"\"", shell=True).strip(' '))
except:
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not select object locks. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege SELECT on the table _SYS_STATISTICS.HOST_OBJECT_LOCK_STATISTICS_BASE.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
log(errorlog, logman)
sql = "DELETE FROM _SYS_STATISTICS.HOST_OBJECT_LOCK_STATISTICS_BASE WHERE OBJECT_NAME = '(unknown)' and SERVER_TIMESTAMP < ADD_DAYS(CURRENT_TIMESTAMP, -"+str(minRetainedObjLockDays)+")"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not delete object locks. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege DELETE on the table _SYS_STATISTICS.HOST_OBJECT_LOCK_STATISTICS_BASE.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nbrObjLockAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select count(*) FROM _SYS_STATISTICS.HOST_OBJECT_LOCK_STATISTICS_BASE WHERE OBJECT_NAME = '(unknown)'\"", shell=True).strip(' '))
return nbrObjLockBefore - nbrObjLockAfter
def clean_objhist(objHistMaxSize, outputObjHist, sqlman, logman):
try:
objHistSizeBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select disk_size from SYS.M_TABLE_PERSISTENCE_LOCATION_STATISTICS where table_name = 'OBJECT_HISTORY'\"", shell=True).strip(' '))
except:
log("\nERROR: The user represented by the key "+sqlman.key+" could not find size of object history. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege SELECT on the table SYS.M_TABLE_PERSISTENCE_LOCATION_STATISTICS.\n", logman)
os._exittr445(1)
if objHistSizeBefore > objHistMaxSize*1000000: #mb --> b
sql = "DELETE FROM _SYS_REPO.OBJECT_HISTORY WHERE (package_id, object_name, object_suffix, version_id) NOT IN (SELECT package_id, object_name, object_suffix, MAX(version_id) AS maxvid from _SYS_REPO.OBJECT_HISTORY GROUP BY package_id, object_name, object_suffix ORDER BY package_id, object_name, object_suffix)"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not clean the object history. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the object privilege DELETE on the table _SYS_REPO.OBJECT_HISTORY.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
objHistSizeAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select disk_size from SYS.M_TABLE_PERSISTENCE_LOCATION_STATISTICS where table_name = 'OBJECT_HISTORY'\"", shell=True).strip(' '))
if outputObjHist:
log("Object History was:"+str(objHistSizeBefore/1000000)+" mb and is now "+str(objHistSizeAfter/1000000)+" mb.", logman)
return (objHistSizeBefore - objHistSizeAfter)/1000000
def max_filesystem_usage_in_percent(file_system, ignore_filesystems, logman):
log("Will now check most used memory in the file systems. If it hangs there is an issue with df -h, then see if the -fs flag helps.", logman)
maxPercentage = 0
lines = None
try:
lines = subprocess.check_output("df -h -P -x fuse.gvfs-fuse-daemon "+file_system, shell=True).splitlines(1) # -x: telling df to ignore /root/.gvfs since normally <sid>adm lacking permissions, -P: Force output in one line for RedHat
except:
log("WARNING: Something went wrong executing df -h, \n therefore the most used memory in your file systems will not be checked. \n As a workaround it is possible to use the -fs flag to only take into account the most relevant file system.", logman)
if lines:
log("The most used filesystem is using ", logman)
used_percentages = []
for line in lines:
if not "Filesystem" in line and not "S.ficheros" in line and not "Dateisystem" in line: # english, spanish, german and ...
words = line.split()
if len(words) == 6:
filesystem_and_mount = [words[0].strip('\n'), words[5].strip('\n')]
elif len(words) == 5:
filesystem_and_mount = [words[0].strip('\n'), words[4].strip('\n')]
else:
log("ERROR, Unexpted number output columns from df -h: \n", words)
if len(words) == 6:
percentage = int(words[4].strip('%'))
if len(words) == 5:
percentage = int(words[3].strip('%'))
if len(words) > 1 and filesystem_and_mount[0] not in ignore_filesystems and filesystem_and_mount[1] not in ignore_filesystems:
used_percentages.append(percentage)
maxPercentage = max(used_percentages)
log(str(maxPercentage)+"%", logman)
return maxPercentage
def find_all(name, path, zipLinks):
result = []
if zipLinks:
pathes = os.walk(path, followlinks=True)
else:
pathes = os.walk(path)
for root, dirs, files in pathes:
if name in files:
result.append(os.path.join(root, name))
return result
def zipBackupLogs(zipBackupLogsSizeLimit, zipBackupPath, zipLinks, zipOut, zipKeep, sqlman, logman):
backup_log_pathes = find_all("backup.log", zipBackupPath, zipLinks)
backint_log_pathes = find_all("backint.log", zipBackupPath, zipLinks)
log_pathes = backup_log_pathes + backint_log_pathes
nZipped = 0
for aLog in log_pathes:
if os.path.getsize(aLog)/1000000.0 > zipBackupLogsSizeLimit:
newname = (aLog.strip(".log")+"_compressed_"+datetime.now().strftime("%Y-%m-%d %H:%M:%S")+".tar.gz").replace(":","-").replace(" ","_").replace("//", "/")
tempname = newname.replace(".tar.gz", ".log")
if sqlman.log:
log("mv "+aLog+" "+tempname, logman)
log("tar -czPf "+newname+" "+tempname, logman) # P to avoid annoying error message
log("rm "+tempname, logman)
if not zipKeep:
log("rm "+newname, logman)
if sqlman.execute:
subprocess.check_output("mv "+aLog+" "+tempname, shell=True)
subprocess.check_output("tar -czPf "+newname+" "+tempname, shell=True) # P to avoid annoying error message
subprocess.check_output("rm "+tempname, shell=True)
if zipOut:
log(aLog+" was compressed to "+newname+" and then removed", logman)
nZipped += 1
if not zipKeep:
subprocess.check_output("rm "+newname, shell=True)
return nZipped
def cdalias(alias): # alias e.g. cdtrace, cdhdb, ...
command_run = subprocess.check_output(['/bin/bash', '-l', '-c', "alias "+alias])
#pieces = command_run.strip("\n").strip("alias "+alias+"=").strip("'").strip("cd ").split("/")
pieces = re.sub(r'.*cd ','',command_run).strip("\n").strip("'").split("/") #to remove ANSI escape codes (only needed in few systems)
path = ''
for piece in pieces:
if piece and piece[0] == '$':
piece = (subprocess.check_output(['/bin/bash', '-l', '-c', "echo "+piece])).strip("\n")
path = path + '/' + piece + '/'
return path
def reclaim_logsegments(maxFreeLogsegments, sqlman, logman):
nTotFreeLogsegmentsBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_LOG_SEGMENTS WHERE STATE = 'Free'\"", shell=True, stderr=subprocess.STDOUT).strip(' '))
if nTotFreeLogsegmentsBefore == 0:
return 0
listOfPorts = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT DISTINCT PORT FROM SYS.M_LOG_SEGMENTS\"", shell=True).splitlines(1)
listOfPorts = [port.strip('\n').strip('|').strip(' ') for port in listOfPorts]
nFreeLogsegmentsPerServices = []
for port in listOfPorts:
nFreeLogs = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_LOG_SEGMENTS WHERE STATE = 'Free' AND PORT = '"+port+"'\"", shell=True).strip(' '))
nFreeLogsegmentsPerServices.append(nFreeLogs)
if max(nFreeLogsegmentsPerServices) > maxFreeLogsegments:
sql = "ALTER SYSTEM RECLAIM LOG"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not reclaim logs. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the privilege LOG ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nTotFreeLogsegmentsAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_LOG_SEGMENTS WHERE STATE = 'Free'\"", shell=True).strip(' '))
return nTotFreeLogsegmentsBefore - nTotFreeLogsegmentsAfter
def clean_events(minRetainedDaysForHandledEvents, minRetainedDaysForEvents, sqlman, logman):
nHandledEventsBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_EVENTS WHERE STATE = 'HANDLED'\"", shell=True).strip(' '))
nEventsBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_EVENTS \"", shell=True).strip(' '))
if nEventsBefore == 0:
return [0,0,0,0]
oldestDayForKeepingHandledEvent = datetime.now() + timedelta(days = -int(minRetainedDaysForHandledEvents))
listOfHandledEventsToRemove = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT HOST, PORT, ID FROM SYS.M_EVENTS WHERE STATE = 'HANDLED' AND CREATE_TIME < '"+oldestDayForKeepingHandledEvent.strftime('%Y-%m-%d')+" 00:00:00'\"", shell=True).splitlines(1)
listOfHandledEventsToRemove = [event.strip('\n').strip('|').split('|') for event in listOfHandledEventsToRemove]
listOfHandledEventsToRemove = [[evComp.strip(' ') for evComp in event] for event in listOfHandledEventsToRemove]
for event in listOfHandledEventsToRemove:
sql1 = "ALTER SYSTEM SET EVENT ACKNOWLEDGED '"+event[0]+":"+event[1]+"' "+event[2]
sql2 = "ALTER SYSTEM DELETE HANDLED EVENT '"+event[0]+":"+event[1]+"' "+event[2]
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not delete handled events. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the privilege MONITOR ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql1+"\nand\n"+sql2+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql1, errorlog, sqlman, logman)
try_execute_sql(sql2, errorlog, sqlman, logman)
oldestDayForKeepingEvent = datetime.now() + timedelta(days = -int(minRetainedDaysForEvents))
listOfEventsToRemove = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT HOST, PORT, ID FROM SYS.M_EVENTS WHERE CREATE_TIME < '"+oldestDayForKeepingEvent.strftime('%Y-%m-%d')+" 00:00:00'\"", shell=True).splitlines(1)
listOfEventsToRemove = [event.strip('\n').strip('|').split('|') for event in listOfEventsToRemove]
listOfEventsToRemove = [[evComp.strip(' ') for evComp in event] for event in listOfEventsToRemove]
for event in listOfEventsToRemove:
sql1 = "ALTER SYSTEM SET EVENT HANDLED '"+event[0]+":"+event[1]+"' "+event[2]
sql2 = "ALTER SYSTEM SET EVENT ACKNOWLEDGED '"+event[0]+":"+event[1]+"' "+event[2]
sql3 = "ALTER SYSTEM DELETE HANDLED EVENT '"+event[0]+":"+event[1]+"' "+event[2]
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not delet events. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the privilege MONITOR ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql1+"\nand\n"+sql2+"\nand\n"+sql3+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql1, errorlog, sqlman, logman)
try_execute_sql(sql2, errorlog, sqlman, logman)
try_execute_sql(sql3, errorlog, sqlman, logman)
nHandledEventsAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_EVENTS WHERE STATE = 'HANDLED'\"", shell=True).strip(' '))
nEventsAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM SYS.M_EVENTS \"", shell=True).strip(' '))
return [nHandledEventsBefore - nHandledEventsAfter, nEventsBefore - nEventsAfter, nEventsAfter, nHandledEventsAfter]
def clean_audit_logs(retainedAuditLogDays, sqlman, logman): # for this, both Audit Admin and Audit Operator is needed
nbrLogsBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.audit_log\"", shell=True).strip(' '))
if nbrLogsBefore == 0:
return 0
oldestRetainedAuditContentDate = datetime.now() + timedelta(days = -int(retainedAuditLogDays))
sql = "ALTER SYSTEM CLEAR AUDIT LOG UNTIL '"+oldestRetainedAuditContentDate.strftime('%Y-%m-%d')+" "+datetime.now().strftime("%H:%M:%S")+"'"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not clear traces. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the system privilege AUDIT ADMIN and/or AUDIT OPERATOR.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nbrLogsAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(*) FROM sys.audit_log\"", shell=True).strip(' '))
return nbrLogsBefore - nbrLogsAfter
def defragment(fragmentationLimit, outputFragmentation, sqlman, logman):
fragPerPortBefore = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT HOST, PORT, USED_SIZE, TOTAL_SIZE from SYS.M_VOLUME_FILES WHERE FILE_TYPE = 'DATA'\" ", shell=True).splitlines(1)
fragPerPortBefore = [port.strip('\n').strip('|').split('|') for port in fragPerPortBefore]
fragPerPortBefore = [[elem.strip(' ') for elem in port] for port in fragPerPortBefore]
fragPerPortBefore = [port+[round(((float(port[3])-float(port[2]))/float(port[3])),2)*100] for port in fragPerPortBefore]
if outputFragmentation:
log("\nBEFORE FRAGMENTATION:", logman)
log("Host Port Used Space [B] Total Space [B] Fragmentation [%]", logman)
for port in fragPerPortBefore:
log(port[0]+" "*(20-len(port[0]))+port[1]+" "*(20-len(port[1]))+port[2]+" "*(30-len(port[2]))+port[3]+" "*(30-len(port[3]))+str(port[4]), logman)
log("\n", logman)
for port in fragPerPortBefore:
if port[4] > fragmentationLimit:
sql = "ALTER SYSTEM RECLAIM DATAVOLUME '"+port[0]+":"+port[1]+"' 120 DEFRAGMENT"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not defragment the data volumes. \nOne possible reason for this is insufficient privilege, \ne.g. lack of the privilege RESOURCE ADMIN.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
errorlog += "Note: If you use System Replication see Q19 in SAP Note 1999880"
try_execute_sql(sql, errorlog, sqlman, logman)
fragPerPortAfter = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT HOST, PORT, USED_SIZE, TOTAL_SIZE from SYS.M_VOLUME_FILES WHERE FILE_TYPE = 'DATA'\" ", shell=True).splitlines(1)
fragPerPortAfter = [port.strip('\n').strip('|').split('|') for port in fragPerPortAfter]
fragPerPortAfter = [[elem.strip(' ') for elem in port] for port in fragPerPortAfter]
fragPerPortAfter = [port+[round(((float(port[3])-float(port[2]))/float(port[3])),2)*100] for port in fragPerPortAfter]
fragChange = []
for i in range(len(fragPerPortBefore)):
if fragPerPortBefore[i][4] > fragPerPortAfter[i][4]:
fragChange.append([fragPerPortBefore[i][0], fragPerPortBefore[i][1], fragPerPortBefore[i][4] - fragPerPortAfter[i][4]])
elif fragPerPortBefore[i][4] > fragmentationLimit:
fragChange.append([fragPerPortBefore[i][0], fragPerPortBefore[i][1], 0])
if outputFragmentation and fragChange:
log("\nAFTER FRAGMENTATION:", logman)
log("Host Port Used Space [B] Total Space [B] Fragmentation [%]", logman)
for port in fragPerPortAfter:
log(port[0]+" "*(20-len(port[0]))+port[1]+" "*(20-len(port[1]))+port[2]+" "*(30-len(port[2]))+port[3]+" "*(30-len(port[3]))+str(port[4]), logman)
log("\n", logman)
return fragChange
def reclaim_rs_containers(outputRcContainers, sqlman, logman):
nTablesWithMultipleRSContainersBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(TABLE_NAME) FROM SYS.M_RS_TABLES WHERE CONTAINER_COUNT > 1\"", shell=True).strip(' '))
nContCount = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(CONTAINER_COUNT) FROM SYS.M_RS_TABLES WHERE CONTAINER_COUNT > 1\"", shell=True).strip(' '))
nUnnecessaryRSContainersBefore = 0
if nContCount:
nUnnecessaryRSContainersBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT SUM(CONTAINER_COUNT) FROM SYS.M_RS_TABLES WHERE CONTAINER_COUNT > 1\"", shell=True).strip(' ')) - nTablesWithMultipleRSContainersBefore
tablesWithMultipleRSContainers = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"SELECT SCHEMA_NAME, TABLE_NAME from SYS.M_RS_TABLES WHERE CONTAINER_COUNT > 1\" ", shell=True).splitlines(1)
tablesWithMultipleRSContainers = [port.strip('\n').strip('|').split('|') for port in tablesWithMultipleRSContainers]
tablesWithMultipleRSContainers = [[elem.strip(' ') for elem in port] for port in tablesWithMultipleRSContainers]
if nUnnecessaryRSContainersBefore > 0:
for table in tablesWithMultipleRSContainers:
sql = "ALTER TABLE "+table[0]+"."+table[1]+" RECLAIM DATA SPACE"
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not reclaim data space of the table "+table[0]+"."+table[1]+". \nOne possible reason for this is insufficient privilege, \ne.g. lack of ALTER privilege on the schema "+table[0]+".\n"
errorlog += "Unfortunately there is NO nice way to give privileges to the DB User to be allowed to do this.\nEither you can run hanacleaner as SYSTEM user (NOT recommended) or grant DATA ADMIN to the user (NOT recommended).\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner"
try_execute_sql(sql, errorlog, sqlman, logman)
nTablesWithMultipleRSContainersAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"SELECT COUNT(TABLE_NAME) FROM SYS.M_RS_TABLES WHERE CONTAINER_COUNT > 1\"", shell=True).strip(' '))
if nTablesWithMultipleRSContainersAfter != 0:
log("\nERROR: Something went wrong. After reclaim of multiple row store table containers we still have "+str(nTablesWithMultipleRSContainersAfter)+" tables with multiple row store containers. Please investigate.", logman)
os._exit(1)
return [str(nTablesWithMultipleRSContainersBefore), str(nUnnecessaryRSContainersBefore)]
def force_compression(maxRawComp, maxEstComp, maxRowComp, maxMemComp, minDistComp, maxQuotaComp, maxUDIVComp, maxBLOCKComp, partComp, mergeBeforeComp, version, revision, mrevision, outComp, sqlman, logman):
#CREATE SQLS
if not partComp:
#Tables with no compression
raw = "raw_record_count_in_main > "+str(maxRawComp)
est = "estimated_max_memory_size_in_total > "+str(maxEstComp)+" * 1024 * 1024 * 1024" #GB
sql_nocomp = """select distinct schema_name, table_name from sys.m_cs_tables where last_compressed_record_count = 0 and """+est+""" and """+raw
#Columns with default compression
join = "inner join SYS.TABLE_COLUMNS TC on C.schema_name = TC.schema_name and C.table_name = TC.table_name and C.column_name = TC.column_name"
default = "C.compression_type = 'DEFAULT'"
dist = "C.distinct_count <= C.count * "+str(minDistComp/100.)
count = "C.count >= "+str(maxRowComp)
mem = "C.memory_size_in_total >= "+str(maxMemComp)+" * 1024 * 1024" #MB
gen = "TC.generation_type is NULL" # a generated column is typically a virtual column that doesn’t allocate space and so compression isn’t possible
sql_default = """select distinct C.schema_name, C.table_name from SYS.M_CS_ALL_COLUMNS C """+join+""" where """+default+""" and """+dist+""" and """+count+""" and """+mem+""" and """+gen
#Tables with too much UDIVs
quota = "max_udiv > "+str(maxQuotaComp)+" / 100. * (raw_record_count_in_main + raw_record_count_in_delta)"
udivs = "max_udiv > "+str(maxUDIVComp)
sql_udivs = """select distinct schema_name, table_name from sys.m_cs_tables where """+quota+""" and """+udivs
#Columns with SPARE or PREFIXED
if version < 2 and revision < 123 and mrevision < 3: #the SPARSE if fixed with 122.02
comp = "compression_type in ('SPARSE', 'PREFIXED')"
else:
comp = "compression_type = 'PREFIXED'"
sql_block = """select distinct schema_name, table_name from SYS.M_CS_COLUMNS where index_type = 'BLOCK' and """+comp+""" and count > """+str(maxBLOCKComp)
else:
#Tables with no compression
raw = "raw_main_count_all_partitions > "+str(maxRawComp)
est = "max_mem_all_partitions > "+str(maxEstComp)+" * 1024 * 1024 * 1024" #GB
sql_int = """select schema_name, table_name, sum(estimated_max_memory_size_in_total) as max_mem_all_partitions, sum(raw_record_count_in_main) as raw_main_count_all_partitions from sys.m_cs_tables where last_compressed_record_count = 0 group by schema_name, table_name"""
sql_nocomp = """select distinct schema_name, table_name from ("""+sql_int+""") where """+est+""" and """+raw
#Columns with default compression
join = "inner join SYS.TABLE_COLUMNS TC on C.schema_name = TC.schema_name and C.table_name = TC.table_name and C.column_name = TC.column_name"
default = "C.compression_type = 'DEFAULT'"
dist = "C.distinct_count <= C.count * "+str(minDistComp/100.)
gen = "TC.generation_type is NULL" # a generated column is typically a virtual column that doesn’t allocate space and so compression isn’t possible
count = "count_all_partitions >= "+str(maxRowComp)
mem = "memory_size_all_partitions >= "+str(maxMemComp)+" * 1024 * 1024" # MB
sql_int = """select C.schema_name as schema_name, C.table_name as table_name, sum(C.memory_size_in_total) as memory_size_all_partitions, sum(C.count) as count_all_partitions from SYS.M_CS_ALL_COLUMNS C """+join+""" where """+default+""" and """+dist+""" and """+gen+""" group by C.schema_name, C.table_name, C.column_name"""
sql_default = """select distinct schema_name, table_name from ("""+sql_int+""") where """+count+""" and """+mem
#Tables with too much UDIVs
quota = "udivs_all_partitions > "+str(maxQuotaComp)+" / 100. * (main_all_partitions + delta_all_partitions)"
udivs = "udivs_all_partitions > "+str(maxUDIVComp)
sql_int = """select schema_name, table_name, sum(max_udiv) as udivs_all_partitions, sum(raw_record_count_in_main) as main_all_partitions, sum(raw_record_count_in_delta) as delta_all_partitions from sys.m_cs_tables group by schema_name, table_name"""
sql_udivs = """select distinct schema_name, table_name from ("""+sql_int+""") where """+quota+""" and """+udivs
#Columns with SPARE or PREFIXED
if version < 2 and revision < 123 and mrevision < 3: #the SPARSE is fixed with 122.02
comp = "compression_type in ('SPARSE', 'PREFIXED')"
else:
comp = "compression_type = 'PREFIXED'"
sql_int = """select schema_name, table_name, column_name, sum(count) as count_all_partitions from SYS.M_CS_COLUMNS where index_type = 'BLOCK' and """+comp+""" group by schema_name, table_name, column_name"""
sql_block = """select distinct schema_name, table_name from ( """+sql_int+""") where count_all_partitions > """+str(maxBLOCKComp)
#FIND TABLES TO COMPRESS
#Tables with no compression
tablesToCompress = []
if all(c > -1 for c in [maxRawComp, maxEstComp]):
tablesToCompress = subprocess.check_output(sqlman.hdbsql_jAaxU + " \""+sql_nocomp+"\" ", shell=True).splitlines(1)
tablesToCompress = [table.strip('\n').strip('|').split('|') for table in tablesToCompress]
tablesToCompress = [[elem.strip(' ') for elem in table] for table in tablesToCompress]
#Columns with default compression
moreTablesToCompress = []
if all(c > -1 for c in [maxRowComp, maxMemComp, minDistComp]):
moreTablesToCompress = subprocess.check_output(sqlman.hdbsql_jAaxU + " \""+sql_default+"\" ", shell=True).splitlines(1)
moreTablesToCompress = [table.strip('\n').strip('|').split('|') for table in moreTablesToCompress]
moreTablesToCompress = [[elem.strip(' ') for elem in table] for table in moreTablesToCompress]
for newtab in moreTablesToCompress:
if not newtab in tablesToCompress:
tablesToCompress.append(newtab)
#Tables with too much UDIVs
moreTablesToCompress = []
if all(c > -1 for c in [maxQuotaComp, maxUDIVComp]):
moreTablesToCompress = subprocess.check_output(sqlman.hdbsql_jAaxU + " \""+sql_udivs+"\" ", shell=True).splitlines(1)
moreTablesToCompress = [table.strip('\n').strip('|').split('|') for table in moreTablesToCompress]
moreTablesToCompress = [[elem.strip(' ') for elem in table] for table in moreTablesToCompress]
for newtab in moreTablesToCompress:
if not newtab in tablesToCompress:
tablesToCompress.append(newtab)
#Columns with SPARE or PREFIXED
moreTablesToCompress = []
if maxBLOCKComp > -1:
moreTablesToCompress = subprocess.check_output(sqlman.hdbsql_jAaxU + " \""+sql_block+"\" ", shell=True).splitlines(1)
moreTablesToCompress = [table.strip('\n').strip('|').split('|') for table in moreTablesToCompress]
moreTablesToCompress = [[elem.strip(' ') for elem in table] for table in moreTablesToCompress]
for newtab in moreTablesToCompress:
if not newtab in tablesToCompress:
tablesToCompress.append(newtab)
#COMPRESS (AND MERGE) TABLES
failed = 0
for tab in tablesToCompress:
sql_merge = 'MERGE DELTA OF \\"'+tab[0]+'\\".\\"'+tab[1]+'\\"' # necessary for tables starting with /
errorlog_merge = "Failed to merge the table "+tab[0]+"."+tab[1]
sql = """UPDATE \\\""""+tab[0]+"""\\\".\\\""""+tab[1]+"""\\\" WITH PARAMETERS ('OPTIMIZE_COMPRESSION' = 'FORCE')""" # necessary for tables starting with /
errorlog = "Failed to re-optimize the compression of the table "+tab[0]+"."+tab[1]
succeeded_merge = True # in case we will not merge before compression, we define merge to be success
if mergeBeforeComp:
[dummyout, succeeded_merge] = try_execute_sql(sql_merge, errorlog_merge, sqlman, logman, exit_on_fail = False)
[dummyout, succeeded] = try_execute_sql(sql, errorlog, sqlman, logman, exit_on_fail = False)
if not succeeded_merge or not succeeded:
failed += 1
if outComp:
log("\n ATTEMPTED COMPRESSION RE-OPTIMIZATION ON FOLLOWING TABLES:", logman)
for tab in tablesToCompress:
log(" "+tab[0]+"."+tab[1], logman)
log("\n", logman)
return [len(tablesToCompress), failed]
def create_vt_statistics(vtSchemas, sqlman, logman): #SAP Note 1872652: Creating statistics on a virtual table can be an expensive operation.
#Default statistics type: HISTOGRAM --> Creates a data statistics object that helps the query optimizer estimate the data distribution in a single-column data source --> Here we create a HISTOGRAM for ALL COLUMNS of the Virtual Tables
nVTs = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select count(*) from SYS.VIRTUAL_TABLES\"", shell=True).strip(' '))
nVTsWithoutStatBefore = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select COUNT(*) from SYS.VIRTUAL_TABLES where TABLE_NAME NOT IN (select distinct DATA_SOURCE_OBJECT_NAME from SYS.DATA_STATISTICS)\"", shell=True).strip(' '))
if not nVTsWithoutStatBefore:
return [nVTs, 0]
listOfVTsWithoutStat = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"select SCHEMA_NAME, TABLE_NAME from SYS.VIRTUAL_TABLES where TABLE_NAME NOT IN (select distinct DATA_SOURCE_OBJECT_NAME from SYS.DATA_STATISTICS)\"", shell=True).splitlines(1)
listOfVTsWithoutStat = [vt.strip('\n').strip('|').split('|') for vt in listOfVTsWithoutStat]
listOfVTsWithoutStat = [[elem.strip(' ') for elem in vt] for vt in listOfVTsWithoutStat]
for vt in listOfVTsWithoutStat:
if not vtSchemas or vt[0] in vtSchemas: #if schemas for virtual tables are provided, then only consider these schemas for creating statistics
columns = subprocess.check_output(sqlman.hdbsql_jAaxU + " \"select column_name from PUBLIC.TABLE_COLUMNS where table_name = '"+vt[1]+"' and schema_name = '"+vt[0]+"'\"", shell=True).splitlines(1)
columns =[col.strip('\n').strip('|').strip(' ') for col in columns]
columns = '\\\", \\\"'.join(columns) # necessary for columns with mixed letter case
sql = 'CREATE STATISTICS ON \\\"'+vt[0]+'\\\".\\\"'+vt[1]+'\\\" (\\\"'+columns+'\\\")' # necessary for tables starting with / and for tables with mixed letter case
errorlog = "\nERROR: The user represented by the key "+sqlman.key+" could not create statistics on "+vt[0]+"."+vt[1]+". \nOne possible reason for this is insufficient privilege\n"
errorlog += "\nTry, as the user represented by the key "+sqlman.key+" to simply do SELECT * FROM "+vt[0]+"."+vt[1]+". If that does not work then it could be that the privileges of source system's technical user (used in the SDA setup) is not sufficient.\n"
errorlog += "If there is another error (i.e. not insufficient privilege) then please try to execute \n"+sql+"\nin e.g. the SQL editor in SAP HANA Studio. If you get the same error then this has nothing to do with hanacleaner\n"
errorlog += "It could be that the respective ODBC driver was not properly set up. Please then follow the SAP HANA Administration Guide."
try_execute_sql(sql, errorlog, sqlman, logman)
nVTsWithoutStatAfter = int(subprocess.check_output(sqlman.hdbsql_jAQaxU + " \"select COUNT(*) from SYS.VIRTUAL_TABLES where TABLE_NAME NOT IN (select distinct DATA_SOURCE_OBJECT_NAME from SYS.DATA_STATISTICS)\"", shell=True).strip(' '))
return [nVTs, nVTsWithoutStatBefore - nVTsWithoutStatAfter]
def clean_output(minRetainedOutputDays, sqlman, logman):
path = logman.path
nFilesBefore = len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name))])
if sqlman.log:
log("find "+path+"/hanacleanerlog* -mtime +"+str(minRetainedOutputDays)+" -delete", logman)
if sqlman.execute:
subprocess.check_output("find "+path+"/hanacleanerlog* -mtime +"+str(minRetainedOutputDays)+" -delete", shell=True)
nFilesAfter = len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name))])
return nFilesBefore - nFilesAfter
def clean_anyfile(retainedAnyFileDays, anyFilePaths, anyFileWords, sqlman, logman):
removedFiles = 0
for path, word in zip(anyFilePaths, anyFileWords):
nFilesBefore = len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name)) and word in name])
with open(os.devnull, 'w') as devnull:
if sqlman.log:
log("find "+path+"/*"+word+"* -mtime +"+str(retainedAnyFileDays)+" -delete", logman)
if sqlman.execute:
try:
subprocess.check_output("find "+path+"/*"+word+"* -mtime +"+str(retainedAnyFileDays)+" -delete", shell=True, stderr=devnull)
except:
pass #File not found, but no need to warn about that
nFilesAfter = len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name)) and word in name])
removedFiles += nFilesBefore - nFilesAfter
return removedFiles
def checkAndConvertBooleanFlag(boolean, flagstring, logman):
boolean = boolean.lower()
if boolean not in ("false", "true"):
log("INPUT ERROR: "+flagstring+" must be either 'true' or 'false'. Please see --help for more information.", logman)
os._exit(1)
boolean = True if boolean == "true" else False
return boolean
def main():
##################### CHECK PYTHON VERSION ###########
if sys.version_info[0] != 2 or not sys.version_info[1] in [7]:
print "VERSION ERROR: hanacleaner is only supported for Python 2.7.x. Did you maybe forget to log in as <sid>adm before executing this?"
os._exit(1)
##################### DEFAULTS ####################
minRetainedBackups = "-1"
minRetainedDays = "-1" #days
deleteBackups = "false"
outputCatalog = "false"
outputDeletedCatalog = "false"
outputTraces = "false"
outputRemovedTraces = "false"
zipBackupLogsSizeLimit = "-1" #mb
zipBackupPath = cdalias('cdtrace')
zipLinks = "false"
zipOut = "false"
zipKeep = "true"
dbuserkeys = ["SYSTEMKEY"] # This/these KEY(S) has to be maintained in hdbuserstore
# so that hdbuserstore LIST gives e.g.
# KEY SYSTEMKEY
# ENV : mo-fc8d991e0:30015
# USER: SYSTEM
dbases = ['']
email_notif = []
retainedTraceContentDays = "-1"
retainedTraceFilesDays = "-1"
retainedDumpDays = "-1"
retainedAnyFileDays = "-1"
anyFilePaths = [""]
anyFileWords = [""]
minRetainedAlertDays = "-1" #days
minRetainedObjLockDays = "-1" #days
outputAlerts = "false"
outputDeletedAlerts = "false"
objHistMaxSize = "-1" #mb (default: not used)
outputObjHist = "false"
maxFreeLogsegments = "-1"
minRetainedDaysForHandledEvents = "-1" #days
minRetainedDaysForEvents = "-1" #days
retainedAuditLogDays = "-1"
fragmentationLimit = "-1" # percent
outputFragmentation = "false"
hanacleaner_interval = "-1"
rcContainers = "false"
outputRcContainers = "false"
maxRawComp = '-1' #number raw rows, e.g. 10000000
maxEstComp = '-1' #GB, e.g. 1
maxRowComp = '-1' #number rows, e.g. 10000000
maxMemComp = '-1' #MB, e.g. 500
minDistComp = '-1' #%, e.g. 5
maxQuotaComp = '-1' #%, e.g. 150
maxUDIVComp = '-1' #number rows, e.g. 10000000
maxBLOCKComp = '-1' #number rows, e.g. 100000
partComp = 'false'
mergeBeforeComp = 'false'
outComp = 'false'
createVTStat = 'false'
vtSchemas = None
minRetainedIniDays = "-1" #days
file_system = "" # by default check all file systems with df -h
flag_file = "" #default: no configuration input file