def assert_routing_entries(self, routing_table, expected_entries): self.assertEqual( sorted(routing_table.entries()), sorted( (GoConnector.parse(str(sc)), se, GoConnector.parse(str(dc)), de) for sc, se, dc, de in expected_entries ), )
def acquire_source(self, msg, connector_type, direction, push_hops=True): """Determine the `str(go_connector)` value that a msg came in on by looking at the connector_type and fetching the appropriate values from the `msg` helper_metadata. Raises `UnroutableMessageError` if the connector_type has a value not appropriate for the direction. Note: `str(go_connector)` is what is stored in Go routing tables. """ msg_mdh = self.get_metadata_helper(msg) if direction == self.INBOUND: allowed_types = (self.TRANSPORT_TAG, self.ROUTER, self.BILLING) else: allowed_types = (self.CONVERSATION, self.ROUTER, self.OPT_OUT, self.BILLING) if connector_type not in allowed_types: raise UnroutableMessageError( "Source connector of invalid type: %s" % connector_type, msg) if connector_type == self.CONVERSATION: conv_info = msg_mdh.get_conversation_info() src_conn = str(GoConnector.for_conversation( conv_info['conversation_type'], conv_info['conversation_key'])) elif connector_type == self.ROUTER: router_info = msg_mdh.get_router_info() src_conn = str(GoConnector.for_router( router_info['router_type'], router_info['router_key'], self.router_direction(direction))) elif connector_type == self.TRANSPORT_TAG: src_conn = str(GoConnector.for_transport_tag(*msg_mdh.tag)) elif connector_type == self.OPT_OUT: src_conn = str(GoConnector.for_opt_out()) elif connector_type == self.BILLING: # when the source is a billing router, outbound messages # are always received from the inbound billing connector # and inbound messages are always received from the outbound # billing connector. src_conn = str( GoConnector.for_billing(self.router_direction(direction))) else: raise UnroutableMessageError( "Serious error. Reached apparently unreachable state" " in which source connector type is both valid" " but unknown. Bad connector type is: %s" % connector_type, msg) src_conn_str = str(src_conn) if push_hops: rmeta = RoutingMetadata(msg) rmeta.push_source(src_conn_str, msg.get_routing_endpoint()) return src_conn_str
def test_remove_router(self): rt = self.make_rt({}) router = FakeRouter("router_1", "12345") rin_conn = str(GoConnector.for_router( router.router_type, router.key, GoConnector.INBOUND)) rout_conn = str(GoConnector.for_router( router.router_type, router.key, GoConnector.OUTBOUND)) rt.add_entry(self.CHANNEL_2, 'default', rin_conn, 'default') rt.add_entry(rin_conn, 'default', self.CHANNEL_2, 'default') rt.add_entry(self.CONV_1, 'default', rout_conn, 'default') rt.add_entry(rout_conn, 'default', self.CONV_1, 'default') self.assertNotEqual(list(rt.entries()), []) rt.remove_router(router) self.assert_routing_entries(rt, [])
def test_connector_direction(self): def assert_inbound(conn): self.assertEqual(GoConnector.INBOUND, conn.direction) def assert_outbound(conn): self.assertEqual(GoConnector.OUTBOUND, conn.direction) assert_inbound(GoConnector.for_opt_out()) assert_inbound(GoConnector.for_conversation("conv_type_1", "12345")) assert_outbound(GoConnector.for_transport_tag("tagpool_1", "tag_1")) assert_inbound( GoConnector.for_router("rb_type_1", "12345", GoConnector.INBOUND)) assert_outbound( GoConnector.for_router("rb_type_1", "12345", GoConnector.OUTBOUND))
def test_create_router_connector(self): c = GoConnector.for_router("rb_type_1", "12345", GoConnector.INBOUND) self.assertEqual(c.ctype, GoConnector.ROUTER) self.assertEqual(c.router_type, "rb_type_1") self.assertEqual(c.router_key, "12345") self.assertEqual(c.direction, GoConnector.INBOUND) self.assertEqual(str(c), "ROUTER:rb_type_1:12345:INBOUND")
def publish_inbound_to_billing(self, config, msg): """Publish an inbound message to the billing worker.""" target = (str(GoConnector.for_billing(self.INBOUND)), 'default') dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.INBOUND) yield self.publish_inbound(msg, dst_connector_name, dst_endpoint)
def test_remove_router(self): rt = self.make_rt({}) router = FakeRouter("router_1", "12345") rin_conn = str( GoConnector.for_router(router.router_type, router.key, GoConnector.INBOUND)) rout_conn = str( GoConnector.for_router(router.router_type, router.key, GoConnector.OUTBOUND)) rt.add_entry(self.CHANNEL_2, 'default', rin_conn, 'default') rt.add_entry(rin_conn, 'default', self.CHANNEL_2, 'default') rt.add_entry(self.CONV_1, 'default', rout_conn, 'default') rt.add_entry(rout_conn, 'default', self.CONV_1, 'default') self.assertNotEqual(list(rt.entries()), []) rt.remove_router(router) self.assert_routing_entries(rt, [])
def test_create_router_connector_for_model(self): c = GoConnector.for_model( FakeRouter("rb_type_1", "12345"), GoConnector.INBOUND) self.assertEqual(c.ctype, GoConnector.ROUTER) self.assertEqual(c.router_type, "rb_type_1") self.assertEqual(c.router_key, "12345") self.assertEqual(c.direction, GoConnector.INBOUND) self.assertEqual(str(c), "ROUTER:rb_type_1:12345:INBOUND")
def test_remove_transport_tag(self): tag = ["pool1", "tag1"] rt = self.make_rt({}) tag_conn = str(GoConnector.for_transport_tag(*tag)) rt.add_entry(tag_conn, "default", self.CONV_1, "default") rt.add_entry(self.CONV_1, "default", tag_conn, "default") rt.remove_transport_tag(tag) self.assert_routing_entries(rt, [])
def publish_outbound_to_billing(self, config, msg, tag): """Publish an outbound message to the billing worker.""" msg_mdh = self.get_metadata_helper(msg) msg_mdh.set_tag(tag) target = (str(GoConnector.for_billing(self.OUTBOUND)), 'default') dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.OUTBOUND) yield self.publish_outbound(msg, dst_connector_name, dst_endpoint)
def test_remove_conversation(self): rt = self.make_rt({}) conv = FakeConversation("conv_type_1", "12345") conv_conn = str( GoConnector.for_conversation(conv.conversation_type, conv.key)) rt.add_entry(conv_conn, "default", self.CHANNEL_2, "default") rt.add_entry(self.CHANNEL_2, "default", conv_conn, "default") rt.remove_conversation(conv) self.assert_routing_entries(rt, [])
def test_flip_router_connector(self): c1 = GoConnector.for_router("dummy", "1", GoConnector.INBOUND) c2 = c1.flip_direction() self.assertEqual(c2.direction, GoConnector.OUTBOUND) self.assertEqual(c2.ctype, GoConnector.ROUTER) self.assertEqual(c2.router_type, "dummy") self.assertEqual(c2.router_key, "1") c3 = c2.flip_direction() self.assertEqual(c3.direction, GoConnector.INBOUND)
def publish_outbound_from_billing(self, config, msg): """Publish an outbound message to its intended destination after billing. """ msg_mdh = self.get_metadata_helper(msg) dst_conn = GoConnector.for_transport_tag(*msg_mdh.tag) dst_connector_name, dst_endpoint = yield self.set_destination( msg, [str(dst_conn), 'default'], self.OUTBOUND) yield self.publish_outbound(msg, dst_connector_name, dst_endpoint)
def test_flip_router_connector(self): c1 = GoConnector.for_router( "dummy", "1", GoConnector.INBOUND) c2 = c1.flip_direction() self.assertEqual(c2.direction, GoConnector.OUTBOUND) self.assertEqual(c2.ctype, GoConnector.ROUTER) self.assertEqual(c2.router_type, "dummy") self.assertEqual(c2.router_key, "1") c3 = c2.flip_direction() self.assertEqual(c3.direction, GoConnector.INBOUND)
def process_outbound(self, config, msg, connector_name): """Process an outbound message. Outbound messages can be from: * conversations * routers * the opt-out worker * the billing worker And may go to: * routers * transports * the billing worker """ log.debug("Processing outbound: %s" % (msg,)) msg_mdh = self.get_metadata_helper(msg) msg_mdh.set_user_account(config.user_account_key) connector_type = self.connector_type(connector_name) src_conn = self.acquire_source(msg, connector_type, self.OUTBOUND) if self.billing_outbound_connector: if connector_type in (self.CONVERSATION, self.ROUTER): msg_mdh.reset_paid() elif connector_type == self.OPT_OUT: tag = yield self.tag_for_reply(msg) yield self.publish_outbound_to_billing(config, msg, tag) return elif connector_type == self.BILLING: yield self.publish_outbound_from_billing(config, msg) return else: if connector_type == self.OPT_OUT: yield self.publish_outbound_optout(config, msg) return target = self.find_target(config, msg, src_conn) if target is None: raise NoTargetError( "No target found for outbound message from '%s': %s" % ( connector_name, msg), msg) if self.billing_outbound_connector: target_conn = GoConnector.parse(target[0]) if target_conn.ctype == target_conn.TRANSPORT_TAG: tag = [target_conn.tagpool, target_conn.tagname] yield self.publish_outbound_to_billing(config, msg, tag) return dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.OUTBOUND) yield self.publish_outbound(msg, dst_connector_name, dst_endpoint)
def process_outbound(self, config, msg, connector_name): """Process an outbound message. Outbound messages can be from: * conversations * routers * the opt-out worker * the billing worker And may go to: * routers * transports * the billing worker """ log.debug("Processing outbound: %s" % (msg, )) msg_mdh = self.get_metadata_helper(msg) msg_mdh.set_user_account(config.user_account_key) connector_type = self.connector_type(connector_name) src_conn = self.acquire_source(msg, connector_type, self.OUTBOUND) if self.billing_outbound_connector: if connector_type in (self.CONVERSATION, self.ROUTER): msg_mdh.reset_paid() elif connector_type == self.OPT_OUT: tag = yield self.tag_for_reply(msg) yield self.publish_outbound_to_billing(config, msg, tag) return elif connector_type == self.BILLING: yield self.publish_outbound_from_billing(config, msg) return else: if connector_type == self.OPT_OUT: yield self.publish_outbound_optout(config, msg) return target = self.find_target(config, msg, src_conn) if target is None: raise NoTargetError( "No target found for outbound message from '%s': %s" % (connector_name, msg), msg) if self.billing_outbound_connector: target_conn = GoConnector.parse(target[0]) if target_conn.ctype == target_conn.TRANSPORT_TAG: tag = [target_conn.tagpool, target_conn.tagname] yield self.publish_outbound_to_billing(config, msg, tag) return dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.OUTBOUND) yield self.publish_outbound(msg, dst_connector_name, dst_endpoint)
def publish_outbound_optout(self, config, msg): """Publish a reply from the opt-out worker. It does this by looking up the original message and sending the reply out via the same tag the original message came in on. """ tag = yield self.tag_for_reply(msg) dst_conn = GoConnector.for_transport_tag(*tag) dst_connector_name, dst_endpoint = yield self.set_destination( msg, [str(dst_conn), 'default'], self.OUTBOUND) yield self.publish_outbound(msg, dst_connector_name, dst_endpoint)
def setup_routing(self, user, account_objects): connectors = {} for conv in account_objects['conversations']: connectors[conv['key']] = GoConnector.for_conversation( conv['conversation_type'], conv['key']) for tag in account_objects['channels']: connectors[tag] = GoConnector.for_transport_tag(*(tag.split(':'))) for router in account_objects['routers']: connectors[router['key'] + ':INBOUND'] = GoConnector.for_router( router['router_type'], router['key'], GoConnector.INBOUND) connectors[router['key'] + ':OUTBOUND'] = GoConnector.for_router( router['router_type'], router['key'], GoConnector.OUTBOUND) rt = RoutingTable() for src, src_ep, dst, dst_ep in account_objects['routing_entries']: rt.add_entry( str(connectors[src]), src_ep, str(connectors[dst]), dst_ep) user_account = vumi_api_for_user(user).get_user_account() user_account.routing_table = rt user_account.save() self.stdout.write('Routing table for %s built\n' % (user.email,))
def process_inbound(self, config, msg, connector_name): """Process an inbound message. Inbound messages can be from: * transports (these might be opt-out messages) * routers * the billing worker And may go to: * routers * conversations * the opt-out worker * the billing worker """ log.debug("Processing inbound: %r" % (msg,)) msg_mdh = self.get_metadata_helper(msg) msg_mdh.set_user_account(config.user_account_key) connector_type = self.connector_type(connector_name) src_conn = self.acquire_source(msg, connector_type, self.INBOUND) if self.billing_inbound_connector: if connector_type == self.TRANSPORT_TAG: yield self.publish_inbound_to_billing(config, msg) return if connector_type == self.BILLING: # set the src_conn to the transport and keep routing src_conn = str(GoConnector.for_transport_tag(*msg_mdh.tag)) if msg_mdh.is_optout_message(): yield self.publish_inbound_optout(config, msg) return target = self.find_target(config, msg, src_conn) if target is None: raise NoTargetError( "No target found for inbound message from %r" % (connector_name,), msg) dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.INBOUND) yield self.publish_inbound(msg, dst_connector_name, dst_endpoint)
def process_inbound(self, config, msg, connector_name): """Process an inbound message. Inbound messages can be from: * transports (these might be opt-out messages) * routers * the billing worker And may go to: * routers * conversations * the opt-out worker * the billing worker """ log.debug("Processing inbound: %r" % (msg, )) msg_mdh = self.get_metadata_helper(msg) msg_mdh.set_user_account(config.user_account_key) connector_type = self.connector_type(connector_name) src_conn = self.acquire_source(msg, connector_type, self.INBOUND) if self.billing_inbound_connector: if connector_type == self.TRANSPORT_TAG: yield self.publish_inbound_to_billing(config, msg) return if connector_type == self.BILLING: # set the src_conn to the transport and keep routing src_conn = str(GoConnector.for_transport_tag(*msg_mdh.tag)) if msg_mdh.is_optout_message(): yield self.publish_inbound_optout(config, msg) return target = self.find_target(config, msg, src_conn) if target is None: raise NoTargetError( "No target found for inbound message from %r" % (connector_name, ), msg) dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.INBOUND) yield self.publish_inbound(msg, dst_connector_name, dst_endpoint)
def handle_remove(self, user_api, options): src_conn, src_endpoint, dst_conn, dst_endpoint = options['remove'] account = user_api.get_user_account() if account.routing_table is None: raise CommandError("No routing table found.") target = account.routing_table.lookup_target(src_conn, src_endpoint) if target is None: raise CommandError("No routing entry found for (%s, %s)." % (src_conn, src_endpoint)) elif target != [GoConnector.parse(dst_conn), dst_endpoint]: raise CommandError( "Existing entry (%s, %s) does not match (%s, %s)." % (target[0], target[1], dst_conn, dst_endpoint)) account.routing_table.remove_entry(src_conn, src_endpoint) try: user_api.validate_routing_table(account) except Exception as e: raise CommandError(e) account.save() self.stdout.write("Routing table entry removed.\n")
def handle_remove(self, user_api, options): src_conn, src_endpoint, dst_conn, dst_endpoint = options['remove'] account = user_api.get_user_account() if account.routing_table is None: raise CommandError("No routing table found.") target = account.routing_table.lookup_target(src_conn, src_endpoint) if target is None: raise CommandError("No routing entry found for (%s, %s)." % ( src_conn, src_endpoint)) elif target != [GoConnector.parse(dst_conn), dst_endpoint]: raise CommandError( "Existing entry (%s, %s) does not match (%s, %s)." % ( target[0], target[1], dst_conn, dst_endpoint)) account.routing_table.remove_entry(src_conn, src_endpoint) try: user_api.validate_routing_table(account) except Exception as e: raise CommandError(e) account.save() self.stdout.write("Routing table entry removed.\n")
def assert_connectors(self, connectors, expected_connectors): self.assertEqual( sorted(connectors), sorted( GoConnector.parse(str(conn)) for conn in expected_connectors))
def test_create_transport_tag_connector_for_model(self): c = GoConnector.for_model(FakePlasticChannel("tagpool_1", "tag_1")) self.assertEqual(c.ctype, GoConnector.TRANSPORT_TAG) self.assertEqual(c.tagpool, "tagpool_1") self.assertEqual(c.tagname, "tag_1") self.assertEqual(str(c), "TRANSPORT_TAG:tagpool_1:tag_1")
def test_parse_conversation_connector(self): c = GoConnector.parse("CONVERSATION:conv_type_1:12345") self.assertEqual(c.ctype, GoConnector.CONVERSATION) self.assertEqual(c.conv_type, "conv_type_1") self.assertEqual(c.conv_key, "12345")
def test_parse_opt_out_connector(self): c = GoConnector.parse("OPT_OUT") self.assertEqual(c.ctype, GoConnector.OPT_OUT)
def test_parse_router_connector(self): c = GoConnector.parse("ROUTER:rb_type_1:12345:OUTBOUND") self.assertEqual(c.ctype, GoConnector.ROUTER) self.assertEqual(c.router_type, "rb_type_1") self.assertEqual(c.router_key, "12345") self.assertEqual(c.direction, GoConnector.OUTBOUND)
def test_create_opt_out_connector(self): c = GoConnector.for_opt_out() self.assertEqual(c.ctype, GoConnector.OPT_OUT) self.assertEqual(str(c), "OPT_OUT")
def get_outbound_connector(self): return GoConnector.for_model(self, GoConnector.OUTBOUND)
def get_connector(self): return GoConnector.for_model(self)
def assert_connectors(self, connectors, expected_connectors): self.assertEqual(sorted(connectors), sorted( GoConnector.parse(str(conn)) for conn in expected_connectors))
def set_destination(self, msg, target, direction, push_hops=True): """Parse a target `(str(go_connector), endpoint)` pair and determine the corresponding dispatcher connector to publish on. Set any appropriate Go helper_metadata required by the destination. Raises `UnroutableMessageError` if the parsed `GoConnector` has a connector type not approriate to the message direction. Note: `str(go_connector)` is what is stored in Go routing tables. """ msg_mdh = self.get_metadata_helper(msg) conn = GoConnector.parse(target[0]) if direction == self.INBOUND: allowed_types = ( self.CONVERSATION, self.ROUTER, self.OPT_OUT, self.BILLING) else: allowed_types = ( self.ROUTER, self.TRANSPORT_TAG, self.BILLING) if conn.ctype not in allowed_types: raise UnroutableMessageError( "Destination connector of invalid type: %s" % conn, msg) if conn.ctype == conn.CONVERSATION: msg_mdh.set_conversation_info(conn.conv_type, conn.conv_key) dst_connector_name = self.get_application_connector(conn.conv_type) elif conn.ctype == conn.ROUTER: msg_mdh.set_router_info(conn.router_type, conn.router_key) dst_connector_name = self.get_router_connector( conn.router_type, self.router_direction(direction)) elif conn.ctype == conn.TRANSPORT_TAG: msg_mdh.set_tag([conn.tagpool, conn.tagname]) tagpool_metadata = yield msg_mdh.get_tagpool_metadata() transport_name = tagpool_metadata.get('transport_name') if transport_name is None: raise UnroutableMessageError( "No transport name found for tagpool %r" % conn.tagpool, msg) if self.connector_type(transport_name) != self.TRANSPORT_TAG: raise UnroutableMessageError( "Transport name %r found in tagpool metadata for pool" " %r is invalid." % (transport_name, conn.tagpool), msg) dst_connector_name = transport_name msg['transport_name'] = transport_name transport_type = tagpool_metadata.get('transport_type') if transport_type is not None: msg['transport_type'] = transport_type else: log.error( "No transport type found for tagpool %r while routing %s" % (conn.tagpool, msg)) elif conn.ctype == conn.OPT_OUT: dst_connector_name = self.opt_out_connector elif conn.ctype == conn.BILLING: if direction == self.INBOUND: dst_connector_name = self.billing_inbound_connector else: dst_connector_name = self.billing_outbound_connector else: raise UnroutableMessageError( "Serious error. Reached apparently unreachable state" " in which destination connector type is valid but" " unknown. Bad connector is: %s" % conn, msg) if push_hops: rmeta = RoutingMetadata(msg) rmeta.push_destination(str(conn), target[1]) returnValue((dst_connector_name, target[1]))
def publish_inbound_optout(self, config, msg): """Publish an inbound opt-out request to the opt-out worker.""" target = [str(GoConnector.for_opt_out()), 'default'] dst_connector_name, dst_endpoint = yield self.set_destination( msg, target, self.INBOUND) yield self.publish_inbound(msg, dst_connector_name, dst_endpoint)
def get_inbound_connector(self): return GoConnector.for_model(self, GoConnector.INBOUND)
def test_create_conversation_connector_for_model(self): c = GoConnector.for_model(FakeConversation("conv_type_1", "12345")) self.assertEqual(c.ctype, GoConnector.CONVERSATION) self.assertEqual(c.conv_type, "conv_type_1") self.assertEqual(c.conv_key, "12345") self.assertEqual(str(c), "CONVERSATION:conv_type_1:12345")
def get_outbound_connector(self): return GoConnector.for_router( self.router_type, self.key, GoConnector.OUTBOUND)
def get_connector(self): return GoConnector.for_conversation(self.conversation_type, self.key)
def test_parse_transport_tag_connector(self): c = GoConnector.parse("TRANSPORT_TAG:tagpool_1:tag_1") self.assertEqual(c.ctype, GoConnector.TRANSPORT_TAG) self.assertEqual(c.tagpool, "tagpool_1") self.assertEqual(c.tagname, "tag_1")
def test_lookup_targets(self): rt = self.make_rt() self.assertEqual(sorted(rt.lookup_targets(self.CONV_1)), [ ("default1.1", [GoConnector.parse(self.CHANNEL_2), "default2"]), ("default1.2", [GoConnector.parse(self.CHANNEL_3), "default3"]), ])
def test_lookup_sources(self): rt = self.make_rt() self.assertEqual(sorted(rt.lookup_sources(self.CHANNEL_3)), [ ("default3", [GoConnector.parse(self.CONV_1), "default1.2"]), ])
def test_flip_non_router_connector(self): c = GoConnector.for_conversation("dummy", "1") self.assertRaises(GoConnectorError, c.flip_direction)
def assert_routing_entries(self, routing_table, expected_entries): self.assertEqual( sorted(routing_table.entries()), sorted((GoConnector.parse(str(sc)), se, GoConnector.parse(str(dc)), de) for sc, se, dc, de in expected_entries))
def test_lookup_target(self): rt = self.make_rt() self.assertEqual(rt.lookup_target(self.CONV_1, "default1.1"), [GoConnector.parse(self.CHANNEL_2), "default2"]) self.assertEqual(rt.lookup_target(self.CONV_1, "default1.2"), [GoConnector.parse(self.CHANNEL_3), "default3"])
def mkconn(thing): if isinstance(thing, basestring): return GoConnector.parse(thing) else: # Assume it's a conversation/channel/router. return thing.get_connector()