class Test_Manager(unittest.TestCase): """ Test the asterisk management interface. """ default_events = AsteriskEmu.default_events def close(self): if self.manager: self.manager.close() self.manager = None self.astemu.close() def setUp(self): self.manager = None self.childpid = None self.events = [] self.evcount = 0 self.queue = Queue() def tearDown(self): self.close() def handler(self, event, manager): self.events.append(event) self.queue.put(self.evcount) self.evcount += 1 def run_manager(self, chatscript): self.astemu = AsteriskEmu(chatscript) self.port = self.astemu.port self.manager = Manager() self.manager.connect('localhost', port=self.port) self.manager.register_event('*', self.handler) def compare_result(self, r_event, event): for k, v in event.iteritems(): if k == 'CONTENT': self.assertEqual(r_event.data, v) elif isinstance(v, str): self.assertEqual(r_event[k], v) else: self.assertEqual(r_event[k], v[-1]) self.assertEqual(sorted(r_event.multiheaders[k]), sorted(list(v))) def test_login(self): self.run_manager({}) r = self.manager.login('account', 'geheim') self.compare_result(r, self.default_events['Login'][0]) self.close() self.assertEqual(self.events, []) def test_command(self): d = dict events = dict \ ( Command = ( Event ( Response = ('Follows',) , Privilege = ('Command',) , CONTENT = """Channel Location State Application(Data) lcr/556 s@attendoparse:9 Up Read(dtmf,,30,noanswer,,2) 1 active channel 1 active call 372 calls processed --END COMMAND--\r """ ) , ) ) self.run_manager(events) r = self.manager.command('core show channels') self.assertEqual(self.events, []) self.compare_result(r, events['Command'][0]) def test_redirect(self): d = dict events = dict \ ( Redirect = ( Event ( Response = ('Success',) , Message = ('Redirect successful',) ) , ) ) self.run_manager(events) r = self.manager.redirect \ ('lcr/556', 'generic', 'Bye', context='attendo') self.assertEqual(self.events, []) self.compare_result(r, events['Redirect'][0]) def test_originate(self): d = dict events = dict \ ( Originate = ( Event ( Response = ('Success',) , Message = ('Originate successfully queued',) ) , Event ( Event = ('Newchannel',) , Privilege = ('call,all',) , Channel = ('lcr/557',) , ChannelState = ('1',) , ChannelStateDesc = ('Rsrvd',) , CallerIDNum = ('',) , CallerIDName = ('',) , AccountCode = ('',) , Exten = ('',) , Context = ('',) , Uniqueid = ('1332366541.558',) ) , Event ( Event = ('NewAccountCode',) , Privilege = ('call,all',) , Channel = ('lcr/557',) , Uniqueid = ('1332366541.558',) , AccountCode = ('4019946397',) , OldAccountCode = ('',) ) , Event ({ 'Event' : ('NewCallerid',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/557',) , 'CallerIDNum' : ('',) , 'CallerIDName' : ('',) , 'Uniqueid' : ('1332366541.558',) , 'CID-CallingPres' : ('0 (Presentation Allowed, Not Screened)',) }) , Event ( Event = ('Newchannel',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('1',) , ChannelStateDesc = ('Rsrvd',) , CallerIDNum = ('',) , CallerIDName = ('',) , AccountCode = ('',) , Exten = ('',) , Context = ('',) , Uniqueid = ('1332366541.559',) ) , Event ( Event = ('Newstate',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('4',) , ChannelStateDesc = ('Ring',) , CallerIDNum = ('0000000000',) , CallerIDName = ('',) , Uniqueid = ('1332366541.559',) ) , Event ( Event = ('Newstate',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('7',) , ChannelStateDesc = ('Busy',) , CallerIDNum = ('0000000000',) , CallerIDName = ('',) , Uniqueid = ('1332366541.559',) ) , Event ({ 'Event' : ('Hangup',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/558',) , 'Uniqueid' : ('1332366541.559',) , 'CallerIDNum' : ('0000000000',) , 'CallerIDName' : ('<unknown>',) , 'Cause' : ('16',) , 'Cause-txt' : ('Normal Clearing',) }) , Event ({ 'Event' : ('Hangup',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/557',) , 'Uniqueid' : ('1332366541.558',) , 'CallerIDNum' : ('<unknown>',) , 'CallerIDName' : ('<unknown>',) , 'Cause' : ('17',) , 'Cause-txt' : ('User busy',) }) , Event ( Event = ('OriginateResponse',) , Privilege = ('call,all',) , Response = ('Failure',) , Channel = ('LCR/Ext1/0000000000',) , Context = ('linecheck',) , Exten = ('1',) , Reason = ('1',) , Uniqueid = ('<null>',) , CallerIDNum = ('<unknown>',) , CallerIDName = ('<unknown>',) ) ) ) self.run_manager(events) r = self.manager.originate \ ('LCR/Ext1/0000000000', '1' , context = 'linecheck' , priority = '1' , account = '4019946397' , variables = {'CALL_DELAY' : '1', 'SOUND' : 'abandon-all-hope'} ) self.compare_result(r, events['Originate'][0]) for k in events['Originate'][1:]: n = self.queue.get() self.compare_result(self.events[n], events['Originate'][n + 1]) def test_misc_events(self): d = dict # Events from SF bug 3470641 # http://sourceforge.net/tracker/ # ?func=detail&aid=3470641&group_id=76162&atid=546272 # But we fail to reproduce the bug. events = dict \ ( Login = ( self.default_events['Login'][0] , Event ({ 'AppData' : '0?begin2' , 'Extension' : 'zap2dahdi' , 'Uniqueid' : '1325950970.698' , 'Priority' : '9' , 'Application': 'GotoIf' , 'Context' : 'macro-dial-one' , 'Privilege' : 'dialplan,all' , 'Event' : 'Newexten' , 'Channel' : 'Local/102@from-queue-a8ca;2' }) , Event ({ 'Value' : '2' , 'Variable' : 'MACRO_DEPTH' , 'Uniqueid' : '1325950970.698' , 'Privilege' : 'dialplan,all' , 'Event' : 'VarSet' , 'Channel' : 'Local/102@from-queue-a8ca;2' }) , Event ({'Privilege': 'dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 9\r\n' 'Application: GotoIf\r\n' 'AppData: 0?begin2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: THISDIAL=SIP/102\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: THISDIAL\r\n' 'Value: SIP/102\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 11\r\n' 'Application: Return\r\n' 'AppData: \r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: GOSUB_RETVAL\r\n' 'Value: \r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: dstring\r\n' 'Priority: 9\r\n' 'Application: Set\r\n' 'AppData: DSTRING=SIP/102&\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: DSTRING\r\n' 'Value: SIP/102&\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: dstring\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: ITER=2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 6\r\n' 'Application: ExecIf\r\n' 'AppData: 0?Set(THISPART2=DAHDI/101)\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: ITER\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 7\r\n' 'Application: Set\r\n' 'AppData: NEWDIAL=SIP/101&\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: NEWDIAL\r\n' 'Value: SIP/101&\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 8\r\n' 'Application: Set\r\n' 'AppData: ITER2=2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: ITER2\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 9\r\n' 'Application: GotoIf\r\n' 'AppData: 0?begin2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: THISDIAL=SIP/101\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: THISDIAL\r\n' 'Value: SIP/101\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2' , 'Variable': 'MACRO_DEPTH' , 'Event': 'VarSet' , 'Value': '2' , 'Uniqueid': '1325950970.696' }) ) ) self.run_manager(events) r = self.manager.login('account', 'geheim') self.compare_result(r, events['Login'][0]) evnames = [] for s in events['Login'][3]['Privilege'].split('\r\n'): if s.startswith('Event:'): evnames.append(s.split(':')[1].strip()) for k in xrange(30): n = self.queue.get() e = self.events[n] if n < 2: self.compare_result(e, events['Login'][n + 1]) elif n == 2: self.assertEqual(e['Event'], 'VarSet') else: self.assertEqual(e['Event'], evnames[n - 3]) self.assertEqual(len(self.events), 30) def test_agent_event(self): d = dict # Events from SF bug 3470641 # http://sourceforge.net/tracker/ # ?func=detail&aid=3470641&group_id=76162&atid=546272 # But we fail to reproduce the bug. events = dict \ ( Login = ( self.default_events['Login'][0] , Event ( Event = ('AgentCalled',) , Privilege = ('agent,all',) , Queue = ('test',) , AgentCalled = ('SIP/s394000',) , AgentName = ('910567',) , ChannelCalling = ('SIP/multifon-00000006',) , DestinationChannel = ('SIP/s394000-00000007',) , CallerIDNum = ('394000',) , CallerIDName = ('Agent',) , Context = ('from-multifon',) , Extension = ('7930456789',) , Priority = ('3',) , Uniqueid = ('1302010429.6',) , Variable = ('data1=456789', 'data2=test') ) ) ) self.run_manager(events) r = self.manager.login('account', 'geheim') self.compare_result(r, events['Login'][0]) for k in events['Login'][1:]: n = self.queue.get() self.compare_result(self.events[n], events['Login'][n + 1])
class Manager(object): def __init__(self): self._sock = None # our socket self.title = None # set by received greeting self._connected = threading.Event() self._running = threading.Event() # our hostname self.hostname = socket.gethostname() # pid -- used for unique naming of ActionID self.pid = os.getpid () # our queues self._message_queue = Queue() self._response_queue = Queue() self._event_queue = Queue() # callbacks for events self._event_callbacks = {} self._reswaiting = [] # who is waiting for a response # sequence stuff self._seqlock = threading.Lock() self._seq = 0 # some threads self.message_thread = threading.Thread(target=self.message_loop) self.event_dispatch_thread = threading.Thread(target=self.event_dispatch) self.message_thread.setDaemon(True) self.event_dispatch_thread.setDaemon(True) def __del__(self): self.close() def connected(self): """ Check if we are connected or not. """ return self._connected.isSet() def next_seq(self): """Return the next number in the sequence, this is used for ActionID""" self._seqlock.acquire() try: return self._seq finally: self._seq += 1 self._seqlock.release() def send_action(self, cdict={}, **kwargs): """ Send a command to the manager If a list is passed to the cdict argument, each item in the list will be sent to asterisk under the same header in the following manner: cdict = {"Action": "Originate", "Variable": ["var1=value", "var2=value"]} send_action(cdict) ... Action: Originate Variable: var1=value Variable: var2=value """ if not self._connected.isSet(): raise ManagerException("Not connected") # fill in our args cdict.update(kwargs) # set the action id if 'ActionID' not in cdict: cdict['ActionID'] = '%s-%04s-%08x' % (self.hostname, self.pid, self.next_seq()) clist = [] # generate the command for key, value in cdict.items(): if isinstance(value, list): for item in value: item = tuple([key, item]) clist.append('%s: %s' % item) else: item = tuple([key, value]) clist.append('%s: %s' % item) clist.append(EOL) command = EOL.join(clist) # lock the socket and send our command try: self._sock.write(command.encode('utf-8')) self._sock.flush() except socket.error as err: errno, reason = err raise ManagerSocketException(errno, reason) self._reswaiting.insert(0,1) response = self._response_queue.get() self._reswaiting.pop(0) if not response: raise ManagerSocketException(0, 'Connection Terminated') return response def _receive_data(self): """ Read the response from a command. """ multiline = False wait_for_marker = False eolcount = 0 # loop while we are sill running and connected while self._running.isSet() and self._connected.isSet(): try: lines = [] for line in self._sock : line = line.decode('utf-8') # check to see if this is the greeting line if not self.title and '/' in line and not ':' in line: # store the title of the manager we are connecting to: self.title = line.split('/')[0].strip() # store the version of the manager we are connecting to: self.version = line.split('/')[1].strip() # fake message header lines.append ('Response: Generated Header\r\n') lines.append (line) break # If the line is EOL marker we have a complete message. # Some commands are broken and contain a \n\r\n # sequence, in the case wait_for_marker is set, we # have such a command where the data ends with the # marker --END COMMAND--, so we ignore embedded # newlines until we see that marker if line == EOL and not wait_for_marker : multiline = False if lines or not self._connected.isSet(): break # ignore empty lines at start continue lines.append(line) # line not ending in \r\n or without ':' isn't a # valid header and starts multiline response if not line.endswith('\r\n') or ':' not in line: multiline = True # Response: Follows indicates we should wait for end # marker --END COMMAND-- if not multiline and line.startswith('Response') and \ line.split(':', 1)[1].strip() == 'Follows': wait_for_marker = True # same when seeing end of multiline response if multiline and line.startswith('--END COMMAND--'): wait_for_marker = False multiline = False if not self._connected.isSet(): break else: # EOF during reading self._sock.close() self._connected.clear() # if we have a message append it to our queue if lines and self._connected.isSet(): self._message_queue.put(lines) else: self._message_queue.put(None) except socket.error: self._sock.close() self._connected.clear() self._message_queue.put(None) def register_event(self, event, function): """ Register a callback for the specfied event. If a callback function returns True, no more callbacks for that event will be executed. """ # get the current value, or an empty list # then add our new callback current_callbacks = self._event_callbacks.get(event, []) current_callbacks.append(function) self._event_callbacks[event] = current_callbacks def unregister_event(self, event, function): """ Unregister a callback for the specified event. """ current_callbacks = self._event_callbacks.get(event, []) current_callbacks.remove(function) self._event_callbacks[event] = current_callbacks def message_loop(self): """ The method for the event thread. This actually recieves all types of messages and places them in the proper queues. """ # start a thread to receive data t = threading.Thread(target=self._receive_data) t.setDaemon(True) t.start() try: # loop getting messages from the queue while self._running.isSet(): # get/wait for messages data = self._message_queue.get() # if we got None as our message we are done if not data: # notify the other queues self._event_queue.put(None) for waiter in self._reswaiting: self._response_queue.put(None) break # parse the data message = ManagerMsg(data) # check if this is an event message if message.has_header('Event'): self._event_queue.put(Event(message)) # check if this is a response elif message.has_header('Response'): self._response_queue.put(message) else: self._response_queue.put(None) print ('No clue what we got\n%s' % message.data) finally: # wait for our data receiving thread to exit t.join() def event_dispatch(self): """This thread is responsible for dispatching events""" # loop dispatching events while self._running.isSet(): # get/wait for an event ev = self._event_queue.get() # if we got None as an event, we are finished if not ev: break # dispatch our events # first build a list of the functions to execute callbacks = (self._event_callbacks.get(ev.name, []) + self._event_callbacks.get('*', [])) # now execute the functions for callback in callbacks: if callback(ev, self): break def connect(self, host, port=5038): """Connect to the manager interface""" if self._connected.isSet(): raise ManagerException('Already connected to manager') # make sure host is a string assert isinstance (host, string_types) port = int(port) # make sure port is an int # create our socket and connect try: _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _sock.connect((host,port)) self._sock = _sock.makefile(mode='rwb') _sock.close() except socket.error as err: errno, reason = err raise ManagerSocketException(errno, reason) # we are connected and running self._connected.set() self._running.set() # start the event thread self.message_thread.start() # start the event dispatching thread self.event_dispatch_thread.start() # get our initial connection response return self._response_queue.get() def close(self): """Shutdown the connection to the manager""" # if we are still running, logout if self._running.isSet() and self._connected.isSet(): self.logoff() if self._running.isSet(): # put None in the message_queue to kill our threads self._message_queue.put(None) # wait for the event thread to exit self.message_thread.join() # make sure we do not join our self (when close is called from event handlers) if threading.currentThread() != self.event_dispatch_thread: # wait for the dispatch thread to exit self.event_dispatch_thread.join() self._running.clear() # Manager actions def login(self, username, secret): """Login to the manager, throws ManagerAuthException when login falis""" cdict = {'Action':'Login'} cdict['Username'] = username cdict['Secret'] = secret response = self.send_action(cdict) if response.get_header('Response') == 'Error': raise ManagerAuthException(response.get_header('Message')) return response def ping(self): """Send a ping action to the manager""" cdict = {'Action':'Ping'} response = self.send_action(cdict) return response def logoff(self): """Logoff from the manager""" cdict = {'Action':'Logoff'} response = self.send_action(cdict) return response def hangup(self, channel): """Hangup the specified channel""" cdict = {'Action':'Hangup'} cdict['Channel'] = channel response = self.send_action(cdict) return response def status(self, channel = ''): """Get a status message from asterisk""" cdict = {'Action':'Status'} cdict['Channel'] = channel response = self.send_action(cdict) return response def redirect(self, channel, exten, priority='1', extra_channel='', context=''): """Redirect a channel""" cdict = {'Action':'Redirect'} cdict['Channel'] = channel cdict['Exten'] = exten cdict['Priority'] = priority if context: cdict['Context'] = context if extra_channel: cdict['ExtraChannel'] = extra_channel response = self.send_action(cdict) return response def originate(self, channel, exten, context='', priority='', timeout='', caller_id='', async=False, account='', variables={}):
class Manager(object): def __init__(self): self._sock = None # our socket self.title = None # set by received greeting self._connected = threading.Event() self._running = threading.Event() # our hostname self.hostname = socket.gethostname() # pid -- used for unique naming of ActionID self.pid = os.getpid() # our queues self._message_queue = Queue() self._response_queue = Queue() self._event_queue = Queue() # callbacks for events self._event_callbacks = {} self._reswaiting = [] # who is waiting for a response # sequence stuff self._seqlock = threading.Lock() self._seq = 0 # some threads self.message_thread = threading.Thread(target=self.message_loop) self.event_dispatch_thread = threading.Thread( target=self.event_dispatch) self.message_thread.setDaemon(True) self.event_dispatch_thread.setDaemon(True) def __del__(self): self.close() def connected(self): """ Check if we are connected or not. """ return self._connected.isSet() def next_seq(self): """Return the next number in the sequence, this is used for ActionID""" self._seqlock.acquire() try: return self._seq finally: self._seq += 1 self._seqlock.release() def send_action(self, cdict={}, **kwargs): """ Send a command to the manager If a list is passed to the cdict argument, each item in the list will be sent to asterisk under the same header in the following manner: cdict = {"Action": "Originate", "Variable": ["var1=value", "var2=value"]} send_action(cdict) ... Action: Originate Variable: var1=value Variable: var2=value """ if not self._connected.isSet(): raise ManagerException("Not connected") # fill in our args cdict.update(kwargs) # set the action id if 'ActionID' not in cdict: cdict['ActionID'] = '%s-%04s-%08x' % (self.hostname, self.pid, self.next_seq()) clist = [] # generate the command for key, value in cdict.items(): if isinstance(value, list): for item in value: item = tuple([key, item]) clist.append('%s: %s' % item) else: item = tuple([key, value]) clist.append('%s: %s' % item) clist.append(EOL) command = EOL.join(clist) # lock the socket and send our command try: self._sock.write(command.encode('utf-8')) self._sock.flush() except socket.error as err: errno, reason = err raise ManagerSocketException(errno, reason) self._reswaiting.insert(0, 1) response = self._response_queue.get() self._reswaiting.pop(0) if not response: raise ManagerSocketException(0, 'Connection Terminated') return response def _receive_data(self): """ Read the response from a command. """ multiline = False wait_for_marker = False eolcount = 0 # loop while we are sill running and connected while self._running.isSet() and self._connected.isSet(): try: lines = [] for line in self._sock: line = line.decode('utf-8') # check to see if this is the greeting line if not self.title and '/' in line and not ':' in line: # store the title of the manager we are connecting to: self.title = line.split('/')[0].strip() # store the version of the manager we are connecting to: self.version = line.split('/')[1].strip() # fake message header lines.append('Response: Generated Header\r\n') lines.append(line) break # If the line is EOL marker we have a complete message. # Some commands are broken and contain a \n\r\n # sequence, in the case wait_for_marker is set, we # have such a command where the data ends with the # marker --END COMMAND--, so we ignore embedded # newlines until we see that marker if line == EOL and not wait_for_marker: multiline = False if lines or not self._connected.isSet(): break # ignore empty lines at start continue lines.append(line) # line not ending in \r\n or without ':' isn't a # valid header and starts multiline response if not line.endswith('\r\n') or ':' not in line: multiline = True # Response: Follows indicates we should wait for end # marker --END COMMAND-- if not multiline and line.startswith('Response') and \ line.split(':', 1)[1].strip() == 'Follows': wait_for_marker = True # same when seeing end of multiline response if multiline and line.startswith('--END COMMAND--'): wait_for_marker = False multiline = False if not self._connected.isSet(): break else: # EOF during reading self._sock.close() self._connected.clear() # if we have a message append it to our queue if lines and self._connected.isSet(): self._message_queue.put(lines) else: self._message_queue.put(None) except socket.error: self._sock.close() self._connected.clear() self._message_queue.put(None) def register_event(self, event, function): """ Register a callback for the specfied event. If a callback function returns True, no more callbacks for that event will be executed. """ # get the current value, or an empty list # then add our new callback current_callbacks = self._event_callbacks.get(event, []) current_callbacks.append(function) self._event_callbacks[event] = current_callbacks def unregister_event(self, event, function): """ Unregister a callback for the specified event. """ current_callbacks = self._event_callbacks.get(event, []) current_callbacks.remove(function) self._event_callbacks[event] = current_callbacks def message_loop(self): """ The method for the event thread. This actually recieves all types of messages and places them in the proper queues. """ # start a thread to receive data t = threading.Thread(target=self._receive_data) t.setDaemon(True) t.start() try: # loop getting messages from the queue while self._running.isSet(): # get/wait for messages data = self._message_queue.get() # if we got None as our message we are done if not data: # notify the other queues self._event_queue.put(None) for waiter in self._reswaiting: self._response_queue.put(None) break # parse the data message = ManagerMsg(data) # check if this is an event message if message.has_header('Event'): self._event_queue.put(Event(message)) # check if this is a response elif message.has_header('Response'): self._response_queue.put(message) else: self._response_queue.put(None) print('No clue what we got\n%s' % message.data) finally: # wait for our data receiving thread to exit t.join() def event_dispatch(self): """This thread is responsible for dispatching events""" # loop dispatching events while self._running.isSet(): # get/wait for an event ev = self._event_queue.get() # if we got None as an event, we are finished if not ev: break # dispatch our events # first build a list of the functions to execute callbacks = (self._event_callbacks.get(ev.name, []) + self._event_callbacks.get('*', [])) # now execute the functions for callback in callbacks: if callback(ev, self): break def connect(self, host, port=5038): """Connect to the manager interface""" if self._connected.isSet(): raise ManagerException('Already connected to manager') # make sure host is a string assert isinstance(host, string_types) port = int(port) # make sure port is an int # create our socket and connect try: _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _sock.connect((host, port)) self._sock = _sock.makefile(mode='rwb') _sock.close() except socket.error as err: errno, reason = err raise ManagerSocketException(errno, reason) # we are connected and running self._connected.set() self._running.set() # start the event thread self.message_thread.start() # start the event dispatching thread self.event_dispatch_thread.start() # get our initial connection response return self._response_queue.get() def close(self): """Shutdown the connection to the manager""" # if we are still running, logout if self._running.isSet() and self._connected.isSet(): self.logoff() if self._running.isSet(): # put None in the message_queue to kill our threads self._message_queue.put(None) # wait for the event thread to exit self.message_thread.join() # make sure we do not join our self (when close is called from event handlers) if threading.currentThread() != self.event_dispatch_thread: # wait for the dispatch thread to exit self.event_dispatch_thread.join() self._running.clear() # Manager actions def login(self, username, secret): """Login to the manager, throws ManagerAuthException when login falis""" cdict = {'Action': 'Login'} cdict['Username'] = username cdict['Secret'] = secret response = self.send_action(cdict) if response.get_header('Response') == 'Error': raise ManagerAuthException(response.get_header('Message')) return response def ping(self): """Send a ping action to the manager""" cdict = {'Action': 'Ping'} response = self.send_action(cdict) return response def logoff(self): """Logoff from the manager""" cdict = {'Action': 'Logoff'} response = self.send_action(cdict) return response def hangup(self, channel): """Hangup the specified channel""" cdict = {'Action': 'Hangup'} cdict['Channel'] = channel response = self.send_action(cdict) return response def status(self, channel=''): """Get a status message from asterisk""" cdict = {'Action': 'Status'} cdict['Channel'] = channel response = self.send_action(cdict) return response def redirect(self, channel, exten, priority='1', extra_channel='', context=''): """Redirect a channel""" cdict = {'Action': 'Redirect'} cdict['Channel'] = channel cdict['Exten'] = exten cdict['Priority'] = priority if context: cdict['Context'] = context if extra_channel: cdict['ExtraChannel'] = extra_channel response = self.send_action(cdict) return response def originate(self, channel, exten, context='', priority='', timeout='', caller_id='', async=False, account='', variables={}):
class Test_Manager(unittest.TestCase): """ Test the asterisk management interface. """ default_events = AsteriskEmu.default_events def close(self): if self.manager: self.manager.close() self.manager = None self.astemu.close() def setUp(self): self.manager = None self.childpid = None self.events = [] self.evcount = 0 self.queue = Queue() def tearDown(self): self.close() def handler(self, event, manager): self.events.append(event) self.queue.put(self.evcount) self.evcount += 1 def run_manager(self, chatscript): self.astemu = AsteriskEmu (chatscript) self.port = self.astemu.port self.manager = Manager() self.manager.connect('localhost', port = self.port) self.manager.register_event ('*', self.handler) def compare_result(self, r_event, event): for k, v in event.iteritems(): if k == 'CONTENT': self.assertEqual(r_event.data, v) elif isinstance(v, str): self.assertEqual(r_event[k], v) else: self.assertEqual(r_event[k], v[-1]) self.assertEqual(sorted(r_event.multiheaders[k]), sorted(list(v))) def test_login(self): self.run_manager({}) r = self.manager.login('account', 'geheim') self.compare_result(r, self.default_events['Login'][0]) self.close() self.assertEqual(self.events, []) def test_command(self): d = dict events = dict \ ( Command = ( Event ( Response = ('Follows',) , Privilege = ('Command',) , CONTENT = """Channel Location State Application(Data) lcr/556 s@attendoparse:9 Up Read(dtmf,,30,noanswer,,2) 1 active channel 1 active call 372 calls processed --END COMMAND--\r """ ) , ) ) self.run_manager(events) r = self.manager.command ('core show channels') self.assertEqual(self.events, []) self.compare_result(r, events['Command'][0]) def test_redirect(self): d = dict events = dict \ ( Redirect = ( Event ( Response = ('Success',) , Message = ('Redirect successful',) ) , ) ) self.run_manager(events) r = self.manager.redirect \ ('lcr/556', 'generic', 'Bye', context='attendo') self.assertEqual(self.events, []) self.compare_result(r, events['Redirect'][0]) def test_originate(self): d = dict events = dict \ ( Originate = ( Event ( Response = ('Success',) , Message = ('Originate successfully queued',) ) , Event ( Event = ('Newchannel',) , Privilege = ('call,all',) , Channel = ('lcr/557',) , ChannelState = ('1',) , ChannelStateDesc = ('Rsrvd',) , CallerIDNum = ('',) , CallerIDName = ('',) , AccountCode = ('',) , Exten = ('',) , Context = ('',) , Uniqueid = ('1332366541.558',) ) , Event ( Event = ('NewAccountCode',) , Privilege = ('call,all',) , Channel = ('lcr/557',) , Uniqueid = ('1332366541.558',) , AccountCode = ('4019946397',) , OldAccountCode = ('',) ) , Event ({ 'Event' : ('NewCallerid',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/557',) , 'CallerIDNum' : ('',) , 'CallerIDName' : ('',) , 'Uniqueid' : ('1332366541.558',) , 'CID-CallingPres' : ('0 (Presentation Allowed, Not Screened)',) }) , Event ( Event = ('Newchannel',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('1',) , ChannelStateDesc = ('Rsrvd',) , CallerIDNum = ('',) , CallerIDName = ('',) , AccountCode = ('',) , Exten = ('',) , Context = ('',) , Uniqueid = ('1332366541.559',) ) , Event ( Event = ('Newstate',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('4',) , ChannelStateDesc = ('Ring',) , CallerIDNum = ('0000000000',) , CallerIDName = ('',) , Uniqueid = ('1332366541.559',) ) , Event ( Event = ('Newstate',) , Privilege = ('call,all',) , Channel = ('lcr/558',) , ChannelState = ('7',) , ChannelStateDesc = ('Busy',) , CallerIDNum = ('0000000000',) , CallerIDName = ('',) , Uniqueid = ('1332366541.559',) ) , Event ({ 'Event' : ('Hangup',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/558',) , 'Uniqueid' : ('1332366541.559',) , 'CallerIDNum' : ('0000000000',) , 'CallerIDName' : ('<unknown>',) , 'Cause' : ('16',) , 'Cause-txt' : ('Normal Clearing',) }) , Event ({ 'Event' : ('Hangup',) , 'Privilege' : ('call,all',) , 'Channel' : ('lcr/557',) , 'Uniqueid' : ('1332366541.558',) , 'CallerIDNum' : ('<unknown>',) , 'CallerIDName' : ('<unknown>',) , 'Cause' : ('17',) , 'Cause-txt' : ('User busy',) }) , Event ( Event = ('OriginateResponse',) , Privilege = ('call,all',) , Response = ('Failure',) , Channel = ('LCR/Ext1/0000000000',) , Context = ('linecheck',) , Exten = ('1',) , Reason = ('1',) , Uniqueid = ('<null>',) , CallerIDNum = ('<unknown>',) , CallerIDName = ('<unknown>',) ) ) ) self.run_manager(events) r = self.manager.originate \ ('LCR/Ext1/0000000000', '1' , context = 'linecheck' , priority = '1' , account = '4019946397' , variables = {'CALL_DELAY' : '1', 'SOUND' : 'abandon-all-hope'} ) self.compare_result(r, events['Originate'][0]) for k in events['Originate'][1:]: n = self.queue.get() self.compare_result(self.events[n], events['Originate'][n+1]) def test_misc_events(self): d = dict # Events from SF bug 3470641 # http://sourceforge.net/tracker/ # ?func=detail&aid=3470641&group_id=76162&atid=546272 # But we fail to reproduce the bug. events = dict \ ( Login = ( self.default_events['Login'][0] , Event ({ 'AppData' : '0?begin2' , 'Extension' : 'zap2dahdi' , 'Uniqueid' : '1325950970.698' , 'Priority' : '9' , 'Application': 'GotoIf' , 'Context' : 'macro-dial-one' , 'Privilege' : 'dialplan,all' , 'Event' : 'Newexten' , 'Channel' : 'Local/102@from-queue-a8ca;2' }) , Event ({ 'Value' : '2' , 'Variable' : 'MACRO_DEPTH' , 'Uniqueid' : '1325950970.698' , 'Privilege' : 'dialplan,all' , 'Event' : 'VarSet' , 'Channel' : 'Local/102@from-queue-a8ca;2' }) , Event ({'Privilege': 'dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 9\r\n' 'Application: GotoIf\r\n' 'AppData: 0?begin2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: THISDIAL=SIP/102\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: THISDIAL\r\n' 'Value: SIP/102\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 11\r\n' 'Application: Return\r\n' 'AppData: \r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: GOSUB_RETVAL\r\n' 'Value: \r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: dstring\r\n' 'Priority: 9\r\n' 'Application: Set\r\n' 'AppData: DSTRING=SIP/102&\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: DSTRING\r\n' 'Value: SIP/102&\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: dstring\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: ITER=2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 6\r\n' 'Application: ExecIf\r\n' 'AppData: 0?Set(THISPART2=DAHDI/101)\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: ITER\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/102@from-queue-a8ca;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.698\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 7\r\n' 'Application: Set\r\n' 'AppData: NEWDIAL=SIP/101&\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: NEWDIAL\r\n' 'Value: SIP/101&\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 8\r\n' 'Application: Set\r\n' 'AppData: ITER2=2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: ITER2\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 9\r\n' 'Application: GotoIf\r\n' 'AppData: 0?begin2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: MACRO_DEPTH\r\n' 'Value: 2\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: Newexten\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Context: macro-dial-one\r\n' 'Extension: zap2dahdi\r\n' 'Priority: 10\r\n' 'Application: Set\r\n' 'AppData: THISDIAL=SIP/101\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2\r\n' 'Variable: THISDIAL\r\n' 'Value: SIP/101\r\n' 'Uniqueid: 1325950970.696\r\n' '\r\n' 'Event: VarSet\r\n' 'Privilege: dialplan,all\r\n' 'Channel: Local/101@from-queue-4406;2' , 'Variable': 'MACRO_DEPTH' , 'Event': 'VarSet' , 'Value': '2' , 'Uniqueid': '1325950970.696' }) ) ) self.run_manager(events) r = self.manager.login('account', 'geheim') self.compare_result(r, events['Login'][0]) evnames = [] for s in events['Login'][3]['Privilege'].split('\r\n'): if s.startswith('Event:'): evnames.append(s.split(':')[1].strip()) for k in xrange(30): n = self.queue.get() e = self.events[n] if n < 2: self.compare_result(e, events['Login'][n+1]) elif n == 2: self.assertEqual(e['Event'], 'VarSet') else: self.assertEqual(e['Event'], evnames[n-3]) self.assertEqual(len(self.events), 30) def test_agent_event(self): d = dict # Events from SF bug 3470641 # http://sourceforge.net/tracker/ # ?func=detail&aid=3470641&group_id=76162&atid=546272 # But we fail to reproduce the bug. events = dict \ ( Login = ( self.default_events['Login'][0] , Event ( Event = ('AgentCalled',) , Privilege = ('agent,all',) , Queue = ('test',) , AgentCalled = ('SIP/s394000',) , AgentName = ('910567',) , ChannelCalling = ('SIP/multifon-00000006',) , DestinationChannel = ('SIP/s394000-00000007',) , CallerIDNum = ('394000',) , CallerIDName = ('Agent',) , Context = ('from-multifon',) , Extension = ('7930456789',) , Priority = ('3',) , Uniqueid = ('1302010429.6',) , Variable = ('data1=456789', 'data2=test') ) ) ) self.run_manager(events) r = self.manager.login('account', 'geheim') self.compare_result(r, events['Login'][0]) for k in events['Login'][1:]: n = self.queue.get() self.compare_result(self.events[n], events['Login'][n+1])