forked from erikng/Cacher
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cacher.py
executable file
·956 lines (891 loc) · 39.8 KB
/
cacher.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
#!/usr/bin/python
from datetime import date, timedelta
from distutils.version import LooseVersion
import glob
import json
import logging
import optparse
import os
import plistlib
import re
import shutil
import subprocess
import sys
import tempfile
import urllib2
"""Cacher rewritten in Python.
Inspired by Michael Lynn https://gist.github.com/pudquick/ffdbdb52ae6960ca8e55
This script will process Caching Server Debug Logs.
You can output this data to stdout, send it to the Apple email alert mechanism,
or send to a slack channel.
Slack section adapted from another one of my tools (APInfo).
https://github.com/erikng/scripts/tree/master/APInfo
Author: Erik Gomez
Last Updated: 06-08-2017
"""
version = '3.0.4'
def cacher(lines, targetDate, friendlyNames):
# Basically run through all the lines a single time and collect all the
# relevant data to slice, do stats with, etc.
noClientIdentityLog = []
sizeLog = []
AC2Log = []
IPLog = []
OSLog = []
osVersionLog = []
iOSModelLog = []
iOSModelOnlyLog = []
fileTypeLog = []
fileTypeUniqueLog = []
urlLog = []
urlUniqueLog = []
deviceNumberLog = []
finalOutput = []
FriendlyLog = []
macOSFamilyLog = []
macOSDeviceNumber = []
iOSFamilyLog = []
iOSDeviceNumber = []
AppleTVNumberLog = []
iPadNumberLog = []
iPhoneNumberLog = []
iPodNumberLog = []
# Friendly Darwin versions for macOS. This allows us to dynamically add
# the macOS version (for the alert), while dynamically looping through the
# logs.
friendlyDarwin = {
'17.0.0': '10.13.0',
'16.7.0': '10.12.6',
'16.6.0': '10.12.5',
'16.5.0': '10.12.4',
'16.4.0': '10.12.3',
'16.3.0': '10.12.2',
'16.1.0': '10.12.1',
'16.0.0': '10.12.0',
'10.12': '10.12.0', # match 10.12 to 10.12.0 for consistency
'15.6.0': '10.11.6',
'15.5.0': '10.11.5',
'15.4.0': '10.11.4',
'15.3.0': '10.11.3',
'15.2.0': '10.11.2',
'15.0.0': '10.11.0/1',
'14.5.0': '10.10.5',
'14.4.0': '10.10.4',
'14.3.0': '10.10.3',
'14.1.1': '10.12.2',
'14.1.0': '10.10.2',
'14.0.0': '10.10.0/1',
}
# Friendly Models of known models. This allows us to dynamically add the
# names to each model (for the alert), while dynamically looping through
# the logs.
friendlyModels = {
'AppleTV3,1': '3rd Generation Apple TVs',
'AppleTV3,2': '4th Generation Apple TVs',
'AppleTV5,3': '5th Generation Apple TVs',
'iPhone3,1': 'iPhone 4 [GSM]',
'iPhone3,2': 'iPhone 4 [GSM 2012]',
'iPhone3,3': 'iPhone 4 [CDMA]',
'iPhone4,1': 'iPhone 4S',
'iPhone5,1': 'iPhone 5 [GSM]',
'iPhone5,2': 'iPhone 5 [CDMA]',
'iPhone5,3': 'iPhone 5C',
'iPhone5,4': 'iPhone 5C [Global]',
'iPhone6,1': 'iPhone 5S',
'iPhone6,2': 'iPhone 5S [China Model]',
'iPhone7,1': 'iPhone 6 Plus',
'iPhone7,2': 'iPhone 6',
'iPhone8,1': 'iPhone 6S',
'iPhone8,2': 'iPhone 6S Plus',
'iPhone8,4': 'iPhone SE',
'iPhone9,1': 'iPhone 7 [Global]',
'iPhone9,2': 'iPhone 7 Plus [Global]',
'iPhone9,3': 'iPhone 7 [GSM]',
'iPhone9,4': 'iPhone 7 Plus [GSM]',
'iPhone10,1': 'iPhone 8 [Global]',
'iPhone10,2': 'iPhone 8 Plus [Global]',
'iPhone10,3': 'iPhone X [Global]',
'iPhone10,4': 'iPhone 8 [GSM]',
'iPhone10,5': 'iPhone 8 Plus [GSM]',
'iPhone10,6': 'iPhone X [GSM]',
'iPad2,1': 'iPad 2nd Generation [Wifi]',
'iPad2,2': 'iPad 2nd Generation [Wifi + GSM]',
'iPad2,3': 'iPad 2nd Generation [Wifi + CDMA]',
'iPad2,4': 'iPad 2nd Generation [M2012 Wifi Revision]',
'iPad2,5': 'iPad Mini 1st Generation [Wifi]',
'iPad2,6': 'iPad Mini 1st Generation [Wifi + GSM]',
'iPad2,7': 'iPad Mini 1st Generation [Wifi + CDMA]',
'iPad3,1': 'iPad 3rd Generation [Wifi]',
'iPad3,2': 'iPad 3rd Generation [Wifi + GSM]',
'iPad3,3': 'iPad 3rd Generation [Wifi + CDMA]',
'iPad3,4': 'iPad 4th Generation [Wifi]',
'iPad3,5': 'iPad 4th Generation [Wifi + GSM]',
'iPad3,6': 'iPad 4th Generation [Wifi + CDMA]',
'iPad4,1': 'iPad Air 1st Generation [Wifi]',
'iPad4,2': 'iPad Air 1st Generation [Wifi + Cellular]',
'iPad4,3': 'iPad Air 1st Generation [China Model]',
'iPad4,4': 'iPad Mini 2nd Generation [Wifi]',
'iPad4,5': 'iPad Mini 2nd Generation [Wifi + Cellular]',
'iPad4,6': 'iPad Mini 2nd Generation [China Model]',
'iPad4,7': 'iPad Mini 3rd Generation [Wifi]',
'iPad4,8': 'iPad Mini 3rd Generation [Wifi + Cellular]',
'iPad4,9': 'iPad Mini 3rd Generation [China Model]',
'iPad5,1': 'iPad Mini 4th Generation [Wifi]',
'iPad5,2': 'iPad Mini 4th Generation [Wifi + Cellular]',
'iPad5,3': 'iPad Air 2nd Generation [Wifi]',
'iPad5,4': 'iPad Air 2nd Generation [Wifi + Cellular]',
'iPad6,3': 'iPad Pro 9.7 Inch 1st Generation [Wifi]',
'iPad6,4': 'iPad Pro 9.7 Inch 1st Generation [Wifi + Cellular]',
'iPad6,7': 'iPad Pro 12.9 Inch 1st Generation [Wifi]',
'iPad6,8': 'iPad Pro 12.9 Inch 1st Generation [Wifi + Cellular]',
'iPad6,11': 'iPad 5th Generation [Wifi]',
'iPad6,12': 'iPad 5th Generation [Wifi + Cellular]',
'iPad7,1': 'iPad Pro 12.9 Inch 2nd Generation [Wifi]',
'iPad7,2': 'iPad Pro 12.9 Inch 2nd Generation [Wifi + Cellular]',
'iPad7,3': 'iPad Pro 10.5 Inch 1st Generation [Wifi]',
'iPad7,4': 'iPad Pro 10.5 Inch 1st Generation [Wifi + Cellular]',
'iPod5,1': 'iPod Touch 5th Generation',
'iPod7,1': 'iPod Touch 6th Generation'
}
totalbytesserved = []
totalbytesfromorigin = []
totalbytesfrompeers = []
for x in lines:
# If there aren't at least 3 pieces somehow, they'll get filled in
# with blanks
datestr, timestr, logmsg = (x.split(' ', 2) + ['', '', ''])[:3]
if datestr == targetDate:
# Only do work if the string is on the date we care about
# try:
linesplit = str.split(logmsg)
# split the logmsg line (by spaces) so I can hardcode some
# calls. Fragile (could break with a Server update) but it meh.
# Beginning of Server bandwidth section
#
# This is a slightly less fragile method to calculate the
# amount of data the caching server has served.
# Eg:
# Served all 39.2 MB of 39.2 MB; 3 KB from cache,
# 39.2 MB stored from Internet, 0 bytes from peers
if 'Served all' in logmsg:
total_served_size = linesplit[3]
total_served_bwtype = linesplit[4]
fromorigin_size = linesplit[12]
fromoriginbwtype = linesplit[13]
frompeers_size = linesplit[17]
frompeersbwtype = linesplit[18]
# Convert size of served to client to bytes
if total_served_bwtype == 'KB':
bytes_served = "%.0f" % (
float(total_served_size) * 1024)
elif total_served_bwtype == 'MB':
bytes_served = "%.0f" % (
float(total_served_size) * 1048576)
elif total_served_bwtype == 'GB':
bytes_served = "%.0f" % (
float(total_served_size) * 1073741824)
elif total_served_bwtype == 'TB':
bytes_served = "%.0f" % (
float(total_served_size) * 1099511627776)
elif total_served_bwtype == 'bytes':
bytes_served = total_served_size
# Convert size of from internet(origin) to bytes
if fromoriginbwtype == 'KB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1024)
elif fromoriginbwtype == 'MB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1048576)
elif fromoriginbwtype == 'GB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1073741824)
elif fromoriginbwtype == 'TB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1099511627776)
elif fromoriginbwtype == 'bytes':
bytesfromorigin = fromorigin_size
# Convert size of from peers to bytes
if frompeersbwtype == 'KB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1024)
elif frompeersbwtype == 'MB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1048576)
elif frompeersbwtype == 'GB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1073741824)
elif frompeersbwtype == 'TB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1099511627776)
elif frompeersbwtype == 'bytes':
bytesfrompeers = frompeers_size
# Append each bw size to the total count
totalbytesserved.append(bytes_served)
totalbytesfromorigin.append(bytesfromorigin)
totalbytesfrompeers.append(bytesfrompeers)
# Search through the logs for incomplete transactions (served)
if 'Served all' not in logmsg and 'Served' in logmsg:
total_served_size = linesplit[2]
total_served_bwtype = linesplit[3]
fromorigin_size = linesplit[11]
fromoriginbwtype = linesplit[12]
frompeers_size = linesplit[16]
frompeersbwtype = linesplit[17]
# Convert size of from cache to bytes
if total_served_bwtype == 'KB':
bytes_served = "%.0f" % (
float(total_served_size) * 1024)
elif total_served_bwtype == 'MB':
bytes_served = "%.0f" % (
float(total_served_size) * 1048576)
elif total_served_bwtype == 'GB':
bytes_served = "%.0f" % (
float(total_served_size) * 1073741824)
elif total_served_bwtype == 'TB':
bytes_served = "%.0f" % (
float(total_served_size) * 1099511627776)
elif total_served_bwtype == 'bytes':
bytes_served = total_served_size
# Convert size of from internet(origin) to bytes
if fromoriginbwtype == 'KB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1024)
elif fromoriginbwtype == 'MB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1048576)
elif fromoriginbwtype == 'GB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1073741824)
elif fromoriginbwtype == 'TB':
bytesfromorigin = "%.0f" % (
float(fromorigin_size) * 1099511627776)
elif fromoriginbwtype == 'bytes':
bytesfromorigin = fromorigin_size
# Convert size of from peers to bytes
if frompeersbwtype == 'KB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1024)
elif frompeersbwtype == 'MB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1048576)
elif frompeersbwtype == 'GB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1073741824)
elif frompeersbwtype == 'TB':
bytesfrompeers = "%.0f" % (
float(frompeers_size) * 1099511627776)
elif frompeersbwtype == 'bytes':
bytesfrompeers = frompeers_size
# Append each bw size to the total count
totalbytesserved.append(bytes_served)
totalbytesfromorigin.append(bytesfromorigin)
totalbytesfrompeers.append(bytesfrompeers)
# Beginning of Server downloads section
#
#
if 'Received GET request by' in logmsg:
noClientIdentityLog.append(logmsg)
elif 'Received GET request from' in logmsg:
# Beginning of IP section
#
#
# Ex: '149.166.73.137:56833'. Split 6th string at ':' and
# pull only pull first value.
ip = linesplit[5].split(":")[0]
IPLog.append(ip)
#
#
# End of IP section
# Beginning of URL section
#
#
# The URL is always at the end so take the split line and
# pull its value.
URL = linesplit[-1]
urlLog.append(URL)
#
#
# End of URL section
# Beginning of OS Family, OS Version and Device section
#
#
# Example: 'Darwin/15.0.0', 'iOS/10.0.2' or 'OS X 10.12.0'
# Replace Look for iOS, Darwin or OS X. If OS X is found,
# Add 'macOS/' to force the consistency and split the
# string at '/'. This allows us to use 'macOS/10.12.2' for
# both osFamily (Ex: macOS) and osVersion (Ex: 10.12.2).
osFamily = re.match(
r'.+? ((iOS|Darwin|OS X)[/ ](([0-9]+\.?){1,}))',
x)
if osFamily is not None:
osFamily = osFamily.group(1).replace(
'OS X ', 'macOS/').split('/')[0]
osVersion = re.match(
r'.+? ((iOS|Darwin|OS X)[/ ](([0-9]+\.?){1,}))',
x)
if osVersion is not None:
osVersion = osVersion.group(1).replace(
'OS X ', 'macOS/').split('/')[1]
configurator = re.match(
r'.+?((Configurator)[/ ](([0-9]+\.?){1,}))',
x)
if configurator is not None:
configurator = configurator.group(1).split('/')[0]
# If 'Darwin' in the name, replace to 'macOS' so our future
# counts will be accurate.
if osFamily == 'Darwin':
osFamily = 'macOS'
# Loop through the friendlyDarwin key/value pairs and if
# osVersion is equal to the key (Ex: 16.3.0) replace it
# with its value (Ex: 10.12.2). This is also a fix for the
# count. Yay for coding in a bubble!
if osVersion is not None:
for k, v in friendlyDarwin.items():
if k == osVersion:
osVersion = v
# The iOS family is more fun, in that Caching Server logs
# the model identifier.
if osFamily == 'iOS':
# Ex: 'model/iPhone7,2'.
iOSModel = re.match(
r'.+? model/([^ ]+?[0-9]+,?[0-9])?', x)
# Since the regular expression is now two goups, only
# take the date from the 2nd group.
# Write the osVersion/osFamily data to iOSModelLog,
# iOSModelOnlyLog and OSLog.
iOSModelLog.append((osVersion, iOSModel.group(1)))
iOSModelOnlyLog.append(iOSModel.group(1))
OSLog.append((osVersion, osFamily))
elif osFamily == 'macOS':
# Write the osVersion/osFamily data to OSLog.
OSLog.append((osVersion, osFamily))
elif configurator == 'Configurator':
AC2Log.append(configurator)
# if 'model/AppleTV' in logmsg:
# I think I still need to do this section but I can't
# remember.
#
#
# End of OS Family, OS Version and Device section
# Beginning of File Type section
#
#
# Regular Expression Part. Using the URL (split early),
# Look for the recognized filetypes (.pkg, .ipa, .ipsw,
# .zip and .epub). Ex:
# 1. '/a-09f98d6971/pre-thinned756.thinned.signed.dpkg.ipa'
# 2. '/031-8/com_apple_MobileAsset_CoreSuggestion/6c93.zip'
# 3. '[icloud:hvRq3yMBV7JO9hUBRo2p]'
if re.match(r'.+(\.pkg|\.ipa|\.ipsw|\.zip|\.epub)', URL):
fileType = re.match(
r'.+(\.pkg|\.ipa|\.ipsw|\.zip|\.epub)', URL)
fileTypeLog.append(fileType.group(1))
# Notice Example 3 posted above. Those are the odd URLs for
# Personal iCloud data. Since it has no discernable suffix,
# log a value of 'personal iCloud'. :shrug:
elif re.match(r'.+(\icloud)', URL):
fileType = re.match(r'.+(\icloud)', URL)
fileTypeLog.append('personal iCloud')
#
#
# End of File Type section
#
#
# End of Server downloads section
# except:
# print x
# raise Exception("Funky line - check it out")
# Beginning of the final output.
#
#
# Append to a new list. This then allows us to call it whenever we need.
# We can then put this into the Server Alert, stdout, Slack, etc.
finalOutput.append(
'Cacher has retrieved the following stats for %s:' % targetDate)
finalOutput.append('')
# Add up our bytes from each store from our list to get a total
totalbytesserved = sum(map(int, totalbytesserved))
totalbytesfromorigin = sum(map(int, totalbytesfromorigin))
totalbytesfrompeers = sum(map(int, totalbytesfrompeers))
# Bail here since there aren't any bandwidth stats.
if not totalbytesserved:
print 'Cacher did not retrieve any stats for %s' % targetDate
sys.exit(1)
finalOutput.append(
'%s of bandwith served to client devices.' % (
convert_bytes_to_human_readable(totalbytesserved)))
finalOutput.append(
' %s of bandwith requested from Apple' % (
convert_bytes_to_human_readable(totalbytesfromorigin)))
finalOutput.append(
' %s of bandwith requested from other Caching Servers' % (
convert_bytes_to_human_readable(totalbytesfrompeers)))
finalOutput.append('')
# Total Numbers of IP addresses
finalOutput.append(
'%s IP Addresses hit the Caching Server yesterday consisting'
' of:' % len(IPLog))
finalOutput.append(' %s Unique IP Addresses.' % len(set(IPLog)))
finalOutput.append('')
# Total Number of iOS devices
# Don't display if 0 downloads
if len(iOSModelOnlyLog) > 0:
finalOutput.append(
'A total of %s iOS downloads were requested '
'from the Caching Server yesterday consisting of:'
% len(iOSModelOnlyLog))
# Sort the list by device type (AppleTV, iPad, iPhone, iPod). If we aren't
# using the friendly names, we use the standard sorting, but if we use the
# friendly names, we will sort the list at the very end.
if friendlyNames:
# Friendly Name Sorting:
# In order to sort the friendly names properly, we create a new list,
# counting the amount of devices and swapping the key/value pairs from
# the friendly names. Since we have to sort by friendly name, we create
# a new list based off the following: modeltype/numberofdevices. We
# then split this output on the "/" which gives us the number of the
# number of devices and the modeltype in proper order.
# Example:
# iPhone3,1 becomes iPhone 4 [GSM]/numberofDevices which is then sorted
# and finally split.
for x in set(iOSModelOnlyLog):
numberofDevices = iOSModelOnlyLog.count(x)
modeltype = x
for k, v in friendlyModels.items():
if k == modeltype:
modeltype = v
FriendlyLog.append('%s/%s' % (modeltype, numberofDevices))
if 'Apple TV' in modeltype:
AppleTVNumberLog.append('%s' % numberofDevices)
elif 'iPad' in modeltype:
iPadNumberLog.append('%s' % numberofDevices)
elif 'iPhone' in modeltype:
iPhoneNumberLog.append('%s' % numberofDevices)
elif 'iPod' in modeltype:
iPodNumberLog.append('%s' % numberofDevices)
# Force conversion of lists to int
AppleTVNumberLog = [int(i) for i in AppleTVNumberLog]
iPadNumberLog = [int(i) for i in iPadNumberLog]
iPhoneNumberLog = [int(i) for i in iPhoneNumberLog]
iPodNumberLog = [int(i) for i in iPodNumberLog]
# Output
if sum(AppleTVNumberLog) > 0:
finalOutput.append(
' A total of %s Apple TV downloads' % sum(AppleTVNumberLog))
if sum(iPadNumberLog) > 0:
finalOutput.append(
' A total of %s iPad downloads' % sum(iPadNumberLog))
if sum(iPhoneNumberLog) > 0:
finalOutput.append(
' A total of %s iPhone downloads' % sum(iPhoneNumberLog))
if sum(iPodNumberLog) > 0:
finalOutput.append(
' A total of %s iPod downloads' % sum(iPodNumberLog))
for x in sorted(set(FriendlyLog)):
numberofDevices = x.split('/')[1]
modeltype = x.split('/')[0]
finalOutput.append(' %s %s' % (numberofDevices, modeltype))
else:
# Non Friendly Name Sorting:
# This one is easier than friendly names as it's alphabetized by
# sorted(). Count the devices and prefix it on the output.
for x in sorted(set(iOSModelOnlyLog)):
numberofDevices = iOSModelOnlyLog.count(x)
modeltype = x
if 'AppleTV' in modeltype:
AppleTVNumberLog.append('%s' % numberofDevices)
elif 'iPad' in modeltype:
iPadNumberLog.append('%s' % numberofDevices)
elif 'iPhone' in modeltype:
iPhoneNumberLog.append('%s' % numberofDevices)
elif 'iPod' in modeltype:
iPodNumberLog.append('%s' % numberofDevices)
# Force conversion of lists to int
AppleTVNumberLog = [int(i) for i in AppleTVNumberLog]
iPadNumberLog = [int(i) for i in iPadNumberLog]
iPhoneNumberLog = [int(i) for i in iPhoneNumberLog]
iPodNumberLog = [int(i) for i in iPodNumberLog]
# Output
if sum(AppleTVNumberLog) > 0:
finalOutput.append(
' A total of %s Apple TV downloads' % sum(AppleTVNumberLog))
if sum(iPadNumberLog) > 0:
finalOutput.append(
' A total of %s iPad downloads' % sum(iPadNumberLog))
if sum(iPhoneNumberLog) > 0:
finalOutput.append(
' A total of %s iPhone downloads' % sum(iPhoneNumberLog))
if sum(iPodNumberLog) > 0:
finalOutput.append(
' A total of %s iPod downloads' % sum(iPodNumberLog))
for x in sorted(set(iOSModelOnlyLog)):
numberofDevices = iOSModelOnlyLog.count(x)
modeltype = x
finalOutput.append(' %s %s' % (numberofDevices, modeltype))
finalOutput.append('')
# Total Number of OS Versions
if len(OSLog) > 0:
finalOutput.append(
'A total of %s OS downloads were requested from the Caching Server'
' yesterday consisting of:' % len(OSLog))
for x in sorted(set(OSLog)):
numberofVersions = OSLog.count(x)
osversion = x[0]
osfamily = x[1]
if osfamily == 'macOS':
macOSFamilyLog.append(
'%s/%s' % (osfamily + ' ' + osversion, numberofVersions))
macOSDeviceNumber.append(numberofVersions)
elif osfamily == 'iOS':
iOSFamilyLog.append(
'%s/%s' % (osfamily + ' ' + osversion, numberofVersions))
iOSDeviceNumber.append(numberofVersions)
# Sort the iOS versions with LooseVersion. StrictVersion fails since I am
# cheating and adding /devicecount to the version. (Ex. iOS 10.2/2000)
if sum(iOSDeviceNumber) > 0:
finalOutput.append(' %s iOS downloads:' % sum(iOSDeviceNumber))
for x in sorted(set(iOSFamilyLog), key=LooseVersion):
numberofVersions = x.split('/')[1]
modeltype = x.split('/')[0]
finalOutput.append(' %s %s' % (numberofVersions, modeltype))
# Sort the macOS versions normally, since they all start with 10.
if sum(macOSDeviceNumber) > 0:
finalOutput.append(' %s macOS downloads:' % sum(macOSDeviceNumber))
for x in sorted(set(macOSFamilyLog)):
numberofVersions = x.split('/')[1]
modeltype = x.split('/')[0]
finalOutput.append(' %s %s' % (numberofVersions, modeltype))
finalOutput.append('')
# Total Number of Apple Configurator 2 files.
# I need logs with Apple Configurator 2 references so I can rewrite this.
# Since you can't disintinguish between the version of AC2, I'm removing
# the secondary line I had in the shell version.
if len(AC2Log) > 0:
finalOutput.append(
'A total of %s files were requested from Apple'
' Configurator 2 devices' % len(AC2Log))
finalOutput.append('')
# Total Number of filetypes downloaded and their respect numbers
if len(fileTypeLog) > 0:
finalOutput.append(
'A total of %s files were downloaded from the Caching'
' Server yesterday consisting of:' % len(fileTypeLog))
for x in set(fileTypeLog):
numberofFiles = fileTypeLog.count(x)
finalOutput.append(' %s %s files' % (numberofFiles, x))
finalOutput.append('')
# Total Number of unique filetypes downloaded and their respect numbers
urlUniqueLog = set(urlLog)
if len(urlUniqueLog) > 0:
finalOutput.append(
'A total of %s unique files were downloaded from the'
' Caching Server yesterday consisting'
' of:' % len(urlUniqueLog))
# Same logic taken from "File Type Section" so I'm not documenting it.
for x in urlUniqueLog:
if re.match(r'.+(\.pkg|\.ipa|\.ipsw|\.zip|\.epub)', x):
fileType = re.match(
r'.+(\.pkg|\.ipa|\.ipsw|\.zip|\.epub)', x)
fileTypeUniqueLog.append(fileType.group(1))
elif re.match(r'.+(\icloud)', URL):
fileType = re.match(r'.+(\icloud)', x)
fileTypeUniqueLog.append('personal iCloud')
for x in set(sorted(fileTypeUniqueLog)):
numberofFiles = fileTypeUniqueLog.count(x)
finalOutput.append(' %s %s files' % (numberofFiles, x))
finalOutput.append('')
# Add Cacher version
finalOutput.append('Cacher version: %s' % version)
finalOutput.append('Uptime: %s' % get_uptime())
# Check to see if there are entries in the noClientLog. If there are,
# print to final message to warn the user.
if noClientIdentityLog:
finalOutput.append('')
finalOutput.append(
"WARNING: Found %s logs that did not contain "
"the client identity. These logs have been dropped and are not "
"counted in the statistics. More than likely LogClientIdentity "
"was incorrectly set or not configured on this date."
% len(noClientIdentityLog))
#
#
# End of the final output.
return finalOutput
# print("\n".join(finalOutput))
def convert_bytes_to_human_readable(number_of_bytes):
if number_of_bytes < 0:
raise ValueError("ERROR: number of bytes can not be less than 0")
step_to_greater_unit = 1024.
number_of_bytes = float(number_of_bytes)
unit = 'bytes'
if (number_of_bytes / step_to_greater_unit) >= 1:
number_of_bytes /= step_to_greater_unit
unit = 'KB'
if (number_of_bytes / step_to_greater_unit) >= 1:
number_of_bytes /= step_to_greater_unit
unit = 'MB'
if (number_of_bytes / step_to_greater_unit) >= 1:
number_of_bytes /= step_to_greater_unit
unit = 'GB'
if (number_of_bytes / step_to_greater_unit) >= 1:
number_of_bytes /= step_to_greater_unit
unit = 'TB'
precision = 1
number_of_bytes = round(number_of_bytes, precision)
return str(number_of_bytes) + ' ' + unit
def check_serverconfig():
try:
config = '/Library/Server/Caching/Config/Config.plist'
plist = plistlib.readPlist(config)
return plist['LogClientIdentity']
except Exception:
return None
def get_serverversion():
try:
serverversion = '/Applications/Server.app/Contents/version.plist'
plist = plistlib.readPlist(serverversion)
return plist['CFBundleShortVersionString']
except Exception:
return None
def get_uptime():
try:
cmd = ['/usr/bin/uptime']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = proc.communicate()
splitout = str.split(output)
uptimeamount = splitout[2]
# Uptime Type to use if units are "less than days"
uptimetype = splitout[3].replace(',', '')
if uptimeamount[-1:]==',':
# Last char is a comma; this likely indicates that the
# `uptimetype` is in hours and not in a "greater" unit
uptimeamount = uptimeamount[:-1] # get rid of the comma at the end
uptimeamount = uptimeamount.split(':')
hourtype = ' hour, ' if uptimeamount[0]==1 else ' hours, '
uptimeamount = uptimeamount[0] + hourtype + uptimeamount[1]
# `uptimetype` to use if main units are in hours and minutes,
# which is not the best way to handle this... but it works.
uptimetype = 'minutes'
return '%s %s' % (uptimeamount, uptimetype)
except Exception:
return None
def send_serveralert(targetDate, cacherdata):
try:
# Change to a directory to remove shell error
os.chdir('/private/tmp')
# Mehhhhhhhhhhhhhh
cmd = ['/Applications/Server.app/Contents/ServerRoot/usr/sbin/server '
'postAlert CustomAlert Common subject ' + '"'
'Caching Server Data: ' + targetDate + '"' + ' message '
'"' + cacherdata + '"<<<""']
subprocess.check_call(cmd, shell=True)
except Exception:
return None
def configureserver():
try:
cmd = [
'/Applications/Server.app/Contents/ServerRoot/usr/sbin/server'
'admin', 'settings', 'caching:LogClientIdentity = yes']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.rstrip()
except Exception:
return None
def serveradmin(action, service):
try:
cmd = [
'/Applications/Server.app/Contents/ServerRoot/usr/sbin/server'
'admin', action, service]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = proc.communicate()
return output.rstrip()
except Exception:
return None
def post_to_slack(targetDate, cacherdata, slackchannel, slackusername,
slackwebhook):
# Server App Icon DL
url = 'https://itunes.apple.com/lookup?id=883878097'
try:
request = urllib2.urlopen(url)
jsondata = json.loads(request.read())
iconurl = jsondata['results'][0]['artworkUrl100']
except (urllib2.URLError, ValueError, KeyError) as e:
# hardcode icon url in case it fails.
iconurl = 'http://is5.mzstatic.com/image/thumb/Purple122/v4/b9/e8/c4' \
'/b9e8c4b9-ce9c-174a-c1a8-d0ad0fc21da9/source/100x100bb.png'
# Slack payload
payload = {
"channel": slackchannel,
"username": slackusername,
"icon_url": iconurl,
"attachments": [
{
'pretext': 'Caching Server Data ' + targetDate,
'text': cacherdata
}
]
}
try:
cmd = ['/usr/bin/curl', '-X', 'POST', '--data-urlencode',
'payload=' + json.dumps(payload), slackwebhook]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = proc.communicate()
except Exception:
print 'Failed to send message to Slack'
def main():
# Check for macOS Server 5.2 or higher. Use LooseVersion just in case.
if LooseVersion(get_serverversion()) >= LooseVersion('5.2'):
pass
else:
print "Server version is %s and not compatible" % get_serverversion()
sys.exit(1)
# Options
usage = '%prog [options]'
o = optparse.OptionParser(usage=usage)
o.add_option('--targetdate',
help=('Optional: Date to parse. Example: 2017-01-15.'))
o.add_option('--logpath',
help=('Optional: Caching Log Path. Defaults to: '
'/Library/Server/Caching/Logs'))
o.add_option('--deviceids',
help='Optional: Use Device IDs (Ex: iPhone7,2). Defaults'
' to: False',
action='store_true')
o.add_option('--nostdout',
help='Optional: Do not print to standard out',
action='store_true')
o.add_option('--configureserver',
help='Optional: Configure Server to log Client Data',
action='store_true')
o.add_option('--serveralert',
help='Optional: Send Server Alert',
action='store_true')
o.add_option("--slackalert", action="store_true", default=False,
help=("Optional: Use Slack"))
o.add_option("--slackwebhook", default=None,
help=("Optional: Slack Webhook URL. Requires Slack Option."))
o.add_option("--slackusername", default=None,
help=("Optional: Slack username. Defaults to Cacher."
"Requires Slack Option."))
o.add_option("--slackchannel", default=None,
help=("Optional: Slack channel. Can be username or channel "
"Ex. #channel or @username. Requires Slack Option."))
opts, args = o.parse_args()
# Configure Server
if opts.configureserver:
configureServer = True
else:
configureServer = False
if configureServer:
if os.getuid() != 0:
print 'Did not configure Caching Server - requires root'
sys.exit(1)
else:
print 'Caching Server settings are now: ' + configureserver()
print '\nRestarting Caching Service...'
print '\n' + serveradmin('stop', 'caching')
print '\n' + serveradmin('start', 'caching')
sys.exit(1)
# Check if LogClientIdentity is configured correctly. If it isn't - bail.
serverconfig = check_serverconfig()
if serverconfig is True:
pass
elif type(serverconfig) is str or type(serverconfig) is int:
print "LogClientIdentity is incorrectly set to: %s - Type: %s" \
% (str(serverconfig), type(serverconfig).__name__)
print "Please run sudo Cacher --configureserver and delete your " \
"log files."
sys.exit(1)
elif not serverconfig:
print "LogClientIdentity is not set"
print "Please run sudo Cacher --configureserver and delete your " \
"log files."
sys.exit(1)
else:
print "LogClientIdentity is set to: %s" % str(serverconfig)
print "Please run sudo Cacher --configureserver and delete your " \
"log files."
sys.exit(1)
# Grab other options
if opts.targetdate:
targetDate = opts.targetdate
else:
targetDate = str(date.today() - timedelta(1))
if opts.logpath:
logPath = opts.logpath
else:
logPath = '/Library/Server/Caching/Logs'
if opts.deviceids:
friendlyNames = False
else:
friendlyNames = True
if opts.nostdout:
stdOut = False
else:
stdOut = True
if opts.serveralert:
serverAlert = True
else:
serverAlert = False
if opts.slackalert:
slackAlert = True
else:
slackAlert = False
slackalert = opts.slackalert
slackwebhook = opts.slackwebhook
if opts.slackusername:
slackusername = opts.slackusername
else:
slackusername = 'Cacher'
slackchannel = opts.slackchannel
# Check if log files exist and if not, bail. Try to delete .DS_Store files
# just in case they exist from the GUI. Chances are we can delete this
# because we are either running as root or the same user that created it.
try:
os.remove(os.path.join(logPath, '.DS_Store'))
except OSError:
pass
if not os.listdir(logPath):
print 'Cacher did not detect log files in %s' % logPath
sys.exit(1)
# Make temporary directory
tmpDir = tempfile.mkdtemp()
# Clone the contents of serverlogs over into the 'cachinglogs' subdirectory
tmpLogs = os.path.join(tmpDir, 'cachinglogs')
shutil.copytree(logPath, tmpLogs)
# Expand any .bz files in the directory (Server 4.1+)
os.chdir(tmpLogs)
for bzLog in glob.glob(os.path.join(tmpLogs, '*.bz2')):
result = subprocess.check_call(["bunzip2", bzLog])
# Now combine all .log files in the destination into a temp file that's
# removed when python exits
rawLog = tempfile.TemporaryFile()
# We only care about Debug logs, not service logs
for anyLog in glob.glob(os.path.join(tmpLogs, 'Debug*')):
with open(anyLog, 'rb') as f:
shutil.copyfileobj(f, rawLog)
# Skip back to the beginning of our newly concatenated log
rawLog.seek(0)
# Purge temporary directory since it's now in memory.
shutil.rmtree(tmpDir)
# Run the function that does most of the work.
cacherdata = cacher(rawLog.readlines(), targetDate, friendlyNames)
# Output conditionals
if stdOut:
print("\n".join(cacherdata))
if slackAlert:
print ''
if serverAlert:
if os.getuid() != 0:
print 'Did not send serverAlert - requires root'
else:
send_serveralert(targetDate, "\n".join(cacherdata))
if slackalert is True:
post_to_slack(targetDate, "\n".join(cacherdata), slackchannel,
slackusername, slackwebhook)
if __name__ == '__main__':
main()