-
Notifications
You must be signed in to change notification settings - Fork 3
/
panel_gen.py
executable file
·1478 lines (1209 loc) · 51.2 KB
/
panel_gen.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/python
#---------------------------------------------------------------------#
# #
# A call generator thing for the telephone switches at the #
# Connections Museum, Seattle WA. #
# #
# Written by Sarah Autumn, 2017-2020 #
# sarah@connectionsmuseum.org #
# github.com/theautumn/panel_gen #
# #
#---------------------------------------------------------------------#
from time import sleep
from os import system
import signal
import subprocess
import argparse
import logging
import uuid
import curses
import re
import threading
import sys
from configparser import ConfigParser
from datetime import datetime
from marshmallow import Schema, fields, post_load
from tabulate import tabulate
from numpy import random
from pycall import CallFile, Call, Application, Context
from asterisk.ami import AMIClient, EventListener, AMIClientAdapter
class Line():
"""
This class defines Line objects.
switch: Set to an instantiated switch object. Usually Rainier,
Adams, or Lakeview
kind: Type of switch for above objects. "panel, 1xb, 5xb"
status: 0 = OnHook, 1 = OffHook
term: String containing the 7-digit terminating line.
timer: Starts with a standard random.gamma, then gets set
subsequently by the call volume attribute of the switch.
ident: Integer starting with 0 that identifies the line.
human_term: Easily readable called line number, for my dyslexic ass.
chan: DAHDI channel the call is being placed on.
magictoken: UUID generated each time a callfile is passed to
Asterisk. Asterisk sends it back to us via AMI, and we
match it against our call.
ast_status: Returned from AMI. Indicates status of line from
Asterisk's perspective.
ami_tmr: Set when we ask Asterisk to do something. Number of seconds to wait
before we expect an AMI event response.
switching_delay: Set when a call is made that requires extra time in the dialing
state, such as a call via ANI trunks.
pending_* Set to true if this line is pending action by Asterisk.
Set to false when Asterisk confirms it took action.
"""
def __init__(self, ident, switch, **kwargs):
self.switch = switch
self.kind = switch.kind
self.status = 0
self.term = self.pick_next_called(term_choices)
self.timer = random.gamma(3,4)
self.ident = ident
self.human_term = phone_format(self.term)
self.chan = '-'
self.magictoken = ""
self.ast_status = 'on_hook'
self.ami_tmr = 0
self.switching_delay = 0
self.longdistance = False
self.pending_call = False
self.pending_dialend = False
self.pending_hangup = False
def __repr__(self):
return 'Line('+ repr(self.ident) + ', ' + repr(self.term) +')'
def tick(self):
"""
Decrement line timer.
Manages the line's state machine by placing calls or hanging up,
depending on status.
Returns the new value of self.timer
"""
try:
if self.switch.running == False:
self.switch.running = True
self.timer -= 0.10
self.ami_tmr -= 0.10
if self.timer <= 0:
if self.ast_status == "on_hook":
if self.switch.is_dialing < self.switch.max_dialing:
self.call()
else:
# Back off until some calls complete.
self.timer = random.gamma(4,4)
logging.warning("Hit sender limit: %s with %s calls " +
"dialing. Delaying call.",
self.switch.max_dialing, self.switch.is_dialing)
elif self.ast_status == "Dialing" or self.ast_status == "Ringing":
self.hangup()
# Check to make sure we're still sane :)
safetynet()
except Exception as e:
logging.exception(e)
return self.timer
def pick_next_called(self, term_choices):
"""
Returns a string containing a 7-digit number to call.
term_choices: List of office codes. Comes from config file
"""
if len(NXX) != len(self.switch.trunk_load):
logging.error("Check your config file! \"nxx\" is of a length %s " +
"and the trunk load of %s switch is %s",
len(NXX), self.switch.kind, len(self.switch.trunk_load))
logging.error("Also check the switch class for the presence of each " +
"trunk load variable that exists in config file.")
if term_choices == []:
term_office = random.choice(NXX, p=self.switch.trunk_load)
else:
term_office = random.choice(term_choices)
# Choose a sane number that appears on the line link or final
# frame of the switches that we're actually calling. If something's
# wrong, then assert false, so it will get caught.
if term_office == 722 or term_office == 365:
term_station = random.randint(Rainier.line_range[0], Rainier.line_range[1])
elif term_office == 832 or term_office == 833 or term_office == 524:
term_station = random.choice(Lakeview.line_range)
elif term_office == 232:
term_station = random.choice(Adams.line_range)
elif term_office == 275:
term_station = random.randint(Step.line_range[0], Step.line_range[1])
elif term_office == 830:
term_station = random.randint(ESS3.line_range[0], ESS3.line_range[1])
else:
logging.error("No terminating line available for this office.")
assert False
term = str(term_office) + str(term_station)
logging.debug('Terminating line selected: %s', term)
self.human_term = phone_format(term)
return term
def call(self, **kwargs):
"""
Places a call. Returns nothing.
kwargs:
originating_switches: switch call is coming from
line: line placing the call
timer: duration of the call
"""
nextchan = self.switch.newchannel(self.switch.channel_choices)
if nextchan == False:
self.timer = random.gamma(4,4)
return
pred = ''
if self.switch.ld_capable == True: # Set in config.
pred = longdistance(self, nextchan)
#CHANNEL = 'DAHDI/{}'.format(self.switch.dahdi_group) + '/wwww%s' % self.term
CHANNEL = 'DAHDI/{}'.format(nextchan) + '/wwww%s' % pred+self.term
logging.debug('To Asterisk: %s on ident %s', CHANNEL, self.ident)
self.timer = self.switch.newtimer()
# Wait value to pass to Asterisk. (We will actually be controlling the
# hangup from here, but this is kind of a safety net so asterisk dumps
# the call if we can't for some reason.)
wait = int(self.timer) + 7
# OoOOOoOOoOOOO!
self.magictoken = str(uuid.uuid4())
# Set wait time for asterisk to auto hangup.
vars = {'waittime': wait}
cid = 'panel_gen <{}>'.format(self.switch.kind)
self.ami_tmr = 4
self.pending_call = True
logging.debug('About to create .call file for line %s', self.ident)
logging.debug('Magic Token: %s', self.magictoken)
# Make the .call file amd throw it into the asterisk spool.
# Pass control of the call to the sarah_callsim context in
# the dialplan.
# Set accountcode to our magic UUID for use later.
c = Call(CHANNEL, variables=vars, callerid=cid,
account=self.magictoken)
con = Context('sarah_callsim', pred+self.term, '1')
cf = CallFile(c, con)
cf.spool()
def hangup(self):
"""
Hangs up a call.
Send an AMI hangup request to Asterisk,
Set a timer to wait for Asterisk's response.
Response is handled in on_Hangup()
"""
adapter.Hangup(Channel='DAHDI/{}-1'.format(self.chan))
self.pending_hangup = True
self.ami_tmr = 3
logging.debug('2: Asked Asterisk to hangup %s on DAHDI/%s, line %s',
self.term, self.chan, self.ident)
self.switching_delay = 0
self.longdistance = False
logging.debug("Pending hangup: %s", self.pending_hangup)
class Switch():
"""
This class is parameters and methods for a switch.
kind: Generic name for type of switch.
running: Whether or not switch is running.
max_dialing: Set based on sender capacity.
is_dialing: Records current number of calls in Dialing state.
dahdi_group: Passed to Asterisk when call is made.
traffic_load: String that contains "light", "heavy", or "normal".
Sets the random.gamma distribution for generating
new call timers.
lines_normal: Number of lines to use in normal traffic mode.
lines_heavy: Number of lines to use in heavy traffic mode.
max_nxx: Values for trunk load. Determined by how many
outgoing trunks we have provisioned on the switch.
trunk_load: List of max_nxx used to compute load on trunks.
line_range: Range of acceptable lines to dial when calling this office.
"""
def __init__(self, **kwargs):
self.kind = kwargs.get('kind',"")
kind = self.kind
self.running = False
self.max_dialing = config.getint(kind, 'max_dialing')
self.is_dialing = 0
self.on_call = 0
self.dahdi_group = config.get(kind, 'dahdi_group')
self.channel_choices = config.get(kind, 'channels').split(",")
self.ld_capable = config.getboolean(kind, 'long_distance')
self.traffic_load = "normal"
self.lines_normal = config.getint(kind, 'lines_normal')
self.lines_heavy = config.getint(kind, 'lines_heavy')
self.max_722 = float(config[kind]['max_722'])
self.max_232 = float(config[kind]['max_232'])
self.max_832 = float(config[kind]['max_832'])
self.max_275 = float(config[kind]['max_275'])
self.max_365 = float(config[kind]['max_365'])
self.max_830 = float(config[kind]['max_830'])
self.max_833 = float(config[kind]['max_833'])
self.max_524 = float(config[kind]['max_524'])
self.trunk_load = [self.max_722, self.max_232,
self.max_832, self.max_275, self.max_365,
self.max_830, self.max_833, self.max_524]
self.line_range = config.get(kind, 'line_range').split(",")
self.n_ga = config.get(kind, 'n_gamma')
self.h_ga = config.get(kind, 'h_gamma')
def __repr__(self):
return 'Switch('+ repr(self.kind) + ')'
def newtimer(self):
"""
Returns timer back to Line() object. Checks to see
if running as __main__ or as a module and act
accordingly.
"""
if self.traffic_load == 'heavy':
a,b = (int(x) for x in self.h_ga.split(","))
timer = random.gamma(a,b)
elif self.traffic_load == 'normal':
a,b = (int(x) for x in self.n_ga.split(","))
timer = random.gamma(a,b)
return timer
def newchannel(self, channel_choices):
"""
We can either ask Asterisk to pick a channel, or
we can do it ourselves. That decision is made in call()
channel_choices: defined in panel_gen.conf
"""
channels_inuse = [l.chan for l in lines]
logging.debug('Begin channel selection')
logging.debug("In use: %s", channels_inuse)
channels_avail = [c for c in channel_choices if not c in channels_inuse]
logging.debug("Avail: %s", channels_avail)
if channels_avail == []:
logging.warning("No channels available on %s. Not placing call.", self.kind)
return False
else:
nextchan = random.choice(channels_avail)
logging.debug("End channel selection. Selected: %s", nextchan)
return nextchan
# +-----------------------------------------------+
# | |
# | <----- BEGIN AMI NONSENSE -----> |
# | |
# +-----------------------------------------------+
def on_DialBegin(event, **kwargs):
"""
Callback function for DialBegin AMI events.
Account Code is a magic number we send to Asterisk and expect
to get back. This is how we match events with calls in progress.
"""
try:
event = str(event)
DB_DestChannel = re.compile('(?<=DestChannel\'\:\s.{7})([^-]*)')
AccountCode = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
DB_DestChannel = DB_DestChannel.findall(event)
AccountCode = AccountCode.findall(event)
if DB_DestChannel == [] or AccountCode == []:
# Fuckin bail out!
logging.debug("***DialBegin regex isn't matching!***")
return
for l in lines:
if AccountCode[0] == l.magictoken:
l.chan = DB_DestChannel[0]
l.ast_status = 'Dialing'
l.switch.is_dialing += 1
l.switch.on_call +=1
l.status = 1
l.pending_call = False
l.pending_dialend = True
l.ami_tmr = 18
logging.info('DialBegin %s on DAHDI/%s from %s ident %s ->>',
l.term, l.chan, l.switch.kind, l.ident)
except Exception as e:
logging.exception(e)
def on_DialEnd(event, **kwargs):
"""
Callback function for DialEnd AMI events.
"""
try:
event = str(event)
DE_DestChannel = re.compile('(?<=DestChannel\'\:\s.{7})([^-]*)')
AccountCode = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
DE_DestChannel = DE_DestChannel.findall(event)
AccountCode = AccountCode.findall(event)
if DE_DestChannel == [] or AccountCode == []:
#Outta here
logging.debug("***DialEnd regex isn't matching!***")
return
for l in lines:
if AccountCode[0] == l.magictoken:
logging.debug('FROM ASTERISK: DialEnd for line %s', l.term)
l.pending_dialend = False
line = l
break
def doDialEnd():
try:
logging.debug("C: DialEnd bookkeeping starting on %s. Pending hangup is %s",
line.term, line.pending_hangup)
if line.pending_hangup == False:
if line.ast_status == 'Dialing':
line.ast_status = 'Ringing'
line.switch.is_dialing -= 1
logging.debug('Ringing %s on line %s', line.term, line.ident)
elif line.ast_status == 'on_hook':
logging.error('How did we get to DialEnd from on_hook?')
pass # xxx this might be problems
logging.debug('on_DialEnd with %s calls dialing', line.switch.is_dialing)
except Exception as e:
logging.exception(e)
if len(lines) > 0:
if line:
enqueue_event(line.switching_delay, doDialEnd)
logging.debug("B: Event enqueued delay %s.", line.switching_delay)
except Exception as e:
logging.exception(e)
def enqueue_event(delay, callback):
try:
eventtimer = threading.Timer(delay, callback)
eventtimer.start()
logging.debug("A: Started event timer delay %s", delay)
except Exception as e:
logging.exception(e)
def on_Hangup(event, **kwargs):
"""
Callback for processing hangup events.
"""
try:
event = str(event)
HU_DestChannel = re.compile('(?<=DestChannel\'\:\s.{7})([^-]*)')
AccountCode = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
AccountCode = AccountCode.findall(event)
HU_DestChannel = HU_DestChannel.findall(event)
if AccountCode == []:
logging.debug("*** AccountCode didn't match on hangup***")
return
for l in lines:
if AccountCode[0] == l.magictoken:
if l.ast_status == 'Dialing':
l.switch.is_dialing -= 1
logging.debug('Hangup while dialing %s on DAHDI %s', l.term, l.chan)
l.status = 0
l.chan = '-'
l.ast_status = 'on_hook'
l.switch.on_call -= 1
l.timer = l.switch.newtimer()
l.term = l.pick_next_called(term_choices)
l.pending_hangup = False
logging.info('<<- Asterisk reports hangup OK. Line %s status is %s',
l.ident, l.status)
except Exception as e:
logging.exception(e)
def parse_args():
# Gets called at runtime and parses arguments given on command line.
# If no arguments are presented, the program will run with default
# mostly sane options.
parser = argparse.ArgumentParser(description='Generate calls to electromechanical switches. '
'Defaults to originate a sane amount of calls from the panel switch if no args are given.')
parser.add_argument('-a', metavar='lines', type=int, default=[], choices=[1,2,3,4,5,6,7,8,9,10],
help='Maximum number of active lines.')
parser.add_argument('-o', metavar='switch', type=str, nargs='?', action='append', default=[],
choices=['1xb','1xbos','5xb','panel','all','722', '832', '232'],
help='Originate calls from a particular switch. Takes either 3 digit NXX values '
'or switch name. 1xb, 1xbos, 5xb, panel, or all. Default is panel.')
parser.add_argument('-t', metavar='switch', type=str, nargs='?', action='append', default=[],
choices=['1xb','5xb','panel','office','step', '722', '832', '232', '365', '275'],
help='Terminate calls only on a particular switch. Takes either 3 digit NXX values '
'or switch name. Defaults to sane options for whichever switch you are originating from.')
parser.add_argument('-v', metavar='volume', type=str, default='normal',
help='Call volume is a proprietary blend of frequency and randomness. Can be light, '
'normal, or heavy. Default is normal, which is good for average load.')
parser.add_argument('-log', metavar='loglevel', type=str, default='INFO',
help='Set log level to WARNING, INFO, DEBUG.')
global args
args = parser.parse_args()
return args
def phone_format(n):
return format(int(n[:-1]), ",").replace(",", "-") + n[-1]
def longdistance(line, chan):
# Some lines can be long distance calls with ANI
newsenders = ['13','14','16','27','28','29','32']
pd = ''
if line.kind == "1xb":
if chan in newsenders:
if line.term[0:3] == "832" or line.term[0:3] == "232":
if line.longdistance == False:
i = random.randint(0,10)
if i >= 7:
logging.info("ANI call being placed on %s to %s, chan %s",
line.kind, line.term, chan)
line.human_term = line.human_term + '*'
pd = '11'
line.switching_delay = 6
line.longdistance = True
if line.kind == "5xb":
too_many = sum(1 for l in lines if l.longdistance == True and l.kind =="5xb")
if line.term[0:3] == "832" or line.term[0:3] == "232":
i=random.randint(0,10)
if i >= 5:
if too_many < 2:
logging.info("ANI call being placed on %s to %s, chan %s",
line.kind, line.term, chan)
line.human_term = line.human_term + '*'
pd = '1'
line.switching_delay = 4
line.longdistance = True
return pd
def safetynet():
# Most of these things should never need to be done
# but its better to be fault tolerant if possible
def doRestartSwitch(reason, kind):
api_stop(switch=s.kind)
api_start(switch=s.kind)
logging.error("Restarted switch %s due to invalid state: %s", kind, reason)
def errorhandle(reason, status):
logging.error("Failed to get AMI %s within allotted time on %s",
status, l)
logging.error("Channel: %s", l.chan)
logging.error("Status: %s", l.status)
logging.error("Asterisk: %s", l.ast_status)
logging.error("Term: %s", l.human_term)
reason = ''
for s in originating_switches:
if s.is_dialing < 0:
reason = "dialing counter < 0"
doRestartSwitch(reason, s.kind)
if s.is_dialing > s.max_dialing:
reason = "exceeded max dialing"
doRestartSwitch(reason, s.kind)
for l in lines:
if l.pending_call == True:
status = "DialBegin"
if l.ami_tmr <= 0:
l.pending_call = False
errorhandle(reason, status)
if l.pending_dialend == True:
status = "DialEnd"
if l.ami_tmr <= 0:
l.pending_dialend = False
errorhandle(reason, status)
if l.pending_hangup == True:
status = "Hangup"
if l.ami_tmr <= 0:
l.pending_hangup = False
# This pass prevents silly threading confusion where
# asterisk will report a hangup before we realize that we've
# asked for one ;P
if l.chan == '-':
pass
else:
errorhandle(reason, status)
def make_switch(args):
# Instantiate some switches so we can work with them later.
# Behave differently if we're running as __main__ or __panel_gen__
global Rainier
global Adams
global Lakeview
global Step
global ESS3
Rainier = Switch(kind='panel')
Adams = Switch(kind='5xb')
Lakeview = Switch(kind='1xb')
Step = Switch(kind='step')
ESS3 = Switch(kind='3ess')
global originating_switches
originating_switches = []
if __name__ == 'panel_gen':
originating_switches.append(Rainier)
originating_switches.append(Adams)
originating_switches.append(Lakeview)
if __name__ == '__main__':
for o in args.o:
if o == 'panel' or o == '722':
originating_switches.append(Rainier)
elif o == '5xb' or o == '232':
originating_switches.append(Adams)
elif o == '1xb' or o == '832':
originating_switches.append(Lakeview)
elif o == 'ess' or o == '830':
originating_switches.append(ESS3)
elif o == 'all':
originating_switches.extend((Lakeview, Adams, Rainier))
if args.o == []:
originating_switches.append(Rainier)
global term_choices
term_choices = []
for t in args.t:
if t == 'panel' or t == '722':
term_choices.append(722)
elif t == '5xb' or t == '232':
term_choices.append(232)
elif t == '1xb' or t == '832':
term_choices.append(832)
elif t == 'office' or t == '365':
term_choices.append(365)
elif t == 'step' or t == '275':
term_choices.append(275)
def make_lines(**kwargs):
"""
Takes several kwargs. Returns a bunch of lines.
source: the origin of the call to this function
switch: the switch where the lines will originate on
originating_switches: list of originating switches passed in from args
numlines: number of lines we should create. should be determined and
passed in before this function is called
"""
try:
source = kwargs.get('source', '')
switch = kwargs.get('switch', '')
originating_switches = kwargs.get('originating_switches','')
numlines = kwargs.get('numlines', '')
new_lines = []
if source == 'main':
if args.a == []:
new_lines = [Line(n, switch) for switch in originating_switches for n in range(switch.lines_normal)]
else:
new_lines = [Line(n, switch) for switch in originating_switches for n in range(args.a)]
elif source == 'api':
new_lines = [Line(n, switch) for n in range(numlines)]
except Exception as e:
logging.exception(e)
return new_lines
def start_ui():
"""
This starts the panel_gen UI. Only useful when run as module.
When run as __main__, the UI is started for you.
:return: Nothing
:args: Nothing
"""
global t_ui
try:
t_ui = ui_thread()
t_ui.daemon = True
t_ui.start()
except Exception as e:
print(e)
def ami_connect(AMI_ADDRESS, AMI_PORT, AMI_USER, AMI_SECRET):
global client
global adapter
client = AMIClient(address=AMI_ADDRESS, port=int(AMI_PORT))
adapter = AMIClientAdapter(client)
future = client.login(username=AMI_USER, secret=AMI_SECRET)
logging.info('Connected to Asterisk AMI')
if future.response.is_error():
raise Exception(str(future.response))
# These listeners are for the AMI so I can catch events.
client.add_event_listener(on_DialBegin, white_list = 'DialBegin')
client.add_event_listener(on_DialEnd, white_list = 'DialEnd')
client.add_event_listener(on_Hangup, white_list = 'Hangup')
# +----------------------------------------------------+
# | |
# | The following chunk of code is for the |
# | panel_gen API, run from http_server.py |
# | The http server starts Flask, Connexion, which |
# | reads the API from swagger.yml, and executes HTTP |
# | requests using the code in switch.py, line.py and |
# | app.py. |
# | |
# | These functions return values to those .py's |
# | when panel_gen is imported as a module. |
# | |
# +----------------------------------------------------+
class AppSchema(Schema):
name = fields.Str()
app_running = fields.Boolean()
panel_running = fields.Boolean()
xb5_running = fields.Boolean()
xb1_running = fields.Boolean()
ui_running = fields.Boolean()
is_paused = fields.Boolean()
num_lines = fields.Integer()
class LineSchema(Schema):
line = fields.Dict()
ident = fields.Integer()
kind = fields.Str()
timer = fields.Integer()
is_dialing = fields.Boolean()
ast_status = fields.Str()
status = fields.Int()
chan = fields.Str()
term = fields.Str()
human_term = fields.Str()
hook_state = fields.Integer()
class SwitchSchema(Schema):
switch = fields.Dict()
kind = fields.Str()
max_dialing = fields.Integer()
is_dialing = fields.Integer()
on_call = fields.Integer()
lines_normal = fields.Integer()
lines_heavy = fields.Integer()
dahdi_group = fields.Str()
trunk_load = fields.List(fields.Str())
line_range = fields.List(fields.Str())
running = fields.Boolean()
timer = fields.Str()
traffic_load = fields.Str()
@post_load
def engage_motherfucker(self, data, **kwargs):
return Switch(**data)
def get_info():
""" Returns info about app state. """
schema = AppSchema()
ui_running = False
try:
if t_ui.started == True:
ui_running = True
except Exception as e:
pass
result = dict([
('name', __name__),
('app_running', t_work.is_alive),
('is_paused', t_work.paused),
('ui_running', ui_running),
('num_lines', len(lines)),
('panel_running', Rainier.running),
('xb5_running', Adams.running),
('xb1_running', Lakeview.running),
])
return schema.dump(result)
def api_start(**kwargs):
"""
Creates new lines when started from API.
- Checks to see if work thread is running
- Assigns generic switch type to instantiated name
- Checks to see if the switch is already running
- A special case is created for Sunday. See further notes
below.
source: Used to log where the start request came from.
switch: Specifies which switch to start calls on.
traffic_load: 'normal' or 'heavy'. Impacts number of lines we start with.
"""
global lines
global new_lines
source = kwargs.get('source', '')
switch = kwargs.get('switch', '')
traffic_load = kwargs.get('traffic_load', '')
try:
if source == 'web':
logging.info("App requested START on %s", switch)
elif source == 'key':
logging.info('Key operated: START on %s', switch)
else:
logging.warning('I dont know why, but we are starting on %s', switch)
if t_work.is_alive == True:
for i in originating_switches:
if switch == i.kind:
if i.running == True:
logging.warning("%s is running. Can't start twice.", i.kind)
elif i.running == False:
# Reset the dialing counter for safety.
i.is_dialing = 0
# This block handles whether or not the user passed in
# a traffic load setting. If not, we'll just use whatever
# we already have.
if traffic_load == "normal" or traffic_load == "heavy":
if traffic_load != i.traffic_load:
i.traffic_load = traffic_load
logging.info('Changing traffic load to %s', traffic_load)
if i.traffic_load == 'heavy':
numlines = i.lines_heavy
if i.traffic_load == 'normal':
numlines = i.lines_normal
if i == Adams:
# Carve out a special case for Sundays. This was requested
# by museum volunteers so that we can give tours of the
# step and 1XB without interruption by the this program.
# This will only be effective if the key is operated.
# Will have no impact when using web app.
if datetime.today().weekday() == 6:
logging.info('Its Sunday!')
if source == 'key':
logging.info('5XB special Sunday mode active')
i.trunk_load = [.1, .80, .1, .0, .0, .0, .0, .0]
new_lines = make_lines(switch=i, numlines=numlines,
source='api')
# Adams: If we start from the web interface, ignore
# those rules.
else:
logging.info('5XB special Sunday mode skipped')
new_lines = make_lines(switch=i, numlines=numlines,
source='api')
# Adams: If its any other day of the week, just act normal.
else:
new_lines = make_lines(switch=i, numlines=numlines,
source='api')
# Everyone else: Make lines.
else:
new_lines = make_lines(switch=i, numlines=numlines,
source='api')
# Append the lines we just created.
for l in new_lines:
lines.append(l)
i.running = True
logging.info('Appended %s lines to %s', len(new_lines), switch)
lines_created = len(new_lines)
result = get_info()
return result
except Exception as e:
logging.execption(e)
return False
def api_stop(**kwargs):
"""
Immediately hang up calls, and destroy lines.
switch: Which switch to hangup and stop. Can be
'panel', '5xb', '1xb' 'all'. Other switches not
yet implemented.
source: Where the request came from. Used for logging.
"""
switch = kwargs.get('switch', '')
source = kwargs.get('source', '')
if source == 'web':
logging.info("App requested STOP on %s", switch)
elif source == 'key':
logging.info('Key operated: STOP on %s', switch)
elif source == 'module':
logging.info('Module exited. Hanging up.')
global lines
try:
if switch == 'all':
for l in lines:
l.hangup()
lines = []
for s in originating_switches:
s.running = False
s.is_dialing = 0
s.on_call = 0
# Just hangup all channels when I use the FORCE button.
# After all. If I hit that button, I'm not kidding.
adapter.Hangup(Channel='/(.*?)/')
# Delete all remaining files in spool.
try:
system("rm /var/spool/asterisk/outgoing/*.call > /dev/null 2>&1")
except Exception as e:
logging.warning("Failed to delete remaining files in spool.")
logging.warning(e)
else:
for s in originating_switches:
if s.kind == switch:
deadlines = [l for l in lines if l.kind == s.kind]
lines = [l for l in lines if l.kind != s.kind]
s.running = False
s.is_dialing = 0
for n in deadlines:
n.hangup()
s.on_call = 0
except Exception as e:
logging.exception(e)
return False
return get_info()
def get_all_lines():
""" Returns formatted list of all lines """
schema = LineSchema()
result = [schema.dump(l) for l in lines]
return result
def get_line(ident):
# Check if ident passed in via API exists in lines.
# If so, send back that line. Else, return False..
api_ident = int(ident)
schema = LineSchema()
result = None
for l in lines:
if api_ident == l.ident:
result = (schema.dump(l))
if result == None:
return False
else:
return result
def create_line(**kwargs):
# Creates a new line using default parameters.
# lines.append uses the current number of lines in list
# to create the ident value for the new line.
schema = LineSchema()
result = []
switch = kwargs.get('switch','')
numlines = kwargs.get('numlines','')
for i in originating_switches:
if switch == i or switch == i.kind:
for n in range(numlines):
lines.append(Line(len(lines), i))
result.append(len(lines) - 1)
if result == []:
return False
else:
return result
def delete_line(**kwargs):
"""
Deletes a specific line.
switch: switch object
kind: type of switch
numlines: number of lines to delete
"""
global lines
switch = kwargs.get('switch','')
for i in originating_switches:
if i == switch or i.kind == kwargs.get('kind',''):
for n in range(kwargs.get('numlines','')):
lines.pop()
result = get_switch(i.kind)