From 6074fe891dc097d9fcdcb112b3a1c1f6a57aba1d Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Tue, 3 Feb 2026 13:19:13 +0000 Subject: [PATCH 01/25] Initial setup to trigger log upload lambda for broadcast alerts --- app/celery/broadcast_message_tasks.py | 5 + app/celery/scheduled_tasks.py | 155 +++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/app/celery/broadcast_message_tasks.py b/app/celery/broadcast_message_tasks.py index 9e3a8ee3a..bfa0acb20 100644 --- a/app/celery/broadcast_message_tasks.py +++ b/app/celery/broadcast_message_tasks.py @@ -131,6 +131,11 @@ def send_broadcast_event(broadcast_event_id): send_broadcast_provider_message.apply_async( kwargs={"broadcast_event_id": broadcast_event_id, "provider": provider}, queue=QueueNames.HIGH_PRIORITY ) + + # request_log_ingest_task.apply_async( + # kwargs={"broadcast_event_id": broadcast_event_id}, queue=QueueNames.BROADCASTS + # ) + except Exception as e: current_app.logger.exception( f"Failed to send broadcast (event id {broadcast_event_id})", diff --git a/app/celery/scheduled_tasks.py b/app/celery/scheduled_tasks.py index 60dfbd5fc..8a24abfbf 100644 --- a/app/celery/scheduled_tasks.py +++ b/app/celery/scheduled_tasks.py @@ -1,6 +1,9 @@ +import json import time from datetime import datetime, timedelta, timezone +import boto3 +import botocore.exceptions from emergency_alerts_utils.celery import QueueNames, TaskNames from flask import current_app from sqlalchemy.exc import SQLAlchemyError @@ -28,7 +31,12 @@ get_user_by_email, save_model_user, ) -from app.models import BroadcastMessage, BroadcastStatusType, Event +from app.models import ( + BroadcastEvent, + BroadcastMessage, + BroadcastStatusType, + Event, +) from app.status.healthcheck import ( get_db_version, post_app_version_to_cloudwatch, @@ -192,3 +200,148 @@ def queue_after_alert_activities(): notify_celery.send_task(name=TaskNames.PUBLISH_GOVUK_ALERTS, queue=QueueNames.GOVUK_ALERTS) # Down the line we will look to request logs from MNOs + + +@notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST_TASK) +def request_log_ingest_task(broadcast_event_id): + """ + Invokes the operator portal log upload Lambda to send invite emails to MNOs + after a broadcast has been sent. + """ + try: + # Get broadcast event details + broadcast_event = BroadcastEvent.query.get(broadcast_event_id) + if not broadcast_event: + current_app.logger.error( + f"Broadcast event {broadcast_event_id} not found", extra={"broadcast_event_id": broadcast_event_id} + ) + return False + + broadcast = broadcast_event.broadcast + + # Build payload for Lambda + payload = { + "alert_reference": broadcast.reference, + "environment": current_app.config.get("ENVIRONMENT"), + "broadcast_start": ( + broadcast_event.transmitted_starts_at.isoformat() if broadcast_event.transmitted_starts_at else None + ), + "broadcast_end": ( + broadcast_event.transmitted_finishes_at.isoformat() if broadcast_event.transmitted_finishes_at else None + ), + "mnos": _get_mno_details(broadcast), + } + + # Invoke the Lambda + lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_NAME") + success = _invoke_log_upload_lambda(lambda_name, payload) + + if success: + current_app.logger.info( + f"Successfully invoked log upload Lambda for broadcast {broadcast.reference}", + extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, + ) + else: + current_app.logger.error( + f"Failed to invoke log upload Lambda for broadcast {broadcast.reference}", + extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, + ) + + return success + + except Exception as e: + current_app.logger.exception( + f"Error in request_log_ingest_task for broadcast_event {broadcast_event_id}", + extra={ + "exception_type": type(e).__name__, + "python_module": __name__, + "exception": str(e), + "broadcast_event_id": broadcast_event_id, + }, + ) + raise + + +def _get_mno_details(broadcast): + """ + Build the MNOs list with their IDs and contact emails + """ + mnos = [] + + # Get the providers used for this broadcast + for provider in broadcast.broadcast_provider_messages: + mno_info = {"mno_id": provider.provider_id, "emails": _get_mno_contact_emails(provider.provider_id)} + mnos.append(mno_info) + + return mnos + + +def _get_mno_contact_emails(provider_id): + """ + Get contact emails for a specific MNO/provider + """ + # TODO: Implement based on where MNO contact details are stored - need to rebuild the database table + + # Example for initial review: + mno_contacts = current_app.config.get("MNO_CONTACT_EMAILS", {}) + return mno_contacts.get(provider_id, []) + + +def _invoke_log_upload_lambda(lambda_name, payload): + """ + Invoke the log upload Lambda function + """ + lambda_client = boto3.client("lambda", region_name="eu-west-2") + payload_bytes = bytes(json.dumps(payload), encoding="utf8") + + try: + current_app.logger.info( + f"Calling log upload lambda {lambda_name}", + extra={ + "lambda_payload": str(payload), + "lambda_invocation_type": "Event", + }, + ) + + response = lambda_client.invoke( + FunctionName=lambda_name, + InvocationType="Event", + Payload=payload_bytes, + ) + + if response["StatusCode"] == 202: + current_app.logger.info( + f"Successfully invoked log upload lambda {lambda_name}", + extra={ + "status_code": response["StatusCode"], + }, + ) + return True + else: + current_app.logger.error( + f"Error invoking log upload lambda {lambda_name}", + extra={ + "status_code": response["StatusCode"], + }, + ) + return False + + except botocore.exceptions.ClientError as e: + current_app.logger.error( + f"Boto3 ClientError calling log upload lambda {lambda_name}", + extra={ + "python_module": __name__, + "error": str(e), + }, + ) + return False + + except Exception as e: + current_app.logger.error( + f"Unexpected error calling log upload lambda {lambda_name}", + extra={ + "python_module": __name__, + "error": str(e), + }, + ) + return False From 70bda68d1304f7a51b9879d60d7a48a7094f4459 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 12:34:04 +0000 Subject: [PATCH 02/25] Added in the name of the lambda to be triggered on alert broadcast --- app/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/config.py b/app/config.py index 6b203a736..5e309e86e 100644 --- a/app/config.py +++ b/app/config.py @@ -63,6 +63,8 @@ class Config(object): CBC_PROXY_ENABLED = True ENABLED_CBCS = {BroadcastProvider.EE, BroadcastProvider.THREE, BroadcastProvider.O2, BroadcastProvider.VODAFONE} + LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + SQLALCHEMY_ENGINE_OPTIONS = { "pool_size": int(os.environ.get("SQLALCHEMY_POOL_SIZE", 5)), "pool_timeout": 30, @@ -229,6 +231,8 @@ class Hosted(Config): CBC_PROXY_ENABLED = True DEBUG = False + LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + TENANT_PREFIX = f"{os.environ.get('TENANT')}-" if os.environ.get("TENANT") is not None else "" ENVIRONMENT = os.getenv("ENVIRONMENT") ENVIRONMENT_PREFIX = ENVIRONMENT if ENVIRONMENT != "development" else "dev" @@ -345,6 +349,8 @@ class Test(Config): REPORTS_SLACK_WEBHOOK_URL = "https://hooks.slack.com/somewhere" CBC_PROXY_ENABLED = True + LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + configs = { "local": Config, From 59a6c5ed4c1920a14246dc822e4b7283a950703f Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 13:47:47 +0000 Subject: [PATCH 03/25] Updated taskname for sending broadcast event on the trigger lambda --- app/celery/broadcast_message_tasks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/celery/broadcast_message_tasks.py b/app/celery/broadcast_message_tasks.py index bfa0acb20..ca9953ea7 100644 --- a/app/celery/broadcast_message_tasks.py +++ b/app/celery/broadcast_message_tasks.py @@ -132,9 +132,9 @@ def send_broadcast_event(broadcast_event_id): kwargs={"broadcast_event_id": broadcast_event_id, "provider": provider}, queue=QueueNames.HIGH_PRIORITY ) - # request_log_ingest_task.apply_async( - # kwargs={"broadcast_event_id": broadcast_event_id}, queue=QueueNames.BROADCASTS - # ) + TaskNames.REQUEST_LOG_INGEST.apply_async( + kwargs={"broadcast_event_id": broadcast_event_id}, queue=QueueNames.BROADCASTS + ) except Exception as e: current_app.logger.exception( From ace88a2d835ce1c3eb3369056664380a96b5455d Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 13:56:05 +0000 Subject: [PATCH 04/25] Corrected import argument for the request_logs_task --- app/celery/broadcast_message_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/celery/broadcast_message_tasks.py b/app/celery/broadcast_message_tasks.py index ca9953ea7..500febb9b 100644 --- a/app/celery/broadcast_message_tasks.py +++ b/app/celery/broadcast_message_tasks.py @@ -5,6 +5,7 @@ from flask import current_app from app import cbc_proxy_client, notify_celery +from app.celery.scheduled_tasks import request_log_ingest_task from app.clients.cbc_proxy import CBCProxyRetryableException from app.dao.broadcast_message_dao import ( create_broadcast_provider_message, @@ -132,7 +133,7 @@ def send_broadcast_event(broadcast_event_id): kwargs={"broadcast_event_id": broadcast_event_id, "provider": provider}, queue=QueueNames.HIGH_PRIORITY ) - TaskNames.REQUEST_LOG_INGEST.apply_async( + request_log_ingest_task.apply_async( kwargs={"broadcast_event_id": broadcast_event_id}, queue=QueueNames.BROADCASTS ) From 9dab1390f3339c49211338bc2f63ef982e1d3a35 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 14:14:09 +0000 Subject: [PATCH 05/25] Moved lambda invocation logic into its own file --- app/celery/broadcast_message_tasks.py | 2 +- app/celery/log_ingest_tasks.py | 156 ++++++++++++++++++++++++++ app/celery/scheduled_tasks.py | 155 +------------------------ 3 files changed, 158 insertions(+), 155 deletions(-) create mode 100644 app/celery/log_ingest_tasks.py diff --git a/app/celery/broadcast_message_tasks.py b/app/celery/broadcast_message_tasks.py index 500febb9b..099685d13 100644 --- a/app/celery/broadcast_message_tasks.py +++ b/app/celery/broadcast_message_tasks.py @@ -5,7 +5,7 @@ from flask import current_app from app import cbc_proxy_client, notify_celery -from app.celery.scheduled_tasks import request_log_ingest_task +from app.celery.log_ingest_tasks import request_log_ingest_task from app.clients.cbc_proxy import CBCProxyRetryableException from app.dao.broadcast_message_dao import ( create_broadcast_provider_message, diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py new file mode 100644 index 000000000..84a4fb4e9 --- /dev/null +++ b/app/celery/log_ingest_tasks.py @@ -0,0 +1,156 @@ +import json + +import boto3 +import botocore.exceptions +from flask import current_app + +from app import notify_celery +from app.celery.tasks import TaskNames +from app.models import BroadcastEvent + + +@notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST_TASK) +def request_log_ingest_task(broadcast_event_id): + """ + Invokes the operator portal log upload Lambda to send invite emails to MNOs + after a broadcast has been sent. + """ + try: + # Get broadcast event details + broadcast_event = BroadcastEvent.query.get(broadcast_event_id) + if not broadcast_event: + current_app.logger.error( + f"Broadcast event {broadcast_event_id} not found", extra={"broadcast_event_id": broadcast_event_id} + ) + return False + + broadcast = broadcast_event.broadcast + + # Build payload for Lambda + payload = { + "alert_reference": broadcast.reference, + "environment": current_app.config.get("ENVIRONMENT"), + "broadcast_start": ( + broadcast_event.transmitted_starts_at.isoformat() if broadcast_event.transmitted_starts_at else None + ), + "broadcast_end": ( + broadcast_event.transmitted_finishes_at.isoformat() if broadcast_event.transmitted_finishes_at else None + ), + "mnos": _get_mno_details(broadcast), + } + + # Invoke the Lambda + lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_NAME") + success = _invoke_log_upload_lambda(lambda_name, payload) + + if success: + current_app.logger.info( + f"Successfully invoked log upload Lambda for broadcast {broadcast.reference}", + extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, + ) + else: + current_app.logger.error( + f"Failed to invoke log upload Lambda for broadcast {broadcast.reference}", + extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, + ) + + return success + + except Exception as e: + current_app.logger.exception( + f"Error in request_log_ingest_task for broadcast_event {broadcast_event_id}", + extra={ + "exception_type": type(e).__name__, + "python_module": __name__, + "exception": str(e), + "broadcast_event_id": broadcast_event_id, + }, + ) + raise + + +def _get_mno_details(broadcast): + """ + Build the MNOs list with their IDs and contact emails + """ + mnos = [] + + # Get the providers used for this broadcast + for provider in broadcast.broadcast_provider_messages: + mno_info = {"mno_id": provider.provider_id, "emails": _get_mno_contact_emails(provider.provider_id)} + mnos.append(mno_info) + + return mnos + + +def _get_mno_contact_emails(provider_id): + """ + Get contact emails for a specific MNO/provider + You'll need to determine where these are stored - perhaps in your database + or config + """ + # TODO: Hardcode placeholder for testing + + # Placeholder example: + mno_contacts = current_app.config.get("MNO_CONTACT_EMAILS", {}) + return mno_contacts.get(provider_id, []) + + +def _invoke_log_upload_lambda(lambda_name, payload): + """ + Invoke the log upload Lambda function + """ + lambda_client = boto3.client("lambda", region_name="eu-west-2") + payload_bytes = bytes(json.dumps(payload), encoding="utf8") + + try: + current_app.logger.info( + f"Calling log upload lambda {lambda_name}", + extra={ + "lambda_payload": str(payload), + "lambda_invocation_type": "Event", + }, + ) + + response = lambda_client.invoke( + FunctionName=lambda_name, + InvocationType="Event", + Payload=payload_bytes, + ) + + if response["StatusCode"] == 202: + current_app.logger.info( + f"Successfully invoked log upload lambda {lambda_name}", + extra={ + "status_code": response["StatusCode"], + }, + ) + return True + else: + current_app.logger.error( + f"Error invoking log upload lambda {lambda_name}", + extra={ + "status_code": response["StatusCode"], + }, + ) + return False + + except botocore.exceptions.ClientError as e: + current_app.logger.error( + f"Boto3 ClientError calling log upload lambda {lambda_name}", + extra={ + "python_module": __name__, + "error": str(e), + }, + ) + return False + + except Exception as e: + current_app.logger.error( + f"Unexpected error calling log upload lambda {lambda_name}", + extra={ + "python_module": __name__, + "error": str(e), + }, + ) + return False diff --git a/app/celery/scheduled_tasks.py b/app/celery/scheduled_tasks.py index 8a24abfbf..60dfbd5fc 100644 --- a/app/celery/scheduled_tasks.py +++ b/app/celery/scheduled_tasks.py @@ -1,9 +1,6 @@ -import json import time from datetime import datetime, timedelta, timezone -import boto3 -import botocore.exceptions from emergency_alerts_utils.celery import QueueNames, TaskNames from flask import current_app from sqlalchemy.exc import SQLAlchemyError @@ -31,12 +28,7 @@ get_user_by_email, save_model_user, ) -from app.models import ( - BroadcastEvent, - BroadcastMessage, - BroadcastStatusType, - Event, -) +from app.models import BroadcastMessage, BroadcastStatusType, Event from app.status.healthcheck import ( get_db_version, post_app_version_to_cloudwatch, @@ -200,148 +192,3 @@ def queue_after_alert_activities(): notify_celery.send_task(name=TaskNames.PUBLISH_GOVUK_ALERTS, queue=QueueNames.GOVUK_ALERTS) # Down the line we will look to request logs from MNOs - - -@notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST_TASK) -def request_log_ingest_task(broadcast_event_id): - """ - Invokes the operator portal log upload Lambda to send invite emails to MNOs - after a broadcast has been sent. - """ - try: - # Get broadcast event details - broadcast_event = BroadcastEvent.query.get(broadcast_event_id) - if not broadcast_event: - current_app.logger.error( - f"Broadcast event {broadcast_event_id} not found", extra={"broadcast_event_id": broadcast_event_id} - ) - return False - - broadcast = broadcast_event.broadcast - - # Build payload for Lambda - payload = { - "alert_reference": broadcast.reference, - "environment": current_app.config.get("ENVIRONMENT"), - "broadcast_start": ( - broadcast_event.transmitted_starts_at.isoformat() if broadcast_event.transmitted_starts_at else None - ), - "broadcast_end": ( - broadcast_event.transmitted_finishes_at.isoformat() if broadcast_event.transmitted_finishes_at else None - ), - "mnos": _get_mno_details(broadcast), - } - - # Invoke the Lambda - lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_NAME") - success = _invoke_log_upload_lambda(lambda_name, payload) - - if success: - current_app.logger.info( - f"Successfully invoked log upload Lambda for broadcast {broadcast.reference}", - extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, - ) - else: - current_app.logger.error( - f"Failed to invoke log upload Lambda for broadcast {broadcast.reference}", - extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, - ) - - return success - - except Exception as e: - current_app.logger.exception( - f"Error in request_log_ingest_task for broadcast_event {broadcast_event_id}", - extra={ - "exception_type": type(e).__name__, - "python_module": __name__, - "exception": str(e), - "broadcast_event_id": broadcast_event_id, - }, - ) - raise - - -def _get_mno_details(broadcast): - """ - Build the MNOs list with their IDs and contact emails - """ - mnos = [] - - # Get the providers used for this broadcast - for provider in broadcast.broadcast_provider_messages: - mno_info = {"mno_id": provider.provider_id, "emails": _get_mno_contact_emails(provider.provider_id)} - mnos.append(mno_info) - - return mnos - - -def _get_mno_contact_emails(provider_id): - """ - Get contact emails for a specific MNO/provider - """ - # TODO: Implement based on where MNO contact details are stored - need to rebuild the database table - - # Example for initial review: - mno_contacts = current_app.config.get("MNO_CONTACT_EMAILS", {}) - return mno_contacts.get(provider_id, []) - - -def _invoke_log_upload_lambda(lambda_name, payload): - """ - Invoke the log upload Lambda function - """ - lambda_client = boto3.client("lambda", region_name="eu-west-2") - payload_bytes = bytes(json.dumps(payload), encoding="utf8") - - try: - current_app.logger.info( - f"Calling log upload lambda {lambda_name}", - extra={ - "lambda_payload": str(payload), - "lambda_invocation_type": "Event", - }, - ) - - response = lambda_client.invoke( - FunctionName=lambda_name, - InvocationType="Event", - Payload=payload_bytes, - ) - - if response["StatusCode"] == 202: - current_app.logger.info( - f"Successfully invoked log upload lambda {lambda_name}", - extra={ - "status_code": response["StatusCode"], - }, - ) - return True - else: - current_app.logger.error( - f"Error invoking log upload lambda {lambda_name}", - extra={ - "status_code": response["StatusCode"], - }, - ) - return False - - except botocore.exceptions.ClientError as e: - current_app.logger.error( - f"Boto3 ClientError calling log upload lambda {lambda_name}", - extra={ - "python_module": __name__, - "error": str(e), - }, - ) - return False - - except Exception as e: - current_app.logger.error( - f"Unexpected error calling log upload lambda {lambda_name}", - extra={ - "python_module": __name__, - "error": str(e), - }, - ) - return False From 9d28bdc3479789d385c4b9a88815236820a0feec Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 14:20:38 +0000 Subject: [PATCH 06/25] Aligned correct naming convention for the TaskName --- app/celery/log_ingest_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 84a4fb4e9..94520a267 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -9,7 +9,7 @@ from app.models import BroadcastEvent -@notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST_TASK) +@notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST) def request_log_ingest_task(broadcast_event_id): """ Invokes the operator portal log upload Lambda to send invite emails to MNOs From 4b253a3339010141765d3bef5267443b8908b6d5 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 14:24:27 +0000 Subject: [PATCH 07/25] Incorrect import statement causing errors, updated --- app/celery/log_ingest_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 94520a267..6f56352a8 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -2,10 +2,10 @@ import boto3 import botocore.exceptions +from emergency_alerts_utils.celery import TaskNames from flask import current_app from app import notify_celery -from app.celery.tasks import TaskNames from app.models import BroadcastEvent From 45a03f1a106aa2264087a25581d6d881168fcb1e Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 14:39:02 +0000 Subject: [PATCH 08/25] update the tests to account for the new request-log-ingest task being called --- tests/app/celery/test_broadcast_message_tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/app/celery/test_broadcast_message_tasks.py b/tests/app/celery/test_broadcast_message_tasks.py index 054ee905f..4e9b9faaf 100644 --- a/tests/app/celery/test_broadcast_message_tasks.py +++ b/tests/app/celery/test_broadcast_message_tasks.py @@ -67,6 +67,8 @@ def test_send_broadcast_event_calls_publish_govuk_alerts_task( "app.celery.broadcast_message_tasks.send_broadcast_provider_message", ) + mocker.patch("app.celery.broadcast_message_tasks.request_log_ingest_task") + mock = mocker.patch("app.celery.broadcast_message_tasks.notify_celery.send_task") with set_config(notify_api, "ENABLED_CBCS", {"ee", "vodafone"}): From f3fa805103570b0a01dbb1c3d6f0db0105b64ab1 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 4 Feb 2026 14:47:34 +0000 Subject: [PATCH 09/25] Hardcoded emails, for initial testing before moving to MNO contact information --- app/celery/log_ingest_tasks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 6f56352a8..e6f5402cc 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -89,11 +89,13 @@ def _get_mno_contact_emails(provider_id): You'll need to determine where these are stored - perhaps in your database or config """ - # TODO: Hardcode placeholder for testing + # TODO: Implement MNO contact information after testing is completed. # Placeholder example: - mno_contacts = current_app.config.get("MNO_CONTACT_EMAILS", {}) - return mno_contacts.get(provider_id, []) + return [ + "alec.ashmore@digital.cabinet-office.gov.uk", + "simon.sorrell@digital.cabinet-office.gov.uk", + ] def _invoke_log_upload_lambda(lambda_name, payload): From 6cc0f56ea2d68d4d70ed60c662358262dad4412a Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 18 Feb 2026 16:29:19 +0000 Subject: [PATCH 10/25] Ensure log ingest task is being picked up --- app/celery/log_ingest_tasks.py | 3 +-- app/config.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index e6f5402cc..9d0239a6a 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -86,8 +86,7 @@ def _get_mno_details(broadcast): def _get_mno_contact_emails(provider_id): """ Get contact emails for a specific MNO/provider - You'll need to determine where these are stored - perhaps in your database - or config + This process is changing! """ # TODO: Implement MNO contact information after testing is completed. diff --git a/app/config.py b/app/config.py index 5e309e86e..a1b320ced 100644 --- a/app/config.py +++ b/app/config.py @@ -143,7 +143,7 @@ class Config(object): SERVICE: Literal["api", "celery"] = os.environ.get("SERVICE") QUEUE_NAME = QueueNames.BROADCASTS if SERVICE == "api" else QueueNames.PERIODIC - TASK_IMPORTS = "broadcast_message_tasks" if SERVICE == "api" else "scheduled_tasks" + TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else "scheduled_tasks" CELERY = { "broker_url": "filesystem://", @@ -241,7 +241,7 @@ class Hosted(Config): SQS_QUEUE_BASE_URL = os.getenv("SQS_QUEUE_BASE_URL") SERVICE = os.environ.get("SERVICE") QUEUE_NAME = QueueNames.BROADCASTS if SERVICE == "api" else QueueNames.PERIODIC - TASK_IMPORTS = "broadcast_message_tasks" if SERVICE == "api" else "scheduled_tasks" + TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else "scheduled_tasks" BEAT_SCHEDULE = { TaskNames.RUN_HEALTH_CHECK: { From c39b88e82c42a76db12ddc2f3d6698ad4d63ed0c Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 11:04:20 +0000 Subject: [PATCH 11/25] addressed issues of lists and arrays colliding on task_imports --- app/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config.py b/app/config.py index a1b320ced..aa1260031 100644 --- a/app/config.py +++ b/app/config.py @@ -241,7 +241,7 @@ class Hosted(Config): SQS_QUEUE_BASE_URL = os.getenv("SQS_QUEUE_BASE_URL") SERVICE = os.environ.get("SERVICE") QUEUE_NAME = QueueNames.BROADCASTS if SERVICE == "api" else QueueNames.PERIODIC - TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else "scheduled_tasks" + TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else ["scheduled_tasks"] BEAT_SCHEDULE = { TaskNames.RUN_HEALTH_CHECK: { @@ -311,7 +311,7 @@ class Hosted(Config): "task_acks_late": True, }, "timezone": "UTC", - "imports": [f"app.celery.{TASK_IMPORTS}"], + "imports": [f"app.celery.{task}" for task in TASK_IMPORTS], "task_queues": [Queue(QUEUE_NAME, Exchange("default"), routing_key=QUEUE_NAME)], "worker_max_tasks_per_child": 10, "beat_schedule": BEAT_SCHEDULE, From 7be7d2c0884743c6424462c012241226de1bfd70 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 12:33:14 +0000 Subject: [PATCH 12/25] addressed issues of lists and arrays colliding on task_imports --- app/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config.py b/app/config.py index aa1260031..4caeacedf 100644 --- a/app/config.py +++ b/app/config.py @@ -143,7 +143,7 @@ class Config(object): SERVICE: Literal["api", "celery"] = os.environ.get("SERVICE") QUEUE_NAME = QueueNames.BROADCASTS if SERVICE == "api" else QueueNames.PERIODIC - TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else "scheduled_tasks" + TASK_IMPORTS = ["broadcast_message_tasks", "log_ingest_tasks"] if SERVICE == "api" else ["scheduled_tasks"] CELERY = { "broker_url": "filesystem://", From 2cbd9c2f6a8042e750003205eda9714dc56d7453 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 14:42:12 +0000 Subject: [PATCH 13/25] Identified error relating to .broadcast not recognised - changed to broadcast_message --- app/celery/log_ingest_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 9d0239a6a..fb6dc8a6e 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -24,7 +24,7 @@ def request_log_ingest_task(broadcast_event_id): ) return False - broadcast = broadcast_event.broadcast + broadcast = broadcast_event.broadcast_message # Build payload for Lambda payload = { From a0e99adec4f28d20ac184a97a76f1d7cc2a94ba1 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 15:10:34 +0000 Subject: [PATCH 14/25] Changed to broadcast_event for the mno contact function --- app/celery/log_ingest_tasks.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index fb6dc8a6e..aedfcde98 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -36,7 +36,7 @@ def request_log_ingest_task(broadcast_event_id): "broadcast_end": ( broadcast_event.transmitted_finishes_at.isoformat() if broadcast_event.transmitted_finishes_at else None ), - "mnos": _get_mno_details(broadcast), + "mnos": _get_mno_details(broadcast_event), } # Invoke the Lambda @@ -69,15 +69,17 @@ def request_log_ingest_task(broadcast_event_id): raise -def _get_mno_details(broadcast): +def _get_mno_details(broadcast_event): """ Build the MNOs list with their IDs and contact emails """ mnos = [] - # Get the providers used for this broadcast - for provider in broadcast.broadcast_provider_messages: - mno_info = {"mno_id": provider.provider_id, "emails": _get_mno_contact_emails(provider.provider_id)} + # Get the providers from the service + providers = broadcast_event.service.get_available_broadcast_providers() + + for provider in providers: + mno_info = {"mno_id": provider.upper(), "emails": _get_mno_contact_emails(provider)} mnos.append(mno_info) return mnos From ebdb5d9d030e15e35f8f4acbdfd1feb3c9f26105 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 15:47:36 +0000 Subject: [PATCH 15/25] Ensured matching lambda names for the log upload to be invoked --- app/celery/log_ingest_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index aedfcde98..f240cace6 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -40,7 +40,7 @@ def request_log_ingest_task(broadcast_event_id): } # Invoke the Lambda - lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_NAME") + lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_ARN") success = _invoke_log_upload_lambda(lambda_name, payload) if success: From d8af2398b93b68aebcc59823ed7bd97e1a7a5244 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 17:45:10 +0000 Subject: [PATCH 16/25] Logging to aid debugging --- app/celery/log_ingest_tasks.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index f240cace6..516103685 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -13,9 +13,10 @@ def request_log_ingest_task(broadcast_event_id): """ Invokes the operator portal log upload Lambda to send invite emails to MNOs - after a broadcast has been sent. """ try: + current_app.logger.info("Starting request_log_ingest_task", extra={"broadcast_event_id": broadcast_event_id}) + # Get broadcast event details broadcast_event = BroadcastEvent.query.get(broadcast_event_id) if not broadcast_event: @@ -39,18 +40,31 @@ def request_log_ingest_task(broadcast_event_id): "mnos": _get_mno_details(broadcast_event), } + current_app.logger.info( + "Built Lambda payload", extra={"alert_reference": broadcast.reference, "payload": json.dumps(payload)} + ) + # Invoke the Lambda - lambda_name = current_app.config.get("LOG_UPLOAD_LAMBDA_ARN") - success = _invoke_log_upload_lambda(lambda_name, payload) + lambda_arn = current_app.config.get("LOG_UPLOAD_LAMBDA_ARN") + + current_app.logger.info( + "About to invoke Lambda", extra={"lambda_arn": lambda_arn, "has_lambda_arn": lambda_arn is not None} + ) + + if not lambda_arn: + current_app.logger.error("LOG_UPLOAD_LAMBDA_ARN not configured!") + return False + + success = _invoke_log_upload_lambda(lambda_arn, payload) if success: current_app.logger.info( - f"Successfully invoked log upload Lambda for broadcast {broadcast.reference}", + "Successfully invoked log upload Lambda", extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, ) else: current_app.logger.error( - f"Failed to invoke log upload Lambda for broadcast {broadcast.reference}", + "Failed to invoke log upload Lambda", extra={"broadcast_reference": broadcast.reference, "broadcast_event_id": broadcast_event_id}, ) From 5a5866ca360f2f2dd26b580a1ba1477eeccc1a74 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Thu, 19 Feb 2026 18:06:51 +0000 Subject: [PATCH 17/25] explicitly listed lambda arn for cross account access --- app/config.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index 4caeacedf..5bdcb3f71 100644 --- a/app/config.py +++ b/app/config.py @@ -63,7 +63,10 @@ class Config(object): CBC_PROXY_ENABLED = True ENABLED_CBCS = {BroadcastProvider.EE, BroadcastProvider.THREE, BroadcastProvider.O2, BroadcastProvider.VODAFONE} - LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + LOG_UPLOAD_LAMBDA_ARN = os.getenv( + "LOG_UPLOAD_LAMBDA_ARN", + "arn:aws:lambda:eu-west-2:435684131547:function:mno-portal-development-log-upload-handler", + ) SQLALCHEMY_ENGINE_OPTIONS = { "pool_size": int(os.environ.get("SQLALCHEMY_POOL_SIZE", 5)), @@ -231,7 +234,10 @@ class Hosted(Config): CBC_PROXY_ENABLED = True DEBUG = False - LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + LOG_UPLOAD_LAMBDA_ARN = os.getenv( + "LOG_UPLOAD_LAMBDA_ARN", + "arn:aws:lambda:eu-west-2:435684131547:function:mno-portal-development-log-upload-handler", + ) TENANT_PREFIX = f"{os.environ.get('TENANT')}-" if os.environ.get("TENANT") is not None else "" ENVIRONMENT = os.getenv("ENVIRONMENT") @@ -349,7 +355,10 @@ class Test(Config): REPORTS_SLACK_WEBHOOK_URL = "https://hooks.slack.com/somewhere" CBC_PROXY_ENABLED = True - LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "mno-portal-development-log-upload-handler") + LOG_UPLOAD_LAMBDA_ARN = os.getenv( + "LOG_UPLOAD_LAMBDA_ARN", + "arn:aws:lambda:eu-west-2:435684131547:function:mno-portal-development-log-upload-handler", + ) configs = { From 758bb993d0971b81739c5ebba271a4e7997bfd32 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Fri, 20 Feb 2026 12:52:08 +0000 Subject: [PATCH 18/25] Added additonal test email to represent MNO --- app/celery/log_ingest_tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 516103685..ffb977f17 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -110,6 +110,7 @@ def _get_mno_contact_emails(provider_id): return [ "alec.ashmore@digital.cabinet-office.gov.uk", "simon.sorrell@digital.cabinet-office.gov.uk", + "mehmet.omer@digital.cabinet-office.gov.uk", ] From 8648cf13b9a9656f7a79d3694159186812167fb6 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Fri, 20 Feb 2026 13:29:35 +0000 Subject: [PATCH 19/25] Remove Mehmet from mno contact --- app/celery/log_ingest_tasks.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index ffb977f17..2fbe7e0c3 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -107,11 +107,7 @@ def _get_mno_contact_emails(provider_id): # TODO: Implement MNO contact information after testing is completed. # Placeholder example: - return [ - "alec.ashmore@digital.cabinet-office.gov.uk", - "simon.sorrell@digital.cabinet-office.gov.uk", - "mehmet.omer@digital.cabinet-office.gov.uk", - ] + return ["alec.ashmore@digital.cabinet-office.gov.uk", "simon.sorrell@digital.cabinet-office.gov.uk"] def _invoke_log_upload_lambda(lambda_name, payload): From 139943d27d1d7accfddf4f2a78fbc146efc03666 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Tue, 21 Apr 2026 09:44:27 +0100 Subject: [PATCH 20/25] MNO contact information --- app/celery/log_ingest_tasks.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 2fbe7e0c3..9520e00fe 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -101,13 +101,18 @@ def _get_mno_details(broadcast_event): def _get_mno_contact_emails(provider_id): """ - Get contact emails for a specific MNO/provider - This process is changing! + Get contact emails for a specific MNO/provider from SSM """ - # TODO: Implement MNO contact information after testing is completed. - # Placeholder example: - return ["alec.ashmore@digital.cabinet-office.gov.uk", "simon.sorrell@digital.cabinet-office.gov.uk"] + ssm = boto3.client("ssm", region_name="eu-west-2") + + try: + response = ssm.get_parameter(Name=f"/operator-portal/mno-emails/{provider_id.lower()}", WithDecryption=True) + emails = response["Parameter"]["Value"].split(",") + return [email.strip() for email in emails] + except ssm.exceptions.ParameterNotFound: + current_app.logger.warning(f"No contact emails configured in SSM for provider {provider_id}") + return [] def _invoke_log_upload_lambda(lambda_name, payload): From 1ab78a11817df7b6ae290e2e8ba82117dc303801 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Tue, 21 Apr 2026 11:32:26 +0100 Subject: [PATCH 21/25] Expanded logging on mno contact function --- app/celery/log_ingest_tasks.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 9520e00fe..ad809be5e 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -107,13 +107,27 @@ def _get_mno_contact_emails(provider_id): ssm = boto3.client("ssm", region_name="eu-west-2") try: - response = ssm.get_parameter(Name=f"/operator-portal/mno-emails/{provider_id.lower()}", WithDecryption=True) + response = ssm.get_parameter( + Name=f"/operator-portal/mno-emails/{provider_id.lower()}", + WithDecryption=True, + ) emails = response["Parameter"]["Value"].split(",") - return [email.strip() for email in emails] + return [email.strip() for email in emails if email.strip()] + except ssm.exceptions.ParameterNotFound: - current_app.logger.warning(f"No contact emails configured in SSM for provider {provider_id}") + current_app.logger.warning( + "No contact emails configured in SSM for provider", + extra={"provider_id": provider_id}, + ) return [] + except botocore.exceptions.ClientError: + current_app.logger.exception( + "Failed retrieving MNO emails from SSM", + extra={"provider_id": provider_id}, + ) + raise + def _invoke_log_upload_lambda(lambda_name, payload): """ From a28de9f2a661088831c2de52ff5800643605de1b Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Tue, 21 Apr 2026 12:27:15 +0100 Subject: [PATCH 22/25] Adjusted helper from boto3 to using ecs task def/config --- app/celery/log_ingest_tasks.py | 39 ++++++++++++++++------------------ app/config.py | 21 ++++++++++++------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index ad809be5e..1fbb4af57 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -100,33 +100,30 @@ def _get_mno_details(broadcast_event): def _get_mno_contact_emails(provider_id): - """ - Get contact emails for a specific MNO/provider from SSM - """ - - ssm = boto3.client("ssm", region_name="eu-west-2") - - try: - response = ssm.get_parameter( - Name=f"/operator-portal/mno-emails/{provider_id.lower()}", - WithDecryption=True, - ) - emails = response["Parameter"]["Value"].split(",") - return [email.strip() for email in emails if email.strip()] - - except ssm.exceptions.ParameterNotFound: + config_key_map = { + "ee": "MNO_EMAILS_EE", + "o2": "MNO_EMAILS_O2", + "three": "MNO_EMAILS_THREE", + "vodafone": "MNO_EMAILS_VODAFONE", + } + + config_key = config_key_map.get(provider_id.lower()) + if not config_key: current_app.logger.warning( - "No contact emails configured in SSM for provider", + "Unknown provider when fetching contact emails", extra={"provider_id": provider_id}, ) return [] - except botocore.exceptions.ClientError: - current_app.logger.exception( - "Failed retrieving MNO emails from SSM", - extra={"provider_id": provider_id}, + raw_value = current_app.config.get(config_key, "") + if not raw_value.strip(): + current_app.logger.warning( + "No contact emails configured for provider", + extra={"provider_id": provider_id, "config_key": config_key}, ) - raise + return [] + + return [email.strip() for email in raw_value.split(",") if email.strip()] def _invoke_log_upload_lambda(lambda_name, payload): diff --git a/app/config.py b/app/config.py index 33645c485..311476861 100644 --- a/app/config.py +++ b/app/config.py @@ -71,6 +71,11 @@ class Config(object): "arn:aws:lambda:eu-west-2:435684131547:function:mno-portal-development-log-upload-handler", ) + MNO_EMAILS_EE = os.getenv("MNO_EMAILS_EE", "") + MNO_EMAILS_O2 = os.getenv("MNO_EMAILS_O2", "") + MNO_EMAILS_THREE = os.getenv("MNO_EMAILS_THREE", "") + MNO_EMAILS_VODAFONE = os.getenv("MNO_EMAILS_VODAFONE", "") + SQLALCHEMY_ENGINE_OPTIONS = { "pool_size": int(os.environ.get("SQLALCHEMY_POOL_SIZE", 5)), "pool_timeout": 30, @@ -84,7 +89,7 @@ class Config(object): ) if os.environ.get("MASTER_USERNAME"): - print("Using master credentials for db connection") + print("Using master credentials for db connection") # noqa: T201 SQLALCHEMY_DATABASE_URI = "postgresql://{user}:{password}@{host}:{port}/{database}".format( user=os.environ.get("MASTER_USERNAME", "root"), password=os.environ.get("MASTER_PASSWORD"), @@ -93,7 +98,7 @@ class Config(object): database=os.environ.get("DATABASE", "emergency_alerts"), ) else: - print("Using no credentials for db connection") + print("Using no credentials for db connection") # noqa: T201 SQLALCHEMY_DATABASE_URI = "postgresql://{user}@{host}:{port}/{database}".format( user=os.environ.get("RDS_USER", "root"), host=os.environ.get("RDS_HOST", "localhost"), @@ -102,7 +107,7 @@ class Config(object): ) if os.environ.get("SQLALCHEMY_LOCAL_OVERRIDE"): - print("Overriding db connection string for local running") + print("Overriding db connection string for local running") # noqa: T201 SQLALCHEMY_DATABASE_URI = os.environ.get("SQLALCHEMY_LOCAL_OVERRIDE") ZENDESK_API_KEY = os.environ.get("ZENDESK_API_KEY") @@ -201,7 +206,9 @@ class Hosted(Config): SUBDOMAIN = ( "dev." if os.environ.get("ENVIRONMENT") == "development" - else f"{os.environ.get('ENVIRONMENT')}." if os.environ.get("ENVIRONMENT") != "production" else "" + else f"{os.environ.get('ENVIRONMENT')}." + if os.environ.get("ENVIRONMENT") != "production" + else "" ) ADMIN_BASE_URL = f"http://admin.{TENANT}ecs.local:6012" ADMIN_EXTERNAL_URL = f"https://{TENANT}admin.{SUBDOMAIN}emergency-alerts.service.gov.uk" @@ -216,7 +223,7 @@ class Hosted(Config): DATABASE_STATEMENT_TIMEOUT_MS = 1200000 if os.getenv("MASTER_USERNAME"): - print("Using master credentials for db connection") + print("Using master credentials for db connection") # noqa: T201 filtered_password = os.environ.get("MASTER_PASSWORD").replace("%", "%%") SQLALCHEMY_DATABASE_URI = "postgresql://{user}:{password}@{host}:{port}/{database}".format( user=os.environ.get("MASTER_USERNAME"), @@ -354,7 +361,9 @@ class Test(Config): SUBDOMAIN = ( "dev." if os.environ.get("ENVIRONMENT") == "development" - else f"{os.environ.get('ENVIRONMENT')}." if os.environ.get("ENVIRONMENT") != "production" else "" + else f"{os.environ.get('ENVIRONMENT')}." + if os.environ.get("ENVIRONMENT") != "production" + else "" ) ADMIN_EXTERNAL_URL = f"https://{TENANT}admin.{SUBDOMAIN}emergency-alerts.service.gov.uk" REPORTS_SLACK_WEBHOOK_URL = "https://hooks.slack.com/somewhere" From 64ac50d4b0d12a0b7457900f61af987021568397 Mon Sep 17 00:00:00 2001 From: leylayaltiligil Date: Tue, 21 Apr 2026 12:46:24 +0100 Subject: [PATCH 23/25] Formatting --- app/config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/config.py b/app/config.py index 311476861..2b626c4c2 100644 --- a/app/config.py +++ b/app/config.py @@ -206,9 +206,7 @@ class Hosted(Config): SUBDOMAIN = ( "dev." if os.environ.get("ENVIRONMENT") == "development" - else f"{os.environ.get('ENVIRONMENT')}." - if os.environ.get("ENVIRONMENT") != "production" - else "" + else f"{os.environ.get('ENVIRONMENT')}." if os.environ.get("ENVIRONMENT") != "production" else "" ) ADMIN_BASE_URL = f"http://admin.{TENANT}ecs.local:6012" ADMIN_EXTERNAL_URL = f"https://{TENANT}admin.{SUBDOMAIN}emergency-alerts.service.gov.uk" @@ -361,9 +359,7 @@ class Test(Config): SUBDOMAIN = ( "dev." if os.environ.get("ENVIRONMENT") == "development" - else f"{os.environ.get('ENVIRONMENT')}." - if os.environ.get("ENVIRONMENT") != "production" - else "" + else f"{os.environ.get('ENVIRONMENT')}." if os.environ.get("ENVIRONMENT") != "production" else "" ) ADMIN_EXTERNAL_URL = f"https://{TENANT}admin.{SUBDOMAIN}emergency-alerts.service.gov.uk" REPORTS_SLACK_WEBHOOK_URL = "https://hooks.slack.com/somewhere" From b269fce884fc5c772ecad2e7496bc316bc2d5b26 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Tue, 28 Apr 2026 09:19:54 +0100 Subject: [PATCH 24/25] Removed the need for mno emails being grabbed from param store, just rely on ids now --- app/celery/log_ingest_tasks.py | 52 +++------------------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/app/celery/log_ingest_tasks.py b/app/celery/log_ingest_tasks.py index 1fbb4af57..cc32f8924 100644 --- a/app/celery/log_ingest_tasks.py +++ b/app/celery/log_ingest_tasks.py @@ -12,12 +12,11 @@ @notify_celery.task(name=TaskNames.REQUEST_LOG_INGEST) def request_log_ingest_task(broadcast_event_id): """ - Invokes the operator portal log upload Lambda to send invite emails to MNOs + Invokes the operator portal log upload Lambda to send invite emails to MNOs. """ try: current_app.logger.info("Starting request_log_ingest_task", extra={"broadcast_event_id": broadcast_event_id}) - # Get broadcast event details broadcast_event = BroadcastEvent.query.get(broadcast_event_id) if not broadcast_event: current_app.logger.error( @@ -27,7 +26,6 @@ def request_log_ingest_task(broadcast_event_id): broadcast = broadcast_event.broadcast_message - # Build payload for Lambda payload = { "alert_reference": broadcast.reference, "environment": current_app.config.get("ENVIRONMENT"), @@ -37,14 +35,15 @@ def request_log_ingest_task(broadcast_event_id): "broadcast_end": ( broadcast_event.transmitted_finishes_at.isoformat() if broadcast_event.transmitted_finishes_at else None ), - "mnos": _get_mno_details(broadcast_event), + "mnos": [ + {"mno_id": provider.upper()} for provider in broadcast_event.service.get_available_broadcast_providers() + ], } current_app.logger.info( "Built Lambda payload", extra={"alert_reference": broadcast.reference, "payload": json.dumps(payload)} ) - # Invoke the Lambda lambda_arn = current_app.config.get("LOG_UPLOAD_LAMBDA_ARN") current_app.logger.info( @@ -83,49 +82,6 @@ def request_log_ingest_task(broadcast_event_id): raise -def _get_mno_details(broadcast_event): - """ - Build the MNOs list with their IDs and contact emails - """ - mnos = [] - - # Get the providers from the service - providers = broadcast_event.service.get_available_broadcast_providers() - - for provider in providers: - mno_info = {"mno_id": provider.upper(), "emails": _get_mno_contact_emails(provider)} - mnos.append(mno_info) - - return mnos - - -def _get_mno_contact_emails(provider_id): - config_key_map = { - "ee": "MNO_EMAILS_EE", - "o2": "MNO_EMAILS_O2", - "three": "MNO_EMAILS_THREE", - "vodafone": "MNO_EMAILS_VODAFONE", - } - - config_key = config_key_map.get(provider_id.lower()) - if not config_key: - current_app.logger.warning( - "Unknown provider when fetching contact emails", - extra={"provider_id": provider_id}, - ) - return [] - - raw_value = current_app.config.get(config_key, "") - if not raw_value.strip(): - current_app.logger.warning( - "No contact emails configured for provider", - extra={"provider_id": provider_id, "config_key": config_key}, - ) - return [] - - return [email.strip() for email in raw_value.split(",") if email.strip()] - - def _invoke_log_upload_lambda(lambda_name, payload): """ Invoke the log upload Lambda function From 389a06109b2f14e05922cb05efb0e17fdca676f6 Mon Sep 17 00:00:00 2001 From: Alec Ashmore Date: Wed, 6 May 2026 09:44:11 +0100 Subject: [PATCH 25/25] Adjusted lambda arn to default as empty --- app/config.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/config.py b/app/config.py index 2b626c4c2..09a682e67 100644 --- a/app/config.py +++ b/app/config.py @@ -89,7 +89,7 @@ class Config(object): ) if os.environ.get("MASTER_USERNAME"): - print("Using master credentials for db connection") # noqa: T201 + print("Using master credentials for db connection") SQLALCHEMY_DATABASE_URI = "postgresql://{user}:{password}@{host}:{port}/{database}".format( user=os.environ.get("MASTER_USERNAME", "root"), password=os.environ.get("MASTER_PASSWORD"), @@ -98,7 +98,7 @@ class Config(object): database=os.environ.get("DATABASE", "emergency_alerts"), ) else: - print("Using no credentials for db connection") # noqa: T201 + print("Using no credentials for db connection") SQLALCHEMY_DATABASE_URI = "postgresql://{user}@{host}:{port}/{database}".format( user=os.environ.get("RDS_USER", "root"), host=os.environ.get("RDS_HOST", "localhost"), @@ -107,7 +107,7 @@ class Config(object): ) if os.environ.get("SQLALCHEMY_LOCAL_OVERRIDE"): - print("Overriding db connection string for local running") # noqa: T201 + print("Overriding db connection string for local running") SQLALCHEMY_DATABASE_URI = os.environ.get("SQLALCHEMY_LOCAL_OVERRIDE") ZENDESK_API_KEY = os.environ.get("ZENDESK_API_KEY") @@ -221,7 +221,7 @@ class Hosted(Config): DATABASE_STATEMENT_TIMEOUT_MS = 1200000 if os.getenv("MASTER_USERNAME"): - print("Using master credentials for db connection") # noqa: T201 + print("Using master credentials for db connection") filtered_password = os.environ.get("MASTER_PASSWORD").replace("%", "%%") SQLALCHEMY_DATABASE_URI = "postgresql://{user}:{password}@{host}:{port}/{database}".format( user=os.environ.get("MASTER_USERNAME"), @@ -367,10 +367,7 @@ class Test(Config): GOVUK_ALERTS_S3_BUCKET_NAME = "test-govuk-alerts-bucket" - LOG_UPLOAD_LAMBDA_ARN = os.getenv( - "LOG_UPLOAD_LAMBDA_ARN", - "arn:aws:lambda:eu-west-2:435684131547:function:mno-portal-development-log-upload-handler", - ) + LOG_UPLOAD_LAMBDA_ARN = os.getenv("LOG_UPLOAD_LAMBDA_ARN", "") configs = {