diff --git a/arkindex/ponos/api.py b/arkindex/ponos/api.py index 91f6d607dac16a1e63429ccd1baee27a6c4a2317..7616e944decdb0f56e25ef1a5e5f537a005d23dd 100644 --- a/arkindex/ponos/api.py +++ b/arkindex/ponos/api.py @@ -12,7 +12,7 @@ from rest_framework.generics import CreateAPIView, ListCreateAPIView, RetrieveUp from rest_framework.response import Response from rest_framework.views import APIView -from arkindex.ponos.models import FINAL_STATES, Artifact, State, Task, task_token_default +from arkindex.ponos.models import FINAL_STATES, Artifact, State, Task, token_default from arkindex.ponos.permissions import ( IsAgentOrArtifactGuest, IsAgentOrTaskGuest, @@ -234,7 +234,7 @@ class TaskRestart(ProcessACLMixin, CreateAPIView): copy.id = uuid.uuid4() copy.slug = basename copy.state = State.Pending - copy.token = task_token_default() + copy.token = token_default() copy.agent_id = None copy.gpu_id = None copy.started = None diff --git a/arkindex/ponos/migrations/0001_initial.py b/arkindex/ponos/migrations/0001_initial.py index 7a72645956ffc490e65c3d2f7d6cfc2a68733cd6..0c9d9a237838fc0c46cff5eca9b76cf9f9c70231 100644 --- a/arkindex/ponos/migrations/0001_initial.py +++ b/arkindex/ponos/migrations/0001_initial.py @@ -114,7 +114,7 @@ class Migration(migrations.Migration): ("updated", models.DateTimeField(auto_now=True)), ("expiry", models.DateTimeField(default=arkindex.ponos.models.expiry_default)), ("extra_files", django.contrib.postgres.fields.hstore.HStoreField(default=dict, blank=True)), - ("token", models.CharField(default=arkindex.ponos.models.task_token_default, max_length=52, unique=True)), + ("token", models.CharField(default=arkindex.ponos.models.token_default, max_length=52, unique=True)), ("agent", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="tasks", to="ponos.agent")), ("gpu", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="tasks", to="ponos.gpu")), ("image_artifact", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="tasks_using_image", to="ponos.artifact")), diff --git a/arkindex/ponos/migrations/0004_index_cleanup.py b/arkindex/ponos/migrations/0004_index_cleanup.py index 6e25ec7c743706c0bf9aa8b530e3dfc9834fbf6c..939e2531189a281d2db82afd1cb4468d3c226bef 100644 --- a/arkindex/ponos/migrations/0004_index_cleanup.py +++ b/arkindex/ponos/migrations/0004_index_cleanup.py @@ -3,7 +3,7 @@ from django.core.validators import RegexValidator from django.db import migrations, models -from arkindex.ponos.models import generate_seed, task_token_default +from arkindex.ponos.models import generate_seed, token_default class Migration(migrations.Migration): @@ -89,7 +89,7 @@ class Migration(migrations.Migration): model_name="task", name="token", field=models.CharField( - default=task_token_default, + default=token_default, max_length=52, ), ), diff --git a/arkindex/ponos/migrations/0015_agent_token.py b/arkindex/ponos/migrations/0015_agent_token.py new file mode 100644 index 0000000000000000000000000000000000000000..b3f118b8f0fdb2ab6ab3ed8bfa197768dab6541a --- /dev/null +++ b/arkindex/ponos/migrations/0015_agent_token.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0.8 on 2025-02-17 13:50 + +from django.db import migrations, models + +from arkindex.ponos.models import token_default + + +def add_agent_tokens(apps, schema_editor): + Agent = apps.get_model("ponos", "Agent") + to_update = [] + for agent in Agent.objects.filter(token=None).only("id").iterator(): + agent.token = token_default() + to_update.append(agent) + Agent.objects.bulk_update(to_update, ["token"], batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ("ponos", "0014_task_task_finished_requires_final_state"), + ] + + operations = [ + migrations.AddField( + model_name="agent", + name="token", + field=models.CharField( + max_length=52, + # Make the field temporarily nullable and not unique, so that we can + # fill the tokens on existing agents before adding the constraints. + null=True, + ), + ), + migrations.RunPython( + add_agent_tokens, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/arkindex/ponos/migrations/0016_agent_token_constraints.py b/arkindex/ponos/migrations/0016_agent_token_constraints.py new file mode 100644 index 0000000000000000000000000000000000000000..c0ae4fd40253090576dfbb165f886627a411531b --- /dev/null +++ b/arkindex/ponos/migrations/0016_agent_token_constraints.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.8 on 2025-02-17 14:26 + +from django.db import migrations, models + +import arkindex.ponos.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("ponos", "0015_agent_token"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="token", + field=models.CharField(default=arkindex.ponos.models.token_default, max_length=52), + ), + migrations.AddConstraint( + model_name="agent", + constraint=models.UniqueConstraint(models.F("token"), name="unique_agent_token"), + ), + ] diff --git a/arkindex/ponos/models.py b/arkindex/ponos/models.py index e77e10df10c44acead36174613d930f84b16ae54..d2671227b8935d9bea9d4237d224771f80963e6d 100644 --- a/arkindex/ponos/models.py +++ b/arkindex/ponos/models.py @@ -34,6 +34,15 @@ def gen_nonce(size=16): return urandom(size) +def token_default(): + """ + Default value for task and agent tokens. + + :rtype: str + """ + return base64.encodebytes(uuid.uuid4().bytes + uuid.uuid4().bytes).strip().decode("utf-8") + + class Farm(models.Model): """ A group of agents, whose ID and seed can be used to register new agents @@ -95,12 +104,22 @@ class Agent(models.Model): ram_load = models.FloatField(null=True, blank=True) last_ping = models.DateTimeField(editable=False) + token = models.CharField( + default=token_default, + # The token generation always returns 52 characters + max_length=52, + ) + class Meta: constraints = [ models.CheckConstraint( check=Q(mode=AgentMode.Slurm) | Q(cpu_cores__isnull=False, cpu_frequency__isnull=False, ram_total__isnull=False), name="slurm_or_hardware_requirements", ), + models.UniqueConstraint( + "token", + name="unique_agent_token", + ), ] def __str__(self) -> str: @@ -224,15 +243,6 @@ def expiry_default(): return timezone.now() + timedelta(days=settings.PONOS_TASK_EXPIRY) -def task_token_default(): - """ - Default value for Task.token. - - :rtype: str - """ - return base64.encodebytes(uuid.uuid4().bytes + uuid.uuid4().bytes).strip().decode("utf-8") - - class TaskLogs(S3FileMixin): s3_bucket = settings.PONOS_S3_LOGS_BUCKET @@ -357,7 +367,7 @@ class Task(models.Model): extra_files = HStoreField(default=dict, blank=True) token = models.CharField( - default=task_token_default, + default=token_default, # The token generation always returns 52 characters max_length=52, ) diff --git a/arkindex/process/builder.py b/arkindex/process/builder.py index 6db8b7afc98a63efc858203f0611604ab3d51d97..9756ba6944a7b36fe0598d6d96626dd810890499 100644 --- a/arkindex/process/builder.py +++ b/arkindex/process/builder.py @@ -13,7 +13,7 @@ from django.utils.functional import cached_property from rest_framework.exceptions import ValidationError from arkindex.images.models import ImageServer -from arkindex.ponos.models import GPU, Task, task_token_default +from arkindex.ponos.models import GPU, Task, token_default class ProcessBuilder: @@ -79,7 +79,7 @@ class ProcessBuilder: Build a Task with default attributes and add it to the current stack. Depth is not set while building individual Task instances. """ - token = task_token_default() + token = token_default() env = { **self.base_env, diff --git a/arkindex/process/models.py b/arkindex/process/models.py index f9fa1efaa52ee049f8c82503e88e40d61f8fcf21..16e89ce50961e7f1c0e1b2c7b42d5ba1ff0e23b3 100644 --- a/arkindex/process/models.py +++ b/arkindex/process/models.py @@ -15,7 +15,7 @@ from enumfields import Enum, EnumField import pgtrigger from arkindex.documents.models import Classification, Element -from arkindex.ponos.models import FINAL_STATES, STATES_ORDERING, State, Task, task_token_default +from arkindex.ponos.models import FINAL_STATES, STATES_ORDERING, State, Task, token_default from arkindex.process.builder import ProcessBuilder from arkindex.process.managers import ( ActivityManager, @@ -1012,7 +1012,7 @@ class WorkerRun(models.Model): ) task_env = env.copy() - token = task_token_default() + token = token_default() task_env["ARKINDEX_TASK_TOKEN"] = token task_env["TASK_ELEMENTS"] = elements_path task_env["ARKINDEX_WORKER_RUN_ID"] = str(self.id) diff --git a/arkindex/process/tests/process/test_run.py b/arkindex/process/tests/process/test_run.py index 8ca2347b3e8be2a151c655d012d5ff9be4f55697..99d606d8642dce04d3a71372ced67ce9c8341828 100644 --- a/arkindex/process/tests/process/test_run.py +++ b/arkindex/process/tests/process/test_run.py @@ -25,7 +25,7 @@ class TestProcessRun(FixtureTestCase): ) @override_settings(PONOS_DEFAULT_ENV={"ARKINDEX_API_TOKEN": "testToken"}) - @patch("arkindex.process.builder.task_token_default") + @patch("arkindex.process.builder.token_default") def test_pdf_import_run(self, token_mock): process = self.corpus.processes.create( creator=self.user,