class CoreBusConsumer(ConsumerMixin): def __init__(self, global_config): self._events_pubsub = Pubsub() self._bus_url = 'amqp://{username}:{password}@{host}:{port}//'.format( **global_config['bus']) self._exchange = Exchange(global_config['bus']['exchange_name'], type=global_config['bus']['exchange_type']) self._queue = kombu.Queue(exclusive=True) self._is_running = False def run(self): logger.info("Running AMQP consumer") with Connection(self._bus_url) as connection: self.connection = connection super().run() def get_consumers(self, Consumer, channel): return [Consumer(self._queue, callbacks=[self._on_bus_message])] def on_connection_error(self, exc, interval): super().on_connection_error(exc, interval) self._is_running = False def on_connection_revived(self): super().on_connection_revived() self._is_running = True def is_running(self): return self._is_running def provide_status(self, status): status['bus_consumer']['status'] = Status.ok if self.is_running( ) else Status.fail def on_ami_event(self, event_type, callback): logger.debug('Added callback on AMI event "%s"', event_type) self._queue.bindings.add( binding(self._exchange, routing_key='ami.{}'.format(event_type))) self._events_pubsub.subscribe(event_type, callback) def _on_bus_message(self, body, message): event = body['data'] try: event_type = event['Event'] except KeyError: logger.error('Invalid AMI event message received: %s', event) else: self._events_pubsub.publish(event_type, event) finally: message.ack()
class BusConsumer(ConsumerMixin): def __init__(self, bus_config): self._bus_url = 'amqp://{username}:{password}@{host}:{port}//'.format( **bus_config) self._exchange = kombu.Exchange(bus_config['subscribe_exchange_name'], type='headers') self._queue = kombu.Queue(exclusive=True) self._pubsub = Pubsub() def run(self): logger.info("Running AMQP consumer") with kombu.Connection(self._bus_url) as connection: self.connection = connection # For internal usage super().run() def get_consumers(self, Consumer, channel): return [Consumer(self._queue, callbacks=[self._on_bus_message])] def on_event(self, event_name, callback): logger.debug('Added callback on event "%s"', event_name) arguments = {'x-match': 'all', 'name': event_name} self._queue.bindings.add( kombu.binding(self._exchange, arguments=arguments)) self._pubsub.subscribe(event_name, callback) def _on_bus_message(self, body, message): try: event_type = body['name'] event = body['data'] except KeyError: logger.error('Invalid event message received: %s', event) message.reject() return try: self._pubsub.publish(event_type, event) except Exception: message.reject() return message.ack()
class CoreBusConsumer(ConsumerMixin): _KEY = 'ami.*' def __init__(self, global_config): self._events_pubsub = Pubsub() self._userevent_pubsub = Pubsub() self._events_pubsub.subscribe( 'UserEvent', lambda message: self._userevent_pubsub.publish( message['UserEvent'], message)) self._bus_url = 'amqp://{username}:{password}@{host}:{port}//'.format( **global_config['bus']) exchange = Exchange(global_config['bus']['exchange_name'], type=global_config['bus']['exchange_type']) self._queue = kombu.Queue(exchange=exchange, routing_key=self._KEY, exclusive=True) def run(self): logger.info("Running AMQP consumer") with Connection(self._bus_url) as connection: self.connection = connection super(CoreBusConsumer, self).run() def get_consumers(self, Consumer, channel): return [ Consumer(self._queue, callbacks=[self._on_bus_message]), ] def on_ami_event(self, event_type, callback): self._events_pubsub.subscribe(event_type, callback) def on_ami_userevent(self, userevent_type, callback): self._userevent_pubsub.subscribe(userevent_type, callback) def _on_bus_message(self, body, message): event = body['data'] event_type = event['Event'] self._events_pubsub.publish(event_type, event) message.ack()
class CoreBusConsumer(ConsumerMixin): _KEY = 'ami.*' def __init__(self, global_config): self._events_pubsub = Pubsub() self._userevent_pubsub = Pubsub() self._events_pubsub.subscribe('UserEvent', lambda message: self._userevent_pubsub.publish(message['UserEvent'], message)) self._bus_url = 'amqp://{username}:{password}@{host}:{port}//'.format(**global_config['bus']) exchange = Exchange(global_config['bus']['exchange_name'], type=global_config['bus']['exchange_type']) self._queue = kombu.Queue(exchange=exchange, routing_key=self._KEY, exclusive=True) def run(self): logger.info("Running AMQP consumer") with Connection(self._bus_url) as connection: self.connection = connection super(CoreBusConsumer, self).run() def get_consumers(self, Consumer, channel): return [ Consumer(self._queue, callbacks=[self._on_bus_message]), ] def on_ami_event(self, event_type, callback): self._events_pubsub.subscribe(event_type, callback) def on_ami_userevent(self, userevent_type, callback): self._userevent_pubsub.subscribe(userevent_type, callback) def _on_bus_message(self, body, message): event = body['data'] event_type = event['Event'] self._events_pubsub.publish(event_type, event) message.ack()
class TestPubsub(unittest.TestCase): def setUp(self): self.pubsub = Pubsub() def test_subscribe_and_publish(self): callback = Mock() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) callback.assert_called_once_with(SOME_MESSAGE) def test_multiple_subscribe_on_same_topic_and_one_publish(self): callback_1 = Mock() callback_2 = Mock() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) callback_1.assert_called_once_with(SOME_MESSAGE) callback_2.assert_called_once_with(SOME_MESSAGE) def test_multiple_subscribe_on_different_topics_and_two_publish(self): callback = Mock() message_1 = Mock() message_2 = Mock() topic_1 = 'abcd' topic_2 = 'efgh' self.pubsub.subscribe(topic_1, callback) self.pubsub.subscribe(topic_2, callback) self.pubsub.publish(topic_1, message_1) self.pubsub.publish(topic_2, message_2) callback.assert_any_call(message_1) callback.assert_any_call(message_2) self.assertEquals(callback.call_count, 2) def test_unsubscribe_when_never_subscribed(self): callback = Mock() try: self.pubsub.unsubscribe(SOME_TOPIC, callback) except Exception: self.fail('unsubscribe should not raise exceptions') def test_unsubscribed_when_subscribed(self): callback = Mock() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.unsubscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) self.assertEquals(callback.call_count, 0) def publish_when_nobody_subscribed(self): try: self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) except Exception: self.fail('publish should not raise exceptions') def test_unsubscribe_when_multiple_subscribers_on_same_topic(self): callback_1 = Mock() callback_2 = Mock() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.unsubscribe(SOME_TOPIC, callback_1) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) assert_that(not_(callback_1.called)) callback_2.assert_called_once_with(SOME_MESSAGE) def test_when_exception_then_exception_is_handled(self): callback = Mock() exception = callback.side_effect = Exception() handler = Mock() self.pubsub.set_exception_handler(handler) self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) handler.assert_called_once_with(callback, SOME_MESSAGE, exception) @patch('xivo.pubsub.logger') def test_when_exception_then_exception_is_logged_by_default(self, logger): callback = Mock() exception = callback.side_effect = Exception() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) logger.exception.assert_called_once_with(exception) def test_when_exception_then_other_callbacks_are_run(self): callback_1, callback_2, callback_3 = Mock(), Mock(), Mock() callback_2.side_effect = Exception() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.subscribe(SOME_TOPIC, callback_3) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) assert_that(callback_1.called) assert_that(callback_3.called)
class TransfersStasis: def __init__(self, amid_client, ari_client, services, state_factory, state_persistor, xivo_uuid): self.ari = ari_client self.amid = amid_client self.services = services self.xivo_uuid = xivo_uuid self.stasis_start_pubsub = Pubsub() self.stasis_start_pubsub.set_exception_handler(self.invalid_event) self.hangup_pubsub = Pubsub() self.hangup_pubsub.set_exception_handler(self.invalid_event) self.state_factory = state_factory self.state_persistor = state_persistor def subscribe(self): self.ari.on_application_registered(DEFAULT_APPLICATION_NAME, self.process_lost_hangups) self.ari.on_channel_event('ChannelEnteredBridge', self.release_hangup_lock) self.ari.on_channel_event('ChannelDestroyed', self.bypass_hangup_lock_from_source) self.ari.on_bridge_event('BridgeDestroyed', self.clean_bridge_variables) self.ari.on_channel_event('ChannelLeftBridge', self.clean_bridge) self.ari.on_channel_event('StasisStart', self.stasis_start) self.stasis_start_pubsub.subscribe('transfer_recipient_called', self.transfer_recipient_answered) self.stasis_start_pubsub.subscribe('create_transfer', self.create_transfer) self.ari.on_channel_event('ChannelDestroyed', self.hangup) self.ari.on_channel_event('StasisEnd', self.hangup) self.ari.on_channel_event('ChannelMohStop', self.moh_stop) self.hangup_pubsub.subscribe(TransferRole.recipient, self.recipient_hangup) self.hangup_pubsub.subscribe(TransferRole.initiator, self.initiator_hangup) self.hangup_pubsub.subscribe(TransferRole.transferred, self.transferred_hangup) self.ari.on_channel_event('ChannelCallerId', self.update_transfer_caller_id) def moh_stop(self, channel, event): logger.debug('received ChannelMohStop for channel %s (%s)', channel.id, event['channel']['name']) try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring ChannelMohStop event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) if transfer_role != TransferRole.transferred: logger.debug('ignoring ChannelMohStop event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_state = self.state_factory.make(transfer) transfer_state.transferred_moh_stop() def invalid_event(self, _, __, exception): if isinstance(exception, InvalidEvent): event = exception.event logger.error('invalid stasis event received: %s', event) elif (isinstance(exception, XiVOAmidError) or isinstance(exception, TransferException)): self.handle_error(exception) else: raise exception def handle_error(self, exception): logger.error('%s: %s', exception.message, exception.details) def process_lost_hangups(self): transfers = list(self.state_persistor.list()) logger.debug('Processing lost hangups since last stop...') for transfer in transfers: transfer_state = self.state_factory.make(transfer) if not Channel(transfer.transferred_call, self.ari).exists(): logger.debug('Transferred hangup from transfer %s', transfer.id) transfer_state = transfer_state.transferred_hangup() if not Channel(transfer.initiator_call, self.ari).exists(): logger.debug('Initiator hangup from transfer %s', transfer.id) transfer_state = transfer_state.initiator_hangup() if not Channel(transfer.recipient_call, self.ari).exists(): logger.debug('Recipient hangup from transfer %s', transfer.id) transfer_state = transfer_state.recipient_hangup() logger.debug('Done.') def stasis_start(self, event_objects, event): channel = event_objects['channel'] try: app_action = event['args'][1] except IndexError: logger.debug( 'ignoring StasisStart event: channel %s, app %s, args %s', event['channel']['name'], event['application'], event['args']) return self.stasis_start_pubsub.publish(app_action, (channel, event)) def hangup(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring StasisEnd event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) self.hangup_pubsub.publish(transfer_role, transfer) def transfer_recipient_answered(self, channel_event): channel, event = channel_event event = TransferRecipientAnsweredEvent(event) try: transfer_bridge = self.ari.bridges.get( bridgeId=event.transfer_bridge) transfer_bridge.addChannel(channel=channel.id) except ARINotFound: logger.error('recipient answered, but transfer was hung up') return try: transfer = self.state_persistor.get(event.transfer_bridge) except KeyError: logger.debug('recipient answered, but transfer was abandoned') for channel_id in transfer_bridge.json['channels']: try: ari_helpers.unring_initiator_call(self.ari, channel_id) except ARINotFound: pass else: logger.debug('recipient answered, transfer continues normally') transfer_state = self.state_factory.make(transfer) transfer_state.recipient_answer() def create_transfer(self, channel_event): channel, event = channel_event event = CreateTransferEvent(event) try: bridge = self.ari.bridges.get(bridgeId=event.transfer_id) except ARINotFound: bridge = self.ari.bridges.createWithId(type='mixing', name='transfer', bridgeId=event.transfer_id) bridge.addChannel(channel=channel.id) channel_ids = bridge.get().json['channels'] if len(channel_ids) == 2: transfer = self.state_persistor.get(event.transfer_id) try: context = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_RECIPIENT_CONTEXT')['value'] exten = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_RECIPIENT_EXTEN')['value'] variables_str = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_VARIABLES')['value'] timeout_str = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_TIMEOUT')['value'] except ARINotFound: logger.error('initiator hung up while creating transfer') try: variables = json.loads(variables_str) except ValueError: logger.warning('could not decode transfer variables "%s"', variables_str) variables = {} timeout = None if timeout_str == 'None' else int(timeout_str) transfer_state = self.state_factory.make(transfer) new_state = transfer_state.start(transfer, context, exten, variables, timeout) if new_state.transfer.flow == 'blind': new_state.complete() def recipient_hangup(self, transfer): logger.debug('recipient hangup = cancel transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.recipient_hangup() def initiator_hangup(self, transfer): logger.debug('initiator hangup = complete transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.initiator_hangup() def transferred_hangup(self, transfer): logger.debug('transferred hangup = abandon transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.transferred_hangup() def clean_bridge(self, channel, event): try: bridge = self.ari.bridges.get(bridgeId=event['bridge']['id']) except ARINotFound: return if bridge.json['bridge_type'] != 'mixing': return logger.debug('cleaning bridge %s', bridge.id) try: self.ari.channels.get(channelId=channel.id) channel_is_hungup = False except ARINotFound: logger.debug('channel who left was hungup') channel_is_hungup = True if len(bridge.json['channels']) == 1 and channel_is_hungup: logger.debug('one channel left in bridge %s', bridge.id) lone_channel_id = bridge.json['channels'][0] try: bridge_is_locked = HangupLock.from_target(self.ari, bridge.id) except InvalidLock: bridge_is_locked = False if not bridge_is_locked: logger.debug('emptying bridge %s', bridge.id) try: self.ari.channels.hangup(channelId=lone_channel_id) except ARINotFound: pass try: bridge = bridge.get() except ARINotFound: return if len(bridge.json['channels']) == 0: self.bypass_hangup_lock_from_target(bridge) logger.debug('destroying bridge %s', bridge.id) try: bridge.destroy() except (ARINotInStasis, ARINotFound): pass def clean_bridge_variables(self, bridge, event): global_variable = 'XIVO_BRIDGE_VARIABLES_{}'.format(bridge.id) self.ari.asterisk.setGlobalVar(variable=global_variable, value='') def release_hangup_lock(self, channel, event): lock_source = channel lock_target_candidate_id = event['bridge']['id'] try: lock = HangupLock(self.ari, lock_source.id, lock_target_candidate_id) lock.release() except InvalidLock: pass def bypass_hangup_lock_from_source(self, channel, event): lock_source = channel for lock in HangupLock.from_source(self.ari, lock_source.id): lock.kill_target() def bypass_hangup_lock_from_target(self, bridge): try: lock = HangupLock.from_target(self.ari, bridge.id) lock.kill_source() except InvalidLock: pass def update_transfer_caller_id(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring ChannelCallerId event: channel %s', event['channel']['name']) return transfer_role = transfer.role(channel.id) if transfer_role != TransferRole.recipient: logger.debug('ignoring ChannelCallerId event: channel %s', event['channel']['name']) return try: ari_helpers.update_connectedline(self.ari, self.amid, transfer.initiator_call, transfer.recipient_call) except ARINotFound: try: ari_helpers.update_connectedline(self.ari, self.amid, transfer.transferred_call, transfer.recipient_call) except ARINotFound: logger.debug( 'cannot update transfer callerid: everyone hung up')
class CoreARI: def __init__(self, config): self._apps = [] self.config = config self._is_running = False self._should_delay_reconnect = True self._should_stop = False self._pubsub = Pubsub() self.client = ARIClientProxy(**config['connection']) def _init_client(self): try: self.client.init() except requests.ConnectionError: logger.info('No ARI server found') return False except requests.HTTPError as e: if asterisk_is_loading(e): logger.info('ARI is not ready yet') return False else: raise self._pubsub.publish('client_initialized', message=None) return True def client_initialized_subscribe(self, callback): self._pubsub.subscribe('client_initialized', callback) def reload(self): self._should_delay_reconnect = False self._trigger_disconnect() def run(self): while not self._should_stop: initialized = self._init_client() if initialized: break connection_delay = self.config['startup_connection_delay'] logger.warning('ARI not initialized, retrying in %s seconds...', connection_delay) time.sleep(connection_delay) self._should_delay_reconnect = False while not self._should_stop: if self._should_delay_reconnect: delay = self.config['reconnection_delay'] logger.warning('Reconnecting to ARI in %s seconds', delay) time.sleep(delay) self._should_delay_reconnect = True self._connect() def _connect(self): logger.debug('ARI client listening...') try: with self._running(): self.client.run(apps=self._apps) except socket.error as e: if e.errno == errno.EPIPE: # bug in ari-py when calling client.close(): ignore it and stop logger.error('Error while listening for ARI events: %s', e) return else: self._connection_error(e) except (WebSocketException, HTTPError) as e: self._connection_error(e) except ValueError: logger.warning('Received non-JSON message from ARI... disconnecting') self.client.close() @contextmanager def _running(self): self._is_running = True try: yield finally: self._is_running = False def register_application(self, app): if app not in self._apps: self._apps.append(app) def deregister_application(self, app): if app in self._apps: self._apps.remove(app) def is_running(self): return self._is_running def provide_status(self, status): status['ari']['status'] = Status.ok if self.is_running() else Status.fail def _connection_error(self, error): logger.warning('ARI connection error: %s...', error) def _sync(self): '''self.sync() should be called before calling self.stop(), in case the ari client does not have the websocket yet''' while self._is_running: try: ari_websockets = self.client.websockets except AsteriskARINotInitialized: ari_websockets = None if ari_websockets: return time.sleep(0.1) def stop(self): self._should_stop = True self._trigger_disconnect() def _trigger_disconnect(self): self._sync() try: self.client.close() except RuntimeError: pass # bug in ari-py when calling client.close()
class TransfersStasis(object): def __init__(self, amid_client, ari_client, services, state_factory, state_persistor, xivo_uuid): self.ari = ari_client self.amid = amid_client self.services = services self.xivo_uuid = xivo_uuid self.stasis_start_pubsub = Pubsub() self.stasis_start_pubsub.set_exception_handler(self.invalid_event) self.hangup_pubsub = Pubsub() self.hangup_pubsub.set_exception_handler(self.invalid_event) self.state_factory = state_factory self.state_persistor = state_persistor def subscribe(self): self.ari.on_application_registered(APPLICATION_NAME, self.process_lost_hangups) self.ari.on_channel_event('ChannelEnteredBridge', self.release_hangup_lock) self.ari.on_channel_event('ChannelDestroyed', self.bypass_hangup_lock_from_source) self.ari.on_bridge_event('BridgeDestroyed', self.clean_bridge_variables) self.ari.on_channel_event('ChannelLeftBridge', self.clean_bridge) self.ari.on_channel_event('StasisStart', self.stasis_start) self.stasis_start_pubsub.subscribe('transfer_recipient_called', self.transfer_recipient_answered) self.stasis_start_pubsub.subscribe('create_transfer', self.create_transfer) self.ari.on_channel_event('ChannelDestroyed', self.hangup) self.ari.on_channel_event('StasisEnd', self.hangup) self.hangup_pubsub.subscribe(TransferRole.recipient, self.recipient_hangup) self.hangup_pubsub.subscribe(TransferRole.initiator, self.initiator_hangup) self.hangup_pubsub.subscribe(TransferRole.transferred, self.transferred_hangup) self.ari.on_channel_event('ChannelCallerId', self.update_transfer_caller_id) def invalid_event(self, _, __, exception): if isinstance(exception, InvalidEvent): event = exception.event logger.error('invalid stasis event received: %s', event) elif (isinstance(exception, XiVOAmidError) or isinstance(exception, TransferException)): self.handle_error(exception) else: raise def handle_error(self, exception): logger.error('%s: %s', exception.message, exception.details) def process_lost_hangups(self): transfers = list(self.state_persistor.list()) logger.debug('Processing lost hangups since last stop...') for transfer in transfers: transfer_state = self.state_factory.make(transfer) if not ari_helpers.channel_exists(self.ari, transfer.transferred_call): logger.debug('Transferred hangup from transfer %s', transfer.id) transfer_state = transfer_state.transferred_hangup() if not ari_helpers.channel_exists(self.ari, transfer.initiator_call): logger.debug('Initiator hangup from transfer %s', transfer.id) transfer_state = transfer_state.initiator_hangup() if not ari_helpers.channel_exists(self.ari, transfer.recipient_call): logger.debug('Recipient hangup from transfer %s', transfer.id) transfer_state = transfer_state.recipient_hangup() logger.debug('Done.') def stasis_start(self, event_objects, event): channel = event_objects['channel'] try: app_action = event['args'][1] except IndexError: logger.debug('ignoring StasisStart event: channel %s, app %s, args %s', event['channel']['name'], event['application'], event['args']) return self.stasis_start_pubsub.publish(app_action, (channel, event)) def hangup(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring StasisEnd event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) self.hangup_pubsub.publish(transfer_role, transfer) def transfer_recipient_answered(self, (channel, event)): event = TransferRecipientAnsweredEvent(event) try: transfer_bridge = self.ari.bridges.get(bridgeId=event.transfer_bridge) transfer_bridge.addChannel(channel=channel.id) except ARINotFound: logger.error('recipient answered, but transfer was hung up') return try: transfer = self.state_persistor.get(event.transfer_bridge) except KeyError: logger.debug('recipient answered, but transfer was abandoned') for channel_id in transfer_bridge.json['channels']: try: ari_helpers.unring_initiator_call(self.ari, channel_id) except ARINotFound: pass else: logger.debug('recipient answered, transfer continues normally') transfer_state = self.state_factory.make(transfer) transfer_state.recipient_answer()