From 832c67eda51d1e76dbe837ceae92003b1fcde56d Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Mon, 28 Feb 2022 14:46:37 -0700 Subject: [PATCH 01/15] Wait to save task result until after attempt was made to notify callback url and update status to notification_failed if notification fails. --- src/requirements.in | 2 +- src/scheduler/scheduler.py | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/requirements.in b/src/requirements.in index 6df1f656..73382008 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -14,7 +14,7 @@ oauthlib~=3.1.0 psycopg2-binary==2.8.5 pyjwt~=1.7.1 raven==6.10.0 -requests-futures==0.9.9 +requests-futures==1.0.0 requests-mock==1.6.0 requests_oauthlib~=1.3.0 #scos_actions @ git+https://github.com/NTIA/scos-actions@bug_fix#egg=scos_actions diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index dfe2db4c..13c84ccd 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -169,13 +169,25 @@ def _call_task_action(self): return status, detail[:MAX_DETAIL_LEN] def _finalize_task_result(self, started, finished, status, detail): + + + tr = self.task_result tr.started = started tr.finished = finished tr.duration = finished - started tr.status = status tr.detail = detail - tr.save() + + def response_task_result_handler(resp, *args, **kwargs): + if resp.ok: + logger.info("POSTed to {}".format(resp.url)) + else: + msg = "Failed to POST to {}: {}" + logger.warning(msg.format(resp.url, resp.reason)) + tr.status = 'notification_failed' + tr.save() + if self.entry.callback_url: try: @@ -196,27 +208,20 @@ def _finalize_task_result(self, started, finished, status, detail): headers=headers, verify=verify_ssl, ) - self._callback_response_handler(client, response) + response_task_result_handler(response) else: token = self.entry.owner.auth_token headers = {"Authorization": "Token " + str(token)} requests_futures_session.post( self.entry.callback_url, json=result_json, - background_callback=self._callback_response_handler, + hooks={'response': response_task_result_handler} headers=headers, - verify=verify_ssl, + verify=verify_ssl ) except Exception as err: logger.error(str(err)) - @staticmethod - def _callback_response_handler(sess, resp): - if resp.ok: - logger.info("POSTed to {}".format(resp.url)) - else: - msg = "Failed to POST to {}: {}" - logger.warning(msg.format(resp.url, resp.reason)) def _queue_pending_tasks(self, schedule_snapshot): pending_queue = TaskQueue() From c46cfb34d146f451a48ab477f18b1ff421a657e7 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Mon, 28 Feb 2022 14:50:03 -0700 Subject: [PATCH 02/15] syntax fix --- src/scheduler/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 13c84ccd..45604942 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -215,7 +215,7 @@ def response_task_result_handler(resp, *args, **kwargs): requests_futures_session.post( self.entry.callback_url, json=result_json, - hooks={'response': response_task_result_handler} + hooks={'response': response_task_result_handler}, headers=headers, verify=verify_ssl ) From 2df13b1ea520a2cb67428c6d0f0334dea33ef8fe Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Mon, 28 Feb 2022 15:11:32 -0700 Subject: [PATCH 03/15] Update status as notification_failed in case of exception. --- src/scheduler/scheduler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 45604942..ea0a044a 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -221,6 +221,8 @@ def response_task_result_handler(resp, *args, **kwargs): ) except Exception as err: logger.error(str(err)) + tr.status = 'notification_failed' + tr.save() def _queue_pending_tasks(self, schedule_snapshot): From 6444b3c862da906ae42a0c42055d1e77cae6ce00 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 1 Mar 2022 07:45:04 -0700 Subject: [PATCH 04/15] Additional checks in notification status. --- src/scheduler/scheduler.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index ea0a044a..b1c1a6ff 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -141,7 +141,6 @@ def _call_task_action(self): entry_name = self.task.schedule_entry_name task_id = self.task.task_id from schedule.serializers import ScheduleEntrySerializer - from tasks.serializers import TaskResultSerializer schedule_entry = ScheduleEntry.objects.get(name=entry_name) @@ -170,8 +169,6 @@ def _call_task_action(self): def _finalize_task_result(self, started, finished, status, detail): - - tr = self.task_result tr.started = started tr.finished = finished @@ -180,18 +177,28 @@ def _finalize_task_result(self, started, finished, status, detail): tr.detail = detail def response_task_result_handler(resp, *args, **kwargs): - if resp.ok: - logger.info("POSTed to {}".format(resp.url)) - else: - msg = "Failed to POST to {}: {}" - logger.warning(msg.format(resp.url, resp.reason)) + try: + if resp: + logger.info("Callback Handler: POSTed to {}".format(resp.url)) + if resp.ok: + logger.info("POSTed to {}".format(resp.url)) + else: + msg = "Failed to POST to {}: {}" + logger.warning(msg.format(resp.url, resp.reason)) + tr.status = 'notification_failed' + else: + msg = "Failed to POST to {}: {}" + logger.warning(msg.format(resp.url, resp.reason)) + tr.status = 'notification_failed' + tr.save() + except Exception as err: + logger.error(str(err)) tr.status = 'notification_failed' - tr.save() - + tr.save() if self.entry.callback_url: try: - logger.debug("Trying callback to URL: " + self.entry.callback_url) + logger.info("Trying callback to URL: " + self.entry.callback_url) context = {"request": self.entry.request} result_json = TaskResultSerializer(tr, context=context).data verify_ssl = settings.CALLBACK_SSL_VERIFICATION @@ -206,7 +213,7 @@ def response_task_result_handler(resp, *args, **kwargs): self.entry.callback_url, data=json.dumps(result_json), headers=headers, - verify=verify_ssl, + verify=verify_ssl ) response_task_result_handler(response) else: @@ -224,7 +231,6 @@ def response_task_result_handler(resp, *args, **kwargs): tr.status = 'notification_failed' tr.save() - def _queue_pending_tasks(self, schedule_snapshot): pending_queue = TaskQueue() for entry in schedule_snapshot: From 7b5bbfe3352541dc9aec692e112d69893d0f9c82 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 1 Mar 2022 08:38:17 -0700 Subject: [PATCH 05/15] debugging --- src/scheduler/scheduler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index b1c1a6ff..d7f9ee31 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -217,6 +217,7 @@ def response_task_result_handler(resp, *args, **kwargs): ) response_task_result_handler(response) else: + logger.info('Posting with token') token = self.entry.owner.auth_token headers = {"Authorization": "Token " + str(token)} requests_futures_session.post( @@ -226,7 +227,9 @@ def response_task_result_handler(resp, *args, **kwargs): headers=headers, verify=verify_ssl ) + logger.info('posted') except Exception as err: + logger.error("Error") logger.error(str(err)) tr.status = 'notification_failed' tr.save() From 61181e31b5486d3c96c00d39635868e01037e3ff Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 1 Mar 2022 09:38:37 -0700 Subject: [PATCH 06/15] Replaced FuturesSession with requests to post notification to callback because FuturesSession won't error on bad url if the result isn't requested. --- src/scheduler/scheduler.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index d7f9ee31..22894ba1 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -5,10 +5,8 @@ import threading from contextlib import contextmanager from pathlib import Path - +import requests from django.utils import timezone -from requests_futures.sessions import FuturesSession - from authentication import oauth from capabilities import get_capabilities from schedule.models import ScheduleEntry @@ -176,26 +174,6 @@ def _finalize_task_result(self, started, finished, status, detail): tr.status = status tr.detail = detail - def response_task_result_handler(resp, *args, **kwargs): - try: - if resp: - logger.info("Callback Handler: POSTed to {}".format(resp.url)) - if resp.ok: - logger.info("POSTed to {}".format(resp.url)) - else: - msg = "Failed to POST to {}: {}" - logger.warning(msg.format(resp.url, resp.reason)) - tr.status = 'notification_failed' - else: - msg = "Failed to POST to {}: {}" - logger.warning(msg.format(resp.url, resp.reason)) - tr.status = 'notification_failed' - tr.save() - except Exception as err: - logger.error(str(err)) - tr.status = 'notification_failed' - tr.save() - if self.entry.callback_url: try: logger.info("Trying callback to URL: " + self.entry.callback_url) @@ -215,25 +193,33 @@ def response_task_result_handler(resp, *args, **kwargs): headers=headers, verify=verify_ssl ) - response_task_result_handler(response) + self._callback_response_handler(response, tr) else: logger.info('Posting with token') token = self.entry.owner.auth_token headers = {"Authorization": "Token " + str(token)} - requests_futures_session.post( + response = requests.post( self.entry.callback_url, json=result_json, - hooks={'response': response_task_result_handler}, headers=headers, verify=verify_ssl ) logger.info('posted') + self._callback_response_handler(response, tr) except Exception as err: logger.error("Error") logger.error(str(err)) tr.status = 'notification_failed' tr.save() + @staticmethod + def _callback_response_handler(resp, task_result): + if resp.ok: + logger.info("POSTed to {}".format(resp.url)) + else: + msg = "Failed to POST to {}: {}" + logger.warning(msg.format(resp.url, resp.reason)) + def _queue_pending_tasks(self, schedule_snapshot): pending_queue = TaskQueue() for entry in schedule_snapshot: From c50d09d56ba55fae056bbcc143ea3a0ea136f008 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 1 Mar 2022 10:12:34 -0700 Subject: [PATCH 07/15] removed unused FuturesSession --- src/scheduler/scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 22894ba1..31e2a943 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -19,7 +19,7 @@ from . import utils logger = logging.getLogger(__name__) -requests_futures_session = FuturesSession() + class Scheduler(threading.Thread): From 4311ba66feea93132ac6cfb38308ac040daec8bc Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Tue, 1 Mar 2022 11:42:01 -0700 Subject: [PATCH 08/15] Make task_result save more efficient and increase length of status field to 19. --- src/scheduler/scheduler.py | 1 - src/tasks/models/task_result.py | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 31e2a943..ae019a17 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -207,7 +207,6 @@ def _finalize_task_result(self, started, finished, status, detail): logger.info('posted') self._callback_response_handler(response, tr) except Exception as err: - logger.error("Error") logger.error(str(err)) tr.status = 'notification_failed' tr.save() diff --git a/src/tasks/models/task_result.py b/src/tasks/models/task_result.py index e9ed49e2..7c45fe04 100644 --- a/src/tasks/models/task_result.py +++ b/src/tasks/models/task_result.py @@ -4,7 +4,6 @@ from django.db import models from django.utils import timezone - from schedule.models import ScheduleEntry from sensor.settings import MAX_DISK_USAGE from tasks.consts import MAX_DETAIL_LEN @@ -44,8 +43,8 @@ class TaskResult(models.Model): ) status = models.CharField( default="in-progress", - max_length=11, - help_text='"success" or "failure"', + max_length=19, + help_text='"success", "failure", or "notification_failed"', choices=RESULT_CHOICES, ) detail = models.CharField( @@ -64,9 +63,9 @@ def __init__(self, *args, **kwargs): def save(self): """Limit disk usage to MAX_DISK_USAGE by removing oldest result.""" - all_results = TaskResult.objects.all().order_by("id") filter = {"schedule_entry__name": self.schedule_entry.name} - same_entry_results = all_results.filter(**filter) + + same_entry_results = TaskResult.objects.filter(**filter).order_by("id") if ( same_entry_results.count() > 0 and same_entry_results[0].id != self.id ): # prevent from deleting this task result's acquisition From 9b6c1445fd6a59dcfd3abd188b59bcd098b1fd87 Mon Sep 17 00:00:00 2001 From: dboulware Date: Wed, 2 Mar 2022 10:46:08 -0700 Subject: [PATCH 09/15] Fixed bug in capabilities init when sensor def doesn't have location and dded migrations and updated requirements. Signed-off-by: dboulware --- docs/openapi.json | 2 +- src/capabilities/__init__.py | 3 +- src/requirements-dev.txt | 42 ++++--------------- src/requirements.in | 1 - src/requirements.txt | 23 +++------- .../migrations/0002_auto_20220302_1504.py | 18 ++++++++ .../migrations/0003_auto_20220302_1504.py | 18 ++++++++ 7 files changed, 53 insertions(+), 54 deletions(-) create mode 100644 src/schedule/migrations/0002_auto_20220302_1504.py create mode 100644 src/tasks/migrations/0003_auto_20220302_1504.py diff --git a/docs/openapi.json b/docs/openapi.json index 3e3d7cf0..39bd9004 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1830,7 +1830,7 @@ }, "status": { "title": "Status", - "description": "\"success\" or \"failure\"", + "description": "\"success\", \"failure\", or \"notification_failed\"", "type": "string", "enum": [ 1, diff --git a/src/capabilities/__init__.py b/src/capabilities/__init__.py index 9c2f4806..5197dd4e 100644 --- a/src/capabilities/__init__.py +++ b/src/capabilities/__init__.py @@ -43,5 +43,6 @@ def get_capabilities(): if location: capabilities["sensor"]["location"] = location else: - del capabilities["sensor"]["location"] + if "location" in capabilities['sensor']: + del capabilities["sensor"]["location"] return capabilities diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index b7eefb8a..a01081b5 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -8,7 +8,7 @@ asgiref==3.5.0 # via # -r requirements.txt # django -aspy.refactor-imports==2.2.1 +aspy-refactor-imports==2.2.1 # via seed-isort-config attrs==21.4.0 # via @@ -23,10 +23,7 @@ bcrypt==3.2.0 black==21.12b0 # via -r requirements-dev.in cached-property==1.5.2 - # via - # -r requirements.txt - # aspy.refactor-imports - # docker-compose + # via aspy-refactor-imports certifi==2021.10.8 # via # -r requirements.txt @@ -130,16 +127,6 @@ idna==3.3 # via # -r requirements.txt # requests -importlib-metadata==4.10.1 - # via - # -r requirements.txt - # click - # jsonschema - # pep517 - # pluggy - # pre-commit - # pytest - # virtualenv inflection==0.5.1 # via # -r requirements.txt @@ -276,27 +263,27 @@ requests==2.27.1 # requests-futures # requests-mock # requests-oauthlib -requests-futures==0.9.9 +requests-futures==1.0.0 # via -r requirements.txt requests-mock==1.6.0 # via -r requirements.txt requests-oauthlib==1.3.1 # via -r requirements.txt -ruamel.yaml==0.16.13 +ruamel-yaml==0.16.13 # via # -r requirements.txt # drf-yasg -ruamel.yaml.clib==0.2.6 +ruamel-yaml-clib==0.2.6 # via # -r requirements.txt - # ruamel.yaml + # ruamel-yaml scipy==1.7.3 # via -r requirements.txt -scos-actions @ git+https://github.com/NTIA/scos-actions@bug_fix +scos_actions @ git+https://github.com/NTIA/scos-actions@bug_fix # via # -r requirements.txt # scos-usrp -scos-usrp @ git+https://github.com/NTIA/scos-usrp@fix_dependencies +scos_usrp @ git+https://github.com/NTIA/scos-usrp@fix_dependencies # via -r requirements.txt seed-isort-config==1.9.1 # via -r requirements-dev.in @@ -335,14 +322,8 @@ tomli==1.2.3 # pep517 tox==3.12.1 # via -r requirements-dev.in -typed-ast==1.5.2 - # via black typing-extensions==4.0.1 - # via - # -r requirements.txt - # asgiref - # black - # importlib-metadata + # via black uritemplate==4.1.1 # via # -r requirements.txt @@ -363,11 +344,6 @@ websocket-client==0.59.0 # docker-compose wheel==0.37.1 # via pip-tools -zipp==3.7.0 - # via - # -r requirements.txt - # importlib-metadata - # pep517 # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/src/requirements.in b/src/requirements.in index 73382008..53c40b6b 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -14,7 +14,6 @@ oauthlib~=3.1.0 psycopg2-binary==2.8.5 pyjwt~=1.7.1 raven==6.10.0 -requests-futures==1.0.0 requests-mock==1.6.0 requests_oauthlib~=1.3.0 #scos_actions @ git+https://github.com/NTIA/scos-actions@bug_fix#egg=scos_actions diff --git a/src/requirements.txt b/src/requirements.txt index ea93af3b..fe2c9727 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -10,8 +10,6 @@ attrs==21.4.0 # via jsonschema bcrypt==3.2.0 # via paramiko -cached-property==1.5.2 - # via docker-compose certifi==2021.10.8 # via requests cffi==1.15.0 @@ -69,8 +67,6 @@ gunicorn==19.9.0 # via -r requirements.in idna==3.3 # via requests -importlib-metadata==4.10.1 - # via jsonschema inflection==0.5.1 # via drf-yasg itypes==1.2.0 @@ -129,28 +125,25 @@ requests==2.27.1 # coreapi # docker # docker-compose - # requests-futures # requests-mock # requests-oauthlib -requests-futures==0.9.9 - # via -r requirements.in requests-mock==1.6.0 # via -r requirements.in requests-oauthlib==1.3.1 # via -r requirements.in -ruamel.yaml==0.16.13 +ruamel-yaml==0.16.13 # via # drf-yasg # scos-actions -ruamel.yaml.clib==0.2.6 +ruamel-yaml-clib==0.2.6 # via - # ruamel.yaml + # ruamel-yaml # scos-actions scipy==1.7.3 # via scos-actions -scos-actions @ git+https://github.com/NTIA/scos-actions@bug_fix +scos_actions @ git+https://github.com/NTIA/scos-actions@bug_fix # via scos-usrp -scos-usrp @ git+https://github.com/NTIA/scos-usrp@fix_dependencies +scos_usrp @ git+https://github.com/NTIA/scos-usrp@fix_dependencies # via -r requirements.in sigmf @ git+https://github.com/NTIA/SigMF.git@multi-recording-archive # via scos-actions @@ -171,10 +164,6 @@ sqlparse==0.4.2 # django-debug-toolbar texttable==1.6.4 # via docker-compose -typing-extensions==4.0.1 - # via - # asgiref - # importlib-metadata uritemplate==4.1.1 # via # coreapi @@ -185,8 +174,6 @@ websocket-client==0.59.0 # via # docker # docker-compose -zipp==3.7.0 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/src/schedule/migrations/0002_auto_20220302_1504.py b/src/schedule/migrations/0002_auto_20220302_1504.py new file mode 100644 index 00000000..c5098a40 --- /dev/null +++ b/src/schedule/migrations/0002_auto_20220302_1504.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2022-03-02 15:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('schedule', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='scheduleentry', + name='action', + field=models.CharField(choices=[('acquire_iq_700MHz_ATT_DL', 'acquire_iq_700MHz_ATT_DL - Capture time-domain IQ samples at 739.00 MHz.'), ('acquire_iq_700MHz_ATT_UL', 'acquire_iq_700MHz_ATT_UL - Capture time-domain IQ samples at 709.00 MHz.'), ('acquire_iq_700MHz_FirstNet_DL', 'acquire_iq_700MHz_FirstNet_DL - Capture time-domain IQ samples at 763.00 MHz.'), ('acquire_iq_700MHz_FirstNet_UL', 'acquire_iq_700MHz_FirstNet_UL - Capture time-domain IQ samples at 793.00 MHz.'), ('acquire_iq_700MHz_P-SafetyNB_DL', 'acquire_iq_700MHz_P-SafetyNB_DL - Capture time-domain IQ samples at 772.00 MHz.'), ('acquire_iq_700MHz_P-SafetyNB_UL', 'acquire_iq_700MHz_P-SafetyNB_UL - Capture time-domain IQ samples at 802.00 MHz.'), ('acquire_iq_700MHz_T-Mobile_DL', 'acquire_iq_700MHz_T-Mobile_DL - Capture time-domain IQ samples at 731.50 MHz.'), ('acquire_iq_700MHz_T-Mobile_UL', 'acquire_iq_700MHz_T-Mobile_UL - Capture time-domain IQ samples at 700.50 MHz.'), ('acquire_iq_700MHz_Verizon_DL', 'acquire_iq_700MHz_Verizon_DL - Capture time-domain IQ samples at 751.00 MHz.'), ('acquire_iq_700MHz_Verizon_UL', 'acquire_iq_700MHz_Verizon_UL - Capture time-domain IQ samples at 782.00 MHz.'), ('acquire_m4s_700MHz_ATT_DL', 'acquire_m4s_700MHz_ATT_DL - Apply m4s detector over 300 1024-pt FFTs at 739.00 MHz.'), ('acquire_m4s_700MHz_ATT_UL', 'acquire_m4s_700MHz_ATT_UL - Apply m4s detector over 300 1024-pt FFTs at 709.00 MHz.'), ('acquire_m4s_700MHz_FirstNet_DL', 'acquire_m4s_700MHz_FirstNet_DL - Apply m4s detector over 300 1024-pt FFTs at 763.00 MHz.'), ('acquire_m4s_700MHz_FirstNet_UL', 'acquire_m4s_700MHz_FirstNet_UL - Apply m4s detector over 300 1024-pt FFTs at 793.00 MHz.'), ('acquire_m4s_700MHz_P-SafetyNB_DL', 'acquire_m4s_700MHz_P-SafetyNB_DL - Apply m4s detector over 300 1024-pt FFTs at 772.00 MHz.'), ('acquire_m4s_700MHz_P-SafetyNB_UL', 'acquire_m4s_700MHz_P-SafetyNB_UL - Apply m4s detector over 300 1024-pt FFTs at 802.00 MHz.'), ('acquire_m4s_700MHz_T-Mobile_DL', 'acquire_m4s_700MHz_T-Mobile_DL - Apply m4s detector over 300 1024-pt FFTs at 731.50 MHz.'), ('acquire_m4s_700MHz_T-Mobile_UL', 'acquire_m4s_700MHz_T-Mobile_UL - Apply m4s detector over 300 1024-pt FFTs at 700.50 MHz.'), ('acquire_m4s_700MHz_Verizon_DL', 'acquire_m4s_700MHz_Verizon_DL - Apply m4s detector over 300 1024-pt FFTs at 751.00 MHz.'), ('acquire_m4s_700MHz_Verizon_UL', 'acquire_m4s_700MHz_Verizon_UL - Apply m4s detector over 300 1024-pt FFTs at 782.00 MHz.'), ('logger', 'logger - Log the message "running test {name}/{tid}".'), ('monitor_usrp', 'monitor_usrp - Monitor signal analyzer connection and restart container if unreachable.'), ('survey_700MHz_band_10dB_1000ms_iq', 'survey_700MHz_band_10dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_10dB_80ms_iq', 'survey_700MHz_band_10dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_20dB_1000ms_iq', 'survey_700MHz_band_20dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_20dB_80ms_iq', 'survey_700MHz_band_20dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_40dB_1000ms_iq', 'survey_700MHz_band_40dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_40dB_80ms_iq', 'survey_700MHz_band_40dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_iq', 'survey_700MHz_band_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('sync_gps', 'sync_gps - Query the GPS and synchronize time and location.')], help_text='[Required] The name of the action to be scheduled', max_length=50), + ), + ] diff --git a/src/tasks/migrations/0003_auto_20220302_1504.py b/src/tasks/migrations/0003_auto_20220302_1504.py new file mode 100644 index 00000000..70517c6a --- /dev/null +++ b/src/tasks/migrations/0003_auto_20220302_1504.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2022-03-02 15:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_auto_20200319_1420'), + ] + + operations = [ + migrations.AlterField( + model_name='taskresult', + name='status', + field=models.CharField(choices=[(1, 'success'), (2, 'failure'), (3, 'in-progress')], default='in-progress', help_text='"success", "failure", or "notification_failed"', max_length=19), + ), + ] From f082482d2e800f6b0caecefc3c571384fc3eae03 Mon Sep 17 00:00:00 2001 From: dboulware Date: Wed, 2 Mar 2022 11:13:41 -0700 Subject: [PATCH 10/15] updated requirements Signed-off-by: dboulware --- src/requirements-dev.txt | 31 ++++++++++++++++++++++++++----- src/requirements.txt | 10 ++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index a01081b5..a29b909d 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -23,7 +23,10 @@ bcrypt==3.2.0 black==21.12b0 # via -r requirements-dev.in cached-property==1.5.2 - # via aspy-refactor-imports + # via + # -r requirements.txt + # aspy-refactor-imports + # docker-compose certifi==2021.10.8 # via # -r requirements.txt @@ -127,6 +130,16 @@ idna==3.3 # via # -r requirements.txt # requests +importlib-metadata==4.11.2 + # via + # -r requirements.txt + # click + # jsonschema + # pep517 + # pluggy + # pre-commit + # pytest + # virtualenv inflection==0.5.1 # via # -r requirements.txt @@ -260,11 +273,8 @@ requests==2.27.1 # coreapi # docker # docker-compose - # requests-futures # requests-mock # requests-oauthlib -requests-futures==1.0.0 - # via -r requirements.txt requests-mock==1.6.0 # via -r requirements.txt requests-oauthlib==1.3.1 @@ -322,8 +332,14 @@ tomli==1.2.3 # pep517 tox==3.12.1 # via -r requirements-dev.in -typing-extensions==4.0.1 +typed-ast==1.5.2 # via black +typing-extensions==4.1.1 + # via + # -r requirements.txt + # asgiref + # black + # importlib-metadata uritemplate==4.1.1 # via # -r requirements.txt @@ -344,6 +360,11 @@ websocket-client==0.59.0 # docker-compose wheel==0.37.1 # via pip-tools +zipp==3.7.0 + # via + # -r requirements.txt + # importlib-metadata + # pep517 # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/src/requirements.txt b/src/requirements.txt index fe2c9727..80a48767 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -10,6 +10,8 @@ attrs==21.4.0 # via jsonschema bcrypt==3.2.0 # via paramiko +cached-property==1.5.2 + # via docker-compose certifi==2021.10.8 # via requests cffi==1.15.0 @@ -67,6 +69,8 @@ gunicorn==19.9.0 # via -r requirements.in idna==3.3 # via requests +importlib-metadata==4.11.2 + # via jsonschema inflection==0.5.1 # via drf-yasg itypes==1.2.0 @@ -164,6 +168,10 @@ sqlparse==0.4.2 # django-debug-toolbar texttable==1.6.4 # via docker-compose +typing-extensions==4.1.1 + # via + # asgiref + # importlib-metadata uritemplate==4.1.1 # via # coreapi @@ -174,6 +182,8 @@ websocket-client==0.59.0 # via # docker # docker-compose +zipp==3.7.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools From b60839b0624d3a4bb5cd4e314e7c6f9ddb4b9f3d Mon Sep 17 00:00:00 2001 From: dboulware Date: Wed, 2 Mar 2022 12:14:34 -0700 Subject: [PATCH 11/15] Added back updating of status and saving of task result in callback. Signed-off-by: dboulware --- src/scheduler/scheduler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index ae019a17..39a83da2 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -218,6 +218,11 @@ def _callback_response_handler(resp, task_result): else: msg = "Failed to POST to {}: {}" logger.warning(msg.format(resp.url, resp.reason)) + task_result.status = 'notification_failed' + + task_result.save() + + def _queue_pending_tasks(self, schedule_snapshot): pending_queue = TaskQueue() From a5f46ffb71454a992b6074d41b355a2b89024051 Mon Sep 17 00:00:00 2001 From: dboulware Date: Wed, 2 Mar 2022 14:37:29 -0700 Subject: [PATCH 12/15] Added test to ensure notification_failed status is set if there is an error posting to callback url. Signed-off-by: dboulware --- src/scheduler/tests/test_scheduler.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/scheduler/tests/test_scheduler.py b/src/scheduler/tests/test_scheduler.py index d312a0e5..06cc1377 100644 --- a/src/scheduler/tests/test_scheduler.py +++ b/src/scheduler/tests/test_scheduler.py @@ -7,6 +7,7 @@ from django import conf from scheduler.scheduler import Scheduler, minimum_duration +from tasks.models import TaskResult from .utils import ( BAD_ACTION_STR, @@ -427,6 +428,17 @@ def cb_request_handler(sess, resp): assert action_flag.is_set() verify_request(request_history) +@pytest.mark.django_db +def test_notification_failed_status(test_scheduler): + entry = create_entry("t", 1, 1, 100, 5, "logger", 'https://badmgr.its.bldrdoc.gov') + entry.save() + entry.refresh_from_db() + print('entry = ' + entry.name) + s = test_scheduler + advance_testclock(s.timefn, 1) + s.run(blocking=False) # queue first 10 tasks + result = TaskResult.objects.first() + assert result.status == 'notification_failed' @pytest.mark.django_db def test_starvation(test_scheduler): @@ -469,3 +481,4 @@ def test_task_pushed_past_stop_still_runs(test_scheduler): def test_str(): str(Scheduler()) + From d5f4ab83777e36f8ce1f9658a98d181e541e2c0e Mon Sep 17 00:00:00 2001 From: dboulware Date: Wed, 2 Mar 2022 15:52:36 -0700 Subject: [PATCH 13/15] Updated db model and migration for noticiation_failed added tests for notification_failed. Signed-off-by: dboulware --- docs/openapi.json | 3 +- src/scheduler/tests/test_scheduler.py | 30 +++++++++++++++---- .../migrations/0004_auto_20220302_2250.py | 18 +++++++++++ src/tasks/models/task_result.py | 2 ++ 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/tasks/migrations/0004_auto_20220302_2250.py diff --git a/docs/openapi.json b/docs/openapi.json index 39bd9004..8dd84829 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1835,7 +1835,8 @@ "enum": [ 1, 2, - 3 + 3, + 4 ] }, "detail": { diff --git a/src/scheduler/tests/test_scheduler.py b/src/scheduler/tests/test_scheduler.py index 06cc1377..0b13ff79 100644 --- a/src/scheduler/tests/test_scheduler.py +++ b/src/scheduler/tests/test_scheduler.py @@ -4,6 +4,7 @@ import pytest import requests_mock +import requests from django import conf from scheduler.scheduler import Scheduler, minimum_duration @@ -320,16 +321,16 @@ def verify_request(request_history, status="success", detail=None): oauth_history = request_history[0] assert oauth_history.verify == conf.settings.PATH_TO_VERIFY_CERT assert ( - oauth_history.text - == f"grant_type=password&username={conf.settings.USER_NAME}&password={conf.settings.PASSWORD}" + oauth_history.text + == f"grant_type=password&username={conf.settings.USER_NAME}&password={conf.settings.PASSWORD}" ) assert oauth_history.cert == conf.settings.PATH_TO_CLIENT_CERT auth_header = oauth_history.headers.get("Authorization") auth_header = auth_header.replace("Basic ", "") auth_header_decoded = base64.b64decode(auth_header).decode("utf-8") assert ( - auth_header_decoded - == f"{conf.settings.CLIENT_ID}:{conf.settings.CLIENT_SECRET}" + auth_header_decoded + == f"{conf.settings.CLIENT_ID}:{conf.settings.CLIENT_SECRET}" ) request_json = request_history[1].json() else: @@ -428,8 +429,9 @@ def cb_request_handler(sess, resp): assert action_flag.is_set() verify_request(request_history) + @pytest.mark.django_db -def test_notification_failed_status(test_scheduler): +def test_notification_failed_status_unknown_host(test_scheduler): entry = create_entry("t", 1, 1, 100, 5, "logger", 'https://badmgr.its.bldrdoc.gov') entry.save() entry.refresh_from_db() @@ -440,6 +442,23 @@ def test_notification_failed_status(test_scheduler): result = TaskResult.objects.first() assert result.status == 'notification_failed' + +@pytest.mark.django_db +def test_notification_failed_status_request_ok_false(test_scheduler): + entry = create_entry("t", 1, 1, 100, 5, "logger") + entry.save() + entry.refresh_from_db() + print('entry = ' + entry.name) + s = test_scheduler + advance_testclock(s.timefn, 1) + s.run(blocking=False) # queue first 10 tasks + tr = TaskResult.objects.first() + response = requests.Response() + response.status_code = 400 + s._callback_response_handler(response, tr) + assert tr.status == 'notification_failed' + + @pytest.mark.django_db def test_starvation(test_scheduler): """A recurring high-pri task should not 'starve' a low-pri task.""" @@ -481,4 +500,3 @@ def test_task_pushed_past_stop_still_runs(test_scheduler): def test_str(): str(Scheduler()) - diff --git a/src/tasks/migrations/0004_auto_20220302_2250.py b/src/tasks/migrations/0004_auto_20220302_2250.py new file mode 100644 index 00000000..66adee8f --- /dev/null +++ b/src/tasks/migrations/0004_auto_20220302_2250.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2022-03-02 22:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_auto_20220302_1504'), + ] + + operations = [ + migrations.AlterField( + model_name='taskresult', + name='status', + field=models.CharField(choices=[(1, 'success'), (2, 'failure'), (3, 'in-progress'), (4, 'notification_failed')], default='in-progress', help_text='"success", "failure", or "notification_failed"', max_length=19), + ), + ] diff --git a/src/tasks/models/task_result.py b/src/tasks/models/task_result.py index 7c45fe04..e543ab88 100644 --- a/src/tasks/models/task_result.py +++ b/src/tasks/models/task_result.py @@ -17,10 +17,12 @@ class TaskResult(models.Model): SUCCESS = 1 FAILURE = 2 IN_PROGRESS = 3 + NOTIFICATION_FAILED = 4 RESULT_CHOICES = ( (SUCCESS, "success"), (FAILURE, "failure"), (IN_PROGRESS, "in-progress"), + (NOTIFICATION_FAILED, "notification_failed") ) schedule_entry = models.ForeignKey( From 259ad4a6a222d24cbc77d2956eaac596c1226804 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Thu, 3 Mar 2022 14:58:37 -0700 Subject: [PATCH 14/15] Removed choices from action in schedule_entry model and removed migration to apply them. Added task_result.save back into scheduler to save task result when there is no callback. --- .../migrations/0002_auto_20220302_1504.py | 18 ------------------ src/schedule/models/schedule_entry.py | 1 - src/scheduler/scheduler.py | 2 ++ 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 src/schedule/migrations/0002_auto_20220302_1504.py diff --git a/src/schedule/migrations/0002_auto_20220302_1504.py b/src/schedule/migrations/0002_auto_20220302_1504.py deleted file mode 100644 index c5098a40..00000000 --- a/src/schedule/migrations/0002_auto_20220302_1504.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1.14 on 2022-03-02 15:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('schedule', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='scheduleentry', - name='action', - field=models.CharField(choices=[('acquire_iq_700MHz_ATT_DL', 'acquire_iq_700MHz_ATT_DL - Capture time-domain IQ samples at 739.00 MHz.'), ('acquire_iq_700MHz_ATT_UL', 'acquire_iq_700MHz_ATT_UL - Capture time-domain IQ samples at 709.00 MHz.'), ('acquire_iq_700MHz_FirstNet_DL', 'acquire_iq_700MHz_FirstNet_DL - Capture time-domain IQ samples at 763.00 MHz.'), ('acquire_iq_700MHz_FirstNet_UL', 'acquire_iq_700MHz_FirstNet_UL - Capture time-domain IQ samples at 793.00 MHz.'), ('acquire_iq_700MHz_P-SafetyNB_DL', 'acquire_iq_700MHz_P-SafetyNB_DL - Capture time-domain IQ samples at 772.00 MHz.'), ('acquire_iq_700MHz_P-SafetyNB_UL', 'acquire_iq_700MHz_P-SafetyNB_UL - Capture time-domain IQ samples at 802.00 MHz.'), ('acquire_iq_700MHz_T-Mobile_DL', 'acquire_iq_700MHz_T-Mobile_DL - Capture time-domain IQ samples at 731.50 MHz.'), ('acquire_iq_700MHz_T-Mobile_UL', 'acquire_iq_700MHz_T-Mobile_UL - Capture time-domain IQ samples at 700.50 MHz.'), ('acquire_iq_700MHz_Verizon_DL', 'acquire_iq_700MHz_Verizon_DL - Capture time-domain IQ samples at 751.00 MHz.'), ('acquire_iq_700MHz_Verizon_UL', 'acquire_iq_700MHz_Verizon_UL - Capture time-domain IQ samples at 782.00 MHz.'), ('acquire_m4s_700MHz_ATT_DL', 'acquire_m4s_700MHz_ATT_DL - Apply m4s detector over 300 1024-pt FFTs at 739.00 MHz.'), ('acquire_m4s_700MHz_ATT_UL', 'acquire_m4s_700MHz_ATT_UL - Apply m4s detector over 300 1024-pt FFTs at 709.00 MHz.'), ('acquire_m4s_700MHz_FirstNet_DL', 'acquire_m4s_700MHz_FirstNet_DL - Apply m4s detector over 300 1024-pt FFTs at 763.00 MHz.'), ('acquire_m4s_700MHz_FirstNet_UL', 'acquire_m4s_700MHz_FirstNet_UL - Apply m4s detector over 300 1024-pt FFTs at 793.00 MHz.'), ('acquire_m4s_700MHz_P-SafetyNB_DL', 'acquire_m4s_700MHz_P-SafetyNB_DL - Apply m4s detector over 300 1024-pt FFTs at 772.00 MHz.'), ('acquire_m4s_700MHz_P-SafetyNB_UL', 'acquire_m4s_700MHz_P-SafetyNB_UL - Apply m4s detector over 300 1024-pt FFTs at 802.00 MHz.'), ('acquire_m4s_700MHz_T-Mobile_DL', 'acquire_m4s_700MHz_T-Mobile_DL - Apply m4s detector over 300 1024-pt FFTs at 731.50 MHz.'), ('acquire_m4s_700MHz_T-Mobile_UL', 'acquire_m4s_700MHz_T-Mobile_UL - Apply m4s detector over 300 1024-pt FFTs at 700.50 MHz.'), ('acquire_m4s_700MHz_Verizon_DL', 'acquire_m4s_700MHz_Verizon_DL - Apply m4s detector over 300 1024-pt FFTs at 751.00 MHz.'), ('acquire_m4s_700MHz_Verizon_UL', 'acquire_m4s_700MHz_Verizon_UL - Apply m4s detector over 300 1024-pt FFTs at 782.00 MHz.'), ('logger', 'logger - Log the message "running test {name}/{tid}".'), ('monitor_usrp', 'monitor_usrp - Monitor signal analyzer connection and restart container if unreachable.'), ('survey_700MHz_band_10dB_1000ms_iq', 'survey_700MHz_band_10dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_10dB_80ms_iq', 'survey_700MHz_band_10dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_20dB_1000ms_iq', 'survey_700MHz_band_20dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_20dB_80ms_iq', 'survey_700MHz_band_20dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_40dB_1000ms_iq', 'survey_700MHz_band_40dB_1000ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_40dB_80ms_iq', 'survey_700MHz_band_40dB_80ms_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('survey_700MHz_band_iq', 'survey_700MHz_band_iq - Capture time-domain IQ samples at the following 10 frequencies: 700.50 MHz, 709.00 MHz, 731.50 MHz, 739.00 MHz, 751.00 MHz, 763.00 MHz, 772.00 MHz, 782.00 MHz, 793.00 MHz, 802.00 MHz.'), ('sync_gps', 'sync_gps - Query the GPS and synchronize time and location.')], help_text='[Required] The name of the action to be scheduled', max_length=50), - ), - ] diff --git a/src/schedule/models/schedule_entry.py b/src/schedule/models/schedule_entry.py index 36dc96eb..8ad6a2d9 100644 --- a/src/schedule/models/schedule_entry.py +++ b/src/schedule/models/schedule_entry.py @@ -69,7 +69,6 @@ class ScheduleEntry(models.Model): help_text="[Required] The unique identifier used in URLs and filenames", ) action = models.CharField( - choices=actions.CHOICES, max_length=actions.MAX_LENGTH, help_text="[Required] The name of the action to be scheduled", ) diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 39a83da2..72431ff5 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -210,6 +210,8 @@ def _finalize_task_result(self, started, finished, status, detail): logger.error(str(err)) tr.status = 'notification_failed' tr.save() + else: + tr.save() @staticmethod def _callback_response_handler(resp, task_result): From e91acfc5fffd6bc4d4e681d3f377e0fb6d54cabf Mon Sep 17 00:00:00 2001 From: dboulware Date: Tue, 8 Mar 2022 11:47:20 -0700 Subject: [PATCH 15/15] Fixed bad action test. --- src/schedule/models/schedule_entry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/schedule/models/schedule_entry.py b/src/schedule/models/schedule_entry.py index 8ad6a2d9..f862d666 100644 --- a/src/schedule/models/schedule_entry.py +++ b/src/schedule/models/schedule_entry.py @@ -1,7 +1,7 @@ import sys from itertools import count -from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, ValidationError from django.db import models import actions @@ -157,9 +157,11 @@ def __init__(self, *args, **kwargs): if self.next_task_time is None: self.next_task_time = self.start - # used by .save to detect whether to reset .next_task_time + # used by .save to detect whether to reset .next_task_times self.__start = self.start self.__interval = self.interval + if self.action not in actions.registered_actions: + raise ValidationError(self.action + ' does not exist') def update(self, *args, **kwargs): super(ScheduleEntry, self).update(*args, **kwargs)