Is your feature request related to a problem? Please describe.
Sub-issue 07 wires up the admin; this one mirrors all of it for the REST API. The split matters because single-device persistent upgrades are an API/scripting use case in this iteration (the admin UI for single-device persistence is deferred to a future iteration), so the API surface has to land now.
Today the API surface has none of the persistence fields: BatchUpgradeSerializer exposes only upgrade_all, group, location; UpgradeOperationSerializer exposes id, device, image, status, log, progress, modified, created; and BuildBatchUpgradeView.post() accepts no is_persistent kwarg to pass through. So an API consumer can't opt a single-device upgrade into persistence, can't filter pending ops via ?status=, and can't read retry_count or next_retry_at.
Describe the solution I would implement
I would like to add full API parity with the admin surface from sub-issue 07.
-
Add a writable is_persistent boolean to BatchUpgradeSerializer:
default=True so omitting the field in the request body produces the same "checked by default" outcome as the admin form.
- Add to
Meta.fields.
-
Update UpgradeOperationSerializer and DeviceUpgradeOperationSerializer:
- Writable
is_persistent field so single-device callers can opt in.
- Read-only
retry_count and next_retry_at fields.
-
BatchUpgradeOperationListSerializer and BatchUpgradeOperationSerializer both use fields = "__all__", so they inherit is_persistent automatically once sub-issue 01 adds it to the model. I'd add an explicit unit test asserting the field appears in the serialized output, so a future serializer refactor doesn't silently drop it.
-
Update BuildBatchUpgradeView.post():
- Read
is_persistent from serializer.validated_data.
- Pass it alongside the existing
firmwareless, group, location kwargs to Build.batch_upgrade(). The method signature update lives in sub-issue 07.
-
Enforce post-launch immutability at the serializer level to mirror sub-issue 01's model-level clean() guard. Override update() on UpgradeOperationSerializer and BatchUpgradeOperationSerializer to raise serializers.ValidationError (400) when is_persistent is in validated_data: a PATCH/PUT always targets a saved row, which is post-launch for UpgradeOperation by definition. For BatchUpgradeOperation the status != "idle" check from sub-issue 01's clean() would fire via is_valid() anyway, but raising explicitly gives a clearer error response.
-
UpgradeOperationFilter (api/filters.py:11–35) and DeviceUpgradeOperationFilter (api/filters.py:38–41) already include status in their Meta.fields, so ?status=pending becomes available automatically once sub-issue 01 adds the new choice. Add explicit test coverage.
-
The dedicated cancel endpoint already exists: POST /api/v1/firmware-upgrader/upgrade-operation/<uuid:pk>/cancel/ routed to UpgradeOperationCancelView at api/views.py:381–451 (URL declaration at api/urls.py:60–64, decorated with @swagger_auto_schema for drf_yasg). The view calls instance.cancel(); once sub-issue 03 extends _CANCELLABLE_STATUS to include pending, the same endpoint cancels pending ops with no view-side change. Add a regression test asserting POST /cancel/ returns 200 for a pending op and surfaces the ValueError from cancel() (mapped to 409) on a terminal-status op.
-
API tests covering: POST with is_is_persistent=true/false/omitted produces correctly-flagged batches and propagates to children; single-device POST with is_is_persistent=true creates a persistent standalone op; GET ?status=pending returns only pending operations; detail response includes retry_count and next_retry_at; PATCH to mutate is_persistent post-launch is rejected with 400; cancel on a pending op returns 200 with the op transitioned to cancelled.
Is your feature request related to a problem? Please describe.
Sub-issue 07 wires up the admin; this one mirrors all of it for the REST API. The split matters because single-device persistent upgrades are an API/scripting use case in this iteration (the admin UI for single-device persistence is deferred to a future iteration), so the API surface has to land now.
Today the API surface has none of the persistence fields:
BatchUpgradeSerializerexposes onlyupgrade_all,group,location;UpgradeOperationSerializerexposesid,device,image,status,log,progress,modified,created; andBuildBatchUpgradeView.post()accepts nois_persistentkwarg to pass through. So an API consumer can't opt a single-device upgrade into persistence, can't filter pending ops via?status=, and can't readretry_countornext_retry_at.Describe the solution I would implement
I would like to add full API parity with the admin surface from sub-issue 07.
Add a writable
is_persistentboolean toBatchUpgradeSerializer:default=Trueso omitting the field in the request body produces the same "checked by default" outcome as the admin form.Meta.fields.Update
UpgradeOperationSerializerandDeviceUpgradeOperationSerializer:is_persistentfield so single-device callers can opt in.retry_countandnext_retry_atfields.BatchUpgradeOperationListSerializerandBatchUpgradeOperationSerializerboth usefields = "__all__", so they inheritis_persistentautomatically once sub-issue 01 adds it to the model. I'd add an explicit unit test asserting the field appears in the serialized output, so a future serializer refactor doesn't silently drop it.Update
BuildBatchUpgradeView.post():is_persistentfromserializer.validated_data.firmwareless,group,locationkwargs toBuild.batch_upgrade(). The method signature update lives in sub-issue 07.Enforce post-launch immutability at the serializer level to mirror sub-issue 01's model-level
clean()guard. Overrideupdate()onUpgradeOperationSerializerandBatchUpgradeOperationSerializerto raiseserializers.ValidationError(400) whenis_persistentis invalidated_data: a PATCH/PUT always targets a saved row, which is post-launch forUpgradeOperationby definition. ForBatchUpgradeOperationthestatus != "idle"check from sub-issue 01'sclean()would fire viais_valid()anyway, but raising explicitly gives a clearer error response.UpgradeOperationFilter(api/filters.py:11–35) andDeviceUpgradeOperationFilter(api/filters.py:38–41) already includestatusin theirMeta.fields, so?status=pendingbecomes available automatically once sub-issue 01 adds the new choice. Add explicit test coverage.The dedicated cancel endpoint already exists:
POST /api/v1/firmware-upgrader/upgrade-operation/<uuid:pk>/cancel/routed toUpgradeOperationCancelViewatapi/views.py:381–451(URL declaration atapi/urls.py:60–64, decorated with@swagger_auto_schemafor drf_yasg). The view callsinstance.cancel(); once sub-issue 03 extends_CANCELLABLE_STATUSto includepending, the same endpoint cancels pending ops with no view-side change. Add a regression test assertingPOST /cancel/returns 200 for a pending op and surfaces theValueErrorfromcancel()(mapped to 409) on a terminal-status op.API tests covering:
POSTwithis_is_persistent=true/false/omitted produces correctly-flagged batches and propagates to children; single-devicePOSTwithis_is_persistent=truecreates a persistent standalone op;GET ?status=pendingreturns only pending operations; detail response includesretry_countandnext_retry_at;PATCHto mutateis_persistentpost-launch is rejected with 400; cancel on a pending op returns 200 with the op transitioned tocancelled.