From 8d62462ec8b39b8e3fe2f6b6d7256cdeba35148b Mon Sep 17 00:00:00 2001 From: Rongxin Liu <312588553+rl-block@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:38:23 +0100 Subject: [PATCH] feat(rollout): add release channel schema, queries and store Storage for release channels and the rollouts that enforce them, aligned with the merged RolloutService contract (#1014): assignments and rollouts are keyed by canonical (manufacturer, model) pairs compared with an ASCII-only fold, firmware is identified by payload checksum with the file's target metadata snapshotted onto the assignment, every pair carries an assignment generation that survives clearing, rollouts carry a revision that a trigger advances on every change to the row or its devices, plus actor attribution and delegated-control columns. Membership views resolve ties to no channel and expose every conflicting relation; managed-deployment provenance is recorded per device. The files service gains the two lookups the domain needs for checksum identity. --- server/generated/sqlc/db.go | 440 +++ server/generated/sqlc/models.go | 142 + server/generated/sqlc/querier.go | 120 + server/generated/sqlc/release_channel.sql.go | 2606 +++++++++++++++++ server/generated/sqlc/retrying_querier.gen.go | 468 +++ .../stores/sqlstores/release_channel.go | 31 + .../db/migration_bridges_test.go | 42 +- .../internal/infrastructure/files/firmware.go | 52 + .../infrastructure/files/firmware_test.go | 35 + .../000148_release_channels.down.sql | 14 + .../migrations/000148_release_channels.up.sql | 339 +++ server/migrations/release_channels_test.go | 50 + server/sqlc/queries/release_channel.sql | 790 +++++ 13 files changed, 5122 insertions(+), 7 deletions(-) create mode 100644 server/generated/sqlc/release_channel.sql.go create mode 100644 server/internal/domain/stores/sqlstores/release_channel.go create mode 100644 server/migrations/000148_release_channels.down.sql create mode 100644 server/migrations/000148_release_channels.up.sql create mode 100644 server/migrations/release_channels_test.go create mode 100644 server/sqlc/queries/release_channel.sql diff --git a/server/generated/sqlc/db.go b/server/generated/sqlc/db.go index 5c24751cd0..2666976f60 100644 --- a/server/generated/sqlc/db.go +++ b/server/generated/sqlc/db.go @@ -39,6 +39,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.adminTerminateCurtailmentEventStmt, err = db.PrepareContext(ctx, adminTerminateCurtailmentEvent); err != nil { return nil, fmt.Errorf("error preparing query AdminTerminateCurtailmentEvent: %w", err) } + if q.advanceFirmwareRolloutStageStmt, err = db.PrepareContext(ctx, advanceFirmwareRolloutStage); err != nil { + return nil, fmt.Errorf("error preparing query AdvanceFirmwareRolloutStage: %w", err) + } if q.advanceFleetMetricRollupProgressStmt, err = db.PrepareContext(ctx, advanceFleetMetricRollupProgress); err != nil { return nil, fmt.Errorf("error preparing query AdvanceFleetMetricRollupProgress: %w", err) } @@ -108,9 +111,15 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.bumpCurtailmentTargetRetryStmt, err = db.PrepareContext(ctx, bumpCurtailmentTargetRetry); err != nil { return nil, fmt.Errorf("error preparing query BumpCurtailmentTargetRetry: %w", err) } + if q.cancelActiveFirmwareRolloutStmt, err = db.PrepareContext(ctx, cancelActiveFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query CancelActiveFirmwareRollout: %w", err) + } if q.cancelEnrollmentForFleetNodeStmt, err = db.PrepareContext(ctx, cancelEnrollmentForFleetNode); err != nil { return nil, fmt.Errorf("error preparing query CancelEnrollmentForFleetNode: %w", err) } + if q.cancelFirmwareRolloutStmt, err = db.PrepareContext(ctx, cancelFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query CancelFirmwareRollout: %w", err) + } if q.cancelPendingEnrollmentStmt, err = db.PrepareContext(ctx, cancelPendingEnrollment); err != nil { return nil, fmt.Errorf("error preparing query CancelPendingEnrollment: %w", err) } @@ -174,6 +183,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.clearRackSlotPositionStmt, err = db.PrepareContext(ctx, clearRackSlotPosition); err != nil { return nil, fmt.Errorf("error preparing query ClearRackSlotPosition: %w", err) } + if q.clearReleaseChannelFirmwareStmt, err = db.PrepareContext(ctx, clearReleaseChannelFirmware); err != nil { + return nil, fmt.Errorf("error preparing query ClearReleaseChannelFirmware: %w", err) + } if q.clearRolePermissionsStmt, err = db.PrepareContext(ctx, clearRolePermissions); err != nil { return nil, fmt.Errorf("error preparing query ClearRolePermissions: %w", err) } @@ -297,6 +309,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.createDeviceSetStmt, err = db.PrepareContext(ctx, createDeviceSet); err != nil { return nil, fmt.Errorf("error preparing query CreateDeviceSet: %w", err) } + if q.createFirmwareRolloutStmt, err = db.PrepareContext(ctx, createFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query CreateFirmwareRollout: %w", err) + } if q.createFleetNodeStmt, err = db.PrepareContext(ctx, createFleetNode); err != nil { return nil, fmt.Errorf("error preparing query CreateFleetNode: %w", err) } @@ -327,6 +342,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.createRackExtensionStmt, err = db.PrepareContext(ctx, createRackExtension); err != nil { return nil, fmt.Errorf("error preparing query CreateRackExtension: %w", err) } + if q.createReleaseChannelStmt, err = db.PrepareContext(ctx, createReleaseChannel); err != nil { + return nil, fmt.Errorf("error preparing query CreateReleaseChannel: %w", err) + } if q.createRepairTicketStmt, err = db.PrepareContext(ctx, createRepairTicket); err != nil { return nil, fmt.Errorf("error preparing query CreateRepairTicket: %w", err) } @@ -408,6 +426,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.deletePoolStmt, err = db.PrepareContext(ctx, deletePool); err != nil { return nil, fmt.Errorf("error preparing query DeletePool: %w", err) } + if q.deleteReleaseChannelStmt, err = db.PrepareContext(ctx, deleteReleaseChannel); err != nil { + return nil, fmt.Errorf("error preparing query DeleteReleaseChannel: %w", err) + } + if q.deleteReleaseChannelTargetsStmt, err = db.PrepareContext(ctx, deleteReleaseChannelTargets); err != nil { + return nil, fmt.Errorf("error preparing query DeleteReleaseChannelTargets: %w", err) + } if q.deleteScheduleTargetsStmt, err = db.PrepareContext(ctx, deleteScheduleTargets); err != nil { return nil, fmt.Errorf("error preparing query DeleteScheduleTargets: %w", err) } @@ -432,6 +456,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.ensureCurtailmentOrgConfigStmt, err = db.PrepareContext(ctx, ensureCurtailmentOrgConfig); err != nil { return nil, fmt.Errorf("error preparing query EnsureCurtailmentOrgConfig: %w", err) } + if q.excludeFirmwareRolloutDevicesStmt, err = db.PrepareContext(ctx, excludeFirmwareRolloutDevices); err != nil { + return nil, fmt.Errorf("error preparing query ExcludeFirmwareRolloutDevices: %w", err) + } if q.findDeviceBuildingConflictsStmt, err = db.PrepareContext(ctx, findDeviceBuildingConflicts); err != nil { return nil, fmt.Errorf("error preparing query FindDeviceBuildingConflicts: %w", err) } @@ -447,12 +474,18 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.findDevicesWithSiteOrBuildingStmt, err = db.PrepareContext(ctx, findDevicesWithSiteOrBuilding); err != nil { return nil, fmt.Errorf("error preparing query FindDevicesWithSiteOrBuilding: %w", err) } + if q.finishFirmwareRolloutStmt, err = db.PrepareContext(ctx, finishFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query FinishFirmwareRollout: %w", err) + } if q.finishTerminalCommandBatchesStmt, err = db.PrepareContext(ctx, finishTerminalCommandBatches); err != nil { return nil, fmt.Errorf("error preparing query FinishTerminalCommandBatches: %w", err) } if q.forceReleaseCurtailmentEventStmt, err = db.PrepareContext(ctx, forceReleaseCurtailmentEvent); err != nil { return nil, fmt.Errorf("error preparing query ForceReleaseCurtailmentEvent: %w", err) } + if q.getActiveFirmwareRolloutForPairStmt, err = db.PrepareContext(ctx, getActiveFirmwareRolloutForPair); err != nil { + return nil, fmt.Errorf("error preparing query GetActiveFirmwareRolloutForPair: %w", err) + } if q.getActiveSchedulesStmt, err = db.PrepareContext(ctx, getActiveSchedules); err != nil { return nil, fmt.Errorf("error preparing query GetActiveSchedules: %w", err) } @@ -699,6 +732,15 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getFilteredTicketStatsStmt, err = db.PrepareContext(ctx, getFilteredTicketStats); err != nil { return nil, fmt.Errorf("error preparing query GetFilteredTicketStats: %w", err) } + if q.getFirmwareRolloutStmt, err = db.PrepareContext(ctx, getFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query GetFirmwareRollout: %w", err) + } + if q.getFirmwareRolloutForUpdateStmt, err = db.PrepareContext(ctx, getFirmwareRolloutForUpdate); err != nil { + return nil, fmt.Errorf("error preparing query GetFirmwareRolloutForUpdate: %w", err) + } + if q.getFirmwareRolloutWithChannelStmt, err = db.PrepareContext(ctx, getFirmwareRolloutWithChannel); err != nil { + return nil, fmt.Errorf("error preparing query GetFirmwareRolloutWithChannel: %w", err) + } if q.getFleetMetricRollupCoverageStmt, err = db.PrepareContext(ctx, getFleetMetricRollupCoverage); err != nil { return nil, fmt.Errorf("error preparing query GetFleetMetricRollupCoverage: %w", err) } @@ -747,6 +789,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getLatestDeviceMetricsStmt, err = db.PrepareContext(ctx, getLatestDeviceMetrics); err != nil { return nil, fmt.Errorf("error preparing query GetLatestDeviceMetrics: %w", err) } + if q.getLatestFirmwareRolloutForPairStmt, err = db.PrepareContext(ctx, getLatestFirmwareRolloutForPair); err != nil { + return nil, fmt.Errorf("error preparing query GetLatestFirmwareRolloutForPair: %w", err) + } if q.getLatestFleetMetricRollupBucketStmt, err = db.PrepareContext(ctx, getLatestFleetMetricRollupBucket); err != nil { return nil, fmt.Errorf("error preparing query GetLatestFleetMetricRollupBucket: %w", err) } @@ -858,6 +903,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getRackSlotsStmt, err = db.PrepareContext(ctx, getRackSlots); err != nil { return nil, fmt.Errorf("error preparing query GetRackSlots: %w", err) } + if q.getReleaseChannelStmt, err = db.PrepareContext(ctx, getReleaseChannel); err != nil { + return nil, fmt.Errorf("error preparing query GetReleaseChannel: %w", err) + } + if q.getReleaseChannelFirmwareStmt, err = db.PrepareContext(ctx, getReleaseChannelFirmware); err != nil { + return nil, fmt.Errorf("error preparing query GetReleaseChannelFirmware: %w", err) + } if q.getReleaseChannelSettingStmt, err = db.PrepareContext(ctx, getReleaseChannelSetting); err != nil { return nil, fmt.Errorf("error preparing query GetReleaseChannelSetting: %w", err) } @@ -945,6 +996,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getUsersForOrganizationStmt, err = db.PrepareContext(ctx, getUsersForOrganization); err != nil { return nil, fmt.Errorf("error preparing query GetUsersForOrganization: %w", err) } + if q.haltFirmwareRolloutDevicesStmt, err = db.PrepareContext(ctx, haltFirmwareRolloutDevices); err != nil { + return nil, fmt.Errorf("error preparing query HaltFirmwareRolloutDevices: %w", err) + } if q.hasUserStmt, err = db.PrepareContext(ctx, hasUser); err != nil { return nil, fmt.Errorf("error preparing query HasUser: %w", err) } @@ -990,6 +1044,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.insertNotificationMetricSamplesStmt, err = db.PrepareContext(ctx, insertNotificationMetricSamples); err != nil { return nil, fmt.Errorf("error preparing query InsertNotificationMetricSamples: %w", err) } + if q.insertReleaseChannelTargetsStmt, err = db.PrepareContext(ctx, insertReleaseChannelTargets); err != nil { + return nil, fmt.Errorf("error preparing query InsertReleaseChannelTargets: %w", err) + } if q.insertRepairTicketPartStmt, err = db.PrepareContext(ctx, insertRepairTicketPart); err != nil { return nil, fmt.Errorf("error preparing query InsertRepairTicketPart: %w", err) } @@ -1014,6 +1071,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.listActiveCurtailmentTargetDevicesByOrgStmt, err = db.PrepareContext(ctx, listActiveCurtailmentTargetDevicesByOrg); err != nil { return nil, fmt.Errorf("error preparing query ListActiveCurtailmentTargetDevicesByOrg: %w", err) } + if q.listActiveFirmwareRolloutsStmt, err = db.PrepareContext(ctx, listActiveFirmwareRollouts); err != nil { + return nil, fmt.Errorf("error preparing query ListActiveFirmwareRollouts: %w", err) + } if q.listActiveNotificationGroupsStmt, err = db.PrepareContext(ctx, listActiveNotificationGroups); err != nil { return nil, fmt.Errorf("error preparing query ListActiveNotificationGroups: %w", err) } @@ -1113,6 +1173,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.listCustomRolesForOrgStmt, err = db.PrepareContext(ctx, listCustomRolesForOrg); err != nil { return nil, fmt.Errorf("error preparing query ListCustomRolesForOrg: %w", err) } + if q.listDeviceIDsByIdentifiersStmt, err = db.PrepareContext(ctx, listDeviceIDsByIdentifiers); err != nil { + return nil, fmt.Errorf("error preparing query ListDeviceIDsByIdentifiers: %w", err) + } if q.listDeviceSetMembersPaginatedStmt, err = db.PrepareContext(ctx, listDeviceSetMembersPaginated); err != nil { return nil, fmt.Errorf("error preparing query ListDeviceSetMembersPaginated: %w", err) } @@ -1149,6 +1212,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.listExistingDeviceIdentifiersStmt, err = db.PrepareContext(ctx, listExistingDeviceIdentifiers); err != nil { return nil, fmt.Errorf("error preparing query ListExistingDeviceIdentifiers: %w", err) } + if q.listFirmwareRolloutDevicesStmt, err = db.PrepareContext(ctx, listFirmwareRolloutDevices); err != nil { + return nil, fmt.Errorf("error preparing query ListFirmwareRolloutDevices: %w", err) + } + if q.listFirmwareRolloutsStmt, err = db.PrepareContext(ctx, listFirmwareRollouts); err != nil { + return nil, fmt.Errorf("error preparing query ListFirmwareRollouts: %w", err) + } if q.listFleetNodeDeviceIDsForRevocationStmt, err = db.PrepareContext(ctx, listFleetNodeDeviceIDsForRevocation); err != nil { return nil, fmt.Errorf("error preparing query ListFleetNodeDeviceIDsForRevocation: %w", err) } @@ -1218,6 +1287,36 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.listRecentlyResolvedCurtailedDevicesByScopeStmt, err = db.PrepareContext(ctx, listRecentlyResolvedCurtailedDevicesByScope); err != nil { return nil, fmt.Errorf("error preparing query ListRecentlyResolvedCurtailedDevicesByScope: %w", err) } + if q.listReleaseChannelFirmwareStmt, err = db.PrepareContext(ctx, listReleaseChannelFirmware); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelFirmware: %w", err) + } + if q.listReleaseChannelFirmwareNeedingRolloutStmt, err = db.PrepareContext(ctx, listReleaseChannelFirmwareNeedingRollout); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelFirmwareNeedingRollout: %w", err) + } + if q.listReleaseChannelMembersStmt, err = db.PrepareContext(ctx, listReleaseChannelMembers); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelMembers: %w", err) + } + if q.listReleaseChannelMembershipConflictsPageStmt, err = db.PrepareContext(ctx, listReleaseChannelMembershipConflictsPage); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelMembershipConflictsPage: %w", err) + } + if q.listReleaseChannelMinersPageStmt, err = db.PrepareContext(ctx, listReleaseChannelMinersPage); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelMinersPage: %w", err) + } + if q.listReleaseChannelMismatchedMembersStmt, err = db.PrepareContext(ctx, listReleaseChannelMismatchedMembers); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelMismatchedMembers: %w", err) + } + if q.listReleaseChannelModelGroupsPageStmt, err = db.PrepareContext(ctx, listReleaseChannelModelGroupsPage); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelModelGroupsPage: %w", err) + } + if q.listReleaseChannelSuppressedMembersStmt, err = db.PrepareContext(ctx, listReleaseChannelSuppressedMembers); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelSuppressedMembers: %w", err) + } + if q.listReleaseChannelTargetsStmt, err = db.PrepareContext(ctx, listReleaseChannelTargets); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannelTargets: %w", err) + } + if q.listReleaseChannelsStmt, err = db.PrepareContext(ctx, listReleaseChannels); err != nil { + return nil, fmt.Errorf("error preparing query ListReleaseChannels: %w", err) + } if q.listRepairTicketCommentsStmt, err = db.PrepareContext(ctx, listRepairTicketComments); err != nil { return nil, fmt.Errorf("error preparing query ListRepairTicketComments: %w", err) } @@ -1362,6 +1461,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.lockRacksForReparentStmt, err = db.PrepareContext(ctx, lockRacksForReparent); err != nil { return nil, fmt.Errorf("error preparing query LockRacksForReparent: %w", err) } + if q.lockReleaseChannelScopesStmt, err = db.PrepareContext(ctx, lockReleaseChannelScopes); err != nil { + return nil, fmt.Errorf("error preparing query LockReleaseChannelScopes: %w", err) + } if q.lockRepairTicketCommentCreateKeyStmt, err = db.PrepareContext(ctx, lockRepairTicketCommentCreateKey); err != nil { return nil, fmt.Errorf("error preparing query LockRepairTicketCommentCreateKey: %w", err) } @@ -1383,6 +1485,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.markCommandBatchProcessingStmt, err = db.PrepareContext(ctx, markCommandBatchProcessing); err != nil { return nil, fmt.Errorf("error preparing query MarkCommandBatchProcessing: %w", err) } + if q.markFirmwareRolloutDevicesSentStmt, err = db.PrepareContext(ctx, markFirmwareRolloutDevicesSent); err != nil { + return nil, fmt.Errorf("error preparing query MarkFirmwareRolloutDevicesSent: %w", err) + } if q.markRepairTicketPartsConsumedStmt, err = db.PrepareContext(ctx, markRepairTicketPartsConsumed); err != nil { return nil, fmt.Errorf("error preparing query MarkRepairTicketPartsConsumed: %w", err) } @@ -1401,6 +1506,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.pauseActiveScheduleStmt, err = db.PrepareContext(ctx, pauseActiveSchedule); err != nil { return nil, fmt.Errorf("error preparing query PauseActiveSchedule: %w", err) } + if q.pauseFirmwareRolloutStmt, err = db.PrepareContext(ctx, pauseFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query PauseFirmwareRollout: %w", err) + } if q.pruneExpiredAlertMaintenanceWindowsStmt, err = db.PrepareContext(ctx, pruneExpiredAlertMaintenanceWindows); err != nil { return nil, fmt.Errorf("error preparing query PruneExpiredAlertMaintenanceWindows: %w", err) } @@ -1440,6 +1548,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.recordCurtailPendingDispatchStmt, err = db.PrepareContext(ctx, recordCurtailPendingDispatch); err != nil { return nil, fmt.Errorf("error preparing query RecordCurtailPendingDispatch: %w", err) } + if q.recordFirmwareDeploymentStmt, err = db.PrepareContext(ctx, recordFirmwareDeployment); err != nil { + return nil, fmt.Errorf("error preparing query RecordFirmwareDeployment: %w", err) + } + if q.recordFirmwareRolloutActionStmt, err = db.PrepareContext(ctx, recordFirmwareRolloutAction); err != nil { + return nil, fmt.Errorf("error preparing query RecordFirmwareRolloutAction: %w", err) + } if q.refreshOpenErrorsLastSeenByDeviceStmt, err = db.PrepareContext(ctx, refreshOpenErrorsLastSeenByDevice); err != nil { return nil, fmt.Errorf("error preparing query RefreshOpenErrorsLastSeenByDevice: %w", err) } @@ -1464,6 +1578,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.requestRigConfigReconciliationStmt, err = db.PrepareContext(ctx, requestRigConfigReconciliation); err != nil { return nil, fmt.Errorf("error preparing query RequestRigConfigReconciliation: %w", err) } + if q.requeueFirmwareRolloutDevicesStmt, err = db.PrepareContext(ctx, requeueFirmwareRolloutDevices); err != nil { + return nil, fmt.Errorf("error preparing query RequeueFirmwareRolloutDevices: %w", err) + } if q.requeueRigConfigReconciliationAfterTerminalFailureStmt, err = db.PrepareContext(ctx, requeueRigConfigReconciliationAfterTerminalFailure); err != nil { return nil, fmt.Errorf("error preparing query RequeueRigConfigReconciliationAfterTerminalFailure: %w", err) } @@ -1494,9 +1611,15 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.resolveMaintenanceMinerContextStmt, err = db.PrepareContext(ctx, resolveMaintenanceMinerContext); err != nil { return nil, fmt.Errorf("error preparing query ResolveMaintenanceMinerContext: %w", err) } + if q.resolveReleaseChannelScopeStmt, err = db.PrepareContext(ctx, resolveReleaseChannelScope); err != nil { + return nil, fmt.Errorf("error preparing query ResolveReleaseChannelScope: %w", err) + } if q.resumeCurtailmentFromRestoringStmt, err = db.PrepareContext(ctx, resumeCurtailmentFromRestoring); err != nil { return nil, fmt.Errorf("error preparing query ResumeCurtailmentFromRestoring: %w", err) } + if q.resumeFirmwareRolloutStmt, err = db.PrepareContext(ctx, resumeFirmwareRollout); err != nil { + return nil, fmt.Errorf("error preparing query ResumeFirmwareRollout: %w", err) + } if q.resumePausedScheduleStmt, err = db.PrepareContext(ctx, resumePausedSchedule); err != nil { return nil, fmt.Errorf("error preparing query ResumePausedSchedule: %w", err) } @@ -1572,6 +1695,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.sitesByIDsStmt, err = db.PrepareContext(ctx, sitesByIDs); err != nil { return nil, fmt.Errorf("error preparing query SitesByIDs: %w", err) } + if q.snapshotFirmwareRolloutDevicesStmt, err = db.PrepareContext(ctx, snapshotFirmwareRolloutDevices); err != nil { + return nil, fmt.Errorf("error preparing query SnapshotFirmwareRolloutDevices: %w", err) + } if q.softDeleteAlertChannelStmt, err = db.PrepareContext(ctx, softDeleteAlertChannel); err != nil { return nil, fmt.Errorf("error preparing query SoftDeleteAlertChannel: %w", err) } @@ -1797,6 +1923,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.updateRackPlacementBulkForSiteStmt, err = db.PrepareContext(ctx, updateRackPlacementBulkForSite); err != nil { return nil, fmt.Errorf("error preparing query UpdateRackPlacementBulkForSite: %w", err) } + if q.updateReleaseChannelStmt, err = db.PrepareContext(ctx, updateReleaseChannel); err != nil { + return nil, fmt.Errorf("error preparing query UpdateReleaseChannel: %w", err) + } if q.updateRepairTicketStmt, err = db.PrepareContext(ctx, updateRepairTicket); err != nil { return nil, fmt.Errorf("error preparing query UpdateRepairTicket: %w", err) } @@ -1878,6 +2007,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.upsertPermissionStmt, err = db.PrepareContext(ctx, upsertPermission); err != nil { return nil, fmt.Errorf("error preparing query UpsertPermission: %w", err) } + if q.upsertReleaseChannelFirmwareStmt, err = db.PrepareContext(ctx, upsertReleaseChannelFirmware); err != nil { + return nil, fmt.Errorf("error preparing query UpsertReleaseChannelFirmware: %w", err) + } if q.upsertReleaseChannelSettingStmt, err = db.PrepareContext(ctx, upsertReleaseChannelSetting); err != nil { return nil, fmt.Errorf("error preparing query UpsertReleaseChannelSetting: %w", err) } @@ -1911,6 +2043,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing adminTerminateCurtailmentEventStmt: %w", cerr) } } + if q.advanceFirmwareRolloutStageStmt != nil { + if cerr := q.advanceFirmwareRolloutStageStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing advanceFirmwareRolloutStageStmt: %w", cerr) + } + } if q.advanceFleetMetricRollupProgressStmt != nil { if cerr := q.advanceFleetMetricRollupProgressStmt.Close(); cerr != nil { err = fmt.Errorf("error closing advanceFleetMetricRollupProgressStmt: %w", cerr) @@ -2026,11 +2163,21 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing bumpCurtailmentTargetRetryStmt: %w", cerr) } } + if q.cancelActiveFirmwareRolloutStmt != nil { + if cerr := q.cancelActiveFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing cancelActiveFirmwareRolloutStmt: %w", cerr) + } + } if q.cancelEnrollmentForFleetNodeStmt != nil { if cerr := q.cancelEnrollmentForFleetNodeStmt.Close(); cerr != nil { err = fmt.Errorf("error closing cancelEnrollmentForFleetNodeStmt: %w", cerr) } } + if q.cancelFirmwareRolloutStmt != nil { + if cerr := q.cancelFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing cancelFirmwareRolloutStmt: %w", cerr) + } + } if q.cancelPendingEnrollmentStmt != nil { if cerr := q.cancelPendingEnrollmentStmt.Close(); cerr != nil { err = fmt.Errorf("error closing cancelPendingEnrollmentStmt: %w", cerr) @@ -2136,6 +2283,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing clearRackSlotPositionStmt: %w", cerr) } } + if q.clearReleaseChannelFirmwareStmt != nil { + if cerr := q.clearReleaseChannelFirmwareStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing clearReleaseChannelFirmwareStmt: %w", cerr) + } + } if q.clearRolePermissionsStmt != nil { if cerr := q.clearRolePermissionsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing clearRolePermissionsStmt: %w", cerr) @@ -2341,6 +2493,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing createDeviceSetStmt: %w", cerr) } } + if q.createFirmwareRolloutStmt != nil { + if cerr := q.createFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createFirmwareRolloutStmt: %w", cerr) + } + } if q.createFleetNodeStmt != nil { if cerr := q.createFleetNodeStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createFleetNodeStmt: %w", cerr) @@ -2391,6 +2548,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing createRackExtensionStmt: %w", cerr) } } + if q.createReleaseChannelStmt != nil { + if cerr := q.createReleaseChannelStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createReleaseChannelStmt: %w", cerr) + } + } if q.createRepairTicketStmt != nil { if cerr := q.createRepairTicketStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createRepairTicketStmt: %w", cerr) @@ -2526,6 +2688,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing deletePoolStmt: %w", cerr) } } + if q.deleteReleaseChannelStmt != nil { + if cerr := q.deleteReleaseChannelStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteReleaseChannelStmt: %w", cerr) + } + } + if q.deleteReleaseChannelTargetsStmt != nil { + if cerr := q.deleteReleaseChannelTargetsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteReleaseChannelTargetsStmt: %w", cerr) + } + } if q.deleteScheduleTargetsStmt != nil { if cerr := q.deleteScheduleTargetsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteScheduleTargetsStmt: %w", cerr) @@ -2566,6 +2738,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing ensureCurtailmentOrgConfigStmt: %w", cerr) } } + if q.excludeFirmwareRolloutDevicesStmt != nil { + if cerr := q.excludeFirmwareRolloutDevicesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing excludeFirmwareRolloutDevicesStmt: %w", cerr) + } + } if q.findDeviceBuildingConflictsStmt != nil { if cerr := q.findDeviceBuildingConflictsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing findDeviceBuildingConflictsStmt: %w", cerr) @@ -2591,6 +2768,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing findDevicesWithSiteOrBuildingStmt: %w", cerr) } } + if q.finishFirmwareRolloutStmt != nil { + if cerr := q.finishFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing finishFirmwareRolloutStmt: %w", cerr) + } + } if q.finishTerminalCommandBatchesStmt != nil { if cerr := q.finishTerminalCommandBatchesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing finishTerminalCommandBatchesStmt: %w", cerr) @@ -2601,6 +2783,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing forceReleaseCurtailmentEventStmt: %w", cerr) } } + if q.getActiveFirmwareRolloutForPairStmt != nil { + if cerr := q.getActiveFirmwareRolloutForPairStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getActiveFirmwareRolloutForPairStmt: %w", cerr) + } + } if q.getActiveSchedulesStmt != nil { if cerr := q.getActiveSchedulesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getActiveSchedulesStmt: %w", cerr) @@ -3011,6 +3198,21 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getFilteredTicketStatsStmt: %w", cerr) } } + if q.getFirmwareRolloutStmt != nil { + if cerr := q.getFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getFirmwareRolloutStmt: %w", cerr) + } + } + if q.getFirmwareRolloutForUpdateStmt != nil { + if cerr := q.getFirmwareRolloutForUpdateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getFirmwareRolloutForUpdateStmt: %w", cerr) + } + } + if q.getFirmwareRolloutWithChannelStmt != nil { + if cerr := q.getFirmwareRolloutWithChannelStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getFirmwareRolloutWithChannelStmt: %w", cerr) + } + } if q.getFleetMetricRollupCoverageStmt != nil { if cerr := q.getFleetMetricRollupCoverageStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getFleetMetricRollupCoverageStmt: %w", cerr) @@ -3091,6 +3293,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getLatestDeviceMetricsStmt: %w", cerr) } } + if q.getLatestFirmwareRolloutForPairStmt != nil { + if cerr := q.getLatestFirmwareRolloutForPairStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getLatestFirmwareRolloutForPairStmt: %w", cerr) + } + } if q.getLatestFleetMetricRollupBucketStmt != nil { if cerr := q.getLatestFleetMetricRollupBucketStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getLatestFleetMetricRollupBucketStmt: %w", cerr) @@ -3276,6 +3483,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getRackSlotsStmt: %w", cerr) } } + if q.getReleaseChannelStmt != nil { + if cerr := q.getReleaseChannelStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getReleaseChannelStmt: %w", cerr) + } + } + if q.getReleaseChannelFirmwareStmt != nil { + if cerr := q.getReleaseChannelFirmwareStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getReleaseChannelFirmwareStmt: %w", cerr) + } + } if q.getReleaseChannelSettingStmt != nil { if cerr := q.getReleaseChannelSettingStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getReleaseChannelSettingStmt: %w", cerr) @@ -3421,6 +3638,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getUsersForOrganizationStmt: %w", cerr) } } + if q.haltFirmwareRolloutDevicesStmt != nil { + if cerr := q.haltFirmwareRolloutDevicesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing haltFirmwareRolloutDevicesStmt: %w", cerr) + } + } if q.hasUserStmt != nil { if cerr := q.hasUserStmt.Close(); cerr != nil { err = fmt.Errorf("error closing hasUserStmt: %w", cerr) @@ -3496,6 +3718,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing insertNotificationMetricSamplesStmt: %w", cerr) } } + if q.insertReleaseChannelTargetsStmt != nil { + if cerr := q.insertReleaseChannelTargetsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertReleaseChannelTargetsStmt: %w", cerr) + } + } if q.insertRepairTicketPartStmt != nil { if cerr := q.insertRepairTicketPartStmt.Close(); cerr != nil { err = fmt.Errorf("error closing insertRepairTicketPartStmt: %w", cerr) @@ -3536,6 +3763,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing listActiveCurtailmentTargetDevicesByOrgStmt: %w", cerr) } } + if q.listActiveFirmwareRolloutsStmt != nil { + if cerr := q.listActiveFirmwareRolloutsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listActiveFirmwareRolloutsStmt: %w", cerr) + } + } if q.listActiveNotificationGroupsStmt != nil { if cerr := q.listActiveNotificationGroupsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listActiveNotificationGroupsStmt: %w", cerr) @@ -3701,6 +3933,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing listCustomRolesForOrgStmt: %w", cerr) } } + if q.listDeviceIDsByIdentifiersStmt != nil { + if cerr := q.listDeviceIDsByIdentifiersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listDeviceIDsByIdentifiersStmt: %w", cerr) + } + } if q.listDeviceSetMembersPaginatedStmt != nil { if cerr := q.listDeviceSetMembersPaginatedStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listDeviceSetMembersPaginatedStmt: %w", cerr) @@ -3761,6 +3998,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing listExistingDeviceIdentifiersStmt: %w", cerr) } } + if q.listFirmwareRolloutDevicesStmt != nil { + if cerr := q.listFirmwareRolloutDevicesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listFirmwareRolloutDevicesStmt: %w", cerr) + } + } + if q.listFirmwareRolloutsStmt != nil { + if cerr := q.listFirmwareRolloutsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listFirmwareRolloutsStmt: %w", cerr) + } + } if q.listFleetNodeDeviceIDsForRevocationStmt != nil { if cerr := q.listFleetNodeDeviceIDsForRevocationStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listFleetNodeDeviceIDsForRevocationStmt: %w", cerr) @@ -3876,6 +4123,56 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing listRecentlyResolvedCurtailedDevicesByScopeStmt: %w", cerr) } } + if q.listReleaseChannelFirmwareStmt != nil { + if cerr := q.listReleaseChannelFirmwareStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelFirmwareStmt: %w", cerr) + } + } + if q.listReleaseChannelFirmwareNeedingRolloutStmt != nil { + if cerr := q.listReleaseChannelFirmwareNeedingRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelFirmwareNeedingRolloutStmt: %w", cerr) + } + } + if q.listReleaseChannelMembersStmt != nil { + if cerr := q.listReleaseChannelMembersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelMembersStmt: %w", cerr) + } + } + if q.listReleaseChannelMembershipConflictsPageStmt != nil { + if cerr := q.listReleaseChannelMembershipConflictsPageStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelMembershipConflictsPageStmt: %w", cerr) + } + } + if q.listReleaseChannelMinersPageStmt != nil { + if cerr := q.listReleaseChannelMinersPageStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelMinersPageStmt: %w", cerr) + } + } + if q.listReleaseChannelMismatchedMembersStmt != nil { + if cerr := q.listReleaseChannelMismatchedMembersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelMismatchedMembersStmt: %w", cerr) + } + } + if q.listReleaseChannelModelGroupsPageStmt != nil { + if cerr := q.listReleaseChannelModelGroupsPageStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelModelGroupsPageStmt: %w", cerr) + } + } + if q.listReleaseChannelSuppressedMembersStmt != nil { + if cerr := q.listReleaseChannelSuppressedMembersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelSuppressedMembersStmt: %w", cerr) + } + } + if q.listReleaseChannelTargetsStmt != nil { + if cerr := q.listReleaseChannelTargetsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelTargetsStmt: %w", cerr) + } + } + if q.listReleaseChannelsStmt != nil { + if cerr := q.listReleaseChannelsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listReleaseChannelsStmt: %w", cerr) + } + } if q.listRepairTicketCommentsStmt != nil { if cerr := q.listRepairTicketCommentsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listRepairTicketCommentsStmt: %w", cerr) @@ -4116,6 +4413,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing lockRacksForReparentStmt: %w", cerr) } } + if q.lockReleaseChannelScopesStmt != nil { + if cerr := q.lockReleaseChannelScopesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing lockReleaseChannelScopesStmt: %w", cerr) + } + } if q.lockRepairTicketCommentCreateKeyStmt != nil { if cerr := q.lockRepairTicketCommentCreateKeyStmt.Close(); cerr != nil { err = fmt.Errorf("error closing lockRepairTicketCommentCreateKeyStmt: %w", cerr) @@ -4151,6 +4453,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing markCommandBatchProcessingStmt: %w", cerr) } } + if q.markFirmwareRolloutDevicesSentStmt != nil { + if cerr := q.markFirmwareRolloutDevicesSentStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing markFirmwareRolloutDevicesSentStmt: %w", cerr) + } + } if q.markRepairTicketPartsConsumedStmt != nil { if cerr := q.markRepairTicketPartsConsumedStmt.Close(); cerr != nil { err = fmt.Errorf("error closing markRepairTicketPartsConsumedStmt: %w", cerr) @@ -4181,6 +4488,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing pauseActiveScheduleStmt: %w", cerr) } } + if q.pauseFirmwareRolloutStmt != nil { + if cerr := q.pauseFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing pauseFirmwareRolloutStmt: %w", cerr) + } + } if q.pruneExpiredAlertMaintenanceWindowsStmt != nil { if cerr := q.pruneExpiredAlertMaintenanceWindowsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing pruneExpiredAlertMaintenanceWindowsStmt: %w", cerr) @@ -4246,6 +4558,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing recordCurtailPendingDispatchStmt: %w", cerr) } } + if q.recordFirmwareDeploymentStmt != nil { + if cerr := q.recordFirmwareDeploymentStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing recordFirmwareDeploymentStmt: %w", cerr) + } + } + if q.recordFirmwareRolloutActionStmt != nil { + if cerr := q.recordFirmwareRolloutActionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing recordFirmwareRolloutActionStmt: %w", cerr) + } + } if q.refreshOpenErrorsLastSeenByDeviceStmt != nil { if cerr := q.refreshOpenErrorsLastSeenByDeviceStmt.Close(); cerr != nil { err = fmt.Errorf("error closing refreshOpenErrorsLastSeenByDeviceStmt: %w", cerr) @@ -4286,6 +4608,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing requestRigConfigReconciliationStmt: %w", cerr) } } + if q.requeueFirmwareRolloutDevicesStmt != nil { + if cerr := q.requeueFirmwareRolloutDevicesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing requeueFirmwareRolloutDevicesStmt: %w", cerr) + } + } if q.requeueRigConfigReconciliationAfterTerminalFailureStmt != nil { if cerr := q.requeueRigConfigReconciliationAfterTerminalFailureStmt.Close(); cerr != nil { err = fmt.Errorf("error closing requeueRigConfigReconciliationAfterTerminalFailureStmt: %w", cerr) @@ -4336,11 +4663,21 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing resolveMaintenanceMinerContextStmt: %w", cerr) } } + if q.resolveReleaseChannelScopeStmt != nil { + if cerr := q.resolveReleaseChannelScopeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing resolveReleaseChannelScopeStmt: %w", cerr) + } + } if q.resumeCurtailmentFromRestoringStmt != nil { if cerr := q.resumeCurtailmentFromRestoringStmt.Close(); cerr != nil { err = fmt.Errorf("error closing resumeCurtailmentFromRestoringStmt: %w", cerr) } } + if q.resumeFirmwareRolloutStmt != nil { + if cerr := q.resumeFirmwareRolloutStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing resumeFirmwareRolloutStmt: %w", cerr) + } + } if q.resumePausedScheduleStmt != nil { if cerr := q.resumePausedScheduleStmt.Close(); cerr != nil { err = fmt.Errorf("error closing resumePausedScheduleStmt: %w", cerr) @@ -4466,6 +4803,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing sitesByIDsStmt: %w", cerr) } } + if q.snapshotFirmwareRolloutDevicesStmt != nil { + if cerr := q.snapshotFirmwareRolloutDevicesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing snapshotFirmwareRolloutDevicesStmt: %w", cerr) + } + } if q.softDeleteAlertChannelStmt != nil { if cerr := q.softDeleteAlertChannelStmt.Close(); cerr != nil { err = fmt.Errorf("error closing softDeleteAlertChannelStmt: %w", cerr) @@ -4841,6 +5183,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing updateRackPlacementBulkForSiteStmt: %w", cerr) } } + if q.updateReleaseChannelStmt != nil { + if cerr := q.updateReleaseChannelStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateReleaseChannelStmt: %w", cerr) + } + } if q.updateRepairTicketStmt != nil { if cerr := q.updateRepairTicketStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateRepairTicketStmt: %w", cerr) @@ -4976,6 +5323,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing upsertPermissionStmt: %w", cerr) } } + if q.upsertReleaseChannelFirmwareStmt != nil { + if cerr := q.upsertReleaseChannelFirmwareStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing upsertReleaseChannelFirmwareStmt: %w", cerr) + } + } if q.upsertReleaseChannelSettingStmt != nil { if cerr := q.upsertReleaseChannelSettingStmt.Close(); cerr != nil { err = fmt.Errorf("error closing upsertReleaseChannelSettingStmt: %w", cerr) @@ -5025,6 +5377,7 @@ type Queries struct { addDevicesToDeviceSetStmt *sql.Stmt adminResetUserPasswordStmt *sql.Stmt adminTerminateCurtailmentEventStmt *sql.Stmt + advanceFirmwareRolloutStageStmt *sql.Stmt advanceFleetMetricRollupProgressStmt *sql.Stmt allDevicesBelongToOrgStmt *sql.Stmt assignBuildingToSiteStmt *sql.Stmt @@ -5048,7 +5401,9 @@ type Queries struct { bulkRefreshAllPairedTargetReadinessStmt *sql.Stmt bulkUpdateTicketStatusStmt *sql.Stmt bumpCurtailmentTargetRetryStmt *sql.Stmt + cancelActiveFirmwareRolloutStmt *sql.Stmt cancelEnrollmentForFleetNodeStmt *sql.Stmt + cancelFirmwareRolloutStmt *sql.Stmt cancelPendingEnrollmentStmt *sql.Stmt cascadeAddedDeviceBuildingsStmt *sql.Stmt cascadeAddedDeviceSitesStmt *sql.Stmt @@ -5070,6 +5425,7 @@ type Queries struct { clearDeviceSitesAndBuildingsStmt *sql.Stmt clearRackPlacementForSoftDeleteStmt *sql.Stmt clearRackSlotPositionStmt *sql.Stmt + clearReleaseChannelFirmwareStmt *sql.Stmt clearRolePermissionsStmt *sql.Stmt closeStaleErrorsStmt *sql.Stmt completeRigConfigReconciliationStmt *sql.Stmt @@ -5111,6 +5467,7 @@ type Queries struct { createCommandBatchLogStmt *sql.Stmt createCustomRoleStmt *sql.Stmt createDeviceSetStmt *sql.Stmt + createFirmwareRolloutStmt *sql.Stmt createFleetNodeStmt *sql.Stmt createFleetNodeApiKeyStmt *sql.Stmt createInfrastructureDeviceStmt *sql.Stmt @@ -5121,6 +5478,7 @@ type Queries struct { createQueueMessageStmt *sql.Stmt createQueueMessagesStmt *sql.Stmt createRackExtensionStmt *sql.Stmt + createReleaseChannelStmt *sql.Stmt createRepairTicketStmt *sql.Stmt createRepairTicketCommentStmt *sql.Stmt createScheduleStmt *sql.Stmt @@ -5148,6 +5506,8 @@ type Queries struct { deleteOrganizationStmt *sql.Stmt deletePairingsForFleetNodeStmt *sql.Stmt deletePoolStmt *sql.Stmt + deleteReleaseChannelStmt *sql.Stmt + deleteReleaseChannelTargetsStmt *sql.Stmt deleteScheduleTargetsStmt *sql.Stmt deviceHasActiveCloudPairingStmt *sql.Stmt deviceHasActivePairingStmt *sql.Stmt @@ -5156,13 +5516,16 @@ type Queries struct { disableCurtailmentAutomationRuleByActiveEventStmt *sql.Stmt disableSyncCommitStmt *sql.Stmt ensureCurtailmentOrgConfigStmt *sql.Stmt + excludeFirmwareRolloutDevicesStmt *sql.Stmt findDeviceBuildingConflictsStmt *sql.Stmt findDeviceSiteConflictsStmt *sql.Stmt findDevicesInBuildingLessPlacedRacksStmt *sql.Stmt findDevicesInSiteLessRacksStmt *sql.Stmt findDevicesWithSiteOrBuildingStmt *sql.Stmt + finishFirmwareRolloutStmt *sql.Stmt finishTerminalCommandBatchesStmt *sql.Stmt forceReleaseCurtailmentEventStmt *sql.Stmt + getActiveFirmwareRolloutForPairStmt *sql.Stmt getActiveSchedulesStmt *sql.Stmt getActiveUnpairedDiscoveredDevicesStmt *sql.Stmt getAddedDeviceSiteConflictsStmt *sql.Stmt @@ -5245,6 +5608,9 @@ type Queries struct { getFilteredDeviceIdentifiersStmt *sql.Stmt getFilteredDeviceIdsStmt *sql.Stmt getFilteredTicketStatsStmt *sql.Stmt + getFirmwareRolloutStmt *sql.Stmt + getFirmwareRolloutForUpdateStmt *sql.Stmt + getFirmwareRolloutWithChannelStmt *sql.Stmt getFleetMetricRollupCoverageStmt *sql.Stmt getFleetNodeByIDStmt *sql.Stmt getFleetNodeByIDUnscopedStmt *sql.Stmt @@ -5261,6 +5627,7 @@ type Queries struct { getKnownSubnetsStmt *sql.Stmt getLatestAllDeviceMetricsStmt *sql.Stmt getLatestDeviceMetricsStmt *sql.Stmt + getLatestFirmwareRolloutForPairStmt *sql.Stmt getLatestFleetMetricRollupBucketStmt *sql.Stmt getMQTTSourceConfigByOrgStmt *sql.Stmt getMQTTSourceStateByIDStmt *sql.Stmt @@ -5298,6 +5665,8 @@ type Queries struct { getRackInfoStmt *sql.Stmt getRackInfoBatchStmt *sql.Stmt getRackSlotsStmt *sql.Stmt + getReleaseChannelStmt *sql.Stmt + getReleaseChannelFirmwareStmt *sql.Stmt getReleaseChannelSettingStmt *sql.Stmt getRepairTicketStmt *sql.Stmt getRepairTicketCommentByIdempotencyKeyStmt *sql.Stmt @@ -5327,6 +5696,7 @@ type Queries struct { getUserRoleNameStmt *sql.Stmt getUserRoleNameForUpdateStmt *sql.Stmt getUsersForOrganizationStmt *sql.Stmt + haltFirmwareRolloutDevicesStmt *sql.Stmt hasUserStmt *sql.Stmt insertActivityLogStmt *sql.Stmt insertAlertChannelStmt *sql.Stmt @@ -5342,6 +5712,7 @@ type Queries struct { insertMinerStateSnapshotStmt *sql.Stmt insertNotificationHistoryStmt *sql.Stmt insertNotificationMetricSamplesStmt *sql.Stmt + insertReleaseChannelTargetsStmt *sql.Stmt insertRepairTicketPartStmt *sql.Stmt inventoryPartExistsBySiteAndNameStmt *sql.Stmt isBatchFinishedStmt *sql.Stmt @@ -5350,6 +5721,7 @@ type Queries struct { listActiveCurtailedDevicesByOrgStmt *sql.Stmt listActiveCurtailmentEventsStmt *sql.Stmt listActiveCurtailmentTargetDevicesByOrgStmt *sql.Stmt + listActiveFirmwareRolloutsStmt *sql.Stmt listActiveNotificationGroupsStmt *sql.Stmt listActiveNotificationsStmt *sql.Stmt listActiveNotificationsByAlertStmt *sql.Stmt @@ -5383,6 +5755,7 @@ type Queries struct { listCurtailmentTargetsByEventPageStmt *sql.Stmt listCurtailmentTopologyMemberDeviceIdentifiersByOrgStmt *sql.Stmt listCustomRolesForOrgStmt *sql.Stmt + listDeviceIDsByIdentifiersStmt *sql.Stmt listDeviceSetMembersPaginatedStmt *sql.Stmt listDeviceSetMembersPaginatedAfterStmt *sql.Stmt listDeviceSetMembersPaginatedFilteredStmt *sql.Stmt @@ -5395,6 +5768,8 @@ type Queries struct { listEnabledCurtailmentAutomationRulesByMQTTSourceStmt *sql.Stmt listEnabledMQTTSourcesStmt *sql.Stmt listExistingDeviceIdentifiersStmt *sql.Stmt + listFirmwareRolloutDevicesStmt *sql.Stmt + listFirmwareRolloutsStmt *sql.Stmt listFleetNodeDeviceIDsForRevocationStmt *sql.Stmt listFleetNodeDevicesStmt *sql.Stmt listFleetNodeDiscoveredDevicesStmt *sql.Stmt @@ -5418,6 +5793,16 @@ type Queries struct { listRacksOutsideBuildingBoundsStmt *sql.Stmt listRecentlyResolvedCurtailedDevicesByOrgStmt *sql.Stmt listRecentlyResolvedCurtailedDevicesByScopeStmt *sql.Stmt + listReleaseChannelFirmwareStmt *sql.Stmt + listReleaseChannelFirmwareNeedingRolloutStmt *sql.Stmt + listReleaseChannelMembersStmt *sql.Stmt + listReleaseChannelMembershipConflictsPageStmt *sql.Stmt + listReleaseChannelMinersPageStmt *sql.Stmt + listReleaseChannelMismatchedMembersStmt *sql.Stmt + listReleaseChannelModelGroupsPageStmt *sql.Stmt + listReleaseChannelSuppressedMembersStmt *sql.Stmt + listReleaseChannelTargetsStmt *sql.Stmt + listReleaseChannelsStmt *sql.Stmt listRepairTicketCommentsStmt *sql.Stmt listRepairTicketPartsStmt *sql.Stmt listRepairTicketsStmt *sql.Stmt @@ -5466,6 +5851,7 @@ type Queries struct { lockMaintenanceSiteForTicketStmt *sql.Stmt lockRackPlacementForWriteStmt *sql.Stmt lockRacksForReparentStmt *sql.Stmt + lockReleaseChannelScopesStmt *sql.Stmt lockRepairTicketCommentCreateKeyStmt *sql.Stmt lockRepairTicketCreateKeyStmt *sql.Stmt lockSchedulePriorityStmt *sql.Stmt @@ -5473,12 +5859,14 @@ type Queries struct { markCommandBatchFinishedStmt *sql.Stmt markCommandBatchFinishedWithStartedAtStmt *sql.Stmt markCommandBatchProcessingStmt *sql.Stmt + markFirmwareRolloutDevicesSentStmt *sql.Stmt markRepairTicketPartsConsumedStmt *sql.Stmt negateSchedulePrioritiesStmt *sql.Stmt nextRepairTicketNumberStmt *sql.Stmt pairDeviceToFleetNodeStmt *sql.Stmt passwordUpdatedAtStmt *sql.Stmt pauseActiveScheduleStmt *sql.Stmt + pauseFirmwareRolloutStmt *sql.Stmt pruneExpiredAlertMaintenanceWindowsStmt *sql.Stmt prunePermissionsOutsideKeysStmt *sql.Stmt queryComponentKeysWithErrorsStmt *sql.Stmt @@ -5492,6 +5880,8 @@ type Queries struct { reconcileAuthenticationNeededPairingStatusByIdentifierStmt *sql.Stmt reconcileDefaultPasswordPairingStatusByIdentifierStmt *sql.Stmt recordCurtailPendingDispatchStmt *sql.Stmt + recordFirmwareDeploymentStmt *sql.Stmt + recordFirmwareRolloutActionStmt *sql.Stmt refreshOpenErrorsLastSeenByDeviceStmt *sql.Stmt releaseInventoryPartStmt *sql.Stmt releaseUndispatchedTargetsForRestoreStmt *sql.Stmt @@ -5500,6 +5890,7 @@ type Queries struct { removeDevicesFromDeviceSetStmt *sql.Stmt renewFleetRuntimeLeaseStmt *sql.Stmt requestRigConfigReconciliationStmt *sql.Stmt + requeueFirmwareRolloutDevicesStmt *sql.Stmt requeueRigConfigReconciliationAfterTerminalFailureStmt *sql.Stmt reserveInventoryPartStmt *sql.Stmt resetCurtailmentTargetsForRecurtailStmt *sql.Stmt @@ -5510,7 +5901,9 @@ type Queries struct { resolveMaintenanceAssigneeStmt *sql.Stmt resolveMaintenanceLocationContextStmt *sql.Stmt resolveMaintenanceMinerContextStmt *sql.Stmt + resolveReleaseChannelScopeStmt *sql.Stmt resumeCurtailmentFromRestoringStmt *sql.Stmt + resumeFirmwareRolloutStmt *sql.Stmt resumePausedScheduleStmt *sql.Stmt retryRigConfigReconciliationStmt *sql.Stmt revertScheduleToActiveStmt *sql.Stmt @@ -5536,6 +5929,7 @@ type Queries struct { setScheduleRunningStmt *sql.Stmt siteBelongsToOrgStmt *sql.Stmt sitesByIDsStmt *sql.Stmt + snapshotFirmwareRolloutDevicesStmt *sql.Stmt softDeleteAlertChannelStmt *sql.Stmt softDeleteBuildingStmt *sql.Stmt softDeleteBuildingsBySiteStmt *sql.Stmt @@ -5611,6 +6005,7 @@ type Queries struct { updateRackPlacementStmt *sql.Stmt updateRackPlacementBulkForBuildingStmt *sql.Stmt updateRackPlacementBulkForSiteStmt *sql.Stmt + updateReleaseChannelStmt *sql.Stmt updateRepairTicketStmt *sql.Stmt updateRoleStmt *sql.Stmt updateScheduleStmt *sql.Stmt @@ -5638,6 +6033,7 @@ type Queries struct { upsertMQTTSourceStateStmt *sql.Stmt upsertMinerCredentialsStmt *sql.Stmt upsertPermissionStmt *sql.Stmt + upsertReleaseChannelFirmwareStmt *sql.Stmt upsertReleaseChannelSettingStmt *sql.Stmt } @@ -5650,6 +6046,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { addDevicesToDeviceSetStmt: q.addDevicesToDeviceSetStmt, adminResetUserPasswordStmt: q.adminResetUserPasswordStmt, adminTerminateCurtailmentEventStmt: q.adminTerminateCurtailmentEventStmt, + advanceFirmwareRolloutStageStmt: q.advanceFirmwareRolloutStageStmt, advanceFleetMetricRollupProgressStmt: q.advanceFleetMetricRollupProgressStmt, allDevicesBelongToOrgStmt: q.allDevicesBelongToOrgStmt, assignBuildingToSiteStmt: q.assignBuildingToSiteStmt, @@ -5673,7 +6070,9 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { bulkRefreshAllPairedTargetReadinessStmt: q.bulkRefreshAllPairedTargetReadinessStmt, bulkUpdateTicketStatusStmt: q.bulkUpdateTicketStatusStmt, bumpCurtailmentTargetRetryStmt: q.bumpCurtailmentTargetRetryStmt, + cancelActiveFirmwareRolloutStmt: q.cancelActiveFirmwareRolloutStmt, cancelEnrollmentForFleetNodeStmt: q.cancelEnrollmentForFleetNodeStmt, + cancelFirmwareRolloutStmt: q.cancelFirmwareRolloutStmt, cancelPendingEnrollmentStmt: q.cancelPendingEnrollmentStmt, cascadeAddedDeviceBuildingsStmt: q.cascadeAddedDeviceBuildingsStmt, cascadeAddedDeviceSitesStmt: q.cascadeAddedDeviceSitesStmt, @@ -5695,6 +6094,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { clearDeviceSitesAndBuildingsStmt: q.clearDeviceSitesAndBuildingsStmt, clearRackPlacementForSoftDeleteStmt: q.clearRackPlacementForSoftDeleteStmt, clearRackSlotPositionStmt: q.clearRackSlotPositionStmt, + clearReleaseChannelFirmwareStmt: q.clearReleaseChannelFirmwareStmt, clearRolePermissionsStmt: q.clearRolePermissionsStmt, closeStaleErrorsStmt: q.closeStaleErrorsStmt, completeRigConfigReconciliationStmt: q.completeRigConfigReconciliationStmt, @@ -5736,6 +6136,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { createCommandBatchLogStmt: q.createCommandBatchLogStmt, createCustomRoleStmt: q.createCustomRoleStmt, createDeviceSetStmt: q.createDeviceSetStmt, + createFirmwareRolloutStmt: q.createFirmwareRolloutStmt, createFleetNodeStmt: q.createFleetNodeStmt, createFleetNodeApiKeyStmt: q.createFleetNodeApiKeyStmt, createInfrastructureDeviceStmt: q.createInfrastructureDeviceStmt, @@ -5746,6 +6147,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { createQueueMessageStmt: q.createQueueMessageStmt, createQueueMessagesStmt: q.createQueueMessagesStmt, createRackExtensionStmt: q.createRackExtensionStmt, + createReleaseChannelStmt: q.createReleaseChannelStmt, createRepairTicketStmt: q.createRepairTicketStmt, createRepairTicketCommentStmt: q.createRepairTicketCommentStmt, createScheduleStmt: q.createScheduleStmt, @@ -5773,6 +6175,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { deleteOrganizationStmt: q.deleteOrganizationStmt, deletePairingsForFleetNodeStmt: q.deletePairingsForFleetNodeStmt, deletePoolStmt: q.deletePoolStmt, + deleteReleaseChannelStmt: q.deleteReleaseChannelStmt, + deleteReleaseChannelTargetsStmt: q.deleteReleaseChannelTargetsStmt, deleteScheduleTargetsStmt: q.deleteScheduleTargetsStmt, deviceHasActiveCloudPairingStmt: q.deviceHasActiveCloudPairingStmt, deviceHasActivePairingStmt: q.deviceHasActivePairingStmt, @@ -5781,13 +6185,16 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { disableCurtailmentAutomationRuleByActiveEventStmt: q.disableCurtailmentAutomationRuleByActiveEventStmt, disableSyncCommitStmt: q.disableSyncCommitStmt, ensureCurtailmentOrgConfigStmt: q.ensureCurtailmentOrgConfigStmt, + excludeFirmwareRolloutDevicesStmt: q.excludeFirmwareRolloutDevicesStmt, findDeviceBuildingConflictsStmt: q.findDeviceBuildingConflictsStmt, findDeviceSiteConflictsStmt: q.findDeviceSiteConflictsStmt, findDevicesInBuildingLessPlacedRacksStmt: q.findDevicesInBuildingLessPlacedRacksStmt, findDevicesInSiteLessRacksStmt: q.findDevicesInSiteLessRacksStmt, findDevicesWithSiteOrBuildingStmt: q.findDevicesWithSiteOrBuildingStmt, + finishFirmwareRolloutStmt: q.finishFirmwareRolloutStmt, finishTerminalCommandBatchesStmt: q.finishTerminalCommandBatchesStmt, forceReleaseCurtailmentEventStmt: q.forceReleaseCurtailmentEventStmt, + getActiveFirmwareRolloutForPairStmt: q.getActiveFirmwareRolloutForPairStmt, getActiveSchedulesStmt: q.getActiveSchedulesStmt, getActiveUnpairedDiscoveredDevicesStmt: q.getActiveUnpairedDiscoveredDevicesStmt, getAddedDeviceSiteConflictsStmt: q.getAddedDeviceSiteConflictsStmt, @@ -5870,6 +6277,9 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getFilteredDeviceIdentifiersStmt: q.getFilteredDeviceIdentifiersStmt, getFilteredDeviceIdsStmt: q.getFilteredDeviceIdsStmt, getFilteredTicketStatsStmt: q.getFilteredTicketStatsStmt, + getFirmwareRolloutStmt: q.getFirmwareRolloutStmt, + getFirmwareRolloutForUpdateStmt: q.getFirmwareRolloutForUpdateStmt, + getFirmwareRolloutWithChannelStmt: q.getFirmwareRolloutWithChannelStmt, getFleetMetricRollupCoverageStmt: q.getFleetMetricRollupCoverageStmt, getFleetNodeByIDStmt: q.getFleetNodeByIDStmt, getFleetNodeByIDUnscopedStmt: q.getFleetNodeByIDUnscopedStmt, @@ -5886,6 +6296,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getKnownSubnetsStmt: q.getKnownSubnetsStmt, getLatestAllDeviceMetricsStmt: q.getLatestAllDeviceMetricsStmt, getLatestDeviceMetricsStmt: q.getLatestDeviceMetricsStmt, + getLatestFirmwareRolloutForPairStmt: q.getLatestFirmwareRolloutForPairStmt, getLatestFleetMetricRollupBucketStmt: q.getLatestFleetMetricRollupBucketStmt, getMQTTSourceConfigByOrgStmt: q.getMQTTSourceConfigByOrgStmt, getMQTTSourceStateByIDStmt: q.getMQTTSourceStateByIDStmt, @@ -5923,6 +6334,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getRackInfoStmt: q.getRackInfoStmt, getRackInfoBatchStmt: q.getRackInfoBatchStmt, getRackSlotsStmt: q.getRackSlotsStmt, + getReleaseChannelStmt: q.getReleaseChannelStmt, + getReleaseChannelFirmwareStmt: q.getReleaseChannelFirmwareStmt, getReleaseChannelSettingStmt: q.getReleaseChannelSettingStmt, getRepairTicketStmt: q.getRepairTicketStmt, getRepairTicketCommentByIdempotencyKeyStmt: q.getRepairTicketCommentByIdempotencyKeyStmt, @@ -5952,6 +6365,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getUserRoleNameStmt: q.getUserRoleNameStmt, getUserRoleNameForUpdateStmt: q.getUserRoleNameForUpdateStmt, getUsersForOrganizationStmt: q.getUsersForOrganizationStmt, + haltFirmwareRolloutDevicesStmt: q.haltFirmwareRolloutDevicesStmt, hasUserStmt: q.hasUserStmt, insertActivityLogStmt: q.insertActivityLogStmt, insertAlertChannelStmt: q.insertAlertChannelStmt, @@ -5967,6 +6381,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { insertMinerStateSnapshotStmt: q.insertMinerStateSnapshotStmt, insertNotificationHistoryStmt: q.insertNotificationHistoryStmt, insertNotificationMetricSamplesStmt: q.insertNotificationMetricSamplesStmt, + insertReleaseChannelTargetsStmt: q.insertReleaseChannelTargetsStmt, insertRepairTicketPartStmt: q.insertRepairTicketPartStmt, inventoryPartExistsBySiteAndNameStmt: q.inventoryPartExistsBySiteAndNameStmt, isBatchFinishedStmt: q.isBatchFinishedStmt, @@ -5975,6 +6390,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { listActiveCurtailedDevicesByOrgStmt: q.listActiveCurtailedDevicesByOrgStmt, listActiveCurtailmentEventsStmt: q.listActiveCurtailmentEventsStmt, listActiveCurtailmentTargetDevicesByOrgStmt: q.listActiveCurtailmentTargetDevicesByOrgStmt, + listActiveFirmwareRolloutsStmt: q.listActiveFirmwareRolloutsStmt, listActiveNotificationGroupsStmt: q.listActiveNotificationGroupsStmt, listActiveNotificationsStmt: q.listActiveNotificationsStmt, listActiveNotificationsByAlertStmt: q.listActiveNotificationsByAlertStmt, @@ -6008,6 +6424,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { listCurtailmentTargetsByEventPageStmt: q.listCurtailmentTargetsByEventPageStmt, listCurtailmentTopologyMemberDeviceIdentifiersByOrgStmt: q.listCurtailmentTopologyMemberDeviceIdentifiersByOrgStmt, listCustomRolesForOrgStmt: q.listCustomRolesForOrgStmt, + listDeviceIDsByIdentifiersStmt: q.listDeviceIDsByIdentifiersStmt, listDeviceSetMembersPaginatedStmt: q.listDeviceSetMembersPaginatedStmt, listDeviceSetMembersPaginatedAfterStmt: q.listDeviceSetMembersPaginatedAfterStmt, listDeviceSetMembersPaginatedFilteredStmt: q.listDeviceSetMembersPaginatedFilteredStmt, @@ -6020,6 +6437,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { listEnabledCurtailmentAutomationRulesByMQTTSourceStmt: q.listEnabledCurtailmentAutomationRulesByMQTTSourceStmt, listEnabledMQTTSourcesStmt: q.listEnabledMQTTSourcesStmt, listExistingDeviceIdentifiersStmt: q.listExistingDeviceIdentifiersStmt, + listFirmwareRolloutDevicesStmt: q.listFirmwareRolloutDevicesStmt, + listFirmwareRolloutsStmt: q.listFirmwareRolloutsStmt, listFleetNodeDeviceIDsForRevocationStmt: q.listFleetNodeDeviceIDsForRevocationStmt, listFleetNodeDevicesStmt: q.listFleetNodeDevicesStmt, listFleetNodeDiscoveredDevicesStmt: q.listFleetNodeDiscoveredDevicesStmt, @@ -6043,6 +6462,16 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { listRacksOutsideBuildingBoundsStmt: q.listRacksOutsideBuildingBoundsStmt, listRecentlyResolvedCurtailedDevicesByOrgStmt: q.listRecentlyResolvedCurtailedDevicesByOrgStmt, listRecentlyResolvedCurtailedDevicesByScopeStmt: q.listRecentlyResolvedCurtailedDevicesByScopeStmt, + listReleaseChannelFirmwareStmt: q.listReleaseChannelFirmwareStmt, + listReleaseChannelFirmwareNeedingRolloutStmt: q.listReleaseChannelFirmwareNeedingRolloutStmt, + listReleaseChannelMembersStmt: q.listReleaseChannelMembersStmt, + listReleaseChannelMembershipConflictsPageStmt: q.listReleaseChannelMembershipConflictsPageStmt, + listReleaseChannelMinersPageStmt: q.listReleaseChannelMinersPageStmt, + listReleaseChannelMismatchedMembersStmt: q.listReleaseChannelMismatchedMembersStmt, + listReleaseChannelModelGroupsPageStmt: q.listReleaseChannelModelGroupsPageStmt, + listReleaseChannelSuppressedMembersStmt: q.listReleaseChannelSuppressedMembersStmt, + listReleaseChannelTargetsStmt: q.listReleaseChannelTargetsStmt, + listReleaseChannelsStmt: q.listReleaseChannelsStmt, listRepairTicketCommentsStmt: q.listRepairTicketCommentsStmt, listRepairTicketPartsStmt: q.listRepairTicketPartsStmt, listRepairTicketsStmt: q.listRepairTicketsStmt, @@ -6091,6 +6520,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { lockMaintenanceSiteForTicketStmt: q.lockMaintenanceSiteForTicketStmt, lockRackPlacementForWriteStmt: q.lockRackPlacementForWriteStmt, lockRacksForReparentStmt: q.lockRacksForReparentStmt, + lockReleaseChannelScopesStmt: q.lockReleaseChannelScopesStmt, lockRepairTicketCommentCreateKeyStmt: q.lockRepairTicketCommentCreateKeyStmt, lockRepairTicketCreateKeyStmt: q.lockRepairTicketCreateKeyStmt, lockSchedulePriorityStmt: q.lockSchedulePriorityStmt, @@ -6098,12 +6528,14 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { markCommandBatchFinishedStmt: q.markCommandBatchFinishedStmt, markCommandBatchFinishedWithStartedAtStmt: q.markCommandBatchFinishedWithStartedAtStmt, markCommandBatchProcessingStmt: q.markCommandBatchProcessingStmt, + markFirmwareRolloutDevicesSentStmt: q.markFirmwareRolloutDevicesSentStmt, markRepairTicketPartsConsumedStmt: q.markRepairTicketPartsConsumedStmt, negateSchedulePrioritiesStmt: q.negateSchedulePrioritiesStmt, nextRepairTicketNumberStmt: q.nextRepairTicketNumberStmt, pairDeviceToFleetNodeStmt: q.pairDeviceToFleetNodeStmt, passwordUpdatedAtStmt: q.passwordUpdatedAtStmt, pauseActiveScheduleStmt: q.pauseActiveScheduleStmt, + pauseFirmwareRolloutStmt: q.pauseFirmwareRolloutStmt, pruneExpiredAlertMaintenanceWindowsStmt: q.pruneExpiredAlertMaintenanceWindowsStmt, prunePermissionsOutsideKeysStmt: q.prunePermissionsOutsideKeysStmt, queryComponentKeysWithErrorsStmt: q.queryComponentKeysWithErrorsStmt, @@ -6117,6 +6549,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { reconcileAuthenticationNeededPairingStatusByIdentifierStmt: q.reconcileAuthenticationNeededPairingStatusByIdentifierStmt, reconcileDefaultPasswordPairingStatusByIdentifierStmt: q.reconcileDefaultPasswordPairingStatusByIdentifierStmt, recordCurtailPendingDispatchStmt: q.recordCurtailPendingDispatchStmt, + recordFirmwareDeploymentStmt: q.recordFirmwareDeploymentStmt, + recordFirmwareRolloutActionStmt: q.recordFirmwareRolloutActionStmt, refreshOpenErrorsLastSeenByDeviceStmt: q.refreshOpenErrorsLastSeenByDeviceStmt, releaseInventoryPartStmt: q.releaseInventoryPartStmt, releaseUndispatchedTargetsForRestoreStmt: q.releaseUndispatchedTargetsForRestoreStmt, @@ -6125,6 +6559,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { removeDevicesFromDeviceSetStmt: q.removeDevicesFromDeviceSetStmt, renewFleetRuntimeLeaseStmt: q.renewFleetRuntimeLeaseStmt, requestRigConfigReconciliationStmt: q.requestRigConfigReconciliationStmt, + requeueFirmwareRolloutDevicesStmt: q.requeueFirmwareRolloutDevicesStmt, requeueRigConfigReconciliationAfterTerminalFailureStmt: q.requeueRigConfigReconciliationAfterTerminalFailureStmt, reserveInventoryPartStmt: q.reserveInventoryPartStmt, resetCurtailmentTargetsForRecurtailStmt: q.resetCurtailmentTargetsForRecurtailStmt, @@ -6135,7 +6570,9 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { resolveMaintenanceAssigneeStmt: q.resolveMaintenanceAssigneeStmt, resolveMaintenanceLocationContextStmt: q.resolveMaintenanceLocationContextStmt, resolveMaintenanceMinerContextStmt: q.resolveMaintenanceMinerContextStmt, + resolveReleaseChannelScopeStmt: q.resolveReleaseChannelScopeStmt, resumeCurtailmentFromRestoringStmt: q.resumeCurtailmentFromRestoringStmt, + resumeFirmwareRolloutStmt: q.resumeFirmwareRolloutStmt, resumePausedScheduleStmt: q.resumePausedScheduleStmt, retryRigConfigReconciliationStmt: q.retryRigConfigReconciliationStmt, revertScheduleToActiveStmt: q.revertScheduleToActiveStmt, @@ -6161,6 +6598,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { setScheduleRunningStmt: q.setScheduleRunningStmt, siteBelongsToOrgStmt: q.siteBelongsToOrgStmt, sitesByIDsStmt: q.sitesByIDsStmt, + snapshotFirmwareRolloutDevicesStmt: q.snapshotFirmwareRolloutDevicesStmt, softDeleteAlertChannelStmt: q.softDeleteAlertChannelStmt, softDeleteBuildingStmt: q.softDeleteBuildingStmt, softDeleteBuildingsBySiteStmt: q.softDeleteBuildingsBySiteStmt, @@ -6236,6 +6674,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { updateRackPlacementStmt: q.updateRackPlacementStmt, updateRackPlacementBulkForBuildingStmt: q.updateRackPlacementBulkForBuildingStmt, updateRackPlacementBulkForSiteStmt: q.updateRackPlacementBulkForSiteStmt, + updateReleaseChannelStmt: q.updateReleaseChannelStmt, updateRepairTicketStmt: q.updateRepairTicketStmt, updateRoleStmt: q.updateRoleStmt, updateScheduleStmt: q.updateScheduleStmt, @@ -6263,6 +6702,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { upsertMQTTSourceStateStmt: q.upsertMQTTSourceStateStmt, upsertMinerCredentialsStmt: q.upsertMinerCredentialsStmt, upsertPermissionStmt: q.upsertPermissionStmt, + upsertReleaseChannelFirmwareStmt: q.upsertReleaseChannelFirmwareStmt, upsertReleaseChannelSettingStmt: q.upsertReleaseChannelSettingStmt, } } diff --git a/server/generated/sqlc/models.go b/server/generated/sqlc/models.go index b729cc88a7..f2d3202b76 100644 --- a/server/generated/sqlc/models.go +++ b/server/generated/sqlc/models.go @@ -730,6 +730,14 @@ type Device struct { BuildingID sql.NullInt64 } +type DeviceFirmwareDeployment struct { + DeviceID int64 + FirmwareChecksum string + FirmwareVersion string + RolloutID sql.NullInt64 + DeployedAt time.Time +} + type DeviceMetric struct { Time time.Time DeviceIdentifier string @@ -928,6 +936,74 @@ type Error struct { SiteID sql.NullInt64 } +type FirmwareRollout struct { + ID int64 + OrgID int64 + ChannelID int64 + Manufacturer string + Model string + FirmwareChecksum string + FirmwareVersion string + PreviousFirmwareChecksum string + PreviousFirmwareVersion string + AssignmentGeneration int64 + Status string + CancelReason string + Stage string + Method string + OrderBy string + BatchSize int32 + PilotSize int32 + WaitBetweenBatchesSeconds int32 + ReviewAfterEachBatch bool + AutoContinue bool + StabilizationSeconds int32 + MaxHashrateDropPercent sql.NullFloat64 + MaxEfficiencyIncreasePercent sql.NullFloat64 + MaxTempIncreaseC sql.NullFloat64 + MaxNewErrors sql.NullInt32 + MinSampleCoveragePercent sql.NullFloat64 + MaxConcurrentOffline int32 + ControllerTimeoutSeconds int32 + BatchCount int32 + CurrentBatch int32 + StageChangedAt time.Time + PausedAt sql.NullTime + Revision int64 + UpdatedAt time.Time + StartedByType string + StartedByID int64 + StartedByName string + LastActionByType string + LastActionByID int64 + LastActionByName string + CreatedAt time.Time + FinishedAt sql.NullTime +} + +type FirmwareRolloutDevice struct { + RolloutID int64 + DeviceID int64 + BatchIndex sql.NullInt32 + Position sql.NullInt32 + Attempts int32 + FirstSentAt sql.NullTime + LastSentAt sql.NullTime + HaltedAt sql.NullTime + HaltReason string + LastError string + SkipNote string + ExcludedAt sql.NullTime + BaselineStatus sql.NullString + BaselineHashRateHs sql.NullFloat64 + BaselinePowerW sql.NullFloat64 + BaselineEfficiencyJh sql.NullFloat64 + BaselineTempC sql.NullFloat64 + BaselineOpenErrors sql.NullInt32 + BaselineAt sql.NullTime + AddedAt time.Time +} + type FleetActiveOrganization struct { OrganizationID string } @@ -1221,12 +1297,78 @@ type RackSlot struct { CreatedAt time.Time } +type ReleaseChannel struct { + ID int64 + OrgID int64 + Name string + Description string + Method string + OrderBy string + BatchSize int32 + PilotSize int32 + WaitBetweenBatchesSeconds int32 + ReviewAfterEachBatch bool + AutoContinue bool + StabilizationSeconds int32 + MaxHashrateDropPercent sql.NullFloat64 + MaxEfficiencyIncreasePercent sql.NullFloat64 + MaxTempIncreaseC sql.NullFloat64 + MaxNewErrors sql.NullInt32 + MinSampleCoveragePercent sql.NullFloat64 + MaxConcurrentOffline int32 + ControllerTimeoutSeconds int32 + CreatedBy int64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type ReleaseChannelConflict struct { + ChannelID int64 + OrgID int64 + DeviceID int64 + Specificity interface{} + Resolution string +} + +type ReleaseChannelFirmware struct { + ChannelID int64 + Manufacturer string + Model string + FirmwareChecksum string + FirmwareVersion string + FirmwareTargetManufacturer string + FirmwareTargetModel string + AssignmentGeneration int64 + AssignedBy int64 + UpdatedAt time.Time +} + +type ReleaseChannelMatch struct { + ChannelID int64 + OrgID int64 + DeviceID int64 + Specificity int32 +} + +type ReleaseChannelMember struct { + ChannelID int64 + OrgID int64 + DeviceID int64 + Conflicted bool +} + type ReleaseChannelSetting struct { OrganizationID int64 Channel string UpdatedAt time.Time } +type ReleaseChannelTarget struct { + ChannelID int64 + TargetType string + TargetID int64 +} + type RepairTicket struct { ID int64 OrgID int64 diff --git a/server/generated/sqlc/querier.go b/server/generated/sqlc/querier.go index f31861c465..6f67ec6b55 100644 --- a/server/generated/sqlc/querier.go +++ b/server/generated/sqlc/querier.go @@ -33,6 +33,9 @@ type Querier interface { // forced termination. Zero-row return lets the caller route active, // in-flight, and already-terminal cases. AdminTerminateCurtailmentEvent(ctx context.Context, arg AdminTerminateCurtailmentEventParams) (CurtailmentEvent, error) + // Stage transitions of an active rollout, attributed to an actor when one + // drove them. Returns the affected row count so callers can detect a lost race. + AdvanceFirmwareRolloutStage(ctx context.Context, arg AdvanceFirmwareRolloutStageParams) (int64, error) AdvanceFleetMetricRollupProgress(ctx context.Context, arg AdvanceFleetMetricRollupProgressParams) error // Returns true if all provided device identifiers belong to the specified organization. // Used for authorization checks - fails fast if any device is not owned by the org. @@ -135,7 +138,11 @@ type Querier interface { // still lands on the next successful state-change write. EXISTS guard // → zero rows → ErrCurtailmentEventStateRaceLoss on terminal parent. BumpCurtailmentTargetRetry(ctx context.Context, arg BumpCurtailmentTargetRetryParams) (int64, error) + // Cancels the pair's active rollout because its assignment changed: + // 'superseded', 'rolled_back' or 'cleared'. + CancelActiveFirmwareRollout(ctx context.Context, arg CancelActiveFirmwareRolloutParams) error CancelEnrollmentForFleetNode(ctx context.Context, arg CancelEnrollmentForFleetNodeParams) (int64, error) + CancelFirmwareRollout(ctx context.Context, arg CancelFirmwareRolloutParams) (int64, error) CancelPendingEnrollment(ctx context.Context, arg CancelPendingEnrollmentParams) (int64, error) // Building peer of CascadeAddedDeviceSites. Rewrites device.building_id // to rack.building_id for added rack members whose current building @@ -237,6 +244,9 @@ type Querier interface { // same transaction; non-rack collection types simply match 0 rows. ClearRackPlacementForSoftDelete(ctx context.Context, arg ClearRackPlacementForSoftDeleteParams) error ClearRackSlotPosition(ctx context.Context, arg ClearRackSlotPositionParams) error + // Clears an assigned pair, advancing its generation; the row stays so the + // generation survives. Returns nothing when the pair was not assigned. + ClearReleaseChannelFirmware(ctx context.Context, arg ClearReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) // Wholesale removal. Used by the role-edit handler when replacing a // role's full permission set in a single transaction (delete-then- // insert inside one tx so there is no zero-permission window). @@ -333,6 +343,8 @@ type Querier interface { CreateCommandBatchLog(ctx context.Context, arg CreateCommandBatchLogParams) (sql.Result, error) CreateCustomRole(ctx context.Context, arg CreateCustomRoleParams) (Role, error) CreateDeviceSet(ctx context.Context, arg CreateDeviceSetParams) (CreateDeviceSetRow, error) + // --- Rollouts --- + CreateFirmwareRollout(ctx context.Context, arg CreateFirmwareRolloutParams) (FirmwareRollout, error) CreateFleetNode(ctx context.Context, arg CreateFleetNodeParams) (CreateFleetNodeRow, error) CreateFleetNodeApiKey(ctx context.Context, arg CreateFleetNodeApiKeyParams) error // Name is unique per (site_id, name) among live rows; the partial @@ -349,6 +361,13 @@ type Querier interface { // composite-keyed; inherit it from device_set so the caller's org_id // must match. site_id / building_id are NULL for unassigned racks. CreateRackExtension(ctx context.Context, arg CreateRackExtensionParams) error + // Firmware release channels and the rollouts that enforce them. + // + // Pair keys (manufacturer, model) are compared ASCII-case-insensitively with + // lower(x COLLATE "C"), which folds A-Z only; observed device identities are + // trimmed before folding. Firmware is identified by payload checksum. + // --- Channels --- + CreateReleaseChannel(ctx context.Context, arg CreateReleaseChannelParams) (ReleaseChannel, error) CreateRepairTicket(ctx context.Context, arg CreateRepairTicketParams) (int64, error) CreateRepairTicketComment(ctx context.Context, arg CreateRepairTicketCommentParams) (CreateRepairTicketCommentRow, error) CreateSchedule(ctx context.Context, arg CreateScheduleParams) (int64, error) @@ -392,6 +411,8 @@ type Querier interface { // Revoke soft-deletes the fleet_node row, so ON DELETE CASCADE doesn't fire. DeletePairingsForFleetNode(ctx context.Context, arg DeletePairingsForFleetNodeParams) (int64, error) DeletePool(ctx context.Context, id int64) error + DeleteReleaseChannel(ctx context.Context, arg DeleteReleaseChannelParams) (int64, error) + DeleteReleaseChannelTargets(ctx context.Context, channelID int64) error DeleteScheduleTargets(ctx context.Context, arg DeleteScheduleTargetsParams) error // True when the device is cloud-dialed: paired-like and not bound to any fleet node. // A device paired to a fleet node is also paired-like (so it reads as paired in @@ -414,6 +435,7 @@ type Querier interface { // Idempotent backfill (INSERT ... DO NOTHING + fallback SELECT). Both // branches require organization.deleted_at IS NULL. EnsureCurtailmentOrgConfig(ctx context.Context, orgID int64) (EnsureCurtailmentOrgConfigRow, error) + ExcludeFirmwareRolloutDevices(ctx context.Context, arg ExcludeFirmwareRolloutDevicesParams) error // Returns one row per device whose live rack has a non-NULL // building_id. Devices with no rack, devices in a rack without a // building, and devices in soft-deleted racks produce NO row at all @@ -458,12 +480,15 @@ type Querier interface { // miner with only a direct building (site NULL, building set, e.g. one // assigned to a site-less building) must trip the confirm too. FindDevicesWithSiteOrBuilding(ctx context.Context, arg FindDevicesWithSiteOrBuildingParams) ([]string, error) + // Ends an active rollout as 'completed' or 'completed_with_failures'. + FinishFirmwareRollout(ctx context.Context, arg FinishFirmwareRolloutParams) (int64, error) FinishTerminalCommandBatches(ctx context.Context, finishLimit int32) (int64, error) // Last-resort recovery: persistently releases curtailment ownership for any // non-terminal event row. Unlike AdminTerminateCurtailmentEvent, this // intentionally supports ACTIVE events and has no in-flight command gate because // the operator intent is to clear policy ownership, not report graceful restore. ForceReleaseCurtailmentEvent(ctx context.Context, arg ForceReleaseCurtailmentEventParams) (CurtailmentEvent, error) + GetActiveFirmwareRolloutForPair(ctx context.Context, arg GetActiveFirmwareRolloutForPairParams) (FirmwareRollout, error) GetActiveSchedules(ctx context.Context) ([]Schedule, error) // Excludes remote-fleet-node-reported rows: server-local pairing // dials these IPs, agent-reported rows route via PairDeviceToFleetNode. @@ -635,6 +660,11 @@ type Querier interface { // Used for bulk command operations. GetFilteredDeviceIds(ctx context.Context, arg GetFilteredDeviceIdsParams) ([]int64, error) GetFilteredTicketStats(ctx context.Context, arg GetFilteredTicketStatsParams) (GetFilteredTicketStatsRow, error) + GetFirmwareRollout(ctx context.Context, arg GetFirmwareRolloutParams) (FirmwareRollout, error) + // Locks the row for a mutation so the revision rule can be checked and the + // change applied without a concurrent actor slipping in between. + GetFirmwareRolloutForUpdate(ctx context.Context, arg GetFirmwareRolloutForUpdateParams) (FirmwareRollout, error) + GetFirmwareRolloutWithChannel(ctx context.Context, arg GetFirmwareRolloutWithChannelParams) (GetFirmwareRolloutWithChannelRow, error) GetFleetMetricRollupCoverage(ctx context.Context) (GetFleetMetricRollupCoverageRow, error) GetFleetNodeByID(ctx context.Context, arg GetFleetNodeByIDParams) (GetFleetNodeByIDRow, error) GetFleetNodeByIDUnscoped(ctx context.Context, id int64) (GetFleetNodeByIDUnscopedRow, error) @@ -659,6 +689,9 @@ type Querier interface { GetKnownSubnets(ctx context.Context, arg GetKnownSubnetsParams) ([]string, error) GetLatestAllDeviceMetrics(ctx context.Context, argTime time.Time) ([]DeviceMetric, error) GetLatestDeviceMetrics(ctx context.Context, arg GetLatestDeviceMetricsParams) ([]DeviceMetric, error) + // The most recent rollout of a pair within one assignment generation; a + // reconciliation rollout inherits its lineage. + GetLatestFirmwareRolloutForPair(ctx context.Context, arg GetLatestFirmwareRolloutForPairParams) (FirmwareRollout, error) GetLatestFleetMetricRollupBucket(ctx context.Context) (time.Time, error) GetMQTTSourceConfigByOrg(ctx context.Context, arg GetMQTTSourceConfigByOrgParams) (CurtailmentMqttSourceConfig, error) GetMQTTSourceStateByID(ctx context.Context, sourceConfigID int64) (CurtailmentMqttSourceState, error) @@ -750,6 +783,8 @@ type Querier interface { GetRackInfo(ctx context.Context, arg GetRackInfoParams) (GetRackInfoRow, error) GetRackInfoBatch(ctx context.Context, arg GetRackInfoBatchParams) ([]GetRackInfoBatchRow, error) GetRackSlots(ctx context.Context, arg GetRackSlotsParams) ([]GetRackSlotsRow, error) + GetReleaseChannel(ctx context.Context, arg GetReleaseChannelParams) (ReleaseChannel, error) + GetReleaseChannelFirmware(ctx context.Context, arg GetReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) // No row means the org has never chosen a channel; the service layer maps // sql.ErrNoRows to the 'stable' default rather than seeding a row here. GetReleaseChannelSetting(ctx context.Context, organizationID int64) (ReleaseChannelSetting, error) @@ -800,6 +835,10 @@ type Querier interface { GetUserRoleName(ctx context.Context, arg GetUserRoleNameParams) (string, error) GetUserRoleNameForUpdate(ctx context.Context, arg GetUserRoleNameForUpdateParams) (string, error) GetUsersForOrganization(ctx context.Context, organizationID int64) ([]User, error) + // Stops retrying miners for this version: 'failed' (attempts exhausted), + // 'canceled' (operator canceled the remaining updates) or 'skipped' (a caller + // settled them without updating, with its note). + HaltFirmwareRolloutDevices(ctx context.Context, arg HaltFirmwareRolloutDevicesParams) error HasUser(ctx context.Context) (bool, error) // The unique partial index on (batch_id, event_type) for '*.completed' event // types lets the Go layer detect idempotent re-inserts via pq unique_violation. @@ -855,6 +894,8 @@ type Querier interface { // Batch insert for the notification_metric_sample hypertable populated by // the in-process metrics provider on every flush. InsertNotificationMetricSamples(ctx context.Context, arg InsertNotificationMetricSamplesParams) error + // target_types and target_ids are parallel arrays. + InsertReleaseChannelTargets(ctx context.Context, arg InsertReleaseChannelTargetsParams) error InsertRepairTicketPart(ctx context.Context, arg InsertRepairTicketPartParams) error InventoryPartExistsBySiteAndName(ctx context.Context, arg InventoryPartExistsBySiteAndNameParams) (bool, error) IsBatchFinished(ctx context.Context, commandBatchLogUuid string) (bool, error) @@ -881,6 +922,9 @@ type Querier interface { // admission to skip miners already owned by other events without excluding // the current targetless scope watcher. ListActiveCurtailmentTargetDevicesByOrg(ctx context.Context, orgID int64) ([]string, error) + // Across all orgs; drives the enforcement loop. Carries the channel's live + // offline budget, which governs every active rollout of the channel. + ListActiveFirmwareRollouts(ctx context.Context) ([]ListActiveFirmwareRolloutsRow, error) // Firing alerts rolled up per rule, worst blast radius first. (alert_name, rule_group) is rule identity: Grafana // keeps titles unique per folder and a rule_group label maps to one folder, so a title repeats only across labels. // Counts and identity aggregate here; the one piece of per-instance detail is picked off in the lateral below. @@ -999,6 +1043,8 @@ type Querier interface { // orgs, so an admin in org A cannot see or assign org B's custom // roles even if they happen to know an internal id. ListCustomRolesForOrg(ctx context.Context, organizationID sql.NullInt64) ([]Role, error) + // Resolves an org's device identifiers to ids; unknown identifiers are dropped. + ListDeviceIDsByIdentifiers(ctx context.Context, arg ListDeviceIDsByIdentifiersParams) ([]ListDeviceIDsByIdentifiersRow, error) ListDeviceSetMembersPaginated(ctx context.Context, arg ListDeviceSetMembersPaginatedParams) ([]ListDeviceSetMembersPaginatedRow, error) ListDeviceSetMembersPaginatedAfter(ctx context.Context, arg ListDeviceSetMembersPaginatedAfterParams) ([]ListDeviceSetMembersPaginatedAfterRow, error) ListDeviceSetMembersPaginatedFiltered(ctx context.Context, arg ListDeviceSetMembersPaginatedFilteredParams) ([]ListDeviceSetMembersPaginatedFilteredRow, error) @@ -1077,6 +1123,14 @@ type Querier interface { // exist as live devices in the org. Used to surface "device_not_found" // conflicts in AssignDevicesToSite without an N+1 lookup. ListExistingDeviceIdentifiers(ctx context.Context, arg ListExistingDeviceIdentifiersParams) ([]string, error) + // --- Rollout devices --- + // Every miner in a rollout with its bookkeeping, baseline, live health (device + // status, latest telemetry within 15 minutes, open errors), provenance and + // whether it is still a member of the channel for the rollout's pair. + ListFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]ListFirmwareRolloutDevicesRow, error) + // Newest first. The cursor is the (created_at, id) of the last row of the + // previous page; rows strictly older than it are returned. + ListFirmwareRollouts(ctx context.Context, arg ListFirmwareRolloutsParams) ([]ListFirmwareRolloutsRow, error) ListFleetNodeDeviceIDsForRevocation(ctx context.Context, arg ListFleetNodeDeviceIDsForRevocationParams) ([]int64, error) ListFleetNodeDevices(ctx context.Context, arg ListFleetNodeDevicesParams) ([]ListFleetNodeDevicesRow, error) // Fleet-node-discovered devices not yet paired to their node. A discovered @@ -1151,6 +1205,41 @@ type Querier interface { // Scoped cooldown lookup: enumerate the request's live candidate devices first, // then probe terminal target history by device identifier. ListRecentlyResolvedCurtailedDevicesByScope(ctx context.Context, arg ListRecentlyResolvedCurtailedDevicesByScopeParams) ([]string, error) + // Every pair row of an org's channels, assigned or cleared. + ListReleaseChannelFirmware(ctx context.Context, orgID int64) ([]ReleaseChannelFirmware, error) + // Assigned pairs with no active rollout and at least one mismatched, + // unsuppressed member: late joiners, re-entries and miners that drifted. + ListReleaseChannelFirmwareNeedingRollout(ctx context.Context) ([]ListReleaseChannelFirmwareNeedingRolloutRow, error) + // --- Membership --- + // Every miner resolved into one of the org's channels, with its observed + // identity, reported firmware and managed-deployment provenance. + ListReleaseChannelMembers(ctx context.Context, orgID int64) ([]ListReleaseChannelMembersRow, error) + // One page of (miner, channel) relations for miners matched by several + // channels, optionally for one channel, ordered by identifier then channel. + ListReleaseChannelMembershipConflictsPage(ctx context.Context, arg ListReleaseChannelMembershipConflictsPageParams) ([]ListReleaseChannelMembershipConflictsPageRow, error) + // One page of a channel's members, optionally filtered by observed + // manufacturer and model (verbatim), ordered by identifier. The cursor is the + // (device_identifier, device_id) of the last row of the previous page. + ListReleaseChannelMinersPage(ctx context.Context, arg ListReleaseChannelMinersPageParams) ([]ListReleaseChannelMinersPageRow, error) + // Members of one pair that are mismatched under the mismatch rule (reported + // version or provenance differs from the assignment), are not already part of + // rollout_id (0 for a new rollout), and are not suppressed: a member halted + // (failed, skipped, or left behind by a cancellation) in the most recent + // rollout of the current generation that holds it stays out until retried. + // Carries the latest efficiency sample for ordering. + ListReleaseChannelMismatchedMembers(ctx context.Context, arg ListReleaseChannelMismatchedMembersParams) ([]ListReleaseChannelMismatchedMembersRow, error) + // One page of a channel's manufacturer/model groups: every observed pair among + // its members plus every assigned pair with no current members, each joined to + // its assignment (by folded key) and active rollout. Ordered by observed + // manufacturer then model; the cursor is the pair of the last row. + ListReleaseChannelModelGroupsPage(ctx context.Context, arg ListReleaseChannelModelGroupsPageParams) ([]ListReleaseChannelModelGroupsPageRow, error) + // Members of one pair the enforcement rule currently suppresses: halted in + // the most recent rollout of the given generation that holds them. + // RetryFailedRolloutDevices re-queues exactly this set. + ListReleaseChannelSuppressedMembers(ctx context.Context, arg ListReleaseChannelSuppressedMembersParams) ([]ListReleaseChannelSuppressedMembersRow, error) + // Targets of every channel in the org; miner targets carry their identifier. + ListReleaseChannelTargets(ctx context.Context, orgID int64) ([]ListReleaseChannelTargetsRow, error) + ListReleaseChannels(ctx context.Context, orgID int64) ([]ReleaseChannel, error) ListRepairTicketComments(ctx context.Context, arg ListRepairTicketCommentsParams) ([]ListRepairTicketCommentsRow, error) ListRepairTicketParts(ctx context.Context, arg ListRepairTicketPartsParams) ([]ListRepairTicketPartsRow, error) ListRepairTickets(ctx context.Context, arg ListRepairTicketsParams) ([]ListRepairTicketsRow, error) @@ -1340,6 +1429,9 @@ type Querier interface { // on the nullable side of an outer join, and every live rack has a // device_set_rack row by lifecycle invariant. LockRacksForReparent(ctx context.Context, arg LockRacksForReparentParams) ([]int64, error) + // Serializes scope writes per org so the overlap check and the target + // insert happen in one critical section. + LockReleaseChannelScopes(ctx context.Context, orgID int64) error LockRepairTicketCommentCreateKey(ctx context.Context, arg LockRepairTicketCommentCreateKeyParams) error LockRepairTicketCreateKey(ctx context.Context, arg LockRepairTicketCreateKeyParams) error LockSchedulePriority(ctx context.Context, dollar_1 string) error @@ -1350,12 +1442,14 @@ type Querier interface { MarkCommandBatchFinished(ctx context.Context, uuid string) (int64, error) MarkCommandBatchFinishedWithStartedAt(ctx context.Context, uuid string) (int64, error) MarkCommandBatchProcessing(ctx context.Context, uuid string) (int64, error) + MarkFirmwareRolloutDevicesSent(ctx context.Context, arg MarkFirmwareRolloutDevicesSentParams) error MarkRepairTicketPartsConsumed(ctx context.Context, arg MarkRepairTicketPartsConsumedParams) error NegateSchedulePriorities(ctx context.Context, arg NegateSchedulePrioritiesParams) error NextRepairTicketNumber(ctx context.Context, orgID int64) (int64, error) PairDeviceToFleetNode(ctx context.Context, arg PairDeviceToFleetNodeParams) (int64, error) PasswordUpdatedAt(ctx context.Context, id int64) (sql.NullTime, error) PauseActiveSchedule(ctx context.Context, arg PauseActiveScheduleParams) (int64, error) + PauseFirmwareRollout(ctx context.Context, arg PauseFirmwareRolloutParams) (int64, error) // Retention: reclaims the org's expired windows (ends_at <= now) that ended before the cutoff, // plus any beyond the newest keep_newest (see maxRetainedExpiredWindowsPerOrg for the why). PruneExpiredAlertMaintenanceWindows(ctx context.Context, arg PruneExpiredAlertMaintenanceWindowsParams) (int64, error) @@ -1416,6 +1510,12 @@ type Querier interface { // AUTHENTICATION_NEEDED, PENDING, or FAILED by another flow. ReconcileDefaultPasswordPairingStatusByIdentifier(ctx context.Context, arg ReconcileDefaultPasswordPairingStatusByIdentifierParams) (ReconcileDefaultPasswordPairingStatusByIdentifierRow, error) RecordCurtailPendingDispatch(ctx context.Context, arg RecordCurtailPendingDispatchParams) (int64, error) + // Managed-deployment provenance: the miners listed reported the artifact a + // rollout dispatched to them. + RecordFirmwareDeployment(ctx context.Context, arg RecordFirmwareDeploymentParams) error + // Attributes an action that changes only the rollout's devices (retry) to + // its actor; the update itself bumps the revision. + RecordFirmwareRolloutAction(ctx context.Context, arg RecordFirmwareRolloutActionParams) error // ============================================================================ // Error Lifecycle Management // ============================================================================ @@ -1459,6 +1559,9 @@ type Querier interface { RemoveDevicesFromDeviceSet(ctx context.Context, arg RemoveDevicesFromDeviceSetParams) ([]string, error) RenewFleetRuntimeLease(ctx context.Context, arg RenewFleetRuntimeLeaseParams) (RenewFleetRuntimeLeaseRow, error) RequestRigConfigReconciliation(ctx context.Context, arg RequestRigConfigReconciliationParams) error + // Re-queues every halted miner of an active rollout from scratch and + // returns them. + RequeueFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]int64, error) // The command queue has bounded per-message retries. Reopen the organization // generation when one config command becomes terminal so reconciliation keeps // retrying instead of treating durable enqueue as durable device application. @@ -1484,11 +1587,16 @@ type Querier interface { ResolveMaintenanceAssignee(ctx context.Context, arg ResolveMaintenanceAssigneeParams) (ResolveMaintenanceAssigneeRow, error) ResolveMaintenanceLocationContext(ctx context.Context, arg ResolveMaintenanceLocationContextParams) (ResolveMaintenanceLocationContextRow, error) ResolveMaintenanceMinerContext(ctx context.Context, arg ResolveMaintenanceMinerContextParams) (ResolveMaintenanceMinerContextRow, error) + // Miners a candidate scope covers, each with the other channel (if any) + // whose selectors already match it. Used to preview a scope and to reject + // overlapping saves; exclude_channel_id is the channel being edited. + ResolveReleaseChannelScope(ctx context.Context, arg ResolveReleaseChannelScopeParams) ([]ResolveReleaseChannelScopeRow, error) // Restore reversal: go back through pending so the curtail dispatcher picks // up reset targets. Preserve fan_off_sent_at and fan_last_error until the // active reconciler has positively reopened airflow; clearing them here can // hide fans that remained off after a failed restore command. ResumeCurtailmentFromRestoring(ctx context.Context, id int64) (CurtailmentEvent, error) + ResumeFirmwareRollout(ctx context.Context, arg ResumeFirmwareRolloutParams) (int64, error) ResumePausedSchedule(ctx context.Context, arg ResumePausedScheduleParams) (int64, error) RetryRigConfigReconciliation(ctx context.Context, arg RetryRigConfigReconciliationParams) error RevertScheduleToActive(ctx context.Context, id int64) error @@ -1543,6 +1651,13 @@ type Querier interface { // cross-org or missing IDs. Mirrors BuildingsByIDs; used to // bulk-validate rack-list site_ids filter references in one round trip. SitesByIDs(ctx context.Context, arg SitesByIDsParams) ([]int64, error) + // Adds miners to a rollout with their batch (NULL for the unbatched rest / + // late joiners), their order (position_offset + index in device_ids; NULL + // offset for late joiners) and a baseline of their health, so post-update + // evidence can be compared against each miner's own past. A miner that + // re-enters the scope after being excluded is re-included and keeps its + // original batch and order. + SnapshotFirmwareRolloutDevices(ctx context.Context, arg SnapshotFirmwareRolloutDevicesParams) error // Clear the encrypted secret on delete: a soft-deleted channel never delivers again, so there's // no reason to retain its webhook URL / bearer. SoftDeleteAlertChannel(ctx context.Context, arg SoftDeleteAlertChannelParams) (int64, error) @@ -1792,6 +1907,7 @@ type Querier interface { // would lose user-curated metadata. NULL (not '') preserves the // collection_sort.go "zone NULLS LAST" semantics. UpdateRackPlacementBulkForSite(ctx context.Context, arg UpdateRackPlacementBulkForSiteParams) error + UpdateReleaseChannel(ctx context.Context, arg UpdateReleaseChannelParams) (ReleaseChannel, error) UpdateRepairTicket(ctx context.Context, arg UpdateRepairTicketParams) (int64, error) UpdateRole(ctx context.Context, arg UpdateRoleParams) error UpdateSchedule(ctx context.Context, arg UpdateScheduleParams) (int64, error) @@ -1868,6 +1984,10 @@ type Querier interface { // the in-code catalog so catalog text changes propagate without a new // migration. UpsertPermission(ctx context.Context, arg UpsertPermissionParams) (Permission, error) + // --- Firmware assignments --- + // Assigns an artifact to a pair and advances the pair's generation. The + // stored key keeps the case it was first written with. + UpsertReleaseChannelFirmware(ctx context.Context, arg UpsertReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) UpsertReleaseChannelSetting(ctx context.Context, arg UpsertReleaseChannelSettingParams) (ReleaseChannelSetting, error) } diff --git a/server/generated/sqlc/release_channel.sql.go b/server/generated/sqlc/release_channel.sql.go new file mode 100644 index 0000000000..484d268f90 --- /dev/null +++ b/server/generated/sqlc/release_channel.sql.go @@ -0,0 +1,2606 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: release_channel.sql + +package sqlc + +import ( + "context" + "database/sql" + "time" + + "github.com/lib/pq" +) + +const advanceFirmwareRolloutStage = `-- name: AdvanceFirmwareRolloutStage :execrows +UPDATE firmware_rollout +SET stage = $1, + current_batch = $2, + stage_changed_at = now(), + last_action_by_type = COALESCE($3::text, last_action_by_type), + last_action_by_id = COALESCE($4::bigint, last_action_by_id), + last_action_by_name = COALESCE($5::text, last_action_by_name) +WHERE id = $6 + AND status = 'active' + AND stage = $7 +` + +type AdvanceFirmwareRolloutStageParams struct { + Stage string + CurrentBatch int32 + ActorType sql.NullString + ActorID sql.NullInt64 + ActorName sql.NullString + RolloutID int64 + FromStage string +} + +// Stage transitions of an active rollout, attributed to an actor when one +// drove them. Returns the affected row count so callers can detect a lost race. +func (q *Queries) AdvanceFirmwareRolloutStage(ctx context.Context, arg AdvanceFirmwareRolloutStageParams) (int64, error) { + result, err := q.exec(ctx, q.advanceFirmwareRolloutStageStmt, advanceFirmwareRolloutStage, + arg.Stage, + arg.CurrentBatch, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.RolloutID, + arg.FromStage, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const cancelActiveFirmwareRollout = `-- name: CancelActiveFirmwareRollout :exec +UPDATE firmware_rollout +SET status = 'canceled', + finished_at = now(), + cancel_reason = $1, + last_action_by_type = $2, + last_action_by_id = $3, + last_action_by_name = $4 +WHERE channel_id = $5 + AND lower(manufacturer COLLATE "C") = lower($6::text COLLATE "C") + AND lower(model COLLATE "C") = lower($7::text COLLATE "C") + AND status = 'active' +` + +type CancelActiveFirmwareRolloutParams struct { + CancelReason string + ActorType string + ActorID int64 + ActorName string + ChannelID int64 + Manufacturer string + Model string +} + +// Cancels the pair's active rollout because its assignment changed: +// 'superseded', 'rolled_back' or 'cleared'. +func (q *Queries) CancelActiveFirmwareRollout(ctx context.Context, arg CancelActiveFirmwareRolloutParams) error { + _, err := q.exec(ctx, q.cancelActiveFirmwareRolloutStmt, cancelActiveFirmwareRollout, + arg.CancelReason, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + ) + return err +} + +const cancelFirmwareRollout = `-- name: CancelFirmwareRollout :execrows +UPDATE firmware_rollout +SET status = 'canceled', + finished_at = now(), + cancel_reason = 'canceled_remaining', + last_action_by_type = $1, + last_action_by_id = $2, + last_action_by_name = $3 +WHERE id = $4 AND status = 'active' +` + +type CancelFirmwareRolloutParams struct { + ActorType string + ActorID int64 + ActorName string + RolloutID int64 +} + +func (q *Queries) CancelFirmwareRollout(ctx context.Context, arg CancelFirmwareRolloutParams) (int64, error) { + result, err := q.exec(ctx, q.cancelFirmwareRolloutStmt, cancelFirmwareRollout, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.RolloutID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const clearReleaseChannelFirmware = `-- name: ClearReleaseChannelFirmware :one +UPDATE release_channel_firmware +SET firmware_checksum = '', + firmware_version = '', + firmware_target_manufacturer = '', + firmware_target_model = '', + assignment_generation = assignment_generation + 1, + assigned_by = $1, + updated_at = now() +WHERE channel_id = $2 + AND lower(manufacturer COLLATE "C") = lower($3::text COLLATE "C") + AND lower(model COLLATE "C") = lower($4::text COLLATE "C") + AND firmware_checksum <> '' +RETURNING channel_id, manufacturer, model, firmware_checksum, firmware_version, firmware_target_manufacturer, firmware_target_model, assignment_generation, assigned_by, updated_at +` + +type ClearReleaseChannelFirmwareParams struct { + AssignedBy int64 + ChannelID int64 + Manufacturer string + Model string +} + +// Clears an assigned pair, advancing its generation; the row stays so the +// generation survives. Returns nothing when the pair was not assigned. +func (q *Queries) ClearReleaseChannelFirmware(ctx context.Context, arg ClearReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + row := q.queryRow(ctx, q.clearReleaseChannelFirmwareStmt, clearReleaseChannelFirmware, + arg.AssignedBy, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + ) + var i ReleaseChannelFirmware + err := row.Scan( + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.AssignedBy, + &i.UpdatedAt, + ) + return i, err +} + +const createFirmwareRollout = `-- name: CreateFirmwareRollout :one + +INSERT INTO firmware_rollout ( + org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, + previous_firmware_checksum, previous_firmware_version, assignment_generation, + stage, + method, order_by, batch_size, pilot_size, wait_between_batches_seconds, + review_after_each_batch, auto_continue, stabilization_seconds, + max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, + min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, + started_by_type, started_by_id, started_by_name, + last_action_by_type, last_action_by_id, last_action_by_name +) +VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, + $10, + $11, $12, $13, $14, $15, + $16, $17, $18, + $19::double precision, $20::double precision, + $21::double precision, $22::int, + $23::double precision, $24, $25, $26, + $27, $28, $29, + $27, $28, $29 +) +RETURNING id, org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, previous_firmware_checksum, previous_firmware_version, assignment_generation, status, cancel_reason, stage, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, current_batch, stage_changed_at, paused_at, revision, updated_at, started_by_type, started_by_id, started_by_name, last_action_by_type, last_action_by_id, last_action_by_name, created_at, finished_at +` + +type CreateFirmwareRolloutParams struct { + OrgID int64 + ChannelID int64 + Manufacturer string + Model string + FirmwareChecksum string + FirmwareVersion string + PreviousFirmwareChecksum string + PreviousFirmwareVersion string + AssignmentGeneration int64 + Stage string + Method string + OrderBy string + BatchSize int32 + PilotSize int32 + WaitBetweenBatchesSeconds int32 + ReviewAfterEachBatch bool + AutoContinue bool + StabilizationSeconds int32 + MaxHashrateDropPercent sql.NullFloat64 + MaxEfficiencyIncreasePercent sql.NullFloat64 + MaxTempIncreaseC sql.NullFloat64 + MaxNewErrors sql.NullInt32 + MinSampleCoveragePercent sql.NullFloat64 + MaxConcurrentOffline int32 + ControllerTimeoutSeconds int32 + BatchCount int32 + ActorType string + ActorID int64 + ActorName string +} + +// --- Rollouts --- +func (q *Queries) CreateFirmwareRollout(ctx context.Context, arg CreateFirmwareRolloutParams) (FirmwareRollout, error) { + row := q.queryRow(ctx, q.createFirmwareRolloutStmt, createFirmwareRollout, + arg.OrgID, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + arg.FirmwareChecksum, + arg.FirmwareVersion, + arg.PreviousFirmwareChecksum, + arg.PreviousFirmwareVersion, + arg.AssignmentGeneration, + arg.Stage, + arg.Method, + arg.OrderBy, + arg.BatchSize, + arg.PilotSize, + arg.WaitBetweenBatchesSeconds, + arg.ReviewAfterEachBatch, + arg.AutoContinue, + arg.StabilizationSeconds, + arg.MaxHashrateDropPercent, + arg.MaxEfficiencyIncreasePercent, + arg.MaxTempIncreaseC, + arg.MaxNewErrors, + arg.MinSampleCoveragePercent, + arg.MaxConcurrentOffline, + arg.ControllerTimeoutSeconds, + arg.BatchCount, + arg.ActorType, + arg.ActorID, + arg.ActorName, + ) + var i FirmwareRollout + err := row.Scan( + &i.ID, + &i.OrgID, + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.PreviousFirmwareChecksum, + &i.PreviousFirmwareVersion, + &i.AssignmentGeneration, + &i.Status, + &i.CancelReason, + &i.Stage, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.BatchCount, + &i.CurrentBatch, + &i.StageChangedAt, + &i.PausedAt, + &i.Revision, + &i.UpdatedAt, + &i.StartedByType, + &i.StartedByID, + &i.StartedByName, + &i.LastActionByType, + &i.LastActionByID, + &i.LastActionByName, + &i.CreatedAt, + &i.FinishedAt, + ) + return i, err +} + +const createReleaseChannel = `-- name: CreateReleaseChannel :one + + +INSERT INTO release_channel ( + org_id, name, description, created_by, + method, order_by, batch_size, pilot_size, wait_between_batches_seconds, + review_after_each_batch, auto_continue, stabilization_seconds, + max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, + min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds +) +VALUES ( + $1, $2, $3, $4, + $5, $6, $7, $8, $9, + $10, $11, $12, + $13::double precision, $14::double precision, + $15::double precision, $16::int, + $17::double precision, $18, $19 +) +RETURNING id, org_id, name, description, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, created_by, created_at, updated_at +` + +type CreateReleaseChannelParams struct { + OrgID int64 + Name string + Description string + CreatedBy int64 + Method string + OrderBy string + BatchSize int32 + PilotSize int32 + WaitBetweenBatchesSeconds int32 + ReviewAfterEachBatch bool + AutoContinue bool + StabilizationSeconds int32 + MaxHashrateDropPercent sql.NullFloat64 + MaxEfficiencyIncreasePercent sql.NullFloat64 + MaxTempIncreaseC sql.NullFloat64 + MaxNewErrors sql.NullInt32 + MinSampleCoveragePercent sql.NullFloat64 + MaxConcurrentOffline int32 + ControllerTimeoutSeconds int32 +} + +// Firmware release channels and the rollouts that enforce them. +// +// Pair keys (manufacturer, model) are compared ASCII-case-insensitively with +// lower(x COLLATE "C"), which folds A-Z only; observed device identities are +// trimmed before folding. Firmware is identified by payload checksum. +// --- Channels --- +func (q *Queries) CreateReleaseChannel(ctx context.Context, arg CreateReleaseChannelParams) (ReleaseChannel, error) { + row := q.queryRow(ctx, q.createReleaseChannelStmt, createReleaseChannel, + arg.OrgID, + arg.Name, + arg.Description, + arg.CreatedBy, + arg.Method, + arg.OrderBy, + arg.BatchSize, + arg.PilotSize, + arg.WaitBetweenBatchesSeconds, + arg.ReviewAfterEachBatch, + arg.AutoContinue, + arg.StabilizationSeconds, + arg.MaxHashrateDropPercent, + arg.MaxEfficiencyIncreasePercent, + arg.MaxTempIncreaseC, + arg.MaxNewErrors, + arg.MinSampleCoveragePercent, + arg.MaxConcurrentOffline, + arg.ControllerTimeoutSeconds, + ) + var i ReleaseChannel + err := row.Scan( + &i.ID, + &i.OrgID, + &i.Name, + &i.Description, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteReleaseChannel = `-- name: DeleteReleaseChannel :execrows +DELETE FROM release_channel +WHERE id = $1 AND org_id = $2 +` + +type DeleteReleaseChannelParams struct { + ChannelID int64 + OrgID int64 +} + +func (q *Queries) DeleteReleaseChannel(ctx context.Context, arg DeleteReleaseChannelParams) (int64, error) { + result, err := q.exec(ctx, q.deleteReleaseChannelStmt, deleteReleaseChannel, arg.ChannelID, arg.OrgID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const deleteReleaseChannelTargets = `-- name: DeleteReleaseChannelTargets :exec +DELETE FROM release_channel_target WHERE channel_id = $1 +` + +func (q *Queries) DeleteReleaseChannelTargets(ctx context.Context, channelID int64) error { + _, err := q.exec(ctx, q.deleteReleaseChannelTargetsStmt, deleteReleaseChannelTargets, channelID) + return err +} + +const excludeFirmwareRolloutDevices = `-- name: ExcludeFirmwareRolloutDevices :exec +UPDATE firmware_rollout_device +SET excluded_at = now() +WHERE rollout_id = $1 + AND device_id = ANY($2::bigint[]) + AND excluded_at IS NULL +` + +type ExcludeFirmwareRolloutDevicesParams struct { + RolloutID int64 + DeviceIds []int64 +} + +func (q *Queries) ExcludeFirmwareRolloutDevices(ctx context.Context, arg ExcludeFirmwareRolloutDevicesParams) error { + _, err := q.exec(ctx, q.excludeFirmwareRolloutDevicesStmt, excludeFirmwareRolloutDevices, arg.RolloutID, pq.Array(arg.DeviceIds)) + return err +} + +const finishFirmwareRollout = `-- name: FinishFirmwareRollout :execrows +UPDATE firmware_rollout +SET status = $1, finished_at = now() +WHERE id = $2 AND status = 'active' +` + +type FinishFirmwareRolloutParams struct { + Status string + RolloutID int64 +} + +// Ends an active rollout as 'completed' or 'completed_with_failures'. +func (q *Queries) FinishFirmwareRollout(ctx context.Context, arg FinishFirmwareRolloutParams) (int64, error) { + result, err := q.exec(ctx, q.finishFirmwareRolloutStmt, finishFirmwareRollout, arg.Status, arg.RolloutID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const getActiveFirmwareRolloutForPair = `-- name: GetActiveFirmwareRolloutForPair :one +SELECT id, org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, previous_firmware_checksum, previous_firmware_version, assignment_generation, status, cancel_reason, stage, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, current_batch, stage_changed_at, paused_at, revision, updated_at, started_by_type, started_by_id, started_by_name, last_action_by_type, last_action_by_id, last_action_by_name, created_at, finished_at FROM firmware_rollout +WHERE channel_id = $1 + AND lower(manufacturer COLLATE "C") = lower($2::text COLLATE "C") + AND lower(model COLLATE "C") = lower($3::text COLLATE "C") + AND status = 'active' +` + +type GetActiveFirmwareRolloutForPairParams struct { + ChannelID int64 + Manufacturer string + Model string +} + +func (q *Queries) GetActiveFirmwareRolloutForPair(ctx context.Context, arg GetActiveFirmwareRolloutForPairParams) (FirmwareRollout, error) { + row := q.queryRow(ctx, q.getActiveFirmwareRolloutForPairStmt, getActiveFirmwareRolloutForPair, arg.ChannelID, arg.Manufacturer, arg.Model) + var i FirmwareRollout + err := row.Scan( + &i.ID, + &i.OrgID, + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.PreviousFirmwareChecksum, + &i.PreviousFirmwareVersion, + &i.AssignmentGeneration, + &i.Status, + &i.CancelReason, + &i.Stage, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.BatchCount, + &i.CurrentBatch, + &i.StageChangedAt, + &i.PausedAt, + &i.Revision, + &i.UpdatedAt, + &i.StartedByType, + &i.StartedByID, + &i.StartedByName, + &i.LastActionByType, + &i.LastActionByID, + &i.LastActionByName, + &i.CreatedAt, + &i.FinishedAt, + ) + return i, err +} + +const getFirmwareRollout = `-- name: GetFirmwareRollout :one +SELECT id, org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, previous_firmware_checksum, previous_firmware_version, assignment_generation, status, cancel_reason, stage, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, current_batch, stage_changed_at, paused_at, revision, updated_at, started_by_type, started_by_id, started_by_name, last_action_by_type, last_action_by_id, last_action_by_name, created_at, finished_at FROM firmware_rollout +WHERE id = $1 AND org_id = $2 +` + +type GetFirmwareRolloutParams struct { + RolloutID int64 + OrgID int64 +} + +func (q *Queries) GetFirmwareRollout(ctx context.Context, arg GetFirmwareRolloutParams) (FirmwareRollout, error) { + row := q.queryRow(ctx, q.getFirmwareRolloutStmt, getFirmwareRollout, arg.RolloutID, arg.OrgID) + var i FirmwareRollout + err := row.Scan( + &i.ID, + &i.OrgID, + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.PreviousFirmwareChecksum, + &i.PreviousFirmwareVersion, + &i.AssignmentGeneration, + &i.Status, + &i.CancelReason, + &i.Stage, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.BatchCount, + &i.CurrentBatch, + &i.StageChangedAt, + &i.PausedAt, + &i.Revision, + &i.UpdatedAt, + &i.StartedByType, + &i.StartedByID, + &i.StartedByName, + &i.LastActionByType, + &i.LastActionByID, + &i.LastActionByName, + &i.CreatedAt, + &i.FinishedAt, + ) + return i, err +} + +const getFirmwareRolloutForUpdate = `-- name: GetFirmwareRolloutForUpdate :one +SELECT id, org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, previous_firmware_checksum, previous_firmware_version, assignment_generation, status, cancel_reason, stage, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, current_batch, stage_changed_at, paused_at, revision, updated_at, started_by_type, started_by_id, started_by_name, last_action_by_type, last_action_by_id, last_action_by_name, created_at, finished_at FROM firmware_rollout +WHERE id = $1 AND org_id = $2 +FOR UPDATE +` + +type GetFirmwareRolloutForUpdateParams struct { + RolloutID int64 + OrgID int64 +} + +// Locks the row for a mutation so the revision rule can be checked and the +// change applied without a concurrent actor slipping in between. +func (q *Queries) GetFirmwareRolloutForUpdate(ctx context.Context, arg GetFirmwareRolloutForUpdateParams) (FirmwareRollout, error) { + row := q.queryRow(ctx, q.getFirmwareRolloutForUpdateStmt, getFirmwareRolloutForUpdate, arg.RolloutID, arg.OrgID) + var i FirmwareRollout + err := row.Scan( + &i.ID, + &i.OrgID, + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.PreviousFirmwareChecksum, + &i.PreviousFirmwareVersion, + &i.AssignmentGeneration, + &i.Status, + &i.CancelReason, + &i.Stage, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.BatchCount, + &i.CurrentBatch, + &i.StageChangedAt, + &i.PausedAt, + &i.Revision, + &i.UpdatedAt, + &i.StartedByType, + &i.StartedByID, + &i.StartedByName, + &i.LastActionByType, + &i.LastActionByID, + &i.LastActionByName, + &i.CreatedAt, + &i.FinishedAt, + ) + return i, err +} + +const getFirmwareRolloutWithChannel = `-- name: GetFirmwareRolloutWithChannel :one +SELECT r.id, r.org_id, r.channel_id, r.manufacturer, r.model, r.firmware_checksum, r.firmware_version, r.previous_firmware_checksum, r.previous_firmware_version, r.assignment_generation, r.status, r.cancel_reason, r.stage, r.method, r.order_by, r.batch_size, r.pilot_size, r.wait_between_batches_seconds, r.review_after_each_batch, r.auto_continue, r.stabilization_seconds, r.max_hashrate_drop_percent, r.max_efficiency_increase_percent, r.max_temp_increase_c, r.max_new_errors, r.min_sample_coverage_percent, r.max_concurrent_offline, r.controller_timeout_seconds, r.batch_count, r.current_batch, r.stage_changed_at, r.paused_at, r.revision, r.updated_at, r.started_by_type, r.started_by_id, r.started_by_name, r.last_action_by_type, r.last_action_by_id, r.last_action_by_name, r.created_at, r.finished_at, c.name AS channel_name +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.id = $1 AND r.org_id = $2 +` + +type GetFirmwareRolloutWithChannelParams struct { + RolloutID int64 + OrgID int64 +} + +type GetFirmwareRolloutWithChannelRow struct { + FirmwareRollout FirmwareRollout + ChannelName string +} + +func (q *Queries) GetFirmwareRolloutWithChannel(ctx context.Context, arg GetFirmwareRolloutWithChannelParams) (GetFirmwareRolloutWithChannelRow, error) { + row := q.queryRow(ctx, q.getFirmwareRolloutWithChannelStmt, getFirmwareRolloutWithChannel, arg.RolloutID, arg.OrgID) + var i GetFirmwareRolloutWithChannelRow + err := row.Scan( + &i.FirmwareRollout.ID, + &i.FirmwareRollout.OrgID, + &i.FirmwareRollout.ChannelID, + &i.FirmwareRollout.Manufacturer, + &i.FirmwareRollout.Model, + &i.FirmwareRollout.FirmwareChecksum, + &i.FirmwareRollout.FirmwareVersion, + &i.FirmwareRollout.PreviousFirmwareChecksum, + &i.FirmwareRollout.PreviousFirmwareVersion, + &i.FirmwareRollout.AssignmentGeneration, + &i.FirmwareRollout.Status, + &i.FirmwareRollout.CancelReason, + &i.FirmwareRollout.Stage, + &i.FirmwareRollout.Method, + &i.FirmwareRollout.OrderBy, + &i.FirmwareRollout.BatchSize, + &i.FirmwareRollout.PilotSize, + &i.FirmwareRollout.WaitBetweenBatchesSeconds, + &i.FirmwareRollout.ReviewAfterEachBatch, + &i.FirmwareRollout.AutoContinue, + &i.FirmwareRollout.StabilizationSeconds, + &i.FirmwareRollout.MaxHashrateDropPercent, + &i.FirmwareRollout.MaxEfficiencyIncreasePercent, + &i.FirmwareRollout.MaxTempIncreaseC, + &i.FirmwareRollout.MaxNewErrors, + &i.FirmwareRollout.MinSampleCoveragePercent, + &i.FirmwareRollout.MaxConcurrentOffline, + &i.FirmwareRollout.ControllerTimeoutSeconds, + &i.FirmwareRollout.BatchCount, + &i.FirmwareRollout.CurrentBatch, + &i.FirmwareRollout.StageChangedAt, + &i.FirmwareRollout.PausedAt, + &i.FirmwareRollout.Revision, + &i.FirmwareRollout.UpdatedAt, + &i.FirmwareRollout.StartedByType, + &i.FirmwareRollout.StartedByID, + &i.FirmwareRollout.StartedByName, + &i.FirmwareRollout.LastActionByType, + &i.FirmwareRollout.LastActionByID, + &i.FirmwareRollout.LastActionByName, + &i.FirmwareRollout.CreatedAt, + &i.FirmwareRollout.FinishedAt, + &i.ChannelName, + ) + return i, err +} + +const getLatestFirmwareRolloutForPair = `-- name: GetLatestFirmwareRolloutForPair :one +SELECT id, org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, previous_firmware_checksum, previous_firmware_version, assignment_generation, status, cancel_reason, stage, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, current_batch, stage_changed_at, paused_at, revision, updated_at, started_by_type, started_by_id, started_by_name, last_action_by_type, last_action_by_id, last_action_by_name, created_at, finished_at FROM firmware_rollout +WHERE channel_id = $1 + AND lower(manufacturer COLLATE "C") = lower($2::text COLLATE "C") + AND lower(model COLLATE "C") = lower($3::text COLLATE "C") + AND assignment_generation = $4 +ORDER BY created_at DESC, id DESC +LIMIT 1 +` + +type GetLatestFirmwareRolloutForPairParams struct { + ChannelID int64 + Manufacturer string + Model string + AssignmentGeneration int64 +} + +// The most recent rollout of a pair within one assignment generation; a +// reconciliation rollout inherits its lineage. +func (q *Queries) GetLatestFirmwareRolloutForPair(ctx context.Context, arg GetLatestFirmwareRolloutForPairParams) (FirmwareRollout, error) { + row := q.queryRow(ctx, q.getLatestFirmwareRolloutForPairStmt, getLatestFirmwareRolloutForPair, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + arg.AssignmentGeneration, + ) + var i FirmwareRollout + err := row.Scan( + &i.ID, + &i.OrgID, + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.PreviousFirmwareChecksum, + &i.PreviousFirmwareVersion, + &i.AssignmentGeneration, + &i.Status, + &i.CancelReason, + &i.Stage, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.BatchCount, + &i.CurrentBatch, + &i.StageChangedAt, + &i.PausedAt, + &i.Revision, + &i.UpdatedAt, + &i.StartedByType, + &i.StartedByID, + &i.StartedByName, + &i.LastActionByType, + &i.LastActionByID, + &i.LastActionByName, + &i.CreatedAt, + &i.FinishedAt, + ) + return i, err +} + +const getReleaseChannel = `-- name: GetReleaseChannel :one +SELECT id, org_id, name, description, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, created_by, created_at, updated_at FROM release_channel +WHERE id = $1 AND org_id = $2 +` + +type GetReleaseChannelParams struct { + ChannelID int64 + OrgID int64 +} + +func (q *Queries) GetReleaseChannel(ctx context.Context, arg GetReleaseChannelParams) (ReleaseChannel, error) { + row := q.queryRow(ctx, q.getReleaseChannelStmt, getReleaseChannel, arg.ChannelID, arg.OrgID) + var i ReleaseChannel + err := row.Scan( + &i.ID, + &i.OrgID, + &i.Name, + &i.Description, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getReleaseChannelFirmware = `-- name: GetReleaseChannelFirmware :one +SELECT channel_id, manufacturer, model, firmware_checksum, firmware_version, firmware_target_manufacturer, firmware_target_model, assignment_generation, assigned_by, updated_at FROM release_channel_firmware +WHERE channel_id = $1 + AND lower(manufacturer COLLATE "C") = lower($2::text COLLATE "C") + AND lower(model COLLATE "C") = lower($3::text COLLATE "C") +` + +type GetReleaseChannelFirmwareParams struct { + ChannelID int64 + Manufacturer string + Model string +} + +func (q *Queries) GetReleaseChannelFirmware(ctx context.Context, arg GetReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + row := q.queryRow(ctx, q.getReleaseChannelFirmwareStmt, getReleaseChannelFirmware, arg.ChannelID, arg.Manufacturer, arg.Model) + var i ReleaseChannelFirmware + err := row.Scan( + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.AssignedBy, + &i.UpdatedAt, + ) + return i, err +} + +const haltFirmwareRolloutDevices = `-- name: HaltFirmwareRolloutDevices :exec +UPDATE firmware_rollout_device +SET halted_at = now(), + halt_reason = $1, + last_error = $2, + skip_note = $3 +WHERE rollout_id = $4 + AND device_id = ANY($5::bigint[]) + AND halted_at IS NULL +` + +type HaltFirmwareRolloutDevicesParams struct { + HaltReason string + LastError string + SkipNote string + RolloutID int64 + DeviceIds []int64 +} + +// Stops retrying miners for this version: 'failed' (attempts exhausted), +// 'canceled' (operator canceled the remaining updates) or 'skipped' (a caller +// settled them without updating, with its note). +func (q *Queries) HaltFirmwareRolloutDevices(ctx context.Context, arg HaltFirmwareRolloutDevicesParams) error { + _, err := q.exec(ctx, q.haltFirmwareRolloutDevicesStmt, haltFirmwareRolloutDevices, + arg.HaltReason, + arg.LastError, + arg.SkipNote, + arg.RolloutID, + pq.Array(arg.DeviceIds), + ) + return err +} + +const insertReleaseChannelTargets = `-- name: InsertReleaseChannelTargets :exec +INSERT INTO release_channel_target (channel_id, target_type, target_id) +SELECT $1, + unnest($2::text[]), + unnest($3::bigint[]) +ON CONFLICT DO NOTHING +` + +type InsertReleaseChannelTargetsParams struct { + ChannelID int64 + TargetTypes []string + TargetIds []int64 +} + +// target_types and target_ids are parallel arrays. +func (q *Queries) InsertReleaseChannelTargets(ctx context.Context, arg InsertReleaseChannelTargetsParams) error { + _, err := q.exec(ctx, q.insertReleaseChannelTargetsStmt, insertReleaseChannelTargets, arg.ChannelID, pq.Array(arg.TargetTypes), pq.Array(arg.TargetIds)) + return err +} + +const listActiveFirmwareRollouts = `-- name: ListActiveFirmwareRollouts :many +SELECT r.id, r.org_id, r.channel_id, r.manufacturer, r.model, r.firmware_checksum, r.firmware_version, r.previous_firmware_checksum, r.previous_firmware_version, r.assignment_generation, r.status, r.cancel_reason, r.stage, r.method, r.order_by, r.batch_size, r.pilot_size, r.wait_between_batches_seconds, r.review_after_each_batch, r.auto_continue, r.stabilization_seconds, r.max_hashrate_drop_percent, r.max_efficiency_increase_percent, r.max_temp_increase_c, r.max_new_errors, r.min_sample_coverage_percent, r.max_concurrent_offline, r.controller_timeout_seconds, r.batch_count, r.current_batch, r.stage_changed_at, r.paused_at, r.revision, r.updated_at, r.started_by_type, r.started_by_id, r.started_by_name, r.last_action_by_type, r.last_action_by_id, r.last_action_by_name, r.created_at, r.finished_at, c.name AS channel_name, c.max_concurrent_offline AS channel_max_concurrent_offline +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.status = 'active' +ORDER BY r.id +` + +type ListActiveFirmwareRolloutsRow struct { + FirmwareRollout FirmwareRollout + ChannelName string + ChannelMaxConcurrentOffline int32 +} + +// Across all orgs; drives the enforcement loop. Carries the channel's live +// offline budget, which governs every active rollout of the channel. +func (q *Queries) ListActiveFirmwareRollouts(ctx context.Context) ([]ListActiveFirmwareRolloutsRow, error) { + rows, err := q.query(ctx, q.listActiveFirmwareRolloutsStmt, listActiveFirmwareRollouts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListActiveFirmwareRolloutsRow + for rows.Next() { + var i ListActiveFirmwareRolloutsRow + if err := rows.Scan( + &i.FirmwareRollout.ID, + &i.FirmwareRollout.OrgID, + &i.FirmwareRollout.ChannelID, + &i.FirmwareRollout.Manufacturer, + &i.FirmwareRollout.Model, + &i.FirmwareRollout.FirmwareChecksum, + &i.FirmwareRollout.FirmwareVersion, + &i.FirmwareRollout.PreviousFirmwareChecksum, + &i.FirmwareRollout.PreviousFirmwareVersion, + &i.FirmwareRollout.AssignmentGeneration, + &i.FirmwareRollout.Status, + &i.FirmwareRollout.CancelReason, + &i.FirmwareRollout.Stage, + &i.FirmwareRollout.Method, + &i.FirmwareRollout.OrderBy, + &i.FirmwareRollout.BatchSize, + &i.FirmwareRollout.PilotSize, + &i.FirmwareRollout.WaitBetweenBatchesSeconds, + &i.FirmwareRollout.ReviewAfterEachBatch, + &i.FirmwareRollout.AutoContinue, + &i.FirmwareRollout.StabilizationSeconds, + &i.FirmwareRollout.MaxHashrateDropPercent, + &i.FirmwareRollout.MaxEfficiencyIncreasePercent, + &i.FirmwareRollout.MaxTempIncreaseC, + &i.FirmwareRollout.MaxNewErrors, + &i.FirmwareRollout.MinSampleCoveragePercent, + &i.FirmwareRollout.MaxConcurrentOffline, + &i.FirmwareRollout.ControllerTimeoutSeconds, + &i.FirmwareRollout.BatchCount, + &i.FirmwareRollout.CurrentBatch, + &i.FirmwareRollout.StageChangedAt, + &i.FirmwareRollout.PausedAt, + &i.FirmwareRollout.Revision, + &i.FirmwareRollout.UpdatedAt, + &i.FirmwareRollout.StartedByType, + &i.FirmwareRollout.StartedByID, + &i.FirmwareRollout.StartedByName, + &i.FirmwareRollout.LastActionByType, + &i.FirmwareRollout.LastActionByID, + &i.FirmwareRollout.LastActionByName, + &i.FirmwareRollout.CreatedAt, + &i.FirmwareRollout.FinishedAt, + &i.ChannelName, + &i.ChannelMaxConcurrentOffline, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listDeviceIDsByIdentifiers = `-- name: ListDeviceIDsByIdentifiers :many +SELECT d.id, d.device_identifier +FROM device d +WHERE d.org_id = $1 + AND d.deleted_at IS NULL + AND d.device_identifier = ANY($2::text[]) +` + +type ListDeviceIDsByIdentifiersParams struct { + OrgID int64 + DeviceIdentifiers []string +} + +type ListDeviceIDsByIdentifiersRow struct { + ID int64 + DeviceIdentifier string +} + +// Resolves an org's device identifiers to ids; unknown identifiers are dropped. +func (q *Queries) ListDeviceIDsByIdentifiers(ctx context.Context, arg ListDeviceIDsByIdentifiersParams) ([]ListDeviceIDsByIdentifiersRow, error) { + rows, err := q.query(ctx, q.listDeviceIDsByIdentifiersStmt, listDeviceIDsByIdentifiers, arg.OrgID, pq.Array(arg.DeviceIdentifiers)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListDeviceIDsByIdentifiersRow + for rows.Next() { + var i ListDeviceIDsByIdentifiersRow + if err := rows.Scan(&i.ID, &i.DeviceIdentifier); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listFirmwareRolloutDevices = `-- name: ListFirmwareRolloutDevices :many + +SELECT rd.device_id, + d.device_identifier, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dd.ip_address, '')::text AS ip_address, + rd.batch_index, + rd.position, + rd.attempts, + rd.first_sent_at, + rd.last_sent_at, + rd.halted_at, + rd.halt_reason, + rd.last_error, + rd.skip_note, + rd.excluded_at, + rd.baseline_status, + rd.baseline_hash_rate_hs, + rd.baseline_power_w, + rd.baseline_efficiency_jh, + rd.baseline_temp_c, + rd.baseline_open_errors, + COALESCE(ds.status::text, '')::text AS status, + hm.hash_rate_hs, + hm.power_w, + hm.efficiency_jh, + hm.temp_c, + (SELECT count(*) FROM errors e + WHERE e.device_id = d.id AND e.closed_at IS NULL AND e.severity IN (1, 2, 3, 4))::int AS open_errors, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum, + EXISTS ( + SELECT 1 FROM release_channel_member m + WHERE m.device_id = d.id AND m.channel_id = r.channel_id + ) + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(r.model COLLATE "C") AS in_scope +FROM firmware_rollout_device rd +JOIN firmware_rollout r ON r.id = rd.rollout_id +JOIN device d ON d.id = rd.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_status ds ON ds.device_id = d.id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.hash_rate_hs, dm.power_w, dm.efficiency_jh, dm.temp_c + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE rd.rollout_id = $1 +ORDER BY rd.position NULLS LAST, d.device_identifier +` + +type ListFirmwareRolloutDevicesRow struct { + DeviceID int64 + DeviceIdentifier string + FirmwareVersion string + IpAddress string + BatchIndex sql.NullInt32 + Position sql.NullInt32 + Attempts int32 + FirstSentAt sql.NullTime + LastSentAt sql.NullTime + HaltedAt sql.NullTime + HaltReason string + LastError string + SkipNote string + ExcludedAt sql.NullTime + BaselineStatus sql.NullString + BaselineHashRateHs sql.NullFloat64 + BaselinePowerW sql.NullFloat64 + BaselineEfficiencyJh sql.NullFloat64 + BaselineTempC sql.NullFloat64 + BaselineOpenErrors sql.NullInt32 + Status string + HashRateHs sql.NullFloat64 + PowerW sql.NullFloat64 + EfficiencyJh sql.NullFloat64 + TempC sql.NullFloat64 + OpenErrors int32 + LastDeployedFirmwareChecksum string + InScope sql.NullBool +} + +// --- Rollout devices --- +// Every miner in a rollout with its bookkeeping, baseline, live health (device +// status, latest telemetry within 15 minutes, open errors), provenance and +// whether it is still a member of the channel for the rollout's pair. +func (q *Queries) ListFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]ListFirmwareRolloutDevicesRow, error) { + rows, err := q.query(ctx, q.listFirmwareRolloutDevicesStmt, listFirmwareRolloutDevices, rolloutID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListFirmwareRolloutDevicesRow + for rows.Next() { + var i ListFirmwareRolloutDevicesRow + if err := rows.Scan( + &i.DeviceID, + &i.DeviceIdentifier, + &i.FirmwareVersion, + &i.IpAddress, + &i.BatchIndex, + &i.Position, + &i.Attempts, + &i.FirstSentAt, + &i.LastSentAt, + &i.HaltedAt, + &i.HaltReason, + &i.LastError, + &i.SkipNote, + &i.ExcludedAt, + &i.BaselineStatus, + &i.BaselineHashRateHs, + &i.BaselinePowerW, + &i.BaselineEfficiencyJh, + &i.BaselineTempC, + &i.BaselineOpenErrors, + &i.Status, + &i.HashRateHs, + &i.PowerW, + &i.EfficiencyJh, + &i.TempC, + &i.OpenErrors, + &i.LastDeployedFirmwareChecksum, + &i.InScope, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listFirmwareRollouts = `-- name: ListFirmwareRollouts :many +SELECT r.id, r.org_id, r.channel_id, r.manufacturer, r.model, r.firmware_checksum, r.firmware_version, r.previous_firmware_checksum, r.previous_firmware_version, r.assignment_generation, r.status, r.cancel_reason, r.stage, r.method, r.order_by, r.batch_size, r.pilot_size, r.wait_between_batches_seconds, r.review_after_each_batch, r.auto_continue, r.stabilization_seconds, r.max_hashrate_drop_percent, r.max_efficiency_increase_percent, r.max_temp_increase_c, r.max_new_errors, r.min_sample_coverage_percent, r.max_concurrent_offline, r.controller_timeout_seconds, r.batch_count, r.current_batch, r.stage_changed_at, r.paused_at, r.revision, r.updated_at, r.started_by_type, r.started_by_id, r.started_by_name, r.last_action_by_type, r.last_action_by_id, r.last_action_by_name, r.created_at, r.finished_at, c.name AS channel_name +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.org_id = $1 + AND ($2::bigint IS NULL OR r.channel_id = $2) + AND ($3::text IS NULL OR r.status = $3) + AND ($4::timestamptz IS NULL OR r.updated_at >= $4) + AND ( + $5::timestamptz IS NULL + OR (r.created_at, r.id) < ($5::timestamptz, $6::bigint) + ) +ORDER BY r.created_at DESC, r.id DESC +LIMIT $7 +` + +type ListFirmwareRolloutsParams struct { + OrgID int64 + ChannelID sql.NullInt64 + Status sql.NullString + UpdatedAfter sql.NullTime + BeforeCreatedAt sql.NullTime + BeforeID sql.NullInt64 + PageLimit int32 +} + +type ListFirmwareRolloutsRow struct { + FirmwareRollout FirmwareRollout + ChannelName string +} + +// Newest first. The cursor is the (created_at, id) of the last row of the +// previous page; rows strictly older than it are returned. +func (q *Queries) ListFirmwareRollouts(ctx context.Context, arg ListFirmwareRolloutsParams) ([]ListFirmwareRolloutsRow, error) { + rows, err := q.query(ctx, q.listFirmwareRolloutsStmt, listFirmwareRollouts, + arg.OrgID, + arg.ChannelID, + arg.Status, + arg.UpdatedAfter, + arg.BeforeCreatedAt, + arg.BeforeID, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListFirmwareRolloutsRow + for rows.Next() { + var i ListFirmwareRolloutsRow + if err := rows.Scan( + &i.FirmwareRollout.ID, + &i.FirmwareRollout.OrgID, + &i.FirmwareRollout.ChannelID, + &i.FirmwareRollout.Manufacturer, + &i.FirmwareRollout.Model, + &i.FirmwareRollout.FirmwareChecksum, + &i.FirmwareRollout.FirmwareVersion, + &i.FirmwareRollout.PreviousFirmwareChecksum, + &i.FirmwareRollout.PreviousFirmwareVersion, + &i.FirmwareRollout.AssignmentGeneration, + &i.FirmwareRollout.Status, + &i.FirmwareRollout.CancelReason, + &i.FirmwareRollout.Stage, + &i.FirmwareRollout.Method, + &i.FirmwareRollout.OrderBy, + &i.FirmwareRollout.BatchSize, + &i.FirmwareRollout.PilotSize, + &i.FirmwareRollout.WaitBetweenBatchesSeconds, + &i.FirmwareRollout.ReviewAfterEachBatch, + &i.FirmwareRollout.AutoContinue, + &i.FirmwareRollout.StabilizationSeconds, + &i.FirmwareRollout.MaxHashrateDropPercent, + &i.FirmwareRollout.MaxEfficiencyIncreasePercent, + &i.FirmwareRollout.MaxTempIncreaseC, + &i.FirmwareRollout.MaxNewErrors, + &i.FirmwareRollout.MinSampleCoveragePercent, + &i.FirmwareRollout.MaxConcurrentOffline, + &i.FirmwareRollout.ControllerTimeoutSeconds, + &i.FirmwareRollout.BatchCount, + &i.FirmwareRollout.CurrentBatch, + &i.FirmwareRollout.StageChangedAt, + &i.FirmwareRollout.PausedAt, + &i.FirmwareRollout.Revision, + &i.FirmwareRollout.UpdatedAt, + &i.FirmwareRollout.StartedByType, + &i.FirmwareRollout.StartedByID, + &i.FirmwareRollout.StartedByName, + &i.FirmwareRollout.LastActionByType, + &i.FirmwareRollout.LastActionByID, + &i.FirmwareRollout.LastActionByName, + &i.FirmwareRollout.CreatedAt, + &i.FirmwareRollout.FinishedAt, + &i.ChannelName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelFirmware = `-- name: ListReleaseChannelFirmware :many +SELECT f.channel_id, f.manufacturer, f.model, f.firmware_checksum, f.firmware_version, f.firmware_target_manufacturer, f.firmware_target_model, f.assignment_generation, f.assigned_by, f.updated_at +FROM release_channel_firmware f +JOIN release_channel c ON c.id = f.channel_id +WHERE c.org_id = $1 +ORDER BY f.channel_id, f.manufacturer, f.model +` + +// Every pair row of an org's channels, assigned or cleared. +func (q *Queries) ListReleaseChannelFirmware(ctx context.Context, orgID int64) ([]ReleaseChannelFirmware, error) { + rows, err := q.query(ctx, q.listReleaseChannelFirmwareStmt, listReleaseChannelFirmware, orgID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReleaseChannelFirmware + for rows.Next() { + var i ReleaseChannelFirmware + if err := rows.Scan( + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.AssignedBy, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelFirmwareNeedingRollout = `-- name: ListReleaseChannelFirmwareNeedingRollout :many +SELECT f.channel_id, f.manufacturer, f.model, f.firmware_checksum, f.firmware_version, + f.firmware_target_manufacturer, f.firmware_target_model, f.assignment_generation, + f.assigned_by, f.updated_at, c.org_id +FROM release_channel_firmware f +JOIN release_channel c ON c.id = f.channel_id +WHERE f.firmware_checksum <> '' +AND NOT EXISTS ( + SELECT 1 FROM firmware_rollout r + WHERE r.channel_id = f.channel_id + AND lower(r.manufacturer COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(r.model COLLATE "C") = lower(f.model COLLATE "C") + AND r.status = 'active' +) +AND EXISTS ( + SELECT 1 + FROM release_channel_member m + JOIN device d ON d.id = m.device_id + JOIN discovered_device dd ON dd.id = d.discovered_device_id + LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id + WHERE m.channel_id = f.channel_id + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(f.model COLLATE "C") + AND NOT ( + COALESCE(dd.firmware_version, '') = f.firmware_version + AND COALESCE(dep.firmware_checksum, '') = f.firmware_checksum + ) + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = f.channel_id + AND lower(r.manufacturer COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(r.model COLLATE "C") = lower(f.model COLLATE "C") + AND r.assignment_generation = f.assignment_generation + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +) +ORDER BY f.channel_id, f.manufacturer, f.model +` + +type ListReleaseChannelFirmwareNeedingRolloutRow struct { + ChannelID int64 + Manufacturer string + Model string + FirmwareChecksum string + FirmwareVersion string + FirmwareTargetManufacturer string + FirmwareTargetModel string + AssignmentGeneration int64 + AssignedBy int64 + UpdatedAt time.Time + OrgID int64 +} + +// Assigned pairs with no active rollout and at least one mismatched, +// unsuppressed member: late joiners, re-entries and miners that drifted. +func (q *Queries) ListReleaseChannelFirmwareNeedingRollout(ctx context.Context) ([]ListReleaseChannelFirmwareNeedingRolloutRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelFirmwareNeedingRolloutStmt, listReleaseChannelFirmwareNeedingRollout) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelFirmwareNeedingRolloutRow + for rows.Next() { + var i ListReleaseChannelFirmwareNeedingRolloutRow + if err := rows.Scan( + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.AssignedBy, + &i.UpdatedAt, + &i.OrgID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelMembers = `-- name: ListReleaseChannelMembers :many + +SELECT m.channel_id, + m.device_id, + m.conflicted, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +WHERE m.org_id = $1 +ORDER BY d.device_identifier +` + +type ListReleaseChannelMembersRow struct { + ChannelID int64 + DeviceID int64 + Conflicted bool + DeviceIdentifier string + Manufacturer string + Model string + FirmwareVersion string + LastDeployedFirmwareChecksum string +} + +// --- Membership --- +// Every miner resolved into one of the org's channels, with its observed +// identity, reported firmware and managed-deployment provenance. +func (q *Queries) ListReleaseChannelMembers(ctx context.Context, orgID int64) ([]ListReleaseChannelMembersRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelMembersStmt, listReleaseChannelMembers, orgID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelMembersRow + for rows.Next() { + var i ListReleaseChannelMembersRow + if err := rows.Scan( + &i.ChannelID, + &i.DeviceID, + &i.Conflicted, + &i.DeviceIdentifier, + &i.Manufacturer, + &i.Model, + &i.FirmwareVersion, + &i.LastDeployedFirmwareChecksum, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelMembershipConflictsPage = `-- name: ListReleaseChannelMembershipConflictsPage :many +SELECT k.device_id, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + k.channel_id, + c.name AS channel_name, + k.specificity::int AS specificity, + k.resolution +FROM release_channel_conflict k +JOIN release_channel c ON c.id = k.channel_id +JOIN device d ON d.id = k.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +WHERE k.org_id = $1 + AND ($2::bigint IS NULL OR k.channel_id = $2) + AND ( + $3::text IS NULL + OR (d.device_identifier, d.id, k.channel_id) > ($3::text, $4::bigint, $5::bigint) + ) +ORDER BY d.device_identifier, d.id, k.channel_id +LIMIT $6 +` + +type ListReleaseChannelMembershipConflictsPageParams struct { + OrgID int64 + ChannelID sql.NullInt64 + AfterIdentifier sql.NullString + AfterDeviceID sql.NullInt64 + AfterChannelID sql.NullInt64 + PageLimit int32 +} + +type ListReleaseChannelMembershipConflictsPageRow struct { + DeviceID int64 + DeviceIdentifier string + Manufacturer string + Model string + ChannelID int64 + ChannelName string + Specificity int32 + Resolution string +} + +// One page of (miner, channel) relations for miners matched by several +// channels, optionally for one channel, ordered by identifier then channel. +func (q *Queries) ListReleaseChannelMembershipConflictsPage(ctx context.Context, arg ListReleaseChannelMembershipConflictsPageParams) ([]ListReleaseChannelMembershipConflictsPageRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelMembershipConflictsPageStmt, listReleaseChannelMembershipConflictsPage, + arg.OrgID, + arg.ChannelID, + arg.AfterIdentifier, + arg.AfterDeviceID, + arg.AfterChannelID, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelMembershipConflictsPageRow + for rows.Next() { + var i ListReleaseChannelMembershipConflictsPageRow + if err := rows.Scan( + &i.DeviceID, + &i.DeviceIdentifier, + &i.Manufacturer, + &i.Model, + &i.ChannelID, + &i.ChannelName, + &i.Specificity, + &i.Resolution, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelMinersPage = `-- name: ListReleaseChannelMinersPage :many +SELECT m.device_id, + m.conflicted, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum +FROM release_channel_member m +JOIN release_channel c ON c.id = m.channel_id +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +WHERE m.channel_id = $1 + AND c.org_id = $2 + AND ($3::text IS NULL OR COALESCE(dd.manufacturer, '') = $3) + AND ($4::text IS NULL OR COALESCE(dd.model, '') = $4) + AND ( + $5::text IS NULL + OR (d.device_identifier, d.id) > ($5::text, $6::bigint) + ) +ORDER BY d.device_identifier, d.id +LIMIT $7 +` + +type ListReleaseChannelMinersPageParams struct { + ChannelID int64 + OrgID int64 + Manufacturer sql.NullString + Model sql.NullString + AfterIdentifier sql.NullString + AfterDeviceID sql.NullInt64 + PageLimit int32 +} + +type ListReleaseChannelMinersPageRow struct { + DeviceID int64 + Conflicted bool + DeviceIdentifier string + Manufacturer string + Model string + FirmwareVersion string + LastDeployedFirmwareChecksum string +} + +// One page of a channel's members, optionally filtered by observed +// manufacturer and model (verbatim), ordered by identifier. The cursor is the +// (device_identifier, device_id) of the last row of the previous page. +func (q *Queries) ListReleaseChannelMinersPage(ctx context.Context, arg ListReleaseChannelMinersPageParams) ([]ListReleaseChannelMinersPageRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelMinersPageStmt, listReleaseChannelMinersPage, + arg.ChannelID, + arg.OrgID, + arg.Manufacturer, + arg.Model, + arg.AfterIdentifier, + arg.AfterDeviceID, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelMinersPageRow + for rows.Next() { + var i ListReleaseChannelMinersPageRow + if err := rows.Scan( + &i.DeviceID, + &i.Conflicted, + &i.DeviceIdentifier, + &i.Manufacturer, + &i.Model, + &i.FirmwareVersion, + &i.LastDeployedFirmwareChecksum, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelMismatchedMembers = `-- name: ListReleaseChannelMismatchedMembers :many +SELECT d.id AS device_id, + d.device_identifier, + hm.efficiency_jh +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.efficiency_jh + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE m.channel_id = $1 + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower($2::text COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower($3::text COLLATE "C") + AND NOT ( + COALESCE(dd.firmware_version, '') = $4::text + AND COALESCE(dep.firmware_checksum, '') = $5::text + ) + AND NOT EXISTS ( + SELECT 1 FROM firmware_rollout_device rd + WHERE rd.rollout_id = $6 AND rd.device_id = d.id + ) + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = $1 + AND lower(r.manufacturer COLLATE "C") = lower($2::text COLLATE "C") + AND lower(r.model COLLATE "C") = lower($3::text COLLATE "C") + AND r.assignment_generation = $7::bigint + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +ORDER BY d.device_identifier +` + +type ListReleaseChannelMismatchedMembersParams struct { + ChannelID int64 + Manufacturer string + Model string + FirmwareVersion string + FirmwareChecksum string + RolloutID int64 + AssignmentGeneration int64 +} + +type ListReleaseChannelMismatchedMembersRow struct { + DeviceID int64 + DeviceIdentifier string + EfficiencyJh sql.NullFloat64 +} + +// Members of one pair that are mismatched under the mismatch rule (reported +// version or provenance differs from the assignment), are not already part of +// rollout_id (0 for a new rollout), and are not suppressed: a member halted +// (failed, skipped, or left behind by a cancellation) in the most recent +// rollout of the current generation that holds it stays out until retried. +// Carries the latest efficiency sample for ordering. +func (q *Queries) ListReleaseChannelMismatchedMembers(ctx context.Context, arg ListReleaseChannelMismatchedMembersParams) ([]ListReleaseChannelMismatchedMembersRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelMismatchedMembersStmt, listReleaseChannelMismatchedMembers, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + arg.FirmwareVersion, + arg.FirmwareChecksum, + arg.RolloutID, + arg.AssignmentGeneration, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelMismatchedMembersRow + for rows.Next() { + var i ListReleaseChannelMismatchedMembersRow + if err := rows.Scan(&i.DeviceID, &i.DeviceIdentifier, &i.EfficiencyJh); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelModelGroupsPage = `-- name: ListReleaseChannelModelGroupsPage :many +WITH members AS ( + SELECT m.device_id, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS deployed_checksum + FROM release_channel_member m + JOIN device d ON d.id = m.device_id + JOIN discovered_device dd ON dd.id = d.discovered_device_id + LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id + WHERE m.channel_id = $1 +), groups AS ( + SELECT manufacturer, + model, + count(*)::int AS miner_count, + (array_agg(DISTINCT firmware_version ORDER BY firmware_version))[1:10]::text[] AS reported_versions, + count(DISTINCT firmware_version)::int AS reported_version_count + FROM members + GROUP BY manufacturer, model + UNION ALL + SELECT f.manufacturer, f.model, 0, ARRAY[]::text[], 0 + FROM release_channel_firmware f + WHERE f.channel_id = $1 + AND f.firmware_checksum <> '' + AND NOT EXISTS ( + SELECT 1 FROM members mm + WHERE lower(btrim(mm.manufacturer) COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(btrim(mm.model) COLLATE "C") = lower(f.model COLLATE "C") + ) +) +SELECT g.manufacturer, + g.model, + g.miner_count, + g.reported_versions, + g.reported_version_count, + COALESCE(f.firmware_checksum, '')::text AS firmware_checksum, + COALESCE(f.firmware_version, '')::text AS firmware_version, + COALESCE(f.firmware_target_manufacturer, '')::text AS firmware_target_manufacturer, + COALESCE(f.firmware_target_model, '')::text AS firmware_target_model, + COALESCE(f.assignment_generation, 0)::bigint AS assignment_generation, + COALESCE(r.id, 0)::bigint AS active_rollout_id, + (SELECT count(*) FROM members mm + WHERE mm.manufacturer = g.manufacturer AND mm.model = g.model + AND f.firmware_checksum IS NOT NULL AND f.firmware_checksum <> '' + AND mm.firmware_version = f.firmware_version + AND mm.deployed_checksum = f.firmware_checksum)::int AS on_target_count +FROM groups g +JOIN release_channel c ON c.id = $1 AND c.org_id = $2 +LEFT JOIN release_channel_firmware f + ON f.channel_id = c.id + AND lower(f.manufacturer COLLATE "C") = lower(btrim(g.manufacturer) COLLATE "C") + AND lower(f.model COLLATE "C") = lower(btrim(g.model) COLLATE "C") +LEFT JOIN firmware_rollout r + ON r.channel_id = c.id AND r.status = 'active' + AND lower(r.manufacturer COLLATE "C") = lower(btrim(g.manufacturer) COLLATE "C") + AND lower(r.model COLLATE "C") = lower(btrim(g.model) COLLATE "C") +WHERE ( + $3::text IS NULL + OR (g.manufacturer, g.model) > ($3::text, $4::text) +) +ORDER BY g.manufacturer, g.model +LIMIT $5 +` + +type ListReleaseChannelModelGroupsPageParams struct { + ChannelID int64 + OrgID int64 + AfterManufacturer sql.NullString + AfterModel sql.NullString + PageLimit int32 +} + +type ListReleaseChannelModelGroupsPageRow struct { + Manufacturer string + Model string + MinerCount int32 + ReportedVersions []string + ReportedVersionCount int32 + FirmwareChecksum string + FirmwareVersion string + FirmwareTargetManufacturer string + FirmwareTargetModel string + AssignmentGeneration int64 + ActiveRolloutID int64 + OnTargetCount int32 +} + +// One page of a channel's manufacturer/model groups: every observed pair among +// its members plus every assigned pair with no current members, each joined to +// its assignment (by folded key) and active rollout. Ordered by observed +// manufacturer then model; the cursor is the pair of the last row. +func (q *Queries) ListReleaseChannelModelGroupsPage(ctx context.Context, arg ListReleaseChannelModelGroupsPageParams) ([]ListReleaseChannelModelGroupsPageRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelModelGroupsPageStmt, listReleaseChannelModelGroupsPage, + arg.ChannelID, + arg.OrgID, + arg.AfterManufacturer, + arg.AfterModel, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelModelGroupsPageRow + for rows.Next() { + var i ListReleaseChannelModelGroupsPageRow + if err := rows.Scan( + &i.Manufacturer, + &i.Model, + &i.MinerCount, + pq.Array(&i.ReportedVersions), + &i.ReportedVersionCount, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.ActiveRolloutID, + &i.OnTargetCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelSuppressedMembers = `-- name: ListReleaseChannelSuppressedMembers :many +SELECT d.id AS device_id, + d.device_identifier +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +WHERE m.channel_id = $1 + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower($2::text COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower($3::text COLLATE "C") + AND EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = $1 + AND lower(r.manufacturer COLLATE "C") = lower($2::text COLLATE "C") + AND lower(r.model COLLATE "C") = lower($3::text COLLATE "C") + AND r.assignment_generation = $4::bigint + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +ORDER BY d.device_identifier +` + +type ListReleaseChannelSuppressedMembersParams struct { + ChannelID int64 + Manufacturer string + Model string + AssignmentGeneration int64 +} + +type ListReleaseChannelSuppressedMembersRow struct { + DeviceID int64 + DeviceIdentifier string +} + +// Members of one pair the enforcement rule currently suppresses: halted in +// the most recent rollout of the given generation that holds them. +// RetryFailedRolloutDevices re-queues exactly this set. +func (q *Queries) ListReleaseChannelSuppressedMembers(ctx context.Context, arg ListReleaseChannelSuppressedMembersParams) ([]ListReleaseChannelSuppressedMembersRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelSuppressedMembersStmt, listReleaseChannelSuppressedMembers, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + arg.AssignmentGeneration, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelSuppressedMembersRow + for rows.Next() { + var i ListReleaseChannelSuppressedMembersRow + if err := rows.Scan(&i.DeviceID, &i.DeviceIdentifier); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannelTargets = `-- name: ListReleaseChannelTargets :many +SELECT t.channel_id, + t.target_type, + t.target_id, + COALESCE(d.device_identifier, '')::text AS device_identifier +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +LEFT JOIN device d ON t.target_type = 'miner' AND d.id = t.target_id +WHERE c.org_id = $1 +ORDER BY t.channel_id, t.target_type, t.target_id +` + +type ListReleaseChannelTargetsRow struct { + ChannelID int64 + TargetType string + TargetID int64 + DeviceIdentifier string +} + +// Targets of every channel in the org; miner targets carry their identifier. +func (q *Queries) ListReleaseChannelTargets(ctx context.Context, orgID int64) ([]ListReleaseChannelTargetsRow, error) { + rows, err := q.query(ctx, q.listReleaseChannelTargetsStmt, listReleaseChannelTargets, orgID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListReleaseChannelTargetsRow + for rows.Next() { + var i ListReleaseChannelTargetsRow + if err := rows.Scan( + &i.ChannelID, + &i.TargetType, + &i.TargetID, + &i.DeviceIdentifier, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listReleaseChannels = `-- name: ListReleaseChannels :many +SELECT id, org_id, name, description, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, created_by, created_at, updated_at FROM release_channel +WHERE org_id = $1 +ORDER BY name +` + +func (q *Queries) ListReleaseChannels(ctx context.Context, orgID int64) ([]ReleaseChannel, error) { + rows, err := q.query(ctx, q.listReleaseChannelsStmt, listReleaseChannels, orgID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReleaseChannel + for rows.Next() { + var i ReleaseChannel + if err := rows.Scan( + &i.ID, + &i.OrgID, + &i.Name, + &i.Description, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const lockReleaseChannelScopes = `-- name: LockReleaseChannelScopes :exec +SELECT pg_advisory_xact_lock(hashtextextended('release_channel_scope:' || ($1::bigint)::text, 0)) +` + +// Serializes scope writes per org so the overlap check and the target +// insert happen in one critical section. +func (q *Queries) LockReleaseChannelScopes(ctx context.Context, orgID int64) error { + _, err := q.exec(ctx, q.lockReleaseChannelScopesStmt, lockReleaseChannelScopes, orgID) + return err +} + +const markFirmwareRolloutDevicesSent = `-- name: MarkFirmwareRolloutDevicesSent :exec +UPDATE firmware_rollout_device +SET attempts = attempts + 1, + first_sent_at = COALESCE(first_sent_at, now()), + last_sent_at = now() +WHERE rollout_id = $1 + AND device_id = ANY($2::bigint[]) +` + +type MarkFirmwareRolloutDevicesSentParams struct { + RolloutID int64 + DeviceIds []int64 +} + +func (q *Queries) MarkFirmwareRolloutDevicesSent(ctx context.Context, arg MarkFirmwareRolloutDevicesSentParams) error { + _, err := q.exec(ctx, q.markFirmwareRolloutDevicesSentStmt, markFirmwareRolloutDevicesSent, arg.RolloutID, pq.Array(arg.DeviceIds)) + return err +} + +const pauseFirmwareRollout = `-- name: PauseFirmwareRollout :execrows +UPDATE firmware_rollout +SET paused_at = now(), + last_action_by_type = $1, + last_action_by_id = $2, + last_action_by_name = $3 +WHERE id = $4 AND status = 'active' AND paused_at IS NULL +` + +type PauseFirmwareRolloutParams struct { + ActorType string + ActorID int64 + ActorName string + RolloutID int64 +} + +func (q *Queries) PauseFirmwareRollout(ctx context.Context, arg PauseFirmwareRolloutParams) (int64, error) { + result, err := q.exec(ctx, q.pauseFirmwareRolloutStmt, pauseFirmwareRollout, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.RolloutID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const recordFirmwareDeployment = `-- name: RecordFirmwareDeployment :exec +INSERT INTO device_firmware_deployment (device_id, firmware_checksum, firmware_version, rollout_id, deployed_at) +SELECT unnest($1::bigint[]), $2, $3, $4, now() +ON CONFLICT (device_id) DO UPDATE +SET firmware_checksum = EXCLUDED.firmware_checksum, + firmware_version = EXCLUDED.firmware_version, + rollout_id = EXCLUDED.rollout_id, + deployed_at = now() +` + +type RecordFirmwareDeploymentParams struct { + DeviceIds []int64 + FirmwareChecksum string + FirmwareVersion string + RolloutID sql.NullInt64 +} + +// Managed-deployment provenance: the miners listed reported the artifact a +// rollout dispatched to them. +func (q *Queries) RecordFirmwareDeployment(ctx context.Context, arg RecordFirmwareDeploymentParams) error { + _, err := q.exec(ctx, q.recordFirmwareDeploymentStmt, recordFirmwareDeployment, + pq.Array(arg.DeviceIds), + arg.FirmwareChecksum, + arg.FirmwareVersion, + arg.RolloutID, + ) + return err +} + +const recordFirmwareRolloutAction = `-- name: RecordFirmwareRolloutAction :exec +UPDATE firmware_rollout +SET last_action_by_type = $1, + last_action_by_id = $2, + last_action_by_name = $3 +WHERE id = $4 +` + +type RecordFirmwareRolloutActionParams struct { + ActorType string + ActorID int64 + ActorName string + RolloutID int64 +} + +// Attributes an action that changes only the rollout's devices (retry) to +// its actor; the update itself bumps the revision. +func (q *Queries) RecordFirmwareRolloutAction(ctx context.Context, arg RecordFirmwareRolloutActionParams) error { + _, err := q.exec(ctx, q.recordFirmwareRolloutActionStmt, recordFirmwareRolloutAction, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.RolloutID, + ) + return err +} + +const requeueFirmwareRolloutDevices = `-- name: RequeueFirmwareRolloutDevices :many +UPDATE firmware_rollout_device +SET halted_at = NULL, + halt_reason = '', + last_error = '', + skip_note = '', + attempts = 0, + first_sent_at = NULL, + last_sent_at = NULL +WHERE rollout_id = $1 + AND halted_at IS NOT NULL +RETURNING device_id +` + +// Re-queues every halted miner of an active rollout from scratch and +// returns them. +func (q *Queries) RequeueFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]int64, error) { + rows, err := q.query(ctx, q.requeueFirmwareRolloutDevicesStmt, requeueFirmwareRolloutDevices, rolloutID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var device_id int64 + if err := rows.Scan(&device_id); err != nil { + return nil, err + } + items = append(items, device_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const resolveReleaseChannelScope = `-- name: ResolveReleaseChannelScope :many +WITH scoped AS ( + SELECT d.id AS device_id + FROM device d + WHERE d.org_id = $2 + AND d.deleted_at IS NULL + AND d.device_identifier = ANY($3::text[]) + UNION + SELECT d.id + FROM fleet_device_placement p + JOIN device d ON d.device_identifier = p.device_id AND d.org_id = p.org_id + WHERE p.org_id = $2 + AND ( + p.group_id = ANY($4::bigint[]) + OR p.rack_id = ANY($5::bigint[]) + OR p.building_id = ANY($6::bigint[]) + OR p.site_id = ANY($7::bigint[]) + ) +) +SELECT s.device_id, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(owner.channel_id, 0)::bigint AS owner_channel_id, + COALESCE(owner.name, '')::text AS owner_channel_name +FROM scoped s +JOIN device d ON d.id = s.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN LATERAL ( + SELECT c.id AS channel_id, c.name + FROM release_channel_match rm + JOIN release_channel c ON c.id = rm.channel_id + WHERE rm.device_id = s.device_id + AND rm.channel_id <> $1 + ORDER BY c.id + LIMIT 1 +) owner ON true +ORDER BY d.device_identifier +` + +type ResolveReleaseChannelScopeParams struct { + ExcludeChannelID int64 + OrgID int64 + DeviceIdentifiers []string + GroupIds []int64 + RackIds []int64 + BuildingIds []int64 + SiteIds []int64 +} + +type ResolveReleaseChannelScopeRow struct { + DeviceID int64 + DeviceIdentifier string + Manufacturer string + Model string + FirmwareVersion string + OwnerChannelID int64 + OwnerChannelName string +} + +// Miners a candidate scope covers, each with the other channel (if any) +// whose selectors already match it. Used to preview a scope and to reject +// overlapping saves; exclude_channel_id is the channel being edited. +func (q *Queries) ResolveReleaseChannelScope(ctx context.Context, arg ResolveReleaseChannelScopeParams) ([]ResolveReleaseChannelScopeRow, error) { + rows, err := q.query(ctx, q.resolveReleaseChannelScopeStmt, resolveReleaseChannelScope, + arg.ExcludeChannelID, + arg.OrgID, + pq.Array(arg.DeviceIdentifiers), + pq.Array(arg.GroupIds), + pq.Array(arg.RackIds), + pq.Array(arg.BuildingIds), + pq.Array(arg.SiteIds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ResolveReleaseChannelScopeRow + for rows.Next() { + var i ResolveReleaseChannelScopeRow + if err := rows.Scan( + &i.DeviceID, + &i.DeviceIdentifier, + &i.Manufacturer, + &i.Model, + &i.FirmwareVersion, + &i.OwnerChannelID, + &i.OwnerChannelName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const resumeFirmwareRollout = `-- name: ResumeFirmwareRollout :execrows +UPDATE firmware_rollout +SET paused_at = NULL, + last_action_by_type = $1, + last_action_by_id = $2, + last_action_by_name = $3 +WHERE id = $4 AND status = 'active' AND paused_at IS NOT NULL +` + +type ResumeFirmwareRolloutParams struct { + ActorType string + ActorID int64 + ActorName string + RolloutID int64 +} + +func (q *Queries) ResumeFirmwareRollout(ctx context.Context, arg ResumeFirmwareRolloutParams) (int64, error) { + result, err := q.exec(ctx, q.resumeFirmwareRolloutStmt, resumeFirmwareRollout, + arg.ActorType, + arg.ActorID, + arg.ActorName, + arg.RolloutID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const snapshotFirmwareRolloutDevices = `-- name: SnapshotFirmwareRolloutDevices :exec +INSERT INTO firmware_rollout_device ( + rollout_id, device_id, batch_index, position, + baseline_status, baseline_hash_rate_hs, baseline_power_w, baseline_efficiency_jh, baseline_temp_c, + baseline_open_errors, baseline_at +) +SELECT $1, + d.id, + $2::int, + $3::int + array_position($4::bigint[], d.id), + ds.status::text, + hm.hash_rate_hs, + hm.power_w, + hm.efficiency_jh, + hm.temp_c, + (SELECT count(*) FROM errors e + WHERE e.device_id = d.id AND e.closed_at IS NULL AND e.severity IN (1, 2, 3, 4))::int, + now() +FROM device d +LEFT JOIN device_status ds ON ds.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.hash_rate_hs, dm.power_w, dm.efficiency_jh, dm.temp_c + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE d.id = ANY($4::bigint[]) +ON CONFLICT (rollout_id, device_id) DO UPDATE +SET batch_index = COALESCE(firmware_rollout_device.batch_index, EXCLUDED.batch_index), + position = COALESCE(firmware_rollout_device.position, EXCLUDED.position), + excluded_at = NULL +` + +type SnapshotFirmwareRolloutDevicesParams struct { + RolloutID int64 + BatchIndex sql.NullInt32 + PositionOffset sql.NullInt32 + DeviceIds []int64 +} + +// Adds miners to a rollout with their batch (NULL for the unbatched rest / +// late joiners), their order (position_offset + index in device_ids; NULL +// offset for late joiners) and a baseline of their health, so post-update +// evidence can be compared against each miner's own past. A miner that +// re-enters the scope after being excluded is re-included and keeps its +// original batch and order. +func (q *Queries) SnapshotFirmwareRolloutDevices(ctx context.Context, arg SnapshotFirmwareRolloutDevicesParams) error { + _, err := q.exec(ctx, q.snapshotFirmwareRolloutDevicesStmt, snapshotFirmwareRolloutDevices, + arg.RolloutID, + arg.BatchIndex, + arg.PositionOffset, + pq.Array(arg.DeviceIds), + ) + return err +} + +const updateReleaseChannel = `-- name: UpdateReleaseChannel :one +UPDATE release_channel +SET name = $1, + description = $2, + method = $3, + order_by = $4, + batch_size = $5, + pilot_size = $6, + wait_between_batches_seconds = $7, + review_after_each_batch = $8, + auto_continue = $9, + stabilization_seconds = $10, + max_hashrate_drop_percent = $11::double precision, + max_efficiency_increase_percent = $12::double precision, + max_temp_increase_c = $13::double precision, + max_new_errors = $14::int, + min_sample_coverage_percent = $15::double precision, + max_concurrent_offline = $16, + controller_timeout_seconds = $17, + updated_at = now() +WHERE id = $18 AND org_id = $19 +RETURNING id, org_id, name, description, method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, created_by, created_at, updated_at +` + +type UpdateReleaseChannelParams struct { + Name string + Description string + Method string + OrderBy string + BatchSize int32 + PilotSize int32 + WaitBetweenBatchesSeconds int32 + ReviewAfterEachBatch bool + AutoContinue bool + StabilizationSeconds int32 + MaxHashrateDropPercent sql.NullFloat64 + MaxEfficiencyIncreasePercent sql.NullFloat64 + MaxTempIncreaseC sql.NullFloat64 + MaxNewErrors sql.NullInt32 + MinSampleCoveragePercent sql.NullFloat64 + MaxConcurrentOffline int32 + ControllerTimeoutSeconds int32 + ChannelID int64 + OrgID int64 +} + +func (q *Queries) UpdateReleaseChannel(ctx context.Context, arg UpdateReleaseChannelParams) (ReleaseChannel, error) { + row := q.queryRow(ctx, q.updateReleaseChannelStmt, updateReleaseChannel, + arg.Name, + arg.Description, + arg.Method, + arg.OrderBy, + arg.BatchSize, + arg.PilotSize, + arg.WaitBetweenBatchesSeconds, + arg.ReviewAfterEachBatch, + arg.AutoContinue, + arg.StabilizationSeconds, + arg.MaxHashrateDropPercent, + arg.MaxEfficiencyIncreasePercent, + arg.MaxTempIncreaseC, + arg.MaxNewErrors, + arg.MinSampleCoveragePercent, + arg.MaxConcurrentOffline, + arg.ControllerTimeoutSeconds, + arg.ChannelID, + arg.OrgID, + ) + var i ReleaseChannel + err := row.Scan( + &i.ID, + &i.OrgID, + &i.Name, + &i.Description, + &i.Method, + &i.OrderBy, + &i.BatchSize, + &i.PilotSize, + &i.WaitBetweenBatchesSeconds, + &i.ReviewAfterEachBatch, + &i.AutoContinue, + &i.StabilizationSeconds, + &i.MaxHashrateDropPercent, + &i.MaxEfficiencyIncreasePercent, + &i.MaxTempIncreaseC, + &i.MaxNewErrors, + &i.MinSampleCoveragePercent, + &i.MaxConcurrentOffline, + &i.ControllerTimeoutSeconds, + &i.CreatedBy, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const upsertReleaseChannelFirmware = `-- name: UpsertReleaseChannelFirmware :one + +INSERT INTO release_channel_firmware ( + channel_id, manufacturer, model, firmware_checksum, firmware_version, + firmware_target_manufacturer, firmware_target_model, assignment_generation, assigned_by +) +VALUES ( + $1, $2, $3, $4, $5, + $6, $7, 1, $8 +) +ON CONFLICT (channel_id, lower(manufacturer COLLATE "C"), lower(model COLLATE "C")) DO UPDATE +SET firmware_checksum = EXCLUDED.firmware_checksum, + firmware_version = EXCLUDED.firmware_version, + firmware_target_manufacturer = EXCLUDED.firmware_target_manufacturer, + firmware_target_model = EXCLUDED.firmware_target_model, + assignment_generation = release_channel_firmware.assignment_generation + 1, + assigned_by = EXCLUDED.assigned_by, + updated_at = now() +RETURNING channel_id, manufacturer, model, firmware_checksum, firmware_version, firmware_target_manufacturer, firmware_target_model, assignment_generation, assigned_by, updated_at +` + +type UpsertReleaseChannelFirmwareParams struct { + ChannelID int64 + Manufacturer string + Model string + FirmwareChecksum string + FirmwareVersion string + FirmwareTargetManufacturer string + FirmwareTargetModel string + AssignedBy int64 +} + +// --- Firmware assignments --- +// Assigns an artifact to a pair and advances the pair's generation. The +// stored key keeps the case it was first written with. +func (q *Queries) UpsertReleaseChannelFirmware(ctx context.Context, arg UpsertReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + row := q.queryRow(ctx, q.upsertReleaseChannelFirmwareStmt, upsertReleaseChannelFirmware, + arg.ChannelID, + arg.Manufacturer, + arg.Model, + arg.FirmwareChecksum, + arg.FirmwareVersion, + arg.FirmwareTargetManufacturer, + arg.FirmwareTargetModel, + arg.AssignedBy, + ) + var i ReleaseChannelFirmware + err := row.Scan( + &i.ChannelID, + &i.Manufacturer, + &i.Model, + &i.FirmwareChecksum, + &i.FirmwareVersion, + &i.FirmwareTargetManufacturer, + &i.FirmwareTargetModel, + &i.AssignmentGeneration, + &i.AssignedBy, + &i.UpdatedAt, + ) + return i, err +} diff --git a/server/generated/sqlc/retrying_querier.gen.go b/server/generated/sqlc/retrying_querier.gen.go index 6ad104ba41..53ca323777 100644 --- a/server/generated/sqlc/retrying_querier.gen.go +++ b/server/generated/sqlc/retrying_querier.gen.go @@ -78,6 +78,18 @@ func (q *retryingQuerier) AdminTerminateCurtailmentEvent(ctx context.Context, ar return result, err } +func (q *retryingQuerier) AdvanceFirmwareRolloutStage(ctx context.Context, arg AdvanceFirmwareRolloutStageParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "AdvanceFirmwareRolloutStage", func() error { + callResult, callErr := q.next.AdvanceFirmwareRolloutStage(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) AdvanceFleetMetricRollupProgress(ctx context.Context, arg AdvanceFleetMetricRollupProgressParams) error { return q.retrier.RetryQuery(ctx, "AdvanceFleetMetricRollupProgress", func() error { return q.next.AdvanceFleetMetricRollupProgress(ctx, arg) @@ -342,6 +354,12 @@ func (q *retryingQuerier) BumpCurtailmentTargetRetry(ctx context.Context, arg Bu return result, err } +func (q *retryingQuerier) CancelActiveFirmwareRollout(ctx context.Context, arg CancelActiveFirmwareRolloutParams) error { + return q.retrier.RetryQuery(ctx, "CancelActiveFirmwareRollout", func() error { + return q.next.CancelActiveFirmwareRollout(ctx, arg) + }) +} + func (q *retryingQuerier) CancelEnrollmentForFleetNode(ctx context.Context, arg CancelEnrollmentForFleetNodeParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "CancelEnrollmentForFleetNode", func() error { @@ -354,6 +372,18 @@ func (q *retryingQuerier) CancelEnrollmentForFleetNode(ctx context.Context, arg return result, err } +func (q *retryingQuerier) CancelFirmwareRollout(ctx context.Context, arg CancelFirmwareRolloutParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "CancelFirmwareRollout", func() error { + callResult, callErr := q.next.CancelFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) CancelPendingEnrollment(ctx context.Context, arg CancelPendingEnrollmentParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "CancelPendingEnrollment", func() error { @@ -588,6 +618,18 @@ func (q *retryingQuerier) ClearRackSlotPosition(ctx context.Context, arg ClearRa }) } +func (q *retryingQuerier) ClearReleaseChannelFirmware(ctx context.Context, arg ClearReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + var result ReleaseChannelFirmware + err := q.retrier.RetryQuery(ctx, "ClearReleaseChannelFirmware", func() error { + callResult, callErr := q.next.ClearReleaseChannelFirmware(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ClearRolePermissions(ctx context.Context, roleID int64) error { return q.retrier.RetryQuery(ctx, "ClearRolePermissions", func() error { return q.next.ClearRolePermissions(ctx, roleID) @@ -1062,6 +1104,18 @@ func (q *retryingQuerier) CreateDeviceSet(ctx context.Context, arg CreateDeviceS return result, err } +func (q *retryingQuerier) CreateFirmwareRollout(ctx context.Context, arg CreateFirmwareRolloutParams) (FirmwareRollout, error) { + var result FirmwareRollout + err := q.retrier.RetryQuery(ctx, "CreateFirmwareRollout", func() error { + callResult, callErr := q.next.CreateFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) CreateFleetNode(ctx context.Context, arg CreateFleetNodeParams) (CreateFleetNodeRow, error) { var result CreateFleetNodeRow err := q.retrier.RetryQuery(ctx, "CreateFleetNode", func() error { @@ -1158,6 +1212,18 @@ func (q *retryingQuerier) CreateRackExtension(ctx context.Context, arg CreateRac }) } +func (q *retryingQuerier) CreateReleaseChannel(ctx context.Context, arg CreateReleaseChannelParams) (ReleaseChannel, error) { + var result ReleaseChannel + err := q.retrier.RetryQuery(ctx, "CreateReleaseChannel", func() error { + callResult, callErr := q.next.CreateReleaseChannel(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) CreateRepairTicket(ctx context.Context, arg CreateRepairTicketParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "CreateRepairTicket", func() error { @@ -1428,6 +1494,24 @@ func (q *retryingQuerier) DeletePool(ctx context.Context, id int64) error { }) } +func (q *retryingQuerier) DeleteReleaseChannel(ctx context.Context, arg DeleteReleaseChannelParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "DeleteReleaseChannel", func() error { + callResult, callErr := q.next.DeleteReleaseChannel(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) DeleteReleaseChannelTargets(ctx context.Context, channelID int64) error { + return q.retrier.RetryQuery(ctx, "DeleteReleaseChannelTargets", func() error { + return q.next.DeleteReleaseChannelTargets(ctx, channelID) + }) +} + func (q *retryingQuerier) DeleteScheduleTargets(ctx context.Context, arg DeleteScheduleTargetsParams) error { return q.retrier.RetryQuery(ctx, "DeleteScheduleTargets", func() error { return q.next.DeleteScheduleTargets(ctx, arg) @@ -1512,6 +1596,12 @@ func (q *retryingQuerier) EnsureCurtailmentOrgConfig(ctx context.Context, orgID return result, err } +func (q *retryingQuerier) ExcludeFirmwareRolloutDevices(ctx context.Context, arg ExcludeFirmwareRolloutDevicesParams) error { + return q.retrier.RetryQuery(ctx, "ExcludeFirmwareRolloutDevices", func() error { + return q.next.ExcludeFirmwareRolloutDevices(ctx, arg) + }) +} + func (q *retryingQuerier) FindDeviceBuildingConflicts(ctx context.Context, arg FindDeviceBuildingConflictsParams) ([]FindDeviceBuildingConflictsRow, error) { var result []FindDeviceBuildingConflictsRow err := q.retrier.RetryQuery(ctx, "FindDeviceBuildingConflicts", func() error { @@ -1572,6 +1662,18 @@ func (q *retryingQuerier) FindDevicesWithSiteOrBuilding(ctx context.Context, arg return result, err } +func (q *retryingQuerier) FinishFirmwareRollout(ctx context.Context, arg FinishFirmwareRolloutParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "FinishFirmwareRollout", func() error { + callResult, callErr := q.next.FinishFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) FinishTerminalCommandBatches(ctx context.Context, finishLimit int32) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "FinishTerminalCommandBatches", func() error { @@ -1596,6 +1698,18 @@ func (q *retryingQuerier) ForceReleaseCurtailmentEvent(ctx context.Context, arg return result, err } +func (q *retryingQuerier) GetActiveFirmwareRolloutForPair(ctx context.Context, arg GetActiveFirmwareRolloutForPairParams) (FirmwareRollout, error) { + var result FirmwareRollout + err := q.retrier.RetryQuery(ctx, "GetActiveFirmwareRolloutForPair", func() error { + callResult, callErr := q.next.GetActiveFirmwareRolloutForPair(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) GetActiveSchedules(ctx context.Context) ([]Schedule, error) { var result []Schedule err := q.retrier.RetryQuery(ctx, "GetActiveSchedules", func() error { @@ -2580,6 +2694,42 @@ func (q *retryingQuerier) GetFilteredTicketStats(ctx context.Context, arg GetFil return result, err } +func (q *retryingQuerier) GetFirmwareRollout(ctx context.Context, arg GetFirmwareRolloutParams) (FirmwareRollout, error) { + var result FirmwareRollout + err := q.retrier.RetryQuery(ctx, "GetFirmwareRollout", func() error { + callResult, callErr := q.next.GetFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) GetFirmwareRolloutForUpdate(ctx context.Context, arg GetFirmwareRolloutForUpdateParams) (FirmwareRollout, error) { + var result FirmwareRollout + err := q.retrier.RetryQuery(ctx, "GetFirmwareRolloutForUpdate", func() error { + callResult, callErr := q.next.GetFirmwareRolloutForUpdate(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) GetFirmwareRolloutWithChannel(ctx context.Context, arg GetFirmwareRolloutWithChannelParams) (GetFirmwareRolloutWithChannelRow, error) { + var result GetFirmwareRolloutWithChannelRow + err := q.retrier.RetryQuery(ctx, "GetFirmwareRolloutWithChannel", func() error { + callResult, callErr := q.next.GetFirmwareRolloutWithChannel(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) GetFleetMetricRollupCoverage(ctx context.Context) (GetFleetMetricRollupCoverageRow, error) { var result GetFleetMetricRollupCoverageRow err := q.retrier.RetryQuery(ctx, "GetFleetMetricRollupCoverage", func() error { @@ -2772,6 +2922,18 @@ func (q *retryingQuerier) GetLatestDeviceMetrics(ctx context.Context, arg GetLat return result, err } +func (q *retryingQuerier) GetLatestFirmwareRolloutForPair(ctx context.Context, arg GetLatestFirmwareRolloutForPairParams) (FirmwareRollout, error) { + var result FirmwareRollout + err := q.retrier.RetryQuery(ctx, "GetLatestFirmwareRolloutForPair", func() error { + callResult, callErr := q.next.GetLatestFirmwareRolloutForPair(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) GetLatestFleetMetricRollupBucket(ctx context.Context) (time.Time, error) { var result time.Time err := q.retrier.RetryQuery(ctx, "GetLatestFleetMetricRollupBucket", func() error { @@ -3216,6 +3378,30 @@ func (q *retryingQuerier) GetRackSlots(ctx context.Context, arg GetRackSlotsPara return result, err } +func (q *retryingQuerier) GetReleaseChannel(ctx context.Context, arg GetReleaseChannelParams) (ReleaseChannel, error) { + var result ReleaseChannel + err := q.retrier.RetryQuery(ctx, "GetReleaseChannel", func() error { + callResult, callErr := q.next.GetReleaseChannel(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) GetReleaseChannelFirmware(ctx context.Context, arg GetReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + var result ReleaseChannelFirmware + err := q.retrier.RetryQuery(ctx, "GetReleaseChannelFirmware", func() error { + callResult, callErr := q.next.GetReleaseChannelFirmware(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) GetReleaseChannelSetting(ctx context.Context, organizationID int64) (ReleaseChannelSetting, error) { var result ReleaseChannelSetting err := q.retrier.RetryQuery(ctx, "GetReleaseChannelSetting", func() error { @@ -3564,6 +3750,12 @@ func (q *retryingQuerier) GetUsersForOrganization(ctx context.Context, organizat return result, err } +func (q *retryingQuerier) HaltFirmwareRolloutDevices(ctx context.Context, arg HaltFirmwareRolloutDevicesParams) error { + return q.retrier.RetryQuery(ctx, "HaltFirmwareRolloutDevices", func() error { + return q.next.HaltFirmwareRolloutDevices(ctx, arg) + }) +} + func (q *retryingQuerier) HasUser(ctx context.Context) (bool, error) { var result bool err := q.retrier.RetryQuery(ctx, "HasUser", func() error { @@ -3708,6 +3900,12 @@ func (q *retryingQuerier) InsertNotificationMetricSamples(ctx context.Context, a }) } +func (q *retryingQuerier) InsertReleaseChannelTargets(ctx context.Context, arg InsertReleaseChannelTargetsParams) error { + return q.retrier.RetryQuery(ctx, "InsertReleaseChannelTargets", func() error { + return q.next.InsertReleaseChannelTargets(ctx, arg) + }) +} + func (q *retryingQuerier) InsertRepairTicketPart(ctx context.Context, arg InsertRepairTicketPartParams) error { return q.retrier.RetryQuery(ctx, "InsertRepairTicketPart", func() error { return q.next.InsertRepairTicketPart(ctx, arg) @@ -3798,6 +3996,18 @@ func (q *retryingQuerier) ListActiveCurtailmentTargetDevicesByOrg(ctx context.Co return result, err } +func (q *retryingQuerier) ListActiveFirmwareRollouts(ctx context.Context) ([]ListActiveFirmwareRolloutsRow, error) { + var result []ListActiveFirmwareRolloutsRow + err := q.retrier.RetryQuery(ctx, "ListActiveFirmwareRollouts", func() error { + callResult, callErr := q.next.ListActiveFirmwareRollouts(ctx) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ListActiveNotificationGroups(ctx context.Context, arg ListActiveNotificationGroupsParams) ([]ListActiveNotificationGroupsRow, error) { var result []ListActiveNotificationGroupsRow err := q.retrier.RetryQuery(ctx, "ListActiveNotificationGroups", func() error { @@ -4194,6 +4404,18 @@ func (q *retryingQuerier) ListCustomRolesForOrg(ctx context.Context, organizatio return result, err } +func (q *retryingQuerier) ListDeviceIDsByIdentifiers(ctx context.Context, arg ListDeviceIDsByIdentifiersParams) ([]ListDeviceIDsByIdentifiersRow, error) { + var result []ListDeviceIDsByIdentifiersRow + err := q.retrier.RetryQuery(ctx, "ListDeviceIDsByIdentifiers", func() error { + callResult, callErr := q.next.ListDeviceIDsByIdentifiers(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ListDeviceSetMembersPaginated(ctx context.Context, arg ListDeviceSetMembersPaginatedParams) ([]ListDeviceSetMembersPaginatedRow, error) { var result []ListDeviceSetMembersPaginatedRow err := q.retrier.RetryQuery(ctx, "ListDeviceSetMembersPaginated", func() error { @@ -4338,6 +4560,30 @@ func (q *retryingQuerier) ListExistingDeviceIdentifiers(ctx context.Context, arg return result, err } +func (q *retryingQuerier) ListFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]ListFirmwareRolloutDevicesRow, error) { + var result []ListFirmwareRolloutDevicesRow + err := q.retrier.RetryQuery(ctx, "ListFirmwareRolloutDevices", func() error { + callResult, callErr := q.next.ListFirmwareRolloutDevices(ctx, rolloutID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListFirmwareRollouts(ctx context.Context, arg ListFirmwareRolloutsParams) ([]ListFirmwareRolloutsRow, error) { + var result []ListFirmwareRolloutsRow + err := q.retrier.RetryQuery(ctx, "ListFirmwareRollouts", func() error { + callResult, callErr := q.next.ListFirmwareRollouts(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ListFleetNodeDeviceIDsForRevocation(ctx context.Context, arg ListFleetNodeDeviceIDsForRevocationParams) ([]int64, error) { var result []int64 err := q.retrier.RetryQuery(ctx, "ListFleetNodeDeviceIDsForRevocation", func() error { @@ -4614,6 +4860,126 @@ func (q *retryingQuerier) ListRecentlyResolvedCurtailedDevicesByScope(ctx contex return result, err } +func (q *retryingQuerier) ListReleaseChannelFirmware(ctx context.Context, orgID int64) ([]ReleaseChannelFirmware, error) { + var result []ReleaseChannelFirmware + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelFirmware", func() error { + callResult, callErr := q.next.ListReleaseChannelFirmware(ctx, orgID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelFirmwareNeedingRollout(ctx context.Context) ([]ListReleaseChannelFirmwareNeedingRolloutRow, error) { + var result []ListReleaseChannelFirmwareNeedingRolloutRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelFirmwareNeedingRollout", func() error { + callResult, callErr := q.next.ListReleaseChannelFirmwareNeedingRollout(ctx) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelMembers(ctx context.Context, orgID int64) ([]ListReleaseChannelMembersRow, error) { + var result []ListReleaseChannelMembersRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelMembers", func() error { + callResult, callErr := q.next.ListReleaseChannelMembers(ctx, orgID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelMembershipConflictsPage(ctx context.Context, arg ListReleaseChannelMembershipConflictsPageParams) ([]ListReleaseChannelMembershipConflictsPageRow, error) { + var result []ListReleaseChannelMembershipConflictsPageRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelMembershipConflictsPage", func() error { + callResult, callErr := q.next.ListReleaseChannelMembershipConflictsPage(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelMinersPage(ctx context.Context, arg ListReleaseChannelMinersPageParams) ([]ListReleaseChannelMinersPageRow, error) { + var result []ListReleaseChannelMinersPageRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelMinersPage", func() error { + callResult, callErr := q.next.ListReleaseChannelMinersPage(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelMismatchedMembers(ctx context.Context, arg ListReleaseChannelMismatchedMembersParams) ([]ListReleaseChannelMismatchedMembersRow, error) { + var result []ListReleaseChannelMismatchedMembersRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelMismatchedMembers", func() error { + callResult, callErr := q.next.ListReleaseChannelMismatchedMembers(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelModelGroupsPage(ctx context.Context, arg ListReleaseChannelModelGroupsPageParams) ([]ListReleaseChannelModelGroupsPageRow, error) { + var result []ListReleaseChannelModelGroupsPageRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelModelGroupsPage", func() error { + callResult, callErr := q.next.ListReleaseChannelModelGroupsPage(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelSuppressedMembers(ctx context.Context, arg ListReleaseChannelSuppressedMembersParams) ([]ListReleaseChannelSuppressedMembersRow, error) { + var result []ListReleaseChannelSuppressedMembersRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelSuppressedMembers", func() error { + callResult, callErr := q.next.ListReleaseChannelSuppressedMembers(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannelTargets(ctx context.Context, orgID int64) ([]ListReleaseChannelTargetsRow, error) { + var result []ListReleaseChannelTargetsRow + err := q.retrier.RetryQuery(ctx, "ListReleaseChannelTargets", func() error { + callResult, callErr := q.next.ListReleaseChannelTargets(ctx, orgID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + +func (q *retryingQuerier) ListReleaseChannels(ctx context.Context, orgID int64) ([]ReleaseChannel, error) { + var result []ReleaseChannel + err := q.retrier.RetryQuery(ctx, "ListReleaseChannels", func() error { + callResult, callErr := q.next.ListReleaseChannels(ctx, orgID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ListRepairTicketComments(ctx context.Context, arg ListRepairTicketCommentsParams) ([]ListRepairTicketCommentsRow, error) { var result []ListRepairTicketCommentsRow err := q.retrier.RetryQuery(ctx, "ListRepairTicketComments", func() error { @@ -5154,6 +5520,12 @@ func (q *retryingQuerier) LockRacksForReparent(ctx context.Context, arg LockRack return result, err } +func (q *retryingQuerier) LockReleaseChannelScopes(ctx context.Context, orgID int64) error { + return q.retrier.RetryQuery(ctx, "LockReleaseChannelScopes", func() error { + return q.next.LockReleaseChannelScopes(ctx, orgID) + }) +} + func (q *retryingQuerier) LockRepairTicketCommentCreateKey(ctx context.Context, arg LockRepairTicketCommentCreateKeyParams) error { return q.retrier.RetryQuery(ctx, "LockRepairTicketCommentCreateKey", func() error { return q.next.LockRepairTicketCommentCreateKey(ctx, arg) @@ -5220,6 +5592,12 @@ func (q *retryingQuerier) MarkCommandBatchProcessing(ctx context.Context, uuid s return result, err } +func (q *retryingQuerier) MarkFirmwareRolloutDevicesSent(ctx context.Context, arg MarkFirmwareRolloutDevicesSentParams) error { + return q.retrier.RetryQuery(ctx, "MarkFirmwareRolloutDevicesSent", func() error { + return q.next.MarkFirmwareRolloutDevicesSent(ctx, arg) + }) +} + func (q *retryingQuerier) MarkRepairTicketPartsConsumed(ctx context.Context, arg MarkRepairTicketPartsConsumedParams) error { return q.retrier.RetryQuery(ctx, "MarkRepairTicketPartsConsumed", func() error { return q.next.MarkRepairTicketPartsConsumed(ctx, arg) @@ -5280,6 +5658,18 @@ func (q *retryingQuerier) PauseActiveSchedule(ctx context.Context, arg PauseActi return result, err } +func (q *retryingQuerier) PauseFirmwareRollout(ctx context.Context, arg PauseFirmwareRolloutParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "PauseFirmwareRollout", func() error { + callResult, callErr := q.next.PauseFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) PruneExpiredAlertMaintenanceWindows(ctx context.Context, arg PruneExpiredAlertMaintenanceWindowsParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "PruneExpiredAlertMaintenanceWindows", func() error { @@ -5430,6 +5820,18 @@ func (q *retryingQuerier) RecordCurtailPendingDispatch(ctx context.Context, arg return result, err } +func (q *retryingQuerier) RecordFirmwareDeployment(ctx context.Context, arg RecordFirmwareDeploymentParams) error { + return q.retrier.RetryQuery(ctx, "RecordFirmwareDeployment", func() error { + return q.next.RecordFirmwareDeployment(ctx, arg) + }) +} + +func (q *retryingQuerier) RecordFirmwareRolloutAction(ctx context.Context, arg RecordFirmwareRolloutActionParams) error { + return q.retrier.RetryQuery(ctx, "RecordFirmwareRolloutAction", func() error { + return q.next.RecordFirmwareRolloutAction(ctx, arg) + }) +} + func (q *retryingQuerier) RefreshOpenErrorsLastSeenByDevice(ctx context.Context, arg RefreshOpenErrorsLastSeenByDeviceParams) (sql.Result, error) { var result sql.Result err := q.retrier.RetryQuery(ctx, "RefreshOpenErrorsLastSeenByDevice", func() error { @@ -5520,6 +5922,18 @@ func (q *retryingQuerier) RequestRigConfigReconciliation(ctx context.Context, ar }) } +func (q *retryingQuerier) RequeueFirmwareRolloutDevices(ctx context.Context, rolloutID int64) ([]int64, error) { + var result []int64 + err := q.retrier.RetryQuery(ctx, "RequeueFirmwareRolloutDevices", func() error { + callResult, callErr := q.next.RequeueFirmwareRolloutDevices(ctx, rolloutID) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) RequeueRigConfigReconciliationAfterTerminalFailure(ctx context.Context, organizationID int64) error { return q.retrier.RetryQuery(ctx, "RequeueRigConfigReconciliationAfterTerminalFailure", func() error { return q.next.RequeueRigConfigReconciliationAfterTerminalFailure(ctx, organizationID) @@ -5622,6 +6036,18 @@ func (q *retryingQuerier) ResolveMaintenanceMinerContext(ctx context.Context, ar return result, err } +func (q *retryingQuerier) ResolveReleaseChannelScope(ctx context.Context, arg ResolveReleaseChannelScopeParams) ([]ResolveReleaseChannelScopeRow, error) { + var result []ResolveReleaseChannelScopeRow + err := q.retrier.RetryQuery(ctx, "ResolveReleaseChannelScope", func() error { + callResult, callErr := q.next.ResolveReleaseChannelScope(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ResumeCurtailmentFromRestoring(ctx context.Context, id int64) (CurtailmentEvent, error) { var result CurtailmentEvent err := q.retrier.RetryQuery(ctx, "ResumeCurtailmentFromRestoring", func() error { @@ -5634,6 +6060,18 @@ func (q *retryingQuerier) ResumeCurtailmentFromRestoring(ctx context.Context, id return result, err } +func (q *retryingQuerier) ResumeFirmwareRollout(ctx context.Context, arg ResumeFirmwareRolloutParams) (int64, error) { + var result int64 + err := q.retrier.RetryQuery(ctx, "ResumeFirmwareRollout", func() error { + callResult, callErr := q.next.ResumeFirmwareRollout(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) ResumePausedSchedule(ctx context.Context, arg ResumePausedScheduleParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "ResumePausedSchedule", func() error { @@ -5856,6 +6294,12 @@ func (q *retryingQuerier) SitesByIDs(ctx context.Context, arg SitesByIDsParams) return result, err } +func (q *retryingQuerier) SnapshotFirmwareRolloutDevices(ctx context.Context, arg SnapshotFirmwareRolloutDevicesParams) error { + return q.retrier.RetryQuery(ctx, "SnapshotFirmwareRolloutDevices", func() error { + return q.next.SnapshotFirmwareRolloutDevices(ctx, arg) + }) +} + func (q *retryingQuerier) SoftDeleteAlertChannel(ctx context.Context, arg SoftDeleteAlertChannelParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "SoftDeleteAlertChannel", func() error { @@ -6576,6 +7020,18 @@ func (q *retryingQuerier) UpdateRackPlacementBulkForSite(ctx context.Context, ar }) } +func (q *retryingQuerier) UpdateReleaseChannel(ctx context.Context, arg UpdateReleaseChannelParams) (ReleaseChannel, error) { + var result ReleaseChannel + err := q.retrier.RetryQuery(ctx, "UpdateReleaseChannel", func() error { + callResult, callErr := q.next.UpdateReleaseChannel(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) UpdateRepairTicket(ctx context.Context, arg UpdateRepairTicketParams) (int64, error) { var result int64 err := q.retrier.RetryQuery(ctx, "UpdateRepairTicket", func() error { @@ -6792,6 +7248,18 @@ func (q *retryingQuerier) UpsertPermission(ctx context.Context, arg UpsertPermis return result, err } +func (q *retryingQuerier) UpsertReleaseChannelFirmware(ctx context.Context, arg UpsertReleaseChannelFirmwareParams) (ReleaseChannelFirmware, error) { + var result ReleaseChannelFirmware + err := q.retrier.RetryQuery(ctx, "UpsertReleaseChannelFirmware", func() error { + callResult, callErr := q.next.UpsertReleaseChannelFirmware(ctx, arg) + if callErr == nil { + result = callResult + } + return callErr + }) + return result, err +} + func (q *retryingQuerier) UpsertReleaseChannelSetting(ctx context.Context, arg UpsertReleaseChannelSettingParams) (ReleaseChannelSetting, error) { var result ReleaseChannelSetting err := q.retrier.RetryQuery(ctx, "UpsertReleaseChannelSetting", func() error { diff --git a/server/internal/domain/stores/sqlstores/release_channel.go b/server/internal/domain/stores/sqlstores/release_channel.go new file mode 100644 index 0000000000..4e1d8c0f44 --- /dev/null +++ b/server/internal/domain/stores/sqlstores/release_channel.go @@ -0,0 +1,31 @@ +package sqlstores + +import ( + "context" + "database/sql" + + "github.com/block/proto-fleet/server/generated/sqlc" +) + +// SQLReleaseChannelStore gives the rollout domain access to the release +// channel and firmware rollout queries. The domain service works with sqlc +// types directly, so this store only manages the connection. +type SQLReleaseChannelStore struct { + SQLConnectionManager +} + +func NewSQLReleaseChannelStore(conn *sql.DB) *SQLReleaseChannelStore { + return &SQLReleaseChannelStore{ + SQLConnectionManager: NewSQLConnectionManager(conn), + } +} + +// Queries returns the querier bound to the current context (transaction-aware). +func (s *SQLReleaseChannelStore) Queries(ctx context.Context) sqlc.Querier { + return s.GetQueries(ctx) +} + +// IsUniqueViolation reports whether err is a PostgreSQL unique_violation. +func IsUniqueViolation(err error) bool { + return isUniqueViolation(err) +} diff --git a/server/internal/infrastructure/db/migration_bridges_test.go b/server/internal/infrastructure/db/migration_bridges_test.go index 644938989a..6f9069a411 100644 --- a/server/internal/infrastructure/db/migration_bridges_test.go +++ b/server/internal/infrastructure/db/migration_bridges_test.go @@ -6,8 +6,10 @@ import ( "database/sql" "encoding/hex" "fmt" + "io/fs" "os" "strconv" + "strings" "testing" "time" @@ -21,7 +23,33 @@ import ( "github.com/block/proto-fleet/server/migrations" ) -const latestMigrationVersion = 147 +// latestMigrationVersion returns the highest migration version in the +// embedded source, so full-run assertions stay valid as migrations are added. +func latestMigrationVersion(t *testing.T) int { + t.Helper() + entries, err := fs.ReadDir(migrations.Migrations, ".") + assert.NoError(t, err) + latest := 0 + for _, entry := range entries { + name := entry.Name() + if !strings.HasSuffix(name, ".up.sql") { + continue + } + idx := strings.IndexByte(name, '_') + if idx <= 0 { + continue + } + version, err := strconv.Atoi(name[:idx]) + if err != nil { + continue + } + if version > latest { + latest = version + } + } + assert.True(t, latest > 0) + return latest +} func TestCurtailmentAuthorizationEnvelopeBridgePreservesLegacyRows(t *testing.T) { if os.Getenv("DB_PASSWORD") == "" { @@ -54,7 +82,7 @@ func TestCurtailmentAuthorizationEnvelopeBridgePreservesLegacyRows(t *testing.T) var dirty bool assert.NoError(t, conn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) assertLegacyCurtailmentEnvelope(t, conn, @@ -331,7 +359,7 @@ func TestCurtailmentAuthorizationEnvelopeBridgeUsesActiveSchema(t *testing.T) { var dirty bool assert.NoError(t, searchPathConn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) assertLegacyCurtailmentEnvelope(t, searchPathConn, "curtailment_response_profile", true) assertLegacyResponseProfileRevisionBinding(t, searchPathConn) @@ -355,7 +383,7 @@ func TestMigrationsThroughVersionNeverDowngrade(t *testing.T) { var dirty bool assert.NoError(t, conn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) } @@ -371,7 +399,7 @@ func TestCurtailmentAuthorizationEnvelopeBridgeAllowsFreshBootstrap(t *testing.T var dirty bool assert.NoError(t, conn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) } @@ -424,7 +452,7 @@ func TestCurtailmentAuthorizationEnvelopeBridgeUsesMigrationLock(t *testing.T) { var dirty bool assert.NoError(t, conn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) assertLegacyResponseProfileRevisionBinding(t, conn) } @@ -446,7 +474,7 @@ func TestCurtailmentAuthorizationEnvelopeBridgeResetsEmptyDirtyRC1(t *testing.T) var dirty bool assert.NoError(t, conn.QueryRowContext(t.Context(), "SELECT version, dirty FROM schema_migrations").Scan(&version, &dirty)) - assert.Equal(t, latestMigrationVersion, version) + assert.Equal(t, latestMigrationVersion(t), version) assert.False(t, dirty) } diff --git a/server/internal/infrastructure/files/firmware.go b/server/internal/infrastructure/files/firmware.go index be89f11af3..0be2c620c8 100644 --- a/server/internal/infrastructure/files/firmware.go +++ b/server/internal/infrastructure/files/firmware.go @@ -847,6 +847,58 @@ func (s *Service) FindFirmwareFileByChecksum(sha256Hex string, metadata Firmware return "", false } +// FirmwareArtifact is a firmware file resolved for a release channel +// assignment: its payload checksum, the identity under which rollouts track +// it, and the metadata snapshotted onto the assignment. +type FirmwareArtifact struct { + FileID string + Checksum string + Metadata FirmwareMetadata +} + +// ResolveFirmwareArtifact returns the checksum and metadata of an uploaded +// firmware file. The metadata must be complete (ValidateFirmwareMetadata) for +// an assignment to snapshot it; callers decide how to report a mismatch with +// the pair being assigned. +func (s *Service) ResolveFirmwareArtifact(fileID string) (FirmwareArtifact, error) { + canonical, err := canonicalizeFirmwareFileID(fileID) + if err != nil { + return FirmwareArtifact{}, err + } + filePath, err := getFirmwareFilePathForCanonicalID(canonical) + if err != nil { + return FirmwareArtifact{}, err + } + metadata, err := readFirmwareMetadata(getFirmwareDirPath(canonical)) + if err != nil { + return FirmwareArtifact{}, fleeterror.NewFailedPreconditionErrorf("firmware file %s has no valid metadata", fileID) + } + if err := ValidateFirmwareMetadata(metadata); err != nil { + return FirmwareArtifact{}, err + } + checksum, err := s.firmwareChecksum(canonical, filePath, true) + if err != nil { + return FirmwareArtifact{}, err + } + return FirmwareArtifact{FileID: canonical, Checksum: checksum, Metadata: metadata.normalized()}, nil +} + +// FindFirmwareFileIDByChecksum returns an uploaded firmware file whose payload +// has the given SHA-256, whatever its name or metadata: under the release +// channel artifact identity rule any such file makes the assignment available +// again. Returns false while no such file is uploaded. +func (s *Service) FindFirmwareFileIDByChecksum(sha256Hex string) (string, bool) { + s.mu.Lock() + ids := append([]string(nil), s.checksumIndex[sha256Hex]...) + s.mu.Unlock() + for _, id := range ids { + if _, err := getFirmwareFilePathForCanonicalID(id); err == nil { + return id, true + } + } + return "", false +} + // DeleteFirmwareFile removes a firmware file from disk and the checksum index. // Returns a NotFoundError if no file with the given ID exists. func (s *Service) DeleteFirmwareFile(fileID string) error { diff --git a/server/internal/infrastructure/files/firmware_test.go b/server/internal/infrastructure/files/firmware_test.go index 754bb0d381..e635198bfa 100644 --- a/server/internal/infrastructure/files/firmware_test.go +++ b/server/internal/infrastructure/files/firmware_test.go @@ -747,6 +747,41 @@ func TestFindFirmwareFileByChecksum_ReturnsTrueForExistingFile(t *testing.T) { assert.Equal(t, fileID, foundID) } +func TestResolveFirmwareArtifact_ReturnsChecksumAndMetadata(t *testing.T) { + svc := setupService(t) + + content := "assignable firmware content" + fileID, err := svc.SaveFirmwareFile("firmware.swu", strings.NewReader(content), testFirmwareMetadata()) + require.NoError(t, err) + + artifact, err := svc.ResolveFirmwareArtifact(fileID) + require.NoError(t, err) + assert.Equal(t, fileID, artifact.FileID) + assert.Equal(t, checksumOf(content), artifact.Checksum) + assert.Equal(t, testFirmwareMetadata(), artifact.Metadata) + + _, err = svc.ResolveFirmwareArtifact("missing-file") + require.Error(t, err) +} + +func TestFindFirmwareFileIDByChecksum_IgnoresMetadataAndFollowsDeletes(t *testing.T) { + svc := setupService(t) + + content := "re-uploadable firmware content" + fileID, err := svc.SaveFirmwareFile("firmware.swu", strings.NewReader(content), testFirmwareMetadata()) + require.NoError(t, err) + + // Any uploaded payload with the checksum makes the artifact available, + // whatever its metadata says. + foundID, ok := svc.FindFirmwareFileIDByChecksum(checksumOf(content)) + assert.True(t, ok) + assert.Equal(t, fileID, foundID) + + require.NoError(t, svc.DeleteFirmwareFile(fileID)) + _, ok = svc.FindFirmwareFileIDByChecksum(checksumOf(content)) + assert.False(t, ok) +} + func TestFindFirmwareFileByChecksum_ReturnsFalseForUnknownChecksum(t *testing.T) { svc := setupService(t) diff --git a/server/migrations/000148_release_channels.down.sql b/server/migrations/000148_release_channels.down.sql new file mode 100644 index 0000000000..673f6c8e85 --- /dev/null +++ b/server/migrations/000148_release_channels.down.sql @@ -0,0 +1,14 @@ +DROP VIEW IF EXISTS release_channel_member; +DROP VIEW IF EXISTS release_channel_conflict; +DROP VIEW IF EXISTS release_channel_match; +DROP TABLE IF EXISTS device_firmware_deployment; +DROP TRIGGER IF EXISTS firmware_rollout_device_updated ON firmware_rollout_device; +DROP TRIGGER IF EXISTS firmware_rollout_device_inserted ON firmware_rollout_device; +DROP FUNCTION IF EXISTS firmware_rollout_devices_changed(); +DROP TABLE IF EXISTS firmware_rollout_device; +DROP TRIGGER IF EXISTS firmware_rollout_revision ON firmware_rollout; +DROP FUNCTION IF EXISTS firmware_rollout_bump_revision(); +DROP TABLE IF EXISTS firmware_rollout; +DROP TABLE IF EXISTS release_channel_firmware; +DROP TABLE IF EXISTS release_channel_target; +DROP TABLE IF EXISTS release_channel; diff --git a/server/migrations/000148_release_channels.up.sql b/server/migrations/000148_release_channels.up.sql new file mode 100644 index 0000000000..5b9cc685e5 --- /dev/null +++ b/server/migrations/000148_release_channels.up.sql @@ -0,0 +1,339 @@ +-- Firmware release channels and the rollouts that enforce them. +-- +-- A release channel is a scope (sites, buildings, racks, groups, miners) +-- resolved live against fleet placement, with one optional firmware +-- assignment per (manufacturer, model) pair. The enforcement loop updates any +-- member of an assigned pair not running its assigned artifact; each run for +-- one (channel, manufacturer, model) tuple is a firmware_rollout. +-- +-- Identity conventions (see proto/rollout/v1/rollout.proto): +-- * Assignment keys are canonical printable-ASCII manufacturer/model strings +-- compared ASCII-case-insensitively: lower(x COLLATE "C") folds A-Z only. +-- Members are matched to a pair by folding their observed identity the +-- same way after trimming. +-- * Firmware is identified by the SHA-256 of its payload +-- (firmware_checksum), never by file id; file ids are resolved at read and +-- dispatch time from the files service. + +CREATE TABLE release_channel ( + id BIGSERIAL PRIMARY KEY, + org_id BIGINT NOT NULL REFERENCES organization(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + -- Update behavior. Copied onto each rollout when it starts so editing + -- the channel never changes a run in flight, except max_concurrent_offline + -- which is channel-wide and live. + method TEXT NOT NULL DEFAULT 'all_at_once', -- all_at_once | batched | pilot_then_continue | delegated + order_by TEXT NOT NULL DEFAULT 'least_efficient_first', -- least_efficient_first | random + batch_size INT NOT NULL DEFAULT 0, -- batched + pilot_size INT NOT NULL DEFAULT 0, -- pilot_then_continue + wait_between_batches_seconds INT NOT NULL DEFAULT 0, -- batched without review + review_after_each_batch BOOLEAN NOT NULL DEFAULT false, -- batched + auto_continue BOOLEAN NOT NULL DEFAULT false, + stabilization_seconds INT NOT NULL DEFAULT 0, + max_hashrate_drop_percent DOUBLE PRECISION NULL, -- NULL: not checked + max_efficiency_increase_percent DOUBLE PRECISION NULL, + max_temp_increase_c DOUBLE PRECISION NULL, + max_new_errors INT NULL, + min_sample_coverage_percent DOUBLE PRECISION NULL, -- NULL: 100 + max_concurrent_offline INT NOT NULL DEFAULT 0, -- 0: no limit + controller_timeout_seconds INT NOT NULL DEFAULT 0, -- delegated; 0: never + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (org_id, name) +); + +-- Which miners a channel applies to. A miner is in the channel when any +-- selector matches its current placement (fleet_device_placement). +CREATE TABLE release_channel_target ( + channel_id BIGINT NOT NULL REFERENCES release_channel(id) ON DELETE CASCADE, + target_type TEXT NOT NULL CHECK (target_type IN ('site', 'building', 'rack', 'group', 'miner')), + target_id BIGINT NOT NULL, + PRIMARY KEY (channel_id, target_type, target_id) +); + +CREATE INDEX idx_release_channel_target_lookup ON release_channel_target(target_type, target_id); + +-- Desired firmware per (channel, manufacturer, model) pair. The row outlives +-- the assignment: clearing sets firmware_checksum to '' and keeps the +-- generation counter, which increments on every assignment change including +-- the clear. A rollout is current while its generation equals the pair's. +CREATE TABLE release_channel_firmware ( + channel_id BIGINT NOT NULL REFERENCES release_channel(id) ON DELETE CASCADE, + manufacturer TEXT NOT NULL, + model TEXT NOT NULL, + firmware_checksum TEXT NOT NULL DEFAULT '', -- '' when unassigned + firmware_version TEXT NOT NULL DEFAULT '', + -- Target metadata snapshotted from the file when assigned. + firmware_target_manufacturer TEXT NOT NULL DEFAULT '', + firmware_target_model TEXT NOT NULL DEFAULT '', + assignment_generation BIGINT NOT NULL DEFAULT 0, + assigned_by BIGINT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (channel_id, manufacturer, model) +); + +CREATE UNIQUE INDEX idx_release_channel_firmware_folded_key + ON release_channel_firmware(channel_id, lower(manufacturer COLLATE "C"), lower(model COLLATE "C")); + +-- One firmware change for one (manufacturer, model) pair within one channel. +CREATE TABLE firmware_rollout ( + id BIGSERIAL PRIMARY KEY, + org_id BIGINT NOT NULL REFERENCES organization(id) ON DELETE CASCADE, + channel_id BIGINT NOT NULL REFERENCES release_channel(id) ON DELETE CASCADE, + manufacturer TEXT NOT NULL, + model TEXT NOT NULL, + firmware_checksum TEXT NOT NULL, + firmware_version TEXT NOT NULL, + -- Assignment lineage: what a rollback restores; '' for a first assignment. + previous_firmware_checksum TEXT NOT NULL DEFAULT '', + previous_firmware_version TEXT NOT NULL DEFAULT '', + -- Generation of the pair's assignment this rollout enforces. + assignment_generation BIGINT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', -- active | completed | completed_with_failures | canceled + cancel_reason TEXT NOT NULL DEFAULT '', -- superseded | canceled_remaining | rolled_back | cleared + stage TEXT NOT NULL DEFAULT 'rest', -- batch | awaiting_review | waiting | rest + -- Behavior snapshot (see release_channel). + method TEXT NOT NULL DEFAULT 'all_at_once', + order_by TEXT NOT NULL DEFAULT 'least_efficient_first', + batch_size INT NOT NULL DEFAULT 0, + pilot_size INT NOT NULL DEFAULT 0, + wait_between_batches_seconds INT NOT NULL DEFAULT 0, + review_after_each_batch BOOLEAN NOT NULL DEFAULT false, + auto_continue BOOLEAN NOT NULL DEFAULT false, + stabilization_seconds INT NOT NULL DEFAULT 0, + max_hashrate_drop_percent DOUBLE PRECISION NULL, + max_efficiency_increase_percent DOUBLE PRECISION NULL, + max_temp_increase_c DOUBLE PRECISION NULL, + max_new_errors INT NULL, + min_sample_coverage_percent DOUBLE PRECISION NULL, + max_concurrent_offline INT NOT NULL DEFAULT 0, + controller_timeout_seconds INT NOT NULL DEFAULT 0, + -- Snapshotted batches (0 for all-at-once and delegated) and the one in flight. + batch_count INT NOT NULL DEFAULT 0, + current_batch INT NOT NULL DEFAULT 0, + stage_changed_at TIMESTAMPTZ NOT NULL DEFAULT now(), + paused_at TIMESTAMPTZ NULL, + -- Revision rule: +1 on every change to this row or to its devices + -- (trigger below); updated_at records when. + revision BIGINT NOT NULL DEFAULT 1, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + -- Actors: user | api_key | system, with id (0 for system) and display name. + started_by_type TEXT NOT NULL DEFAULT 'system', + started_by_id BIGINT NOT NULL DEFAULT 0, + started_by_name TEXT NOT NULL DEFAULT '', + last_action_by_type TEXT NOT NULL DEFAULT 'system', + last_action_by_id BIGINT NOT NULL DEFAULT 0, + last_action_by_name TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + finished_at TIMESTAMPTZ NULL +); + +-- At most one active rollout per (channel, manufacturer, model). +CREATE UNIQUE INDEX idx_one_active_rollout_per_pair + ON firmware_rollout(channel_id, lower(manufacturer COLLATE "C"), lower(model COLLATE "C")) + WHERE status = 'active'; + +CREATE INDEX idx_firmware_rollout_org_created ON firmware_rollout(org_id, created_at DESC); +CREATE INDEX idx_firmware_rollout_org_updated ON firmware_rollout(org_id, updated_at); + +CREATE OR REPLACE FUNCTION firmware_rollout_bump_revision() +RETURNS TRIGGER AS $$ +BEGIN + NEW.revision = OLD.revision + 1; + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER firmware_rollout_revision + BEFORE UPDATE ON firmware_rollout + FOR EACH ROW + EXECUTE FUNCTION firmware_rollout_bump_revision(); + +-- Every miner a rollout targets: snapshotted at start (with its baseline +-- health), late joiners appended by the enforcement loop, leavers marked +-- excluded. This is the rollout's target set; membership is not re-derived +-- from the channel once a rollout has finished, so history stays stable. +CREATE TABLE firmware_rollout_device ( + rollout_id BIGINT NOT NULL REFERENCES firmware_rollout(id) ON DELETE CASCADE, + device_id BIGINT NOT NULL REFERENCES device(id) ON DELETE CASCADE, + -- 0-based batch; NULL for the unbatched rest / late joiners. + batch_index INT NULL, + -- Order within the rollout (efficiency-ranked or shuffled at start); + -- NULL for late joiners, which sort last. + position INT NULL, + -- Update commands sent so far; a miner is halted as failed once + -- attempts are exhausted without it verifying. + attempts INT NOT NULL DEFAULT 0, + first_sent_at TIMESTAMPTZ NULL, + last_sent_at TIMESTAMPTZ NULL, + -- Set when the miner will not be retried for this version: attempts + -- exhausted ('failed'), the rollout was canceled ('canceled'), or a caller + -- settled it without updating ('skipped'). Enforcement suppresses failed + -- and skipped miners for the rest of the assignment generation; + -- RetryFailedRolloutDevices clears it. + halted_at TIMESTAMPTZ NULL, + halt_reason TEXT NOT NULL DEFAULT '', -- failed | canceled | skipped + last_error TEXT NOT NULL DEFAULT '', + skip_note TEXT NOT NULL DEFAULT '', + -- Set when the miner left the channel scope while the rollout ran. + excluded_at TIMESTAMPTZ NULL, + -- Health when the miner was snapshotted into the rollout; NULL for late + -- joiners, which are judged on version + online only. + baseline_status TEXT NULL, + baseline_hash_rate_hs DOUBLE PRECISION NULL, + baseline_power_w DOUBLE PRECISION NULL, + baseline_efficiency_jh DOUBLE PRECISION NULL, + baseline_temp_c DOUBLE PRECISION NULL, + baseline_open_errors INT NULL, + baseline_at TIMESTAMPTZ NULL, + added_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (rollout_id, device_id) +); + +CREATE INDEX idx_firmware_rollout_device_device ON firmware_rollout_device(device_id); + +-- Any change to a rollout's devices is a change to the rollout under the +-- revision rule: one bump per statement per rollout. +CREATE OR REPLACE FUNCTION firmware_rollout_devices_changed() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE firmware_rollout + SET updated_at = now() + WHERE id IN (SELECT DISTINCT rollout_id FROM changed_rows); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER firmware_rollout_device_inserted + AFTER INSERT ON firmware_rollout_device + REFERENCING NEW TABLE AS changed_rows + FOR EACH STATEMENT + EXECUTE FUNCTION firmware_rollout_devices_changed(); + +CREATE TRIGGER firmware_rollout_device_updated + AFTER UPDATE ON firmware_rollout_device + REFERENCING NEW TABLE AS changed_rows + FOR EACH STATEMENT + EXECUTE FUNCTION firmware_rollout_devices_changed(); + +-- Managed-deployment provenance: the artifact Fleet last deployed to a miner, +-- recorded when a rollout target reports the target version. A member is on +-- target only when its reported version and its provenance both match the +-- assignment. +CREATE TABLE device_firmware_deployment ( + device_id BIGINT PRIMARY KEY REFERENCES device(id) ON DELETE CASCADE, + firmware_checksum TEXT NOT NULL, + firmware_version TEXT NOT NULL, + rollout_id BIGINT NULL REFERENCES firmware_rollout(id) ON DELETE SET NULL, + deployed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Every (channel, miner) selector hit, with how specific the selector was. +-- One row per hit, so a miner in a rack and a group of the same channel +-- appears twice; consume through release_channel_member or aggregate. +-- +-- Placement follows the same rules as fleet_device_placement (000134): a +-- miner's rack is its live 'rack' set membership, its building the rack's +-- building falling back to device.building_id, its site device.site_id, and +-- its groups its live 'group' set memberships. The joins are inlined rather +-- than read through that view so neither view blocks changes to the other. +CREATE VIEW release_channel_match AS +SELECT t.channel_id, c.org_id, d.id AS device_id, 1 AS specificity +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +JOIN device d ON d.id = t.target_id AND d.org_id = c.org_id AND d.deleted_at IS NULL +WHERE t.target_type = 'miner' +UNION ALL +SELECT t.channel_id, c.org_id, d.id, 2 +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +JOIN device_set gs ON gs.id = t.target_id AND gs.org_id = c.org_id AND gs.type = 'group' AND gs.deleted_at IS NULL +JOIN device_set_membership gm ON gm.device_set_id = gs.id AND gm.device_set_type = 'group' +JOIN device d ON d.id = gm.device_id AND d.org_id = c.org_id AND d.deleted_at IS NULL +WHERE t.target_type = 'group' +UNION ALL +SELECT t.channel_id, c.org_id, d.id, 3 +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +JOIN device_set rs ON rs.id = t.target_id AND rs.org_id = c.org_id AND rs.type = 'rack' AND rs.deleted_at IS NULL +JOIN device_set_membership rm ON rm.device_set_id = rs.id AND rm.device_set_type = 'rack' +JOIN device d ON d.id = rm.device_id AND d.org_id = c.org_id AND d.deleted_at IS NULL +WHERE t.target_type = 'rack' +UNION ALL +SELECT t.channel_id, c.org_id, d.id, 4 +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +JOIN device d ON d.org_id = c.org_id AND d.deleted_at IS NULL +LEFT JOIN device_set_membership rm ON rm.org_id = d.org_id AND rm.device_id = d.id AND rm.device_set_type = 'rack' +LEFT JOIN device_set rs ON rs.id = rm.device_set_id AND rs.deleted_at IS NULL +LEFT JOIN device_set_rack dsr ON dsr.device_set_id = rs.id +WHERE t.target_type = 'building' + AND COALESCE(dsr.building_id, d.building_id) = t.target_id +UNION ALL +SELECT t.channel_id, c.org_id, d.id, 5 +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +JOIN device d ON d.org_id = c.org_id AND d.deleted_at IS NULL AND d.site_id = t.target_id +WHERE t.target_type = 'site'; + +-- How every channel that matches a miner is resolved. Rows exist only for +-- miners matched by more than one channel: the unique most specific match is +-- the 'winner', less specific matches are 'loser', and when several channels +-- tie at the most specific level every one of them is 'excluded_tie' and the +-- miner belongs to no channel until the conflict is removed. +CREATE VIEW release_channel_conflict AS +WITH per_channel AS ( + SELECT channel_id, org_id, device_id, min(specificity) AS specificity + FROM release_channel_match + GROUP BY channel_id, org_id, device_id +), per_device AS ( + SELECT device_id, min(specificity) AS best, count(*) AS channels + FROM per_channel + GROUP BY device_id +), at_best AS ( + SELECT pc.device_id, count(*) AS n + FROM per_channel pc + JOIN per_device pd ON pd.device_id = pc.device_id AND pc.specificity = pd.best + GROUP BY pc.device_id +) +SELECT pc.channel_id, + pc.org_id, + pc.device_id, + pc.specificity, + CASE + WHEN pc.specificity = pd.best AND ab.n = 1 THEN 'winner' + WHEN pc.specificity = pd.best THEN 'excluded_tie' + ELSE 'loser' + END AS resolution +FROM per_channel pc +JOIN per_device pd ON pd.device_id = pc.device_id +JOIN at_best ab ON ab.device_id = pc.device_id +WHERE pd.channels > 1; + +-- The channel each miner belongs to: at most one row per miner. A miner +-- matched by one channel belongs to it; a miner matched by several belongs to +-- the unique most specific one (flagged conflicted) or to none on a tie. +CREATE VIEW release_channel_member AS +WITH per_channel AS ( + SELECT channel_id, org_id, device_id, min(specificity) AS specificity + FROM release_channel_match + GROUP BY channel_id, org_id, device_id +), per_device AS ( + SELECT device_id, min(specificity) AS best, count(*) AS channels + FROM per_channel + GROUP BY device_id +), winners AS ( + SELECT pc.channel_id, + pc.org_id, + pc.device_id, + pd.channels, + count(*) OVER (PARTITION BY pc.device_id) AS at_best + FROM per_channel pc + JOIN per_device pd ON pd.device_id = pc.device_id AND pc.specificity = pd.best +) +SELECT channel_id, org_id, device_id, channels > 1 AS conflicted +FROM winners +WHERE at_best = 1; diff --git a/server/migrations/release_channels_test.go b/server/migrations/release_channels_test.go new file mode 100644 index 0000000000..03aec0a96c --- /dev/null +++ b/server/migrations/release_channels_test.go @@ -0,0 +1,50 @@ +package migrations_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/block/proto-fleet/server/internal/testutil" + "github.com/block/proto-fleet/server/migrations" +) + +// The release channel migration must round-trip: down removes every object +// it created (including the membership views), and up restores them. +func TestReleaseChannelsMigrationDownAndUp(t *testing.T) { + if testing.Short() { + t.Skip("Skipping database integration test in short mode") + } + db := testutil.GetTestDB(t) + ctx := t.Context() + downSQL, err := migrations.Migrations.ReadFile("000148_release_channels.down.sql") + require.NoError(t, err) + upSQL, err := migrations.Migrations.ReadFile("000148_release_channels.up.sql") + require.NoError(t, err) + + objects := []string{"release_channel", "release_channel_target", "release_channel_firmware", "firmware_rollout", "firmware_rollout_device", "release_channel_match", "release_channel_member"} + exists := func(name string) bool { + var n int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM pg_class WHERE relname = $1 AND relkind IN ('r', 'v')`, name).Scan(&n)) + return n == 1 + } + for _, name := range objects { + require.True(t, exists(name), "%s should exist after migrating up", name) + } + + _, err = db.ExecContext(ctx, string(downSQL)) + require.NoError(t, err) + for _, name := range objects { + require.False(t, exists(name), "%s should be gone after migrating down", name) + } + + _, err = db.ExecContext(ctx, string(upSQL)) + require.NoError(t, err) + for _, name := range objects { + require.True(t, exists(name), "%s should exist after migrating up again", name) + } + // The views are queryable on an empty schema. + var members int + require.NoError(t, db.QueryRowContext(ctx, `SELECT count(*) FROM release_channel_member`).Scan(&members)) + require.Equal(t, 0, members) +} diff --git a/server/sqlc/queries/release_channel.sql b/server/sqlc/queries/release_channel.sql new file mode 100644 index 0000000000..f7deb25caa --- /dev/null +++ b/server/sqlc/queries/release_channel.sql @@ -0,0 +1,790 @@ +-- Firmware release channels and the rollouts that enforce them. +-- +-- Pair keys (manufacturer, model) are compared ASCII-case-insensitively with +-- lower(x COLLATE "C"), which folds A-Z only; observed device identities are +-- trimmed before folding. Firmware is identified by payload checksum. + +-- --- Channels --- + +-- name: CreateReleaseChannel :one +INSERT INTO release_channel ( + org_id, name, description, created_by, + method, order_by, batch_size, pilot_size, wait_between_batches_seconds, + review_after_each_batch, auto_continue, stabilization_seconds, + max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, + min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds +) +VALUES ( + sqlc.arg('org_id'), sqlc.arg('name'), sqlc.arg('description'), sqlc.arg('created_by'), + sqlc.arg('method'), sqlc.arg('order_by'), sqlc.arg('batch_size'), sqlc.arg('pilot_size'), sqlc.arg('wait_between_batches_seconds'), + sqlc.arg('review_after_each_batch'), sqlc.arg('auto_continue'), sqlc.arg('stabilization_seconds'), + sqlc.narg('max_hashrate_drop_percent')::double precision, sqlc.narg('max_efficiency_increase_percent')::double precision, + sqlc.narg('max_temp_increase_c')::double precision, sqlc.narg('max_new_errors')::int, + sqlc.narg('min_sample_coverage_percent')::double precision, sqlc.arg('max_concurrent_offline'), sqlc.arg('controller_timeout_seconds') +) +RETURNING *; + +-- name: UpdateReleaseChannel :one +UPDATE release_channel +SET name = sqlc.arg('name'), + description = sqlc.arg('description'), + method = sqlc.arg('method'), + order_by = sqlc.arg('order_by'), + batch_size = sqlc.arg('batch_size'), + pilot_size = sqlc.arg('pilot_size'), + wait_between_batches_seconds = sqlc.arg('wait_between_batches_seconds'), + review_after_each_batch = sqlc.arg('review_after_each_batch'), + auto_continue = sqlc.arg('auto_continue'), + stabilization_seconds = sqlc.arg('stabilization_seconds'), + max_hashrate_drop_percent = sqlc.narg('max_hashrate_drop_percent')::double precision, + max_efficiency_increase_percent = sqlc.narg('max_efficiency_increase_percent')::double precision, + max_temp_increase_c = sqlc.narg('max_temp_increase_c')::double precision, + max_new_errors = sqlc.narg('max_new_errors')::int, + min_sample_coverage_percent = sqlc.narg('min_sample_coverage_percent')::double precision, + max_concurrent_offline = sqlc.arg('max_concurrent_offline'), + controller_timeout_seconds = sqlc.arg('controller_timeout_seconds'), + updated_at = now() +WHERE id = sqlc.arg('channel_id') AND org_id = sqlc.arg('org_id') +RETURNING *; + +-- name: GetReleaseChannel :one +SELECT * FROM release_channel +WHERE id = sqlc.arg('channel_id') AND org_id = sqlc.arg('org_id'); + +-- name: ListReleaseChannels :many +SELECT * FROM release_channel +WHERE org_id = sqlc.arg('org_id') +ORDER BY name; + +-- name: DeleteReleaseChannel :execrows +DELETE FROM release_channel +WHERE id = sqlc.arg('channel_id') AND org_id = sqlc.arg('org_id'); + +-- name: LockReleaseChannelScopes :exec +-- Serializes scope writes per org so the overlap check and the target +-- insert happen in one critical section. +SELECT pg_advisory_xact_lock(hashtextextended('release_channel_scope:' || (sqlc.arg('org_id')::bigint)::text, 0)); + +-- name: DeleteReleaseChannelTargets :exec +DELETE FROM release_channel_target WHERE channel_id = sqlc.arg('channel_id'); + +-- name: InsertReleaseChannelTargets :exec +-- target_types and target_ids are parallel arrays. +INSERT INTO release_channel_target (channel_id, target_type, target_id) +SELECT sqlc.arg('channel_id'), + unnest(sqlc.arg('target_types')::text[]), + unnest(sqlc.arg('target_ids')::bigint[]) +ON CONFLICT DO NOTHING; + +-- name: ListReleaseChannelTargets :many +-- Targets of every channel in the org; miner targets carry their identifier. +SELECT t.channel_id, + t.target_type, + t.target_id, + COALESCE(d.device_identifier, '')::text AS device_identifier +FROM release_channel_target t +JOIN release_channel c ON c.id = t.channel_id +LEFT JOIN device d ON t.target_type = 'miner' AND d.id = t.target_id +WHERE c.org_id = sqlc.arg('org_id') +ORDER BY t.channel_id, t.target_type, t.target_id; + +-- name: ListDeviceIDsByIdentifiers :many +-- Resolves an org's device identifiers to ids; unknown identifiers are dropped. +SELECT d.id, d.device_identifier +FROM device d +WHERE d.org_id = sqlc.arg('org_id') + AND d.deleted_at IS NULL + AND d.device_identifier = ANY(sqlc.arg('device_identifiers')::text[]); + +-- --- Membership --- + +-- name: ListReleaseChannelMembers :many +-- Every miner resolved into one of the org's channels, with its observed +-- identity, reported firmware and managed-deployment provenance. +SELECT m.channel_id, + m.device_id, + m.conflicted, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +WHERE m.org_id = sqlc.arg('org_id') +ORDER BY d.device_identifier; + +-- name: ListReleaseChannelMinersPage :many +-- One page of a channel's members, optionally filtered by observed +-- manufacturer and model (verbatim), ordered by identifier. The cursor is the +-- (device_identifier, device_id) of the last row of the previous page. +SELECT m.device_id, + m.conflicted, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum +FROM release_channel_member m +JOIN release_channel c ON c.id = m.channel_id +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +WHERE m.channel_id = sqlc.arg('channel_id') + AND c.org_id = sqlc.arg('org_id') + AND (sqlc.narg('manufacturer')::text IS NULL OR COALESCE(dd.manufacturer, '') = sqlc.narg('manufacturer')) + AND (sqlc.narg('model')::text IS NULL OR COALESCE(dd.model, '') = sqlc.narg('model')) + AND ( + sqlc.narg('after_identifier')::text IS NULL + OR (d.device_identifier, d.id) > (sqlc.narg('after_identifier')::text, sqlc.narg('after_device_id')::bigint) + ) +ORDER BY d.device_identifier, d.id +LIMIT sqlc.arg('page_limit'); + +-- name: ListReleaseChannelModelGroupsPage :many +-- One page of a channel's manufacturer/model groups: every observed pair among +-- its members plus every assigned pair with no current members, each joined to +-- its assignment (by folded key) and active rollout. Ordered by observed +-- manufacturer then model; the cursor is the pair of the last row. +WITH members AS ( + SELECT m.device_id, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dep.firmware_checksum, '')::text AS deployed_checksum + FROM release_channel_member m + JOIN device d ON d.id = m.device_id + JOIN discovered_device dd ON dd.id = d.discovered_device_id + LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id + WHERE m.channel_id = sqlc.arg('channel_id') +), groups AS ( + SELECT manufacturer, + model, + count(*)::int AS miner_count, + (array_agg(DISTINCT firmware_version ORDER BY firmware_version))[1:10]::text[] AS reported_versions, + count(DISTINCT firmware_version)::int AS reported_version_count + FROM members + GROUP BY manufacturer, model + UNION ALL + SELECT f.manufacturer, f.model, 0, ARRAY[]::text[], 0 + FROM release_channel_firmware f + WHERE f.channel_id = sqlc.arg('channel_id') + AND f.firmware_checksum <> '' + AND NOT EXISTS ( + SELECT 1 FROM members mm + WHERE lower(btrim(mm.manufacturer) COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(btrim(mm.model) COLLATE "C") = lower(f.model COLLATE "C") + ) +) +SELECT g.manufacturer, + g.model, + g.miner_count, + g.reported_versions, + g.reported_version_count, + COALESCE(f.firmware_checksum, '')::text AS firmware_checksum, + COALESCE(f.firmware_version, '')::text AS firmware_version, + COALESCE(f.firmware_target_manufacturer, '')::text AS firmware_target_manufacturer, + COALESCE(f.firmware_target_model, '')::text AS firmware_target_model, + COALESCE(f.assignment_generation, 0)::bigint AS assignment_generation, + COALESCE(r.id, 0)::bigint AS active_rollout_id, + (SELECT count(*) FROM members mm + WHERE mm.manufacturer = g.manufacturer AND mm.model = g.model + AND f.firmware_checksum IS NOT NULL AND f.firmware_checksum <> '' + AND mm.firmware_version = f.firmware_version + AND mm.deployed_checksum = f.firmware_checksum)::int AS on_target_count +FROM groups g +JOIN release_channel c ON c.id = sqlc.arg('channel_id') AND c.org_id = sqlc.arg('org_id') +LEFT JOIN release_channel_firmware f + ON f.channel_id = c.id + AND lower(f.manufacturer COLLATE "C") = lower(btrim(g.manufacturer) COLLATE "C") + AND lower(f.model COLLATE "C") = lower(btrim(g.model) COLLATE "C") +LEFT JOIN firmware_rollout r + ON r.channel_id = c.id AND r.status = 'active' + AND lower(r.manufacturer COLLATE "C") = lower(btrim(g.manufacturer) COLLATE "C") + AND lower(r.model COLLATE "C") = lower(btrim(g.model) COLLATE "C") +WHERE ( + sqlc.narg('after_manufacturer')::text IS NULL + OR (g.manufacturer, g.model) > (sqlc.narg('after_manufacturer')::text, sqlc.narg('after_model')::text) +) +ORDER BY g.manufacturer, g.model +LIMIT sqlc.arg('page_limit'); + +-- name: ListReleaseChannelMembershipConflictsPage :many +-- One page of (miner, channel) relations for miners matched by several +-- channels, optionally for one channel, ordered by identifier then channel. +SELECT k.device_id, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + k.channel_id, + c.name AS channel_name, + k.specificity::int AS specificity, + k.resolution +FROM release_channel_conflict k +JOIN release_channel c ON c.id = k.channel_id +JOIN device d ON d.id = k.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +WHERE k.org_id = sqlc.arg('org_id') + AND (sqlc.narg('channel_id')::bigint IS NULL OR k.channel_id = sqlc.narg('channel_id')) + AND ( + sqlc.narg('after_identifier')::text IS NULL + OR (d.device_identifier, d.id, k.channel_id) > (sqlc.narg('after_identifier')::text, sqlc.narg('after_device_id')::bigint, sqlc.narg('after_channel_id')::bigint) + ) +ORDER BY d.device_identifier, d.id, k.channel_id +LIMIT sqlc.arg('page_limit'); + +-- name: ResolveReleaseChannelScope :many +-- Miners a candidate scope covers, each with the other channel (if any) +-- whose selectors already match it. Used to preview a scope and to reject +-- overlapping saves; exclude_channel_id is the channel being edited. +WITH scoped AS ( + SELECT d.id AS device_id + FROM device d + WHERE d.org_id = sqlc.arg('org_id') + AND d.deleted_at IS NULL + AND d.device_identifier = ANY(sqlc.arg('device_identifiers')::text[]) + UNION + SELECT d.id + FROM fleet_device_placement p + JOIN device d ON d.device_identifier = p.device_id AND d.org_id = p.org_id + WHERE p.org_id = sqlc.arg('org_id') + AND ( + p.group_id = ANY(sqlc.arg('group_ids')::bigint[]) + OR p.rack_id = ANY(sqlc.arg('rack_ids')::bigint[]) + OR p.building_id = ANY(sqlc.arg('building_ids')::bigint[]) + OR p.site_id = ANY(sqlc.arg('site_ids')::bigint[]) + ) +) +SELECT s.device_id, + d.device_identifier, + COALESCE(dd.manufacturer, '')::text AS manufacturer, + COALESCE(dd.model, '')::text AS model, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(owner.channel_id, 0)::bigint AS owner_channel_id, + COALESCE(owner.name, '')::text AS owner_channel_name +FROM scoped s +JOIN device d ON d.id = s.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN LATERAL ( + SELECT c.id AS channel_id, c.name + FROM release_channel_match rm + JOIN release_channel c ON c.id = rm.channel_id + WHERE rm.device_id = s.device_id + AND rm.channel_id <> sqlc.arg('exclude_channel_id') + ORDER BY c.id + LIMIT 1 +) owner ON true +ORDER BY d.device_identifier; + +-- --- Firmware assignments --- + +-- name: UpsertReleaseChannelFirmware :one +-- Assigns an artifact to a pair and advances the pair's generation. The +-- stored key keeps the case it was first written with. +INSERT INTO release_channel_firmware ( + channel_id, manufacturer, model, firmware_checksum, firmware_version, + firmware_target_manufacturer, firmware_target_model, assignment_generation, assigned_by +) +VALUES ( + sqlc.arg('channel_id'), sqlc.arg('manufacturer'), sqlc.arg('model'), sqlc.arg('firmware_checksum'), sqlc.arg('firmware_version'), + sqlc.arg('firmware_target_manufacturer'), sqlc.arg('firmware_target_model'), 1, sqlc.arg('assigned_by') +) +ON CONFLICT (channel_id, lower(manufacturer COLLATE "C"), lower(model COLLATE "C")) DO UPDATE +SET firmware_checksum = EXCLUDED.firmware_checksum, + firmware_version = EXCLUDED.firmware_version, + firmware_target_manufacturer = EXCLUDED.firmware_target_manufacturer, + firmware_target_model = EXCLUDED.firmware_target_model, + assignment_generation = release_channel_firmware.assignment_generation + 1, + assigned_by = EXCLUDED.assigned_by, + updated_at = now() +RETURNING *; + +-- name: ClearReleaseChannelFirmware :one +-- Clears an assigned pair, advancing its generation; the row stays so the +-- generation survives. Returns nothing when the pair was not assigned. +UPDATE release_channel_firmware +SET firmware_checksum = '', + firmware_version = '', + firmware_target_manufacturer = '', + firmware_target_model = '', + assignment_generation = assignment_generation + 1, + assigned_by = sqlc.arg('assigned_by'), + updated_at = now() +WHERE channel_id = sqlc.arg('channel_id') + AND lower(manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND firmware_checksum <> '' +RETURNING *; + +-- name: GetReleaseChannelFirmware :one +SELECT * FROM release_channel_firmware +WHERE channel_id = sqlc.arg('channel_id') + AND lower(manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C"); + +-- name: ListReleaseChannelFirmware :many +-- Every pair row of an org's channels, assigned or cleared. +SELECT f.* +FROM release_channel_firmware f +JOIN release_channel c ON c.id = f.channel_id +WHERE c.org_id = sqlc.arg('org_id') +ORDER BY f.channel_id, f.manufacturer, f.model; + +-- name: ListReleaseChannelMismatchedMembers :many +-- Members of one pair that are mismatched under the mismatch rule (reported +-- version or provenance differs from the assignment), are not already part of +-- rollout_id (0 for a new rollout), and are not suppressed: a member halted +-- (failed, skipped, or left behind by a cancellation) in the most recent +-- rollout of the current generation that holds it stays out until retried. +-- Carries the latest efficiency sample for ordering. +SELECT d.id AS device_id, + d.device_identifier, + hm.efficiency_jh +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.efficiency_jh + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE m.channel_id = sqlc.arg('channel_id') + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND NOT ( + COALESCE(dd.firmware_version, '') = sqlc.arg('firmware_version')::text + AND COALESCE(dep.firmware_checksum, '') = sqlc.arg('firmware_checksum')::text + ) + AND NOT EXISTS ( + SELECT 1 FROM firmware_rollout_device rd + WHERE rd.rollout_id = sqlc.arg('rollout_id') AND rd.device_id = d.id + ) + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = sqlc.arg('channel_id') + AND lower(r.manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(r.model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND r.assignment_generation = sqlc.arg('assignment_generation')::bigint + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +ORDER BY d.device_identifier; + +-- name: ListReleaseChannelSuppressedMembers :many +-- Members of one pair the enforcement rule currently suppresses: halted in +-- the most recent rollout of the given generation that holds them. +-- RetryFailedRolloutDevices re-queues exactly this set. +SELECT d.id AS device_id, + d.device_identifier +FROM release_channel_member m +JOIN device d ON d.id = m.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +WHERE m.channel_id = sqlc.arg('channel_id') + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = sqlc.arg('channel_id') + AND lower(r.manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(r.model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND r.assignment_generation = sqlc.arg('assignment_generation')::bigint + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +ORDER BY d.device_identifier; + +-- name: ListReleaseChannelFirmwareNeedingRollout :many +-- Assigned pairs with no active rollout and at least one mismatched, +-- unsuppressed member: late joiners, re-entries and miners that drifted. +SELECT f.channel_id, f.manufacturer, f.model, f.firmware_checksum, f.firmware_version, + f.firmware_target_manufacturer, f.firmware_target_model, f.assignment_generation, + f.assigned_by, f.updated_at, c.org_id +FROM release_channel_firmware f +JOIN release_channel c ON c.id = f.channel_id +WHERE f.firmware_checksum <> '' +AND NOT EXISTS ( + SELECT 1 FROM firmware_rollout r + WHERE r.channel_id = f.channel_id + AND lower(r.manufacturer COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(r.model COLLATE "C") = lower(f.model COLLATE "C") + AND r.status = 'active' +) +AND EXISTS ( + SELECT 1 + FROM release_channel_member m + JOIN device d ON d.id = m.device_id + JOIN discovered_device dd ON dd.id = d.discovered_device_id + LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id + WHERE m.channel_id = f.channel_id + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(f.model COLLATE "C") + AND NOT ( + COALESCE(dd.firmware_version, '') = f.firmware_version + AND COALESCE(dep.firmware_checksum, '') = f.firmware_checksum + ) + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd + JOIN firmware_rollout r ON r.id = rd.rollout_id + WHERE rd.device_id = d.id + AND r.channel_id = f.channel_id + AND lower(r.manufacturer COLLATE "C") = lower(f.manufacturer COLLATE "C") + AND lower(r.model COLLATE "C") = lower(f.model COLLATE "C") + AND r.assignment_generation = f.assignment_generation + AND rd.halted_at IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM firmware_rollout_device rd2 + JOIN firmware_rollout r2 ON r2.id = rd2.rollout_id + WHERE rd2.device_id = d.id + AND r2.channel_id = r.channel_id + AND r2.assignment_generation = r.assignment_generation + AND lower(r2.manufacturer COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(r2.model COLLATE "C") = lower(r.model COLLATE "C") + AND r2.created_at > r.created_at + ) + ) +) +ORDER BY f.channel_id, f.manufacturer, f.model; + +-- --- Rollouts --- + +-- name: CreateFirmwareRollout :one +INSERT INTO firmware_rollout ( + org_id, channel_id, manufacturer, model, firmware_checksum, firmware_version, + previous_firmware_checksum, previous_firmware_version, assignment_generation, + stage, + method, order_by, batch_size, pilot_size, wait_between_batches_seconds, + review_after_each_batch, auto_continue, stabilization_seconds, + max_hashrate_drop_percent, max_efficiency_increase_percent, max_temp_increase_c, max_new_errors, + min_sample_coverage_percent, max_concurrent_offline, controller_timeout_seconds, batch_count, + started_by_type, started_by_id, started_by_name, + last_action_by_type, last_action_by_id, last_action_by_name +) +VALUES ( + sqlc.arg('org_id'), sqlc.arg('channel_id'), sqlc.arg('manufacturer'), sqlc.arg('model'), sqlc.arg('firmware_checksum'), sqlc.arg('firmware_version'), + sqlc.arg('previous_firmware_checksum'), sqlc.arg('previous_firmware_version'), sqlc.arg('assignment_generation'), + sqlc.arg('stage'), + sqlc.arg('method'), sqlc.arg('order_by'), sqlc.arg('batch_size'), sqlc.arg('pilot_size'), sqlc.arg('wait_between_batches_seconds'), + sqlc.arg('review_after_each_batch'), sqlc.arg('auto_continue'), sqlc.arg('stabilization_seconds'), + sqlc.narg('max_hashrate_drop_percent')::double precision, sqlc.narg('max_efficiency_increase_percent')::double precision, + sqlc.narg('max_temp_increase_c')::double precision, sqlc.narg('max_new_errors')::int, + sqlc.narg('min_sample_coverage_percent')::double precision, sqlc.arg('max_concurrent_offline'), sqlc.arg('controller_timeout_seconds'), sqlc.arg('batch_count'), + sqlc.arg('actor_type'), sqlc.arg('actor_id'), sqlc.arg('actor_name'), + sqlc.arg('actor_type'), sqlc.arg('actor_id'), sqlc.arg('actor_name') +) +RETURNING *; + +-- name: GetFirmwareRollout :one +SELECT * FROM firmware_rollout +WHERE id = sqlc.arg('rollout_id') AND org_id = sqlc.arg('org_id'); + +-- name: GetFirmwareRolloutForUpdate :one +-- Locks the row for a mutation so the revision rule can be checked and the +-- change applied without a concurrent actor slipping in between. +SELECT * FROM firmware_rollout +WHERE id = sqlc.arg('rollout_id') AND org_id = sqlc.arg('org_id') +FOR UPDATE; + +-- name: GetFirmwareRolloutWithChannel :one +SELECT sqlc.embed(r), c.name AS channel_name +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.id = sqlc.arg('rollout_id') AND r.org_id = sqlc.arg('org_id'); + +-- name: ListFirmwareRollouts :many +-- Newest first. The cursor is the (created_at, id) of the last row of the +-- previous page; rows strictly older than it are returned. +SELECT sqlc.embed(r), c.name AS channel_name +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.org_id = sqlc.arg('org_id') + AND (sqlc.narg('channel_id')::bigint IS NULL OR r.channel_id = sqlc.narg('channel_id')) + AND (sqlc.narg('status')::text IS NULL OR r.status = sqlc.narg('status')) + AND (sqlc.narg('updated_after')::timestamptz IS NULL OR r.updated_at >= sqlc.narg('updated_after')) + AND ( + sqlc.narg('before_created_at')::timestamptz IS NULL + OR (r.created_at, r.id) < (sqlc.narg('before_created_at')::timestamptz, sqlc.narg('before_id')::bigint) + ) +ORDER BY r.created_at DESC, r.id DESC +LIMIT sqlc.arg('page_limit'); + +-- name: ListActiveFirmwareRollouts :many +-- Across all orgs; drives the enforcement loop. Carries the channel's live +-- offline budget, which governs every active rollout of the channel. +SELECT sqlc.embed(r), c.name AS channel_name, c.max_concurrent_offline AS channel_max_concurrent_offline +FROM firmware_rollout r +JOIN release_channel c ON c.id = r.channel_id +WHERE r.status = 'active' +ORDER BY r.id; + +-- name: GetLatestFirmwareRolloutForPair :one +-- The most recent rollout of a pair within one assignment generation; a +-- reconciliation rollout inherits its lineage. +SELECT * FROM firmware_rollout +WHERE channel_id = sqlc.arg('channel_id') + AND lower(manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND assignment_generation = sqlc.arg('assignment_generation') +ORDER BY created_at DESC, id DESC +LIMIT 1; + +-- name: GetActiveFirmwareRolloutForPair :one +SELECT * FROM firmware_rollout +WHERE channel_id = sqlc.arg('channel_id') + AND lower(manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND status = 'active'; + +-- name: CancelActiveFirmwareRollout :exec +-- Cancels the pair's active rollout because its assignment changed: +-- 'superseded', 'rolled_back' or 'cleared'. +UPDATE firmware_rollout +SET status = 'canceled', + finished_at = now(), + cancel_reason = sqlc.arg('cancel_reason'), + last_action_by_type = sqlc.arg('actor_type'), + last_action_by_id = sqlc.arg('actor_id'), + last_action_by_name = sqlc.arg('actor_name') +WHERE channel_id = sqlc.arg('channel_id') + AND lower(manufacturer COLLATE "C") = lower(sqlc.arg('manufacturer')::text COLLATE "C") + AND lower(model COLLATE "C") = lower(sqlc.arg('model')::text COLLATE "C") + AND status = 'active'; + +-- name: CancelFirmwareRollout :execrows +UPDATE firmware_rollout +SET status = 'canceled', + finished_at = now(), + cancel_reason = 'canceled_remaining', + last_action_by_type = sqlc.arg('actor_type'), + last_action_by_id = sqlc.arg('actor_id'), + last_action_by_name = sqlc.arg('actor_name') +WHERE id = sqlc.arg('rollout_id') AND status = 'active'; + +-- name: FinishFirmwareRollout :execrows +-- Ends an active rollout as 'completed' or 'completed_with_failures'. +UPDATE firmware_rollout +SET status = sqlc.arg('status'), finished_at = now() +WHERE id = sqlc.arg('rollout_id') AND status = 'active'; + +-- name: AdvanceFirmwareRolloutStage :execrows +-- Stage transitions of an active rollout, attributed to an actor when one +-- drove them. Returns the affected row count so callers can detect a lost race. +UPDATE firmware_rollout +SET stage = sqlc.arg('stage'), + current_batch = sqlc.arg('current_batch'), + stage_changed_at = now(), + last_action_by_type = COALESCE(sqlc.narg('actor_type')::text, last_action_by_type), + last_action_by_id = COALESCE(sqlc.narg('actor_id')::bigint, last_action_by_id), + last_action_by_name = COALESCE(sqlc.narg('actor_name')::text, last_action_by_name) +WHERE id = sqlc.arg('rollout_id') + AND status = 'active' + AND stage = sqlc.arg('from_stage'); + +-- name: PauseFirmwareRollout :execrows +UPDATE firmware_rollout +SET paused_at = now(), + last_action_by_type = sqlc.arg('actor_type'), + last_action_by_id = sqlc.arg('actor_id'), + last_action_by_name = sqlc.arg('actor_name') +WHERE id = sqlc.arg('rollout_id') AND status = 'active' AND paused_at IS NULL; + +-- name: ResumeFirmwareRollout :execrows +UPDATE firmware_rollout +SET paused_at = NULL, + last_action_by_type = sqlc.arg('actor_type'), + last_action_by_id = sqlc.arg('actor_id'), + last_action_by_name = sqlc.arg('actor_name') +WHERE id = sqlc.arg('rollout_id') AND status = 'active' AND paused_at IS NOT NULL; + +-- name: RecordFirmwareRolloutAction :exec +-- Attributes an action that changes only the rollout's devices (retry) to +-- its actor; the update itself bumps the revision. +UPDATE firmware_rollout +SET last_action_by_type = sqlc.arg('actor_type'), + last_action_by_id = sqlc.arg('actor_id'), + last_action_by_name = sqlc.arg('actor_name') +WHERE id = sqlc.arg('rollout_id'); + +-- --- Rollout devices --- + +-- name: ListFirmwareRolloutDevices :many +-- Every miner in a rollout with its bookkeeping, baseline, live health (device +-- status, latest telemetry within 15 minutes, open errors), provenance and +-- whether it is still a member of the channel for the rollout's pair. +SELECT rd.device_id, + d.device_identifier, + COALESCE(dd.firmware_version, '')::text AS firmware_version, + COALESCE(dd.ip_address, '')::text AS ip_address, + rd.batch_index, + rd.position, + rd.attempts, + rd.first_sent_at, + rd.last_sent_at, + rd.halted_at, + rd.halt_reason, + rd.last_error, + rd.skip_note, + rd.excluded_at, + rd.baseline_status, + rd.baseline_hash_rate_hs, + rd.baseline_power_w, + rd.baseline_efficiency_jh, + rd.baseline_temp_c, + rd.baseline_open_errors, + COALESCE(ds.status::text, '')::text AS status, + hm.hash_rate_hs, + hm.power_w, + hm.efficiency_jh, + hm.temp_c, + (SELECT count(*) FROM errors e + WHERE e.device_id = d.id AND e.closed_at IS NULL AND e.severity IN (1, 2, 3, 4))::int AS open_errors, + COALESCE(dep.firmware_checksum, '')::text AS last_deployed_firmware_checksum, + EXISTS ( + SELECT 1 FROM release_channel_member m + WHERE m.device_id = d.id AND m.channel_id = r.channel_id + ) + AND lower(btrim(COALESCE(dd.manufacturer, '')) COLLATE "C") = lower(r.manufacturer COLLATE "C") + AND lower(btrim(COALESCE(dd.model, '')) COLLATE "C") = lower(r.model COLLATE "C") AS in_scope +FROM firmware_rollout_device rd +JOIN firmware_rollout r ON r.id = rd.rollout_id +JOIN device d ON d.id = rd.device_id +JOIN discovered_device dd ON dd.id = d.discovered_device_id +LEFT JOIN device_status ds ON ds.device_id = d.id +LEFT JOIN device_firmware_deployment dep ON dep.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.hash_rate_hs, dm.power_w, dm.efficiency_jh, dm.temp_c + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE rd.rollout_id = sqlc.arg('rollout_id') +ORDER BY rd.position NULLS LAST, d.device_identifier; + +-- name: SnapshotFirmwareRolloutDevices :exec +-- Adds miners to a rollout with their batch (NULL for the unbatched rest / +-- late joiners), their order (position_offset + index in device_ids; NULL +-- offset for late joiners) and a baseline of their health, so post-update +-- evidence can be compared against each miner's own past. A miner that +-- re-enters the scope after being excluded is re-included and keeps its +-- original batch and order. +INSERT INTO firmware_rollout_device ( + rollout_id, device_id, batch_index, position, + baseline_status, baseline_hash_rate_hs, baseline_power_w, baseline_efficiency_jh, baseline_temp_c, + baseline_open_errors, baseline_at +) +SELECT sqlc.arg('rollout_id'), + d.id, + sqlc.narg('batch_index')::int, + sqlc.narg('position_offset')::int + array_position(sqlc.arg('device_ids')::bigint[], d.id), + ds.status::text, + hm.hash_rate_hs, + hm.power_w, + hm.efficiency_jh, + hm.temp_c, + (SELECT count(*) FROM errors e + WHERE e.device_id = d.id AND e.closed_at IS NULL AND e.severity IN (1, 2, 3, 4))::int, + now() +FROM device d +LEFT JOIN device_status ds ON ds.device_id = d.id +LEFT JOIN LATERAL ( + SELECT dm.hash_rate_hs, dm.power_w, dm.efficiency_jh, dm.temp_c + FROM device_metrics dm + WHERE dm.device_identifier = d.device_identifier + AND dm.time >= now() - INTERVAL '15 minutes' + ORDER BY dm.time DESC + LIMIT 1 +) hm ON true +WHERE d.id = ANY(sqlc.arg('device_ids')::bigint[]) +ON CONFLICT (rollout_id, device_id) DO UPDATE +SET batch_index = COALESCE(firmware_rollout_device.batch_index, EXCLUDED.batch_index), + position = COALESCE(firmware_rollout_device.position, EXCLUDED.position), + excluded_at = NULL; + +-- name: MarkFirmwareRolloutDevicesSent :exec +UPDATE firmware_rollout_device +SET attempts = attempts + 1, + first_sent_at = COALESCE(first_sent_at, now()), + last_sent_at = now() +WHERE rollout_id = sqlc.arg('rollout_id') + AND device_id = ANY(sqlc.arg('device_ids')::bigint[]); + +-- name: HaltFirmwareRolloutDevices :exec +-- Stops retrying miners for this version: 'failed' (attempts exhausted), +-- 'canceled' (operator canceled the remaining updates) or 'skipped' (a caller +-- settled them without updating, with its note). +UPDATE firmware_rollout_device +SET halted_at = now(), + halt_reason = sqlc.arg('halt_reason'), + last_error = sqlc.arg('last_error'), + skip_note = sqlc.arg('skip_note') +WHERE rollout_id = sqlc.arg('rollout_id') + AND device_id = ANY(sqlc.arg('device_ids')::bigint[]) + AND halted_at IS NULL; + +-- name: RequeueFirmwareRolloutDevices :many +-- Re-queues every halted miner of an active rollout from scratch and +-- returns them. +UPDATE firmware_rollout_device +SET halted_at = NULL, + halt_reason = '', + last_error = '', + skip_note = '', + attempts = 0, + first_sent_at = NULL, + last_sent_at = NULL +WHERE rollout_id = sqlc.arg('rollout_id') + AND halted_at IS NOT NULL +RETURNING device_id; + +-- name: ExcludeFirmwareRolloutDevices :exec +UPDATE firmware_rollout_device +SET excluded_at = now() +WHERE rollout_id = sqlc.arg('rollout_id') + AND device_id = ANY(sqlc.arg('device_ids')::bigint[]) + AND excluded_at IS NULL; + +-- name: RecordFirmwareDeployment :exec +-- Managed-deployment provenance: the miners listed reported the artifact a +-- rollout dispatched to them. +INSERT INTO device_firmware_deployment (device_id, firmware_checksum, firmware_version, rollout_id, deployed_at) +SELECT unnest(sqlc.arg('device_ids')::bigint[]), sqlc.arg('firmware_checksum'), sqlc.arg('firmware_version'), sqlc.arg('rollout_id'), now() +ON CONFLICT (device_id) DO UPDATE +SET firmware_checksum = EXCLUDED.firmware_checksum, + firmware_version = EXCLUDED.firmware_version, + rollout_id = EXCLUDED.rollout_id, + deployed_at = now();