diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 5284783ea0c1845ae9a9feaf1d890c7d0797e1da..1b122a91b85da11e7c974ce4aeb2fa88fd86669b 100644 --- a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.9 on 2020-08-19 13:24 +# Generated by Django 3.0.8 on 2020-09-02 16:52 from django.conf import settings import django.contrib.postgres.fields @@ -288,6 +288,7 @@ class Migration(migrations.Migration): ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)), ('description', models.CharField(help_text='A longer description of this object.', max_length=255)), ('capacity', models.BigIntegerField(help_text='Capacity in bytes')), + ('directory', models.CharField(help_text='Root directory under which we are allowed to write our data.', max_length=1024)), ], options={ 'abstract': False, @@ -342,6 +343,7 @@ class Migration(migrations.Migration): ('private_data', models.BooleanField(default=True, help_text='True if data of this project is sensitive. Sensitive data is not made public.')), ('expert', models.BooleanField(default=False, help_text='Expert projects put more responsibility on the PI.')), ('filler', models.BooleanField(default=False, help_text='Use this project to fill up idle telescope time.')), + ('archive_subdirectory', models.CharField(help_text='Subdirectory in which this project will store its data in the LTA. The full directory is constructed by prefixing with archive_location→directory.', max_length=1024)), ], options={ 'abstract': False, @@ -1044,6 +1046,11 @@ class Migration(migrations.Migration): name='resource_type', field=models.ForeignKey(help_text='Resource type.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.ResourceType'), ), + migrations.AddField( + model_name='project', + name='archive_location', + field=models.ForeignKey(help_text='Ingest data to this LTA cluster only (NULLable). NULL means: no preference.', null=True, on_delete=django.db.models.deletion.PROTECT, to='tmssapp.Filesystem'), + ), migrations.AddField( model_name='project', name='cycles', diff --git a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py index dbbc9ba3248624ef7b310e56075583387a86f5aa..9981cc0c58324cd0e5def205ac3decf9715e9655 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py @@ -309,6 +309,13 @@ class DataproductTransform(BasicCommon): class Filesystem(NamedCommon): capacity = BigIntegerField(help_text='Capacity in bytes') cluster = ForeignKey('Cluster', on_delete=PROTECT, help_text='Cluster hosting this filesystem.') + directory = CharField(max_length=1024, help_text='Root directory under which we are allowed to write our data.') + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.directory and not self.directory.endswith('/'): + raise ValueError('directory value must end with a trailing slash!') # todo: ...and needs to start with slash? + + super().save(force_insert, force_update, using, update_fields) class Cluster(NamedCommon): diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index f292c06a9e03d7a0e9a3d9e44626715c30daa714..d1d4c025536364b66a070d0e9ee9ec9c11ad9f18 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -323,6 +323,16 @@ class Project(NamedCommonPK): filler = BooleanField(default=False, help_text='Use this project to fill up idle telescope time.') project_category = ForeignKey('ProjectCategory', null=True, on_delete=PROTECT, help_text='Project category.') period_category = ForeignKey('PeriodCategory', null=True, on_delete=PROTECT, help_text='Period category.') + archive_location = ForeignKey('Filesystem', null=True, on_delete=PROTECT, help_text='Ingest data to this LTA cluster only (NULLable). NULL means: no preference.') + archive_subdirectory = CharField(max_length=1024, help_text='Subdirectory in which this project will store its data in the LTA. The full directory is constructed by prefixing with archive_location→directory.') + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.archive_subdirectory and not self.archive_subdirectory.endswith('/'): + raise ValueError('directory value must end with a trailing slash!') + if self.archive_subdirectory and self.archive_subdirectory.startswith('/'): + raise ValueError('directory value must be a relative path (and not start with a slash)!') + + super().save(force_insert, force_update, using, update_fields) # JK, 29/07/20 - after discussion with Sander, it turns out that the ticket TMSS-277 was a misunderstanding. # 'default' does not refer to 'default values' that are supposed to be filled in by the backend. diff --git a/SAS/TMSS/test/t_tmssapp_scheduling_django_API.py b/SAS/TMSS/test/t_tmssapp_scheduling_django_API.py index 719013734259c39a65aa0f8afc9719d3ee25658a..e874abfa07ce6c9e4f0254517e605b0c5d531c90 100755 --- a/SAS/TMSS/test/t_tmssapp_scheduling_django_API.py +++ b/SAS/TMSS/test/t_tmssapp_scheduling_django_API.py @@ -376,6 +376,15 @@ class FilesystemTest(unittest.TestCase): self.assertLess(before, entry.updated_at) self.assertGreater(after, entry.updated_at) + def test_Filesystem_raises_ValueError_on_invalid_directory_name(self): + + # setup + test_data = Filesystem_test_data(directory="/no/trailing/slash") + + # assert + with self.assertRaises(ValueError): + entry = models.Filesystem.objects.create(**test_data) + class ClusterTest(unittest.TestCase): def test_Cluster_gets_created_with_correct_creation_timestamp(self): diff --git a/SAS/TMSS/test/t_tmssapp_specification_django_API.py b/SAS/TMSS/test/t_tmssapp_specification_django_API.py index e994df895e9f5167535d8981ce9ab552bc3cd69b..b2a6a26940dbc92980c3dee255527f1a0e28a837 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_django_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_django_API.py @@ -249,6 +249,19 @@ class ProjectTest(unittest.TestCase): self.assertLess(before, entry.updated_at) self.assertGreater(after, entry.updated_at) + def test_Project_raises_ValueError_on_invalid_archive_subdirectory_name(self): + + # setup + test_data_1 = Project_test_data(archive_subdirectory="no/trailing/slash") + test_data_2 = Project_test_data(archive_subdirectory="/with/leading/slash/") + + # assert + with self.assertRaises(ValueError): + entry = models.Project.objects.create(**test_data_1) + + with self.assertRaises(ValueError): + entry = models.Project.objects.create(**test_data_2) + class ProjectQuotaTest(unittest.TestCase): def test_ProjectQuota_prevents_missing_project(self): diff --git a/SAS/TMSS/test/tmss_test_data_django_models.py b/SAS/TMSS/test/tmss_test_data_django_models.py index 3242c7e89f2f990fd1162c002c6c8488ccce787d..f3c6d8664c49fb4b09e05324c36982cdd0d23d8e 100644 --- a/SAS/TMSS/test/tmss_test_data_django_models.py +++ b/SAS/TMSS/test/tmss_test_data_django_models.py @@ -123,7 +123,7 @@ def Cycle_test_data() -> dict: "start": datetime.utcnow().isoformat(), "stop": datetime.utcnow().isoformat()} -def Project_test_data() -> dict: +def Project_test_data(archive_subdirectory="my_project/") -> dict: return { #"cycles": [models.Cycle.objects.create(**Cycle_test_data())], # ManyToMany, use set() "name": 'my_project_' + str(uuid.uuid4()), "description": 'my description ' + str(uuid.uuid4()), @@ -133,7 +133,8 @@ def Project_test_data() -> dict: "can_trigger": False, "private_data": True, "expert": True, - "filler": False} + "filler": False, + "archive_subdirectory": archive_subdirectory} def ResourceType_test_data() -> dict: return { @@ -435,9 +436,10 @@ def DataproductTransform_test_data() -> dict: "identity": True, "tags": ['tmss', 'testing']} -def Filesystem_test_data() -> dict: +def Filesystem_test_data(directory="/") -> dict: return {"capacity": 1111111111, "cluster": models.Cluster.objects.create(**Cluster_test_data()), + "directory": directory, "tags": ['tmss', 'testing']} def Cluster_test_data(name="default cluster") -> dict: diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index eaa5801124ac19072048d948a5b898bd9f0b36ca..12efcc9bb893093bbdbb817ce69409d0033f8692 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -176,7 +176,8 @@ class TMSSRESTTestDataCreator(): "trigger_priority": 1000, "can_trigger": False, "private_data": True, - "cycles": []} + "cycles": [], + "archive_subdirectory": 'my_project/'} def ResourceType(self, description="my resource_type description"): return { @@ -458,7 +459,7 @@ class TMSSRESTTestDataCreator(): specifications_template_url = self.post_data_and_get_url(self.SubtaskTemplate(), '/subtask_template/') if specifications_doc is None: - specifications_doc = requests.get(specifications_template_url + 'default_specification/', auth=self.auth).content.decode('utf-8') + specifications_doc = requests.get(specifications_template_url + '/default_specification/', auth=self.auth).content.decode('utf-8') return {"start_time": datetime.utcnow().isoformat(), "stop_time": datetime.utcnow().isoformat(), @@ -583,5 +584,6 @@ class TMSSRESTTestDataCreator(): "description": 'My one filesystem', "capacity": 1111111111, "cluster": cluster_url, + "directory": '/', "tags": ['tmss', 'testing']}