diff --git a/goosebit/api/v1/devices/routes.py b/goosebit/api/v1/devices/routes.py index 209056db..867eaa26 100644 --- a/goosebit/api/v1/devices/routes.py +++ b/goosebit/api/v1/devices/routes.py @@ -59,6 +59,8 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon if await Device.get_or_none(id=dev_id) is None: raise HTTPException(404, f"Device with ID {dev_id} not found") device = await DeviceManager.get_device(dev_id) + if config.feed is not None: + await DeviceManager.update_feed(device, config.feed) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) @@ -71,8 +73,6 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon await DeviceManager.update_update(device, UpdateModeEnum.PINNED, None) if config.name is not None: await DeviceManager.update_name(device, config.name) - if config.feed is not None: - await DeviceManager.update_feed(device, config.feed) if config.force_update is not None: await DeviceManager.update_force_update(device, config.force_update) if config.auth_token is not None: @@ -87,6 +87,8 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon async def devices_put(_: Request, config: DevicesPutRequest) -> StatusResponse: for dev_id in config.devices: device = await DeviceManager.get_device(dev_id) + if config.feed is not None: + await DeviceManager.update_feed(device, config.feed) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) @@ -99,8 +101,6 @@ async def devices_put(_: Request, config: DevicesPutRequest) -> StatusResponse: await DeviceManager.update_update(device, UpdateModeEnum.PINNED, None) if config.name is not None: await DeviceManager.update_name(device, config.name) - if config.feed is not None: - await DeviceManager.update_feed(device, config.feed) if config.force_update is not None: await DeviceManager.update_force_update(device, config.force_update) if config.auth_token is not None: diff --git a/goosebit/db/migrations/models/5_20250619090242_null_feed.py b/goosebit/db/migrations/models/5_20250619090242_null_feed.py new file mode 100644 index 00000000..f407a954 --- /dev/null +++ b/goosebit/db/migrations/models/5_20250619090242_null_feed.py @@ -0,0 +1,83 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + dialect = db.schema_generator.DIALECT + + if dialect == "postgres": + return """ + ALTER TABLE "device" ALTER COLUMN "feed" DROP NOT NULL;""" + + return """PRAGMA foreign_keys=off; + + CREATE TABLE "device_new" ( + "id" CHAR(255) NOT NULL PRIMARY KEY, + "name" CHAR(255), + "assigned_software_id" INT, + "force_update" INT NOT NULL DEFAULT 0, + "sw_version" CHAR(255), + "hardware_id" INT NOT NULL, + "feed" CHAR(255) DEFAULT 'default', -- NULL allowed here + "update_mode" INT NOT NULL DEFAULT 0, + "last_state" INT NOT NULL DEFAULT 0, + "progress" INT, + "last_log" TEXT, + "last_seen" BIGINT, + "last_ip" CHAR(15), + "last_ipv6" CHAR(40), + "auth_token" CHAR(32) + ); + + INSERT INTO "device_new" SELECT + id, name, assigned_software_id, force_update, sw_version, + hardware_id, feed, update_mode, last_state, progress, + last_log, last_seen, last_ip, last_ipv6, auth_token + FROM "device"; + + DROP TABLE "device"; + + ALTER TABLE "device_new" RENAME TO "device"; + + PRAGMA foreign_keys=on; + """ + + +async def downgrade(db: BaseDBAsyncClient) -> str: + dialect = db.schema_generator.DIALECT + + if dialect == "postgres": + return """ + ALTER TABLE "device" ALTER COLUMN "feed" SET NOT NULL;""" + + return """PRAGMA foreign_keys=off; + + CREATE TABLE "device_old" ( + "id" CHAR(255) NOT NULL PRIMARY KEY, + "name" CHAR(255), + "assigned_software_id" INT, + "force_update" INT NOT NULL DEFAULT 0, + "sw_version" CHAR(255), + "hardware_id" INT NOT NULL, + "feed" CHAR(255) NOT NULL DEFAULT 'default', -- NOT NULL again + "update_mode" INT NOT NULL DEFAULT 0, + "last_state" INT NOT NULL DEFAULT 0, + "progress" INT, + "last_log" TEXT, + "last_seen" BIGINT, + "last_ip" CHAR(15), + "last_ipv6" CHAR(40), + "auth_token" CHAR(32) + ); + + INSERT INTO "device_old" SELECT + id, name, assigned_software_id, force_update, sw_version, + hardware_id, feed, update_mode, last_state, progress, + last_log, last_seen, last_ip, last_ipv6, auth_token + FROM "device"; + + DROP TABLE "device"; + + ALTER TABLE "device_old" RENAME TO "device"; + + PRAGMA foreign_keys=on; + """ diff --git a/goosebit/db/models.py b/goosebit/db/models.py index 94358502..7dcb63c3 100644 --- a/goosebit/db/models.py +++ b/goosebit/db/models.py @@ -64,7 +64,7 @@ class Device(Model): force_update = fields.BooleanField(default=False) sw_version = fields.CharField(max_length=255, null=True) hardware = fields.ForeignKeyField("models.Hardware", related_name="devices") - feed = fields.CharField(max_length=255, default="default") + feed = fields.CharField(max_length=255, default="default", null=True) update_mode = fields.IntEnumField(UpdateModeEnum, default=UpdateModeEnum.ROLLOUT) last_state = fields.IntEnumField(UpdateStateEnum, default=UpdateStateEnum.UNKNOWN) progress = fields.IntField(null=True) @@ -76,6 +76,10 @@ class Device(Model): tags = fields.ManyToManyField("models.Tag", related_name="devices", through="device_tags") async def save(self, *args, **kwargs): + # ensure if using rollout that feed is set + if self.update_mode == UpdateModeEnum.ROLLOUT: + if self.feed is None: + raise ValidationError("Feed must be set in order to use rollout.") # Check if the software is compatible with the hardware before saving if self.assigned_software and self.hardware: # Check if the assigned software is compatible with the hardware diff --git a/goosebit/device_manager.py b/goosebit/device_manager.py index b2611d3e..9510e595 100644 --- a/goosebit/device_manager.py +++ b/goosebit/device_manager.py @@ -108,7 +108,9 @@ async def update_last_connection(device: Device, last_seen: int, last_ip: str | async def update_update(device: Device, update_mode: UpdateModeEnum, software: Software | None): device.assigned_software = software device.update_mode = update_mode - await DeviceManager.save_device(device, update_fields=["assigned_software_id", "update_mode"]) + if not update_mode == UpdateModeEnum.ROLLOUT: + device.feed = None + await DeviceManager.save_device(device, update_fields=["assigned_software_id", "update_mode", "feed"]) @staticmethod async def update_name(device: Device, name: str): diff --git a/goosebit/schema/devices.py b/goosebit/schema/devices.py index fd4b21a2..c33772ea 100644 --- a/goosebit/schema/devices.py +++ b/goosebit/schema/devices.py @@ -37,7 +37,7 @@ class DeviceSchema(BaseModel): assigned_software: SoftwareSchema | None = Field(exclude=True) hardware: HardwareSchema | None = Field(exclude=True) - feed: str + feed: str | None progress: int | None last_state: Annotated[UpdateStateSchema, BeforeValidator(UpdateStateSchema.convert)] # type: ignore[valid-type] update_mode: Annotated[UpdateModeSchema, BeforeValidator(UpdateModeSchema.convert)] # type: ignore[valid-type] diff --git a/goosebit/ui/bff/devices/routes.py b/goosebit/ui/bff/devices/routes.py index 046ca86a..e101f74b 100644 --- a/goosebit/ui/bff/devices/routes.py +++ b/goosebit/ui/bff/devices/routes.py @@ -68,6 +68,8 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon if await Device.get_or_none(id=dev_id) is None: raise HTTPException(404, f"Device with ID {dev_id} not found") device = await get_device(dev_id) + if config.feed is not None: + await DeviceManager.update_feed(device, config.feed) if config.software is not None: if config.software == "rollout": await DeviceManager.update_update(device, UpdateModeEnum.ROLLOUT, None) @@ -80,8 +82,6 @@ async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusRespon await DeviceManager.update_update(device, UpdateModeEnum.PINNED, None) if config.name is not None: await DeviceManager.update_name(device, config.name) - if config.feed is not None: - await DeviceManager.update_feed(device, config.feed) if config.force_update is not None: await DeviceManager.update_force_update(device, config.force_update) if config.auth_token is not None: