def wait_for_it(deferred=None): """Waits for the client to reset before connecting the web socket. Keyword Arguments: deferred -- The twisted.defer instance to use for chaining callbacks (optional) (default None). """ msg = '{0} '.format(self) if not deferred: deferred = defer.Deferred() if not self.clean: LOGGER.debug(msg + 'I\'m not so fresh so clean.') reactor.callLater(1, wait_for_it, deferred) else: LOGGER.debug(msg + 'Connecting web socket.') self.__ari = ARI(self.__host, userpass=self.__credentials) self.__factory = AriClientFactory(receiver=self, host=self.__host, port=self.__port, apps=self.name, userpass=self.__credentials) deferred.callback(self.__factory.connect())
class AriClient(ObservableObject): """The ARI client. This class serves as a facade for ARI and AriClientFactory. It is responsible for creating and persisting the connection state needed to execute a test scenario. """ def __init__(self, host, port, credentials, name='testsuite'): """Constructor. Keyword Arguments: host -- The [bindaddr] of the Asterisk HTTP web server. port -- The [bindport] of the Asterisk HTTP web server. credentials -- User credentials for ARI. A tuple. E.g.: ('username', 'password'). name -- The name of the app to register in Stasis via ARI (optional) (default 'testsuite'). """ super(AriClient, self).__init__(name, ['on_channelcreated', 'on_channeldestroyed', 'on_channelvarset', 'on_client_start', 'on_client_stop', 'on_stasisend', 'on_stasisstart', 'on_ws_open', 'on_ws_closed']) self.__ari = None self.__factory = None self.__ws_client = None self.__channels = [] self.__host = host self.__port = port self.__credentials = credentials def connect_websocket(self): """Creates an AriClientFactory instance and connects to it.""" def wait_for_it(deferred=None): """Waits for the client to reset before connecting the web socket. Keyword Arguments: deferred -- The twisted.defer instance to use for chaining callbacks (optional) (default None). """ msg = '{0} '.format(self) if not deferred: deferred = defer.Deferred() if not self.clean: LOGGER.debug(msg + 'I\'m not so fresh so clean.') reactor.callLater(1, wait_for_it, deferred) else: LOGGER.debug(msg + 'Connecting web socket.') self.__ari = ARI(self.__host, userpass=self.__credentials) self.__factory = AriClientFactory(receiver=self, host=self.__host, port=self.__port, apps=self.name, userpass=self.__credentials) deferred.callback(self.__factory.connect()) self.__reset() wait_for_it() return def __delete_all_channels(self): """Deletes all the channels.""" if len(self.__channels) == 0: return if self.__ari is not None: allow_errors = self.__ari.allow_errors self.__ari.set_allow_errors(True) channels = list().extend(self.__channels) for channel in channels: self.hangup_channel(channel) self.__ari.set_allow_errors(allow_errors) else: del self.__channels[:] return def disconnect_websocket(self): """Disconnects the web socket.""" msg = '{0} '.format(self) if self.__ws_client is None: info = 'Cannot disconnect; no web socket is connected.' LOGGER.debug(msg + info) return self if self.__ari is not None: warning = 'Disconnecting web socket with an active ARI connection.' LOGGER.warn(msg + warning) LOGGER.debug(msg + 'Disconnecting the web socket.') self.__ws_client.transport.loseConnection() return self def hangup_channel(self, channel_id): """Deletes a channel. Keyword Arguments: channel_id -- The id of the channel to delete. Returns: The JSON response object from the DELETE to ARI. Raises: ValueError """ msg = '{0} '.format(self) if self.__ari is None: msg += 'Cannot hangup channel; ARI instance has no value.' raise ValueError(msg.format(self)) LOGGER.debug(msg + 'Deleting channel [{0}].'.format(channel_id)) try: self.__channels.remove(channel_id) except ValueError: pass return self.__ari.delete('channels', channel_id) def on_channelcreated(self, message): """Callback for the ARI 'ChannelCreated' event. Keyword Arguments: message -- the JSON message """ channel = message['channel']['id'] if channel not in self.__channels: self.__channels.append(channel) self.notify_observers('on_channelcreated', message) def on_channeldestroyed(self, message): """Callback for the ARI 'ChannelDestroyed' event. Keyword Arguments: message -- the JSON message """ channel = message['channel']['id'] try: self.__channels.remove(channel) except ValueError: pass self.notify_observers('on_channeldestroyed', message) def on_channelvarset(self, message): """Callback for the ARI 'ChannelVarset' event. Keyword Arguments: message -- the JSON message """ self.notify_observers('on_channelvarset', message) def on_client_start(self): """Notifies the observers of the 'on_client_start' event.""" LOGGER.debug('{0} Client is started.'.format(self)) self.notify_observers('on_client_start', None, True) def on_client_stop(self): """Notifies the observers of the 'on_client_stop' event.""" LOGGER.debug('{0} Client is stopped.'.format(self)) self.notify_observers('on_client_stop', None, True) def on_stasisend(self, message): """Callback for the ARI 'StasisEnd' event Keyword Arguments: message -- the JSON message """ self.notify_observers('on_stasisend', message) def on_stasisstart(self, message): """Callback for the ARI 'StasisEnd' event Keyword Arguments: message -- the JSON message """ self.notify_observers('on_stasisstart', message) def on_ws_closed(self, ws_client): """Callback for AriClientProtocol 'onClose' handler. Keyword Arguments: ws_client -- The AriClientProtocol object that raised the event. """ LOGGER.debug('{0} WebSocket connection closed.'.format(self)) self.__ws_client = None self.notify_observers('on_ws_closed', None) def on_ws_event(self, message): """Callback for AriClientProtocol 'onMessage' handler. Keyword Arguments: message -- The event payload. """ LOGGER.debug("{0} In on_ws_event; message={1}".format(self, message)) event = 'on_{0}'.format(message.get('type').lower()) if event == 'on_ws_open' or event == 'on_ws_closed': return callback = getattr(self, event, None) if callback and callable(callback): callback(message) self.notify_observers(event, message) def on_ws_open(self, ws_client): """Callback for AriClientProtocol 'onOpen' handler. Keyword Arguments: ws_client -- The AriClientProtocol object that raised the event. """ LOGGER.debug('{0} WebSocket connection opened.'.format(self)) self.__ws_client = ws_client self.notify_observers('on_ws_open', None) self.on_client_start() def originate(self, endpoint, app=None): """Originates a channel. Keyword Arguments: endpoint -- The endpoint to use for the ARI request. app -- The name of the Stasis app (optional) (default None). Returns: The JSON response object from the POST to ARI. Raises: ValueError """ msg = '{0} '.format(self) if self.__ari is None: msg += 'Cannot originate channel; ARI instance has no value.' raise ValueError(msg) channel = dict() if app is not None: channel['app'] = app channel['channelId'] = str(uuid.uuid4()) channel['endpoint'] = endpoint msg += 'Originating channel [{0}].' LOGGER.debug(msg.format(channel['channelId'])) return self.__ari.post('channels', **channel) def __reset(self): """Resets the AriClient to its initial state. Returns: A twisted.defer instance. """ if not self.clean: LOGGER.debug('{0} About to reset my state!'.format(self)) self.__tear_down() return def start(self): """Starts the client.""" LOGGER.debug('{0} Starting client connections.'.format(self)) self.connect_websocket() def stop(self): """Stops the client.""" LOGGER.debug('{0} Stopping client connections.'.format(self)) self.suspend() self.__reset() def __tear_down(self): """Tears down the channels and web socket.""" def wait_for_it(deferred=None, run=0): """Disposes each piece, one at a time. The first run (run=0) initialized the deferred and kicks of the process to destroy all of our channels. The second run (run=1) waits for all the channels to be destroyed then kicks off the process to disconnect the web socket. The third run (run=2) waits for the web socket to disconnect then cleans up the remaining state variables. Keyword Arguments: deferred -- The twisted.defer instance to use for chaining callbacks (optional) (default None). run -- The current phase of tear down: 0=Entry phase 1=Waiting for ARI to destroy all channels 2=Calls ARI to Disconnects the web socket 3=Waiting for ARI to disconnect the web socket """ msg = '{0} '.format(self) if not deferred: deferred = defer.Deferred() self.suspend() if run == 0: LOGGER.debug(msg + 'Tearing down active connections.') self.__delete_all_channels() reactor.callLater(2, wait_for_it, deferred, 1) elif run == 1: if len(self.__channels) > 0: msg += 'Waiting for channels to be destroyed.' LOGGER.debug(msg) reactor.callLater(2, wait_for_it, deferred, 1) reactor.callLater(2, wait_for_it, deferred, 2) elif run == 2: LOGGER.debug(msg + 'Disconnecting web socket.') self.__ari = None self.__factory = None self.disconnect_websocket() reactor.callLater(2, wait_for_it, deferred, 3) elif run == 3: if self.__ws_client is not None: msg += 'Waiting for web socket to be destroyed.' LOGGER.debug(msg) reactor.callLater(2, wait_for_it, deferred, 3) else: LOGGER.debug(msg + 'Client successfully torn down.') reactor.callLater(0, self.on_client_stop) reactor.callLater(2, self.reset_registrar) deferred.callback(self.resume()) wait_for_it() return @property def clean(self): """Returns True if the client has no orphaned connections needing to be torn down. False otherwise.""" if len(self.__channels) == 0: LOGGER.debug('{0} No channels!'.format(self)) if self.__ws_client is None: LOGGER.debug('{0} No ws_client!'.format(self)) if self.__ari is None: LOGGER.debug('{0} No ari!'.format(self)) if self.__factory is None: LOGGER.debug('{0} No factory!'.format(self)) LOGGER.debug('{0} I\'m clean!'.format(self)) return True return False