def _delete_number(db_sess, sip_uri, private_id, delete_digest, on_success, on_failure, force_delete): """ Deletes all information associated with a private/public identity pair, optionally deleting the digest associated with the private identity """ def on_upstream_deletion(*args): _log.info("Deletion from Homestead and Homer was OK - returning number to pool") numbers.remove_owner(db_sess, sip_uri) db_sess.commit() on_success(args) # Concurrently, delete data from Homestead and Homer request_group = HTTPCallbackGroup(on_upstream_deletion, on_failure) if force_delete: request_group = HTTPCallbackGroup(on_success, on_failure) _log.info("Returning number to pool before attempting deletion from Homestead and Homer") numbers.remove_owner(db_sess, sip_uri) db_sess.commit() if delete_digest: # Deleting the private ID will delete its associated IRS (and # therefore any subsidiary public IDs and their service profiles) homestead.delete_private_id(private_id, request_group.callback()) else: homestead.delete_public_id(sip_uri, request_group.callback()) xdm.delete_simservs(sip_uri, request_group.callback())
class SipPasswordHandler(_base.LoggedInHandler): @asynchronous def post(self, username, sip_uri): """Resets the password for the given SIP URI.""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self.sip_digest_realm = utils.sip_uri_to_domain(sip_uri) self.sip_password = utils.generate_sip_password() # Fetch private ID from Homestead for this public ID self._request_group = HTTPCallbackGroup(self.on_get_privates_success, self.on_get_privates_failure) homestead.get_associated_privates(sip_uri, self._request_group.callback()) def on_get_privates_success(self, responses): _log.debug("Got related private ids") # Body is of format {"public_id": "<public_id>", # "private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] # Do not expect a response body, as long as there is no error, we are fine self._request_group = HTTPCallbackGroup(self.on_put_password_success, self.on_put_password_failure) homestead.put_password(private_id, self.sip_digest_realm, self.sip_password, self._request_group.callback()) def on_get_privates_failure(self, response): _log.error("Failed to get associated private ID from homestead %s", response) self.forward_error(response) def on_put_password_success(self, responses): self.finish({"sip_password": self.sip_password}) def on_put_password_failure(self, response): _log.error("Failed to set password in homestead %s", response) self.forward_error(response)
def get(self, username, sip_uri): """Fetches document from remote""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self._request_group = HTTPCallbackGroup(self._on_get_success, self._on_get_failure) self.remote_get(sip_uri, self._request_group.callback())
def _on_get_privates_success(responses): _log.debug("Got related private ids") # Body is of format {"private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] request_group2 = HTTPCallbackGroup(partial(_on_get_publics_success, private_id), on_failure) homestead.get_associated_publics(private_id, request_group2.callback())
def put(self, username, sip_uri): """Updates document on remote""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) response_body = self.request.body self._request_group = HTTPCallbackGroup(self._on_put_success, self._on_put_failure) self.remote_put(sip_uri, response_body, self._request_group.callback())
def _on_get_privates_success(responses): _log.debug("Got related private ids") # Body is of format {"private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] request_group2 = HTTPCallbackGroup( partial(_on_get_publics_success, private_id), on_failure) homestead.get_associated_publics(private_id, request_group2.callback())
class RemoteProxyHandler(_base.LoggedInHandler): def __init__(self, application, request, **kwargs): """ Abstract handler that proxies requests to a remote server (e.g. Homer or Homestead) Subclasses should override self.remote_get and self.remote_put with relevant methods for contacting remotes """ super(RemoteProxyHandler, self).__init__(application, request, **kwargs) # Using the request group approach as we may need to pull/push multiple docs # in the future self._request_group = None self.__response = None self.remote_name = None self.remote_get = None self.remote_put = None @asynchronous def get(self, username, sip_uri): """Fetches document from remote""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self._request_group = HTTPCallbackGroup(self._on_get_success, self._on_get_failure) self.remote_get(sip_uri, self._request_group.callback()) def _on_get_success(self, responses): _log.debug("Successfully fetched from %s" % self.remote_name) self.finish(responses[0].body) def _on_get_failure(self, response): _log.warn("Failed to fetch from %s" % self.remote_name) self.forward_error(response) @asynchronous def put(self, username, sip_uri): """Updates document on remote""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) response_body = self.request.body self._request_group = HTTPCallbackGroup(self._on_put_success, self._on_put_failure) self.remote_put(sip_uri, response_body, self._request_group.callback()) def _on_put_success(self, responses): _log.debug("Successfully updated %s" % self.remote_name) self.finish(self.__response) def _on_put_failure(self, response): _log.warn("Failed to update %s" % self.remote_name) self.forward_error(response)
def remove_public_id(db_sess, sip_uri, on_success, on_failure, force_delete): """ Looks up the private id related to the sip_uri, and then the public ids related the retrieved private id. If there are multiple public ids, then only the sip_uri is is removed from Homestead, and the association to its private id is destroyed. If this is the only public id associated with the private id, then both the private and public ids are removed from Homestead along with their associations """ def _on_get_privates_success(responses): _log.debug("Got related private ids") # Body is of format {"private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] request_group2 = HTTPCallbackGroup( partial(_on_get_publics_success, private_id), on_failure) homestead.get_associated_publics(private_id, request_group2.callback()) def _on_get_publics_success(private_id, responses): _log.debug("Got related public ids") parsed_body = json.loads(responses[0].body) public_ids = parsed_body["associated_public_ids"] if (utils.sip_public_id_to_private(sip_uri) == private_id and len(public_ids) > 1): # Do not permit deletion of an original public identity if others exist # otherwise another may claim the same private id from the pool _log.error( "Failed to delete default public ID %s while other IDs are attached to the same private ID" % sip_uri) _log.error("Other public IDs are %s" % ", ".join(public_ids)) on_failure(responses[0]) else: # Only delete the digest if there is only a single private identity # associated with our sip_uri (i.e. this is the last public id) delete_digest = (len(public_ids) == 1) _delete_number(db_sess, sip_uri, private_id, delete_digest, on_success, on_failure, force_delete) def _on_get_privates_failure(response): if (response.code == 404) or force_delete: # The number has no records in Homestead _log.debug("Failed to retrieve private IDs for a public ID") _log.debug("Returning %s to the pool" % sip_uri) numbers.remove_owner(db_sess, sip_uri) db_sess.commit() on_success({}) else: # pragma: no cover _log.warn("Non-404 response - not returning number to pool") on_failure(response) request_group = HTTPCallbackGroup(_on_get_privates_success, _on_get_privates_failure) homestead.get_associated_privates(sip_uri, request_group.callback())
def post(self, username, sip_uri): """Resets the password for the given SIP URI.""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self.sip_digest_realm = utils.sip_uri_to_domain(sip_uri) self.sip_password = utils.generate_sip_password() # Fetch private ID from Homestead for this public ID self._request_group = HTTPCallbackGroup(self.on_get_privates_success, self.on_get_privates_failure) homestead.get_associated_privates(sip_uri, self._request_group.callback())
def remove_public_id(db_sess, sip_uri, on_success, on_failure, force_delete): """ Looks up the private id related to the sip_uri, and then the public ids related the retrieved private id. If there are multiple public ids, then only the sip_uri is is removed from Homestead, and the association to its private id is destroyed. If this is the only public id associated with the private id, then both the private and public ids are removed from Homestead along with their associations """ def _on_get_privates_success(responses): _log.debug("Got related private ids") # Body is of format {"private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] request_group2 = HTTPCallbackGroup(partial(_on_get_publics_success, private_id), on_failure) homestead.get_associated_publics(private_id, request_group2.callback()) def _on_get_publics_success(private_id, responses): _log.debug("Got related public ids") parsed_body = json.loads(responses[0].body) public_ids = parsed_body["associated_public_ids"] if utils.sip_public_id_to_private(sip_uri) == private_id and len(public_ids) > 1: # Do not permit deletion of an original public identity if others exist # otherwise another may claim the same private id from the pool _log.error( "Failed to delete default public ID %s while other IDs are attached to the same private ID" % sip_uri ) _log.error("Other public IDs are %s" % ", ".join(public_ids)) on_failure(responses[0]) else: # Only delete the digest if there is only a single private identity # associated with our sip_uri (i.e. this is the last public id) delete_digest = len(public_ids) == 1 _delete_number(db_sess, sip_uri, private_id, delete_digest, on_success, on_failure, force_delete) def _on_get_privates_failure(response): if (response.code == 404) or force_delete: # The number has no records in Homestead _log.debug("Failed to retrieve private IDs for a public ID") _log.debug("Returning %s to the pool" % sip_uri) numbers.remove_owner(db_sess, sip_uri) db_sess.commit() on_success({}) else: _log.warn("Non-404 response - not returning number to pool") on_failure(response) request_group = HTTPCallbackGroup(_on_get_privates_success, _on_get_privates_failure) homestead.get_associated_privates(sip_uri, request_group.callback())
def on_get_privates_success(self, responses): _log.debug("Got related private ids") # Body is of format {"public_id": "<public_id>", # "private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] # Do not expect a response body, as long as there is no error, we are fine self._request_group = HTTPCallbackGroup(self.on_put_password_success, self.on_put_password_failure) homestead.put_password(private_id, self.sip_digest_realm, self.sip_password, self._request_group.callback())
def _delete_number(db_sess, sip_uri, private_id, delete_digest, on_success, on_failure): """ Deletes all information associated with a private/public identity pair, optionally deleting the digest associated with the private identity """ numbers.remove_owner(db_sess, sip_uri) db_sess.commit() # Concurrently, delete data from Homestead and Homer request_group = HTTPCallbackGroup(on_success, on_failure) if delete_digest: homestead.delete_private_id(private_id, request_group.callback()) homestead.delete_public_id(sip_uri, request_group.callback()) xdm.delete_simservs(sip_uri, request_group.callback())
class TestHTTPCallbackGroup(unittest.TestCase): def setUp(self): super(TestHTTPCallbackGroup, self).setUp() self.on_success = Mock() self.on_failure = Mock() self.group = HTTPCallbackGroup(self.on_success, self.on_failure) def test_get_callback(self): cb = self.group.callback() cb2 = self.group.callback() self.assertTrue(cb in self.group._live_callbacks) resp = mock_resp() cb(resp) self.assertFalse(cb in self.group._live_callbacks) self.assertTrue(cb2 in self.group._live_callbacks) self.assertTrue(resp in self.group.responses) def test_finish_success(self): cb = self.group.callback() cb2 = self.group.callback() self.assertFalse(self.on_failure.called) self.assertFalse(self.on_success.called) resp = mock_resp() cb(resp) self.assertFalse(self.on_failure.called) self.assertFalse(self.on_success.called) resp2 = mock_resp() cb2(resp2) self.assertFalse(self.on_failure.called) self.on_success.assert_called_once_with([resp, resp2]) def test_finish_success_delete_404(self): cb = self.group.callback() cb2 = self.group.callback() self.assertFalse(self.on_failure.called) self.assertFalse(self.on_success.called) resp = mock_resp(code=404, method="DELETE") cb(resp) self.assertFalse(self.on_failure.called) self.assertFalse(self.on_success.called) resp2 = mock_resp() cb2(resp2) self.assertFalse(self.on_failure.called) self.on_success.assert_called_once_with([resp, resp2]) def test_finish_failure(self): cb = self.group.callback() cb2 = self.group.callback() resp = mock_resp(404) cb(resp) self.on_failure.assert_called_once_with(resp) self.assertFalse(self.on_success.called) self.on_failure.reset_mock() resp2 = mock_resp() cb2(resp2) self.assertFalse(self.on_failure.called) self.assertFalse(self.on_success.called)
def _delete_number(db_sess, sip_uri, private_id, delete_digest, on_success, on_failure, force_delete): """ Deletes all information associated with a private/public identity pair, optionally deleting the digest associated with the private identity """ def on_upstream_deletion(*args): _log.info( "Deletion from Homestead and Homer was OK - returning number to pool" ) numbers.remove_owner(db_sess, sip_uri) db_sess.commit() on_success(args) # Concurrently, delete data from Homestead and Homer request_group = HTTPCallbackGroup(on_upstream_deletion, on_failure) if force_delete: # pragma: no cover request_group = HTTPCallbackGroup(on_success, on_failure) _log.info( "Returning number to pool before attempting deletion from Homestead and Homer" ) numbers.remove_owner(db_sess, sip_uri) db_sess.commit() if delete_digest: # Deleting the private ID will delete its associated IRS (and # therefore any subsidiary public IDs and their service profiles) homestead.delete_private_id(private_id, request_group.callback()) else: homestead.delete_public_id(sip_uri, request_group.callback()) xdm.delete_simservs(sip_uri, request_group.callback())
def post(self, username, sip_uri): """Resets the password for the given SIP URI.""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self.sip_password = utils.generate_sip_password() # Fetch private ID from Homestead for this public ID self._request_group = HTTPCallbackGroup(self.on_get_privates_success, self.on_get_privates_failure) homestead.get_associated_privates(sip_uri, self._request_group.callback())
def get(self, username): """Retrieve list of phone numbers.""" user_id = self.get_and_check_user_id(username) self._numbers = numbers.get_numbers(self.db_session(), user_id) if len(self._numbers) == 0: self.finish({"numbers": []}) return for number in self._numbers: number["number_id"] = number["number_id"].hex number["sip_uri"] = number["number"] number["sip_username"] = utils.sip_uri_to_phone_number(number["number"]) number["domain"] = utils.sip_uri_to_domain(number["number"]) number["number"] = utils.sip_uri_to_phone_number(number["number"]) number["formatted_number"] = format_phone_number(number["number"]) _request_group = HTTPCallbackGroup(partial(self._on_get_success, number["sip_uri"]), self._on_get_failure) # We only store the public identities in Ellis, and must query # Homestead for the associated private identities homestead.get_associated_privates(number["sip_uri"], _request_group.callback())
class SipPasswordHandler(_base.LoggedInHandler): @asynchronous def post(self, username, sip_uri): """Resets the password for the given SIP URI.""" user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) self.sip_digest_realm = utils.sip_uri_to_domain(sip_uri) self.sip_password = utils.generate_sip_password() # Fetch private ID from Homestead for this public ID self._request_group = HTTPCallbackGroup(self.on_get_privates_success, self.on_get_privates_failure) homestead.get_associated_privates(sip_uri, self._request_group.callback()) def on_get_privates_success(self, responses): _log.debug("Got related private ids") # Body is of format {"public_id": "<public_id>", # "private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(responses[0].body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] # Do not expect a response body, as long as there is no error, we are fine self._request_group = HTTPCallbackGroup(self.on_put_password_success, self.on_put_password_failure) homestead.put_password(private_id, self.sip_digest_realm, self.sip_password, self._request_group.callback()) def on_get_privates_failure(self, response): # pragma: no cover _log.error("Failed to get associated private ID from homestead %s", response) self.forward_error(response) def on_put_password_success(self, responses): self.finish({"sip_password": self.sip_password}) def on_put_password_failure(self, response): # pragma: no cover _log.error("Failed to set password in homestead %s", response) self.forward_error(response)
def get(self, username): """Retrieve list of phone numbers.""" user_id = self.get_and_check_user_id(username) self._numbers = numbers.get_numbers(self.db_session(), user_id) if len(self._numbers) == 0: self.finish({"numbers": []}) return for number in self._numbers: number["number_id"] = number["number_id"].hex number["sip_uri"] = number["number"] number["sip_username"] = utils.sip_uri_to_phone_number( number["number"]) number["domain"] = utils.sip_uri_to_domain(number["number"]) number["number"] = utils.sip_uri_to_phone_number(number["number"]) number["formatted_number"] = format_phone_number(number["number"]) _request_group = HTTPCallbackGroup( partial(self._on_get_success, number["sip_uri"]), partial(self._on_get_failure, number["sip_uri"])) # We only store the public identities in Ellis, and must query # Homestead for the associated private identities homestead.get_associated_privates(number["sip_uri"], _request_group.callback())
class NumbersHandler(_base.LoggedInHandler): def __init__(self, application, request, **kwargs): super(NumbersHandler, self).__init__(application, request, **kwargs) self._request_group = None self.__response = None self.__failure_response = None self._numbers = None @asynchronous def get(self, username): """Retrieve list of phone numbers.""" user_id = self.get_and_check_user_id(username) self._numbers = numbers.get_numbers(self.db_session(), user_id) if len(self._numbers) == 0: self.finish({"numbers": []}) return for number in self._numbers: number["number_id"] = number["number_id"].hex number["sip_uri"] = number["number"] number["sip_username"] = utils.sip_uri_to_phone_number(number["number"]) number["domain"] = utils.sip_uri_to_domain(number["number"]) number["number"] = utils.sip_uri_to_phone_number(number["number"]) number["formatted_number"] = utils.format_phone_number(number["number"]) _request_group = HTTPCallbackGroup(partial(self._on_get_success, number["sip_uri"]), self._on_get_failure) # We only store the public identities in Ellis, and must query # Homestead for the associated private identities homestead.get_associated_privates(number["sip_uri"], _request_group.callback()) def _on_get_success(self, public_id, responses): _log.debug("Successfully fetched associated private identities") for response in responses: try: # Body is of format {"public_id": "<public_id>", # "private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(response.body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] for number in [n for n in self._numbers if n["sip_uri"] == public_id]: number["private_id"] = private_id except (TypeError, KeyError) as e: _log.error("Could not parse response: %s", response.body) self.send_error(httplib.BAD_GATEWAY, reason="Upstream request failed: could not parse private identity list") return for number in self._numbers: if "private_id" not in number: _log.debug("Not all numbers have private IDs associated, not sending response to the browser yet...") return self.finish({"numbers": self._numbers}) def _on_get_failure(self, response): _log.warn("Failed to fetch private identities from homestead") self.forward_error(response) @asynchronous def post(self, username): """Allocate a phone number.""" _log.debug("Number allocation API call (PSTN = %s)", self.get_argument('pstn', 'false')) user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false') == 'true' private_id = self.get_argument('private_id', None) try: number_id = numbers.allocate_number(db_sess, user_id, pstn) sip_uri = numbers.get_number(db_sess, number_id, user_id) self.sip_uri = sip_uri _log.debug("SIP URI %s", sip_uri) # FIXME We shouldn't commit until we know XDM/HS have succeeded but # if we hold the transaction open we can deadlock # * Request 1 comes in and allocates a number, kicks off requests # to XDM/Homestead, has transaction open, returns thread to Tornado # * Request 2 comes in, allocates same number, can't lock it for # update because Request 1 is holding it. Blocks. # * Request 1 gets response but the thread is tied up # * Request 2 SQL transaction times out. # * Request 1 probably completes.. db_sess.commit() except NotFound: # FIXME email operator to tell them we're out of numbers! _log.warning("No available numbers") raise HTTPError(httplib.SERVICE_UNAVAILABLE, "No available numbers") # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = utils.format_phone_number(number) self.__response = {"sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex} # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) sip_password = utils.generate_sip_password() homestead.create_private_id(private_id, sip_password, self._request_group.callback()) self.__response["sip_password"] = sip_password # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id(private_id, sip_uri, ifcs.generate_ifcs(settings.SIP_DIGEST_REALM), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. with open(settings.XDM_DEFAULT_SIMSERVS_FILE) as xml_file: default_xml = xml_file.read() xdm.put_simservs(sip_uri, default_xml, self._request_group.callback()) def _on_post_success(self, responses): _log.debug("Successfully updated all the backends") self.finish(self.__response) def _on_post_failure(self, response): _log.warn("Failed to update all the backends") # Save off response so we can pass error through self.__failure_response = response # Try to back out the changes so we don't leave orphaned data. remove_public_id(self.db_session(), self.sip_uri, self._on_backout_success, self._on_backout_failure) def _on_backout_success(self, responses): _log.warn("Backed out changes after failure") self.forward_error(self.__failure_response) def _on_backout_failure(self, responses): _log.warn("Failed to back out changes after failure") self.forward_error(self.__failure_response)
def post(self, username): """Allocate a phone number.""" _log.debug("Number allocation API call (PSTN = %s)", self.get_argument('pstn', 'false')) user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false').lower() == 'true' private_id = self.get_argument('private_id', None) try: number_id = numbers.allocate_number(db_sess, user_id, pstn) sip_uri = numbers.get_number(db_sess, number_id, user_id) self.sip_uri = sip_uri _log.debug("SIP URI %s", sip_uri) # FIXME We shouldn't commit until we know XDM/HS have succeeded but # if we hold the transaction open we can deadlock # * Request 1 comes in and allocates a number, kicks off requests # to XDM/Homestead, has transaction open, returns thread to Tornado # * Request 2 comes in, allocates same number, can't lock it for # update because Request 1 is holding it. Blocks. # * Request 1 gets response but the thread is tied up # * Request 2 SQL transaction times out. # * Request 1 probably completes.. db_sess.commit() except NotFound: # pragma: no cover # FIXME email operator to tell them we're out of numbers! db_sess.rollback() _log.warning("No available numbers") raise HTTPError(httplib.SERVICE_UNAVAILABLE, "No available numbers") # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id(private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback()) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. xdm.put_simservs(sip_uri, simservs.default_simservs(), self._request_group.callback())
def post(self, username): """Allocate a phone number.""" _log.debug("Number allocation API call (PSTN = %s)", self.get_argument('pstn', 'false')) user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false') == 'true' private_id = self.get_argument('private_id', None) try: number_id = numbers.allocate_number(db_sess, user_id, pstn) sip_uri = numbers.get_number(db_sess, number_id, user_id) self.sip_uri = sip_uri _log.debug("SIP URI %s", sip_uri) # FIXME We shouldn't commit until we know XDM/HS have succeeded but # if we hold the transaction open we can deadlock # * Request 1 comes in and allocates a number, kicks off requests # to XDM/Homestead, has transaction open, returns thread to Tornado # * Request 2 comes in, allocates same number, can't lock it for # update because Request 1 is holding it. Blocks. # * Request 1 gets response but the thread is tied up # * Request 2 SQL transaction times out. # * Request 1 probably completes.. db_sess.commit() except NotFound: # FIXME email operator to tell them we're out of numbers! _log.warning("No available numbers") raise HTTPError(httplib.SERVICE_UNAVAILABLE, "No available numbers") # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = utils.format_phone_number(number) self.__response = {"sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex} # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) sip_password = utils.generate_sip_password() homestead.create_private_id(private_id, sip_password, self._request_group.callback()) self.__response["sip_password"] = sip_password # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id(private_id, sip_uri, ifcs.generate_ifcs(settings.SIP_DIGEST_REALM), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. with open(settings.XDM_DEFAULT_SIMSERVS_FILE) as xml_file: default_xml = xml_file.read() xdm.put_simservs(sip_uri, default_xml, self._request_group.callback())
class NumberHandler(_base.LoggedInHandler): def __init__(self, application, request, **kwargs): super(NumberHandler, self).__init__(application, request, **kwargs) self.__password_response = None self.__ifc_response = None self.__xdm_response = None @asynchronous def delete(self, username, sip_uri): """Deletes a given SIP URI.""" _log.info("Request to delete %s by %s", sip_uri, username) user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) # Set force_delete=False - if we can't remove the number from # Homestead/Homer, we'll keep the number assigned to the user # (who can delete it in future) rather than removing it and # leaving orphaned data in Homestead. remove_public_id(self.db_session(), sip_uri, self._on_delete_success, self._on_delete_failure, force_delete=False) def _on_delete_success(self, responses): _log.debug("All requests successful.") self.finish({}) def _on_delete_failure(self, response): # pragma: no cover _log.debug("At least one request failed.") self.forward_error(response) @asynchronous def post(self, username, sip_uri): # pragma: no cover """Allocate a phone number.""" _log.debug("Specific number allocation API call (%s)", sip_uri) self.is_admin_request() user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false').lower() == 'true' private_id = self.get_argument('private_id', None) new_private_id = self.get_argument('new_private_id', 'false').lower() == 'true' try: number_id = uuid.UUID( numbers.get_sip_uri_number_id(db_sess, sip_uri)) except NotFound: # This SIP URI is not currently in the pool, so add it number_id = numbers.add_number_to_pool(db_sess, sip_uri, False, True) numbers.allocate_specific_number(db_sess, user_id, number_id) self.sip_uri = sip_uri db_sess.commit() # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) new_private_id = True if new_private_id: sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id(private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback()) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password self.__response["sip_username"] = private_id # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. xdm.put_simservs(sip_uri, simservs.default_simservs(), self._request_group.callback()) def _on_post_success(self, responses): # pragma: no cover _log.debug("Successfully updated all the backends") self.finish(self.__response) def _on_post_failure(self, response): # pragma: no cover _log.warn("Failed to update all the backends") # Save off response so we can pass error through self.__failure_response = response # Try to back out the changes so we don't leave orphaned data. # Set force_delete=True - this means that we remove the # partially-created line from Ellis even if Homestead is still # down and our DELETE requests aren't successful. remove_public_id(self.db_session(), self.sip_uri, self._on_backout_success, self._on_backout_failure, force_delete=True)
class NumbersHandler(_base.LoggedInHandler): def __init__(self, application, request, **kwargs): super(NumbersHandler, self).__init__(application, request, **kwargs) self._request_group = None self.__response = None self.__failure_response = None self._numbers = None @asynchronous def get(self, username): """Retrieve list of phone numbers.""" user_id = self.get_and_check_user_id(username) self._numbers = numbers.get_numbers(self.db_session(), user_id) if len(self._numbers) == 0: self.finish({"numbers": []}) return for number in self._numbers: number["number_id"] = number["number_id"].hex number["sip_uri"] = number["number"] number["sip_username"] = utils.sip_uri_to_phone_number( number["number"]) number["domain"] = utils.sip_uri_to_domain(number["number"]) number["number"] = utils.sip_uri_to_phone_number(number["number"]) number["formatted_number"] = format_phone_number(number["number"]) _request_group = HTTPCallbackGroup( partial(self._on_get_success, number["sip_uri"]), partial(self._on_get_failure, number["sip_uri"])) # We only store the public identities in Ellis, and must query # Homestead for the associated private identities homestead.get_associated_privates(number["sip_uri"], _request_group.callback()) def _on_get_success(self, public_id, responses): _log.debug("Successfully fetched associated private identities") for response in responses: try: # Body is of format {"public_id": "<public_id>", # "private_ids": ["<private_id_1>", "<private_id_2>"...]} parsed_body = json.loads(response.body) # We only support one private id per public id, so only pull out first in list private_id = parsed_body["private_ids"][0] for number in [ n for n in self._numbers if n["sip_uri"] == public_id ]: number["private_id"] = private_id except (TypeError, KeyError): # pragma: no cover _log.error("Could not parse response: %s", response.body) self.send_error( httplib.BAD_GATEWAY, reason= "Upstream request failed: could not parse private identity list" ) return for number in self._numbers: if "private_id" not in number: _log.debug( "Not all numbers have private IDs associated, not sending response to the browser yet..." ) return self.finish({"numbers": self._numbers}) def _on_get_failure(self, sip_uri, response): # pragma: no cover _log.warn("Failed to fetch private identities from homestead for %s", sip_uri) if hasattr(response, 'code') and response.code == 404: # The number has no records in Homestead, so forget about it locally _log.warn("Returning %s to the pool", sip_uri) db_sess = self.db_session() numbers.remove_owner(db_sess, sip_uri) db_sess.commit() # Also try and remove it from Homer, but do nothing if we fail xdm.delete_simservs(sip_uri, lambda responses: None) self.forward_error(response) @asynchronous def post(self, username): """Allocate a phone number.""" _log.debug("Number allocation API call (PSTN = %s)", self.get_argument('pstn', 'false')) user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false').lower() == 'true' private_id = self.get_argument('private_id', None) try: number_id = numbers.allocate_number(db_sess, user_id, pstn) sip_uri = numbers.get_number(db_sess, number_id, user_id) self.sip_uri = sip_uri _log.debug("SIP URI %s", sip_uri) # FIXME We shouldn't commit until we know XDM/HS have succeeded but # if we hold the transaction open we can deadlock # * Request 1 comes in and allocates a number, kicks off requests # to XDM/Homestead, has transaction open, returns thread to Tornado # * Request 2 comes in, allocates same number, can't lock it for # update because Request 1 is holding it. Blocks. # * Request 1 gets response but the thread is tied up # * Request 2 SQL transaction times out. # * Request 1 probably completes.. db_sess.commit() except NotFound: # pragma: no cover # FIXME email operator to tell them we're out of numbers! db_sess.rollback() _log.warning("No available numbers") raise HTTPError(httplib.SERVICE_UNAVAILABLE, "No available numbers") # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id(private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback()) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. xdm.put_simservs(sip_uri, simservs.default_simservs(), self._request_group.callback()) def _on_post_success(self, responses): _log.debug("Successfully updated all the backends") self.finish(self.__response) def _on_post_failure(self, response): _log.warn("Failed to update all the backends") # Save off response so we can pass error through self.__failure_response = response # Try to back out the changes so we don't leave orphaned data. # Try to back out the changes so we don't leave orphaned data. # Set force_delete=True - this means that we remove the # partially-created line from Ellis even if Homestead is still # down and our DELETE requests aren't successful. remove_public_id(self.db_session(), self.sip_uri, self._on_backout_success, self._on_backout_failure, force_delete=True) def _on_backout_success(self, responses): # pragma: no cover _log.warn("Backed out changes after failure") self.forward_error(self.__failure_response) def _on_backout_failure(self, responses): # pragma: no cover _log.warn("Failed to back out changes after failure") self.forward_error(self.__failure_response)
def post(self, username, sip_uri): # pragma: no cover """Allocate a phone number.""" _log.debug("Specific number allocation API call (%s)", sip_uri) self.is_admin_request() user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument('pstn', 'false').lower() == 'true' private_id = self.get_argument('private_id', None) new_private_id = self.get_argument('new_private_id', 'false').lower() == 'true' try: number_id = uuid.UUID( numbers.get_sip_uri_number_id(db_sess, sip_uri)) except NotFound: # This SIP URI is not currently in the pool, so add it number_id = numbers.add_number_to_pool(db_sess, sip_uri, False, True) numbers.allocate_specific_number(db_sess, user_id, number_id) self.sip_uri = sip_uri db_sess.commit() # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) new_private_id = True if new_private_id: sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id(private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback()) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password self.__response["sip_username"] = private_id # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. xdm.put_simservs(sip_uri, simservs.default_simservs(), self._request_group.callback())
def post(self, username, sip_uri): """Allocate a phone number.""" _log.debug("Specific number allocation API call (%s)", sip_uri) self.is_admin_request() user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument("pstn", "false").lower() == "true" private_id = self.get_argument("private_id", None) new_private_id = self.get_argument("new_private_id", "false").lower() == "true" try: number_id = uuid.UUID(numbers.get_sip_uri_number_id(db_sess, sip_uri)) except NotFound: # This SIP URI is not currently in the pool, so add it number_id = numbers.add_number_to_pool(db_sess, sip_uri, False, True) numbers.allocate_specific_number(db_sess, user_id, number_id) self.sip_uri = sip_uri db_sess.commit() # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex, } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) new_private_id = True if new_private_id: sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id( private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback() ) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password self.__response["sip_username"] = private_id # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback ) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. with open(settings.XDM_DEFAULT_SIMSERVS_FILE) as xml_file: default_xml = xml_file.read() xdm.put_simservs(sip_uri, default_xml, self._request_group.callback())
class NumberHandler(_base.LoggedInHandler): def __init__(self, application, request, **kwargs): super(NumberHandler, self).__init__(application, request, **kwargs) self.__password_response = None self.__ifc_response = None self.__xdm_response = None @asynchronous def delete(self, username, sip_uri): """Deletes a given SIP URI.""" _log.info("Request to delete %s by %s", sip_uri, username) user_id = self.get_and_check_user_id(username) self.check_number_ownership(sip_uri, user_id) # Set force_delete=False - if we can't remove the number from # Homestead/Homer, we'll keep the number assigned to the user # (who can delete it in future) rather than removing it and # leaving orphaned data in Homestead. remove_public_id( self.db_session(), sip_uri, self._on_delete_success, self._on_delete_failure, force_delete=False ) def _on_delete_success(self, responses): _log.debug("All requests successful.") self.finish({}) def _on_delete_failure(self, response): _log.debug("At least one request failed.") self.forward_error(response) @asynchronous def post(self, username, sip_uri): """Allocate a phone number.""" _log.debug("Specific number allocation API call (%s)", sip_uri) self.is_admin_request() user_id = self.get_and_check_user_id(username) db_sess = self.db_session() pstn = self.get_argument("pstn", "false").lower() == "true" private_id = self.get_argument("private_id", None) new_private_id = self.get_argument("new_private_id", "false").lower() == "true" try: number_id = uuid.UUID(numbers.get_sip_uri_number_id(db_sess, sip_uri)) except NotFound: # This SIP URI is not currently in the pool, so add it number_id = numbers.add_number_to_pool(db_sess, sip_uri, False, True) numbers.allocate_specific_number(db_sess, user_id, number_id) self.sip_uri = sip_uri db_sess.commit() # Work out the response we'll send if the upstream requests # are successful. number = utils.sip_uri_to_phone_number(sip_uri) pretty_number = format_phone_number(number) self.__response = { "sip_uri": sip_uri, "sip_username": number, "number": number, "pstn": pstn, "formatted_number": pretty_number, "number_id": number_id.hex, } # Generate a random password and store it in Homestead. _log.debug("Populating other servers...") self._request_group = HTTPCallbackGroup(self._on_post_success, self._on_post_failure) public_callback = self._request_group.callback() if private_id == None: # No private id was provided, so we need to create a new # digest in Homestead private_id = utils.sip_public_id_to_private(sip_uri) new_private_id = True if new_private_id: sip_password = utils.generate_sip_password() _log.debug("About to create private ID at Homestead") homestead.create_private_id( private_id, utils.sip_uri_to_domain(sip_uri), sip_password, self._request_group.callback() ) _log.debug("Created private ID at Homestead") self.__response["sip_password"] = sip_password self.__response["sip_username"] = private_id # Associate the new public identity with the private identity in Homestead # and store the iFCs in homestead. homestead.create_public_id( private_id, sip_uri, ifcs.generate_ifcs(utils.sip_uri_to_domain(sip_uri)), public_callback ) self.__response["private_id"] = private_id # Concurrently, store the default simservs in XDM. with open(settings.XDM_DEFAULT_SIMSERVS_FILE) as xml_file: default_xml = xml_file.read() xdm.put_simservs(sip_uri, default_xml, self._request_group.callback()) def _on_post_success(self, responses): _log.debug("Successfully updated all the backends") self.finish(self.__response) def _on_post_failure(self, response): _log.warn("Failed to update all the backends") # Save off response so we can pass error through self.__failure_response = response # Try to back out the changes so we don't leave orphaned data. # Set force_delete=True - this means that we remove the # partially-created line from Ellis even if Homestead is still # down and our DELETE requests aren't successful. remove_public_id( self.db_session(), self.sip_uri, self._on_backout_success, self._on_backout_failure, force_delete=True )
def setUp(self): super(TestHTTPCallbackGroup, self).setUp() self.on_success = Mock() self.on_failure = Mock() self.group = HTTPCallbackGroup(self.on_success, self.on_failure)