diff --git a/client/src/protoFleet/api/buildings.ts b/client/src/protoFleet/api/buildings.ts index 233214e51..dae42b783 100644 --- a/client/src/protoFleet/api/buildings.ts +++ b/client/src/protoFleet/api/buildings.ts @@ -68,13 +68,20 @@ interface DeleteBuildingProps { onFinally?: () => void; } -interface AssignRackToBuildingProps { +// RackPlacementInput is one rack's slot in an AssignRacksToBuilding +// batch. aisleIndex + positionInAisle must be paired (both set or both +// unset); the server rejects half-set inputs. +export interface RackPlacementInput { rackId: bigint; - // Unset = unassign from any building. - buildingId?: bigint; - // Optional grid cell. Must be paired. aisleIndex?: number; positionInAisle?: number; +} + +interface AssignRacksToBuildingProps { + // Bulk-friendly. Pass a single-element array for the singular case. + racks: RackPlacementInput[]; + // Unset = unassign every rack in the batch from any building. + targetBuildingId?: bigint; signal?: AbortSignal; onSuccess?: (siteReassignedDeviceCount: bigint) => void; onError?: (message: string) => void; @@ -353,29 +360,17 @@ const useBuildings = () => { [handleAuthErrors], ); - // assignRackToBuilding wraps the dedicated rack-positioning RPC. - // Unset `buildingId` unassigns the rack; passing both `aisleIndex` - // and `positionInAisle` positions the rack at that grid cell. - // Passing one without the other is rejected by the server; the - // wrapper preserves the failure surface so callers can react. - const assignRackToBuilding = useCallback( - async ({ - rackId, - buildingId, - aisleIndex, - positionInAisle, - signal, - onSuccess, - onError, - onFinally, - }: AssignRackToBuildingProps) => { + // assignRacksToBuilding wraps the bulk rack-positioning RPC. + // Unset `targetBuildingId` unassigns every rack in the batch; + // each rack's `aisleIndex` + `positionInAisle` must be paired. + // The server rejects half-set inputs and out-of-bounds positions. + const assignRacksToBuilding = useCallback( + async ({ racks, targetBuildingId, signal, onSuccess, onError, onFinally }: AssignRacksToBuildingProps) => { try { - const response = await buildingsClient.assignRackToBuilding( + const response = await buildingsClient.assignRacksToBuilding( { - rackId, - buildingId, - aisleIndex, - positionInAisle, + racks, + targetBuildingId, }, { signal }, ); @@ -404,7 +399,7 @@ const useBuildings = () => { createBuilding, updateBuilding, deleteBuilding, - assignRackToBuilding, + assignRacksToBuilding, }; }; diff --git a/client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts b/client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts index 9656e675f..92277fcf9 100644 --- a/client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts +++ b/client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; export const file_buildings_v1_buildings: GenFile = /*@__PURE__*/ fileDesc( - "ChxidWlsZGluZ3MvdjEvYnVpbGRpbmdzLnByb3RvEgxidWlsZGluZ3MudjEioQMKCEJ1aWxkaW5nEgoKAmlkGAEgASgDEhQKB3NpdGVfaWQYAiABKANIAIgBARIMCgRuYW1lGAMgASgJEhMKC2Rlc2NyaXB0aW9uGAQgASgJEhAKCHBvd2VyX2t3GAUgASgBEhMKC292ZXJoZWFkX2t3GAYgASgBEg4KBmFpc2xlcxgHIAEoBRIbChNwaHlzaWNhbF9yYWNrX2NvdW50GAggASgFEhcKD3JhY2tzX3Blcl9haXNsZRgJIAEoBRIZChFkZWZhdWx0X3JhY2tfcm93cxgKIAEoBRIcChRkZWZhdWx0X3JhY2tfY29sdW1ucxgLIAEoBRI+ChhkZWZhdWx0X3JhY2tfb3JkZXJfaW5kZXgYDCABKA4yHC5idWlsZGluZ3MudjEuUmFja09yZGVySW5kZXgSLgoKY3JlYXRlZF9hdBgNIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKdXBkYXRlZF9hdBgOIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCCgoIX3NpdGVfaWQiUgoSQnVpbGRpbmdXaXRoQ291bnRzEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nEhIKCnJhY2tfY291bnQYAiABKAMiXAoUTGlzdEJ1aWxkaW5nc1JlcXVlc3QSGgoHc2l0ZV9pZBgBIAEoA0IHukgEIgIgAEgAEhkKD3VuYXNzaWduZWRfb25seRgCIAEoCEgAQg0KC3NpdGVfZmlsdGVyIkwKFUxpc3RCdWlsZGluZ3NSZXNwb25zZRIzCglidWlsZGluZ3MYASADKAsyIC5idWlsZGluZ3MudjEuQnVpbGRpbmdXaXRoQ291bnRzIikKEkdldEJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgACI/ChNHZXRCdWlsZGluZ1Jlc3BvbnNlEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nIuoEChVDcmVhdGVCdWlsZGluZ1JlcXVlc3QSHQoHc2l0ZV9pZBgBIAEoA0IHukgEIgIgAEgAiAEBEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESHQoLZGVzY3JpcHRpb24YAyABKAlCCLpIBXIDGIAgEiAKCHBvd2VyX2t3GAQgASgBQg66SAsSCSkAAAAAAAAAABIjCgtvdmVyaGVhZF9rdxgFIAEoAUIOukgLEgkpAAAAAAAAAAASGQoGYWlzbGVzGAYgASgFQgm6SAYaBBhkKAASJAoTcGh5c2ljYWxfcmFja19jb3VudBgHIAEoBUIHukgEGgIoABIiCg9yYWNrc19wZXJfYWlzbGUYCCABKAVCCbpIBhoEGGQoABIiChFkZWZhdWx0X3JhY2tfcm93cxgJIAEoBUIHukgEGgIoABIlChRkZWZhdWx0X3JhY2tfY29sdW1ucxgKIAEoBUIHukgEGgIoABJIChhkZWZhdWx0X3JhY2tfb3JkZXJfaW5kZXgYCyABKA4yHC5idWlsZGluZ3MudjEuUmFja09yZGVySW5kZXhCCLpIBYIBAhABOqsBukinARqkAQoYZGVmYXVsdF9yYWNrX2RpbXNfcGFpcmVkEkVkZWZhdWx0X3JhY2tfcm93cyBhbmQgZGVmYXVsdF9yYWNrX2NvbHVtbnMgbXVzdCBib3RoIGJlIDAgb3IgYm90aCA+IDAaQSh0aGlzLmRlZmF1bHRfcmFja19yb3dzID09IDApID09ICh0aGlzLmRlZmF1bHRfcmFja19jb2x1bW5zID09IDApQgoKCF9zaXRlX2lkIkIKFkNyZWF0ZUJ1aWxkaW5nUmVzcG9uc2USKAoIYnVpbGRpbmcYASABKAsyFi5idWlsZGluZ3MudjEuQnVpbGRpbmci1AQKFVVwZGF0ZUJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgABIYCgRuYW1lGAIgASgJQgq6SAdyBRABGP8BEh0KC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxiAIBIgCghwb3dlcl9rdxgEIAEoAUIOukgLEgkpAAAAAAAAAAASIwoLb3ZlcmhlYWRfa3cYBSABKAFCDrpICxIJKQAAAAAAAAAAEhkKBmFpc2xlcxgGIAEoBUIJukgGGgQYZCgAEiQKE3BoeXNpY2FsX3JhY2tfY291bnQYByABKAVCB7pIBBoCKAASIgoPcmFja3NfcGVyX2Fpc2xlGAggASgFQgm6SAYaBBhkKAASIgoRZGVmYXVsdF9yYWNrX3Jvd3MYCSABKAVCB7pIBBoCKAASJQoUZGVmYXVsdF9yYWNrX2NvbHVtbnMYCiABKAVCB7pIBBoCKAASSAoYZGVmYXVsdF9yYWNrX29yZGVyX2luZGV4GAsgASgOMhwuYnVpbGRpbmdzLnYxLlJhY2tPcmRlckluZGV4Qgi6SAWCAQIQATqrAbpIpwEapAEKGGRlZmF1bHRfcmFja19kaW1zX3BhaXJlZBJFZGVmYXVsdF9yYWNrX3Jvd3MgYW5kIGRlZmF1bHRfcmFja19jb2x1bW5zIG11c3QgYm90aCBiZSAwIG9yIGJvdGggPiAwGkEodGhpcy5kZWZhdWx0X3JhY2tfcm93cyA9PSAwKSA9PSAodGhpcy5kZWZhdWx0X3JhY2tfY29sdW1ucyA9PSAwKSJCChZVcGRhdGVCdWlsZGluZ1Jlc3BvbnNlEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nIiwKFURlbGV0ZUJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgACI3ChZEZWxldGVCdWlsZGluZ1Jlc3BvbnNlEh0KFXVuYXNzaWduZWRfcmFja19jb3VudBgBIAEoAyJ1ChhMaXN0QnVpbGRpbmdSYWNrc1JlcXVlc3QSHAoLYnVpbGRpbmdfaWQYASABKANCB7pIBCICIAASHQoJcGFnZV9zaXplGAIgASgFQgq6SAcaBRjoBygAEhwKCnBhZ2VfdG9rZW4YAyABKAlCCLpIBXIDGIAQIpMBCgxCdWlsZGluZ1JhY2sSDwoHcmFja19pZBgBIAEoAxISCgpyYWNrX2xhYmVsGAIgASgJEhgKC2Fpc2xlX2luZGV4GAMgASgFSACIAQESHgoRcG9zaXRpb25faW5fYWlzbGUYBCABKAVIAYgBAUIOCgxfYWlzbGVfaW5kZXhCFAoSX3Bvc2l0aW9uX2luX2Fpc2xlIl8KGUxpc3RCdWlsZGluZ1JhY2tzUmVzcG9uc2USKQoFcmFja3MYASADKAsyGi5idWlsZGluZ3MudjEuQnVpbGRpbmdSYWNrEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSL2AgobQXNzaWduUmFja1RvQnVpbGRpbmdSZXF1ZXN0EhgKB3JhY2tfaWQYASABKANCB7pIBCICIAASIQoLYnVpbGRpbmdfaWQYAiABKANCB7pIBCICIABIAIgBARIhCgthaXNsZV9pbmRleBgDIAEoBUIHukgEGgIoAEgBiAEBEicKEXBvc2l0aW9uX2luX2Fpc2xlGAQgASgFQge6SAQaAigASAKIAQE6lwG6SJMBGpABChZwb3NpdGlvbl9wYWlyX3JlcXVpcmVkEkBhaXNsZV9pbmRleCBhbmQgcG9zaXRpb25faW5fYWlzbGUgbXVzdCBib3RoIGJlIHNldCBvciBib3RoIHVuc2V0GjRoYXModGhpcy5haXNsZV9pbmRleCkgPT0gaGFzKHRoaXMucG9zaXRpb25faW5fYWlzbGUpQg4KDF9idWlsZGluZ19pZEIOCgxfYWlzbGVfaW5kZXhCFAoSX3Bvc2l0aW9uX2luX2Fpc2xlIkQKHEFzc2lnblJhY2tUb0J1aWxkaW5nUmVzcG9uc2USJAocc2l0ZV9yZWFzc2lnbmVkX2RldmljZV9jb3VudBgBIAEoAyI3ChdHZXRCdWlsZGluZ1N0YXRzUmVxdWVzdBIcCgtidWlsZGluZ19pZBgBIAEoA0IHukgEIgIgACLWAwoYR2V0QnVpbGRpbmdTdGF0c1Jlc3BvbnNlEhMKC2J1aWxkaW5nX2lkGAEgASgDEhIKCnJhY2tfY291bnQYAiABKAUSFAoMZGV2aWNlX2NvdW50GAMgASgFEhcKD3JlcG9ydGluZ19jb3VudBgEIAEoBRIaChJ0b3RhbF9oYXNocmF0ZV90aHMYBSABKAESGgoSYXZnX2VmZmljaWVuY3lfanRoGAYgASgBEhYKDnRvdGFsX3Bvd2VyX2t3GAcgASgBEhUKDWhhc2hpbmdfY291bnQYCCABKAUSFAoMYnJva2VuX2NvdW50GAkgASgFEhUKDW9mZmxpbmVfY291bnQYCiABKAUSFgoOc2xlZXBpbmdfY291bnQYCyABKAUSNQoLcmFja19oZWFsdGgYDCADKAsyIC5idWlsZGluZ3MudjEuQnVpbGRpbmdSYWNrSGVhbHRoEhoKEmRldmljZV9pZGVudGlmaWVycxgNIAMoCRIgChhoYXNocmF0ZV9yZXBvcnRpbmdfY291bnQYDiABKAUSIgoaZWZmaWNpZW5jeV9yZXBvcnRpbmdfY291bnQYDyABKAUSHQoVcG93ZXJfcmVwb3J0aW5nX2NvdW50GBAgASgFIvUBChJCdWlsZGluZ1JhY2tIZWFsdGgSDwoHcmFja19pZBgBIAEoAxISCgpyYWNrX2xhYmVsGAIgASgJEhgKC2Fpc2xlX2luZGV4GAMgASgFSACIAQESHgoRcG9zaXRpb25faW5fYWlzbGUYBCABKAVIAYgBARIVCg1oYXNoaW5nX2NvdW50GAUgASgFEhQKDGJyb2tlbl9jb3VudBgGIAEoBRIVCg1vZmZsaW5lX2NvdW50GAcgASgFEhYKDnNsZWVwaW5nX2NvdW50GAggASgFQg4KDF9haXNsZV9pbmRleEIUChJfcG9zaXRpb25faW5fYWlzbGUqtgEKDlJhY2tPcmRlckluZGV4EiAKHFJBQ0tfT1JERVJfSU5ERVhfVU5TUEVDSUZJRUQQABIgChxSQUNLX09SREVSX0lOREVYX0JPVFRPTV9MRUZUEAESHQoZUkFDS19PUkRFUl9JTkRFWF9UT1BfTEVGVBACEiEKHVJBQ0tfT1JERVJfSU5ERVhfQk9UVE9NX1JJR0hUEAMSHgoaUkFDS19PUkRFUl9JTkRFWF9UT1BfUklHSFQQBDKOBgoPQnVpbGRpbmdTZXJ2aWNlElgKDUxpc3RCdWlsZGluZ3MSIi5idWlsZGluZ3MudjEuTGlzdEJ1aWxkaW5nc1JlcXVlc3QaIy5idWlsZGluZ3MudjEuTGlzdEJ1aWxkaW5nc1Jlc3BvbnNlElIKC0dldEJ1aWxkaW5nEiAuYnVpbGRpbmdzLnYxLkdldEJ1aWxkaW5nUmVxdWVzdBohLmJ1aWxkaW5ncy52MS5HZXRCdWlsZGluZ1Jlc3BvbnNlElsKDkNyZWF0ZUJ1aWxkaW5nEiMuYnVpbGRpbmdzLnYxLkNyZWF0ZUJ1aWxkaW5nUmVxdWVzdBokLmJ1aWxkaW5ncy52MS5DcmVhdGVCdWlsZGluZ1Jlc3BvbnNlElsKDlVwZGF0ZUJ1aWxkaW5nEiMuYnVpbGRpbmdzLnYxLlVwZGF0ZUJ1aWxkaW5nUmVxdWVzdBokLmJ1aWxkaW5ncy52MS5VcGRhdGVCdWlsZGluZ1Jlc3BvbnNlElsKDkRlbGV0ZUJ1aWxkaW5nEiMuYnVpbGRpbmdzLnYxLkRlbGV0ZUJ1aWxkaW5nUmVxdWVzdBokLmJ1aWxkaW5ncy52MS5EZWxldGVCdWlsZGluZ1Jlc3BvbnNlEmQKEUxpc3RCdWlsZGluZ1JhY2tzEiYuYnVpbGRpbmdzLnYxLkxpc3RCdWlsZGluZ1JhY2tzUmVxdWVzdBonLmJ1aWxkaW5ncy52MS5MaXN0QnVpbGRpbmdSYWNrc1Jlc3BvbnNlEm0KFEFzc2lnblJhY2tUb0J1aWxkaW5nEikuYnVpbGRpbmdzLnYxLkFzc2lnblJhY2tUb0J1aWxkaW5nUmVxdWVzdBoqLmJ1aWxkaW5ncy52MS5Bc3NpZ25SYWNrVG9CdWlsZGluZ1Jlc3BvbnNlEmEKEEdldEJ1aWxkaW5nU3RhdHMSJS5idWlsZGluZ3MudjEuR2V0QnVpbGRpbmdTdGF0c1JlcXVlc3QaJi5idWlsZGluZ3MudjEuR2V0QnVpbGRpbmdTdGF0c1Jlc3BvbnNlQsABChBjb20uYnVpbGRpbmdzLnYxQg5CdWlsZGluZ3NQcm90b1ABWktnaXRodWIuY29tL2Jsb2NrL3Byb3RvLWZsZWV0L3NlcnZlci9nZW5lcmF0ZWQvZ3JwYy9idWlsZGluZ3MvdjE7YnVpbGRpbmdzdjGiAgNCWFiqAgxCdWlsZGluZ3MuVjHKAgxCdWlsZGluZ3NcVjHiAhhCdWlsZGluZ3NcVjFcR1BCTWV0YWRhdGHqAg1CdWlsZGluZ3M6OlYxYgZwcm90bzM", + "ChxidWlsZGluZ3MvdjEvYnVpbGRpbmdzLnByb3RvEgxidWlsZGluZ3MudjEioQMKCEJ1aWxkaW5nEgoKAmlkGAEgASgDEhQKB3NpdGVfaWQYAiABKANIAIgBARIMCgRuYW1lGAMgASgJEhMKC2Rlc2NyaXB0aW9uGAQgASgJEhAKCHBvd2VyX2t3GAUgASgBEhMKC292ZXJoZWFkX2t3GAYgASgBEg4KBmFpc2xlcxgHIAEoBRIbChNwaHlzaWNhbF9yYWNrX2NvdW50GAggASgFEhcKD3JhY2tzX3Blcl9haXNsZRgJIAEoBRIZChFkZWZhdWx0X3JhY2tfcm93cxgKIAEoBRIcChRkZWZhdWx0X3JhY2tfY29sdW1ucxgLIAEoBRI+ChhkZWZhdWx0X3JhY2tfb3JkZXJfaW5kZXgYDCABKA4yHC5idWlsZGluZ3MudjEuUmFja09yZGVySW5kZXgSLgoKY3JlYXRlZF9hdBgNIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKdXBkYXRlZF9hdBgOIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCCgoIX3NpdGVfaWQiUgoSQnVpbGRpbmdXaXRoQ291bnRzEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nEhIKCnJhY2tfY291bnQYAiABKAMiXAoUTGlzdEJ1aWxkaW5nc1JlcXVlc3QSGgoHc2l0ZV9pZBgBIAEoA0IHukgEIgIgAEgAEhkKD3VuYXNzaWduZWRfb25seRgCIAEoCEgAQg0KC3NpdGVfZmlsdGVyIkwKFUxpc3RCdWlsZGluZ3NSZXNwb25zZRIzCglidWlsZGluZ3MYASADKAsyIC5idWlsZGluZ3MudjEuQnVpbGRpbmdXaXRoQ291bnRzIikKEkdldEJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgACI/ChNHZXRCdWlsZGluZ1Jlc3BvbnNlEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nIuoEChVDcmVhdGVCdWlsZGluZ1JlcXVlc3QSHQoHc2l0ZV9pZBgBIAEoA0IHukgEIgIgAEgAiAEBEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESHQoLZGVzY3JpcHRpb24YAyABKAlCCLpIBXIDGIAgEiAKCHBvd2VyX2t3GAQgASgBQg66SAsSCSkAAAAAAAAAABIjCgtvdmVyaGVhZF9rdxgFIAEoAUIOukgLEgkpAAAAAAAAAAASGQoGYWlzbGVzGAYgASgFQgm6SAYaBBhkKAASJAoTcGh5c2ljYWxfcmFja19jb3VudBgHIAEoBUIHukgEGgIoABIiCg9yYWNrc19wZXJfYWlzbGUYCCABKAVCCbpIBhoEGGQoABIiChFkZWZhdWx0X3JhY2tfcm93cxgJIAEoBUIHukgEGgIoABIlChRkZWZhdWx0X3JhY2tfY29sdW1ucxgKIAEoBUIHukgEGgIoABJIChhkZWZhdWx0X3JhY2tfb3JkZXJfaW5kZXgYCyABKA4yHC5idWlsZGluZ3MudjEuUmFja09yZGVySW5kZXhCCLpIBYIBAhABOqsBukinARqkAQoYZGVmYXVsdF9yYWNrX2RpbXNfcGFpcmVkEkVkZWZhdWx0X3JhY2tfcm93cyBhbmQgZGVmYXVsdF9yYWNrX2NvbHVtbnMgbXVzdCBib3RoIGJlIDAgb3IgYm90aCA+IDAaQSh0aGlzLmRlZmF1bHRfcmFja19yb3dzID09IDApID09ICh0aGlzLmRlZmF1bHRfcmFja19jb2x1bW5zID09IDApQgoKCF9zaXRlX2lkIkIKFkNyZWF0ZUJ1aWxkaW5nUmVzcG9uc2USKAoIYnVpbGRpbmcYASABKAsyFi5idWlsZGluZ3MudjEuQnVpbGRpbmci1AQKFVVwZGF0ZUJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgABIYCgRuYW1lGAIgASgJQgq6SAdyBRABGP8BEh0KC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxiAIBIgCghwb3dlcl9rdxgEIAEoAUIOukgLEgkpAAAAAAAAAAASIwoLb3ZlcmhlYWRfa3cYBSABKAFCDrpICxIJKQAAAAAAAAAAEhkKBmFpc2xlcxgGIAEoBUIJukgGGgQYZCgAEiQKE3BoeXNpY2FsX3JhY2tfY291bnQYByABKAVCB7pIBBoCKAASIgoPcmFja3NfcGVyX2Fpc2xlGAggASgFQgm6SAYaBBhkKAASIgoRZGVmYXVsdF9yYWNrX3Jvd3MYCSABKAVCB7pIBBoCKAASJQoUZGVmYXVsdF9yYWNrX2NvbHVtbnMYCiABKAVCB7pIBBoCKAASSAoYZGVmYXVsdF9yYWNrX29yZGVyX2luZGV4GAsgASgOMhwuYnVpbGRpbmdzLnYxLlJhY2tPcmRlckluZGV4Qgi6SAWCAQIQATqrAbpIpwEapAEKGGRlZmF1bHRfcmFja19kaW1zX3BhaXJlZBJFZGVmYXVsdF9yYWNrX3Jvd3MgYW5kIGRlZmF1bHRfcmFja19jb2x1bW5zIG11c3QgYm90aCBiZSAwIG9yIGJvdGggPiAwGkEodGhpcy5kZWZhdWx0X3JhY2tfcm93cyA9PSAwKSA9PSAodGhpcy5kZWZhdWx0X3JhY2tfY29sdW1ucyA9PSAwKSJCChZVcGRhdGVCdWlsZGluZ1Jlc3BvbnNlEigKCGJ1aWxkaW5nGAEgASgLMhYuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nIiwKFURlbGV0ZUJ1aWxkaW5nUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgACI3ChZEZWxldGVCdWlsZGluZ1Jlc3BvbnNlEh0KFXVuYXNzaWduZWRfcmFja19jb3VudBgBIAEoAyJ1ChhMaXN0QnVpbGRpbmdSYWNrc1JlcXVlc3QSHAoLYnVpbGRpbmdfaWQYASABKANCB7pIBCICIAASHQoJcGFnZV9zaXplGAIgASgFQgq6SAcaBRjoBygAEhwKCnBhZ2VfdG9rZW4YAyABKAlCCLpIBXIDGIAQIpMBCgxCdWlsZGluZ1JhY2sSDwoHcmFja19pZBgBIAEoAxISCgpyYWNrX2xhYmVsGAIgASgJEhgKC2Fpc2xlX2luZGV4GAMgASgFSACIAQESHgoRcG9zaXRpb25faW5fYWlzbGUYBCABKAVIAYgBAUIOCgxfYWlzbGVfaW5kZXhCFAoSX3Bvc2l0aW9uX2luX2Fpc2xlIl8KGUxpc3RCdWlsZGluZ1JhY2tzUmVzcG9uc2USKQoFcmFja3MYASADKAsyGi5idWlsZGluZ3MudjEuQnVpbGRpbmdSYWNrEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSK1AgoNUmFja1BsYWNlbWVudBIYCgdyYWNrX2lkGAEgASgDQge6SAQiAiAAEiEKC2Fpc2xlX2luZGV4GAIgASgFQge6SAQaAigASACIAQESJwoRcG9zaXRpb25faW5fYWlzbGUYAyABKAVCB7pIBBoCKABIAYgBATqXAbpIkwEakAEKFnBvc2l0aW9uX3BhaXJfcmVxdWlyZWQSQGFpc2xlX2luZGV4IGFuZCBwb3NpdGlvbl9pbl9haXNsZSBtdXN0IGJvdGggYmUgc2V0IG9yIGJvdGggdW5zZXQaNGhhcyh0aGlzLmFpc2xlX2luZGV4KSA9PSBoYXModGhpcy5wb3NpdGlvbl9pbl9haXNsZSlCDgoMX2Fpc2xlX2luZGV4QhQKEl9wb3NpdGlvbl9pbl9haXNsZSKYAQocQXNzaWduUmFja3NUb0J1aWxkaW5nUmVxdWVzdBI3CgVyYWNrcxgBIAMoCzIbLmJ1aWxkaW5ncy52MS5SYWNrUGxhY2VtZW50Qgu6SAiSAQUIARDoBxIoChJ0YXJnZXRfYnVpbGRpbmdfaWQYAiABKANCB7pIBCICIABIAIgBAUIVChNfdGFyZ2V0X2J1aWxkaW5nX2lkIkUKHUFzc2lnblJhY2tzVG9CdWlsZGluZ1Jlc3BvbnNlEiQKHHNpdGVfcmVhc3NpZ25lZF9kZXZpY2VfY291bnQYASABKAMiNwoXR2V0QnVpbGRpbmdTdGF0c1JlcXVlc3QSHAoLYnVpbGRpbmdfaWQYASABKANCB7pIBCICIAAi1gMKGEdldEJ1aWxkaW5nU3RhdHNSZXNwb25zZRITCgtidWlsZGluZ19pZBgBIAEoAxISCgpyYWNrX2NvdW50GAIgASgFEhQKDGRldmljZV9jb3VudBgDIAEoBRIXCg9yZXBvcnRpbmdfY291bnQYBCABKAUSGgoSdG90YWxfaGFzaHJhdGVfdGhzGAUgASgBEhoKEmF2Z19lZmZpY2llbmN5X2p0aBgGIAEoARIWCg50b3RhbF9wb3dlcl9rdxgHIAEoARIVCg1oYXNoaW5nX2NvdW50GAggASgFEhQKDGJyb2tlbl9jb3VudBgJIAEoBRIVCg1vZmZsaW5lX2NvdW50GAogASgFEhYKDnNsZWVwaW5nX2NvdW50GAsgASgFEjUKC3JhY2tfaGVhbHRoGAwgAygLMiAuYnVpbGRpbmdzLnYxLkJ1aWxkaW5nUmFja0hlYWx0aBIaChJkZXZpY2VfaWRlbnRpZmllcnMYDSADKAkSIAoYaGFzaHJhdGVfcmVwb3J0aW5nX2NvdW50GA4gASgFEiIKGmVmZmljaWVuY3lfcmVwb3J0aW5nX2NvdW50GA8gASgFEh0KFXBvd2VyX3JlcG9ydGluZ19jb3VudBgQIAEoBSL1AQoSQnVpbGRpbmdSYWNrSGVhbHRoEg8KB3JhY2tfaWQYASABKAMSEgoKcmFja19sYWJlbBgCIAEoCRIYCgthaXNsZV9pbmRleBgDIAEoBUgAiAEBEh4KEXBvc2l0aW9uX2luX2Fpc2xlGAQgASgFSAGIAQESFQoNaGFzaGluZ19jb3VudBgFIAEoBRIUCgxicm9rZW5fY291bnQYBiABKAUSFQoNb2ZmbGluZV9jb3VudBgHIAEoBRIWCg5zbGVlcGluZ19jb3VudBgIIAEoBUIOCgxfYWlzbGVfaW5kZXhCFAoSX3Bvc2l0aW9uX2luX2Fpc2xlKrYBCg5SYWNrT3JkZXJJbmRleBIgChxSQUNLX09SREVSX0lOREVYX1VOU1BFQ0lGSUVEEAASIAocUkFDS19PUkRFUl9JTkRFWF9CT1RUT01fTEVGVBABEh0KGVJBQ0tfT1JERVJfSU5ERVhfVE9QX0xFRlQQAhIhCh1SQUNLX09SREVSX0lOREVYX0JPVFRPTV9SSUdIVBADEh4KGlJBQ0tfT1JERVJfSU5ERVhfVE9QX1JJR0hUEAQykQYKD0J1aWxkaW5nU2VydmljZRJYCg1MaXN0QnVpbGRpbmdzEiIuYnVpbGRpbmdzLnYxLkxpc3RCdWlsZGluZ3NSZXF1ZXN0GiMuYnVpbGRpbmdzLnYxLkxpc3RCdWlsZGluZ3NSZXNwb25zZRJSCgtHZXRCdWlsZGluZxIgLmJ1aWxkaW5ncy52MS5HZXRCdWlsZGluZ1JlcXVlc3QaIS5idWlsZGluZ3MudjEuR2V0QnVpbGRpbmdSZXNwb25zZRJbCg5DcmVhdGVCdWlsZGluZxIjLmJ1aWxkaW5ncy52MS5DcmVhdGVCdWlsZGluZ1JlcXVlc3QaJC5idWlsZGluZ3MudjEuQ3JlYXRlQnVpbGRpbmdSZXNwb25zZRJbCg5VcGRhdGVCdWlsZGluZxIjLmJ1aWxkaW5ncy52MS5VcGRhdGVCdWlsZGluZ1JlcXVlc3QaJC5idWlsZGluZ3MudjEuVXBkYXRlQnVpbGRpbmdSZXNwb25zZRJbCg5EZWxldGVCdWlsZGluZxIjLmJ1aWxkaW5ncy52MS5EZWxldGVCdWlsZGluZ1JlcXVlc3QaJC5idWlsZGluZ3MudjEuRGVsZXRlQnVpbGRpbmdSZXNwb25zZRJkChFMaXN0QnVpbGRpbmdSYWNrcxImLmJ1aWxkaW5ncy52MS5MaXN0QnVpbGRpbmdSYWNrc1JlcXVlc3QaJy5idWlsZGluZ3MudjEuTGlzdEJ1aWxkaW5nUmFja3NSZXNwb25zZRJwChVBc3NpZ25SYWNrc1RvQnVpbGRpbmcSKi5idWlsZGluZ3MudjEuQXNzaWduUmFja3NUb0J1aWxkaW5nUmVxdWVzdBorLmJ1aWxkaW5ncy52MS5Bc3NpZ25SYWNrc1RvQnVpbGRpbmdSZXNwb25zZRJhChBHZXRCdWlsZGluZ1N0YXRzEiUuYnVpbGRpbmdzLnYxLkdldEJ1aWxkaW5nU3RhdHNSZXF1ZXN0GiYuYnVpbGRpbmdzLnYxLkdldEJ1aWxkaW5nU3RhdHNSZXNwb25zZULAAQoQY29tLmJ1aWxkaW5ncy52MUIOQnVpbGRpbmdzUHJvdG9QAVpLZ2l0aHViLmNvbS9ibG9jay9wcm90by1mbGVldC9zZXJ2ZXIvZ2VuZXJhdGVkL2dycGMvYnVpbGRpbmdzL3YxO2J1aWxkaW5nc3YxogIDQlhYqgIMQnVpbGRpbmdzLlYxygIMQnVpbGRpbmdzXFYx4gIYQnVpbGRpbmdzXFYxXEdQQk1ldGFkYXRh6gINQnVpbGRpbmdzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp], ); @@ -547,53 +547,75 @@ export const ListBuildingRacksResponseSchema: GenMessage & { +export type RackPlacement = Message<"buildings.v1.RackPlacement"> & { /** * @generated from field: int64 rack_id = 1; */ rackId: bigint; - /** - * Unset = unassign rack from any building. When present, must be > 0. - * - * @generated from field: optional int64 building_id = 2; - */ - buildingId?: bigint | undefined; - /** * Optional grid placement inside the target building. Must be set * together or both unset (a half-set position would be ambiguous). * Upper bounds are validated server-side against the target * building's aisles / racks_per_aisle. * - * @generated from field: optional int32 aisle_index = 3; + * @generated from field: optional int32 aisle_index = 2; */ aisleIndex?: number | undefined; /** - * @generated from field: optional int32 position_in_aisle = 4; + * @generated from field: optional int32 position_in_aisle = 3; */ positionInAisle?: number | undefined; }; /** - * Describes the message buildings.v1.AssignRackToBuildingRequest. - * Use `create(AssignRackToBuildingRequestSchema)` to create a new message. + * Describes the message buildings.v1.RackPlacement. + * Use `create(RackPlacementSchema)` to create a new message. */ -export const AssignRackToBuildingRequestSchema: GenMessage = +export const RackPlacementSchema: GenMessage = /*@__PURE__*/ messageDesc(file_buildings_v1_buildings, 15); /** - * @generated from message buildings.v1.AssignRackToBuildingResponse + * @generated from message buildings.v1.AssignRacksToBuildingRequest */ -export type AssignRackToBuildingResponse = Message<"buildings.v1.AssignRackToBuildingResponse"> & { +export type AssignRacksToBuildingRequest = Message<"buildings.v1.AssignRacksToBuildingRequest"> & { /** - * Cascade impact: how many descendant devices had their site_id - * re-stamped to the target building's site_id (or NULL when the - * rack moved to unassigned). Zero when no cascade was needed. + * @generated from field: repeated buildings.v1.RackPlacement racks = 1; + */ + racks: RackPlacement[]; + + /** + * Unset = unassign racks from any building. When present, must be > 0. + * + * @generated from field: optional int64 target_building_id = 2; + */ + targetBuildingId?: bigint | undefined; +}; + +/** + * Describes the message buildings.v1.AssignRacksToBuildingRequest. + * Use `create(AssignRacksToBuildingRequestSchema)` to create a new message. + */ +export const AssignRacksToBuildingRequestSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_buildings_v1_buildings, 16); + +/** + * @generated from message buildings.v1.AssignRacksToBuildingResponse + */ +export type AssignRacksToBuildingResponse = Message<"buildings.v1.AssignRacksToBuildingResponse"> & { + /** + * Cascade impact: aggregate descendant devices that had their + * site_id re-stamped across every rack in the batch. Zero when no + * cascade was needed. * * @generated from field: int64 site_reassigned_device_count = 1; */ @@ -601,12 +623,12 @@ export type AssignRackToBuildingResponse = Message<"buildings.v1.AssignRackToBui }; /** - * Describes the message buildings.v1.AssignRackToBuildingResponse. - * Use `create(AssignRackToBuildingResponseSchema)` to create a new message. + * Describes the message buildings.v1.AssignRacksToBuildingResponse. + * Use `create(AssignRacksToBuildingResponseSchema)` to create a new message. */ -export const AssignRackToBuildingResponseSchema: GenMessage = +export const AssignRacksToBuildingResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_buildings_v1_buildings, 16); + messageDesc(file_buildings_v1_buildings, 17); /** * @generated from message buildings.v1.GetBuildingStatsRequest @@ -624,7 +646,7 @@ export type GetBuildingStatsRequest = Message<"buildings.v1.GetBuildingStatsRequ */ export const GetBuildingStatsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_buildings_v1_buildings, 17); + messageDesc(file_buildings_v1_buildings, 18); /** * GetBuildingStatsResponse mirrors DeviceSetStats for the rollup fields @@ -746,7 +768,7 @@ export type GetBuildingStatsResponse = Message<"buildings.v1.GetBuildingStatsRes */ export const GetBuildingStatsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_buildings_v1_buildings, 18); + messageDesc(file_buildings_v1_buildings, 19); /** * BuildingRackHealth is the per-rack rollup returned by GetBuildingStats. @@ -804,7 +826,7 @@ export type BuildingRackHealth = Message<"buildings.v1.BuildingRackHealth"> & { */ export const BuildingRackHealthSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_buildings_v1_buildings, 19); + messageDesc(file_buildings_v1_buildings, 20); /** * RackOrderIndex mirrors device_set/v1's enum. We re-declare it here @@ -895,7 +917,7 @@ export const BuildingService: GenService<{ /** * UpdateBuilding mutates name, description, capacity, layout * defaults. Site assignment is *not* changed by UpdateBuilding — - * use AssignBuildingToSite (SiteService) for that, since it has + * use AssignBuildingsToSite (SiteService) for that, since it has * cross-collection enforcement. * * @generated from rpc buildings.v1.BuildingService.UpdateBuilding @@ -932,21 +954,23 @@ export const BuildingService: GenService<{ output: typeof ListBuildingRacksResponseSchema; }; /** - * AssignRackToBuilding sets the rack's building_id and optionally + * AssignRacksToBuilding sets each rack's building_id and optionally * positions it at (aisle_index, position_in_aisle) inside the - * building's grid. Leaving building_id unset unassigns the rack - * from any building. Position fields must be set together or - * both unset; the server clears them automatically on any - * building_id transition. Runs the same site-cascade for - * descendant device.site_id that SaveRack runs when the rack's - * site changes. + * target building's grid. Leaving target_building_id unset + * unassigns every rack in the batch from any building. Each entry + * carries its own optional placement; entries with unset placement + * have their grid cell cleared (single-call mix of placed + cleared + * racks is supported). The entire batch runs in one transaction; if + * any rack fails validation or its update, no row is touched. Runs + * the same site-cascade for descendant device.site_id that SaveRack + * runs when a rack's site changes. * - * @generated from rpc buildings.v1.BuildingService.AssignRackToBuilding + * @generated from rpc buildings.v1.BuildingService.AssignRacksToBuilding */ - assignRackToBuilding: { + assignRacksToBuilding: { methodKind: "unary"; - input: typeof AssignRackToBuildingRequestSchema; - output: typeof AssignRackToBuildingResponseSchema; + input: typeof AssignRacksToBuildingRequestSchema; + output: typeof AssignRacksToBuildingResponseSchema; }; /** * GetBuildingStats returns server-rolled telemetry + miner-state diff --git a/client/src/protoFleet/api/generated/collection/v1/collection_pb.ts b/client/src/protoFleet/api/generated/collection/v1/collection_pb.ts index 4050e9176..3b654bc29 100644 --- a/client/src/protoFleet/api/generated/collection/v1/collection_pb.ts +++ b/client/src/protoFleet/api/generated/collection/v1/collection_pb.ts @@ -21,7 +21,7 @@ import type { Message } from "@bufbuild/protobuf"; export const file_collection_v1_collection: GenFile = /*@__PURE__*/ fileDesc( - "Ch5jb2xsZWN0aW9uL3YxL2NvbGxlY3Rpb24ucHJvdG8SDWNvbGxlY3Rpb24udjEi0wIKEERldmljZUNvbGxlY3Rpb24SCgoCaWQYASABKAMSKwoEdHlwZRgCIAEoDjIdLmNvbGxlY3Rpb24udjEuQ29sbGVjdGlvblR5cGUSDQoFbGFiZWwYAyABKAkSEwoLZGVzY3JpcHRpb24YBCABKAkSFAoMZGV2aWNlX2NvdW50GAUgASgFEi4KCmNyZWF0ZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiwKCXJhY2tfaW5mbxgIIAEoCzIXLmNvbGxlY3Rpb24udjEuUmFja0luZm9IABIuCgpncm91cF9pbmZvGAkgASgLMhguY29sbGVjdGlvbi52MS5Hcm91cEluZm9IAEIOCgx0eXBlX2RldGFpbHMiiAIKCFJhY2tJbmZvEhUKBHJvd3MYASABKAVCB7pIBBoCIAASGAoHY29sdW1ucxgCIAEoBUIHukgEGgIgABIVCgR6b25lGAMgASgJQge6SARyAhhkEjIKC29yZGVyX2luZGV4GAQgASgOMh0uY29sbGVjdGlvbi52MS5SYWNrT3JkZXJJbmRleBI0Cgxjb29saW5nX3R5cGUYBSABKA4yHi5jb2xsZWN0aW9uLnYxLlJhY2tDb29saW5nVHlwZRIUCgdzaXRlX2lkGAYgASgDSACIAQESGAoLYnVpbGRpbmdfaWQYByABKANIAYgBAUIKCghfc2l0ZV9pZEIOCgxfYnVpbGRpbmdfaWQiCwoJR3JvdXBJbmZvIp8BChBDb2xsZWN0aW9uTWVtYmVyEhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEiwKCGFkZGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgRyYWNrGAMgASgLMiAuY29sbGVjdGlvbi52MS5SYWNrTWVtYmVyRGV0YWlsc0gAQhAKDm1lbWJlcl9kZXRhaWxzIksKEVJhY2tNZW1iZXJEZXRhaWxzEjYKDXNsb3RfcG9zaXRpb24YASABKAsyHy5jb2xsZWN0aW9uLnYxLlJhY2tTbG90UG9zaXRpb24iQQoQUmFja1Nsb3RQb3NpdGlvbhIUCgNyb3cYASABKAVCB7pIBBoCKAASFwoGY29sdW1uGAIgASgFQge6SAQaAigAIskCChdDcmVhdGVDb2xsZWN0aW9uUmVxdWVzdBI3CgR0eXBlGAEgASgOMh0uY29sbGVjdGlvbi52MS5Db2xsZWN0aW9uVHlwZUIKukgHggEEEAEgABIbCgVsYWJlbBgCIAEoCUIMukgJyAEBcgQQARhkEh0KC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxj0AxIsCglyYWNrX2luZm8YBCABKAsyFy5jb2xsZWN0aW9uLnYxLlJhY2tJbmZvSAASLgoKZ3JvdXBfaW5mbxgFIAEoCzIYLmNvbGxlY3Rpb24udjEuR3JvdXBJbmZvSAASNwoPZGV2aWNlX3NlbGVjdG9yGAYgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9ySAGIAQFCDgoMdHlwZV9kZXRhaWxzQhIKEF9kZXZpY2Vfc2VsZWN0b3IiZAoYQ3JlYXRlQ29sbGVjdGlvblJlc3BvbnNlEjMKCmNvbGxlY3Rpb24YASABKAsyHy5jb2xsZWN0aW9uLnYxLkRldmljZUNvbGxlY3Rpb24SEwoLYWRkZWRfY291bnQYAiABKAUiNgoUR2V0Q29sbGVjdGlvblJlcXVlc3QSHgoNY29sbGVjdGlvbl9pZBgBIAEoA0IHukgEIgIgACJMChVHZXRDb2xsZWN0aW9uUmVzcG9uc2USMwoKY29sbGVjdGlvbhgBIAEoCzIfLmNvbGxlY3Rpb24udjEuRGV2aWNlQ29sbGVjdGlvbiK+AgoXVXBkYXRlQ29sbGVjdGlvblJlcXVlc3QSHgoNY29sbGVjdGlvbl9pZBgBIAEoA0IHukgEIgIgABIgCgVsYWJlbBgCIAEoCUIMukgJ2AEBcgQQARhkSAGIAQESJQoLZGVzY3JpcHRpb24YAyABKAlCC7pICNgBAXIDGPQDSAKIAQESLAoJcmFja19pbmZvGAQgASgLMhcuY29sbGVjdGlvbi52MS5SYWNrSW5mb0gAEi4KCmdyb3VwX2luZm8YBSABKAsyGC5jb2xsZWN0aW9uLnYxLkdyb3VwSW5mb0gAEjIKD2RldmljZV9zZWxlY3RvchgGIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIOCgx0eXBlX2RldGFpbHNCCAoGX2xhYmVsQg4KDF9kZXNjcmlwdGlvbiJPChhVcGRhdGVDb2xsZWN0aW9uUmVzcG9uc2USMwoKY29sbGVjdGlvbhgBIAEoCzIfLmNvbGxlY3Rpb24udjEuRGV2aWNlQ29sbGVjdGlvbiI5ChdEZWxldGVDb2xsZWN0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAIhoKGERlbGV0ZUNvbGxlY3Rpb25SZXNwb25zZSLsAQoWTGlzdENvbGxlY3Rpb25zUmVxdWVzdBI1CgR0eXBlGAEgASgOMh0uY29sbGVjdGlvbi52MS5Db2xsZWN0aW9uVHlwZUIIukgFggECEAESGgoJcGFnZV9zaXplGAIgASgFQge6SAQaAigAEhIKCnBhZ2VfdG9rZW4YAyABKAkSIwoEc29ydBgEIAEoCzIVLmNvbW1vbi52MS5Tb3J0Q29uZmlnEjcKFWVycm9yX2NvbXBvbmVudF90eXBlcxgFIAMoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlEg0KBXpvbmVzGAYgAygJIn0KF0xpc3RDb2xsZWN0aW9uc1Jlc3BvbnNlEjQKC2NvbGxlY3Rpb25zGAEgAygLMh8uY29sbGVjdGlvbi52MS5EZXZpY2VDb2xsZWN0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoBSJ7Ch1BZGREZXZpY2VzVG9Db2xsZWN0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAEjoKD2RldmljZV9zZWxlY3RvchgCIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBImsKHkFkZERldmljZXNUb0NvbGxlY3Rpb25SZXNwb25zZRIVCg1jb2xsZWN0aW9uX2lkGAEgASgDEhMKC2FkZGVkX2NvdW50GAIgASgFEh0KFXNpdGVfcmVhc3NpZ25lZF9jb3VudBgDIAEoBSKAAQoiUmVtb3ZlRGV2aWNlc0Zyb21Db2xsZWN0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAEjoKD2RldmljZV9zZWxlY3RvchgCIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBIjwKI1JlbW92ZURldmljZXNGcm9tQ29sbGVjdGlvblJlc3BvbnNlEhUKDXJlbW92ZWRfY291bnQYASABKAUibgocTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAEhoKCXBhZ2Vfc2l6ZRgCIAEoBUIHukgEGgIoABISCgpwYWdlX3Rva2VuGAMgASgJImoKHUxpc3RDb2xsZWN0aW9uTWVtYmVyc1Jlc3BvbnNlEjAKB21lbWJlcnMYASADKAsyHy5jb2xsZWN0aW9uLnYxLkNvbGxlY3Rpb25NZW1iZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIm4KG0dldERldmljZUNvbGxlY3Rpb25zUmVxdWVzdBIiChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIHukgEcgIQARIrCgR0eXBlGAIgASgOMh0uY29sbGVjdGlvbi52MS5Db2xsZWN0aW9uVHlwZSJUChxHZXREZXZpY2VDb2xsZWN0aW9uc1Jlc3BvbnNlEjQKC2NvbGxlY3Rpb25zGAEgAygLMh8uY29sbGVjdGlvbi52MS5EZXZpY2VDb2xsZWN0aW9uIpsBChpTZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABEjkKCHBvc2l0aW9uGAMgASgLMh8uY29sbGVjdGlvbi52MS5SYWNrU2xvdFBvc2l0aW9uQga6SAPIAQEiWwobU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEhUKDWNvbGxlY3Rpb25faWQYASABKAMSJQoEc2xvdBgCIAEoCzIXLmNvbGxlY3Rpb24udjEuUmFja1Nsb3QiYgocQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABIh8KHUNsZWFyUmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlIjUKE0dldFJhY2tTbG90c1JlcXVlc3QSHgoNY29sbGVjdGlvbl9pZBgBIAEoA0IHukgEIgIgACJYCghSYWNrU2xvdBIZChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCRIxCghwb3NpdGlvbhgCIAEoCzIfLmNvbGxlY3Rpb24udjEuUmFja1Nsb3RQb3NpdGlvbiI+ChRHZXRSYWNrU2xvdHNSZXNwb25zZRImCgVzbG90cxgBIAMoCzIXLmNvbGxlY3Rpb24udjEuUmFja1Nsb3Qi7gQKD0NvbGxlY3Rpb25TdGF0cxIVCg1jb2xsZWN0aW9uX2lkGAEgASgDEhQKDGRldmljZV9jb3VudBgCIAEoBRIXCg9yZXBvcnRpbmdfY291bnQYAyABKAUSGgoSdG90YWxfaGFzaHJhdGVfdGhzGAQgASgBEhoKEmF2Z19lZmZpY2llbmN5X2p0aBgFIAEoARIWCg50b3RhbF9wb3dlcl9rdxgGIAEoARIZChFtaW5fdGVtcGVyYXR1cmVfYxgHIAEoARIZChFtYXhfdGVtcGVyYXR1cmVfYxgIIAEoARIVCg1oYXNoaW5nX2NvdW50GAkgASgFEhQKDGJyb2tlbl9jb3VudBgKIAEoBRIVCg1vZmZsaW5lX2NvdW50GAsgASgFEhYKDnNsZWVwaW5nX2NvdW50GAwgASgFEiAKGGhhc2hyYXRlX3JlcG9ydGluZ19jb3VudBgNIAEoBRIiChplZmZpY2llbmN5X3JlcG9ydGluZ19jb3VudBgOIAEoBRIdChVwb3dlcl9yZXBvcnRpbmdfY291bnQYDyABKAUSIwobdGVtcGVyYXR1cmVfcmVwb3J0aW5nX2NvdW50GBAgASgFEiEKGWNvbnRyb2xfYm9hcmRfaXNzdWVfY291bnQYESABKAUSFwoPZmFuX2lzc3VlX2NvdW50GBIgASgFEh4KFmhhc2hfYm9hcmRfaXNzdWVfY291bnQYEyABKAUSFwoPcHN1X2lzc3VlX2NvdW50GBQgASgFEjQKDXNsb3Rfc3RhdHVzZXMYFSADKAsyHS5jb2xsZWN0aW9uLnYxLlJhY2tTbG90U3RhdHVzIjMKGUdldENvbGxlY3Rpb25TdGF0c1JlcXVlc3QSFgoOY29sbGVjdGlvbl9pZHMYASADKAMiSwoaR2V0Q29sbGVjdGlvblN0YXRzUmVzcG9uc2USLQoFc3RhdHMYASADKAsyHi5jb2xsZWN0aW9uLnYxLkNvbGxlY3Rpb25TdGF0cyJeCg5SYWNrU2xvdFN0YXR1cxILCgNyb3cYASABKAUSDgoGY29sdW1uGAIgASgFEi8KBnN0YXR1cxgDIAEoDjIfLmNvbGxlY3Rpb24udjEuU2xvdERldmljZVN0YXR1cyIWChRMaXN0UmFja1pvbmVzUmVxdWVzdCImChVMaXN0UmFja1pvbmVzUmVzcG9uc2USDQoFem9uZXMYASADKAkiFgoUTGlzdFJhY2tUeXBlc1JlcXVlc3QiPQoIUmFja1R5cGUSDAoEcm93cxgBIAEoBRIPCgdjb2x1bW5zGAIgASgFEhIKCnJhY2tfY291bnQYAyABKAUiRAoVTGlzdFJhY2tUeXBlc1Jlc3BvbnNlEisKCnJhY2tfdHlwZXMYASADKAsyFy5jb2xsZWN0aW9uLnYxLlJhY2tUeXBlIosCCg9TYXZlUmFja1JlcXVlc3QSJgoNY29sbGVjdGlvbl9pZBgBIAEoA0IKukgH2AEBIgIgAEgAiAEBEhsKBWxhYmVsGAIgASgJQgy6SAnIAQFyBBABGGQSMgoJcmFja19pbmZvGAMgASgLMhcuY29sbGVjdGlvbi52MS5SYWNrSW5mb0IGukgDyAEBEjoKD2RldmljZV9zZWxlY3RvchgEIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBEjEKEHNsb3RfYXNzaWdubWVudHMYBSADKAsyFy5jb2xsZWN0aW9uLnYxLlJhY2tTbG90QhAKDl9jb2xsZWN0aW9uX2lkIn4KEFNhdmVSYWNrUmVzcG9uc2USMwoKY29sbGVjdGlvbhgBIAEoCzIfLmNvbGxlY3Rpb24udjEuRGV2aWNlQ29sbGVjdGlvbhIWCg5hc3NpZ25lZF9jb3VudBgCIAEoBRIdChVzaXRlX3JlYXNzaWduZWRfY291bnQYAyABKAUqZgoOQ29sbGVjdGlvblR5cGUSHwobQ09MTEVDVElPTl9UWVBFX1VOU1BFQ0lGSUVEEAASGQoVQ09MTEVDVElPTl9UWVBFX0dST1VQEAESGAoUQ09MTEVDVElPTl9UWVBFX1JBQ0sQAiq2AQoOUmFja09yZGVySW5kZXgSIAocUkFDS19PUkRFUl9JTkRFWF9VTlNQRUNJRklFRBAAEiAKHFJBQ0tfT1JERVJfSU5ERVhfQk9UVE9NX0xFRlQQARIdChlSQUNLX09SREVSX0lOREVYX1RPUF9MRUZUEAISIQodUkFDS19PUkRFUl9JTkRFWF9CT1RUT01fUklHSFQQAxIeChpSQUNLX09SREVSX0lOREVYX1RPUF9SSUdIVBAEKnAKD1JhY2tDb29saW5nVHlwZRIhCh1SQUNLX0NPT0xJTkdfVFlQRV9VTlNQRUNJRklFRBAAEhkKFVJBQ0tfQ09PTElOR19UWVBFX0FJUhABEh8KG1JBQ0tfQ09PTElOR19UWVBFX0lNTUVSU0lPThACKt0BChBTbG90RGV2aWNlU3RhdHVzEiIKHlNMT1RfREVWSUNFX1NUQVRVU19VTlNQRUNJRklFRBAAEhwKGFNMT1RfREVWSUNFX1NUQVRVU19FTVBUWRABEh4KGlNMT1RfREVWSUNFX1NUQVRVU19IRUFMVEhZEAISJgoiU0xPVF9ERVZJQ0VfU1RBVFVTX05FRURTX0FUVEVOVElPThADEh4KGlNMT1RfREVWSUNFX1NUQVRVU19PRkZMSU5FEAQSHwobU0xPVF9ERVZJQ0VfU1RBVFVTX1NMRUVQSU5HEAUylA0KF0RldmljZUNvbGxlY3Rpb25TZXJ2aWNlEmMKEENyZWF0ZUNvbGxlY3Rpb24SJi5jb2xsZWN0aW9uLnYxLkNyZWF0ZUNvbGxlY3Rpb25SZXF1ZXN0GicuY29sbGVjdGlvbi52MS5DcmVhdGVDb2xsZWN0aW9uUmVzcG9uc2USWgoNR2V0Q29sbGVjdGlvbhIjLmNvbGxlY3Rpb24udjEuR2V0Q29sbGVjdGlvblJlcXVlc3QaJC5jb2xsZWN0aW9uLnYxLkdldENvbGxlY3Rpb25SZXNwb25zZRJjChBVcGRhdGVDb2xsZWN0aW9uEiYuY29sbGVjdGlvbi52MS5VcGRhdGVDb2xsZWN0aW9uUmVxdWVzdBonLmNvbGxlY3Rpb24udjEuVXBkYXRlQ29sbGVjdGlvblJlc3BvbnNlEmMKEERlbGV0ZUNvbGxlY3Rpb24SJi5jb2xsZWN0aW9uLnYxLkRlbGV0ZUNvbGxlY3Rpb25SZXF1ZXN0GicuY29sbGVjdGlvbi52MS5EZWxldGVDb2xsZWN0aW9uUmVzcG9uc2USYAoPTGlzdENvbGxlY3Rpb25zEiUuY29sbGVjdGlvbi52MS5MaXN0Q29sbGVjdGlvbnNSZXF1ZXN0GiYuY29sbGVjdGlvbi52MS5MaXN0Q29sbGVjdGlvbnNSZXNwb25zZRJ1ChZBZGREZXZpY2VzVG9Db2xsZWN0aW9uEiwuY29sbGVjdGlvbi52MS5BZGREZXZpY2VzVG9Db2xsZWN0aW9uUmVxdWVzdBotLmNvbGxlY3Rpb24udjEuQWRkRGV2aWNlc1RvQ29sbGVjdGlvblJlc3BvbnNlEoQBChtSZW1vdmVEZXZpY2VzRnJvbUNvbGxlY3Rpb24SMS5jb2xsZWN0aW9uLnYxLlJlbW92ZURldmljZXNGcm9tQ29sbGVjdGlvblJlcXVlc3QaMi5jb2xsZWN0aW9uLnYxLlJlbW92ZURldmljZXNGcm9tQ29sbGVjdGlvblJlc3BvbnNlEnIKFUxpc3RDb2xsZWN0aW9uTWVtYmVycxIrLmNvbGxlY3Rpb24udjEuTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVxdWVzdBosLmNvbGxlY3Rpb24udjEuTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVzcG9uc2USbwoUR2V0RGV2aWNlQ29sbGVjdGlvbnMSKi5jb2xsZWN0aW9uLnYxLkdldERldmljZUNvbGxlY3Rpb25zUmVxdWVzdBorLmNvbGxlY3Rpb24udjEuR2V0RGV2aWNlQ29sbGVjdGlvbnNSZXNwb25zZRJsChNTZXRSYWNrU2xvdFBvc2l0aW9uEikuY29sbGVjdGlvbi52MS5TZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBoqLmNvbGxlY3Rpb24udjEuU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEnIKFUNsZWFyUmFja1Nsb3RQb3NpdGlvbhIrLmNvbGxlY3Rpb24udjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBosLmNvbGxlY3Rpb24udjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2USVwoMR2V0UmFja1Nsb3RzEiIuY29sbGVjdGlvbi52MS5HZXRSYWNrU2xvdHNSZXF1ZXN0GiMuY29sbGVjdGlvbi52MS5HZXRSYWNrU2xvdHNSZXNwb25zZRJpChJHZXRDb2xsZWN0aW9uU3RhdHMSKC5jb2xsZWN0aW9uLnYxLkdldENvbGxlY3Rpb25TdGF0c1JlcXVlc3QaKS5jb2xsZWN0aW9uLnYxLkdldENvbGxlY3Rpb25TdGF0c1Jlc3BvbnNlEloKDUxpc3RSYWNrWm9uZXMSIy5jb2xsZWN0aW9uLnYxLkxpc3RSYWNrWm9uZXNSZXF1ZXN0GiQuY29sbGVjdGlvbi52MS5MaXN0UmFja1pvbmVzUmVzcG9uc2USWgoNTGlzdFJhY2tUeXBlcxIjLmNvbGxlY3Rpb24udjEuTGlzdFJhY2tUeXBlc1JlcXVlc3QaJC5jb2xsZWN0aW9uLnYxLkxpc3RSYWNrVHlwZXNSZXNwb25zZRJLCghTYXZlUmFjaxIeLmNvbGxlY3Rpb24udjEuU2F2ZVJhY2tSZXF1ZXN0Gh8uY29sbGVjdGlvbi52MS5TYXZlUmFja1Jlc3BvbnNlQsgBChFjb20uY29sbGVjdGlvbi52MUIPQ29sbGVjdGlvblByb3RvUAFaTWdpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL2NvbGxlY3Rpb24vdjE7Y29sbGVjdGlvbnYxogIDQ1hYqgINQ29sbGVjdGlvbi5WMcoCDUNvbGxlY3Rpb25cVjHiAhlDb2xsZWN0aW9uXFYxXEdQQk1ldGFkYXRh6gIOQ29sbGVjdGlvbjo6VjFiBnByb3RvMw", + "Ch5jb2xsZWN0aW9uL3YxL2NvbGxlY3Rpb24ucHJvdG8SDWNvbGxlY3Rpb24udjEi0wIKEERldmljZUNvbGxlY3Rpb24SCgoCaWQYASABKAMSKwoEdHlwZRgCIAEoDjIdLmNvbGxlY3Rpb24udjEuQ29sbGVjdGlvblR5cGUSDQoFbGFiZWwYAyABKAkSEwoLZGVzY3JpcHRpb24YBCABKAkSFAoMZGV2aWNlX2NvdW50GAUgASgFEi4KCmNyZWF0ZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiwKCXJhY2tfaW5mbxgIIAEoCzIXLmNvbGxlY3Rpb24udjEuUmFja0luZm9IABIuCgpncm91cF9pbmZvGAkgASgLMhguY29sbGVjdGlvbi52MS5Hcm91cEluZm9IAEIOCgx0eXBlX2RldGFpbHMiiAIKCFJhY2tJbmZvEhUKBHJvd3MYASABKAVCB7pIBBoCIAASGAoHY29sdW1ucxgCIAEoBUIHukgEGgIgABIVCgR6b25lGAMgASgJQge6SARyAhhkEjIKC29yZGVyX2luZGV4GAQgASgOMh0uY29sbGVjdGlvbi52MS5SYWNrT3JkZXJJbmRleBI0Cgxjb29saW5nX3R5cGUYBSABKA4yHi5jb2xsZWN0aW9uLnYxLlJhY2tDb29saW5nVHlwZRIUCgdzaXRlX2lkGAYgASgDSACIAQESGAoLYnVpbGRpbmdfaWQYByABKANIAYgBAUIKCghfc2l0ZV9pZEIOCgxfYnVpbGRpbmdfaWQiCwoJR3JvdXBJbmZvIp8BChBDb2xsZWN0aW9uTWVtYmVyEhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEiwKCGFkZGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgRyYWNrGAMgASgLMiAuY29sbGVjdGlvbi52MS5SYWNrTWVtYmVyRGV0YWlsc0gAQhAKDm1lbWJlcl9kZXRhaWxzIksKEVJhY2tNZW1iZXJEZXRhaWxzEjYKDXNsb3RfcG9zaXRpb24YASABKAsyHy5jb2xsZWN0aW9uLnYxLlJhY2tTbG90UG9zaXRpb24iQQoQUmFja1Nsb3RQb3NpdGlvbhIUCgNyb3cYASABKAVCB7pIBBoCKAASFwoGY29sdW1uGAIgASgFQge6SAQaAigAIskCChdDcmVhdGVDb2xsZWN0aW9uUmVxdWVzdBI3CgR0eXBlGAEgASgOMh0uY29sbGVjdGlvbi52MS5Db2xsZWN0aW9uVHlwZUIKukgHggEEEAEgABIbCgVsYWJlbBgCIAEoCUIMukgJyAEBcgQQARhkEh0KC2Rlc2NyaXB0aW9uGAMgASgJQgi6SAVyAxj0AxIsCglyYWNrX2luZm8YBCABKAsyFy5jb2xsZWN0aW9uLnYxLlJhY2tJbmZvSAASLgoKZ3JvdXBfaW5mbxgFIAEoCzIYLmNvbGxlY3Rpb24udjEuR3JvdXBJbmZvSAASNwoPZGV2aWNlX3NlbGVjdG9yGAYgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9ySAGIAQFCDgoMdHlwZV9kZXRhaWxzQhIKEF9kZXZpY2Vfc2VsZWN0b3IiZAoYQ3JlYXRlQ29sbGVjdGlvblJlc3BvbnNlEjMKCmNvbGxlY3Rpb24YASABKAsyHy5jb2xsZWN0aW9uLnYxLkRldmljZUNvbGxlY3Rpb24SEwoLYWRkZWRfY291bnQYAiABKAUiNgoUR2V0Q29sbGVjdGlvblJlcXVlc3QSHgoNY29sbGVjdGlvbl9pZBgBIAEoA0IHukgEIgIgACJMChVHZXRDb2xsZWN0aW9uUmVzcG9uc2USMwoKY29sbGVjdGlvbhgBIAEoCzIfLmNvbGxlY3Rpb24udjEuRGV2aWNlQ29sbGVjdGlvbiK+AgoXVXBkYXRlQ29sbGVjdGlvblJlcXVlc3QSHgoNY29sbGVjdGlvbl9pZBgBIAEoA0IHukgEIgIgABIgCgVsYWJlbBgCIAEoCUIMukgJ2AEBcgQQARhkSAGIAQESJQoLZGVzY3JpcHRpb24YAyABKAlCC7pICNgBAXIDGPQDSAKIAQESLAoJcmFja19pbmZvGAQgASgLMhcuY29sbGVjdGlvbi52MS5SYWNrSW5mb0gAEi4KCmdyb3VwX2luZm8YBSABKAsyGC5jb2xsZWN0aW9uLnYxLkdyb3VwSW5mb0gAEjIKD2RldmljZV9zZWxlY3RvchgGIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIOCgx0eXBlX2RldGFpbHNCCAoGX2xhYmVsQg4KDF9kZXNjcmlwdGlvbiJPChhVcGRhdGVDb2xsZWN0aW9uUmVzcG9uc2USMwoKY29sbGVjdGlvbhgBIAEoCzIfLmNvbGxlY3Rpb24udjEuRGV2aWNlQ29sbGVjdGlvbiI5ChdEZWxldGVDb2xsZWN0aW9uUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAIhoKGERlbGV0ZUNvbGxlY3Rpb25SZXNwb25zZSLsAQoWTGlzdENvbGxlY3Rpb25zUmVxdWVzdBI1CgR0eXBlGAEgASgOMh0uY29sbGVjdGlvbi52MS5Db2xsZWN0aW9uVHlwZUIIukgFggECEAESGgoJcGFnZV9zaXplGAIgASgFQge6SAQaAigAEhIKCnBhZ2VfdG9rZW4YAyABKAkSIwoEc29ydBgEIAEoCzIVLmNvbW1vbi52MS5Tb3J0Q29uZmlnEjcKFWVycm9yX2NvbXBvbmVudF90eXBlcxgFIAMoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlEg0KBXpvbmVzGAYgAygJIn0KF0xpc3RDb2xsZWN0aW9uc1Jlc3BvbnNlEjQKC2NvbGxlY3Rpb25zGAEgAygLMh8uY29sbGVjdGlvbi52MS5EZXZpY2VDb2xsZWN0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoBSJuChxMaXN0Q29sbGVjdGlvbk1lbWJlcnNSZXF1ZXN0Eh4KDWNvbGxlY3Rpb25faWQYASABKANCB7pIBCICIAASGgoJcGFnZV9zaXplGAIgASgFQge6SAQaAigAEhIKCnBhZ2VfdG9rZW4YAyABKAkiagodTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVzcG9uc2USMAoHbWVtYmVycxgBIAMoCzIfLmNvbGxlY3Rpb24udjEuQ29sbGVjdGlvbk1lbWJlchIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkibgobR2V0RGV2aWNlQ29sbGVjdGlvbnNSZXF1ZXN0EiIKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQge6SARyAhABEisKBHR5cGUYAiABKA4yHS5jb2xsZWN0aW9uLnYxLkNvbGxlY3Rpb25UeXBlIlQKHEdldERldmljZUNvbGxlY3Rpb25zUmVzcG9uc2USNAoLY29sbGVjdGlvbnMYASADKAsyHy5jb2xsZWN0aW9uLnYxLkRldmljZUNvbGxlY3Rpb24imwEKGlNldFJhY2tTbG90UG9zaXRpb25SZXF1ZXN0Eh4KDWNvbGxlY3Rpb25faWQYASABKANCB7pIBCICIAASIgoRZGV2aWNlX2lkZW50aWZpZXIYAiABKAlCB7pIBHICEAESOQoIcG9zaXRpb24YAyABKAsyHy5jb2xsZWN0aW9uLnYxLlJhY2tTbG90UG9zaXRpb25CBrpIA8gBASJbChtTZXRSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2USFQoNY29sbGVjdGlvbl9pZBgBIAEoAxIlCgRzbG90GAIgASgLMhcuY29sbGVjdGlvbi52MS5SYWNrU2xvdCJiChxDbGVhclJhY2tTbG90UG9zaXRpb25SZXF1ZXN0Eh4KDWNvbGxlY3Rpb25faWQYASABKANCB7pIBCICIAASIgoRZGV2aWNlX2lkZW50aWZpZXIYAiABKAlCB7pIBHICEAEiHwodQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2UiNQoTR2V0UmFja1Nsb3RzUmVxdWVzdBIeCg1jb2xsZWN0aW9uX2lkGAEgASgDQge6SAQiAiAAIlgKCFJhY2tTbG90EhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEjEKCHBvc2l0aW9uGAIgASgLMh8uY29sbGVjdGlvbi52MS5SYWNrU2xvdFBvc2l0aW9uIj4KFEdldFJhY2tTbG90c1Jlc3BvbnNlEiYKBXNsb3RzGAEgAygLMhcuY29sbGVjdGlvbi52MS5SYWNrU2xvdCLuBAoPQ29sbGVjdGlvblN0YXRzEhUKDWNvbGxlY3Rpb25faWQYASABKAMSFAoMZGV2aWNlX2NvdW50GAIgASgFEhcKD3JlcG9ydGluZ19jb3VudBgDIAEoBRIaChJ0b3RhbF9oYXNocmF0ZV90aHMYBCABKAESGgoSYXZnX2VmZmljaWVuY3lfanRoGAUgASgBEhYKDnRvdGFsX3Bvd2VyX2t3GAYgASgBEhkKEW1pbl90ZW1wZXJhdHVyZV9jGAcgASgBEhkKEW1heF90ZW1wZXJhdHVyZV9jGAggASgBEhUKDWhhc2hpbmdfY291bnQYCSABKAUSFAoMYnJva2VuX2NvdW50GAogASgFEhUKDW9mZmxpbmVfY291bnQYCyABKAUSFgoOc2xlZXBpbmdfY291bnQYDCABKAUSIAoYaGFzaHJhdGVfcmVwb3J0aW5nX2NvdW50GA0gASgFEiIKGmVmZmljaWVuY3lfcmVwb3J0aW5nX2NvdW50GA4gASgFEh0KFXBvd2VyX3JlcG9ydGluZ19jb3VudBgPIAEoBRIjCht0ZW1wZXJhdHVyZV9yZXBvcnRpbmdfY291bnQYECABKAUSIQoZY29udHJvbF9ib2FyZF9pc3N1ZV9jb3VudBgRIAEoBRIXCg9mYW5faXNzdWVfY291bnQYEiABKAUSHgoWaGFzaF9ib2FyZF9pc3N1ZV9jb3VudBgTIAEoBRIXCg9wc3VfaXNzdWVfY291bnQYFCABKAUSNAoNc2xvdF9zdGF0dXNlcxgVIAMoCzIdLmNvbGxlY3Rpb24udjEuUmFja1Nsb3RTdGF0dXMiMwoZR2V0Q29sbGVjdGlvblN0YXRzUmVxdWVzdBIWCg5jb2xsZWN0aW9uX2lkcxgBIAMoAyJLChpHZXRDb2xsZWN0aW9uU3RhdHNSZXNwb25zZRItCgVzdGF0cxgBIAMoCzIeLmNvbGxlY3Rpb24udjEuQ29sbGVjdGlvblN0YXRzIl4KDlJhY2tTbG90U3RhdHVzEgsKA3JvdxgBIAEoBRIOCgZjb2x1bW4YAiABKAUSLwoGc3RhdHVzGAMgASgOMh8uY29sbGVjdGlvbi52MS5TbG90RGV2aWNlU3RhdHVzIhYKFExpc3RSYWNrWm9uZXNSZXF1ZXN0IiYKFUxpc3RSYWNrWm9uZXNSZXNwb25zZRINCgV6b25lcxgBIAMoCSIWChRMaXN0UmFja1R5cGVzUmVxdWVzdCI9CghSYWNrVHlwZRIMCgRyb3dzGAEgASgFEg8KB2NvbHVtbnMYAiABKAUSEgoKcmFja19jb3VudBgDIAEoBSJEChVMaXN0UmFja1R5cGVzUmVzcG9uc2USKwoKcmFja190eXBlcxgBIAMoCzIXLmNvbGxlY3Rpb24udjEuUmFja1R5cGUiiwIKD1NhdmVSYWNrUmVxdWVzdBImCg1jb2xsZWN0aW9uX2lkGAEgASgDQgq6SAfYAQEiAiAASACIAQESGwoFbGFiZWwYAiABKAlCDLpICcgBAXIEEAEYZBIyCglyYWNrX2luZm8YAyABKAsyFy5jb2xsZWN0aW9uLnYxLlJhY2tJbmZvQga6SAPIAQESOgoPZGV2aWNlX3NlbGVjdG9yGAQgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQESMQoQc2xvdF9hc3NpZ25tZW50cxgFIAMoCzIXLmNvbGxlY3Rpb24udjEuUmFja1Nsb3RCEAoOX2NvbGxlY3Rpb25faWQifgoQU2F2ZVJhY2tSZXNwb25zZRIzCgpjb2xsZWN0aW9uGAEgASgLMh8uY29sbGVjdGlvbi52MS5EZXZpY2VDb2xsZWN0aW9uEhYKDmFzc2lnbmVkX2NvdW50GAIgASgFEh0KFXNpdGVfcmVhc3NpZ25lZF9jb3VudBgDIAEoBSpmCg5Db2xsZWN0aW9uVHlwZRIfChtDT0xMRUNUSU9OX1RZUEVfVU5TUEVDSUZJRUQQABIZChVDT0xMRUNUSU9OX1RZUEVfR1JPVVAQARIYChRDT0xMRUNUSU9OX1RZUEVfUkFDSxACKrYBCg5SYWNrT3JkZXJJbmRleBIgChxSQUNLX09SREVSX0lOREVYX1VOU1BFQ0lGSUVEEAASIAocUkFDS19PUkRFUl9JTkRFWF9CT1RUT01fTEVGVBABEh0KGVJBQ0tfT1JERVJfSU5ERVhfVE9QX0xFRlQQAhIhCh1SQUNLX09SREVSX0lOREVYX0JPVFRPTV9SSUdIVBADEh4KGlJBQ0tfT1JERVJfSU5ERVhfVE9QX1JJR0hUEAQqcAoPUmFja0Nvb2xpbmdUeXBlEiEKHVJBQ0tfQ09PTElOR19UWVBFX1VOU1BFQ0lGSUVEEAASGQoVUkFDS19DT09MSU5HX1RZUEVfQUlSEAESHwobUkFDS19DT09MSU5HX1RZUEVfSU1NRVJTSU9OEAIq3QEKEFNsb3REZXZpY2VTdGF0dXMSIgoeU0xPVF9ERVZJQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYU0xPVF9ERVZJQ0VfU1RBVFVTX0VNUFRZEAESHgoaU0xPVF9ERVZJQ0VfU1RBVFVTX0hFQUxUSFkQAhImCiJTTE9UX0RFVklDRV9TVEFUVVNfTkVFRFNfQVRURU5USU9OEAMSHgoaU0xPVF9ERVZJQ0VfU1RBVFVTX09GRkxJTkUQBBIfChtTTE9UX0RFVklDRV9TVEFUVVNfU0xFRVBJTkcQBTKWCwoXRGV2aWNlQ29sbGVjdGlvblNlcnZpY2USYwoQQ3JlYXRlQ29sbGVjdGlvbhImLmNvbGxlY3Rpb24udjEuQ3JlYXRlQ29sbGVjdGlvblJlcXVlc3QaJy5jb2xsZWN0aW9uLnYxLkNyZWF0ZUNvbGxlY3Rpb25SZXNwb25zZRJaCg1HZXRDb2xsZWN0aW9uEiMuY29sbGVjdGlvbi52MS5HZXRDb2xsZWN0aW9uUmVxdWVzdBokLmNvbGxlY3Rpb24udjEuR2V0Q29sbGVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbGxlY3Rpb24SJi5jb2xsZWN0aW9uLnYxLlVwZGF0ZUNvbGxlY3Rpb25SZXF1ZXN0GicuY29sbGVjdGlvbi52MS5VcGRhdGVDb2xsZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29sbGVjdGlvbhImLmNvbGxlY3Rpb24udjEuRGVsZXRlQ29sbGVjdGlvblJlcXVlc3QaJy5jb2xsZWN0aW9uLnYxLkRlbGV0ZUNvbGxlY3Rpb25SZXNwb25zZRJgCg9MaXN0Q29sbGVjdGlvbnMSJS5jb2xsZWN0aW9uLnYxLkxpc3RDb2xsZWN0aW9uc1JlcXVlc3QaJi5jb2xsZWN0aW9uLnYxLkxpc3RDb2xsZWN0aW9uc1Jlc3BvbnNlEnIKFUxpc3RDb2xsZWN0aW9uTWVtYmVycxIrLmNvbGxlY3Rpb24udjEuTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVxdWVzdBosLmNvbGxlY3Rpb24udjEuTGlzdENvbGxlY3Rpb25NZW1iZXJzUmVzcG9uc2USbwoUR2V0RGV2aWNlQ29sbGVjdGlvbnMSKi5jb2xsZWN0aW9uLnYxLkdldERldmljZUNvbGxlY3Rpb25zUmVxdWVzdBorLmNvbGxlY3Rpb24udjEuR2V0RGV2aWNlQ29sbGVjdGlvbnNSZXNwb25zZRJsChNTZXRSYWNrU2xvdFBvc2l0aW9uEikuY29sbGVjdGlvbi52MS5TZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBoqLmNvbGxlY3Rpb24udjEuU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEnIKFUNsZWFyUmFja1Nsb3RQb3NpdGlvbhIrLmNvbGxlY3Rpb24udjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBosLmNvbGxlY3Rpb24udjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2USVwoMR2V0UmFja1Nsb3RzEiIuY29sbGVjdGlvbi52MS5HZXRSYWNrU2xvdHNSZXF1ZXN0GiMuY29sbGVjdGlvbi52MS5HZXRSYWNrU2xvdHNSZXNwb25zZRJpChJHZXRDb2xsZWN0aW9uU3RhdHMSKC5jb2xsZWN0aW9uLnYxLkdldENvbGxlY3Rpb25TdGF0c1JlcXVlc3QaKS5jb2xsZWN0aW9uLnYxLkdldENvbGxlY3Rpb25TdGF0c1Jlc3BvbnNlEloKDUxpc3RSYWNrWm9uZXMSIy5jb2xsZWN0aW9uLnYxLkxpc3RSYWNrWm9uZXNSZXF1ZXN0GiQuY29sbGVjdGlvbi52MS5MaXN0UmFja1pvbmVzUmVzcG9uc2USWgoNTGlzdFJhY2tUeXBlcxIjLmNvbGxlY3Rpb24udjEuTGlzdFJhY2tUeXBlc1JlcXVlc3QaJC5jb2xsZWN0aW9uLnYxLkxpc3RSYWNrVHlwZXNSZXNwb25zZRJLCghTYXZlUmFjaxIeLmNvbGxlY3Rpb24udjEuU2F2ZVJhY2tSZXF1ZXN0Gh8uY29sbGVjdGlvbi52MS5TYXZlUmFja1Jlc3BvbnNlQsgBChFjb20uY29sbGVjdGlvbi52MUIPQ29sbGVjdGlvblByb3RvUAFaTWdpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL2NvbGxlY3Rpb24vdjE7Y29sbGVjdGlvbnYxogIDQ1hYqgINQ29sbGVjdGlvbi5WMcoCDUNvbGxlY3Rpb25cVjHiAhlDb2xsZWN0aW9uXFYxXEdQQk1ldGFkYXRh6gIOQ29sbGVjdGlvbjo6VjFiBnByb3RvMw", [ file_google_protobuf_timestamp, file_buf_validate_validate, @@ -660,127 +660,6 @@ export const ListCollectionsResponseSchema: GenMessage /*@__PURE__*/ messageDesc(file_collection_v1_collection, 15); -/** - * Request to add devices to a collection. - * - * When the target is a site-stamped rack, the server rewrites - * device.site_id to match the rack for every added device. The affected - * count is returned in site_reassigned_count. Groups are exempt. - * - * @generated from message collection.v1.AddDevicesToCollectionRequest - */ -export type AddDevicesToCollectionRequest = Message<"collection.v1.AddDevicesToCollectionRequest"> & { - /** - * ID of the collection to add devices to - * - * @generated from field: int64 collection_id = 1; - */ - collectionId: bigint; - - /** - * Devices to add: specific list or all paired devices - * - * @generated from field: common.v1.DeviceSelector device_selector = 2; - */ - deviceSelector?: DeviceSelector | undefined; -}; - -/** - * Describes the message collection.v1.AddDevicesToCollectionRequest. - * Use `create(AddDevicesToCollectionRequestSchema)` to create a new message. - */ -export const AddDevicesToCollectionRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 16); - -/** - * Response after adding devices to a collection - * - * @generated from message collection.v1.AddDevicesToCollectionResponse - */ -export type AddDevicesToCollectionResponse = Message<"collection.v1.AddDevicesToCollectionResponse"> & { - /** - * ID of the collection devices were added to - * - * @generated from field: int64 collection_id = 1; - */ - collectionId: bigint; - - /** - * Number of devices successfully added - * May be less than requested if some devices were already members - * - * @generated from field: int32 added_count = 2; - */ - addedCount: number; - - /** - * Number of devices whose site_id was rewritten by the cascade. - * - * @generated from field: int32 site_reassigned_count = 3; - */ - siteReassignedCount: number; -}; - -/** - * Describes the message collection.v1.AddDevicesToCollectionResponse. - * Use `create(AddDevicesToCollectionResponseSchema)` to create a new message. - */ -export const AddDevicesToCollectionResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 17); - -/** - * Request to remove devices from a collection - * - * @generated from message collection.v1.RemoveDevicesFromCollectionRequest - */ -export type RemoveDevicesFromCollectionRequest = Message<"collection.v1.RemoveDevicesFromCollectionRequest"> & { - /** - * ID of the collection to remove devices from - * - * @generated from field: int64 collection_id = 1; - */ - collectionId: bigint; - - /** - * Devices to remove: specific list or all paired devices - * - * @generated from field: common.v1.DeviceSelector device_selector = 2; - */ - deviceSelector?: DeviceSelector | undefined; -}; - -/** - * Describes the message collection.v1.RemoveDevicesFromCollectionRequest. - * Use `create(RemoveDevicesFromCollectionRequestSchema)` to create a new message. - */ -export const RemoveDevicesFromCollectionRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 18); - -/** - * Response after removing devices from a collection - * - * @generated from message collection.v1.RemoveDevicesFromCollectionResponse - */ -export type RemoveDevicesFromCollectionResponse = Message<"collection.v1.RemoveDevicesFromCollectionResponse"> & { - /** - * Number of devices successfully removed - * - * @generated from field: int32 removed_count = 1; - */ - removedCount: number; -}; - -/** - * Describes the message collection.v1.RemoveDevicesFromCollectionResponse. - * Use `create(RemoveDevicesFromCollectionResponseSchema)` to create a new message. - */ -export const RemoveDevicesFromCollectionResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 19); - /** * Request to list members of a collection * @@ -815,7 +694,7 @@ export type ListCollectionMembersRequest = Message<"collection.v1.ListCollection */ export const ListCollectionMembersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 20); + messageDesc(file_collection_v1_collection, 16); /** * Response containing collection members @@ -844,7 +723,7 @@ export type ListCollectionMembersResponse = Message<"collection.v1.ListCollectio */ export const ListCollectionMembersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 21); + messageDesc(file_collection_v1_collection, 17); /** * Request to get collections for a device @@ -873,7 +752,7 @@ export type GetDeviceCollectionsRequest = Message<"collection.v1.GetDeviceCollec */ export const GetDeviceCollectionsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 22); + messageDesc(file_collection_v1_collection, 18); /** * Response containing collections the device belongs to @@ -895,7 +774,7 @@ export type GetDeviceCollectionsResponse = Message<"collection.v1.GetDeviceColle */ export const GetDeviceCollectionsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 23); + messageDesc(file_collection_v1_collection, 19); /** * Request to set a device's slot position within a rack @@ -931,7 +810,7 @@ export type SetRackSlotPositionRequest = Message<"collection.v1.SetRackSlotPosit */ export const SetRackSlotPositionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 24); + messageDesc(file_collection_v1_collection, 20); /** * Response after setting a rack slot position @@ -960,7 +839,7 @@ export type SetRackSlotPositionResponse = Message<"collection.v1.SetRackSlotPosi */ export const SetRackSlotPositionResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 25); + messageDesc(file_collection_v1_collection, 21); /** * Request to clear a device's slot position within a rack @@ -989,7 +868,7 @@ export type ClearRackSlotPositionRequest = Message<"collection.v1.ClearRackSlotP */ export const ClearRackSlotPositionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 26); + messageDesc(file_collection_v1_collection, 22); /** * Response after clearing a rack slot position @@ -1004,7 +883,7 @@ export type ClearRackSlotPositionResponse = Message<"collection.v1.ClearRackSlot */ export const ClearRackSlotPositionResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 27); + messageDesc(file_collection_v1_collection, 23); /** * Request to list all occupied slots in a rack @@ -1026,7 +905,7 @@ export type GetRackSlotsRequest = Message<"collection.v1.GetRackSlotsRequest"> & */ export const GetRackSlotsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 28); + messageDesc(file_collection_v1_collection, 24); /** * Represents a device assigned to a specific slot in a rack @@ -1053,7 +932,7 @@ export type RackSlot = Message<"collection.v1.RackSlot"> & { * Describes the message collection.v1.RackSlot. * Use `create(RackSlotSchema)` to create a new message. */ -export const RackSlotSchema: GenMessage = /*@__PURE__*/ messageDesc(file_collection_v1_collection, 29); +export const RackSlotSchema: GenMessage = /*@__PURE__*/ messageDesc(file_collection_v1_collection, 25); /** * Response containing all occupied rack slots @@ -1075,7 +954,7 @@ export type GetRackSlotsResponse = Message<"collection.v1.GetRackSlotsResponse"> */ export const GetRackSlotsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 30); + messageDesc(file_collection_v1_collection, 26); /** * Aggregated telemetry stats for a single collection @@ -1220,7 +1099,7 @@ export type CollectionStats = Message<"collection.v1.CollectionStats"> & { */ export const CollectionStatsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 31); + messageDesc(file_collection_v1_collection, 27); /** * Request to get aggregated stats for collections @@ -1242,7 +1121,7 @@ export type GetCollectionStatsRequest = Message<"collection.v1.GetCollectionStat */ export const GetCollectionStatsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 32); + messageDesc(file_collection_v1_collection, 28); /** * Response containing stats for each requested collection @@ -1264,7 +1143,7 @@ export type GetCollectionStatsResponse = Message<"collection.v1.GetCollectionSta */ export const GetCollectionStatsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 33); + messageDesc(file_collection_v1_collection, 29); /** * Status of a single slot in a rack grid @@ -1300,7 +1179,7 @@ export type RackSlotStatus = Message<"collection.v1.RackSlotStatus"> & { */ export const RackSlotStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 34); + messageDesc(file_collection_v1_collection, 30); /** * Request to list all distinct rack zones for the organization @@ -1315,7 +1194,7 @@ export type ListRackZonesRequest = Message<"collection.v1.ListRackZonesRequest"> */ export const ListRackZonesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 35); + messageDesc(file_collection_v1_collection, 31); /** * Response containing all distinct rack zones @@ -1337,7 +1216,7 @@ export type ListRackZonesResponse = Message<"collection.v1.ListRackZonesResponse */ export const ListRackZonesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 36); + messageDesc(file_collection_v1_collection, 32); /** * Request to list all distinct rack types for the organization @@ -1352,7 +1231,7 @@ export type ListRackTypesRequest = Message<"collection.v1.ListRackTypesRequest"> */ export const ListRackTypesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 37); + messageDesc(file_collection_v1_collection, 33); /** * A rack type defined by its row/column dimensions and how many racks use it @@ -1386,7 +1265,7 @@ export type RackType = Message<"collection.v1.RackType"> & { * Describes the message collection.v1.RackType. * Use `create(RackTypeSchema)` to create a new message. */ -export const RackTypeSchema: GenMessage = /*@__PURE__*/ messageDesc(file_collection_v1_collection, 38); +export const RackTypeSchema: GenMessage = /*@__PURE__*/ messageDesc(file_collection_v1_collection, 34); /** * Response containing all distinct rack types @@ -1408,7 +1287,7 @@ export type ListRackTypesResponse = Message<"collection.v1.ListRackTypesResponse */ export const ListRackTypesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 39); + messageDesc(file_collection_v1_collection, 35); /** * Request to atomically create or update a rack with membership and slot assignments. @@ -1465,7 +1344,7 @@ export type SaveRackRequest = Message<"collection.v1.SaveRackRequest"> & { */ export const SaveRackRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 40); + messageDesc(file_collection_v1_collection, 36); /** * Response after saving a rack. @@ -1501,7 +1380,7 @@ export type SaveRackResponse = Message<"collection.v1.SaveRackResponse"> & { */ export const SaveRackResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_collection_v1_collection, 41); + messageDesc(file_collection_v1_collection, 37); /** * Type of collection @@ -1702,26 +1581,6 @@ export const DeviceCollectionService: GenService<{ input: typeof ListCollectionsRequestSchema; output: typeof ListCollectionsResponseSchema; }; - /** - * Adds devices to a collection - * - * @generated from rpc collection.v1.DeviceCollectionService.AddDevicesToCollection - */ - addDevicesToCollection: { - methodKind: "unary"; - input: typeof AddDevicesToCollectionRequestSchema; - output: typeof AddDevicesToCollectionResponseSchema; - }; - /** - * Removes devices from a collection - * - * @generated from rpc collection.v1.DeviceCollectionService.RemoveDevicesFromCollection - */ - removeDevicesFromCollection: { - methodKind: "unary"; - input: typeof RemoveDevicesFromCollectionRequestSchema; - output: typeof RemoveDevicesFromCollectionResponseSchema; - }; /** * Lists members of a collection * diff --git a/client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts b/client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts index 58f422208..65d9e28ca 100644 --- a/client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts +++ b/client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts @@ -23,7 +23,7 @@ import type { Message } from "@bufbuild/protobuf"; export const file_device_set_v1_device_set: GenFile = /*@__PURE__*/ fileDesc( - "Ch5kZXZpY2Vfc2V0L3YxL2RldmljZV9zZXQucHJvdG8SDWRldmljZV9zZXQudjEiywIKCURldmljZVNldBIKCgJpZBgBIAEoAxIqCgR0eXBlGAIgASgOMhwuZGV2aWNlX3NldC52MS5EZXZpY2VTZXRUeXBlEg0KBWxhYmVsGAMgASgJEhMKC2Rlc2NyaXB0aW9uGAQgASgJEhQKDGRldmljZV9jb3VudBgFIAEoBRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgp1cGRhdGVkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIsCglyYWNrX2luZm8YCCABKAsyFy5kZXZpY2Vfc2V0LnYxLlJhY2tJbmZvSAASLgoKZ3JvdXBfaW5mbxgJIAEoCzIYLmRldmljZV9zZXQudjEuR3JvdXBJbmZvSABCDgoMdHlwZV9kZXRhaWxzIogCCghSYWNrSW5mbxIVCgRyb3dzGAEgASgFQge6SAQaAiAAEhgKB2NvbHVtbnMYAiABKAVCB7pIBBoCIAASFQoEem9uZRgDIAEoCUIHukgEcgIYZBIyCgtvcmRlcl9pbmRleBgEIAEoDjIdLmRldmljZV9zZXQudjEuUmFja09yZGVySW5kZXgSNAoMY29vbGluZ190eXBlGAUgASgOMh4uZGV2aWNlX3NldC52MS5SYWNrQ29vbGluZ1R5cGUSFAoHc2l0ZV9pZBgGIAEoA0gAiAEBEhgKC2J1aWxkaW5nX2lkGAcgASgDSAGIAQFCCgoIX3NpdGVfaWRCDgoMX2J1aWxkaW5nX2lkIgsKCUdyb3VwSW5mbyKeAQoPRGV2aWNlU2V0TWVtYmVyEhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEiwKCGFkZGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgRyYWNrGAMgASgLMiAuZGV2aWNlX3NldC52MS5SYWNrTWVtYmVyRGV0YWlsc0gAQhAKDm1lbWJlcl9kZXRhaWxzIksKEVJhY2tNZW1iZXJEZXRhaWxzEjYKDXNsb3RfcG9zaXRpb24YASABKAsyHy5kZXZpY2Vfc2V0LnYxLlJhY2tTbG90UG9zaXRpb24iQQoQUmFja1Nsb3RQb3NpdGlvbhIUCgNyb3cYASABKAVCB7pIBBoCKAASFwoGY29sdW1uGAIgASgFQge6SAQaAigAIscCChZDcmVhdGVEZXZpY2VTZXRSZXF1ZXN0EjYKBHR5cGUYASABKA4yHC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFR5cGVCCrpIB4IBBBABIAASGwoFbGFiZWwYAiABKAlCDLpICcgBAXIEEAEYZBIdCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMY9AMSLAoJcmFja19pbmZvGAQgASgLMhcuZGV2aWNlX3NldC52MS5SYWNrSW5mb0gAEi4KCmdyb3VwX2luZm8YBSABKAsyGC5kZXZpY2Vfc2V0LnYxLkdyb3VwSW5mb0gAEjcKD2RldmljZV9zZWxlY3RvchgGIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckgBiAEBQg4KDHR5cGVfZGV0YWlsc0ISChBfZGV2aWNlX3NlbGVjdG9yIlwKF0NyZWF0ZURldmljZVNldFJlc3BvbnNlEiwKCmRldmljZV9zZXQYASABKAsyGC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldBITCgthZGRlZF9jb3VudBgCIAEoBSI1ChNHZXREZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAAiRAoUR2V0RGV2aWNlU2V0UmVzcG9uc2USLAoKZGV2aWNlX3NldBgBIAEoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0Ir0CChZVcGRhdGVEZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAASIAoFbGFiZWwYAiABKAlCDLpICdgBAXIEEAEYZEgBiAEBEiUKC2Rlc2NyaXB0aW9uGAMgASgJQgu6SAjYAQFyAxj0A0gCiAEBEiwKCXJhY2tfaW5mbxgEIAEoCzIXLmRldmljZV9zZXQudjEuUmFja0luZm9IABIuCgpncm91cF9pbmZvGAUgASgLMhguZGV2aWNlX3NldC52MS5Hcm91cEluZm9IABIyCg9kZXZpY2Vfc2VsZWN0b3IYBiABKAsyGS5jb21tb24udjEuRGV2aWNlU2VsZWN0b3JCDgoMdHlwZV9kZXRhaWxzQggKBl9sYWJlbEIOCgxfZGVzY3JpcHRpb24iRwoXVXBkYXRlRGV2aWNlU2V0UmVzcG9uc2USLAoKZGV2aWNlX3NldBgBIAEoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0IjgKFkRlbGV0ZURldmljZVNldFJlcXVlc3QSHgoNZGV2aWNlX3NldF9pZBgBIAEoA0IHukgEIgIgACIZChdEZWxldGVEZXZpY2VTZXRSZXNwb25zZSLIAgoVTGlzdERldmljZVNldHNSZXF1ZXN0EjQKBHR5cGUYASABKA4yHC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFR5cGVCCLpIBYIBAhABEhoKCXBhZ2Vfc2l6ZRgCIAEoBUIHukgEGgIoABISCgpwYWdlX3Rva2VuGAMgASgJEiMKBHNvcnQYBCABKAsyFS5jb21tb24udjEuU29ydENvbmZpZxI3ChVlcnJvcl9jb21wb25lbnRfdHlwZXMYBSADKA4yGC5lcnJvcnMudjEuQ29tcG9uZW50VHlwZRIRCgV6b25lcxgGIAMoCUICGAESFAoMYnVpbGRpbmdfaWRzGAcgAygDEhsKE2luY2x1ZGVfbm9fYnVpbGRpbmcYCCABKAgSJQoJem9uZV9rZXlzGAkgAygLMhIuY29tbW9uLnYxLlpvbmVLZXkidQoWTGlzdERldmljZVNldHNSZXNwb25zZRItCgtkZXZpY2Vfc2V0cxgBIAMoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoBSJ6ChxBZGREZXZpY2VzVG9EZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAASOgoPZGV2aWNlX3NlbGVjdG9yGAIgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQEiagodQWRkRGV2aWNlc1RvRGV2aWNlU2V0UmVzcG9uc2USFQoNZGV2aWNlX3NldF9pZBgBIAEoAxITCgthZGRlZF9jb3VudBgCIAEoBRIdChVzaXRlX3JlYXNzaWduZWRfY291bnQYAyABKAUifwohUmVtb3ZlRGV2aWNlc0Zyb21EZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAASOgoPZGV2aWNlX3NlbGVjdG9yGAIgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQEiOwoiUmVtb3ZlRGV2aWNlc0Zyb21EZXZpY2VTZXRSZXNwb25zZRIVCg1yZW1vdmVkX2NvdW50GAEgASgFIm0KG0xpc3REZXZpY2VTZXRNZW1iZXJzUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEhoKCXBhZ2Vfc2l6ZRgCIAEoBUIHukgEGgIoABISCgpwYWdlX3Rva2VuGAMgASgJImgKHExpc3REZXZpY2VTZXRNZW1iZXJzUmVzcG9uc2USLwoHbWVtYmVycxgBIAMoCzIeLmRldmljZV9zZXQudjEuRGV2aWNlU2V0TWVtYmVyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJsChpHZXREZXZpY2VEZXZpY2VTZXRzUmVxdWVzdBIiChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIHukgEcgIQARIqCgR0eXBlGAIgASgOMhwuZGV2aWNlX3NldC52MS5EZXZpY2VTZXRUeXBlIkwKG0dldERldmljZURldmljZVNldHNSZXNwb25zZRItCgtkZXZpY2Vfc2V0cxgBIAMoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0IpsBChpTZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABEjkKCHBvc2l0aW9uGAMgASgLMh8uZGV2aWNlX3NldC52MS5SYWNrU2xvdFBvc2l0aW9uQga6SAPIAQEiWwobU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEhUKDWRldmljZV9zZXRfaWQYASABKAMSJQoEc2xvdBgCIAEoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3QiYgocQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABIh8KHUNsZWFyUmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlIjUKE0dldFJhY2tTbG90c1JlcXVlc3QSHgoNZGV2aWNlX3NldF9pZBgBIAEoA0IHukgEIgIgACJYCghSYWNrU2xvdBIZChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCRIxCghwb3NpdGlvbhgCIAEoCzIfLmRldmljZV9zZXQudjEuUmFja1Nsb3RQb3NpdGlvbiI+ChRHZXRSYWNrU2xvdHNSZXNwb25zZRImCgVzbG90cxgBIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3Qi7QQKDkRldmljZVNldFN0YXRzEhUKDWRldmljZV9zZXRfaWQYASABKAMSFAoMZGV2aWNlX2NvdW50GAIgASgFEhcKD3JlcG9ydGluZ19jb3VudBgDIAEoBRIaChJ0b3RhbF9oYXNocmF0ZV90aHMYBCABKAESGgoSYXZnX2VmZmljaWVuY3lfanRoGAUgASgBEhYKDnRvdGFsX3Bvd2VyX2t3GAYgASgBEhkKEW1pbl90ZW1wZXJhdHVyZV9jGAcgASgBEhkKEW1heF90ZW1wZXJhdHVyZV9jGAggASgBEhUKDWhhc2hpbmdfY291bnQYCSABKAUSFAoMYnJva2VuX2NvdW50GAogASgFEhUKDW9mZmxpbmVfY291bnQYCyABKAUSFgoOc2xlZXBpbmdfY291bnQYDCABKAUSIAoYaGFzaHJhdGVfcmVwb3J0aW5nX2NvdW50GA0gASgFEiIKGmVmZmljaWVuY3lfcmVwb3J0aW5nX2NvdW50GA4gASgFEh0KFXBvd2VyX3JlcG9ydGluZ19jb3VudBgPIAEoBRIjCht0ZW1wZXJhdHVyZV9yZXBvcnRpbmdfY291bnQYECABKAUSIQoZY29udHJvbF9ib2FyZF9pc3N1ZV9jb3VudBgRIAEoBRIXCg9mYW5faXNzdWVfY291bnQYEiABKAUSHgoWaGFzaF9ib2FyZF9pc3N1ZV9jb3VudBgTIAEoBRIXCg9wc3VfaXNzdWVfY291bnQYFCABKAUSNAoNc2xvdF9zdGF0dXNlcxgVIAMoCzIdLmRldmljZV9zZXQudjEuUmFja1Nsb3RTdGF0dXMiMgoYR2V0RGV2aWNlU2V0U3RhdHNSZXF1ZXN0EhYKDmRldmljZV9zZXRfaWRzGAEgAygDIkkKGUdldERldmljZVNldFN0YXRzUmVzcG9uc2USLAoFc3RhdHMYASADKAsyHS5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFN0YXRzIl4KDlJhY2tTbG90U3RhdHVzEgsKA3JvdxgBIAEoBRIOCgZjb2x1bW4YAiABKAUSLwoGc3RhdHVzGAMgASgOMh8uZGV2aWNlX3NldC52MS5TbG90RGV2aWNlU3RhdHVzIhYKFExpc3RSYWNrWm9uZXNSZXF1ZXN0IiYKFUxpc3RSYWNrWm9uZXNSZXNwb25zZRINCgV6b25lcxgBIAMoCSIZChdMaXN0UmFja1pvbmVSZWZzUmVxdWVzdCI9ChhMaXN0UmFja1pvbmVSZWZzUmVzcG9uc2USIQoFem9uZXMYASADKAsyEi5jb21tb24udjEuWm9uZVJlZiIWChRMaXN0UmFja1R5cGVzUmVxdWVzdCI9CghSYWNrVHlwZRIMCgRyb3dzGAEgASgFEg8KB2NvbHVtbnMYAiABKAUSEgoKcmFja19jb3VudBgDIAEoBSJEChVMaXN0UmFja1R5cGVzUmVzcG9uc2USKwoKcmFja190eXBlcxgBIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1R5cGUiiwIKD1NhdmVSYWNrUmVxdWVzdBImCg1kZXZpY2Vfc2V0X2lkGAEgASgDQgq6SAfYAQEiAiAASACIAQESGwoFbGFiZWwYAiABKAlCDLpICcgBAXIEEAEYZBIyCglyYWNrX2luZm8YAyABKAsyFy5kZXZpY2Vfc2V0LnYxLlJhY2tJbmZvQga6SAPIAQESOgoPZGV2aWNlX3NlbGVjdG9yGAQgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQESMQoQc2xvdF9hc3NpZ25tZW50cxgFIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3RCEAoOX2RldmljZV9zZXRfaWQidwoQU2F2ZVJhY2tSZXNwb25zZRIsCgpkZXZpY2Vfc2V0GAEgASgLMhguZGV2aWNlX3NldC52MS5EZXZpY2VTZXQSFgoOYXNzaWduZWRfY291bnQYAiABKAUSHQoVc2l0ZV9yZWFzc2lnbmVkX2NvdW50GAMgASgFKmUKDURldmljZVNldFR5cGUSHwobREVWSUNFX1NFVF9UWVBFX1VOU1BFQ0lGSUVEEAASGQoVREVWSUNFX1NFVF9UWVBFX0dST1VQEAESGAoUREVWSUNFX1NFVF9UWVBFX1JBQ0sQAiq2AQoOUmFja09yZGVySW5kZXgSIAocUkFDS19PUkRFUl9JTkRFWF9VTlNQRUNJRklFRBAAEiAKHFJBQ0tfT1JERVJfSU5ERVhfQk9UVE9NX0xFRlQQARIdChlSQUNLX09SREVSX0lOREVYX1RPUF9MRUZUEAISIQodUkFDS19PUkRFUl9JTkRFWF9CT1RUT01fUklHSFQQAxIeChpSQUNLX09SREVSX0lOREVYX1RPUF9SSUdIVBAEKnAKD1JhY2tDb29saW5nVHlwZRIhCh1SQUNLX0NPT0xJTkdfVFlQRV9VTlNQRUNJRklFRBAAEhkKFVJBQ0tfQ09PTElOR19UWVBFX0FJUhABEh8KG1JBQ0tfQ09PTElOR19UWVBFX0lNTUVSU0lPThACKt0BChBTbG90RGV2aWNlU3RhdHVzEiIKHlNMT1RfREVWSUNFX1NUQVRVU19VTlNQRUNJRklFRBAAEhwKGFNMT1RfREVWSUNFX1NUQVRVU19FTVBUWRABEh4KGlNMT1RfREVWSUNFX1NUQVRVU19IRUFMVEhZEAISJgoiU0xPVF9ERVZJQ0VfU1RBVFVTX05FRURTX0FUVEVOVElPThADEh4KGlNMT1RfREVWSUNFX1NUQVRVU19PRkZMSU5FEAQSHwobU0xPVF9ERVZJQ0VfU1RBVFVTX1NMRUVQSU5HEAUy1A0KEERldmljZVNldFNlcnZpY2USYAoPQ3JlYXRlRGV2aWNlU2V0EiUuZGV2aWNlX3NldC52MS5DcmVhdGVEZXZpY2VTZXRSZXF1ZXN0GiYuZGV2aWNlX3NldC52MS5DcmVhdGVEZXZpY2VTZXRSZXNwb25zZRJXCgxHZXREZXZpY2VTZXQSIi5kZXZpY2Vfc2V0LnYxLkdldERldmljZVNldFJlcXVlc3QaIy5kZXZpY2Vfc2V0LnYxLkdldERldmljZVNldFJlc3BvbnNlEmAKD1VwZGF0ZURldmljZVNldBIlLmRldmljZV9zZXQudjEuVXBkYXRlRGV2aWNlU2V0UmVxdWVzdBomLmRldmljZV9zZXQudjEuVXBkYXRlRGV2aWNlU2V0UmVzcG9uc2USYAoPRGVsZXRlRGV2aWNlU2V0EiUuZGV2aWNlX3NldC52MS5EZWxldGVEZXZpY2VTZXRSZXF1ZXN0GiYuZGV2aWNlX3NldC52MS5EZWxldGVEZXZpY2VTZXRSZXNwb25zZRJdCg5MaXN0RGV2aWNlU2V0cxIkLmRldmljZV9zZXQudjEuTGlzdERldmljZVNldHNSZXF1ZXN0GiUuZGV2aWNlX3NldC52MS5MaXN0RGV2aWNlU2V0c1Jlc3BvbnNlEnIKFUFkZERldmljZXNUb0RldmljZVNldBIrLmRldmljZV9zZXQudjEuQWRkRGV2aWNlc1RvRGV2aWNlU2V0UmVxdWVzdBosLmRldmljZV9zZXQudjEuQWRkRGV2aWNlc1RvRGV2aWNlU2V0UmVzcG9uc2USgQEKGlJlbW92ZURldmljZXNGcm9tRGV2aWNlU2V0EjAuZGV2aWNlX3NldC52MS5SZW1vdmVEZXZpY2VzRnJvbURldmljZVNldFJlcXVlc3QaMS5kZXZpY2Vfc2V0LnYxLlJlbW92ZURldmljZXNGcm9tRGV2aWNlU2V0UmVzcG9uc2USbwoUTGlzdERldmljZVNldE1lbWJlcnMSKi5kZXZpY2Vfc2V0LnYxLkxpc3REZXZpY2VTZXRNZW1iZXJzUmVxdWVzdBorLmRldmljZV9zZXQudjEuTGlzdERldmljZVNldE1lbWJlcnNSZXNwb25zZRJsChNHZXREZXZpY2VEZXZpY2VTZXRzEikuZGV2aWNlX3NldC52MS5HZXREZXZpY2VEZXZpY2VTZXRzUmVxdWVzdBoqLmRldmljZV9zZXQudjEuR2V0RGV2aWNlRGV2aWNlU2V0c1Jlc3BvbnNlEmwKE1NldFJhY2tTbG90UG9zaXRpb24SKS5kZXZpY2Vfc2V0LnYxLlNldFJhY2tTbG90UG9zaXRpb25SZXF1ZXN0GiouZGV2aWNlX3NldC52MS5TZXRSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2UScgoVQ2xlYXJSYWNrU2xvdFBvc2l0aW9uEisuZGV2aWNlX3NldC52MS5DbGVhclJhY2tTbG90UG9zaXRpb25SZXF1ZXN0GiwuZGV2aWNlX3NldC52MS5DbGVhclJhY2tTbG90UG9zaXRpb25SZXNwb25zZRJXCgxHZXRSYWNrU2xvdHMSIi5kZXZpY2Vfc2V0LnYxLkdldFJhY2tTbG90c1JlcXVlc3QaIy5kZXZpY2Vfc2V0LnYxLkdldFJhY2tTbG90c1Jlc3BvbnNlEmYKEUdldERldmljZVNldFN0YXRzEicuZGV2aWNlX3NldC52MS5HZXREZXZpY2VTZXRTdGF0c1JlcXVlc3QaKC5kZXZpY2Vfc2V0LnYxLkdldERldmljZVNldFN0YXRzUmVzcG9uc2USWgoNTGlzdFJhY2tab25lcxIjLmRldmljZV9zZXQudjEuTGlzdFJhY2tab25lc1JlcXVlc3QaJC5kZXZpY2Vfc2V0LnYxLkxpc3RSYWNrWm9uZXNSZXNwb25zZRJjChBMaXN0UmFja1pvbmVSZWZzEiYuZGV2aWNlX3NldC52MS5MaXN0UmFja1pvbmVSZWZzUmVxdWVzdBonLmRldmljZV9zZXQudjEuTGlzdFJhY2tab25lUmVmc1Jlc3BvbnNlEloKDUxpc3RSYWNrVHlwZXMSIy5kZXZpY2Vfc2V0LnYxLkxpc3RSYWNrVHlwZXNSZXF1ZXN0GiQuZGV2aWNlX3NldC52MS5MaXN0UmFja1R5cGVzUmVzcG9uc2USSwoIU2F2ZVJhY2sSHi5kZXZpY2Vfc2V0LnYxLlNhdmVSYWNrUmVxdWVzdBofLmRldmljZV9zZXQudjEuU2F2ZVJhY2tSZXNwb25zZULDAQoRY29tLmRldmljZV9zZXQudjFCDkRldmljZVNldFByb3RvUAFaTWdpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL2RldmljZV9zZXQvdjE7ZGV2aWNlX3NldHYxogIDRFhYqgIMRGV2aWNlU2V0LlYxygIMRGV2aWNlU2V0XFYx4gIYRGV2aWNlU2V0XFYxXEdQQk1ldGFkYXRh6gINRGV2aWNlU2V0OjpWMWIGcHJvdG8z", + "Ch5kZXZpY2Vfc2V0L3YxL2RldmljZV9zZXQucHJvdG8SDWRldmljZV9zZXQudjEiywIKCURldmljZVNldBIKCgJpZBgBIAEoAxIqCgR0eXBlGAIgASgOMhwuZGV2aWNlX3NldC52MS5EZXZpY2VTZXRUeXBlEg0KBWxhYmVsGAMgASgJEhMKC2Rlc2NyaXB0aW9uGAQgASgJEhQKDGRldmljZV9jb3VudBgFIAEoBRIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgp1cGRhdGVkX2F0GAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIsCglyYWNrX2luZm8YCCABKAsyFy5kZXZpY2Vfc2V0LnYxLlJhY2tJbmZvSAASLgoKZ3JvdXBfaW5mbxgJIAEoCzIYLmRldmljZV9zZXQudjEuR3JvdXBJbmZvSABCDgoMdHlwZV9kZXRhaWxzIogCCghSYWNrSW5mbxIVCgRyb3dzGAEgASgFQge6SAQaAiAAEhgKB2NvbHVtbnMYAiABKAVCB7pIBBoCIAASFQoEem9uZRgDIAEoCUIHukgEcgIYZBIyCgtvcmRlcl9pbmRleBgEIAEoDjIdLmRldmljZV9zZXQudjEuUmFja09yZGVySW5kZXgSNAoMY29vbGluZ190eXBlGAUgASgOMh4uZGV2aWNlX3NldC52MS5SYWNrQ29vbGluZ1R5cGUSFAoHc2l0ZV9pZBgGIAEoA0gAiAEBEhgKC2J1aWxkaW5nX2lkGAcgASgDSAGIAQFCCgoIX3NpdGVfaWRCDgoMX2J1aWxkaW5nX2lkIgsKCUdyb3VwSW5mbyKeAQoPRGV2aWNlU2V0TWVtYmVyEhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEiwKCGFkZGVkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgRyYWNrGAMgASgLMiAuZGV2aWNlX3NldC52MS5SYWNrTWVtYmVyRGV0YWlsc0gAQhAKDm1lbWJlcl9kZXRhaWxzIksKEVJhY2tNZW1iZXJEZXRhaWxzEjYKDXNsb3RfcG9zaXRpb24YASABKAsyHy5kZXZpY2Vfc2V0LnYxLlJhY2tTbG90UG9zaXRpb24iQQoQUmFja1Nsb3RQb3NpdGlvbhIUCgNyb3cYASABKAVCB7pIBBoCKAASFwoGY29sdW1uGAIgASgFQge6SAQaAigAIscCChZDcmVhdGVEZXZpY2VTZXRSZXF1ZXN0EjYKBHR5cGUYASABKA4yHC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFR5cGVCCrpIB4IBBBABIAASGwoFbGFiZWwYAiABKAlCDLpICcgBAXIEEAEYZBIdCgtkZXNjcmlwdGlvbhgDIAEoCUIIukgFcgMY9AMSLAoJcmFja19pbmZvGAQgASgLMhcuZGV2aWNlX3NldC52MS5SYWNrSW5mb0gAEi4KCmdyb3VwX2luZm8YBSABKAsyGC5kZXZpY2Vfc2V0LnYxLkdyb3VwSW5mb0gAEjcKD2RldmljZV9zZWxlY3RvchgGIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckgBiAEBQg4KDHR5cGVfZGV0YWlsc0ISChBfZGV2aWNlX3NlbGVjdG9yIlwKF0NyZWF0ZURldmljZVNldFJlc3BvbnNlEiwKCmRldmljZV9zZXQYASABKAsyGC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldBITCgthZGRlZF9jb3VudBgCIAEoBSI1ChNHZXREZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAAiRAoUR2V0RGV2aWNlU2V0UmVzcG9uc2USLAoKZGV2aWNlX3NldBgBIAEoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0Ir0CChZVcGRhdGVEZXZpY2VTZXRSZXF1ZXN0Eh4KDWRldmljZV9zZXRfaWQYASABKANCB7pIBCICIAASIAoFbGFiZWwYAiABKAlCDLpICdgBAXIEEAEYZEgBiAEBEiUKC2Rlc2NyaXB0aW9uGAMgASgJQgu6SAjYAQFyAxj0A0gCiAEBEiwKCXJhY2tfaW5mbxgEIAEoCzIXLmRldmljZV9zZXQudjEuUmFja0luZm9IABIuCgpncm91cF9pbmZvGAUgASgLMhguZGV2aWNlX3NldC52MS5Hcm91cEluZm9IABIyCg9kZXZpY2Vfc2VsZWN0b3IYBiABKAsyGS5jb21tb24udjEuRGV2aWNlU2VsZWN0b3JCDgoMdHlwZV9kZXRhaWxzQggKBl9sYWJlbEIOCgxfZGVzY3JpcHRpb24iRwoXVXBkYXRlRGV2aWNlU2V0UmVzcG9uc2USLAoKZGV2aWNlX3NldBgBIAEoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0IjgKFkRlbGV0ZURldmljZVNldFJlcXVlc3QSHgoNZGV2aWNlX3NldF9pZBgBIAEoA0IHukgEIgIgACIZChdEZWxldGVEZXZpY2VTZXRSZXNwb25zZSLIAgoVTGlzdERldmljZVNldHNSZXF1ZXN0EjQKBHR5cGUYASABKA4yHC5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFR5cGVCCLpIBYIBAhABEhoKCXBhZ2Vfc2l6ZRgCIAEoBUIHukgEGgIoABISCgpwYWdlX3Rva2VuGAMgASgJEiMKBHNvcnQYBCABKAsyFS5jb21tb24udjEuU29ydENvbmZpZxI3ChVlcnJvcl9jb21wb25lbnRfdHlwZXMYBSADKA4yGC5lcnJvcnMudjEuQ29tcG9uZW50VHlwZRIRCgV6b25lcxgGIAMoCUICGAESFAoMYnVpbGRpbmdfaWRzGAcgAygDEhsKE2luY2x1ZGVfbm9fYnVpbGRpbmcYCCABKAgSJQoJem9uZV9rZXlzGAkgAygLMhIuY29tbW9uLnYxLlpvbmVLZXkidQoWTGlzdERldmljZVNldHNSZXNwb25zZRItCgtkZXZpY2Vfc2V0cxgBIAMoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoBSJ4ChhBZGREZXZpY2VzVG9Hcm91cFJlcXVlc3QSIAoPdGFyZ2V0X2dyb3VwX2lkGAEgASgDQge6SAQiAiAAEjoKD2RldmljZV9zZWxlY3RvchgCIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBIjAKGUFkZERldmljZXNUb0dyb3VwUmVzcG9uc2USEwoLYWRkZWRfY291bnQYASABKAMifQodUmVtb3ZlRGV2aWNlc0Zyb21Hcm91cFJlcXVlc3QSIAoPdGFyZ2V0X2dyb3VwX2lkGAEgASgDQge6SAQiAiAAEjoKD2RldmljZV9zZWxlY3RvchgCIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBIjcKHlJlbW92ZURldmljZXNGcm9tR3JvdXBSZXNwb25zZRIVCg1yZW1vdmVkX2NvdW50GAEgASgDIm0KG0xpc3REZXZpY2VTZXRNZW1iZXJzUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEhoKCXBhZ2Vfc2l6ZRgCIAEoBUIHukgEGgIoABISCgpwYWdlX3Rva2VuGAMgASgJImgKHExpc3REZXZpY2VTZXRNZW1iZXJzUmVzcG9uc2USLwoHbWVtYmVycxgBIAMoCzIeLmRldmljZV9zZXQudjEuRGV2aWNlU2V0TWVtYmVyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJsChpHZXREZXZpY2VEZXZpY2VTZXRzUmVxdWVzdBIiChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIHukgEcgIQARIqCgR0eXBlGAIgASgOMhwuZGV2aWNlX3NldC52MS5EZXZpY2VTZXRUeXBlIkwKG0dldERldmljZURldmljZVNldHNSZXNwb25zZRItCgtkZXZpY2Vfc2V0cxgBIAMoCzIYLmRldmljZV9zZXQudjEuRGV2aWNlU2V0IpsBChpTZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABEjkKCHBvc2l0aW9uGAMgASgLMh8uZGV2aWNlX3NldC52MS5SYWNrU2xvdFBvc2l0aW9uQga6SAPIAQEiWwobU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEhUKDWRldmljZV9zZXRfaWQYASABKAMSJQoEc2xvdBgCIAEoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3QiYgocQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBIeCg1kZXZpY2Vfc2V0X2lkGAEgASgDQge6SAQiAiAAEiIKEWRldmljZV9pZGVudGlmaWVyGAIgASgJQge6SARyAhABIh8KHUNsZWFyUmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlIjUKE0dldFJhY2tTbG90c1JlcXVlc3QSHgoNZGV2aWNlX3NldF9pZBgBIAEoA0IHukgEIgIgACJYCghSYWNrU2xvdBIZChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCRIxCghwb3NpdGlvbhgCIAEoCzIfLmRldmljZV9zZXQudjEuUmFja1Nsb3RQb3NpdGlvbiI+ChRHZXRSYWNrU2xvdHNSZXNwb25zZRImCgVzbG90cxgBIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3Qi7QQKDkRldmljZVNldFN0YXRzEhUKDWRldmljZV9zZXRfaWQYASABKAMSFAoMZGV2aWNlX2NvdW50GAIgASgFEhcKD3JlcG9ydGluZ19jb3VudBgDIAEoBRIaChJ0b3RhbF9oYXNocmF0ZV90aHMYBCABKAESGgoSYXZnX2VmZmljaWVuY3lfanRoGAUgASgBEhYKDnRvdGFsX3Bvd2VyX2t3GAYgASgBEhkKEW1pbl90ZW1wZXJhdHVyZV9jGAcgASgBEhkKEW1heF90ZW1wZXJhdHVyZV9jGAggASgBEhUKDWhhc2hpbmdfY291bnQYCSABKAUSFAoMYnJva2VuX2NvdW50GAogASgFEhUKDW9mZmxpbmVfY291bnQYCyABKAUSFgoOc2xlZXBpbmdfY291bnQYDCABKAUSIAoYaGFzaHJhdGVfcmVwb3J0aW5nX2NvdW50GA0gASgFEiIKGmVmZmljaWVuY3lfcmVwb3J0aW5nX2NvdW50GA4gASgFEh0KFXBvd2VyX3JlcG9ydGluZ19jb3VudBgPIAEoBRIjCht0ZW1wZXJhdHVyZV9yZXBvcnRpbmdfY291bnQYECABKAUSIQoZY29udHJvbF9ib2FyZF9pc3N1ZV9jb3VudBgRIAEoBRIXCg9mYW5faXNzdWVfY291bnQYEiABKAUSHgoWaGFzaF9ib2FyZF9pc3N1ZV9jb3VudBgTIAEoBRIXCg9wc3VfaXNzdWVfY291bnQYFCABKAUSNAoNc2xvdF9zdGF0dXNlcxgVIAMoCzIdLmRldmljZV9zZXQudjEuUmFja1Nsb3RTdGF0dXMiMgoYR2V0RGV2aWNlU2V0U3RhdHNSZXF1ZXN0EhYKDmRldmljZV9zZXRfaWRzGAEgAygDIkkKGUdldERldmljZVNldFN0YXRzUmVzcG9uc2USLAoFc3RhdHMYASADKAsyHS5kZXZpY2Vfc2V0LnYxLkRldmljZVNldFN0YXRzIl4KDlJhY2tTbG90U3RhdHVzEgsKA3JvdxgBIAEoBRIOCgZjb2x1bW4YAiABKAUSLwoGc3RhdHVzGAMgASgOMh8uZGV2aWNlX3NldC52MS5TbG90RGV2aWNlU3RhdHVzIhYKFExpc3RSYWNrWm9uZXNSZXF1ZXN0IiYKFUxpc3RSYWNrWm9uZXNSZXNwb25zZRINCgV6b25lcxgBIAMoCSIZChdMaXN0UmFja1pvbmVSZWZzUmVxdWVzdCI9ChhMaXN0UmFja1pvbmVSZWZzUmVzcG9uc2USIQoFem9uZXMYASADKAsyEi5jb21tb24udjEuWm9uZVJlZiIWChRMaXN0UmFja1R5cGVzUmVxdWVzdCI9CghSYWNrVHlwZRIMCgRyb3dzGAEgASgFEg8KB2NvbHVtbnMYAiABKAUSEgoKcmFja19jb3VudBgDIAEoBSJEChVMaXN0UmFja1R5cGVzUmVzcG9uc2USKwoKcmFja190eXBlcxgBIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1R5cGUiiwIKD1NhdmVSYWNrUmVxdWVzdBImCg1kZXZpY2Vfc2V0X2lkGAEgASgDQgq6SAfYAQEiAiAASACIAQESGwoFbGFiZWwYAiABKAlCDLpICcgBAXIEEAEYZBIyCglyYWNrX2luZm8YAyABKAsyFy5kZXZpY2Vfc2V0LnYxLlJhY2tJbmZvQga6SAPIAQESOgoPZGV2aWNlX3NlbGVjdG9yGAQgASgLMhkuY29tbW9uLnYxLkRldmljZVNlbGVjdG9yQga6SAPIAQESMQoQc2xvdF9hc3NpZ25tZW50cxgFIAMoCzIXLmRldmljZV9zZXQudjEuUmFja1Nsb3RCEAoOX2RldmljZV9zZXRfaWQidwoQU2F2ZVJhY2tSZXNwb25zZRIsCgpkZXZpY2Vfc2V0GAEgASgLMhguZGV2aWNlX3NldC52MS5EZXZpY2VTZXQSFgoOYXNzaWduZWRfY291bnQYAiABKAUSHQoVc2l0ZV9yZWFzc2lnbmVkX2NvdW50GAMgASgFIpEBChpBc3NpZ25EZXZpY2VzVG9SYWNrUmVxdWVzdBIkCg50YXJnZXRfcmFja19pZBgBIAEoA0IHukgEIgIgAEgAiAEBEjoKD2RldmljZV9zZWxlY3RvchgCIAEoCzIZLmNvbW1vbi52MS5EZXZpY2VTZWxlY3RvckIGukgDyAEBQhEKD190YXJnZXRfcmFja19pZCJrChtBc3NpZ25EZXZpY2VzVG9SYWNrUmVzcG9uc2USFgoOYXNzaWduZWRfY291bnQYASABKAMSHQoVc2l0ZV9yZWFzc2lnbmVkX2NvdW50GAIgASgDEhUKDXJlbW92ZWRfY291bnQYAyABKAMqZQoNRGV2aWNlU2V0VHlwZRIfChtERVZJQ0VfU0VUX1RZUEVfVU5TUEVDSUZJRUQQABIZChVERVZJQ0VfU0VUX1RZUEVfR1JPVVAQARIYChRERVZJQ0VfU0VUX1RZUEVfUkFDSxACKrYBCg5SYWNrT3JkZXJJbmRleBIgChxSQUNLX09SREVSX0lOREVYX1VOU1BFQ0lGSUVEEAASIAocUkFDS19PUkRFUl9JTkRFWF9CT1RUT01fTEVGVBABEh0KGVJBQ0tfT1JERVJfSU5ERVhfVE9QX0xFRlQQAhIhCh1SQUNLX09SREVSX0lOREVYX0JPVFRPTV9SSUdIVBADEh4KGlJBQ0tfT1JERVJfSU5ERVhfVE9QX1JJR0hUEAQqcAoPUmFja0Nvb2xpbmdUeXBlEiEKHVJBQ0tfQ09PTElOR19UWVBFX1VOU1BFQ0lGSUVEEAASGQoVUkFDS19DT09MSU5HX1RZUEVfQUlSEAESHwobUkFDS19DT09MSU5HX1RZUEVfSU1NRVJTSU9OEAIq3QEKEFNsb3REZXZpY2VTdGF0dXMSIgoeU0xPVF9ERVZJQ0VfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHAoYU0xPVF9ERVZJQ0VfU1RBVFVTX0VNUFRZEAESHgoaU0xPVF9ERVZJQ0VfU1RBVFVTX0hFQUxUSFkQAhImCiJTTE9UX0RFVklDRV9TVEFUVVNfTkVFRFNfQVRURU5USU9OEAMSHgoaU0xPVF9ERVZJQ0VfU1RBVFVTX09GRkxJTkUQBBIfChtTTE9UX0RFVklDRV9TVEFUVVNfU0xFRVBJTkcQBTKpDgoQRGV2aWNlU2V0U2VydmljZRJgCg9DcmVhdGVEZXZpY2VTZXQSJS5kZXZpY2Vfc2V0LnYxLkNyZWF0ZURldmljZVNldFJlcXVlc3QaJi5kZXZpY2Vfc2V0LnYxLkNyZWF0ZURldmljZVNldFJlc3BvbnNlElcKDEdldERldmljZVNldBIiLmRldmljZV9zZXQudjEuR2V0RGV2aWNlU2V0UmVxdWVzdBojLmRldmljZV9zZXQudjEuR2V0RGV2aWNlU2V0UmVzcG9uc2USYAoPVXBkYXRlRGV2aWNlU2V0EiUuZGV2aWNlX3NldC52MS5VcGRhdGVEZXZpY2VTZXRSZXF1ZXN0GiYuZGV2aWNlX3NldC52MS5VcGRhdGVEZXZpY2VTZXRSZXNwb25zZRJgCg9EZWxldGVEZXZpY2VTZXQSJS5kZXZpY2Vfc2V0LnYxLkRlbGV0ZURldmljZVNldFJlcXVlc3QaJi5kZXZpY2Vfc2V0LnYxLkRlbGV0ZURldmljZVNldFJlc3BvbnNlEl0KDkxpc3REZXZpY2VTZXRzEiQuZGV2aWNlX3NldC52MS5MaXN0RGV2aWNlU2V0c1JlcXVlc3QaJS5kZXZpY2Vfc2V0LnYxLkxpc3REZXZpY2VTZXRzUmVzcG9uc2USZgoRQWRkRGV2aWNlc1RvR3JvdXASJy5kZXZpY2Vfc2V0LnYxLkFkZERldmljZXNUb0dyb3VwUmVxdWVzdBooLmRldmljZV9zZXQudjEuQWRkRGV2aWNlc1RvR3JvdXBSZXNwb25zZRJ1ChZSZW1vdmVEZXZpY2VzRnJvbUdyb3VwEiwuZGV2aWNlX3NldC52MS5SZW1vdmVEZXZpY2VzRnJvbUdyb3VwUmVxdWVzdBotLmRldmljZV9zZXQudjEuUmVtb3ZlRGV2aWNlc0Zyb21Hcm91cFJlc3BvbnNlEm8KFExpc3REZXZpY2VTZXRNZW1iZXJzEiouZGV2aWNlX3NldC52MS5MaXN0RGV2aWNlU2V0TWVtYmVyc1JlcXVlc3QaKy5kZXZpY2Vfc2V0LnYxLkxpc3REZXZpY2VTZXRNZW1iZXJzUmVzcG9uc2USbAoTR2V0RGV2aWNlRGV2aWNlU2V0cxIpLmRldmljZV9zZXQudjEuR2V0RGV2aWNlRGV2aWNlU2V0c1JlcXVlc3QaKi5kZXZpY2Vfc2V0LnYxLkdldERldmljZURldmljZVNldHNSZXNwb25zZRJsChNTZXRSYWNrU2xvdFBvc2l0aW9uEikuZGV2aWNlX3NldC52MS5TZXRSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBoqLmRldmljZV9zZXQudjEuU2V0UmFja1Nsb3RQb3NpdGlvblJlc3BvbnNlEnIKFUNsZWFyUmFja1Nsb3RQb3NpdGlvbhIrLmRldmljZV9zZXQudjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVxdWVzdBosLmRldmljZV9zZXQudjEuQ2xlYXJSYWNrU2xvdFBvc2l0aW9uUmVzcG9uc2USVwoMR2V0UmFja1Nsb3RzEiIuZGV2aWNlX3NldC52MS5HZXRSYWNrU2xvdHNSZXF1ZXN0GiMuZGV2aWNlX3NldC52MS5HZXRSYWNrU2xvdHNSZXNwb25zZRJmChFHZXREZXZpY2VTZXRTdGF0cxInLmRldmljZV9zZXQudjEuR2V0RGV2aWNlU2V0U3RhdHNSZXF1ZXN0GiguZGV2aWNlX3NldC52MS5HZXREZXZpY2VTZXRTdGF0c1Jlc3BvbnNlEloKDUxpc3RSYWNrWm9uZXMSIy5kZXZpY2Vfc2V0LnYxLkxpc3RSYWNrWm9uZXNSZXF1ZXN0GiQuZGV2aWNlX3NldC52MS5MaXN0UmFja1pvbmVzUmVzcG9uc2USYwoQTGlzdFJhY2tab25lUmVmcxImLmRldmljZV9zZXQudjEuTGlzdFJhY2tab25lUmVmc1JlcXVlc3QaJy5kZXZpY2Vfc2V0LnYxLkxpc3RSYWNrWm9uZVJlZnNSZXNwb25zZRJaCg1MaXN0UmFja1R5cGVzEiMuZGV2aWNlX3NldC52MS5MaXN0UmFja1R5cGVzUmVxdWVzdBokLmRldmljZV9zZXQudjEuTGlzdFJhY2tUeXBlc1Jlc3BvbnNlEksKCFNhdmVSYWNrEh4uZGV2aWNlX3NldC52MS5TYXZlUmFja1JlcXVlc3QaHy5kZXZpY2Vfc2V0LnYxLlNhdmVSYWNrUmVzcG9uc2USbAoTQXNzaWduRGV2aWNlc1RvUmFjaxIpLmRldmljZV9zZXQudjEuQXNzaWduRGV2aWNlc1RvUmFja1JlcXVlc3QaKi5kZXZpY2Vfc2V0LnYxLkFzc2lnbkRldmljZXNUb1JhY2tSZXNwb25zZULDAQoRY29tLmRldmljZV9zZXQudjFCDkRldmljZVNldFByb3RvUAFaTWdpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL2RldmljZV9zZXQvdjE7ZGV2aWNlX3NldHYxogIDRFhYqgIMRGV2aWNlU2V0LlYxygIMRGV2aWNlU2V0XFYx4gIYRGV2aWNlU2V0XFYxXEdQQk1ldGFkYXRh6gINRGV2aWNlU2V0OjpWMWIGcHJvdG8z", [ file_google_protobuf_timestamp, file_buf_validate_validate, @@ -701,24 +701,24 @@ export const ListDeviceSetsResponseSchema: GenMessage = messageDesc(file_device_set_v1_device_set, 15); /** - * Request to add devices to a device set. + * Request to add devices to a group. Groups have many-to-many device + * membership: devices may already belong to other groups and to a rack, + * none of which are touched. Server rejects with InvalidArgument when + * target_group_id refers to a non-group device set. * - * When the target is a site-stamped rack, the server rewrites - * device.site_id to match the rack for every added device. The affected - * count is returned in site_reassigned_count. Groups are exempt. - * - * @generated from message device_set.v1.AddDevicesToDeviceSetRequest + * @generated from message device_set.v1.AddDevicesToGroupRequest */ -export type AddDevicesToDeviceSetRequest = Message<"device_set.v1.AddDevicesToDeviceSetRequest"> & { +export type AddDevicesToGroupRequest = Message<"device_set.v1.AddDevicesToGroupRequest"> & { /** - * ID of the device set to add devices to + * ID of the group to add devices to. Must reference a device set of + * type GROUP; racks are rejected. * - * @generated from field: int64 device_set_id = 1; + * @generated from field: int64 target_group_id = 1; */ - deviceSetId: bigint; + targetGroupId: bigint; /** - * Devices to add: specific list or all paired devices + * Devices to add: specific list or all paired devices. * * @generated from field: common.v1.DeviceSelector device_selector = 2; */ @@ -726,65 +726,54 @@ export type AddDevicesToDeviceSetRequest = Message<"device_set.v1.AddDevicesToDe }; /** - * Describes the message device_set.v1.AddDevicesToDeviceSetRequest. - * Use `create(AddDevicesToDeviceSetRequestSchema)` to create a new message. + * Describes the message device_set.v1.AddDevicesToGroupRequest. + * Use `create(AddDevicesToGroupRequestSchema)` to create a new message. */ -export const AddDevicesToDeviceSetRequestSchema: GenMessage = +export const AddDevicesToGroupRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_device_set_v1_device_set, 16); /** - * Response after adding devices to a device set + * Response after adding devices to a group. * - * @generated from message device_set.v1.AddDevicesToDeviceSetResponse + * @generated from message device_set.v1.AddDevicesToGroupResponse */ -export type AddDevicesToDeviceSetResponse = Message<"device_set.v1.AddDevicesToDeviceSetResponse"> & { - /** - * ID of the device set devices were added to - * - * @generated from field: int64 device_set_id = 1; - */ - deviceSetId: bigint; - - /** - * Number of devices successfully added - * May be less than requested if some devices were already members - * - * @generated from field: int32 added_count = 2; - */ - addedCount: number; - +export type AddDevicesToGroupResponse = Message<"device_set.v1.AddDevicesToGroupResponse"> & { /** - * Number of devices whose site_id was rewritten by the cascade. + * Number of devices successfully added. May be less than requested + * if some devices were already members. * - * @generated from field: int32 site_reassigned_count = 3; + * @generated from field: int64 added_count = 1; */ - siteReassignedCount: number; + addedCount: bigint; }; /** - * Describes the message device_set.v1.AddDevicesToDeviceSetResponse. - * Use `create(AddDevicesToDeviceSetResponseSchema)` to create a new message. + * Describes the message device_set.v1.AddDevicesToGroupResponse. + * Use `create(AddDevicesToGroupResponseSchema)` to create a new message. */ -export const AddDevicesToDeviceSetResponseSchema: GenMessage = +export const AddDevicesToGroupResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_device_set_v1_device_set, 17); /** - * Request to remove devices from a device set + * Request to remove devices from a group. Server rejects with + * InvalidArgument when target_group_id refers to a non-group device set; + * use AssignDevicesToRack to clear rack membership. * - * @generated from message device_set.v1.RemoveDevicesFromDeviceSetRequest + * @generated from message device_set.v1.RemoveDevicesFromGroupRequest */ -export type RemoveDevicesFromDeviceSetRequest = Message<"device_set.v1.RemoveDevicesFromDeviceSetRequest"> & { +export type RemoveDevicesFromGroupRequest = Message<"device_set.v1.RemoveDevicesFromGroupRequest"> & { /** - * ID of the device set to remove devices from + * ID of the group to remove devices from. Must reference a device + * set of type GROUP; racks are rejected. * - * @generated from field: int64 device_set_id = 1; + * @generated from field: int64 target_group_id = 1; */ - deviceSetId: bigint; + targetGroupId: bigint; /** - * Devices to remove: specific list or all paired devices + * Devices to remove: specific list or all paired devices. * * @generated from field: common.v1.DeviceSelector device_selector = 2; */ @@ -792,32 +781,32 @@ export type RemoveDevicesFromDeviceSetRequest = Message<"device_set.v1.RemoveDev }; /** - * Describes the message device_set.v1.RemoveDevicesFromDeviceSetRequest. - * Use `create(RemoveDevicesFromDeviceSetRequestSchema)` to create a new message. + * Describes the message device_set.v1.RemoveDevicesFromGroupRequest. + * Use `create(RemoveDevicesFromGroupRequestSchema)` to create a new message. */ -export const RemoveDevicesFromDeviceSetRequestSchema: GenMessage = +export const RemoveDevicesFromGroupRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_device_set_v1_device_set, 18); /** - * Response after removing devices from a device set + * Response after removing devices from a group. * - * @generated from message device_set.v1.RemoveDevicesFromDeviceSetResponse + * @generated from message device_set.v1.RemoveDevicesFromGroupResponse */ -export type RemoveDevicesFromDeviceSetResponse = Message<"device_set.v1.RemoveDevicesFromDeviceSetResponse"> & { +export type RemoveDevicesFromGroupResponse = Message<"device_set.v1.RemoveDevicesFromGroupResponse"> & { /** - * Number of devices successfully removed + * Number of devices successfully removed. * - * @generated from field: int32 removed_count = 1; + * @generated from field: int64 removed_count = 1; */ - removedCount: number; + removedCount: bigint; }; /** - * Describes the message device_set.v1.RemoveDevicesFromDeviceSetResponse. - * Use `create(RemoveDevicesFromDeviceSetResponseSchema)` to create a new message. + * Describes the message device_set.v1.RemoveDevicesFromGroupResponse. + * Use `create(RemoveDevicesFromGroupResponseSchema)` to create a new message. */ -export const RemoveDevicesFromDeviceSetResponseSchema: GenMessage = +export const RemoveDevicesFromGroupResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_device_set_v1_device_set, 19); @@ -1586,6 +1575,79 @@ export const SaveRackResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_device_set_v1_device_set, 43); +/** + * @generated from message device_set.v1.AssignDevicesToRackRequest + */ +export type AssignDevicesToRackRequest = Message<"device_set.v1.AssignDevicesToRackRequest"> & { + /** + * Unset = clear rack membership without re-assigning. When + * present, must be > 0. + * + * @generated from field: optional int64 target_rack_id = 1; + */ + targetRackId?: bigint | undefined; + + /** + * Devices to assign. Only the device_list variant is accepted; the + * all_devices variant is rejected with InvalidArgument because + * moving every paired device into a single rack is never the + * intended operation. Filter-based selectors are reserved for a + * future expansion. + * + * @generated from field: common.v1.DeviceSelector device_selector = 2; + */ + deviceSelector?: DeviceSelector | undefined; +}; + +/** + * Describes the message device_set.v1.AssignDevicesToRackRequest. + * Use `create(AssignDevicesToRackRequestSchema)` to create a new message. + */ +export const AssignDevicesToRackRequestSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_device_set_v1_device_set, 44); + +/** + * @generated from message device_set.v1.AssignDevicesToRackResponse + */ +export type AssignDevicesToRackResponse = Message<"device_set.v1.AssignDevicesToRackResponse"> & { + /** + * Number of devices whose rack membership now points at + * target_rack_id. When target_rack_id is unset, this is 0 and + * removed_count carries the cleared-rack count. + * + * @generated from field: int64 assigned_count = 1; + */ + assignedCount: bigint; + + /** + * Number of devices whose site_id was rewritten by the cascade + * because the target rack's site differed from the device's prior + * site. Zero on unassign and on same-site moves. + * + * @generated from field: int64 site_reassigned_count = 2; + */ + siteReassignedCount: bigint; + + /** + * Number of devices whose prior rack membership was cleared. On + * unassign (target_rack_id unset) this equals the number of devices + * that were in a rack. On a re-assign this counts the prior rack + * membership rows that were deleted before the new ones inserted. + * + * @generated from field: int64 removed_count = 3; + */ + removedCount: bigint; +}; + +/** + * Describes the message device_set.v1.AssignDevicesToRackResponse. + * Use `create(AssignDevicesToRackResponseSchema)` to create a new message. + */ +export const AssignDevicesToRackResponseSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_device_set_v1_device_set, 45); + /** * Type of device set * @@ -1785,24 +1847,27 @@ export const DeviceSetService: GenService<{ output: typeof ListDeviceSetsResponseSchema; }; /** - * Adds devices to a device set + * Adds devices to a group (many-to-many). Rejects non-group targets + * with InvalidArgument; use AssignDevicesToRack for racks. * - * @generated from rpc device_set.v1.DeviceSetService.AddDevicesToDeviceSet + * @generated from rpc device_set.v1.DeviceSetService.AddDevicesToGroup */ - addDevicesToDeviceSet: { + addDevicesToGroup: { methodKind: "unary"; - input: typeof AddDevicesToDeviceSetRequestSchema; - output: typeof AddDevicesToDeviceSetResponseSchema; + input: typeof AddDevicesToGroupRequestSchema; + output: typeof AddDevicesToGroupResponseSchema; }; /** - * Removes devices from a device set + * Removes devices from a group. Rejects non-group targets with + * InvalidArgument; rack membership is cleared via + * AssignDevicesToRack with target_rack_id unset. * - * @generated from rpc device_set.v1.DeviceSetService.RemoveDevicesFromDeviceSet + * @generated from rpc device_set.v1.DeviceSetService.RemoveDevicesFromGroup */ - removeDevicesFromDeviceSet: { + removeDevicesFromGroup: { methodKind: "unary"; - input: typeof RemoveDevicesFromDeviceSetRequestSchema; - output: typeof RemoveDevicesFromDeviceSetResponseSchema; + input: typeof RemoveDevicesFromGroupRequestSchema; + output: typeof RemoveDevicesFromGroupResponseSchema; }; /** * Lists members of a device set @@ -1910,4 +1975,29 @@ export const DeviceSetService: GenService<{ input: typeof SaveRackRequestSchema; output: typeof SaveRackResponseSchema; }; + /** + * AssignDevicesToRack atomically moves devices into the target rack + * in one transaction: removes any existing rack membership for each + * device, inserts new membership at target_rack_id, and cascades + * device.site_id when the target rack's site differs from the + * device's current site. target_rack_id unset clears rack + * membership without re-assigning (site/building stay intact). + * + * Closes the orphan window where a client-side remove + add pair + * could leave devices without rack membership on transport failure + * between the two calls, and the cross-rack rename race where the + * source rack's id resolution drifts between the resolve and the + * remove. + * + * device_selector accepts only the device_list variant. all_devices + * is rejected with InvalidArgument because moving every paired + * device into a single rack is never the intended operation. + * + * @generated from rpc device_set.v1.DeviceSetService.AssignDevicesToRack + */ + assignDevicesToRack: { + methodKind: "unary"; + input: typeof AssignDevicesToRackRequestSchema; + output: typeof AssignDevicesToRackResponseSchema; + }; }> = /*@__PURE__*/ serviceDesc(file_device_set_v1_device_set, 0); diff --git a/client/src/protoFleet/api/generated/sites/v1/sites_pb.ts b/client/src/protoFleet/api/generated/sites/v1/sites_pb.ts index 3f1bd2c01..68ca9a270 100644 --- a/client/src/protoFleet/api/generated/sites/v1/sites_pb.ts +++ b/client/src/protoFleet/api/generated/sites/v1/sites_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; export const file_sites_v1_sites: GenFile = /*@__PURE__*/ fileDesc( - "ChRzaXRlcy92MS9zaXRlcy5wcm90bxIIc2l0ZXMudjEi4gIKBFNpdGUSCgoCaWQYASABKAMSDAoEbmFtZRgCIAEoCRIVCg1sb2NhdGlvbl9jaXR5GAQgASgJEhYKDmxvY2F0aW9uX3N0YXRlGAUgASgJEhAKCHRpbWV6b25lGAYgASgJEhkKEXBvd2VyX2NhcGFjaXR5X213GAcgASgBEhYKDm5ldHdvcmtfY29uZmlnGAggASgJEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg8KB2FkZHJlc3MYCyABKAkSEwoLcG9zdGFsX2NvZGUYDSABKAkSDwoHY291bnRyeRgOIAEoCRINCgVub3RlcxgPIAEoCUoECAMQBEoECAwQDVILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIicAoOU2l0ZVdpdGhDb3VudHMSHAoEc2l0ZRgBIAEoCzIOLnNpdGVzLnYxLlNpdGUSFAoMZGV2aWNlX2NvdW50GAIgASgDEhYKDmJ1aWxkaW5nX2NvdW50GAMgASgDEhIKCnJhY2tfY291bnQYBCABKAMiEgoQTGlzdFNpdGVzUmVxdWVzdCI8ChFMaXN0U2l0ZXNSZXNwb25zZRInCgVzaXRlcxgBIAMoCzIYLnNpdGVzLnYxLlNpdGVXaXRoQ291bnRzIu0CChFDcmVhdGVTaXRlUmVxdWVzdBIYCgRuYW1lGAEgASgJQgq6SAdyBRABGP8BEh8KDWxvY2F0aW9uX2NpdHkYAyABKAlCCLpIBXIDGP8BEiAKDmxvY2F0aW9uX3N0YXRlGAQgASgJQgi6SAVyAxj/ARIZCgh0aW1lem9uZRgFIAEoCUIHukgEcgIYQBIpChFwb3dlcl9jYXBhY2l0eV9tdxgGIAEoAUIOukgLEgkpAAAAAAAAAAASIQoObmV0d29ya19jb25maWcYByABKAlCCbpIBnIEKICAARIZCgdhZGRyZXNzGAggASgJQgi6SAVyAxj/ARIcCgtwb3N0YWxfY29kZRgKIAEoCUIHukgEcgIYIBIYCgdjb3VudHJ5GAsgASgJQge6SARyAhgCEhcKBW5vdGVzGAwgASgJQgi6SAVyAxiAIEoECAIQA0oECAkQClILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIiUwoSQ3JlYXRlU2l0ZVJlc3BvbnNlEhwKBHNpdGUYASABKAsyDi5zaXRlcy52MS5TaXRlEh8KF25ldHdvcmtfY29uZmlnX3dhcm5pbmdzGAIgAygJIoIDChFVcGRhdGVTaXRlUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgABIYCgRuYW1lGAIgASgJQgq6SAdyBRABGP8BEh8KDWxvY2F0aW9uX2NpdHkYBCABKAlCCLpIBXIDGP8BEiAKDmxvY2F0aW9uX3N0YXRlGAUgASgJQgi6SAVyAxj/ARIZCgh0aW1lem9uZRgGIAEoCUIHukgEcgIYQBIpChFwb3dlcl9jYXBhY2l0eV9tdxgHIAEoAUIOukgLEgkpAAAAAAAAAAASIQoObmV0d29ya19jb25maWcYCCABKAlCCbpIBnIEKICAARIZCgdhZGRyZXNzGAkgASgJQgi6SAVyAxj/ARIcCgtwb3N0YWxfY29kZRgLIAEoCUIHukgEcgIYIBIYCgdjb3VudHJ5GAwgASgJQge6SARyAhgCEhcKBW5vdGVzGA0gASgJQgi6SAVyAxiAIEoECAMQBEoECAoQC1ILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIiUwoSVXBkYXRlU2l0ZVJlc3BvbnNlEhwKBHNpdGUYASABKAsyDi5zaXRlcy52MS5TaXRlEh8KF25ldHdvcmtfY29uZmlnX3dhcm5pbmdzGAIgAygJIigKEURlbGV0ZVNpdGVSZXF1ZXN0EhMKAmlkGAEgASgDQge6SAQiAiAAInQKEkRlbGV0ZVNpdGVSZXNwb25zZRIfChd1bmFzc2lnbmVkX2RldmljZV9jb3VudBgBIAEoAxIeChZkZWxldGVkX2J1aWxkaW5nX2NvdW50GAIgASgDEh0KFXVuYXNzaWduZWRfcmFja19jb3VudBgDIAEoAyKJAQocUmVhc3NpZ25EZXZpY2VzVG9TaXRlUmVxdWVzdBIkCg50YXJnZXRfc2l0ZV9pZBgBIAEoA0IHukgEIgIgAEgAiAEBEjAKEmRldmljZV9pZGVudGlmaWVycxgCIAMoCUIUukgRkgEOCAEQkE4iB3IFEAEYgAJCEQoPX3RhcmdldF9zaXRlX2lkIn4KEVBlckRldmljZUNvbmZsaWN0EhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEjEKBnJlYXNvbhgCIAEoDjIhLnNpdGVzLnYxLlBlckRldmljZUNvbmZsaWN0UmVhc29uEhsKE2NvbmZsaWN0aW5nX3NpdGVfaWQYAyABKAMiaQodUmVhc3NpZ25EZXZpY2VzVG9TaXRlUmVzcG9uc2USGAoQcmVhc3NpZ25lZF9jb3VudBgBIAEoAxIuCgljb25mbGljdHMYAiADKAsyGy5zaXRlcy52MS5QZXJEZXZpY2VDb25mbGljdCJ0ChtBc3NpZ25CdWlsZGluZ1RvU2l0ZVJlcXVlc3QSHAoLYnVpbGRpbmdfaWQYASABKANCB7pIBCICIAASJAoOdGFyZ2V0X3NpdGVfaWQYAiABKANCB7pIBCICIABIAIgBAUIRCg9fdGFyZ2V0X3NpdGVfaWQiXgocQXNzaWduQnVpbGRpbmdUb1NpdGVSZXNwb25zZRIdChVyZWFzc2lnbmVkX3JhY2tfY291bnQYASABKAMSHwoXcmVhc3NpZ25lZF9kZXZpY2VfY291bnQYAiABKAMiLwoTR2V0U2l0ZVN0YXRzUmVxdWVzdBIYCgdzaXRlX2lkGAEgASgDQge6SAQiAiAAIv8CChRHZXRTaXRlU3RhdHNSZXNwb25zZRIPCgdzaXRlX2lkGAEgASgDEhYKDmJ1aWxkaW5nX2NvdW50GAIgASgFEhQKDGRldmljZV9jb3VudBgDIAEoBRIXCg9yZXBvcnRpbmdfY291bnQYBCABKAUSGgoSdG90YWxfaGFzaHJhdGVfdGhzGAUgASgBEhoKEmF2Z19lZmZpY2llbmN5X2p0aBgGIAEoARIWCg50b3RhbF9wb3dlcl9rdxgHIAEoARIVCg1oYXNoaW5nX2NvdW50GAggASgFEhQKDGJyb2tlbl9jb3VudBgJIAEoBRIVCg1vZmZsaW5lX2NvdW50GAogASgFEhYKDnNsZWVwaW5nX2NvdW50GAsgASgFEiAKGGhhc2hyYXRlX3JlcG9ydGluZ19jb3VudBgMIAEoBRIiChplZmZpY2llbmN5X3JlcG9ydGluZ19jb3VudBgNIAEoBRIdChVwb3dlcl9yZXBvcnRpbmdfY291bnQYDiABKAUqswEKF1BlckRldmljZUNvbmZsaWN0UmVhc29uEioKJlBFUl9ERVZJQ0VfQ09ORkxJQ1RfUkVBU09OX1VOU1BFQ0lGSUVEEAASLworUEVSX0RFVklDRV9DT05GTElDVF9SRUFTT05fREVWSUNFX05PVF9GT1VORBABEjsKN1BFUl9ERVZJQ0VfQ09ORkxJQ1RfUkVBU09OX0RFVklDRV9JTl9SQUNLX0FUX09USEVSX1NJVEUQAjLOBAoLU2l0ZVNlcnZpY2USRAoJTGlzdFNpdGVzEhouc2l0ZXMudjEuTGlzdFNpdGVzUmVxdWVzdBobLnNpdGVzLnYxLkxpc3RTaXRlc1Jlc3BvbnNlEkcKCkNyZWF0ZVNpdGUSGy5zaXRlcy52MS5DcmVhdGVTaXRlUmVxdWVzdBocLnNpdGVzLnYxLkNyZWF0ZVNpdGVSZXNwb25zZRJHCgpVcGRhdGVTaXRlEhsuc2l0ZXMudjEuVXBkYXRlU2l0ZVJlcXVlc3QaHC5zaXRlcy52MS5VcGRhdGVTaXRlUmVzcG9uc2USRwoKRGVsZXRlU2l0ZRIbLnNpdGVzLnYxLkRlbGV0ZVNpdGVSZXF1ZXN0Ghwuc2l0ZXMudjEuRGVsZXRlU2l0ZVJlc3BvbnNlEmgKFVJlYXNzaWduRGV2aWNlc1RvU2l0ZRImLnNpdGVzLnYxLlJlYXNzaWduRGV2aWNlc1RvU2l0ZVJlcXVlc3QaJy5zaXRlcy52MS5SZWFzc2lnbkRldmljZXNUb1NpdGVSZXNwb25zZRJlChRBc3NpZ25CdWlsZGluZ1RvU2l0ZRIlLnNpdGVzLnYxLkFzc2lnbkJ1aWxkaW5nVG9TaXRlUmVxdWVzdBomLnNpdGVzLnYxLkFzc2lnbkJ1aWxkaW5nVG9TaXRlUmVzcG9uc2USTQoMR2V0U2l0ZVN0YXRzEh0uc2l0ZXMudjEuR2V0U2l0ZVN0YXRzUmVxdWVzdBoeLnNpdGVzLnYxLkdldFNpdGVTdGF0c1Jlc3BvbnNlQqABCgxjb20uc2l0ZXMudjFCClNpdGVzUHJvdG9QAVpDZ2l0aHViLmNvbS9ibG9jay9wcm90by1mbGVldC9zZXJ2ZXIvZ2VuZXJhdGVkL2dycGMvc2l0ZXMvdjE7c2l0ZXN2MaICA1NYWKoCCFNpdGVzLlYxygIIU2l0ZXNcVjHiAhRTaXRlc1xWMVxHUEJNZXRhZGF0YeoCCVNpdGVzOjpWMWIGcHJvdG8z", + "ChRzaXRlcy92MS9zaXRlcy5wcm90bxIIc2l0ZXMudjEi4gIKBFNpdGUSCgoCaWQYASABKAMSDAoEbmFtZRgCIAEoCRIVCg1sb2NhdGlvbl9jaXR5GAQgASgJEhYKDmxvY2F0aW9uX3N0YXRlGAUgASgJEhAKCHRpbWV6b25lGAYgASgJEhkKEXBvd2VyX2NhcGFjaXR5X213GAcgASgBEhYKDm5ldHdvcmtfY29uZmlnGAggASgJEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg8KB2FkZHJlc3MYCyABKAkSEwoLcG9zdGFsX2NvZGUYDSABKAkSDwoHY291bnRyeRgOIAEoCRINCgVub3RlcxgPIAEoCUoECAMQBEoECAwQDVILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIicAoOU2l0ZVdpdGhDb3VudHMSHAoEc2l0ZRgBIAEoCzIOLnNpdGVzLnYxLlNpdGUSFAoMZGV2aWNlX2NvdW50GAIgASgDEhYKDmJ1aWxkaW5nX2NvdW50GAMgASgDEhIKCnJhY2tfY291bnQYBCABKAMiEgoQTGlzdFNpdGVzUmVxdWVzdCI8ChFMaXN0U2l0ZXNSZXNwb25zZRInCgVzaXRlcxgBIAMoCzIYLnNpdGVzLnYxLlNpdGVXaXRoQ291bnRzIu0CChFDcmVhdGVTaXRlUmVxdWVzdBIYCgRuYW1lGAEgASgJQgq6SAdyBRABGP8BEh8KDWxvY2F0aW9uX2NpdHkYAyABKAlCCLpIBXIDGP8BEiAKDmxvY2F0aW9uX3N0YXRlGAQgASgJQgi6SAVyAxj/ARIZCgh0aW1lem9uZRgFIAEoCUIHukgEcgIYQBIpChFwb3dlcl9jYXBhY2l0eV9tdxgGIAEoAUIOukgLEgkpAAAAAAAAAAASIQoObmV0d29ya19jb25maWcYByABKAlCCbpIBnIEKICAARIZCgdhZGRyZXNzGAggASgJQgi6SAVyAxj/ARIcCgtwb3N0YWxfY29kZRgKIAEoCUIHukgEcgIYIBIYCgdjb3VudHJ5GAsgASgJQge6SARyAhgCEhcKBW5vdGVzGAwgASgJQgi6SAVyAxiAIEoECAIQA0oECAkQClILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIiUwoSQ3JlYXRlU2l0ZVJlc3BvbnNlEhwKBHNpdGUYASABKAsyDi5zaXRlcy52MS5TaXRlEh8KF25ldHdvcmtfY29uZmlnX3dhcm5pbmdzGAIgAygJIoIDChFVcGRhdGVTaXRlUmVxdWVzdBITCgJpZBgBIAEoA0IHukgEIgIgABIYCgRuYW1lGAIgASgJQgq6SAdyBRABGP8BEh8KDWxvY2F0aW9uX2NpdHkYBCABKAlCCLpIBXIDGP8BEiAKDmxvY2F0aW9uX3N0YXRlGAUgASgJQgi6SAVyAxj/ARIZCgh0aW1lem9uZRgGIAEoCUIHukgEcgIYQBIpChFwb3dlcl9jYXBhY2l0eV9tdxgHIAEoAUIOukgLEgkpAAAAAAAAAAASIQoObmV0d29ya19jb25maWcYCCABKAlCCbpIBnIEKICAARIZCgdhZGRyZXNzGAkgASgJQgi6SAVyAxj/ARIcCgtwb3N0YWxfY29kZRgLIAEoCUIHukgEcgIYIBIYCgdjb3VudHJ5GAwgASgJQge6SARyAhgCEhcKBW5vdGVzGA0gASgJQgi6SAVyAxiAIEoECAMQBEoECAoQC1ILZGVzY3JpcHRpb25SDWFkZHJlc3NfbGluZTIiUwoSVXBkYXRlU2l0ZVJlc3BvbnNlEhwKBHNpdGUYASABKAsyDi5zaXRlcy52MS5TaXRlEh8KF25ldHdvcmtfY29uZmlnX3dhcm5pbmdzGAIgAygJIigKEURlbGV0ZVNpdGVSZXF1ZXN0EhMKAmlkGAEgASgDQge6SAQiAiAAInQKEkRlbGV0ZVNpdGVSZXNwb25zZRIfChd1bmFzc2lnbmVkX2RldmljZV9jb3VudBgBIAEoAxIeChZkZWxldGVkX2J1aWxkaW5nX2NvdW50GAIgASgDEh0KFXVuYXNzaWduZWRfcmFja19jb3VudBgDIAEoAyLpAQoaQXNzaWduRGV2aWNlc1RvU2l0ZVJlcXVlc3QSJAoOdGFyZ2V0X3NpdGVfaWQYASABKANCB7pIBCICIABIAIgBARIwChJkZXZpY2VfaWRlbnRpZmllcnMYAiADKAlCFLpIEZIBDggBEJBOIgdyBRABGIACEjQKJ2ZvcmNlX2NsZWFyX2NvbmZsaWN0aW5nX3JhY2tfbWVtYmVyc2hpcBgDIAEoCEgBiAEBQhEKD190YXJnZXRfc2l0ZV9pZEIqCihfZm9yY2VfY2xlYXJfY29uZmxpY3RpbmdfcmFja19tZW1iZXJzaGlwIn4KEVBlckRldmljZUNvbmZsaWN0EhkKEWRldmljZV9pZGVudGlmaWVyGAEgASgJEjEKBnJlYXNvbhgCIAEoDjIhLnNpdGVzLnYxLlBlckRldmljZUNvbmZsaWN0UmVhc29uEhsKE2NvbmZsaWN0aW5nX3NpdGVfaWQYAyABKAMiZwobQXNzaWduRGV2aWNlc1RvU2l0ZVJlc3BvbnNlEhgKEHJlYXNzaWduZWRfY291bnQYASABKAMSLgoJY29uZmxpY3RzGAIgAygLMhsuc2l0ZXMudjEuUGVyRGV2aWNlQ29uZmxpY3QigAEKHEFzc2lnbkJ1aWxkaW5nc1RvU2l0ZVJlcXVlc3QSJwoMYnVpbGRpbmdfaWRzGAEgAygDQhG6SA6SAQsIARDoByIEIgIgABIkCg50YXJnZXRfc2l0ZV9pZBgCIAEoA0IHukgEIgIgAEgAiAEBQhEKD190YXJnZXRfc2l0ZV9pZCJfCh1Bc3NpZ25CdWlsZGluZ3NUb1NpdGVSZXNwb25zZRIdChVyZWFzc2lnbmVkX3JhY2tfY291bnQYASABKAMSHwoXcmVhc3NpZ25lZF9kZXZpY2VfY291bnQYAiABKAMieAoYQXNzaWduUmFja3NUb1NpdGVSZXF1ZXN0EiMKCHJhY2tfaWRzGAEgAygDQhG6SA6SAQsIARDoByIEIgIgABIkCg50YXJnZXRfc2l0ZV9pZBgCIAEoA0IHukgEIgIgAEgAiAEBQhEKD190YXJnZXRfc2l0ZV9pZCJcChlBc3NpZ25SYWNrc1RvU2l0ZVJlc3BvbnNlEh8KF3JlYXNzaWduZWRfZGV2aWNlX2NvdW50GAEgASgDEh4KFmNsZWFyZWRfYnVpbGRpbmdfY291bnQYAiABKAMiLwoTR2V0U2l0ZVN0YXRzUmVxdWVzdBIYCgdzaXRlX2lkGAEgASgDQge6SAQiAiAAIv8CChRHZXRTaXRlU3RhdHNSZXNwb25zZRIPCgdzaXRlX2lkGAEgASgDEhYKDmJ1aWxkaW5nX2NvdW50GAIgASgFEhQKDGRldmljZV9jb3VudBgDIAEoBRIXCg9yZXBvcnRpbmdfY291bnQYBCABKAUSGgoSdG90YWxfaGFzaHJhdGVfdGhzGAUgASgBEhoKEmF2Z19lZmZpY2llbmN5X2p0aBgGIAEoARIWCg50b3RhbF9wb3dlcl9rdxgHIAEoARIVCg1oYXNoaW5nX2NvdW50GAggASgFEhQKDGJyb2tlbl9jb3VudBgJIAEoBRIVCg1vZmZsaW5lX2NvdW50GAogASgFEhYKDnNsZWVwaW5nX2NvdW50GAsgASgFEiAKGGhhc2hyYXRlX3JlcG9ydGluZ19jb3VudBgMIAEoBRIiChplZmZpY2llbmN5X3JlcG9ydGluZ19jb3VudBgNIAEoBRIdChVwb3dlcl9yZXBvcnRpbmdfY291bnQYDiABKAUqswEKF1BlckRldmljZUNvbmZsaWN0UmVhc29uEioKJlBFUl9ERVZJQ0VfQ09ORkxJQ1RfUkVBU09OX1VOU1BFQ0lGSUVEEAASLworUEVSX0RFVklDRV9DT05GTElDVF9SRUFTT05fREVWSUNFX05PVF9GT1VORBABEjsKN1BFUl9ERVZJQ0VfQ09ORkxJQ1RfUkVBU09OX0RFVklDRV9JTl9SQUNLX0FUX09USEVSX1NJVEUQAjKpBQoLU2l0ZVNlcnZpY2USRAoJTGlzdFNpdGVzEhouc2l0ZXMudjEuTGlzdFNpdGVzUmVxdWVzdBobLnNpdGVzLnYxLkxpc3RTaXRlc1Jlc3BvbnNlEkcKCkNyZWF0ZVNpdGUSGy5zaXRlcy52MS5DcmVhdGVTaXRlUmVxdWVzdBocLnNpdGVzLnYxLkNyZWF0ZVNpdGVSZXNwb25zZRJHCgpVcGRhdGVTaXRlEhsuc2l0ZXMudjEuVXBkYXRlU2l0ZVJlcXVlc3QaHC5zaXRlcy52MS5VcGRhdGVTaXRlUmVzcG9uc2USRwoKRGVsZXRlU2l0ZRIbLnNpdGVzLnYxLkRlbGV0ZVNpdGVSZXF1ZXN0Ghwuc2l0ZXMudjEuRGVsZXRlU2l0ZVJlc3BvbnNlEmIKE0Fzc2lnbkRldmljZXNUb1NpdGUSJC5zaXRlcy52MS5Bc3NpZ25EZXZpY2VzVG9TaXRlUmVxdWVzdBolLnNpdGVzLnYxLkFzc2lnbkRldmljZXNUb1NpdGVSZXNwb25zZRJoChVBc3NpZ25CdWlsZGluZ3NUb1NpdGUSJi5zaXRlcy52MS5Bc3NpZ25CdWlsZGluZ3NUb1NpdGVSZXF1ZXN0Gicuc2l0ZXMudjEuQXNzaWduQnVpbGRpbmdzVG9TaXRlUmVzcG9uc2USXAoRQXNzaWduUmFja3NUb1NpdGUSIi5zaXRlcy52MS5Bc3NpZ25SYWNrc1RvU2l0ZVJlcXVlc3QaIy5zaXRlcy52MS5Bc3NpZ25SYWNrc1RvU2l0ZVJlc3BvbnNlEk0KDEdldFNpdGVTdGF0cxIdLnNpdGVzLnYxLkdldFNpdGVTdGF0c1JlcXVlc3QaHi5zaXRlcy52MS5HZXRTaXRlU3RhdHNSZXNwb25zZUKgAQoMY29tLnNpdGVzLnYxQgpTaXRlc1Byb3RvUAFaQ2dpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL3NpdGVzL3YxO3NpdGVzdjGiAgNTWFiqAghTaXRlcy5WMcoCCFNpdGVzXFYx4gIUU2l0ZXNcVjFcR1BCTWV0YWRhdGHqAglTaXRlczo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_protobuf_timestamp], ); @@ -414,9 +414,9 @@ export const DeleteSiteResponseSchema: GenMessage = messageDesc(file_sites_v1_sites, 9); /** - * @generated from message sites.v1.ReassignDevicesToSiteRequest + * @generated from message sites.v1.AssignDevicesToSiteRequest */ -export type ReassignDevicesToSiteRequest = Message<"sites.v1.ReassignDevicesToSiteRequest"> & { +export type AssignDevicesToSiteRequest = Message<"sites.v1.AssignDevicesToSiteRequest"> & { /** * Unset = move to the "Unassigned" bucket. When present, must be > 0. * @@ -428,19 +428,36 @@ export type ReassignDevicesToSiteRequest = Message<"sites.v1.ReassignDevicesToSi * @generated from field: repeated string device_identifiers = 2; */ deviceIdentifiers: string[]; + + /** + * When true, the server — inside the same transaction as the site + * write — removes any conflicting rack memberships for the listed + * devices before applying the site assignment. This closes the + * cross-site reparent orphan window the client-side + * remove-then-assign loop in MinerReparentPicker had: a transport + * failure between the per-rack unassign and the site reassign + * would leave miners rack-less but still pinned to the old site. + * + * When false (default), preserves today's behavior: any device + * currently in a rack at a different site rejects the whole batch + * with a PerDeviceConflict[] of DEVICE_IN_RACK_AT_OTHER_SITE. + * + * @generated from field: optional bool force_clear_conflicting_rack_membership = 3; + */ + forceClearConflictingRackMembership?: boolean | undefined; }; /** - * Describes the message sites.v1.ReassignDevicesToSiteRequest. - * Use `create(ReassignDevicesToSiteRequestSchema)` to create a new message. + * Describes the message sites.v1.AssignDevicesToSiteRequest. + * Use `create(AssignDevicesToSiteRequestSchema)` to create a new message. */ -export const ReassignDevicesToSiteRequestSchema: GenMessage = +export const AssignDevicesToSiteRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sites_v1_sites, 10); /** * PerDeviceConflict carries the per-device error details surfaced - * when ReassignDevicesToSite rejects. The whole batch is rejected; + * when AssignDevicesToSite rejects. The whole batch is rejected; * no rows are mutated. * * @generated from message sites.v1.PerDeviceConflict @@ -474,9 +491,9 @@ export const PerDeviceConflictSchema: GenMessage = messageDesc(file_sites_v1_sites, 11); /** - * @generated from message sites.v1.ReassignDevicesToSiteResponse + * @generated from message sites.v1.AssignDevicesToSiteResponse */ -export type ReassignDevicesToSiteResponse = Message<"sites.v1.ReassignDevicesToSiteResponse"> & { +export type AssignDevicesToSiteResponse = Message<"sites.v1.AssignDevicesToSiteResponse"> & { /** * @generated from field: int64 reassigned_count = 1; */ @@ -491,24 +508,24 @@ export type ReassignDevicesToSiteResponse = Message<"sites.v1.ReassignDevicesToS }; /** - * Describes the message sites.v1.ReassignDevicesToSiteResponse. - * Use `create(ReassignDevicesToSiteResponseSchema)` to create a new message. + * Describes the message sites.v1.AssignDevicesToSiteResponse. + * Use `create(AssignDevicesToSiteResponseSchema)` to create a new message. */ -export const ReassignDevicesToSiteResponseSchema: GenMessage = +export const AssignDevicesToSiteResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sites_v1_sites, 12); /** - * @generated from message sites.v1.AssignBuildingToSiteRequest + * @generated from message sites.v1.AssignBuildingsToSiteRequest */ -export type AssignBuildingToSiteRequest = Message<"sites.v1.AssignBuildingToSiteRequest"> & { +export type AssignBuildingsToSiteRequest = Message<"sites.v1.AssignBuildingsToSiteRequest"> & { /** - * @generated from field: int64 building_id = 1; + * @generated from field: repeated int64 building_ids = 1; */ - buildingId: bigint; + buildingIds: bigint[]; /** - * Unset = move building to "Unassigned". When present, must be > 0. + * Unset = move buildings to "Unassigned". When present, must be > 0. * * @generated from field: optional int64 target_site_id = 2; */ @@ -516,20 +533,21 @@ export type AssignBuildingToSiteRequest = Message<"sites.v1.AssignBuildingToSite }; /** - * Describes the message sites.v1.AssignBuildingToSiteRequest. - * Use `create(AssignBuildingToSiteRequestSchema)` to create a new message. + * Describes the message sites.v1.AssignBuildingsToSiteRequest. + * Use `create(AssignBuildingsToSiteRequestSchema)` to create a new message. */ -export const AssignBuildingToSiteRequestSchema: GenMessage = +export const AssignBuildingsToSiteRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sites_v1_sites, 13); /** - * @generated from message sites.v1.AssignBuildingToSiteResponse + * @generated from message sites.v1.AssignBuildingsToSiteResponse */ -export type AssignBuildingToSiteResponse = Message<"sites.v1.AssignBuildingToSiteResponse"> & { +export type AssignBuildingsToSiteResponse = Message<"sites.v1.AssignBuildingsToSiteResponse"> & { /** * Cascade impact: how many racks and devices were re-stamped with - * the new site_id in the same transaction. + * the new site_id in the same transaction, aggregated across every + * building in the batch. * * @generated from field: int64 reassigned_rack_count = 1; */ @@ -542,13 +560,69 @@ export type AssignBuildingToSiteResponse = Message<"sites.v1.AssignBuildingToSit }; /** - * Describes the message sites.v1.AssignBuildingToSiteResponse. - * Use `create(AssignBuildingToSiteResponseSchema)` to create a new message. + * Describes the message sites.v1.AssignBuildingsToSiteResponse. + * Use `create(AssignBuildingsToSiteResponseSchema)` to create a new message. */ -export const AssignBuildingToSiteResponseSchema: GenMessage = +export const AssignBuildingsToSiteResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sites_v1_sites, 14); +/** + * @generated from message sites.v1.AssignRacksToSiteRequest + */ +export type AssignRacksToSiteRequest = Message<"sites.v1.AssignRacksToSiteRequest"> & { + /** + * @generated from field: repeated int64 rack_ids = 1; + */ + rackIds: bigint[]; + + /** + * Unset = move racks to "Unassigned" (site_id NULL). When present, + * must be > 0. + * + * @generated from field: optional int64 target_site_id = 2; + */ + targetSiteId?: bigint | undefined; +}; + +/** + * Describes the message sites.v1.AssignRacksToSiteRequest. + * Use `create(AssignRacksToSiteRequestSchema)` to create a new message. + */ +export const AssignRacksToSiteRequestSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_sites_v1_sites, 15); + +/** + * @generated from message sites.v1.AssignRacksToSiteResponse + */ +export type AssignRacksToSiteResponse = Message<"sites.v1.AssignRacksToSiteResponse"> & { + /** + * Cascade impact: how many devices had their site_id re-stamped + * across every rack in the batch. + * + * @generated from field: int64 reassigned_device_count = 1; + */ + reassignedDeviceCount: bigint; + + /** + * Number of racks whose building_id was cleared because the move + * crossed site boundaries. UI can use this to prompt for + * re-assignment to a building in the new site. + * + * @generated from field: int64 cleared_building_count = 2; + */ + clearedBuildingCount: bigint; +}; + +/** + * Describes the message sites.v1.AssignRacksToSiteResponse. + * Use `create(AssignRacksToSiteResponseSchema)` to create a new message. + */ +export const AssignRacksToSiteResponseSchema: GenMessage = + /*@__PURE__*/ + messageDesc(file_sites_v1_sites, 16); + /** * @generated from message sites.v1.GetSiteStatsRequest */ @@ -565,7 +639,7 @@ export type GetSiteStatsRequest = Message<"sites.v1.GetSiteStatsRequest"> & { */ export const GetSiteStatsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sites_v1_sites, 15); + messageDesc(file_sites_v1_sites, 17); /** * GetSiteStatsResponse mirrors device_set.v1.DeviceSetStats for the @@ -661,11 +735,11 @@ export type GetSiteStatsResponse = Message<"sites.v1.GetSiteStatsResponse"> & { */ export const GetSiteStatsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sites_v1_sites, 16); + messageDesc(file_sites_v1_sites, 18); /** * PerDeviceConflictReason enumerates the reasons a device can be - * rejected by ReassignDevicesToSite. + * rejected by AssignDevicesToSite. * * @generated from enum sites.v1.PerDeviceConflictReason */ @@ -751,31 +825,50 @@ export const SiteService: GenService<{ output: typeof DeleteSiteResponseSchema; }; /** - * ReassignDevicesToSite is an all-or-nothing bulk update of + * AssignDevicesToSite is an all-or-nothing bulk update of * device.site_id. If any device is in a rack whose site_id differs * from the target, the entire batch rejects with per-device error * details and no row is touched. Omit target_site_id (or leave it * unset) to move devices to the "Unassigned" bucket. * - * @generated from rpc sites.v1.SiteService.ReassignDevicesToSite + * @generated from rpc sites.v1.SiteService.AssignDevicesToSite + */ + assignDevicesToSite: { + methodKind: "unary"; + input: typeof AssignDevicesToSiteRequestSchema; + output: typeof AssignDevicesToSiteResponseSchema; + }; + /** + * AssignBuildingsToSite moves one or more buildings to a target site + * (or to "Unassigned" when target_site_id is unset). All updates run + * in a single transaction; if any building fails, the batch rolls + * back. The same transaction cascades site_id down to each + * building's racks and their devices. Returns the aggregate cascade + * counts across every building in the batch. + * + * @generated from rpc sites.v1.SiteService.AssignBuildingsToSite */ - reassignDevicesToSite: { + assignBuildingsToSite: { methodKind: "unary"; - input: typeof ReassignDevicesToSiteRequestSchema; - output: typeof ReassignDevicesToSiteResponseSchema; + input: typeof AssignBuildingsToSiteRequestSchema; + output: typeof AssignBuildingsToSiteResponseSchema; }; /** - * AssignBuildingToSite moves a building to a different site - * (or to "Unassigned" when target_site_id is unset). The same - * transaction cascades site_id down to the building's racks and - * their devices. Returns the cascade counts. + * AssignRacksToSite moves one or more racks to a target site (or + * to "Unassigned" when target_site_id is unset) as a partial + * update — the rack's label, layout, members, and slot + * assignments stay untouched, only site_id changes. building_id is + * auto-cleared on any site transition since a building belongs to + * a single site; the response carries the count of racks whose + * building was cleared so the UI can prompt for re-assignment. + * Same transaction cascades device.site_id for every rack member. * - * @generated from rpc sites.v1.SiteService.AssignBuildingToSite + * @generated from rpc sites.v1.SiteService.AssignRacksToSite */ - assignBuildingToSite: { + assignRacksToSite: { methodKind: "unary"; - input: typeof AssignBuildingToSiteRequestSchema; - output: typeof AssignBuildingToSiteResponseSchema; + input: typeof AssignRacksToSiteRequestSchema; + output: typeof AssignRacksToSiteResponseSchema; }; /** * GetSiteStats returns server-rolled telemetry + miner-state counts diff --git a/client/src/protoFleet/api/sites.ts b/client/src/protoFleet/api/sites.ts index 4966645f6..fef95e25b 100644 --- a/client/src/protoFleet/api/sites.ts +++ b/client/src/protoFleet/api/sites.ts @@ -114,11 +114,17 @@ interface DeleteSiteProps { onFinally?: () => void; } -interface ReassignDevicesToSiteProps { +interface AssignDevicesToSiteProps { // Unset routes the devices to the "Unassigned" bucket; the create flow // always supplies a target so this is typically set in practice. targetSiteId?: bigint; deviceIdentifiers: string[]; + // When true, the server clears any conflicting rack memberships + // inside the same transaction as the site write. Lets cross-site + // reparent skip the client-side remove-from-rack loop and the + // orphan window it created. When false/unset the server returns + // DEVICE_IN_RACK_AT_OTHER_SITE conflicts (today's behavior). + forceClearConflictingRackMembership?: boolean; signal?: AbortSignal; onSuccess?: (reassignedCount: bigint) => void; // conflicts is populated when the server rejects the batch on @@ -128,9 +134,10 @@ interface ReassignDevicesToSiteProps { onFinally?: () => void; } -interface AssignBuildingToSiteProps { - buildingId: bigint; - // Unset moves the building to "Unassigned". +interface AssignBuildingsToSiteProps { + // Bulk-friendly. Pass a single-element array for the singular case. + buildingIds: bigint[]; + // Unset moves the buildings to "Unassigned". targetSiteId?: bigint; signal?: AbortSignal; onSuccess?: (reassignedRackCount: bigint, reassignedDeviceCount: bigint) => void; @@ -138,6 +145,24 @@ interface AssignBuildingToSiteProps { onFinally?: () => void; } +interface AssignRacksToSiteProps { + // Bulk-friendly. Pass a single-element array for the singular case. + rackIds: bigint[]; + // Unset moves the racks to "Unassigned". + targetSiteId?: bigint; + signal?: AbortSignal; + // onSuccess args: device cascade count, count of racks whose + // building was auto-cleared because the move crossed sites. + // TODO(issue-420 follow-up): consumers must surface clearedBuildingCount + // to the operator (toast or modal) — buildings belong to a single site, + // so crossing sites silently clears the rack's building. No UI consumer + // of this RPC exists yet; when one is wired, push a toast on + // clearedBuildingCount > 0 directing the operator to reassign. + onSuccess?: (reassignedDeviceCount: bigint, clearedBuildingCount: bigint) => void; + onError?: (message: string) => void; + onFinally?: () => void; +} + const useSites = () => { const { handleAuthErrors } = useAuthErrors(); @@ -267,13 +292,22 @@ const useSites = () => { [handleAuthErrors], ); - const reassignDevicesToSite = useCallback( - async ({ targetSiteId, deviceIdentifiers, signal, onSuccess, onError, onFinally }: ReassignDevicesToSiteProps) => { + const assignDevicesToSite = useCallback( + async ({ + targetSiteId, + deviceIdentifiers, + forceClearConflictingRackMembership, + signal, + onSuccess, + onError, + onFinally, + }: AssignDevicesToSiteProps) => { try { - const response = await sitesClient.reassignDevicesToSite( + const response = await sitesClient.assignDevicesToSite( { targetSiteId, deviceIdentifiers, + forceClearConflictingRackMembership, }, { signal }, ); @@ -298,12 +332,12 @@ const useSites = () => { [handleAuthErrors], ); - const assignBuildingToSite = useCallback( - async ({ buildingId, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingToSiteProps) => { + const assignBuildingsToSite = useCallback( + async ({ buildingIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingsToSiteProps) => { try { - const response = await sitesClient.assignBuildingToSite( + const response = await sitesClient.assignBuildingsToSite( { - buildingId, + buildingIds, targetSiteId, }, { signal }, @@ -325,7 +359,42 @@ const useSites = () => { [handleAuthErrors], ); - return { listSites, createSite, updateSite, deleteSite, reassignDevicesToSite, assignBuildingToSite }; + const assignRacksToSite = useCallback( + async ({ rackIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignRacksToSiteProps) => { + try { + const response = await sitesClient.assignRacksToSite( + { + rackIds, + targetSiteId, + }, + { signal }, + ); + if (signal?.aborted) return; + onSuccess?.(response.reassignedDeviceCount, response.clearedBuildingCount); + } catch (err) { + if (signal?.aborted) return; + handleAuthErrors({ + error: err, + onError: (error) => { + onError?.(getErrorMessage(error)); + }, + }); + } finally { + onFinally?.(); + } + }, + [handleAuthErrors], + ); + + return { + listSites, + createSite, + updateSite, + deleteSite, + assignDevicesToSite, + assignBuildingsToSite, + assignRacksToSite, + }; }; export { useSites }; diff --git a/client/src/protoFleet/api/useDeviceSets.ts b/client/src/protoFleet/api/useDeviceSets.ts index e7ef2603b..3ab2924f0 100644 --- a/client/src/protoFleet/api/useDeviceSets.ts +++ b/client/src/protoFleet/api/useDeviceSets.ts @@ -63,11 +63,12 @@ interface ListDeviceSetsProps { onFinally?: () => void; } -interface AddDevicesToDeviceSetProps { - deviceSetId: bigint; +interface AddDevicesToGroupProps { + targetGroupId: bigint; deviceIdentifiers?: string[]; allDevices?: boolean; - onSuccess?: (addedCount: number) => void; + signal?: AbortSignal; + onSuccess?: (addedCount: bigint) => void; onError?: (message: string) => void; onFinally?: () => void; } @@ -119,11 +120,12 @@ interface ListGroupMembersProps { onFinally?: () => void; } -interface RemoveDevicesFromDeviceSetProps { - deviceSetId: bigint; +interface RemoveDevicesFromGroupProps { + targetGroupId: bigint; deviceIdentifiers?: string[]; allDevices?: boolean; - onSuccess?: (removedCount: number) => void; + signal?: AbortSignal; + onSuccess?: (removedCount: bigint) => void; onError?: (message: string) => void; onFinally?: () => void; } @@ -165,6 +167,17 @@ interface ClearRackSlotPositionProps { onFinally?: () => void; } +interface AssignDevicesToRackProps { + // Unset clears rack membership without re-assigning (site/building + // stay intact). + targetRackId?: bigint; + deviceIdentifiers: string[]; + signal?: AbortSignal; + onSuccess?: (assignedCount: bigint, siteReassignedCount: bigint, removedCount: bigint) => void; + onError?: (message: string) => void; + onFinally?: () => void; +} + interface SaveRackProps { deviceSetId?: bigint; label: string; @@ -487,28 +500,37 @@ const useDeviceSets = () => { [handleAuthErrors], ); - const addDevicesToDeviceSet = useCallback( + // addDevicesToGroup adds devices to a group (many-to-many). The + // server rejects non-group targets with InvalidArgument; for rack + // adds use assignDevicesToRack, which atomically clears any prior + // rack membership and cascades the rack's site onto the device. + const addDevicesToGroup = useCallback( async ({ - deviceSetId, + targetGroupId, deviceIdentifiers, allDevices, + signal, onSuccess, onError, onFinally, - }: AddDevicesToDeviceSetProps) => { + }: AddDevicesToGroupProps) => { try { - const deviceSelector = - allDevices || (deviceIdentifiers && deviceIdentifiers.length > 0) - ? buildDeviceSelector(deviceIdentifiers, allDevices) - : undefined; + const deviceSelector = buildDeviceSelector(deviceIdentifiers, allDevices); - const response = await deviceSetClient.addDevicesToDeviceSet({ - deviceSetId, - deviceSelector, - }); + const response = await deviceSetClient.addDevicesToGroup( + { + targetGroupId, + deviceSelector, + }, + { signal }, + ); + if (signal?.aborted) return; onSuccess?.(response.addedCount); } catch (err) { + if (isAbortError(err, signal)) { + return; + } handleAuthErrors({ error: err, onError: () => { @@ -601,28 +623,84 @@ const useDeviceSets = () => { [handleAuthErrors], ); - const removeDevicesFromDeviceSet = useCallback( + // assignDevicesToRack wraps the atomic rack-reassignment RPC. + // Replaces the old client-side remove + add orchestration so a + // server error / network blip between the two calls can't orphan + // miners from rack assignment (issue #420). Pass targetRackId + // unset to clear rack membership without re-assigning. + const assignDevicesToRack = useCallback( + async ({ targetRackId, deviceIdentifiers, signal, onSuccess, onError, onFinally }: AssignDevicesToRackProps) => { + try { + // Server requires the device_list variant; the all_devices + // variant is rejected with InvalidArgument upstream. Always + // build the device_list selector explicitly so callers can't + // accidentally trigger that error by passing an empty array. + const deviceSelector = create(DeviceSelectorSchema, { + selectionType: { + case: "deviceList", + value: create(DeviceIdentifierListSchema, { + deviceIdentifiers, + }), + }, + }); + + const response = await deviceSetClient.assignDevicesToRack( + { + targetRackId, + deviceSelector, + }, + { signal }, + ); + if (signal?.aborted) return; + onSuccess?.(response.assignedCount, response.siteReassignedCount, response.removedCount); + } catch (err) { + if (isAbortError(err, signal)) { + return; + } + handleAuthErrors({ + error: err, + onError: () => { + onError?.(getErrorMessage(err)); + }, + }); + } finally { + onFinally?.(); + } + }, + [handleAuthErrors], + ); + + // removeDevicesFromGroup drops devices from a group. The server + // rejects non-group targets with InvalidArgument; for rack removal + // use assignDevicesToRack with targetRackId unset, which clears rack + // membership in a single transaction (site/building stay intact). + const removeDevicesFromGroup = useCallback( async ({ - deviceSetId, + targetGroupId, deviceIdentifiers, allDevices, + signal, onSuccess, onError, onFinally, - }: RemoveDevicesFromDeviceSetProps) => { + }: RemoveDevicesFromGroupProps) => { try { - const deviceSelector = - allDevices || (deviceIdentifiers && deviceIdentifiers.length > 0) - ? buildDeviceSelector(deviceIdentifiers, allDevices) - : undefined; + const deviceSelector = buildDeviceSelector(deviceIdentifiers, allDevices); - const response = await deviceSetClient.removeDevicesFromDeviceSet({ - deviceSetId, - deviceSelector, - }); + const response = await deviceSetClient.removeDevicesFromGroup( + { + targetGroupId, + deviceSelector, + }, + { signal }, + ); + if (signal?.aborted) return; onSuccess?.(response.removedCount); } catch (err) { + if (isAbortError(err, signal)) { + return; + } handleAuthErrors({ error: err, onError: () => { @@ -847,8 +925,9 @@ const useDeviceSets = () => { listRackTypes, listGroupMembers, getDeviceSetStats, - addDevicesToDeviceSet, - removeDevicesFromDeviceSet, + addDevicesToGroup, + assignDevicesToRack, + removeDevicesFromGroup, getRackSlots, setRackSlotPosition, clearRackSlotPosition, diff --git a/client/src/protoFleet/features/buildings/components/BuildingModals/BuildingModals.test.tsx b/client/src/protoFleet/features/buildings/components/BuildingModals/BuildingModals.test.tsx index b24648043..188f07046 100644 --- a/client/src/protoFleet/features/buildings/components/BuildingModals/BuildingModals.test.tsx +++ b/client/src/protoFleet/features/buildings/components/BuildingModals/BuildingModals.test.tsx @@ -21,7 +21,7 @@ const mockApi = { createBuilding: vi.fn(), updateBuilding: vi.fn(), deleteBuilding: vi.fn(), - assignRackToBuilding: vi.fn(), + assignRacksToBuilding: vi.fn(), }; vi.mock("@/protoFleet/api/buildings", async () => { const actual = await vi.importActual("@/protoFleet/api/buildings"); diff --git a/client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx b/client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx index b339ef350..0d3fed39d 100644 --- a/client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx +++ b/client/src/protoFleet/features/buildings/components/ManageBuildingModal/ManageBuildingModal.tsx @@ -6,7 +6,7 @@ import { type AssignmentEntry, buildByNameAssignments, buildManualAssignments } import BuildingGridPane from "./BuildingGridPane"; import BuildingRacksPane, { type AssignedRackRow } from "./BuildingRacksPane"; import { type BuildingAssignmentMode, type GridCellKey, parseCellKey } from "./types"; -import { useBuildings } from "@/protoFleet/api/buildings"; +import { type RackPlacementInput, useBuildings } from "@/protoFleet/api/buildings"; import { type Building, type BuildingRack } from "@/protoFleet/api/generated/buildings/v1/buildings_pb"; import FullScreenTwoPaneModal from "@/protoFleet/components/FullScreenTwoPaneModal"; import { DismissCircle } from "@/shared/assets/icons"; @@ -42,7 +42,7 @@ const ManageBuildingModal = ({ onDeleteRequested, onSaved, }: ManageBuildingModalProps) => { - const { listBuildingRacks, assignRackToBuilding } = useBuildings(); + const { listBuildingRacks, assignRacksToBuilding } = useBuildings(); // Aisles / racks_per_aisle are read straight from the building prop — // BuildingSettingsModal owns those fields now and threads any edits back @@ -76,10 +76,16 @@ const ManageBuildingModal = ({ const [errorMsg, setErrorMsg] = useState(""); // Snapshot of the server's positions at load time so Save only fires - // assignRackToBuilding for racks whose position actually changed. Keyed + // assignRacksToBuilding for racks whose position actually changed. Keyed // by rackId → "aisle:position" (or "unplaced") so we can string-compare. const initialPlacementRef = useRef>(new Map()); + // Synchronous in-flight guard for Save dispatches. setState batching + // means the `isSaving` prop driving the button's `disabled` lags one + // render behind the click — a double-click would otherwise reach the + // dispatch path twice. Mirrors useSiteModals' savingRef pattern. + const savingRef = useRef(false); + // (Re)load assignments when the modal opens. useEffect(() => { if (!open) return; @@ -331,33 +337,32 @@ const ManageBuildingModal = ({ ); // Save: walk activeAssignments, diff against the load-time snapshot, and - // fire AssignRackToBuilding in two phases: - // - // Phase 1 (vacate): every rack whose placement is changing AND that was - // previously placed sends a "clear position" write first (same building, - // no aisle/position). Removed-from-building racks also unassign here so - // their cells free up before phase 2 lands. This frees every (building, - // aisle, position) tuple that will be re-used in phase 2 BEFORE any - // placement write runs. + // fire AssignRacksToBuilding once per target building bucket. // - // Phase 2 (place): every rack with a new placement target writes its - // final (aisle, position). Because phase 1 already cleared every cell - // that's about to be re-used, swaps and "move into occupied cell" cases - // no longer collide on the uk_device_set_rack_building_position partial - // unique index. + // All racks staying in this building (placements, unplacements, + // swaps, "move into occupied cell") ship as a single mixed batch. + // The server's AssignRacksToBuilding transaction now runs a two-pass + // write internally — pass 1 clears every requested rack's cell, then + // pass 2 writes the new (aisle, position) values — so the partial + // unique index uk_device_set_rack_building_position can't collide + // mid-batch. That removes the old client-side vacate-then-place + // split (and its "retry to finish saving" partial-failure path). // - // Per-rack writes within a phase still run concurrently via Promise.all — - // serialization is *between* phases, not within. Layout writes live in - // BuildingSettingsModal. + // Racks removed from this building go in a second call with + // targetBuildingId=undefined since they need a different building + // bucket. Layout writes live in BuildingSettingsModal. const handleSave = useCallback(async () => { + if (savingRef.current) return; + savingRef.current = true; setErrorMsg(""); setIsSaving(true); try { const initial = initialPlacementRef.current; - const vacates: Promise[] = []; - const places: Promise[] = []; const currentIds = new Set(entries.map((e) => e.rackId.toString())); + const inBuilding: RackPlacementInput[] = []; + const unassign: RackPlacementInput[] = []; + for (const entry of entries) { const idStr = entry.rackId.toString(); const placedKey = rackToCell.get(idStr); @@ -370,95 +375,66 @@ const ManageBuildingModal = ({ const prior = initial.get(idStr) ?? "missing"; if (prior === next) continue; - // If the rack was previously placed, vacate its old cell first. - // Sending the same building with no aisle/position clears position - // without touching membership. - if (prior !== "unplaced" && prior !== "missing") { - vacates.push( - new Promise((resolve, reject) => { - void assignRackToBuilding({ - rackId: entry.rackId, - buildingId: building.id, - onSuccess: () => resolve(), - onError: (msg) => reject(new Error(msg)), - }); - }), - ); - } - - // Phase 2: write the new state. + // Single mixed batch. // - placedKey present → place at the new (aisle, position). + // Covers both first-time placement and moves; the server's + // pass-1 clear handles any prior occupant inside the batch. + // - placedKey absent + prior previously placed → send a + // member-only entry. The server NULLs the rack's cell in + // pass 1 (no pass-2 write because no position is supplied). // - placedKey absent + prior === "missing" → rack is new to // the working set with no chosen cell yet. Send a member- // only assign so the BE links the rack to this building // even without a position. Without this branch, racks // added via Manage racks but never dragged to a cell // silently drop on save. - // - placedKey absent + prior was placed → already handled - // by the phase-1 vacate above, which clears the cell. if (placedKey) { const { aisle, position } = parseCellKey(placedKey); - places.push( - new Promise((resolve, reject) => { - void assignRackToBuilding({ - rackId: entry.rackId, - buildingId: building.id, - aisleIndex: aisle, - positionInAisle: position, - onSuccess: () => resolve(), - onError: (msg) => reject(new Error(msg)), - }); - }), - ); - } else if (prior === "missing") { - places.push( - new Promise((resolve, reject) => { - void assignRackToBuilding({ - rackId: entry.rackId, - buildingId: building.id, - onSuccess: () => resolve(), - onError: (msg) => reject(new Error(msg)), - }); - }), - ); + inBuilding.push({ + rackId: entry.rackId, + aisleIndex: aisle, + positionInAisle: position, + }); + } else { + inBuilding.push({ rackId: entry.rackId }); } } - // Racks removed from this building (in snapshot, not in entries) need - // an explicit unassign. Land them in phase 1 so their cells free up - // before any phase-2 placement targets them. + // Racks removed from this building (in snapshot, not in entries) + // need an explicit unassign — different target building bucket so + // they can't ride the in-building batch. for (const idStr of initial.keys()) { if (currentIds.has(idStr)) continue; - vacates.push( - new Promise((resolve, reject) => { - void assignRackToBuilding({ - rackId: BigInt(idStr), - buildingId: undefined, - onSuccess: () => resolve(), - onError: (msg) => reject(new Error(msg)), - }); - }), - ); + unassign.push({ rackId: BigInt(idStr) }); } - try { - await Promise.all(vacates); - } catch (err) { - setErrorMsg( - err instanceof Error - ? `Failed to clear previous rack positions: ${err.message}. No new positions were written — retry to apply your changes.` - : "Failed to clear previous rack positions.", - ); - return; - } + const dispatch = (racks: RackPlacementInput[], targetBuildingId?: bigint) => + new Promise((resolve, reject) => { + if (racks.length === 0) { + resolve(); + return; + } + void assignRacksToBuilding({ + racks, + targetBuildingId, + onSuccess: () => resolve(), + onError: (msg) => reject(new Error(msg)), + }); + }); try { - await Promise.all(places); + // Serialize: unassign (vacate) before in-building (claim). + // Each AssignRacksToBuilding call has internal clear-before- + // place ordering inside its tx, but that doesn't protect + // across two separate calls. If rack A is being removed and + // rack B is being placed at A's former cell, the in-building + // call can hit the partial unique index before the unassign + // commits. dispatch short-circuits when the list is empty. + await dispatch(unassign, undefined); + await dispatch(inBuilding, building.id); } catch (err) { setErrorMsg( - err instanceof Error - ? `Failed to apply new rack positions: ${err.message}. Some cells may now be empty — retry to finish saving.` - : "Failed to apply new rack positions.", + err instanceof Error ? `Failed to save rack positions: ${err.message}.` : "Failed to save rack positions.", ); return; } @@ -467,9 +443,10 @@ const ManageBuildingModal = ({ onSaved?.(building); onDismiss(); } finally { + savingRef.current = false; setIsSaving(false); } - }, [building, rackToCell, entries, assignRackToBuilding, onSaved, onDismiss]); + }, [building, rackToCell, entries, assignRacksToBuilding, onSaved, onDismiss]); if (!open) return null; diff --git a/client/src/protoFleet/features/buildings/components/ManageBuildingModal/types.ts b/client/src/protoFleet/features/buildings/components/ManageBuildingModal/types.ts index 7ce918194..d992504a9 100644 --- a/client/src/protoFleet/features/buildings/components/ManageBuildingModal/types.ts +++ b/client/src/protoFleet/features/buildings/components/ManageBuildingModal/types.ts @@ -16,7 +16,7 @@ export const parseCellKey = (key: GridCellKey): { aisle: number; position: numbe const [aisle, position] = key.split("-").map(Number); // All callers in this feature produce keys via cellKey() so this // guard is belt-and-suspenders — a NaN coordinate would silently - // flow into AssignRackToBuilding's aisle_index / position_in_aisle + // flow into AssignRacksToBuilding's aisle_index / position_in_aisle // params and surface as a server-side InvalidArgument far from the // cause. if (!Number.isFinite(aisle) || !Number.isFinite(position)) { diff --git a/client/src/protoFleet/features/fleetManagement/components/FleetLayout/FleetLayout.test.tsx b/client/src/protoFleet/features/fleetManagement/components/FleetLayout/FleetLayout.test.tsx index 85f0f45e4..37d045553 100644 --- a/client/src/protoFleet/features/fleetManagement/components/FleetLayout/FleetLayout.test.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/FleetLayout/FleetLayout.test.tsx @@ -30,7 +30,7 @@ vi.mock("@/protoFleet/api/sites", async (importOriginal) => { createSite: vi.fn(), updateSite: vi.fn(), deleteSite: vi.fn(), - reassignDevicesToSite: vi.fn(), + assignDevicesToSite: vi.fn(), }), }; }); diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx index f16bbaf3e..f603cbee8 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerActionModalStack.tsx @@ -31,7 +31,7 @@ const MinerActionModalStack = ({ displayCount, onActionBoundary, }: MinerActionModalStackProps) => { - const { addDevicesToDeviceSet, createGroup } = useDeviceSets(); + const { addDevicesToGroup, createGroup } = useDeviceSets(); const wrap = useCallback( (handler: (...args: Args) => void) => onActionBoundary @@ -54,8 +54,8 @@ const MinerActionModalStack = ({ const dispatchAddToGroup = useCallback( (groupId: bigint) => new Promise((resolve, reject) => { - void addDevicesToDeviceSet({ - deviceSetId: groupId, + void addDevicesToGroup({ + targetGroupId: groupId, deviceIdentifiers, allDevices, onSuccess: () => { @@ -71,7 +71,7 @@ const MinerActionModalStack = ({ }, }); }), - [addDevicesToDeviceSet, deviceIdentifiers, allDevices, sourceLabel], + [addDevicesToGroup, deviceIdentifiers, allDevices, sourceLabel], ); const handleAddToGroupConfirm = useCallback( diff --git a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerReparentPicker.tsx b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerReparentPicker.tsx index d8a3aa632..25ebc5bed 100644 --- a/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerReparentPicker.tsx +++ b/client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/MinerReparentPicker.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { create } from "@bufbuild/protobuf"; import { fleetManagementClient } from "@/protoFleet/api/clients"; @@ -62,17 +62,21 @@ const rackOverflowMessage = (rack: DeviceSet, currentMembers: Set, ids: // Paginate listMinerStateSnapshots filtered to the target rack so the // capacity guard can discount ids that are already members. Capped at // MAX_MINERS for the same reason as resolveAllModeIds. -const resolveRackMembers = async (rackId: bigint): Promise> => { +const resolveRackMembers = async (rackId: bigint, signal?: AbortSignal): Promise> => { const filter = create(MinerListFilterSchema, { rackIds: [rackId] }); const members = new Set(); let cursor = ""; let exhausted = false; for (let i = 0; i < MAX_SNAPSHOT_PAGES; i++) { - const response = await fleetManagementClient.listMinerStateSnapshots({ - pageSize: SNAPSHOT_PAGE_SIZE, - cursor, - filter, - }); + const response = await fleetManagementClient.listMinerStateSnapshots( + { + pageSize: SNAPSHOT_PAGE_SIZE, + cursor, + filter, + }, + { signal }, + ); + if (signal?.aborted) return members; for (const miner of response.miners) members.add(miner.deviceIdentifier); if (!response.cursor) { exhausted = true; @@ -86,32 +90,6 @@ const resolveRackMembers = async (rackId: bigint): Promise> => { return members; }; -// Group ids that need to move out of a source rack before they can be -// added to the target. `idx_one_rack_per_device` enforces a single -// rack per miner, so a same-batch INSERT against a different rack -// aborts the whole transaction; we orchestrate the remove→add ourselves. -const groupBySourceRack = ( - ids: string[], - miners: Record | undefined, - targetRack: DeviceSet, - currentMembers: Set, -): Map => { - const movesByLabel = new Map(); - if (!miners) return movesByLabel; - const targetLabel = targetRack.label; - for (const id of ids) { - if (currentMembers.has(id)) continue; - const snapshot = miners[id]; - if (!snapshot) continue; - const sourceLabel = snapshot.rackLabel; - if (!sourceLabel || sourceLabel === targetLabel) continue; - const bucket = movesByLabel.get(sourceLabel) ?? []; - bucket.push(id); - movesByLabel.set(sourceLabel, bucket); - } - return movesByLabel; -}; - // Detect miners whose current rack lives at a different site than the // target. `ReassignDevicesToSite` rejects these with // `DEVICE_IN_RACK_AT_OTHER_SITE` and aborts the whole batch; we @@ -142,17 +120,22 @@ const groupRackSiteConflicts = ( const resolveAllModeIds = async ( filter: MinerListFilter, + signal?: AbortSignal, ): Promise<{ ids: string[]; snapshots: Record }> => { const ids: string[] = []; const snapshots: Record = {}; let cursor = ""; let exhausted = false; for (let i = 0; i < MAX_SNAPSHOT_PAGES; i++) { - const response = await fleetManagementClient.listMinerStateSnapshots({ - pageSize: SNAPSHOT_PAGE_SIZE, - cursor, - filter, - }); + const response = await fleetManagementClient.listMinerStateSnapshots( + { + pageSize: SNAPSHOT_PAGE_SIZE, + cursor, + filter, + }, + { signal }, + ); + if (signal?.aborted) return { ids, snapshots }; for (const miner of response.miners) { ids.push(miner.deviceIdentifier); snapshots[miner.deviceIdentifier] = miner; @@ -188,8 +171,8 @@ const MinerReparentPicker = ({ onClose, onRefetchMiners, }: MinerReparentPickerProps) => { - const { reassignDevicesToSite } = useSites(); - const { addDevicesToDeviceSet, getDeviceSet, listRacks, removeDevicesFromDeviceSet } = useDeviceSets(); + const { assignDevicesToSite } = useSites(); + const { assignDevicesToRack, getDeviceSet, listRacks } = useDeviceSets(); const [siteMoveConfirmation, setSiteMoveConfirmation] = useState(null); const [siteMoveInFlight, setSiteMoveInFlight] = useState(false); // Resolver for the onConfirm promise during the cross-site confirm @@ -198,6 +181,19 @@ const MinerReparentPicker = ({ // → onClose) after the operator picks Continue or Cancel. const dialogResolveRef = useRef<(() => void) | null>(null); + // Abort in-flight snapshot pagination and bulk RPCs on unmount so a + // long-running resolveAllModeIds / resolveRackMembers / dispatch + // loop doesn't keep firing after the operator dismisses the picker. + const abortRef = useRef(null); + if (abortRef.current === null) { + abortRef.current = new AbortController(); + } + useEffect(() => { + return () => { + abortRef.current?.abort(); + }; + }, []); + const fetchAllRacks = () => new Promise((resolve, reject) => { void listRacks({ @@ -206,16 +202,6 @@ const MinerReparentPicker = ({ }); }); - const removeFromRack = (rackId: bigint, ids: string[]) => - new Promise((resolve, reject) => { - void removeDevicesFromDeviceSet({ - deviceSetId: rackId, - deviceIdentifiers: ids, - onSuccess: () => resolve(), - onError: (msg) => reject(new Error(msg)), - }); - }); - const fetchRack = (rackId: bigint) => new Promise((resolve, reject) => { void getDeviceSet({ @@ -226,40 +212,39 @@ const MinerReparentPicker = ({ }); }); - const dispatchSiteReassign = (targetSiteId: bigint, ids: string[]) => { - void reassignDevicesToSite({ - targetSiteId, - deviceIdentifiers: ids, - onSuccess: (count) => { - pushToast({ message: successMessage(count, "site"), status: STATUSES.success }); - onRefetchMiners?.(); - }, - onError: (msg) => pushToast({ message: `Couldn't move miners: ${msg}`, status: STATUSES.error }), + const dispatchSiteReassign = (targetSiteId: bigint, ids: string[], forceClearConflictingRackMembership = false) => + new Promise((resolve) => { + void assignDevicesToSite({ + targetSiteId, + deviceIdentifiers: ids, + forceClearConflictingRackMembership, + onSuccess: (count) => { + pushToast({ message: successMessage(count, "site"), status: STATUSES.success }); + onRefetchMiners?.(); + resolve(); + }, + onError: (msg) => { + pushToast({ message: `Couldn't move miners: ${msg}`, status: STATUSES.error }); + resolve(); + }, + }); }); - }; + // Cross-site move with conflicting rack memberships: a single + // server-side transaction strips the prior rack rows and applies + // the site write. Previously this was a client-side loop of + // removeDevicesFromDeviceSet calls followed by assignDevicesToSite, + // which left a window where a transport failure between the two + // RPCs would orphan miners (rack-less but still on the old site). const dispatchSiteMoveWithUnassign = async (confirmation: SiteMoveConfirmation) => { setSiteMoveInFlight(true); const movingToast = pushToast({ - message: `Unassigning miners from ${confirmation.conflictsByLabel.size} rack${confirmation.conflictsByLabel.size === 1 ? "" : "s"}…`, + message: "Moving miners to the new site…", status: STATUSES.loading, longRunning: true, }); - try { - for (const [sourceLabel, sourceIds] of confirmation.conflictsByLabel) { - const sourceRackId = confirmation.labelToRackId.get(sourceLabel); - if (sourceRackId === undefined) continue; - await removeFromRack(sourceRackId, sourceIds); - } - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to remove miners from current rack."; - updateToast(movingToast, { message, status: STATUSES.error }); - setSiteMoveInFlight(false); - setSiteMoveConfirmation(null); - return; - } + await dispatchSiteReassign(confirmation.targetSiteId, confirmation.ids, true); removeToast(movingToast); - dispatchSiteReassign(confirmation.targetSiteId, confirmation.ids); setSiteMoveInFlight(false); setSiteMoveConfirmation(null); }; @@ -298,7 +283,7 @@ const MinerReparentPicker = ({ const conflictsByLabel = groupRackSiteConflicts(ids, minerSnapshots, labelToSiteId, targetSiteId); if (conflictsByLabel.size === 0) { - dispatchSiteReassign(targetSiteId, ids); + await dispatchSiteReassign(targetSiteId, ids); return; } @@ -311,70 +296,47 @@ const MinerReparentPicker = ({ }); }; - const dispatchReparentToRack = async ( - targetRackId: bigint, - ids: string[], - minerSnapshots: Record | undefined, - ) => { + const dispatchReparentToRack = async (targetRackId: bigint, ids: string[]) => { let rack: DeviceSet; let currentMembers: Set; try { - [rack, currentMembers] = await Promise.all([fetchRack(targetRackId), resolveRackMembers(targetRackId)]); + [rack, currentMembers] = await Promise.all([ + fetchRack(targetRackId), + resolveRackMembers(targetRackId, abortRef.current?.signal), + ]); } catch (err) { const message = err instanceof Error ? err.message : "Couldn't load rack."; pushToast({ message, status: STATUSES.error }); return; } + if (abortRef.current?.signal.aborted) return; const overflow = rackOverflowMessage(rack, currentMembers, ids); if (overflow) { pushToast({ message: overflow, status: STATUSES.error }); return; } - // Miners currently in a different rack need to leave that rack - // first — server enforces one rack per device. Orchestrate the - // remove-then-add so the picker behaves as a re-parent rather than - // a duplicate insert. - const movesByLabel = groupBySourceRack(ids, minerSnapshots, rack, currentMembers); - if (movesByLabel.size > 0) { - let racks: DeviceSet[]; - try { - racks = await fetchAllRacks(); - } catch (err) { - const message = err instanceof Error ? err.message : "Couldn't load racks."; - pushToast({ message, status: STATUSES.error }); - return; - } - const labelToRackId = new Map(); - for (const r of racks) if (r.label) labelToRackId.set(r.label, r.id); - - const movingToast = pushToast({ - message: `Moving miners from ${movesByLabel.size} other rack${movesByLabel.size === 1 ? "" : "s"}…`, - status: STATUSES.loading, - longRunning: true, + // AssignDevicesToRack clears prior rack membership and inserts the + // new membership inside one server-side transaction. Previously + // this was a client-side remove-then-add loop that resolved source + // racks via listRacks and could (a) orphan miners on a transport + // failure between the two calls, and (b) skip a rack whose label + // got renamed mid-confirm. The atomic RPC closes both holes. + await new Promise((resolve) => { + void assignDevicesToRack({ + targetRackId, + deviceIdentifiers: ids, + signal: abortRef.current?.signal, + onSuccess: (count) => { + pushToast({ message: successMessage(count, "rack"), status: STATUSES.success }); + onRefetchMiners?.(); + resolve(); + }, + onError: (msg) => { + pushToast({ message: `Couldn't move miners to rack: ${msg}`, status: STATUSES.error }); + resolve(); + }, }); - try { - for (const [src, sourceIds] of movesByLabel) { - const sourceRackId = labelToRackId.get(src); - if (sourceRackId === undefined) continue; - await removeFromRack(sourceRackId, sourceIds); - } - } catch (err) { - const message = err instanceof Error ? err.message : "Failed to remove miners from current rack."; - updateToast(movingToast, { message, status: STATUSES.error }); - return; - } - removeToast(movingToast); - } - - void addDevicesToDeviceSet({ - deviceSetId: targetRackId, - deviceIdentifiers: ids, - onSuccess: (count) => { - pushToast({ message: successMessage(count, "rack"), status: STATUSES.success }); - onRefetchMiners?.(); - }, - onError: (msg) => pushToast({ message: `Couldn't add miners to rack: ${msg}`, status: STATUSES.error }), }); }; @@ -383,9 +345,7 @@ const MinerReparentPicker = ({ ids: string[], minerSnapshots: Record | undefined, ) => - kind === "site" - ? dispatchReparentToSite(targetId, ids, minerSnapshots) - : dispatchReparentToRack(targetId, ids, minerSnapshots); + kind === "site" ? dispatchReparentToSite(targetId, ids, minerSnapshots) : dispatchReparentToRack(targetId, ids); const settleDialog = () => { const resolve = dialogResolveRef.current; @@ -442,7 +402,11 @@ const MinerReparentPicker = ({ longRunning: true, }); try { - const resolved = await resolveAllModeIds(effectiveFilter); + const resolved = await resolveAllModeIds(effectiveFilter, abortRef.current?.signal); + if (abortRef.current?.signal.aborted) { + removeToast(loadingToast); + return; + } ids = resolved.ids; snapshots = resolved.snapshots; } catch (err) { diff --git a/client/src/protoFleet/features/fleetManagement/pages/FleetBuildingsPage.tsx b/client/src/protoFleet/features/fleetManagement/pages/FleetBuildingsPage.tsx index 9f1a53688..ed8dd3292 100644 --- a/client/src/protoFleet/features/fleetManagement/pages/FleetBuildingsPage.tsx +++ b/client/src/protoFleet/features/fleetManagement/pages/FleetBuildingsPage.tsx @@ -117,7 +117,7 @@ const FleetBuildingsPage = () => { [buildingModals, siteNameById], ); - const { assignBuildingToSite } = useSites(); + const { assignBuildingsToSite } = useSites(); const [reparentTarget, setReparentTarget] = useState(null); const handleAddBuildingToSite = useCallback((row: BuildingWithCounts) => setReparentTarget(row), []); @@ -251,8 +251,8 @@ const FleetBuildingsPage = () => { } const name = reparentTarget.building.name || "building"; const buildingId = reparentTarget.building.id; - void assignBuildingToSite({ - buildingId, + void assignBuildingsToSite({ + buildingIds: [buildingId], targetSiteId, onSuccess: () => { pushToast({ message: `Moved "${name}" to selected site.`, status: STATUSES.success }); diff --git a/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx b/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx index dafcc5edb..3356142a8 100644 --- a/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx +++ b/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.test.tsx @@ -119,7 +119,7 @@ function mockResolvedRackPageData(deviceSet = rack): void { mockUseDeviceSets.mockReturnValue({ getDeviceSet: ({ onSuccess }: { onSuccess: (resolvedDeviceSet: typeof rack) => void }) => onSuccess(deviceSet), listGroupMembers: ({ onSuccess }: { onSuccess: (deviceIds: string[]) => void }) => onSuccess([]), - addDevicesToDeviceSet: vi.fn(), + assignDevicesToRack: vi.fn(), setRackSlotPosition: vi.fn(), deleteGroup: vi.fn(), }); diff --git a/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx b/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx index 3d8792807..e31c7f473 100644 --- a/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx +++ b/client/src/protoFleet/features/fleetManagement/pages/RackOverviewPage.tsx @@ -59,7 +59,7 @@ const RackOverviewPage = () => { const sleepActionRef = useRef<(() => void) | null>(null); const actionActiveRef = useRef(false); - const { getDeviceSet, listGroupMembers, addDevicesToDeviceSet, setRackSlotPosition, deleteGroup } = useDeviceSets(); + const { getDeviceSet, listGroupMembers, assignDevicesToRack, setRackSlotPosition, deleteGroup } = useDeviceSets(); // Request versioning to guard against stale resolution callbacks const resolveVersionRef = useRef(0); @@ -449,11 +449,14 @@ const RackOverviewPage = () => { const slot = searchMinerSlot; setSearchMinerSlot(null); - // Two-step: add miner to rack membership, then assign to slot. - // No single API supports both atomically without resending the full rack state. - // On partial success (added but slot failed), we still refresh so the UI stays consistent. - addDevicesToDeviceSet({ - deviceSetId: rack.id, + // Two-step: atomically move the miner into this rack + // (clearing any prior rack membership in one tx), then + // assign the slot. No single API supports both atomically + // without resending the full rack state. On partial + // success (assigned but slot failed), we still refresh so + // the UI stays consistent. + assignDevicesToRack({ + targetRackId: rack.id, deviceIdentifiers: [minerId], onSuccess: () => { setRackSlotPosition({ diff --git a/client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx b/client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx index 05fd53606..ee65fd319 100644 --- a/client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx +++ b/client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx @@ -69,9 +69,9 @@ const RACK_COLUMNS_STANDALONE: DeviceSetColumn[] = [ const RacksPage = () => { const navigate = useNavigate(); const { listRacks, listRackZones, deleteGroup } = useDeviceSets(); - const { listAllBuildings, assignRackToBuilding } = useBuildings(); + const { listAllBuildings, assignRacksToBuilding } = useBuildings(); const canEditRack = useHasPermission("rack:manage"); - const canAssignRackToBuilding = useHasPermission("site:manage"); + const canAssignRacksToBuilding = useHasPermission("site:manage"); const [reparentTarget, setReparentTarget] = useState(null); const { listSites } = useSites(); const [searchParams, setSearchParams] = useSearchParams(); @@ -408,10 +408,10 @@ const RacksPage = () => { label: "Add to building", icon: , onClick: () => setReparentTarget(rack), - hidden: !canAssignRackToBuilding, + hidden: !canAssignRacksToBuilding, }, ], - [navigate, handleEditRack, canEditRack, canAssignRackToBuilding], + [navigate, handleEditRack, canEditRack, canAssignRacksToBuilding], ); const renderName = useCallback( @@ -761,9 +761,9 @@ const RacksPage = () => { return; } const rackName = reparentTarget.label || "rack"; - void assignRackToBuilding({ - rackId: reparentTarget.id, - buildingId, + void assignRacksToBuilding({ + racks: [{ rackId: reparentTarget.id }], + targetBuildingId: buildingId, onSuccess: () => { pushToast({ message: `Moved "${rackName}" to selected building.`, status: STATUSES.success }); resetAndFetch(); diff --git a/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts b/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts index 3b13e2fed..b14bcd25d 100644 --- a/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts +++ b/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts @@ -23,7 +23,7 @@ vi.mock("@/protoFleet/api/clients", () => ({ createSite: vi.fn(), updateSite: vi.fn(), deleteSite: vi.fn(), - reassignDevicesToSite: vi.fn(), + assignDevicesToSite: vi.fn(), }, })); diff --git a/client/src/protoFleet/features/sites/hooks/useSiteModals.ts b/client/src/protoFleet/features/sites/hooks/useSiteModals.ts index 307d3ed8d..3d21b1e93 100644 --- a/client/src/protoFleet/features/sites/hooks/useSiteModals.ts +++ b/client/src/protoFleet/features/sites/hooks/useSiteModals.ts @@ -144,8 +144,19 @@ const useSiteModals = ({ refetchSites }: UseSiteModalsOptions): SiteModalsApi => const detailsSaveEdit = useCallback( async (values: SiteFormValues) => { if (savingRef.current) return; - if (state.kind !== "manageEditEditingDetails") return; - const id = state.site.id; + // Functional setState reads the current state synchronously so a + // mid-flight dismiss (state transition back to manageEdit or none) + // can't drive a save against a stale captured `state` value. + // Matches the onSuccess setState pattern below. + let resolvedId: bigint | null = null; + setState((prev) => { + if (prev.kind === "manageEditEditingDetails") { + resolvedId = prev.site.id; + } + return prev; + }); + if (resolvedId === null) return; + const id: bigint = resolvedId; savingRef.current = true; setSaving(true); await new Promise((resolve) => { @@ -180,7 +191,7 @@ const useSiteModals = ({ refetchSites }: UseSiteModalsOptions): SiteModalsApi => }); }); }, - [state, updateSite, refetchSites], + [updateSite, refetchSites], ); const manageEditDetails = useCallback(() => { @@ -252,7 +263,7 @@ const useSiteModals = ({ refetchSites }: UseSiteModalsOptions): SiteModalsApi => }, }); // TODO(phase-1b #199): once the miner picker ships, follow this - // CreateSite call with `reassignDevicesToSite({ targetSiteId: site.id, + // CreateSite call with `assignDevicesToSite({ targetSiteId: site.id, // deviceIdentifiers: pendingDeviceIds })` inside the onSuccess branch // (and gate setSaving(false) on the inner onFinally). Partial-failure // toast wording is already drafted in PR #292 review. diff --git a/proto/buildings/v1/buildings.proto b/proto/buildings/v1/buildings.proto index 664627320..01faaee6b 100644 --- a/proto/buildings/v1/buildings.proto +++ b/proto/buildings/v1/buildings.proto @@ -29,7 +29,7 @@ service BuildingService { // UpdateBuilding mutates name, description, capacity, layout // defaults. Site assignment is *not* changed by UpdateBuilding — - // use AssignBuildingToSite (SiteService) for that, since it has + // use AssignBuildingsToSite (SiteService) for that, since it has // cross-collection enforcement. rpc UpdateBuilding(UpdateBuildingRequest) returns (UpdateBuildingResponse); @@ -45,15 +45,17 @@ service BuildingService { // grid placement. rpc ListBuildingRacks(ListBuildingRacksRequest) returns (ListBuildingRacksResponse); - // AssignRackToBuilding sets the rack's building_id and optionally + // AssignRacksToBuilding sets each rack's building_id and optionally // positions it at (aisle_index, position_in_aisle) inside the - // building's grid. Leaving building_id unset unassigns the rack - // from any building. Position fields must be set together or - // both unset; the server clears them automatically on any - // building_id transition. Runs the same site-cascade for - // descendant device.site_id that SaveRack runs when the rack's - // site changes. - rpc AssignRackToBuilding(AssignRackToBuildingRequest) returns (AssignRackToBuildingResponse); + // target building's grid. Leaving target_building_id unset + // unassigns every rack in the batch from any building. Each entry + // carries its own optional placement; entries with unset placement + // have their grid cell cleared (single-call mix of placed + cleared + // racks is supported). The entire batch runs in one transaction; if + // any rack fails validation or its update, no row is touched. Runs + // the same site-cascade for descendant device.site_id that SaveRack + // runs when a rack's site changes. + rpc AssignRacksToBuilding(AssignRacksToBuildingRequest) returns (AssignRacksToBuildingResponse); // GetBuildingStats returns server-rolled telemetry + miner-state // counts for every device whose rack lives in the building, plus a @@ -245,18 +247,19 @@ message ListBuildingRacksResponse { string next_page_token = 2; } -// AssignRackToBuilding +// AssignRacksToBuilding -message AssignRackToBuildingRequest { +// RackPlacement carries one rack's identity plus its optional grid +// placement inside the target building. Used by AssignRacksToBuilding +// for bulk updates that mix placed + cleared positions in one call. +message RackPlacement { int64 rack_id = 1 [(buf.validate.field).int64.gt = 0]; - // Unset = unassign rack from any building. When present, must be > 0. - optional int64 building_id = 2 [(buf.validate.field).int64.gt = 0]; // Optional grid placement inside the target building. Must be set // together or both unset (a half-set position would be ambiguous). // Upper bounds are validated server-side against the target // building's aisles / racks_per_aisle. - optional int32 aisle_index = 3 [(buf.validate.field).int32.gte = 0]; - optional int32 position_in_aisle = 4 [(buf.validate.field).int32.gte = 0]; + optional int32 aisle_index = 2 [(buf.validate.field).int32.gte = 0]; + optional int32 position_in_aisle = 3 [(buf.validate.field).int32.gte = 0]; // aisle_index and position_in_aisle must be set together or both // unset. Setting one without the other is an invalid request. @@ -267,10 +270,19 @@ message AssignRackToBuildingRequest { }; } -message AssignRackToBuildingResponse { - // Cascade impact: how many descendant devices had their site_id - // re-stamped to the target building's site_id (or NULL when the - // rack moved to unassigned). Zero when no cascade was needed. +message AssignRacksToBuildingRequest { + repeated RackPlacement racks = 1 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 1000 + }]; + // Unset = unassign racks from any building. When present, must be > 0. + optional int64 target_building_id = 2 [(buf.validate.field).int64.gt = 0]; +} + +message AssignRacksToBuildingResponse { + // Cascade impact: aggregate descendant devices that had their + // site_id re-stamped across every rack in the batch. Zero when no + // cascade was needed. int64 site_reassigned_device_count = 1; } diff --git a/proto/collection/v1/collection.proto b/proto/collection/v1/collection.proto index d9973e002..f2fdca2d0 100644 --- a/proto/collection/v1/collection.proto +++ b/proto/collection/v1/collection.proto @@ -27,12 +27,6 @@ service DeviceCollectionService { // Lists all collections for the organization rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsResponse); - // Adds devices to a collection - rpc AddDevicesToCollection(AddDevicesToCollectionRequest) returns (AddDevicesToCollectionResponse); - - // Removes devices from a collection - rpc RemoveDevicesFromCollection(RemoveDevicesFromCollectionRequest) returns (RemoveDevicesFromCollectionResponse); - // Lists members of a collection rpc ListCollectionMembers(ListCollectionMembersRequest) returns (ListCollectionMembersResponse); @@ -308,47 +302,6 @@ message ListCollectionsResponse { int32 total_count = 3; } -// Request to add devices to a collection. -// -// When the target is a site-stamped rack, the server rewrites -// device.site_id to match the rack for every added device. The affected -// count is returned in site_reassigned_count. Groups are exempt. -message AddDevicesToCollectionRequest { - // ID of the collection to add devices to - int64 collection_id = 1 [(buf.validate.field).int64.gt = 0]; - - // Devices to add: specific list or all paired devices - common.v1.DeviceSelector device_selector = 2 [(buf.validate.field).required = true]; -} - -// Response after adding devices to a collection -message AddDevicesToCollectionResponse { - // ID of the collection devices were added to - int64 collection_id = 1; - - // Number of devices successfully added - // May be less than requested if some devices were already members - int32 added_count = 2; - - // Number of devices whose site_id was rewritten by the cascade. - int32 site_reassigned_count = 3; -} - -// Request to remove devices from a collection -message RemoveDevicesFromCollectionRequest { - // ID of the collection to remove devices from - int64 collection_id = 1 [(buf.validate.field).int64.gt = 0]; - - // Devices to remove: specific list or all paired devices - common.v1.DeviceSelector device_selector = 2 [(buf.validate.field).required = true]; -} - -// Response after removing devices from a collection -message RemoveDevicesFromCollectionResponse { - // Number of devices successfully removed - int32 removed_count = 1; -} - // Request to list members of a collection message ListCollectionMembersRequest { // ID of the collection to list members for diff --git a/proto/device_set/v1/device_set.proto b/proto/device_set/v1/device_set.proto index d8e25f6f4..e6df2d96d 100644 --- a/proto/device_set/v1/device_set.proto +++ b/proto/device_set/v1/device_set.proto @@ -27,11 +27,14 @@ service DeviceSetService { // Lists all device sets for the organization rpc ListDeviceSets(ListDeviceSetsRequest) returns (ListDeviceSetsResponse); - // Adds devices to a device set - rpc AddDevicesToDeviceSet(AddDevicesToDeviceSetRequest) returns (AddDevicesToDeviceSetResponse); + // Adds devices to a group (many-to-many). Rejects non-group targets + // with InvalidArgument; use AssignDevicesToRack for racks. + rpc AddDevicesToGroup(AddDevicesToGroupRequest) returns (AddDevicesToGroupResponse); - // Removes devices from a device set - rpc RemoveDevicesFromDeviceSet(RemoveDevicesFromDeviceSetRequest) returns (RemoveDevicesFromDeviceSetResponse); + // Removes devices from a group. Rejects non-group targets with + // InvalidArgument; rack membership is cleared via + // AssignDevicesToRack with target_rack_id unset. + rpc RemoveDevicesFromGroup(RemoveDevicesFromGroupRequest) returns (RemoveDevicesFromGroupResponse); // Lists members of a device set rpc ListDeviceSetMembers(ListDeviceSetMembersRequest) returns (ListDeviceSetMembersResponse); @@ -68,6 +71,24 @@ service DeviceSetService { // Atomically creates or updates a rack with its membership and slot assignments. // All operations (metadata, membership, slot positions) are applied in a single transaction. rpc SaveRack(SaveRackRequest) returns (SaveRackResponse); + + // AssignDevicesToRack atomically moves devices into the target rack + // in one transaction: removes any existing rack membership for each + // device, inserts new membership at target_rack_id, and cascades + // device.site_id when the target rack's site differs from the + // device's current site. target_rack_id unset clears rack + // membership without re-assigning (site/building stay intact). + // + // Closes the orphan window where a client-side remove + add pair + // could leave devices without rack membership on transport failure + // between the two calls, and the cross-rack rename race where the + // source rack's id resolution drifts between the resolve and the + // remove. + // + // device_selector accepts only the device_list variant. all_devices + // is rejected with InvalidArgument because moving every paired + // device into a single rack is never the intended operation. + rpc AssignDevicesToRack(AssignDevicesToRackRequest) returns (AssignDevicesToRackResponse); } // Type of device set @@ -342,45 +363,42 @@ message ListDeviceSetsResponse { int32 total_count = 3; } -// Request to add devices to a device set. -// -// When the target is a site-stamped rack, the server rewrites -// device.site_id to match the rack for every added device. The affected -// count is returned in site_reassigned_count. Groups are exempt. -message AddDevicesToDeviceSetRequest { - // ID of the device set to add devices to - int64 device_set_id = 1 [(buf.validate.field).int64.gt = 0]; +// Request to add devices to a group. Groups have many-to-many device +// membership: devices may already belong to other groups and to a rack, +// none of which are touched. Server rejects with InvalidArgument when +// target_group_id refers to a non-group device set. +message AddDevicesToGroupRequest { + // ID of the group to add devices to. Must reference a device set of + // type GROUP; racks are rejected. + int64 target_group_id = 1 [(buf.validate.field).int64.gt = 0]; - // Devices to add: specific list or all paired devices + // Devices to add: specific list or all paired devices. common.v1.DeviceSelector device_selector = 2 [(buf.validate.field).required = true]; } -// Response after adding devices to a device set -message AddDevicesToDeviceSetResponse { - // ID of the device set devices were added to - int64 device_set_id = 1; - - // Number of devices successfully added - // May be less than requested if some devices were already members - int32 added_count = 2; - - // Number of devices whose site_id was rewritten by the cascade. - int32 site_reassigned_count = 3; +// Response after adding devices to a group. +message AddDevicesToGroupResponse { + // Number of devices successfully added. May be less than requested + // if some devices were already members. + int64 added_count = 1; } -// Request to remove devices from a device set -message RemoveDevicesFromDeviceSetRequest { - // ID of the device set to remove devices from - int64 device_set_id = 1 [(buf.validate.field).int64.gt = 0]; +// Request to remove devices from a group. Server rejects with +// InvalidArgument when target_group_id refers to a non-group device set; +// use AssignDevicesToRack to clear rack membership. +message RemoveDevicesFromGroupRequest { + // ID of the group to remove devices from. Must reference a device + // set of type GROUP; racks are rejected. + int64 target_group_id = 1 [(buf.validate.field).int64.gt = 0]; - // Devices to remove: specific list or all paired devices + // Devices to remove: specific list or all paired devices. common.v1.DeviceSelector device_selector = 2 [(buf.validate.field).required = true]; } -// Response after removing devices from a device set -message RemoveDevicesFromDeviceSetResponse { - // Number of devices successfully removed - int32 removed_count = 1; +// Response after removing devices from a group. +message RemoveDevicesFromGroupResponse { + // Number of devices successfully removed. + int64 removed_count = 1; } // Request to list members of a device set @@ -629,3 +647,34 @@ message SaveRackResponse { // Number of devices whose site_id was rewritten by the cascade. int32 site_reassigned_count = 3; } + +// AssignDevicesToRack + +message AssignDevicesToRackRequest { + // Unset = clear rack membership without re-assigning. When + // present, must be > 0. + optional int64 target_rack_id = 1 [(buf.validate.field).int64.gt = 0]; + + // Devices to assign. Only the device_list variant is accepted; the + // all_devices variant is rejected with InvalidArgument because + // moving every paired device into a single rack is never the + // intended operation. Filter-based selectors are reserved for a + // future expansion. + common.v1.DeviceSelector device_selector = 2 [(buf.validate.field).required = true]; +} + +message AssignDevicesToRackResponse { + // Number of devices whose rack membership now points at + // target_rack_id. When target_rack_id is unset, this is 0 and + // removed_count carries the cleared-rack count. + int64 assigned_count = 1; + // Number of devices whose site_id was rewritten by the cascade + // because the target rack's site differed from the device's prior + // site. Zero on unassign and on same-site moves. + int64 site_reassigned_count = 2; + // Number of devices whose prior rack membership was cleared. On + // unassign (target_rack_id unset) this equals the number of devices + // that were in a rack. On a re-assign this counts the prior rack + // membership rows that were deleted before the new ones inserted. + int64 removed_count = 3; +} diff --git a/proto/sites/v1/sites.proto b/proto/sites/v1/sites.proto index 3e803a5e3..3d5871133 100644 --- a/proto/sites/v1/sites.proto +++ b/proto/sites/v1/sites.proto @@ -30,20 +30,33 @@ service SiteService { // result in a toast/activity row. rpc DeleteSite(DeleteSiteRequest) returns (DeleteSiteResponse); - // ReassignDevicesToSite is an all-or-nothing bulk update of + // AssignDevicesToSite is an all-or-nothing bulk update of // device.site_id. If any device is in a rack whose site_id differs // from the target, the entire batch rejects with per-device error // details and no row is touched. Omit target_site_id (or leave it // unset) to move devices to the "Unassigned" bucket. - rpc ReassignDevicesToSite(ReassignDevicesToSiteRequest) - returns (ReassignDevicesToSiteResponse); - - // AssignBuildingToSite moves a building to a different site - // (or to "Unassigned" when target_site_id is unset). The same - // transaction cascades site_id down to the building's racks and - // their devices. Returns the cascade counts. - rpc AssignBuildingToSite(AssignBuildingToSiteRequest) - returns (AssignBuildingToSiteResponse); + rpc AssignDevicesToSite(AssignDevicesToSiteRequest) + returns (AssignDevicesToSiteResponse); + + // AssignBuildingsToSite moves one or more buildings to a target site + // (or to "Unassigned" when target_site_id is unset). All updates run + // in a single transaction; if any building fails, the batch rolls + // back. The same transaction cascades site_id down to each + // building's racks and their devices. Returns the aggregate cascade + // counts across every building in the batch. + rpc AssignBuildingsToSite(AssignBuildingsToSiteRequest) + returns (AssignBuildingsToSiteResponse); + + // AssignRacksToSite moves one or more racks to a target site (or + // to "Unassigned" when target_site_id is unset) as a partial + // update — the rack's label, layout, members, and slot + // assignments stay untouched, only site_id changes. building_id is + // auto-cleared on any site transition since a building belongs to + // a single site; the response carries the count of racks whose + // building was cleared so the UI can prompt for re-assignment. + // Same transaction cascades device.site_id for every rack member. + rpc AssignRacksToSite(AssignRacksToSiteRequest) + returns (AssignRacksToSiteResponse); // GetSiteStats returns server-rolled telemetry + miner-state counts // for every device assigned to the site, including devices whose @@ -181,9 +194,9 @@ message DeleteSiteResponse { int64 unassigned_rack_count = 3; } -// ReassignDevicesToSite +// AssignDevicesToSite -message ReassignDevicesToSiteRequest { +message AssignDevicesToSiteRequest { // Unset = move to the "Unassigned" bucket. When present, must be > 0. optional int64 target_site_id = 1 [(buf.validate.field).int64.gt = 0]; repeated string device_identifiers = 2 [(buf.validate.field).repeated = { @@ -191,10 +204,22 @@ message ReassignDevicesToSiteRequest { max_items: 10000 items: {string: {min_len: 1, max_len: 256}} }]; + // When true, the server — inside the same transaction as the site + // write — removes any conflicting rack memberships for the listed + // devices before applying the site assignment. This closes the + // cross-site reparent orphan window the client-side + // remove-then-assign loop in MinerReparentPicker had: a transport + // failure between the per-rack unassign and the site reassign + // would leave miners rack-less but still pinned to the old site. + // + // When false (default), preserves today's behavior: any device + // currently in a rack at a different site rejects the whole batch + // with a PerDeviceConflict[] of DEVICE_IN_RACK_AT_OTHER_SITE. + optional bool force_clear_conflicting_rack_membership = 3; } // PerDeviceConflictReason enumerates the reasons a device can be -// rejected by ReassignDevicesToSite. +// rejected by AssignDevicesToSite. enum PerDeviceConflictReason { PER_DEVICE_CONFLICT_REASON_UNSPECIFIED = 0; PER_DEVICE_CONFLICT_REASON_DEVICE_NOT_FOUND = 1; @@ -202,7 +227,7 @@ enum PerDeviceConflictReason { } // PerDeviceConflict carries the per-device error details surfaced -// when ReassignDevicesToSite rejects. The whole batch is rejected; +// when AssignDevicesToSite rejects. The whole batch is rejected; // no rows are mutated. message PerDeviceConflict { string device_identifier = 1; @@ -212,27 +237,55 @@ message PerDeviceConflict { int64 conflicting_site_id = 3; } -message ReassignDevicesToSiteResponse { +message AssignDevicesToSiteResponse { int64 reassigned_count = 1; // Populated only on rejection. Empty on success. repeated PerDeviceConflict conflicts = 2; } -// AssignBuildingToSite +// AssignBuildingsToSite -message AssignBuildingToSiteRequest { - int64 building_id = 1 [(buf.validate.field).int64.gt = 0]; - // Unset = move building to "Unassigned". When present, must be > 0. +message AssignBuildingsToSiteRequest { + repeated int64 building_ids = 1 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 1000 + items: {int64: {gt: 0}} + }]; + // Unset = move buildings to "Unassigned". When present, must be > 0. optional int64 target_site_id = 2 [(buf.validate.field).int64.gt = 0]; } -message AssignBuildingToSiteResponse { +message AssignBuildingsToSiteResponse { // Cascade impact: how many racks and devices were re-stamped with - // the new site_id in the same transaction. + // the new site_id in the same transaction, aggregated across every + // building in the batch. int64 reassigned_rack_count = 1; int64 reassigned_device_count = 2; } +// AssignRacksToSite + +message AssignRacksToSiteRequest { + repeated int64 rack_ids = 1 [(buf.validate.field).repeated = { + min_items: 1 + max_items: 1000 + items: {int64: {gt: 0}} + }]; + // Unset = move racks to "Unassigned" (site_id NULL). When present, + // must be > 0. + optional int64 target_site_id = 2 [(buf.validate.field).int64.gt = 0]; +} + +message AssignRacksToSiteResponse { + // Cascade impact: how many devices had their site_id re-stamped + // across every rack in the batch. + int64 reassigned_device_count = 1; + // Number of racks whose building_id was cleared because the move + // crossed site boundaries. UI can use this to prompt for + // re-assignment to a building in the new site. + int64 cleared_building_count = 2; +} + // GetSiteStats message GetSiteStatsRequest { diff --git a/server/cmd/fleetd/main.go b/server/cmd/fleetd/main.go index a04fc8215..8cbbf0bd2 100644 --- a/server/cmd/fleetd/main.go +++ b/server/cmd/fleetd/main.go @@ -158,6 +158,7 @@ var reflectEnabledServices = []string{ sitesv1connect.SiteServiceName, buildingsv1connect.BuildingServiceName, curtailmentv1connect.CurtailmentServiceName, + device_setv1connect.DeviceSetServiceName, } func start(config *Config) error { @@ -453,7 +454,7 @@ func start(config *Config) error { ) curtailmentResponseProfileSvc := curtailmentDomain.NewResponseProfileService(curtailmentStore) - sitesSvc := sitesDomain.NewService(siteStore, buildingStore, deviceStore, telemetryService, transactor, activitySvc) + sitesSvc := sitesDomain.NewService(siteStore, buildingStore, collectionStore, deviceStore, telemetryService, transactor, activitySvc) buildingsSvc := buildingsDomain.NewService(buildingStore, siteStore, collectionStore, deviceStore, telemetryService, transactor, activitySvc) // Register the schedule-conflict preflight filter on commandSvc so every diff --git a/server/generated/grpc/buildings/v1/buildings.pb.go b/server/generated/grpc/buildings/v1/buildings.pb.go index 543d7ab8c..a60be7fc6 100644 --- a/server/generated/grpc/buildings/v1/buildings.pb.go +++ b/server/generated/grpc/buildings/v1/buildings.pb.go @@ -1133,35 +1133,36 @@ func (x *ListBuildingRacksResponse) GetNextPageToken() string { return "" } -type AssignRackToBuildingRequest struct { +// RackPlacement carries one rack's identity plus its optional grid +// placement inside the target building. Used by AssignRacksToBuilding +// for bulk updates that mix placed + cleared positions in one call. +type RackPlacement struct { state protoimpl.MessageState `protogen:"open.v1"` RackId int64 `protobuf:"varint,1,opt,name=rack_id,json=rackId,proto3" json:"rack_id,omitempty"` - // Unset = unassign rack from any building. When present, must be > 0. - BuildingId *int64 `protobuf:"varint,2,opt,name=building_id,json=buildingId,proto3,oneof" json:"building_id,omitempty"` // Optional grid placement inside the target building. Must be set // together or both unset (a half-set position would be ambiguous). // Upper bounds are validated server-side against the target // building's aisles / racks_per_aisle. - AisleIndex *int32 `protobuf:"varint,3,opt,name=aisle_index,json=aisleIndex,proto3,oneof" json:"aisle_index,omitempty"` - PositionInAisle *int32 `protobuf:"varint,4,opt,name=position_in_aisle,json=positionInAisle,proto3,oneof" json:"position_in_aisle,omitempty"` + AisleIndex *int32 `protobuf:"varint,2,opt,name=aisle_index,json=aisleIndex,proto3,oneof" json:"aisle_index,omitempty"` + PositionInAisle *int32 `protobuf:"varint,3,opt,name=position_in_aisle,json=positionInAisle,proto3,oneof" json:"position_in_aisle,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AssignRackToBuildingRequest) Reset() { - *x = AssignRackToBuildingRequest{} +func (x *RackPlacement) Reset() { + *x = RackPlacement{} mi := &file_buildings_v1_buildings_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AssignRackToBuildingRequest) String() string { +func (x *RackPlacement) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AssignRackToBuildingRequest) ProtoMessage() {} +func (*RackPlacement) ProtoMessage() {} -func (x *AssignRackToBuildingRequest) ProtoReflect() protoreflect.Message { +func (x *RackPlacement) ProtoReflect() protoreflect.Message { mi := &file_buildings_v1_buildings_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1173,63 +1174,55 @@ func (x *AssignRackToBuildingRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AssignRackToBuildingRequest.ProtoReflect.Descriptor instead. -func (*AssignRackToBuildingRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use RackPlacement.ProtoReflect.Descriptor instead. +func (*RackPlacement) Descriptor() ([]byte, []int) { return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{15} } -func (x *AssignRackToBuildingRequest) GetRackId() int64 { +func (x *RackPlacement) GetRackId() int64 { if x != nil { return x.RackId } return 0 } -func (x *AssignRackToBuildingRequest) GetBuildingId() int64 { - if x != nil && x.BuildingId != nil { - return *x.BuildingId - } - return 0 -} - -func (x *AssignRackToBuildingRequest) GetAisleIndex() int32 { +func (x *RackPlacement) GetAisleIndex() int32 { if x != nil && x.AisleIndex != nil { return *x.AisleIndex } return 0 } -func (x *AssignRackToBuildingRequest) GetPositionInAisle() int32 { +func (x *RackPlacement) GetPositionInAisle() int32 { if x != nil && x.PositionInAisle != nil { return *x.PositionInAisle } return 0 } -type AssignRackToBuildingResponse struct { +type AssignRacksToBuildingRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Cascade impact: how many descendant devices had their site_id - // re-stamped to the target building's site_id (or NULL when the - // rack moved to unassigned). Zero when no cascade was needed. - SiteReassignedDeviceCount int64 `protobuf:"varint,1,opt,name=site_reassigned_device_count,json=siteReassignedDeviceCount,proto3" json:"site_reassigned_device_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Racks []*RackPlacement `protobuf:"bytes,1,rep,name=racks,proto3" json:"racks,omitempty"` + // Unset = unassign racks from any building. When present, must be > 0. + TargetBuildingId *int64 `protobuf:"varint,2,opt,name=target_building_id,json=targetBuildingId,proto3,oneof" json:"target_building_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *AssignRackToBuildingResponse) Reset() { - *x = AssignRackToBuildingResponse{} +func (x *AssignRacksToBuildingRequest) Reset() { + *x = AssignRacksToBuildingRequest{} mi := &file_buildings_v1_buildings_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AssignRackToBuildingResponse) String() string { +func (x *AssignRacksToBuildingRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AssignRackToBuildingResponse) ProtoMessage() {} +func (*AssignRacksToBuildingRequest) ProtoMessage() {} -func (x *AssignRackToBuildingResponse) ProtoReflect() protoreflect.Message { +func (x *AssignRacksToBuildingRequest) ProtoReflect() protoreflect.Message { mi := &file_buildings_v1_buildings_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1241,12 +1234,66 @@ func (x *AssignRackToBuildingResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AssignRackToBuildingResponse.ProtoReflect.Descriptor instead. -func (*AssignRackToBuildingResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AssignRacksToBuildingRequest.ProtoReflect.Descriptor instead. +func (*AssignRacksToBuildingRequest) Descriptor() ([]byte, []int) { return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{16} } -func (x *AssignRackToBuildingResponse) GetSiteReassignedDeviceCount() int64 { +func (x *AssignRacksToBuildingRequest) GetRacks() []*RackPlacement { + if x != nil { + return x.Racks + } + return nil +} + +func (x *AssignRacksToBuildingRequest) GetTargetBuildingId() int64 { + if x != nil && x.TargetBuildingId != nil { + return *x.TargetBuildingId + } + return 0 +} + +type AssignRacksToBuildingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Cascade impact: aggregate descendant devices that had their + // site_id re-stamped across every rack in the batch. Zero when no + // cascade was needed. + SiteReassignedDeviceCount int64 `protobuf:"varint,1,opt,name=site_reassigned_device_count,json=siteReassignedDeviceCount,proto3" json:"site_reassigned_device_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignRacksToBuildingResponse) Reset() { + *x = AssignRacksToBuildingResponse{} + mi := &file_buildings_v1_buildings_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssignRacksToBuildingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssignRacksToBuildingResponse) ProtoMessage() {} + +func (x *AssignRacksToBuildingResponse) ProtoReflect() protoreflect.Message { + mi := &file_buildings_v1_buildings_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssignRacksToBuildingResponse.ProtoReflect.Descriptor instead. +func (*AssignRacksToBuildingResponse) Descriptor() ([]byte, []int) { + return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{17} +} + +func (x *AssignRacksToBuildingResponse) GetSiteReassignedDeviceCount() int64 { if x != nil { return x.SiteReassignedDeviceCount } @@ -1262,7 +1309,7 @@ type GetBuildingStatsRequest struct { func (x *GetBuildingStatsRequest) Reset() { *x = GetBuildingStatsRequest{} - mi := &file_buildings_v1_buildings_proto_msgTypes[17] + mi := &file_buildings_v1_buildings_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1274,7 +1321,7 @@ func (x *GetBuildingStatsRequest) String() string { func (*GetBuildingStatsRequest) ProtoMessage() {} func (x *GetBuildingStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_buildings_v1_buildings_proto_msgTypes[17] + mi := &file_buildings_v1_buildings_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1287,7 +1334,7 @@ func (x *GetBuildingStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBuildingStatsRequest.ProtoReflect.Descriptor instead. func (*GetBuildingStatsRequest) Descriptor() ([]byte, []int) { - return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{17} + return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{18} } func (x *GetBuildingStatsRequest) GetBuildingId() int64 { @@ -1346,7 +1393,7 @@ type GetBuildingStatsResponse struct { func (x *GetBuildingStatsResponse) Reset() { *x = GetBuildingStatsResponse{} - mi := &file_buildings_v1_buildings_proto_msgTypes[18] + mi := &file_buildings_v1_buildings_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1358,7 +1405,7 @@ func (x *GetBuildingStatsResponse) String() string { func (*GetBuildingStatsResponse) ProtoMessage() {} func (x *GetBuildingStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_buildings_v1_buildings_proto_msgTypes[18] + mi := &file_buildings_v1_buildings_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1371,7 +1418,7 @@ func (x *GetBuildingStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBuildingStatsResponse.ProtoReflect.Descriptor instead. func (*GetBuildingStatsResponse) Descriptor() ([]byte, []int) { - return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{18} + return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{19} } func (x *GetBuildingStatsResponse) GetBuildingId() int64 { @@ -1506,7 +1553,7 @@ type BuildingRackHealth struct { func (x *BuildingRackHealth) Reset() { *x = BuildingRackHealth{} - mi := &file_buildings_v1_buildings_proto_msgTypes[19] + mi := &file_buildings_v1_buildings_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1565,7 @@ func (x *BuildingRackHealth) String() string { func (*BuildingRackHealth) ProtoMessage() {} func (x *BuildingRackHealth) ProtoReflect() protoreflect.Message { - mi := &file_buildings_v1_buildings_proto_msgTypes[19] + mi := &file_buildings_v1_buildings_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1531,7 +1578,7 @@ func (x *BuildingRackHealth) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildingRackHealth.ProtoReflect.Descriptor instead. func (*BuildingRackHealth) Descriptor() ([]byte, []int) { - return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{19} + return file_buildings_v1_buildings_proto_rawDescGZIP(), []int{20} } func (x *BuildingRackHealth) GetRackId() int64 { @@ -1805,185 +1852,192 @@ var file_buildings_v1_buildings_proto_rawDesc = string([]byte{ 0x76, 0x31, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x05, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xa7, - 0x03, 0x0a, 0x1b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, - 0x0a, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x06, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x64, - 0x12, 0x2d, 0x0a, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, - 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, - 0x2d, 0x0a, 0x0b, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x48, 0x01, 0x52, - 0x0a, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x12, 0x38, - 0x0a, 0x11, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, - 0x73, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, - 0x28, 0x00, 0x48, 0x02, 0x52, 0x0f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, - 0x41, 0x69, 0x73, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x3a, 0x97, 0x01, 0xba, 0x48, 0x93, 0x01, 0x1a, - 0x90, 0x01, 0x0a, 0x16, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x69, - 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x40, 0x61, 0x69, 0x73, 0x6c, - 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x20, 0x6d, 0x75, - 0x73, 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, - 0x72, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x20, 0x75, 0x6e, 0x73, 0x65, 0x74, 0x1a, 0x34, 0x68, 0x61, - 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, - 0x65, 0x78, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, - 0x65, 0x29, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, - 0x69, 0x64, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, - 0x65, 0x78, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x22, 0x5f, 0x0a, 0x1c, 0x41, 0x73, 0x73, 0x69, - 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x1c, 0x73, 0x69, 0x74, 0x65, - 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, - 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x43, 0x0a, 0x17, 0x47, 0x65, 0x74, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, - 0x20, 0x00, 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x22, 0xda, - 0x05, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, - 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, - 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, - 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, - 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6a, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, 0x45, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, - 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, - 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x4b, 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, - 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, - 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, - 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, - 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x41, 0x0a, - 0x0b, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x52, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x12, 0x2d, 0x0a, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, - 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, 0x66, - 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, - 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x65, - 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, - 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, 0x72, - 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xdd, 0x02, 0x0a, 0x12, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x61, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x72, 0x61, 0x63, 0x6b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0b, 0x61, 0x69, - 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, - 0x00, 0x52, 0x0a, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, - 0x12, 0x2f, 0x0a, 0x11, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, - 0x61, 0x69, 0x73, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x0f, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x41, 0x69, 0x73, 0x6c, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, + 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xda, + 0x02, 0x0a, 0x0d, 0x52, 0x61, 0x63, 0x6b, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x12, 0x20, 0x0a, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x06, 0x72, 0x61, 0x63, 0x6b, + 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x0b, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, + 0x48, 0x00, 0x52, 0x0a, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, + 0x01, 0x12, 0x38, 0x0a, 0x11, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, + 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, + 0x04, 0x1a, 0x02, 0x28, 0x00, 0x48, 0x01, 0x52, 0x0f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x6e, 0x41, 0x69, 0x73, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x3a, 0x97, 0x01, 0xba, 0x48, + 0x93, 0x01, 0x1a, 0x90, 0x01, 0x0a, 0x16, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x70, 0x61, 0x69, 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x40, 0x61, + 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, + 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, + 0x74, 0x20, 0x6f, 0x72, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x20, 0x75, 0x6e, 0x73, 0x65, 0x74, 0x1a, + 0x34, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x68, 0x61, 0x73, 0x28, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, + 0x69, 0x73, 0x6c, 0x65, 0x29, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x22, 0xb1, 0x01, 0x0a, 0x1c, + 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x05, + 0x72, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x50, + 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0xba, 0x48, 0x08, 0x92, 0x01, 0x05, + 0x08, 0x01, 0x10, 0xe8, 0x07, 0x52, 0x05, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x3a, 0x0a, 0x12, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, + 0x00, 0x48, 0x00, 0x52, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x22, + 0x60, 0x0a, 0x1d, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3f, 0x0a, 0x1c, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x22, 0x43, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0b, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x22, 0xda, 0x05, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, + 0x6e, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, 0x2c, 0x0a, + 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, + 0x6a, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, 0x45, 0x66, + 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x4b, + 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, + 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, - 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, - 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, - 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, - 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, - 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, - 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, - 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, - 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, - 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, - 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, - 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, - 0x48, 0x54, 0x10, 0x04, 0x32, 0x8e, 0x06, 0x0a, 0x0f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x22, 0x2e, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x12, 0x20, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x23, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x23, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x5b, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, - 0x6e, 0x67, 0x12, 0x23, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, - 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, - 0x11, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, - 0x6b, 0x73, 0x12, 0x26, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, - 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x62, 0x75, 0x69, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x68, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, + 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x0a, 0x72, 0x61, + 0x63, 0x6b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x2d, 0x0a, 0x12, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x0d, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, + 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, + 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, + 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x32, 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, + 0x70, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x22, 0xdd, 0x02, 0x0a, 0x12, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x61, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x61, + 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x61, 0x63, + 0x6b, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x4c, 0x61, 0x62, + 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0b, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x69, 0x73, 0x6c, 0x65, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x11, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x0f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x6e, 0x41, 0x69, 0x73, 0x6c, 0x65, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, + 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, + 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x0e, 0x0a, + 0x0c, 0x5f, 0x61, 0x69, 0x73, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x14, 0x0a, + 0x12, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x61, 0x69, + 0x73, 0x6c, 0x65, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, + 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, + 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, + 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, + 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, + 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, + 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, + 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, + 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, + 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x32, 0x91, 0x06, 0x0a, + 0x0f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x58, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x22, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, + 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x47, 0x65, + 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x20, 0x2e, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, + 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x12, 0x23, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x23, 0x2e, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x23, 0x2e, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x24, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x26, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x14, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, - 0x6b, 0x54, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x29, 0x2e, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, - 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, - 0x54, 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x61, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xc0, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4b, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, - 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, - 0x02, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, - 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x69, 0x6e, 0x67, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x61, + 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x15, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x12, 0x2a, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, + 0x6f, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2b, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, + 0x10, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x12, 0x25, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0xc0, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, + 0x67, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, + 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, + 0x67, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1999,35 +2053,36 @@ func file_buildings_v1_buildings_proto_rawDescGZIP() []byte { } var file_buildings_v1_buildings_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_buildings_v1_buildings_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_buildings_v1_buildings_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_buildings_v1_buildings_proto_goTypes = []any{ - (RackOrderIndex)(0), // 0: buildings.v1.RackOrderIndex - (*Building)(nil), // 1: buildings.v1.Building - (*BuildingWithCounts)(nil), // 2: buildings.v1.BuildingWithCounts - (*ListBuildingsRequest)(nil), // 3: buildings.v1.ListBuildingsRequest - (*ListBuildingsResponse)(nil), // 4: buildings.v1.ListBuildingsResponse - (*GetBuildingRequest)(nil), // 5: buildings.v1.GetBuildingRequest - (*GetBuildingResponse)(nil), // 6: buildings.v1.GetBuildingResponse - (*CreateBuildingRequest)(nil), // 7: buildings.v1.CreateBuildingRequest - (*CreateBuildingResponse)(nil), // 8: buildings.v1.CreateBuildingResponse - (*UpdateBuildingRequest)(nil), // 9: buildings.v1.UpdateBuildingRequest - (*UpdateBuildingResponse)(nil), // 10: buildings.v1.UpdateBuildingResponse - (*DeleteBuildingRequest)(nil), // 11: buildings.v1.DeleteBuildingRequest - (*DeleteBuildingResponse)(nil), // 12: buildings.v1.DeleteBuildingResponse - (*ListBuildingRacksRequest)(nil), // 13: buildings.v1.ListBuildingRacksRequest - (*BuildingRack)(nil), // 14: buildings.v1.BuildingRack - (*ListBuildingRacksResponse)(nil), // 15: buildings.v1.ListBuildingRacksResponse - (*AssignRackToBuildingRequest)(nil), // 16: buildings.v1.AssignRackToBuildingRequest - (*AssignRackToBuildingResponse)(nil), // 17: buildings.v1.AssignRackToBuildingResponse - (*GetBuildingStatsRequest)(nil), // 18: buildings.v1.GetBuildingStatsRequest - (*GetBuildingStatsResponse)(nil), // 19: buildings.v1.GetBuildingStatsResponse - (*BuildingRackHealth)(nil), // 20: buildings.v1.BuildingRackHealth - (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp + (RackOrderIndex)(0), // 0: buildings.v1.RackOrderIndex + (*Building)(nil), // 1: buildings.v1.Building + (*BuildingWithCounts)(nil), // 2: buildings.v1.BuildingWithCounts + (*ListBuildingsRequest)(nil), // 3: buildings.v1.ListBuildingsRequest + (*ListBuildingsResponse)(nil), // 4: buildings.v1.ListBuildingsResponse + (*GetBuildingRequest)(nil), // 5: buildings.v1.GetBuildingRequest + (*GetBuildingResponse)(nil), // 6: buildings.v1.GetBuildingResponse + (*CreateBuildingRequest)(nil), // 7: buildings.v1.CreateBuildingRequest + (*CreateBuildingResponse)(nil), // 8: buildings.v1.CreateBuildingResponse + (*UpdateBuildingRequest)(nil), // 9: buildings.v1.UpdateBuildingRequest + (*UpdateBuildingResponse)(nil), // 10: buildings.v1.UpdateBuildingResponse + (*DeleteBuildingRequest)(nil), // 11: buildings.v1.DeleteBuildingRequest + (*DeleteBuildingResponse)(nil), // 12: buildings.v1.DeleteBuildingResponse + (*ListBuildingRacksRequest)(nil), // 13: buildings.v1.ListBuildingRacksRequest + (*BuildingRack)(nil), // 14: buildings.v1.BuildingRack + (*ListBuildingRacksResponse)(nil), // 15: buildings.v1.ListBuildingRacksResponse + (*RackPlacement)(nil), // 16: buildings.v1.RackPlacement + (*AssignRacksToBuildingRequest)(nil), // 17: buildings.v1.AssignRacksToBuildingRequest + (*AssignRacksToBuildingResponse)(nil), // 18: buildings.v1.AssignRacksToBuildingResponse + (*GetBuildingStatsRequest)(nil), // 19: buildings.v1.GetBuildingStatsRequest + (*GetBuildingStatsResponse)(nil), // 20: buildings.v1.GetBuildingStatsResponse + (*BuildingRackHealth)(nil), // 21: buildings.v1.BuildingRackHealth + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_buildings_v1_buildings_proto_depIdxs = []int32{ 0, // 0: buildings.v1.Building.default_rack_order_index:type_name -> buildings.v1.RackOrderIndex - 21, // 1: buildings.v1.Building.created_at:type_name -> google.protobuf.Timestamp - 21, // 2: buildings.v1.Building.updated_at:type_name -> google.protobuf.Timestamp + 22, // 1: buildings.v1.Building.created_at:type_name -> google.protobuf.Timestamp + 22, // 2: buildings.v1.Building.updated_at:type_name -> google.protobuf.Timestamp 1, // 3: buildings.v1.BuildingWithCounts.building:type_name -> buildings.v1.Building 2, // 4: buildings.v1.ListBuildingsResponse.buildings:type_name -> buildings.v1.BuildingWithCounts 1, // 5: buildings.v1.GetBuildingResponse.building:type_name -> buildings.v1.Building @@ -2036,28 +2091,29 @@ var file_buildings_v1_buildings_proto_depIdxs = []int32{ 0, // 8: buildings.v1.UpdateBuildingRequest.default_rack_order_index:type_name -> buildings.v1.RackOrderIndex 1, // 9: buildings.v1.UpdateBuildingResponse.building:type_name -> buildings.v1.Building 14, // 10: buildings.v1.ListBuildingRacksResponse.racks:type_name -> buildings.v1.BuildingRack - 20, // 11: buildings.v1.GetBuildingStatsResponse.rack_health:type_name -> buildings.v1.BuildingRackHealth - 3, // 12: buildings.v1.BuildingService.ListBuildings:input_type -> buildings.v1.ListBuildingsRequest - 5, // 13: buildings.v1.BuildingService.GetBuilding:input_type -> buildings.v1.GetBuildingRequest - 7, // 14: buildings.v1.BuildingService.CreateBuilding:input_type -> buildings.v1.CreateBuildingRequest - 9, // 15: buildings.v1.BuildingService.UpdateBuilding:input_type -> buildings.v1.UpdateBuildingRequest - 11, // 16: buildings.v1.BuildingService.DeleteBuilding:input_type -> buildings.v1.DeleteBuildingRequest - 13, // 17: buildings.v1.BuildingService.ListBuildingRacks:input_type -> buildings.v1.ListBuildingRacksRequest - 16, // 18: buildings.v1.BuildingService.AssignRackToBuilding:input_type -> buildings.v1.AssignRackToBuildingRequest - 18, // 19: buildings.v1.BuildingService.GetBuildingStats:input_type -> buildings.v1.GetBuildingStatsRequest - 4, // 20: buildings.v1.BuildingService.ListBuildings:output_type -> buildings.v1.ListBuildingsResponse - 6, // 21: buildings.v1.BuildingService.GetBuilding:output_type -> buildings.v1.GetBuildingResponse - 8, // 22: buildings.v1.BuildingService.CreateBuilding:output_type -> buildings.v1.CreateBuildingResponse - 10, // 23: buildings.v1.BuildingService.UpdateBuilding:output_type -> buildings.v1.UpdateBuildingResponse - 12, // 24: buildings.v1.BuildingService.DeleteBuilding:output_type -> buildings.v1.DeleteBuildingResponse - 15, // 25: buildings.v1.BuildingService.ListBuildingRacks:output_type -> buildings.v1.ListBuildingRacksResponse - 17, // 26: buildings.v1.BuildingService.AssignRackToBuilding:output_type -> buildings.v1.AssignRackToBuildingResponse - 19, // 27: buildings.v1.BuildingService.GetBuildingStats:output_type -> buildings.v1.GetBuildingStatsResponse - 20, // [20:28] is the sub-list for method output_type - 12, // [12:20] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 16, // 11: buildings.v1.AssignRacksToBuildingRequest.racks:type_name -> buildings.v1.RackPlacement + 21, // 12: buildings.v1.GetBuildingStatsResponse.rack_health:type_name -> buildings.v1.BuildingRackHealth + 3, // 13: buildings.v1.BuildingService.ListBuildings:input_type -> buildings.v1.ListBuildingsRequest + 5, // 14: buildings.v1.BuildingService.GetBuilding:input_type -> buildings.v1.GetBuildingRequest + 7, // 15: buildings.v1.BuildingService.CreateBuilding:input_type -> buildings.v1.CreateBuildingRequest + 9, // 16: buildings.v1.BuildingService.UpdateBuilding:input_type -> buildings.v1.UpdateBuildingRequest + 11, // 17: buildings.v1.BuildingService.DeleteBuilding:input_type -> buildings.v1.DeleteBuildingRequest + 13, // 18: buildings.v1.BuildingService.ListBuildingRacks:input_type -> buildings.v1.ListBuildingRacksRequest + 17, // 19: buildings.v1.BuildingService.AssignRacksToBuilding:input_type -> buildings.v1.AssignRacksToBuildingRequest + 19, // 20: buildings.v1.BuildingService.GetBuildingStats:input_type -> buildings.v1.GetBuildingStatsRequest + 4, // 21: buildings.v1.BuildingService.ListBuildings:output_type -> buildings.v1.ListBuildingsResponse + 6, // 22: buildings.v1.BuildingService.GetBuilding:output_type -> buildings.v1.GetBuildingResponse + 8, // 23: buildings.v1.BuildingService.CreateBuilding:output_type -> buildings.v1.CreateBuildingResponse + 10, // 24: buildings.v1.BuildingService.UpdateBuilding:output_type -> buildings.v1.UpdateBuildingResponse + 12, // 25: buildings.v1.BuildingService.DeleteBuilding:output_type -> buildings.v1.DeleteBuildingResponse + 15, // 26: buildings.v1.BuildingService.ListBuildingRacks:output_type -> buildings.v1.ListBuildingRacksResponse + 18, // 27: buildings.v1.BuildingService.AssignRacksToBuilding:output_type -> buildings.v1.AssignRacksToBuildingResponse + 20, // 28: buildings.v1.BuildingService.GetBuildingStats:output_type -> buildings.v1.GetBuildingStatsResponse + 21, // [21:29] is the sub-list for method output_type + 13, // [13:21] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_buildings_v1_buildings_proto_init() } @@ -2073,14 +2129,15 @@ func file_buildings_v1_buildings_proto_init() { file_buildings_v1_buildings_proto_msgTypes[6].OneofWrappers = []any{} file_buildings_v1_buildings_proto_msgTypes[13].OneofWrappers = []any{} file_buildings_v1_buildings_proto_msgTypes[15].OneofWrappers = []any{} - file_buildings_v1_buildings_proto_msgTypes[19].OneofWrappers = []any{} + file_buildings_v1_buildings_proto_msgTypes[16].OneofWrappers = []any{} + file_buildings_v1_buildings_proto_msgTypes[20].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_buildings_v1_buildings_proto_rawDesc), len(file_buildings_v1_buildings_proto_rawDesc)), NumEnums: 1, - NumMessages: 20, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/server/generated/grpc/buildings/v1/buildingsv1connect/buildings.connect.go b/server/generated/grpc/buildings/v1/buildingsv1connect/buildings.connect.go index 1fd1ea24e..82c86a977 100644 --- a/server/generated/grpc/buildings/v1/buildingsv1connect/buildings.connect.go +++ b/server/generated/grpc/buildings/v1/buildingsv1connect/buildings.connect.go @@ -52,9 +52,9 @@ const ( // BuildingServiceListBuildingRacksProcedure is the fully-qualified name of the BuildingService's // ListBuildingRacks RPC. BuildingServiceListBuildingRacksProcedure = "/buildings.v1.BuildingService/ListBuildingRacks" - // BuildingServiceAssignRackToBuildingProcedure is the fully-qualified name of the BuildingService's - // AssignRackToBuilding RPC. - BuildingServiceAssignRackToBuildingProcedure = "/buildings.v1.BuildingService/AssignRackToBuilding" + // BuildingServiceAssignRacksToBuildingProcedure is the fully-qualified name of the + // BuildingService's AssignRacksToBuilding RPC. + BuildingServiceAssignRacksToBuildingProcedure = "/buildings.v1.BuildingService/AssignRacksToBuilding" // BuildingServiceGetBuildingStatsProcedure is the fully-qualified name of the BuildingService's // GetBuildingStats RPC. BuildingServiceGetBuildingStatsProcedure = "/buildings.v1.BuildingService/GetBuildingStats" @@ -78,7 +78,7 @@ type BuildingServiceClient interface { CreateBuilding(context.Context, *connect.Request[v1.CreateBuildingRequest]) (*connect.Response[v1.CreateBuildingResponse], error) // UpdateBuilding mutates name, description, capacity, layout // defaults. Site assignment is *not* changed by UpdateBuilding — - // use AssignBuildingToSite (SiteService) for that, since it has + // use AssignBuildingsToSite (SiteService) for that, since it has // cross-collection enforcement. UpdateBuilding(context.Context, *connect.Request[v1.UpdateBuildingRequest]) (*connect.Response[v1.UpdateBuildingResponse], error) // DeleteBuilding soft-deletes the building and, in the same @@ -91,15 +91,17 @@ type BuildingServiceClient interface { // grid view; the broader ListDeviceSets RPC stays unaware of // grid placement. ListBuildingRacks(context.Context, *connect.Request[v1.ListBuildingRacksRequest]) (*connect.Response[v1.ListBuildingRacksResponse], error) - // AssignRackToBuilding sets the rack's building_id and optionally + // AssignRacksToBuilding sets each rack's building_id and optionally // positions it at (aisle_index, position_in_aisle) inside the - // building's grid. Leaving building_id unset unassigns the rack - // from any building. Position fields must be set together or - // both unset; the server clears them automatically on any - // building_id transition. Runs the same site-cascade for - // descendant device.site_id that SaveRack runs when the rack's - // site changes. - AssignRackToBuilding(context.Context, *connect.Request[v1.AssignRackToBuildingRequest]) (*connect.Response[v1.AssignRackToBuildingResponse], error) + // target building's grid. Leaving target_building_id unset + // unassigns every rack in the batch from any building. Each entry + // carries its own optional placement; entries with unset placement + // have their grid cell cleared (single-call mix of placed + cleared + // racks is supported). The entire batch runs in one transaction; if + // any rack fails validation or its update, no row is touched. Runs + // the same site-cascade for descendant device.site_id that SaveRack + // runs when a rack's site changes. + AssignRacksToBuilding(context.Context, *connect.Request[v1.AssignRacksToBuildingRequest]) (*connect.Response[v1.AssignRacksToBuildingResponse], error) // GetBuildingStats returns server-rolled telemetry + miner-state // counts for every device whose rack lives in the building, plus a // per-rack health summary (rack id, label, position, state counts) @@ -148,9 +150,9 @@ func NewBuildingServiceClient(httpClient connect.HTTPClient, baseURL string, opt baseURL+BuildingServiceListBuildingRacksProcedure, opts..., ), - assignRackToBuilding: connect.NewClient[v1.AssignRackToBuildingRequest, v1.AssignRackToBuildingResponse]( + assignRacksToBuilding: connect.NewClient[v1.AssignRacksToBuildingRequest, v1.AssignRacksToBuildingResponse]( httpClient, - baseURL+BuildingServiceAssignRackToBuildingProcedure, + baseURL+BuildingServiceAssignRacksToBuildingProcedure, opts..., ), getBuildingStats: connect.NewClient[v1.GetBuildingStatsRequest, v1.GetBuildingStatsResponse]( @@ -163,14 +165,14 @@ func NewBuildingServiceClient(httpClient connect.HTTPClient, baseURL string, opt // buildingServiceClient implements BuildingServiceClient. type buildingServiceClient struct { - listBuildings *connect.Client[v1.ListBuildingsRequest, v1.ListBuildingsResponse] - getBuilding *connect.Client[v1.GetBuildingRequest, v1.GetBuildingResponse] - createBuilding *connect.Client[v1.CreateBuildingRequest, v1.CreateBuildingResponse] - updateBuilding *connect.Client[v1.UpdateBuildingRequest, v1.UpdateBuildingResponse] - deleteBuilding *connect.Client[v1.DeleteBuildingRequest, v1.DeleteBuildingResponse] - listBuildingRacks *connect.Client[v1.ListBuildingRacksRequest, v1.ListBuildingRacksResponse] - assignRackToBuilding *connect.Client[v1.AssignRackToBuildingRequest, v1.AssignRackToBuildingResponse] - getBuildingStats *connect.Client[v1.GetBuildingStatsRequest, v1.GetBuildingStatsResponse] + listBuildings *connect.Client[v1.ListBuildingsRequest, v1.ListBuildingsResponse] + getBuilding *connect.Client[v1.GetBuildingRequest, v1.GetBuildingResponse] + createBuilding *connect.Client[v1.CreateBuildingRequest, v1.CreateBuildingResponse] + updateBuilding *connect.Client[v1.UpdateBuildingRequest, v1.UpdateBuildingResponse] + deleteBuilding *connect.Client[v1.DeleteBuildingRequest, v1.DeleteBuildingResponse] + listBuildingRacks *connect.Client[v1.ListBuildingRacksRequest, v1.ListBuildingRacksResponse] + assignRacksToBuilding *connect.Client[v1.AssignRacksToBuildingRequest, v1.AssignRacksToBuildingResponse] + getBuildingStats *connect.Client[v1.GetBuildingStatsRequest, v1.GetBuildingStatsResponse] } // ListBuildings calls buildings.v1.BuildingService.ListBuildings. @@ -203,9 +205,9 @@ func (c *buildingServiceClient) ListBuildingRacks(ctx context.Context, req *conn return c.listBuildingRacks.CallUnary(ctx, req) } -// AssignRackToBuilding calls buildings.v1.BuildingService.AssignRackToBuilding. -func (c *buildingServiceClient) AssignRackToBuilding(ctx context.Context, req *connect.Request[v1.AssignRackToBuildingRequest]) (*connect.Response[v1.AssignRackToBuildingResponse], error) { - return c.assignRackToBuilding.CallUnary(ctx, req) +// AssignRacksToBuilding calls buildings.v1.BuildingService.AssignRacksToBuilding. +func (c *buildingServiceClient) AssignRacksToBuilding(ctx context.Context, req *connect.Request[v1.AssignRacksToBuildingRequest]) (*connect.Response[v1.AssignRacksToBuildingResponse], error) { + return c.assignRacksToBuilding.CallUnary(ctx, req) } // GetBuildingStats calls buildings.v1.BuildingService.GetBuildingStats. @@ -231,7 +233,7 @@ type BuildingServiceHandler interface { CreateBuilding(context.Context, *connect.Request[v1.CreateBuildingRequest]) (*connect.Response[v1.CreateBuildingResponse], error) // UpdateBuilding mutates name, description, capacity, layout // defaults. Site assignment is *not* changed by UpdateBuilding — - // use AssignBuildingToSite (SiteService) for that, since it has + // use AssignBuildingsToSite (SiteService) for that, since it has // cross-collection enforcement. UpdateBuilding(context.Context, *connect.Request[v1.UpdateBuildingRequest]) (*connect.Response[v1.UpdateBuildingResponse], error) // DeleteBuilding soft-deletes the building and, in the same @@ -244,15 +246,17 @@ type BuildingServiceHandler interface { // grid view; the broader ListDeviceSets RPC stays unaware of // grid placement. ListBuildingRacks(context.Context, *connect.Request[v1.ListBuildingRacksRequest]) (*connect.Response[v1.ListBuildingRacksResponse], error) - // AssignRackToBuilding sets the rack's building_id and optionally + // AssignRacksToBuilding sets each rack's building_id and optionally // positions it at (aisle_index, position_in_aisle) inside the - // building's grid. Leaving building_id unset unassigns the rack - // from any building. Position fields must be set together or - // both unset; the server clears them automatically on any - // building_id transition. Runs the same site-cascade for - // descendant device.site_id that SaveRack runs when the rack's - // site changes. - AssignRackToBuilding(context.Context, *connect.Request[v1.AssignRackToBuildingRequest]) (*connect.Response[v1.AssignRackToBuildingResponse], error) + // target building's grid. Leaving target_building_id unset + // unassigns every rack in the batch from any building. Each entry + // carries its own optional placement; entries with unset placement + // have their grid cell cleared (single-call mix of placed + cleared + // racks is supported). The entire batch runs in one transaction; if + // any rack fails validation or its update, no row is touched. Runs + // the same site-cascade for descendant device.site_id that SaveRack + // runs when a rack's site changes. + AssignRacksToBuilding(context.Context, *connect.Request[v1.AssignRacksToBuildingRequest]) (*connect.Response[v1.AssignRacksToBuildingResponse], error) // GetBuildingStats returns server-rolled telemetry + miner-state // counts for every device whose rack lives in the building, plus a // per-rack health summary (rack id, label, position, state counts) @@ -297,9 +301,9 @@ func NewBuildingServiceHandler(svc BuildingServiceHandler, opts ...connect.Handl svc.ListBuildingRacks, opts..., ) - buildingServiceAssignRackToBuildingHandler := connect.NewUnaryHandler( - BuildingServiceAssignRackToBuildingProcedure, - svc.AssignRackToBuilding, + buildingServiceAssignRacksToBuildingHandler := connect.NewUnaryHandler( + BuildingServiceAssignRacksToBuildingProcedure, + svc.AssignRacksToBuilding, opts..., ) buildingServiceGetBuildingStatsHandler := connect.NewUnaryHandler( @@ -321,8 +325,8 @@ func NewBuildingServiceHandler(svc BuildingServiceHandler, opts ...connect.Handl buildingServiceDeleteBuildingHandler.ServeHTTP(w, r) case BuildingServiceListBuildingRacksProcedure: buildingServiceListBuildingRacksHandler.ServeHTTP(w, r) - case BuildingServiceAssignRackToBuildingProcedure: - buildingServiceAssignRackToBuildingHandler.ServeHTTP(w, r) + case BuildingServiceAssignRacksToBuildingProcedure: + buildingServiceAssignRacksToBuildingHandler.ServeHTTP(w, r) case BuildingServiceGetBuildingStatsProcedure: buildingServiceGetBuildingStatsHandler.ServeHTTP(w, r) default: @@ -358,8 +362,8 @@ func (UnimplementedBuildingServiceHandler) ListBuildingRacks(context.Context, *c return nil, connect.NewError(connect.CodeUnimplemented, errors.New("buildings.v1.BuildingService.ListBuildingRacks is not implemented")) } -func (UnimplementedBuildingServiceHandler) AssignRackToBuilding(context.Context, *connect.Request[v1.AssignRackToBuildingRequest]) (*connect.Response[v1.AssignRackToBuildingResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("buildings.v1.BuildingService.AssignRackToBuilding is not implemented")) +func (UnimplementedBuildingServiceHandler) AssignRacksToBuilding(context.Context, *connect.Request[v1.AssignRacksToBuildingRequest]) (*connect.Response[v1.AssignRacksToBuildingResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("buildings.v1.BuildingService.AssignRacksToBuilding is not implemented")) } func (UnimplementedBuildingServiceHandler) GetBuildingStats(context.Context, *connect.Request[v1.GetBuildingStatsRequest]) (*connect.Response[v1.GetBuildingStatsResponse], error) { diff --git a/server/generated/grpc/collection/v1/collection.pb.go b/server/generated/grpc/collection/v1/collection.pb.go index 7a3fad121..506a9b068 100644 --- a/server/generated/grpc/collection/v1/collection.pb.go +++ b/server/generated/grpc/collection/v1/collection.pb.go @@ -1406,231 +1406,6 @@ func (x *ListCollectionsResponse) GetTotalCount() int32 { return 0 } -// Request to add devices to a collection. -// -// When the target is a site-stamped rack, the server rewrites -// device.site_id to match the rack for every added device. The affected -// count is returned in site_reassigned_count. Groups are exempt. -type AddDevicesToCollectionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // ID of the collection to add devices to - CollectionId int64 `protobuf:"varint,1,opt,name=collection_id,json=collectionId,proto3" json:"collection_id,omitempty"` - // Devices to add: specific list or all paired devices - DeviceSelector *v1.DeviceSelector `protobuf:"bytes,2,opt,name=device_selector,json=deviceSelector,proto3" json:"device_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AddDevicesToCollectionRequest) Reset() { - *x = AddDevicesToCollectionRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AddDevicesToCollectionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AddDevicesToCollectionRequest) ProtoMessage() {} - -func (x *AddDevicesToCollectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[16] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AddDevicesToCollectionRequest.ProtoReflect.Descriptor instead. -func (*AddDevicesToCollectionRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{16} -} - -func (x *AddDevicesToCollectionRequest) GetCollectionId() int64 { - if x != nil { - return x.CollectionId - } - return 0 -} - -func (x *AddDevicesToCollectionRequest) GetDeviceSelector() *v1.DeviceSelector { - if x != nil { - return x.DeviceSelector - } - return nil -} - -// Response after adding devices to a collection -type AddDevicesToCollectionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // ID of the collection devices were added to - CollectionId int64 `protobuf:"varint,1,opt,name=collection_id,json=collectionId,proto3" json:"collection_id,omitempty"` - // Number of devices successfully added - // May be less than requested if some devices were already members - AddedCount int32 `protobuf:"varint,2,opt,name=added_count,json=addedCount,proto3" json:"added_count,omitempty"` - // Number of devices whose site_id was rewritten by the cascade. - SiteReassignedCount int32 `protobuf:"varint,3,opt,name=site_reassigned_count,json=siteReassignedCount,proto3" json:"site_reassigned_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AddDevicesToCollectionResponse) Reset() { - *x = AddDevicesToCollectionResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AddDevicesToCollectionResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AddDevicesToCollectionResponse) ProtoMessage() {} - -func (x *AddDevicesToCollectionResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AddDevicesToCollectionResponse.ProtoReflect.Descriptor instead. -func (*AddDevicesToCollectionResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{17} -} - -func (x *AddDevicesToCollectionResponse) GetCollectionId() int64 { - if x != nil { - return x.CollectionId - } - return 0 -} - -func (x *AddDevicesToCollectionResponse) GetAddedCount() int32 { - if x != nil { - return x.AddedCount - } - return 0 -} - -func (x *AddDevicesToCollectionResponse) GetSiteReassignedCount() int32 { - if x != nil { - return x.SiteReassignedCount - } - return 0 -} - -// Request to remove devices from a collection -type RemoveDevicesFromCollectionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // ID of the collection to remove devices from - CollectionId int64 `protobuf:"varint,1,opt,name=collection_id,json=collectionId,proto3" json:"collection_id,omitempty"` - // Devices to remove: specific list or all paired devices - DeviceSelector *v1.DeviceSelector `protobuf:"bytes,2,opt,name=device_selector,json=deviceSelector,proto3" json:"device_selector,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RemoveDevicesFromCollectionRequest) Reset() { - *x = RemoveDevicesFromCollectionRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RemoveDevicesFromCollectionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RemoveDevicesFromCollectionRequest) ProtoMessage() {} - -func (x *RemoveDevicesFromCollectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RemoveDevicesFromCollectionRequest.ProtoReflect.Descriptor instead. -func (*RemoveDevicesFromCollectionRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{18} -} - -func (x *RemoveDevicesFromCollectionRequest) GetCollectionId() int64 { - if x != nil { - return x.CollectionId - } - return 0 -} - -func (x *RemoveDevicesFromCollectionRequest) GetDeviceSelector() *v1.DeviceSelector { - if x != nil { - return x.DeviceSelector - } - return nil -} - -// Response after removing devices from a collection -type RemoveDevicesFromCollectionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Number of devices successfully removed - RemovedCount int32 `protobuf:"varint,1,opt,name=removed_count,json=removedCount,proto3" json:"removed_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RemoveDevicesFromCollectionResponse) Reset() { - *x = RemoveDevicesFromCollectionResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *RemoveDevicesFromCollectionResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RemoveDevicesFromCollectionResponse) ProtoMessage() {} - -func (x *RemoveDevicesFromCollectionResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[19] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RemoveDevicesFromCollectionResponse.ProtoReflect.Descriptor instead. -func (*RemoveDevicesFromCollectionResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{19} -} - -func (x *RemoveDevicesFromCollectionResponse) GetRemovedCount() int32 { - if x != nil { - return x.RemovedCount - } - return 0 -} - // Request to list members of a collection type ListCollectionMembersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1646,7 +1421,7 @@ type ListCollectionMembersRequest struct { func (x *ListCollectionMembersRequest) Reset() { *x = ListCollectionMembersRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[20] + mi := &file_collection_v1_collection_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1658,7 +1433,7 @@ func (x *ListCollectionMembersRequest) String() string { func (*ListCollectionMembersRequest) ProtoMessage() {} func (x *ListCollectionMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[20] + mi := &file_collection_v1_collection_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1671,7 +1446,7 @@ func (x *ListCollectionMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListCollectionMembersRequest.ProtoReflect.Descriptor instead. func (*ListCollectionMembersRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{20} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{16} } func (x *ListCollectionMembersRequest) GetCollectionId() int64 { @@ -1708,7 +1483,7 @@ type ListCollectionMembersResponse struct { func (x *ListCollectionMembersResponse) Reset() { *x = ListCollectionMembersResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[21] + mi := &file_collection_v1_collection_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1720,7 +1495,7 @@ func (x *ListCollectionMembersResponse) String() string { func (*ListCollectionMembersResponse) ProtoMessage() {} func (x *ListCollectionMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[21] + mi := &file_collection_v1_collection_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1733,7 +1508,7 @@ func (x *ListCollectionMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListCollectionMembersResponse.ProtoReflect.Descriptor instead. func (*ListCollectionMembersResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{21} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{17} } func (x *ListCollectionMembersResponse) GetMembers() []*CollectionMember { @@ -1763,7 +1538,7 @@ type GetDeviceCollectionsRequest struct { func (x *GetDeviceCollectionsRequest) Reset() { *x = GetDeviceCollectionsRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[22] + mi := &file_collection_v1_collection_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1775,7 +1550,7 @@ func (x *GetDeviceCollectionsRequest) String() string { func (*GetDeviceCollectionsRequest) ProtoMessage() {} func (x *GetDeviceCollectionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[22] + mi := &file_collection_v1_collection_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1788,7 +1563,7 @@ func (x *GetDeviceCollectionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDeviceCollectionsRequest.ProtoReflect.Descriptor instead. func (*GetDeviceCollectionsRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{22} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{18} } func (x *GetDeviceCollectionsRequest) GetDeviceIdentifier() string { @@ -1816,7 +1591,7 @@ type GetDeviceCollectionsResponse struct { func (x *GetDeviceCollectionsResponse) Reset() { *x = GetDeviceCollectionsResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[23] + mi := &file_collection_v1_collection_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1828,7 +1603,7 @@ func (x *GetDeviceCollectionsResponse) String() string { func (*GetDeviceCollectionsResponse) ProtoMessage() {} func (x *GetDeviceCollectionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[23] + mi := &file_collection_v1_collection_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1841,7 +1616,7 @@ func (x *GetDeviceCollectionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDeviceCollectionsResponse.ProtoReflect.Descriptor instead. func (*GetDeviceCollectionsResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{23} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{19} } func (x *GetDeviceCollectionsResponse) GetCollections() []*DeviceCollection { @@ -1866,7 +1641,7 @@ type SetRackSlotPositionRequest struct { func (x *SetRackSlotPositionRequest) Reset() { *x = SetRackSlotPositionRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[24] + mi := &file_collection_v1_collection_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1878,7 +1653,7 @@ func (x *SetRackSlotPositionRequest) String() string { func (*SetRackSlotPositionRequest) ProtoMessage() {} func (x *SetRackSlotPositionRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[24] + mi := &file_collection_v1_collection_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1891,7 +1666,7 @@ func (x *SetRackSlotPositionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRackSlotPositionRequest.ProtoReflect.Descriptor instead. func (*SetRackSlotPositionRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{24} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{20} } func (x *SetRackSlotPositionRequest) GetCollectionId() int64 { @@ -1928,7 +1703,7 @@ type SetRackSlotPositionResponse struct { func (x *SetRackSlotPositionResponse) Reset() { *x = SetRackSlotPositionResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[25] + mi := &file_collection_v1_collection_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1940,7 +1715,7 @@ func (x *SetRackSlotPositionResponse) String() string { func (*SetRackSlotPositionResponse) ProtoMessage() {} func (x *SetRackSlotPositionResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[25] + mi := &file_collection_v1_collection_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1953,7 +1728,7 @@ func (x *SetRackSlotPositionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRackSlotPositionResponse.ProtoReflect.Descriptor instead. func (*SetRackSlotPositionResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{25} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{21} } func (x *SetRackSlotPositionResponse) GetCollectionId() int64 { @@ -1983,7 +1758,7 @@ type ClearRackSlotPositionRequest struct { func (x *ClearRackSlotPositionRequest) Reset() { *x = ClearRackSlotPositionRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[26] + mi := &file_collection_v1_collection_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1995,7 +1770,7 @@ func (x *ClearRackSlotPositionRequest) String() string { func (*ClearRackSlotPositionRequest) ProtoMessage() {} func (x *ClearRackSlotPositionRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[26] + mi := &file_collection_v1_collection_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2008,7 +1783,7 @@ func (x *ClearRackSlotPositionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearRackSlotPositionRequest.ProtoReflect.Descriptor instead. func (*ClearRackSlotPositionRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{26} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{22} } func (x *ClearRackSlotPositionRequest) GetCollectionId() int64 { @@ -2034,7 +1809,7 @@ type ClearRackSlotPositionResponse struct { func (x *ClearRackSlotPositionResponse) Reset() { *x = ClearRackSlotPositionResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[27] + mi := &file_collection_v1_collection_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2046,7 +1821,7 @@ func (x *ClearRackSlotPositionResponse) String() string { func (*ClearRackSlotPositionResponse) ProtoMessage() {} func (x *ClearRackSlotPositionResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[27] + mi := &file_collection_v1_collection_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2059,7 +1834,7 @@ func (x *ClearRackSlotPositionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearRackSlotPositionResponse.ProtoReflect.Descriptor instead. func (*ClearRackSlotPositionResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{27} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{23} } // Request to list all occupied slots in a rack @@ -2073,7 +1848,7 @@ type GetRackSlotsRequest struct { func (x *GetRackSlotsRequest) Reset() { *x = GetRackSlotsRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[28] + mi := &file_collection_v1_collection_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2085,7 +1860,7 @@ func (x *GetRackSlotsRequest) String() string { func (*GetRackSlotsRequest) ProtoMessage() {} func (x *GetRackSlotsRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[28] + mi := &file_collection_v1_collection_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2098,7 +1873,7 @@ func (x *GetRackSlotsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRackSlotsRequest.ProtoReflect.Descriptor instead. func (*GetRackSlotsRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{28} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{24} } func (x *GetRackSlotsRequest) GetCollectionId() int64 { @@ -2121,7 +1896,7 @@ type RackSlot struct { func (x *RackSlot) Reset() { *x = RackSlot{} - mi := &file_collection_v1_collection_proto_msgTypes[29] + mi := &file_collection_v1_collection_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2133,7 +1908,7 @@ func (x *RackSlot) String() string { func (*RackSlot) ProtoMessage() {} func (x *RackSlot) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[29] + mi := &file_collection_v1_collection_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2146,7 +1921,7 @@ func (x *RackSlot) ProtoReflect() protoreflect.Message { // Deprecated: Use RackSlot.ProtoReflect.Descriptor instead. func (*RackSlot) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{29} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{25} } func (x *RackSlot) GetDeviceIdentifier() string { @@ -2174,7 +1949,7 @@ type GetRackSlotsResponse struct { func (x *GetRackSlotsResponse) Reset() { *x = GetRackSlotsResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[30] + mi := &file_collection_v1_collection_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2186,7 +1961,7 @@ func (x *GetRackSlotsResponse) String() string { func (*GetRackSlotsResponse) ProtoMessage() {} func (x *GetRackSlotsResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[30] + mi := &file_collection_v1_collection_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2199,7 +1974,7 @@ func (x *GetRackSlotsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRackSlotsResponse.ProtoReflect.Descriptor instead. func (*GetRackSlotsResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{30} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{26} } func (x *GetRackSlotsResponse) GetSlots() []*RackSlot { @@ -2248,7 +2023,7 @@ type CollectionStats struct { func (x *CollectionStats) Reset() { *x = CollectionStats{} - mi := &file_collection_v1_collection_proto_msgTypes[31] + mi := &file_collection_v1_collection_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2260,7 +2035,7 @@ func (x *CollectionStats) String() string { func (*CollectionStats) ProtoMessage() {} func (x *CollectionStats) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[31] + mi := &file_collection_v1_collection_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2273,7 +2048,7 @@ func (x *CollectionStats) ProtoReflect() protoreflect.Message { // Deprecated: Use CollectionStats.ProtoReflect.Descriptor instead. func (*CollectionStats) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{31} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{27} } func (x *CollectionStats) GetCollectionId() int64 { @@ -2434,7 +2209,7 @@ type GetCollectionStatsRequest struct { func (x *GetCollectionStatsRequest) Reset() { *x = GetCollectionStatsRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[32] + mi := &file_collection_v1_collection_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2446,7 +2221,7 @@ func (x *GetCollectionStatsRequest) String() string { func (*GetCollectionStatsRequest) ProtoMessage() {} func (x *GetCollectionStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[32] + mi := &file_collection_v1_collection_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2459,7 +2234,7 @@ func (x *GetCollectionStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCollectionStatsRequest.ProtoReflect.Descriptor instead. func (*GetCollectionStatsRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{32} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{28} } func (x *GetCollectionStatsRequest) GetCollectionIds() []int64 { @@ -2480,7 +2255,7 @@ type GetCollectionStatsResponse struct { func (x *GetCollectionStatsResponse) Reset() { *x = GetCollectionStatsResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[33] + mi := &file_collection_v1_collection_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2492,7 +2267,7 @@ func (x *GetCollectionStatsResponse) String() string { func (*GetCollectionStatsResponse) ProtoMessage() {} func (x *GetCollectionStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[33] + mi := &file_collection_v1_collection_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2505,7 +2280,7 @@ func (x *GetCollectionStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCollectionStatsResponse.ProtoReflect.Descriptor instead. func (*GetCollectionStatsResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{33} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{29} } func (x *GetCollectionStatsResponse) GetStats() []*CollectionStats { @@ -2530,7 +2305,7 @@ type RackSlotStatus struct { func (x *RackSlotStatus) Reset() { *x = RackSlotStatus{} - mi := &file_collection_v1_collection_proto_msgTypes[34] + mi := &file_collection_v1_collection_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2542,7 +2317,7 @@ func (x *RackSlotStatus) String() string { func (*RackSlotStatus) ProtoMessage() {} func (x *RackSlotStatus) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[34] + mi := &file_collection_v1_collection_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2555,7 +2330,7 @@ func (x *RackSlotStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use RackSlotStatus.ProtoReflect.Descriptor instead. func (*RackSlotStatus) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{34} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{30} } func (x *RackSlotStatus) GetRow() int32 { @@ -2588,7 +2363,7 @@ type ListRackZonesRequest struct { func (x *ListRackZonesRequest) Reset() { *x = ListRackZonesRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[35] + mi := &file_collection_v1_collection_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2600,7 +2375,7 @@ func (x *ListRackZonesRequest) String() string { func (*ListRackZonesRequest) ProtoMessage() {} func (x *ListRackZonesRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[35] + mi := &file_collection_v1_collection_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2613,7 +2388,7 @@ func (x *ListRackZonesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRackZonesRequest.ProtoReflect.Descriptor instead. func (*ListRackZonesRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{35} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{31} } // Response containing all distinct rack zones @@ -2627,7 +2402,7 @@ type ListRackZonesResponse struct { func (x *ListRackZonesResponse) Reset() { *x = ListRackZonesResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[36] + mi := &file_collection_v1_collection_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2639,7 +2414,7 @@ func (x *ListRackZonesResponse) String() string { func (*ListRackZonesResponse) ProtoMessage() {} func (x *ListRackZonesResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[36] + mi := &file_collection_v1_collection_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2652,7 +2427,7 @@ func (x *ListRackZonesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRackZonesResponse.ProtoReflect.Descriptor instead. func (*ListRackZonesResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{36} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{32} } func (x *ListRackZonesResponse) GetZones() []string { @@ -2671,7 +2446,7 @@ type ListRackTypesRequest struct { func (x *ListRackTypesRequest) Reset() { *x = ListRackTypesRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[37] + mi := &file_collection_v1_collection_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2683,7 +2458,7 @@ func (x *ListRackTypesRequest) String() string { func (*ListRackTypesRequest) ProtoMessage() {} func (x *ListRackTypesRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[37] + mi := &file_collection_v1_collection_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2696,7 +2471,7 @@ func (x *ListRackTypesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRackTypesRequest.ProtoReflect.Descriptor instead. func (*ListRackTypesRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{37} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{33} } // A rack type defined by its row/column dimensions and how many racks use it @@ -2714,7 +2489,7 @@ type RackType struct { func (x *RackType) Reset() { *x = RackType{} - mi := &file_collection_v1_collection_proto_msgTypes[38] + mi := &file_collection_v1_collection_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2726,7 +2501,7 @@ func (x *RackType) String() string { func (*RackType) ProtoMessage() {} func (x *RackType) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[38] + mi := &file_collection_v1_collection_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2739,7 +2514,7 @@ func (x *RackType) ProtoReflect() protoreflect.Message { // Deprecated: Use RackType.ProtoReflect.Descriptor instead. func (*RackType) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{38} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{34} } func (x *RackType) GetRows() int32 { @@ -2774,7 +2549,7 @@ type ListRackTypesResponse struct { func (x *ListRackTypesResponse) Reset() { *x = ListRackTypesResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[39] + mi := &file_collection_v1_collection_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2786,7 +2561,7 @@ func (x *ListRackTypesResponse) String() string { func (*ListRackTypesResponse) ProtoMessage() {} func (x *ListRackTypesResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[39] + mi := &file_collection_v1_collection_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2799,7 +2574,7 @@ func (x *ListRackTypesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRackTypesResponse.ProtoReflect.Descriptor instead. func (*ListRackTypesResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{39} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{35} } func (x *ListRackTypesResponse) GetRackTypes() []*RackType { @@ -2835,7 +2610,7 @@ type SaveRackRequest struct { func (x *SaveRackRequest) Reset() { *x = SaveRackRequest{} - mi := &file_collection_v1_collection_proto_msgTypes[40] + mi := &file_collection_v1_collection_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2847,7 +2622,7 @@ func (x *SaveRackRequest) String() string { func (*SaveRackRequest) ProtoMessage() {} func (x *SaveRackRequest) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[40] + mi := &file_collection_v1_collection_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2860,7 +2635,7 @@ func (x *SaveRackRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveRackRequest.ProtoReflect.Descriptor instead. func (*SaveRackRequest) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{40} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{36} } func (x *SaveRackRequest) GetCollectionId() int64 { @@ -2913,7 +2688,7 @@ type SaveRackResponse struct { func (x *SaveRackResponse) Reset() { *x = SaveRackResponse{} - mi := &file_collection_v1_collection_proto_msgTypes[41] + mi := &file_collection_v1_collection_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2925,7 +2700,7 @@ func (x *SaveRackResponse) String() string { func (*SaveRackResponse) ProtoMessage() {} func (x *SaveRackResponse) ProtoReflect() protoreflect.Message { - mi := &file_collection_v1_collection_proto_msgTypes[41] + mi := &file_collection_v1_collection_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2938,7 +2713,7 @@ func (x *SaveRackResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveRackResponse.ProtoReflect.Descriptor instead. func (*SaveRackResponse) Descriptor() ([]byte, []int) { - return file_collection_v1_collection_proto_rawDescGZIP(), []int{41} + return file_collection_v1_collection_proto_rawDescGZIP(), []int{37} } func (x *SaveRackResponse) GetCollection() *DeviceCollection { @@ -3156,405 +2931,355 @@ var file_collection_v1_collection_proto_rawDesc = string([]byte{ 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x99, 0x01, 0x0a, 0x1d, 0x41, 0x64, 0x64, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, - 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x22, 0x9a, 0x01, 0x0a, 0x1e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x54, 0x6f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x61, - 0x64, 0x64, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, - 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x73, 0x69, 0x74, - 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x22, 0x9e, 0x01, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, - 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, - 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x22, 0x4a, 0x0a, 0x23, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, - 0x76, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x91, 0x01, - 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, - 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x09, - 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, - 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x22, 0x82, 0x01, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, - 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x26, - 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x86, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x31, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, - 0x61, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x41, 0x0a, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x22, 0xc5, 0x01, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, - 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, - 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, - 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, - 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, - 0x52, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6f, 0x0a, 0x1b, 0x53, 0x65, - 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, - 0x0a, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, - 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x22, 0x82, 0x01, 0x0a, 0x1c, - 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x22, 0x1f, 0x0a, 0x1d, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, - 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x43, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, + 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x91, 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x74, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, - 0x6f, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, + 0x28, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x82, 0x01, 0x0a, 0x1d, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, + 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, + 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x22, 0x86, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x31, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x61, 0x0a, 0x1c, 0x47, 0x65, 0x74, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0b, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc5, 0x01, 0x0a, + 0x1a, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, - 0x3b, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x43, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x14, - 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x05, 0x73, 0x6c, - 0x6f, 0x74, 0x73, 0x22, 0xe0, 0x07, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, - 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, - 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6a, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, 0x45, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, - 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, - 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x4b, 0x77, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x69, - 0x6e, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x6d, 0x69, 0x6e, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x74, 0x65, - 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x01, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x43, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, - 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, - 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, - 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, - 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, - 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, - 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, - 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, - 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x19, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x62, 0x6f, - 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x6f, - 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, - 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x12, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x66, 0x61, 0x6e, 0x49, 0x73, 0x73, 0x75, 0x65, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x16, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x62, 0x6f, - 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x13, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x68, 0x61, 0x73, 0x68, 0x42, 0x6f, 0x61, 0x72, 0x64, - 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x73, - 0x75, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0d, 0x70, 0x73, 0x75, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x0d, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x65, 0x73, 0x18, 0x15, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, - 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0c, 0x73, 0x6c, 0x6f, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x22, 0x42, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x22, 0x52, 0x0a, 0x1a, 0x47, 0x65, - 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x73, - 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x10, 0x0a, 0x03, 0x72, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x72, - 0x6f, 0x77, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6c, 0x6f, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, - 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x15, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x57, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x72, 0x6f, - 0x77, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4f, 0x0a, 0x15, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xcb, 0x02, 0x0a, - 0x0f, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x34, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xd8, 0x01, 0x01, 0x22, - 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x72, 0x04, 0x10, - 0x01, 0x18, 0x64, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x3c, 0x0a, 0x09, 0x72, 0x61, - 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x6f, 0x6e, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6f, 0x0a, 0x1b, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, + 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x04, 0x73, 0x6c, 0x6f, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, + 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x22, 0x82, 0x01, 0x0a, 0x1c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, + 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x1d, 0x43, 0x6c, + 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x43, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, + 0x20, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x22, 0x74, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x12, 0x2b, 0x0a, 0x11, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, + 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, + 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, + 0x0a, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, - 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, - 0x72, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, - 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x12, 0x42, 0x0a, 0x10, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x61, 0x73, 0x73, - 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x0f, 0x73, 0x6c, 0x6f, 0x74, 0x41, 0x73, 0x73, - 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xae, 0x01, 0x0a, 0x10, 0x53, - 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3f, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, - 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0x66, 0x0a, 0x0e, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, - 0x1b, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, - 0x0a, 0x15, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4c, - 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, - 0x4b, 0x10, 0x02, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, - 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, - 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, - 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, - 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, - 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, - 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, - 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, - 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, - 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a, 0x70, 0x0a, 0x0f, - 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, - 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, - 0x1b, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x49, 0x4d, 0x4d, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x2a, 0xdd, - 0x01, 0x0a, 0x10, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c, 0x4f, 0x54, 0x5f, - 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4d, - 0x50, 0x54, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48, 0x45, 0x41, 0x4c, - 0x54, 0x48, 0x59, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, - 0x53, 0x5f, 0x41, 0x54, 0x54, 0x45, 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1e, 0x0a, - 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12, 0x1f, 0x0a, - 0x1b, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4c, 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x32, 0x94, - 0x0d, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, - 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x5a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x63, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x16, 0x41, 0x64, 0x64, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x43, 0x6f, + 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x22, 0xe0, 0x07, + 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, + 0x72, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, + 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, + 0x63, 0x79, 0x5f, 0x6a, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, + 0x67, 0x45, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, + 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, + 0x65, 0x72, 0x4b, 0x77, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x69, 0x6e, 0x5f, 0x74, 0x65, 0x6d, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, + 0x0f, 0x6d, 0x69, 0x6e, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, + 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x6d, 0x61, 0x78, + 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x12, 0x23, 0x0a, 0x0d, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, + 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, + 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, + 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, + 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, + 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1b, + 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x19, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, + 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0d, 0x66, 0x61, 0x6e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x33, 0x0a, 0x16, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x13, 0x68, 0x61, 0x73, 0x68, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x73, 0x75, 0x5f, 0x69, 0x73, 0x73, 0x75, + 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x70, + 0x73, 0x75, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x0d, + 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x15, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x0c, 0x73, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, + 0x22, 0x42, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, + 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x73, 0x22, 0x52, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x73, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, + 0x53, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x6f, + 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x72, 0x6f, 0x77, 0x12, 0x16, 0x0a, 0x06, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x6f, + 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x16, 0x0a, + 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, + 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x7a, + 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, + 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x57, 0x0a, 0x08, + 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x63, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4f, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, + 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, + 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x72, 0x61, 0x63, + 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xcb, 0x02, 0x0a, 0x0f, 0x53, 0x61, 0x76, 0x65, 0x52, + 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x0d, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xd8, 0x01, 0x01, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, + 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, + 0x12, 0x22, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x0c, 0xba, 0x48, 0x09, 0xc8, 0x01, 0x01, 0x72, 0x04, 0x10, 0x01, 0x18, 0x64, 0x52, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x12, 0x3c, 0x0a, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x66, + 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, + 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x42, + 0x0a, 0x10, 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, + 0x74, 0x52, 0x0f, 0x73, 0x6c, 0x6f, 0x74, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xae, 0x01, 0x0a, 0x10, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0x66, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x43, 0x4f, 0x4c, 0x4c, 0x45, + 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4c, 0x4c, + 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x52, 0x4f, 0x55, + 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x10, 0x02, 0x2a, 0xb6, 0x01, + 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, + 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, + 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, + 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, + 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, + 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, + 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, + 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, + 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, + 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a, 0x70, 0x0a, 0x0f, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, + 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, + 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x41, 0x43, 0x4b, 0x5f, + 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4d, 0x4d, + 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x2a, 0xdd, 0x01, 0x0a, 0x10, 0x53, 0x6c, 0x6f, + 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, + 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4d, 0x50, 0x54, 0x59, 0x10, 0x01, 0x12, + 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x02, 0x12, + 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x53, 0x5f, 0x41, 0x54, 0x54, 0x45, + 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, + 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, + 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4c, 0x4f, 0x54, 0x5f, + 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4c, + 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x32, 0x96, 0x0b, 0x0a, 0x17, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2d, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x84, - 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, - 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, + 0x24, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, - 0x72, 0x6f, 0x6d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2b, - 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x47, 0x65, 0x74, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, + 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, + 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x60, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x72, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x53, 0x65, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, + 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x29, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, + 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, + 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, + 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, 0x6c, 0x65, 0x61, - 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, - 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, - 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, + 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, + 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x28, 0x2e, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, - 0x73, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, - 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x23, 0x2e, + 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x69, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x28, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x29, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x61, 0x76, 0x65, - 0x52, 0x61, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xc8, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x42, 0x0f, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4d, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, - 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, - 0x3b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x76, 0x31, 0xa2, 0x02, 0x03, - 0x43, 0x58, 0x58, 0xaa, 0x02, 0x0d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x19, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x3a, 0x56, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, + 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, + 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, + 0x12, 0x1e, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1f, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0xc8, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x42, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, + 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x58, 0x58, 0xaa, + 0x02, 0x0d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x31, 0xca, + 0x02, 0x0d, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x56, 0x31, 0xe2, + 0x02, 0x19, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5c, 0x56, 0x31, 0x5c, + 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -3570,138 +3295,128 @@ func file_collection_v1_collection_proto_rawDescGZIP() []byte { } var file_collection_v1_collection_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_collection_v1_collection_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_collection_v1_collection_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_collection_v1_collection_proto_goTypes = []any{ - (CollectionType)(0), // 0: collection.v1.CollectionType - (RackOrderIndex)(0), // 1: collection.v1.RackOrderIndex - (RackCoolingType)(0), // 2: collection.v1.RackCoolingType - (SlotDeviceStatus)(0), // 3: collection.v1.SlotDeviceStatus - (*DeviceCollection)(nil), // 4: collection.v1.DeviceCollection - (*RackInfo)(nil), // 5: collection.v1.RackInfo - (*GroupInfo)(nil), // 6: collection.v1.GroupInfo - (*CollectionMember)(nil), // 7: collection.v1.CollectionMember - (*RackMemberDetails)(nil), // 8: collection.v1.RackMemberDetails - (*RackSlotPosition)(nil), // 9: collection.v1.RackSlotPosition - (*CreateCollectionRequest)(nil), // 10: collection.v1.CreateCollectionRequest - (*CreateCollectionResponse)(nil), // 11: collection.v1.CreateCollectionResponse - (*GetCollectionRequest)(nil), // 12: collection.v1.GetCollectionRequest - (*GetCollectionResponse)(nil), // 13: collection.v1.GetCollectionResponse - (*UpdateCollectionRequest)(nil), // 14: collection.v1.UpdateCollectionRequest - (*UpdateCollectionResponse)(nil), // 15: collection.v1.UpdateCollectionResponse - (*DeleteCollectionRequest)(nil), // 16: collection.v1.DeleteCollectionRequest - (*DeleteCollectionResponse)(nil), // 17: collection.v1.DeleteCollectionResponse - (*ListCollectionsRequest)(nil), // 18: collection.v1.ListCollectionsRequest - (*ListCollectionsResponse)(nil), // 19: collection.v1.ListCollectionsResponse - (*AddDevicesToCollectionRequest)(nil), // 20: collection.v1.AddDevicesToCollectionRequest - (*AddDevicesToCollectionResponse)(nil), // 21: collection.v1.AddDevicesToCollectionResponse - (*RemoveDevicesFromCollectionRequest)(nil), // 22: collection.v1.RemoveDevicesFromCollectionRequest - (*RemoveDevicesFromCollectionResponse)(nil), // 23: collection.v1.RemoveDevicesFromCollectionResponse - (*ListCollectionMembersRequest)(nil), // 24: collection.v1.ListCollectionMembersRequest - (*ListCollectionMembersResponse)(nil), // 25: collection.v1.ListCollectionMembersResponse - (*GetDeviceCollectionsRequest)(nil), // 26: collection.v1.GetDeviceCollectionsRequest - (*GetDeviceCollectionsResponse)(nil), // 27: collection.v1.GetDeviceCollectionsResponse - (*SetRackSlotPositionRequest)(nil), // 28: collection.v1.SetRackSlotPositionRequest - (*SetRackSlotPositionResponse)(nil), // 29: collection.v1.SetRackSlotPositionResponse - (*ClearRackSlotPositionRequest)(nil), // 30: collection.v1.ClearRackSlotPositionRequest - (*ClearRackSlotPositionResponse)(nil), // 31: collection.v1.ClearRackSlotPositionResponse - (*GetRackSlotsRequest)(nil), // 32: collection.v1.GetRackSlotsRequest - (*RackSlot)(nil), // 33: collection.v1.RackSlot - (*GetRackSlotsResponse)(nil), // 34: collection.v1.GetRackSlotsResponse - (*CollectionStats)(nil), // 35: collection.v1.CollectionStats - (*GetCollectionStatsRequest)(nil), // 36: collection.v1.GetCollectionStatsRequest - (*GetCollectionStatsResponse)(nil), // 37: collection.v1.GetCollectionStatsResponse - (*RackSlotStatus)(nil), // 38: collection.v1.RackSlotStatus - (*ListRackZonesRequest)(nil), // 39: collection.v1.ListRackZonesRequest - (*ListRackZonesResponse)(nil), // 40: collection.v1.ListRackZonesResponse - (*ListRackTypesRequest)(nil), // 41: collection.v1.ListRackTypesRequest - (*RackType)(nil), // 42: collection.v1.RackType - (*ListRackTypesResponse)(nil), // 43: collection.v1.ListRackTypesResponse - (*SaveRackRequest)(nil), // 44: collection.v1.SaveRackRequest - (*SaveRackResponse)(nil), // 45: collection.v1.SaveRackResponse - (*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp - (*v1.DeviceSelector)(nil), // 47: common.v1.DeviceSelector - (*v1.SortConfig)(nil), // 48: common.v1.SortConfig - (v11.ComponentType)(0), // 49: errors.v1.ComponentType + (CollectionType)(0), // 0: collection.v1.CollectionType + (RackOrderIndex)(0), // 1: collection.v1.RackOrderIndex + (RackCoolingType)(0), // 2: collection.v1.RackCoolingType + (SlotDeviceStatus)(0), // 3: collection.v1.SlotDeviceStatus + (*DeviceCollection)(nil), // 4: collection.v1.DeviceCollection + (*RackInfo)(nil), // 5: collection.v1.RackInfo + (*GroupInfo)(nil), // 6: collection.v1.GroupInfo + (*CollectionMember)(nil), // 7: collection.v1.CollectionMember + (*RackMemberDetails)(nil), // 8: collection.v1.RackMemberDetails + (*RackSlotPosition)(nil), // 9: collection.v1.RackSlotPosition + (*CreateCollectionRequest)(nil), // 10: collection.v1.CreateCollectionRequest + (*CreateCollectionResponse)(nil), // 11: collection.v1.CreateCollectionResponse + (*GetCollectionRequest)(nil), // 12: collection.v1.GetCollectionRequest + (*GetCollectionResponse)(nil), // 13: collection.v1.GetCollectionResponse + (*UpdateCollectionRequest)(nil), // 14: collection.v1.UpdateCollectionRequest + (*UpdateCollectionResponse)(nil), // 15: collection.v1.UpdateCollectionResponse + (*DeleteCollectionRequest)(nil), // 16: collection.v1.DeleteCollectionRequest + (*DeleteCollectionResponse)(nil), // 17: collection.v1.DeleteCollectionResponse + (*ListCollectionsRequest)(nil), // 18: collection.v1.ListCollectionsRequest + (*ListCollectionsResponse)(nil), // 19: collection.v1.ListCollectionsResponse + (*ListCollectionMembersRequest)(nil), // 20: collection.v1.ListCollectionMembersRequest + (*ListCollectionMembersResponse)(nil), // 21: collection.v1.ListCollectionMembersResponse + (*GetDeviceCollectionsRequest)(nil), // 22: collection.v1.GetDeviceCollectionsRequest + (*GetDeviceCollectionsResponse)(nil), // 23: collection.v1.GetDeviceCollectionsResponse + (*SetRackSlotPositionRequest)(nil), // 24: collection.v1.SetRackSlotPositionRequest + (*SetRackSlotPositionResponse)(nil), // 25: collection.v1.SetRackSlotPositionResponse + (*ClearRackSlotPositionRequest)(nil), // 26: collection.v1.ClearRackSlotPositionRequest + (*ClearRackSlotPositionResponse)(nil), // 27: collection.v1.ClearRackSlotPositionResponse + (*GetRackSlotsRequest)(nil), // 28: collection.v1.GetRackSlotsRequest + (*RackSlot)(nil), // 29: collection.v1.RackSlot + (*GetRackSlotsResponse)(nil), // 30: collection.v1.GetRackSlotsResponse + (*CollectionStats)(nil), // 31: collection.v1.CollectionStats + (*GetCollectionStatsRequest)(nil), // 32: collection.v1.GetCollectionStatsRequest + (*GetCollectionStatsResponse)(nil), // 33: collection.v1.GetCollectionStatsResponse + (*RackSlotStatus)(nil), // 34: collection.v1.RackSlotStatus + (*ListRackZonesRequest)(nil), // 35: collection.v1.ListRackZonesRequest + (*ListRackZonesResponse)(nil), // 36: collection.v1.ListRackZonesResponse + (*ListRackTypesRequest)(nil), // 37: collection.v1.ListRackTypesRequest + (*RackType)(nil), // 38: collection.v1.RackType + (*ListRackTypesResponse)(nil), // 39: collection.v1.ListRackTypesResponse + (*SaveRackRequest)(nil), // 40: collection.v1.SaveRackRequest + (*SaveRackResponse)(nil), // 41: collection.v1.SaveRackResponse + (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp + (*v1.DeviceSelector)(nil), // 43: common.v1.DeviceSelector + (*v1.SortConfig)(nil), // 44: common.v1.SortConfig + (v11.ComponentType)(0), // 45: errors.v1.ComponentType } var file_collection_v1_collection_proto_depIdxs = []int32{ 0, // 0: collection.v1.DeviceCollection.type:type_name -> collection.v1.CollectionType - 46, // 1: collection.v1.DeviceCollection.created_at:type_name -> google.protobuf.Timestamp - 46, // 2: collection.v1.DeviceCollection.updated_at:type_name -> google.protobuf.Timestamp + 42, // 1: collection.v1.DeviceCollection.created_at:type_name -> google.protobuf.Timestamp + 42, // 2: collection.v1.DeviceCollection.updated_at:type_name -> google.protobuf.Timestamp 5, // 3: collection.v1.DeviceCollection.rack_info:type_name -> collection.v1.RackInfo 6, // 4: collection.v1.DeviceCollection.group_info:type_name -> collection.v1.GroupInfo 1, // 5: collection.v1.RackInfo.order_index:type_name -> collection.v1.RackOrderIndex 2, // 6: collection.v1.RackInfo.cooling_type:type_name -> collection.v1.RackCoolingType - 46, // 7: collection.v1.CollectionMember.added_at:type_name -> google.protobuf.Timestamp + 42, // 7: collection.v1.CollectionMember.added_at:type_name -> google.protobuf.Timestamp 8, // 8: collection.v1.CollectionMember.rack:type_name -> collection.v1.RackMemberDetails 9, // 9: collection.v1.RackMemberDetails.slot_position:type_name -> collection.v1.RackSlotPosition 0, // 10: collection.v1.CreateCollectionRequest.type:type_name -> collection.v1.CollectionType 5, // 11: collection.v1.CreateCollectionRequest.rack_info:type_name -> collection.v1.RackInfo 6, // 12: collection.v1.CreateCollectionRequest.group_info:type_name -> collection.v1.GroupInfo - 47, // 13: collection.v1.CreateCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector + 43, // 13: collection.v1.CreateCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector 4, // 14: collection.v1.CreateCollectionResponse.collection:type_name -> collection.v1.DeviceCollection 4, // 15: collection.v1.GetCollectionResponse.collection:type_name -> collection.v1.DeviceCollection 5, // 16: collection.v1.UpdateCollectionRequest.rack_info:type_name -> collection.v1.RackInfo 6, // 17: collection.v1.UpdateCollectionRequest.group_info:type_name -> collection.v1.GroupInfo - 47, // 18: collection.v1.UpdateCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector + 43, // 18: collection.v1.UpdateCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector 4, // 19: collection.v1.UpdateCollectionResponse.collection:type_name -> collection.v1.DeviceCollection 0, // 20: collection.v1.ListCollectionsRequest.type:type_name -> collection.v1.CollectionType - 48, // 21: collection.v1.ListCollectionsRequest.sort:type_name -> common.v1.SortConfig - 49, // 22: collection.v1.ListCollectionsRequest.error_component_types:type_name -> errors.v1.ComponentType + 44, // 21: collection.v1.ListCollectionsRequest.sort:type_name -> common.v1.SortConfig + 45, // 22: collection.v1.ListCollectionsRequest.error_component_types:type_name -> errors.v1.ComponentType 4, // 23: collection.v1.ListCollectionsResponse.collections:type_name -> collection.v1.DeviceCollection - 47, // 24: collection.v1.AddDevicesToCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector - 47, // 25: collection.v1.RemoveDevicesFromCollectionRequest.device_selector:type_name -> common.v1.DeviceSelector - 7, // 26: collection.v1.ListCollectionMembersResponse.members:type_name -> collection.v1.CollectionMember - 0, // 27: collection.v1.GetDeviceCollectionsRequest.type:type_name -> collection.v1.CollectionType - 4, // 28: collection.v1.GetDeviceCollectionsResponse.collections:type_name -> collection.v1.DeviceCollection - 9, // 29: collection.v1.SetRackSlotPositionRequest.position:type_name -> collection.v1.RackSlotPosition - 33, // 30: collection.v1.SetRackSlotPositionResponse.slot:type_name -> collection.v1.RackSlot - 9, // 31: collection.v1.RackSlot.position:type_name -> collection.v1.RackSlotPosition - 33, // 32: collection.v1.GetRackSlotsResponse.slots:type_name -> collection.v1.RackSlot - 38, // 33: collection.v1.CollectionStats.slot_statuses:type_name -> collection.v1.RackSlotStatus - 35, // 34: collection.v1.GetCollectionStatsResponse.stats:type_name -> collection.v1.CollectionStats - 3, // 35: collection.v1.RackSlotStatus.status:type_name -> collection.v1.SlotDeviceStatus - 42, // 36: collection.v1.ListRackTypesResponse.rack_types:type_name -> collection.v1.RackType - 5, // 37: collection.v1.SaveRackRequest.rack_info:type_name -> collection.v1.RackInfo - 47, // 38: collection.v1.SaveRackRequest.device_selector:type_name -> common.v1.DeviceSelector - 33, // 39: collection.v1.SaveRackRequest.slot_assignments:type_name -> collection.v1.RackSlot - 4, // 40: collection.v1.SaveRackResponse.collection:type_name -> collection.v1.DeviceCollection - 10, // 41: collection.v1.DeviceCollectionService.CreateCollection:input_type -> collection.v1.CreateCollectionRequest - 12, // 42: collection.v1.DeviceCollectionService.GetCollection:input_type -> collection.v1.GetCollectionRequest - 14, // 43: collection.v1.DeviceCollectionService.UpdateCollection:input_type -> collection.v1.UpdateCollectionRequest - 16, // 44: collection.v1.DeviceCollectionService.DeleteCollection:input_type -> collection.v1.DeleteCollectionRequest - 18, // 45: collection.v1.DeviceCollectionService.ListCollections:input_type -> collection.v1.ListCollectionsRequest - 20, // 46: collection.v1.DeviceCollectionService.AddDevicesToCollection:input_type -> collection.v1.AddDevicesToCollectionRequest - 22, // 47: collection.v1.DeviceCollectionService.RemoveDevicesFromCollection:input_type -> collection.v1.RemoveDevicesFromCollectionRequest - 24, // 48: collection.v1.DeviceCollectionService.ListCollectionMembers:input_type -> collection.v1.ListCollectionMembersRequest - 26, // 49: collection.v1.DeviceCollectionService.GetDeviceCollections:input_type -> collection.v1.GetDeviceCollectionsRequest - 28, // 50: collection.v1.DeviceCollectionService.SetRackSlotPosition:input_type -> collection.v1.SetRackSlotPositionRequest - 30, // 51: collection.v1.DeviceCollectionService.ClearRackSlotPosition:input_type -> collection.v1.ClearRackSlotPositionRequest - 32, // 52: collection.v1.DeviceCollectionService.GetRackSlots:input_type -> collection.v1.GetRackSlotsRequest - 36, // 53: collection.v1.DeviceCollectionService.GetCollectionStats:input_type -> collection.v1.GetCollectionStatsRequest - 39, // 54: collection.v1.DeviceCollectionService.ListRackZones:input_type -> collection.v1.ListRackZonesRequest - 41, // 55: collection.v1.DeviceCollectionService.ListRackTypes:input_type -> collection.v1.ListRackTypesRequest - 44, // 56: collection.v1.DeviceCollectionService.SaveRack:input_type -> collection.v1.SaveRackRequest - 11, // 57: collection.v1.DeviceCollectionService.CreateCollection:output_type -> collection.v1.CreateCollectionResponse - 13, // 58: collection.v1.DeviceCollectionService.GetCollection:output_type -> collection.v1.GetCollectionResponse - 15, // 59: collection.v1.DeviceCollectionService.UpdateCollection:output_type -> collection.v1.UpdateCollectionResponse - 17, // 60: collection.v1.DeviceCollectionService.DeleteCollection:output_type -> collection.v1.DeleteCollectionResponse - 19, // 61: collection.v1.DeviceCollectionService.ListCollections:output_type -> collection.v1.ListCollectionsResponse - 21, // 62: collection.v1.DeviceCollectionService.AddDevicesToCollection:output_type -> collection.v1.AddDevicesToCollectionResponse - 23, // 63: collection.v1.DeviceCollectionService.RemoveDevicesFromCollection:output_type -> collection.v1.RemoveDevicesFromCollectionResponse - 25, // 64: collection.v1.DeviceCollectionService.ListCollectionMembers:output_type -> collection.v1.ListCollectionMembersResponse - 27, // 65: collection.v1.DeviceCollectionService.GetDeviceCollections:output_type -> collection.v1.GetDeviceCollectionsResponse - 29, // 66: collection.v1.DeviceCollectionService.SetRackSlotPosition:output_type -> collection.v1.SetRackSlotPositionResponse - 31, // 67: collection.v1.DeviceCollectionService.ClearRackSlotPosition:output_type -> collection.v1.ClearRackSlotPositionResponse - 34, // 68: collection.v1.DeviceCollectionService.GetRackSlots:output_type -> collection.v1.GetRackSlotsResponse - 37, // 69: collection.v1.DeviceCollectionService.GetCollectionStats:output_type -> collection.v1.GetCollectionStatsResponse - 40, // 70: collection.v1.DeviceCollectionService.ListRackZones:output_type -> collection.v1.ListRackZonesResponse - 43, // 71: collection.v1.DeviceCollectionService.ListRackTypes:output_type -> collection.v1.ListRackTypesResponse - 45, // 72: collection.v1.DeviceCollectionService.SaveRack:output_type -> collection.v1.SaveRackResponse - 57, // [57:73] is the sub-list for method output_type - 41, // [41:57] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 7, // 24: collection.v1.ListCollectionMembersResponse.members:type_name -> collection.v1.CollectionMember + 0, // 25: collection.v1.GetDeviceCollectionsRequest.type:type_name -> collection.v1.CollectionType + 4, // 26: collection.v1.GetDeviceCollectionsResponse.collections:type_name -> collection.v1.DeviceCollection + 9, // 27: collection.v1.SetRackSlotPositionRequest.position:type_name -> collection.v1.RackSlotPosition + 29, // 28: collection.v1.SetRackSlotPositionResponse.slot:type_name -> collection.v1.RackSlot + 9, // 29: collection.v1.RackSlot.position:type_name -> collection.v1.RackSlotPosition + 29, // 30: collection.v1.GetRackSlotsResponse.slots:type_name -> collection.v1.RackSlot + 34, // 31: collection.v1.CollectionStats.slot_statuses:type_name -> collection.v1.RackSlotStatus + 31, // 32: collection.v1.GetCollectionStatsResponse.stats:type_name -> collection.v1.CollectionStats + 3, // 33: collection.v1.RackSlotStatus.status:type_name -> collection.v1.SlotDeviceStatus + 38, // 34: collection.v1.ListRackTypesResponse.rack_types:type_name -> collection.v1.RackType + 5, // 35: collection.v1.SaveRackRequest.rack_info:type_name -> collection.v1.RackInfo + 43, // 36: collection.v1.SaveRackRequest.device_selector:type_name -> common.v1.DeviceSelector + 29, // 37: collection.v1.SaveRackRequest.slot_assignments:type_name -> collection.v1.RackSlot + 4, // 38: collection.v1.SaveRackResponse.collection:type_name -> collection.v1.DeviceCollection + 10, // 39: collection.v1.DeviceCollectionService.CreateCollection:input_type -> collection.v1.CreateCollectionRequest + 12, // 40: collection.v1.DeviceCollectionService.GetCollection:input_type -> collection.v1.GetCollectionRequest + 14, // 41: collection.v1.DeviceCollectionService.UpdateCollection:input_type -> collection.v1.UpdateCollectionRequest + 16, // 42: collection.v1.DeviceCollectionService.DeleteCollection:input_type -> collection.v1.DeleteCollectionRequest + 18, // 43: collection.v1.DeviceCollectionService.ListCollections:input_type -> collection.v1.ListCollectionsRequest + 20, // 44: collection.v1.DeviceCollectionService.ListCollectionMembers:input_type -> collection.v1.ListCollectionMembersRequest + 22, // 45: collection.v1.DeviceCollectionService.GetDeviceCollections:input_type -> collection.v1.GetDeviceCollectionsRequest + 24, // 46: collection.v1.DeviceCollectionService.SetRackSlotPosition:input_type -> collection.v1.SetRackSlotPositionRequest + 26, // 47: collection.v1.DeviceCollectionService.ClearRackSlotPosition:input_type -> collection.v1.ClearRackSlotPositionRequest + 28, // 48: collection.v1.DeviceCollectionService.GetRackSlots:input_type -> collection.v1.GetRackSlotsRequest + 32, // 49: collection.v1.DeviceCollectionService.GetCollectionStats:input_type -> collection.v1.GetCollectionStatsRequest + 35, // 50: collection.v1.DeviceCollectionService.ListRackZones:input_type -> collection.v1.ListRackZonesRequest + 37, // 51: collection.v1.DeviceCollectionService.ListRackTypes:input_type -> collection.v1.ListRackTypesRequest + 40, // 52: collection.v1.DeviceCollectionService.SaveRack:input_type -> collection.v1.SaveRackRequest + 11, // 53: collection.v1.DeviceCollectionService.CreateCollection:output_type -> collection.v1.CreateCollectionResponse + 13, // 54: collection.v1.DeviceCollectionService.GetCollection:output_type -> collection.v1.GetCollectionResponse + 15, // 55: collection.v1.DeviceCollectionService.UpdateCollection:output_type -> collection.v1.UpdateCollectionResponse + 17, // 56: collection.v1.DeviceCollectionService.DeleteCollection:output_type -> collection.v1.DeleteCollectionResponse + 19, // 57: collection.v1.DeviceCollectionService.ListCollections:output_type -> collection.v1.ListCollectionsResponse + 21, // 58: collection.v1.DeviceCollectionService.ListCollectionMembers:output_type -> collection.v1.ListCollectionMembersResponse + 23, // 59: collection.v1.DeviceCollectionService.GetDeviceCollections:output_type -> collection.v1.GetDeviceCollectionsResponse + 25, // 60: collection.v1.DeviceCollectionService.SetRackSlotPosition:output_type -> collection.v1.SetRackSlotPositionResponse + 27, // 61: collection.v1.DeviceCollectionService.ClearRackSlotPosition:output_type -> collection.v1.ClearRackSlotPositionResponse + 30, // 62: collection.v1.DeviceCollectionService.GetRackSlots:output_type -> collection.v1.GetRackSlotsResponse + 33, // 63: collection.v1.DeviceCollectionService.GetCollectionStats:output_type -> collection.v1.GetCollectionStatsResponse + 36, // 64: collection.v1.DeviceCollectionService.ListRackZones:output_type -> collection.v1.ListRackZonesResponse + 39, // 65: collection.v1.DeviceCollectionService.ListRackTypes:output_type -> collection.v1.ListRackTypesResponse + 41, // 66: collection.v1.DeviceCollectionService.SaveRack:output_type -> collection.v1.SaveRackResponse + 53, // [53:67] is the sub-list for method output_type + 39, // [39:53] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_collection_v1_collection_proto_init() } @@ -3725,14 +3440,14 @@ func file_collection_v1_collection_proto_init() { (*UpdateCollectionRequest_RackInfo)(nil), (*UpdateCollectionRequest_GroupInfo)(nil), } - file_collection_v1_collection_proto_msgTypes[40].OneofWrappers = []any{} + file_collection_v1_collection_proto_msgTypes[36].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_collection_v1_collection_proto_rawDesc), len(file_collection_v1_collection_proto_rawDesc)), NumEnums: 4, - NumMessages: 42, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/server/generated/grpc/collection/v1/collectionv1connect/collection.connect.go b/server/generated/grpc/collection/v1/collectionv1connect/collection.connect.go index ad3d9d194..12eda73da 100644 --- a/server/generated/grpc/collection/v1/collectionv1connect/collection.connect.go +++ b/server/generated/grpc/collection/v1/collectionv1connect/collection.connect.go @@ -49,12 +49,6 @@ const ( // DeviceCollectionServiceListCollectionsProcedure is the fully-qualified name of the // DeviceCollectionService's ListCollections RPC. DeviceCollectionServiceListCollectionsProcedure = "/collection.v1.DeviceCollectionService/ListCollections" - // DeviceCollectionServiceAddDevicesToCollectionProcedure is the fully-qualified name of the - // DeviceCollectionService's AddDevicesToCollection RPC. - DeviceCollectionServiceAddDevicesToCollectionProcedure = "/collection.v1.DeviceCollectionService/AddDevicesToCollection" - // DeviceCollectionServiceRemoveDevicesFromCollectionProcedure is the fully-qualified name of the - // DeviceCollectionService's RemoveDevicesFromCollection RPC. - DeviceCollectionServiceRemoveDevicesFromCollectionProcedure = "/collection.v1.DeviceCollectionService/RemoveDevicesFromCollection" // DeviceCollectionServiceListCollectionMembersProcedure is the fully-qualified name of the // DeviceCollectionService's ListCollectionMembers RPC. DeviceCollectionServiceListCollectionMembersProcedure = "/collection.v1.DeviceCollectionService/ListCollectionMembers" @@ -96,10 +90,6 @@ type DeviceCollectionServiceClient interface { DeleteCollection(context.Context, *connect.Request[v1.DeleteCollectionRequest]) (*connect.Response[v1.DeleteCollectionResponse], error) // Lists all collections for the organization ListCollections(context.Context, *connect.Request[v1.ListCollectionsRequest]) (*connect.Response[v1.ListCollectionsResponse], error) - // Adds devices to a collection - AddDevicesToCollection(context.Context, *connect.Request[v1.AddDevicesToCollectionRequest]) (*connect.Response[v1.AddDevicesToCollectionResponse], error) - // Removes devices from a collection - RemoveDevicesFromCollection(context.Context, *connect.Request[v1.RemoveDevicesFromCollectionRequest]) (*connect.Response[v1.RemoveDevicesFromCollectionResponse], error) // Lists members of a collection ListCollectionMembers(context.Context, *connect.Request[v1.ListCollectionMembersRequest]) (*connect.Response[v1.ListCollectionMembersResponse], error) // Gets collections that a device belongs to @@ -156,16 +146,6 @@ func NewDeviceCollectionServiceClient(httpClient connect.HTTPClient, baseURL str baseURL+DeviceCollectionServiceListCollectionsProcedure, opts..., ), - addDevicesToCollection: connect.NewClient[v1.AddDevicesToCollectionRequest, v1.AddDevicesToCollectionResponse]( - httpClient, - baseURL+DeviceCollectionServiceAddDevicesToCollectionProcedure, - opts..., - ), - removeDevicesFromCollection: connect.NewClient[v1.RemoveDevicesFromCollectionRequest, v1.RemoveDevicesFromCollectionResponse]( - httpClient, - baseURL+DeviceCollectionServiceRemoveDevicesFromCollectionProcedure, - opts..., - ), listCollectionMembers: connect.NewClient[v1.ListCollectionMembersRequest, v1.ListCollectionMembersResponse]( httpClient, baseURL+DeviceCollectionServiceListCollectionMembersProcedure, @@ -216,22 +196,20 @@ func NewDeviceCollectionServiceClient(httpClient connect.HTTPClient, baseURL str // deviceCollectionServiceClient implements DeviceCollectionServiceClient. type deviceCollectionServiceClient struct { - createCollection *connect.Client[v1.CreateCollectionRequest, v1.CreateCollectionResponse] - getCollection *connect.Client[v1.GetCollectionRequest, v1.GetCollectionResponse] - updateCollection *connect.Client[v1.UpdateCollectionRequest, v1.UpdateCollectionResponse] - deleteCollection *connect.Client[v1.DeleteCollectionRequest, v1.DeleteCollectionResponse] - listCollections *connect.Client[v1.ListCollectionsRequest, v1.ListCollectionsResponse] - addDevicesToCollection *connect.Client[v1.AddDevicesToCollectionRequest, v1.AddDevicesToCollectionResponse] - removeDevicesFromCollection *connect.Client[v1.RemoveDevicesFromCollectionRequest, v1.RemoveDevicesFromCollectionResponse] - listCollectionMembers *connect.Client[v1.ListCollectionMembersRequest, v1.ListCollectionMembersResponse] - getDeviceCollections *connect.Client[v1.GetDeviceCollectionsRequest, v1.GetDeviceCollectionsResponse] - setRackSlotPosition *connect.Client[v1.SetRackSlotPositionRequest, v1.SetRackSlotPositionResponse] - clearRackSlotPosition *connect.Client[v1.ClearRackSlotPositionRequest, v1.ClearRackSlotPositionResponse] - getRackSlots *connect.Client[v1.GetRackSlotsRequest, v1.GetRackSlotsResponse] - getCollectionStats *connect.Client[v1.GetCollectionStatsRequest, v1.GetCollectionStatsResponse] - listRackZones *connect.Client[v1.ListRackZonesRequest, v1.ListRackZonesResponse] - listRackTypes *connect.Client[v1.ListRackTypesRequest, v1.ListRackTypesResponse] - saveRack *connect.Client[v1.SaveRackRequest, v1.SaveRackResponse] + createCollection *connect.Client[v1.CreateCollectionRequest, v1.CreateCollectionResponse] + getCollection *connect.Client[v1.GetCollectionRequest, v1.GetCollectionResponse] + updateCollection *connect.Client[v1.UpdateCollectionRequest, v1.UpdateCollectionResponse] + deleteCollection *connect.Client[v1.DeleteCollectionRequest, v1.DeleteCollectionResponse] + listCollections *connect.Client[v1.ListCollectionsRequest, v1.ListCollectionsResponse] + listCollectionMembers *connect.Client[v1.ListCollectionMembersRequest, v1.ListCollectionMembersResponse] + getDeviceCollections *connect.Client[v1.GetDeviceCollectionsRequest, v1.GetDeviceCollectionsResponse] + setRackSlotPosition *connect.Client[v1.SetRackSlotPositionRequest, v1.SetRackSlotPositionResponse] + clearRackSlotPosition *connect.Client[v1.ClearRackSlotPositionRequest, v1.ClearRackSlotPositionResponse] + getRackSlots *connect.Client[v1.GetRackSlotsRequest, v1.GetRackSlotsResponse] + getCollectionStats *connect.Client[v1.GetCollectionStatsRequest, v1.GetCollectionStatsResponse] + listRackZones *connect.Client[v1.ListRackZonesRequest, v1.ListRackZonesResponse] + listRackTypes *connect.Client[v1.ListRackTypesRequest, v1.ListRackTypesResponse] + saveRack *connect.Client[v1.SaveRackRequest, v1.SaveRackResponse] } // CreateCollection calls collection.v1.DeviceCollectionService.CreateCollection. @@ -259,17 +237,6 @@ func (c *deviceCollectionServiceClient) ListCollections(ctx context.Context, req return c.listCollections.CallUnary(ctx, req) } -// AddDevicesToCollection calls collection.v1.DeviceCollectionService.AddDevicesToCollection. -func (c *deviceCollectionServiceClient) AddDevicesToCollection(ctx context.Context, req *connect.Request[v1.AddDevicesToCollectionRequest]) (*connect.Response[v1.AddDevicesToCollectionResponse], error) { - return c.addDevicesToCollection.CallUnary(ctx, req) -} - -// RemoveDevicesFromCollection calls -// collection.v1.DeviceCollectionService.RemoveDevicesFromCollection. -func (c *deviceCollectionServiceClient) RemoveDevicesFromCollection(ctx context.Context, req *connect.Request[v1.RemoveDevicesFromCollectionRequest]) (*connect.Response[v1.RemoveDevicesFromCollectionResponse], error) { - return c.removeDevicesFromCollection.CallUnary(ctx, req) -} - // ListCollectionMembers calls collection.v1.DeviceCollectionService.ListCollectionMembers. func (c *deviceCollectionServiceClient) ListCollectionMembers(ctx context.Context, req *connect.Request[v1.ListCollectionMembersRequest]) (*connect.Response[v1.ListCollectionMembersResponse], error) { return c.listCollectionMembers.CallUnary(ctx, req) @@ -328,10 +295,6 @@ type DeviceCollectionServiceHandler interface { DeleteCollection(context.Context, *connect.Request[v1.DeleteCollectionRequest]) (*connect.Response[v1.DeleteCollectionResponse], error) // Lists all collections for the organization ListCollections(context.Context, *connect.Request[v1.ListCollectionsRequest]) (*connect.Response[v1.ListCollectionsResponse], error) - // Adds devices to a collection - AddDevicesToCollection(context.Context, *connect.Request[v1.AddDevicesToCollectionRequest]) (*connect.Response[v1.AddDevicesToCollectionResponse], error) - // Removes devices from a collection - RemoveDevicesFromCollection(context.Context, *connect.Request[v1.RemoveDevicesFromCollectionRequest]) (*connect.Response[v1.RemoveDevicesFromCollectionResponse], error) // Lists members of a collection ListCollectionMembers(context.Context, *connect.Request[v1.ListCollectionMembersRequest]) (*connect.Response[v1.ListCollectionMembersResponse], error) // Gets collections that a device belongs to @@ -384,16 +347,6 @@ func NewDeviceCollectionServiceHandler(svc DeviceCollectionServiceHandler, opts svc.ListCollections, opts..., ) - deviceCollectionServiceAddDevicesToCollectionHandler := connect.NewUnaryHandler( - DeviceCollectionServiceAddDevicesToCollectionProcedure, - svc.AddDevicesToCollection, - opts..., - ) - deviceCollectionServiceRemoveDevicesFromCollectionHandler := connect.NewUnaryHandler( - DeviceCollectionServiceRemoveDevicesFromCollectionProcedure, - svc.RemoveDevicesFromCollection, - opts..., - ) deviceCollectionServiceListCollectionMembersHandler := connect.NewUnaryHandler( DeviceCollectionServiceListCollectionMembersProcedure, svc.ListCollectionMembers, @@ -451,10 +404,6 @@ func NewDeviceCollectionServiceHandler(svc DeviceCollectionServiceHandler, opts deviceCollectionServiceDeleteCollectionHandler.ServeHTTP(w, r) case DeviceCollectionServiceListCollectionsProcedure: deviceCollectionServiceListCollectionsHandler.ServeHTTP(w, r) - case DeviceCollectionServiceAddDevicesToCollectionProcedure: - deviceCollectionServiceAddDevicesToCollectionHandler.ServeHTTP(w, r) - case DeviceCollectionServiceRemoveDevicesFromCollectionProcedure: - deviceCollectionServiceRemoveDevicesFromCollectionHandler.ServeHTTP(w, r) case DeviceCollectionServiceListCollectionMembersProcedure: deviceCollectionServiceListCollectionMembersHandler.ServeHTTP(w, r) case DeviceCollectionServiceGetDeviceCollectionsProcedure: @@ -502,14 +451,6 @@ func (UnimplementedDeviceCollectionServiceHandler) ListCollections(context.Conte return nil, connect.NewError(connect.CodeUnimplemented, errors.New("collection.v1.DeviceCollectionService.ListCollections is not implemented")) } -func (UnimplementedDeviceCollectionServiceHandler) AddDevicesToCollection(context.Context, *connect.Request[v1.AddDevicesToCollectionRequest]) (*connect.Response[v1.AddDevicesToCollectionResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("collection.v1.DeviceCollectionService.AddDevicesToCollection is not implemented")) -} - -func (UnimplementedDeviceCollectionServiceHandler) RemoveDevicesFromCollection(context.Context, *connect.Request[v1.RemoveDevicesFromCollectionRequest]) (*connect.Response[v1.RemoveDevicesFromCollectionResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("collection.v1.DeviceCollectionService.RemoveDevicesFromCollection is not implemented")) -} - func (UnimplementedDeviceCollectionServiceHandler) ListCollectionMembers(context.Context, *connect.Request[v1.ListCollectionMembersRequest]) (*connect.Response[v1.ListCollectionMembersResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("collection.v1.DeviceCollectionService.ListCollectionMembers is not implemented")) } diff --git a/server/generated/grpc/device_set/v1/device_set.pb.go b/server/generated/grpc/device_set/v1/device_set.pb.go index 8d9ebc532..de2646852 100644 --- a/server/generated/grpc/device_set/v1/device_set.pb.go +++ b/server/generated/grpc/device_set/v1/device_set.pb.go @@ -1453,35 +1453,35 @@ func (x *ListDeviceSetsResponse) GetTotalCount() int32 { return 0 } -// Request to add devices to a device set. -// -// When the target is a site-stamped rack, the server rewrites -// device.site_id to match the rack for every added device. The affected -// count is returned in site_reassigned_count. Groups are exempt. -type AddDevicesToDeviceSetRequest struct { +// Request to add devices to a group. Groups have many-to-many device +// membership: devices may already belong to other groups and to a rack, +// none of which are touched. Server rejects with InvalidArgument when +// target_group_id refers to a non-group device set. +type AddDevicesToGroupRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // ID of the device set to add devices to - DeviceSetId int64 `protobuf:"varint,1,opt,name=device_set_id,json=deviceSetId,proto3" json:"device_set_id,omitempty"` - // Devices to add: specific list or all paired devices + // ID of the group to add devices to. Must reference a device set of + // type GROUP; racks are rejected. + TargetGroupId int64 `protobuf:"varint,1,opt,name=target_group_id,json=targetGroupId,proto3" json:"target_group_id,omitempty"` + // Devices to add: specific list or all paired devices. DeviceSelector *v1.DeviceSelector `protobuf:"bytes,2,opt,name=device_selector,json=deviceSelector,proto3" json:"device_selector,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AddDevicesToDeviceSetRequest) Reset() { - *x = AddDevicesToDeviceSetRequest{} +func (x *AddDevicesToGroupRequest) Reset() { + *x = AddDevicesToGroupRequest{} mi := &file_device_set_v1_device_set_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AddDevicesToDeviceSetRequest) String() string { +func (x *AddDevicesToGroupRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AddDevicesToDeviceSetRequest) ProtoMessage() {} +func (*AddDevicesToGroupRequest) ProtoMessage() {} -func (x *AddDevicesToDeviceSetRequest) ProtoReflect() protoreflect.Message { +func (x *AddDevicesToGroupRequest) ProtoReflect() protoreflect.Message { mi := &file_device_set_v1_device_set_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1493,53 +1493,49 @@ func (x *AddDevicesToDeviceSetRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AddDevicesToDeviceSetRequest.ProtoReflect.Descriptor instead. -func (*AddDevicesToDeviceSetRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use AddDevicesToGroupRequest.ProtoReflect.Descriptor instead. +func (*AddDevicesToGroupRequest) Descriptor() ([]byte, []int) { return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{16} } -func (x *AddDevicesToDeviceSetRequest) GetDeviceSetId() int64 { +func (x *AddDevicesToGroupRequest) GetTargetGroupId() int64 { if x != nil { - return x.DeviceSetId + return x.TargetGroupId } return 0 } -func (x *AddDevicesToDeviceSetRequest) GetDeviceSelector() *v1.DeviceSelector { +func (x *AddDevicesToGroupRequest) GetDeviceSelector() *v1.DeviceSelector { if x != nil { return x.DeviceSelector } return nil } -// Response after adding devices to a device set -type AddDevicesToDeviceSetResponse struct { +// Response after adding devices to a group. +type AddDevicesToGroupResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // ID of the device set devices were added to - DeviceSetId int64 `protobuf:"varint,1,opt,name=device_set_id,json=deviceSetId,proto3" json:"device_set_id,omitempty"` - // Number of devices successfully added - // May be less than requested if some devices were already members - AddedCount int32 `protobuf:"varint,2,opt,name=added_count,json=addedCount,proto3" json:"added_count,omitempty"` - // Number of devices whose site_id was rewritten by the cascade. - SiteReassignedCount int32 `protobuf:"varint,3,opt,name=site_reassigned_count,json=siteReassignedCount,proto3" json:"site_reassigned_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Number of devices successfully added. May be less than requested + // if some devices were already members. + AddedCount int64 `protobuf:"varint,1,opt,name=added_count,json=addedCount,proto3" json:"added_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *AddDevicesToDeviceSetResponse) Reset() { - *x = AddDevicesToDeviceSetResponse{} +func (x *AddDevicesToGroupResponse) Reset() { + *x = AddDevicesToGroupResponse{} mi := &file_device_set_v1_device_set_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AddDevicesToDeviceSetResponse) String() string { +func (x *AddDevicesToGroupResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AddDevicesToDeviceSetResponse) ProtoMessage() {} +func (*AddDevicesToGroupResponse) ProtoMessage() {} -func (x *AddDevicesToDeviceSetResponse) ProtoReflect() protoreflect.Message { +func (x *AddDevicesToGroupResponse) ProtoReflect() protoreflect.Message { mi := &file_device_set_v1_device_set_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1551,57 +1547,46 @@ func (x *AddDevicesToDeviceSetResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AddDevicesToDeviceSetResponse.ProtoReflect.Descriptor instead. -func (*AddDevicesToDeviceSetResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AddDevicesToGroupResponse.ProtoReflect.Descriptor instead. +func (*AddDevicesToGroupResponse) Descriptor() ([]byte, []int) { return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{17} } -func (x *AddDevicesToDeviceSetResponse) GetDeviceSetId() int64 { - if x != nil { - return x.DeviceSetId - } - return 0 -} - -func (x *AddDevicesToDeviceSetResponse) GetAddedCount() int32 { +func (x *AddDevicesToGroupResponse) GetAddedCount() int64 { if x != nil { return x.AddedCount } return 0 } -func (x *AddDevicesToDeviceSetResponse) GetSiteReassignedCount() int32 { - if x != nil { - return x.SiteReassignedCount - } - return 0 -} - -// Request to remove devices from a device set -type RemoveDevicesFromDeviceSetRequest struct { +// Request to remove devices from a group. Server rejects with +// InvalidArgument when target_group_id refers to a non-group device set; +// use AssignDevicesToRack to clear rack membership. +type RemoveDevicesFromGroupRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // ID of the device set to remove devices from - DeviceSetId int64 `protobuf:"varint,1,opt,name=device_set_id,json=deviceSetId,proto3" json:"device_set_id,omitempty"` - // Devices to remove: specific list or all paired devices + // ID of the group to remove devices from. Must reference a device + // set of type GROUP; racks are rejected. + TargetGroupId int64 `protobuf:"varint,1,opt,name=target_group_id,json=targetGroupId,proto3" json:"target_group_id,omitempty"` + // Devices to remove: specific list or all paired devices. DeviceSelector *v1.DeviceSelector `protobuf:"bytes,2,opt,name=device_selector,json=deviceSelector,proto3" json:"device_selector,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RemoveDevicesFromDeviceSetRequest) Reset() { - *x = RemoveDevicesFromDeviceSetRequest{} +func (x *RemoveDevicesFromGroupRequest) Reset() { + *x = RemoveDevicesFromGroupRequest{} mi := &file_device_set_v1_device_set_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RemoveDevicesFromDeviceSetRequest) String() string { +func (x *RemoveDevicesFromGroupRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RemoveDevicesFromDeviceSetRequest) ProtoMessage() {} +func (*RemoveDevicesFromGroupRequest) ProtoMessage() {} -func (x *RemoveDevicesFromDeviceSetRequest) ProtoReflect() protoreflect.Message { +func (x *RemoveDevicesFromGroupRequest) ProtoReflect() protoreflect.Message { mi := &file_device_set_v1_device_set_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1613,48 +1598,48 @@ func (x *RemoveDevicesFromDeviceSetRequest) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use RemoveDevicesFromDeviceSetRequest.ProtoReflect.Descriptor instead. -func (*RemoveDevicesFromDeviceSetRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use RemoveDevicesFromGroupRequest.ProtoReflect.Descriptor instead. +func (*RemoveDevicesFromGroupRequest) Descriptor() ([]byte, []int) { return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{18} } -func (x *RemoveDevicesFromDeviceSetRequest) GetDeviceSetId() int64 { +func (x *RemoveDevicesFromGroupRequest) GetTargetGroupId() int64 { if x != nil { - return x.DeviceSetId + return x.TargetGroupId } return 0 } -func (x *RemoveDevicesFromDeviceSetRequest) GetDeviceSelector() *v1.DeviceSelector { +func (x *RemoveDevicesFromGroupRequest) GetDeviceSelector() *v1.DeviceSelector { if x != nil { return x.DeviceSelector } return nil } -// Response after removing devices from a device set -type RemoveDevicesFromDeviceSetResponse struct { +// Response after removing devices from a group. +type RemoveDevicesFromGroupResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // Number of devices successfully removed - RemovedCount int32 `protobuf:"varint,1,opt,name=removed_count,json=removedCount,proto3" json:"removed_count,omitempty"` + // Number of devices successfully removed. + RemovedCount int64 `protobuf:"varint,1,opt,name=removed_count,json=removedCount,proto3" json:"removed_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RemoveDevicesFromDeviceSetResponse) Reset() { - *x = RemoveDevicesFromDeviceSetResponse{} +func (x *RemoveDevicesFromGroupResponse) Reset() { + *x = RemoveDevicesFromGroupResponse{} mi := &file_device_set_v1_device_set_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RemoveDevicesFromDeviceSetResponse) String() string { +func (x *RemoveDevicesFromGroupResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RemoveDevicesFromDeviceSetResponse) ProtoMessage() {} +func (*RemoveDevicesFromGroupResponse) ProtoMessage() {} -func (x *RemoveDevicesFromDeviceSetResponse) ProtoReflect() protoreflect.Message { +func (x *RemoveDevicesFromGroupResponse) ProtoReflect() protoreflect.Message { mi := &file_device_set_v1_device_set_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1666,12 +1651,12 @@ func (x *RemoveDevicesFromDeviceSetResponse) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use RemoveDevicesFromDeviceSetResponse.ProtoReflect.Descriptor instead. -func (*RemoveDevicesFromDeviceSetResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use RemoveDevicesFromGroupResponse.ProtoReflect.Descriptor instead. +func (*RemoveDevicesFromGroupResponse) Descriptor() ([]byte, []int) { return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{19} } -func (x *RemoveDevicesFromDeviceSetResponse) GetRemovedCount() int32 { +func (x *RemoveDevicesFromGroupResponse) GetRemovedCount() int64 { if x != nil { return x.RemovedCount } @@ -3099,6 +3084,135 @@ func (x *SaveRackResponse) GetSiteReassignedCount() int32 { return 0 } +type AssignDevicesToRackRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unset = clear rack membership without re-assigning. When + // present, must be > 0. + TargetRackId *int64 `protobuf:"varint,1,opt,name=target_rack_id,json=targetRackId,proto3,oneof" json:"target_rack_id,omitempty"` + // Devices to assign. Only the device_list variant is accepted; the + // all_devices variant is rejected with InvalidArgument because + // moving every paired device into a single rack is never the + // intended operation. Filter-based selectors are reserved for a + // future expansion. + DeviceSelector *v1.DeviceSelector `protobuf:"bytes,2,opt,name=device_selector,json=deviceSelector,proto3" json:"device_selector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignDevicesToRackRequest) Reset() { + *x = AssignDevicesToRackRequest{} + mi := &file_device_set_v1_device_set_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssignDevicesToRackRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssignDevicesToRackRequest) ProtoMessage() {} + +func (x *AssignDevicesToRackRequest) ProtoReflect() protoreflect.Message { + mi := &file_device_set_v1_device_set_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssignDevicesToRackRequest.ProtoReflect.Descriptor instead. +func (*AssignDevicesToRackRequest) Descriptor() ([]byte, []int) { + return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{44} +} + +func (x *AssignDevicesToRackRequest) GetTargetRackId() int64 { + if x != nil && x.TargetRackId != nil { + return *x.TargetRackId + } + return 0 +} + +func (x *AssignDevicesToRackRequest) GetDeviceSelector() *v1.DeviceSelector { + if x != nil { + return x.DeviceSelector + } + return nil +} + +type AssignDevicesToRackResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Number of devices whose rack membership now points at + // target_rack_id. When target_rack_id is unset, this is 0 and + // removed_count carries the cleared-rack count. + AssignedCount int64 `protobuf:"varint,1,opt,name=assigned_count,json=assignedCount,proto3" json:"assigned_count,omitempty"` + // Number of devices whose site_id was rewritten by the cascade + // because the target rack's site differed from the device's prior + // site. Zero on unassign and on same-site moves. + SiteReassignedCount int64 `protobuf:"varint,2,opt,name=site_reassigned_count,json=siteReassignedCount,proto3" json:"site_reassigned_count,omitempty"` + // Number of devices whose prior rack membership was cleared. On + // unassign (target_rack_id unset) this equals the number of devices + // that were in a rack. On a re-assign this counts the prior rack + // membership rows that were deleted before the new ones inserted. + RemovedCount int64 `protobuf:"varint,3,opt,name=removed_count,json=removedCount,proto3" json:"removed_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignDevicesToRackResponse) Reset() { + *x = AssignDevicesToRackResponse{} + mi := &file_device_set_v1_device_set_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssignDevicesToRackResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssignDevicesToRackResponse) ProtoMessage() {} + +func (x *AssignDevicesToRackResponse) ProtoReflect() protoreflect.Message { + mi := &file_device_set_v1_device_set_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssignDevicesToRackResponse.ProtoReflect.Descriptor instead. +func (*AssignDevicesToRackResponse) Descriptor() ([]byte, []int) { + return file_device_set_v1_device_set_proto_rawDescGZIP(), []int{45} +} + +func (x *AssignDevicesToRackResponse) GetAssignedCount() int64 { + if x != nil { + return x.AssignedCount + } + return 0 +} + +func (x *AssignDevicesToRackResponse) GetSiteReassignedCount() int64 { + if x != nil { + return x.SiteReassignedCount + } + return 0 +} + +func (x *AssignDevicesToRackResponse) GetRemovedCount() int64 { + if x != nil { + return x.RemovedCount + } + return 0 +} + var File_device_set_v1_device_set_proto protoreflect.FileDescriptor var file_device_set_v1_device_set_proto_rawDesc = string([]byte{ @@ -3300,411 +3414,432 @@ var file_device_set_v1_device_set_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x97, 0x01, - 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, - 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, + 0x0a, 0x18, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x0f, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0d, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x98, 0x01, 0x0a, 0x1d, 0x41, 0x64, 0x64, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x65, 0x76, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x3c, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x1d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, + 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x22, 0x45, 0x0a, 0x1e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8f, 0x01, 0x0a, 0x1b, + 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x1a, 0x02, 0x28, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x80, 0x01, + 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, + 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, + 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, + 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x22, 0x84, 0x01, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, + 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x58, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, + 0x73, 0x22, 0xc4, 0x01, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, + 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, + 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x34, 0x0a, + 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, + 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, + 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x1b, 0x53, 0x65, 0x74, 0x52, + 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x04, 0x73, + 0x6c, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, + 0x6f, 0x74, 0x52, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x22, 0x81, 0x01, 0x0a, 0x1c, 0x43, 0x6c, 0x65, + 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, - 0x0b, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, - 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, - 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x73, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x21, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, - 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, - 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x22, 0x49, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, - 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8f, 0x01, 0x0a, - 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x09, 0x70, 0x61, 0x67, - 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x1a, 0x02, 0x28, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x80, - 0x01, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x38, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, - 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x22, 0x84, 0x01, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, - 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x58, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x74, 0x73, 0x22, 0xc4, 0x01, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, - 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, - 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x34, - 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, - 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x1b, 0x53, 0x65, 0x74, - 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x04, - 0x73, 0x6c, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, - 0x6c, 0x6f, 0x74, 0x52, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x22, 0x81, 0x01, 0x0a, 0x1c, 0x43, 0x6c, - 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x1f, 0x0a, - 0x1d, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, - 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, - 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x49, 0x64, 0x22, 0x74, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x12, 0x2b, - 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x08, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, - 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, - 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x22, - 0xde, 0x07, 0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, - 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, - 0x72, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, - 0x12, 0x2c, 0x0a, 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, - 0x63, 0x79, 0x5f, 0x6a, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, - 0x67, 0x45, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, - 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, - 0x65, 0x72, 0x4b, 0x77, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x69, 0x6e, 0x5f, 0x74, 0x65, 0x6d, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, - 0x0f, 0x6d, 0x69, 0x6e, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, - 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x6d, 0x61, 0x78, - 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x12, 0x23, 0x0a, 0x0d, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, - 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, - 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, - 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, - 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, - 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1b, - 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x19, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, - 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x69, - 0x73, 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0d, 0x66, 0x61, 0x6e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x33, 0x0a, 0x16, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x13, 0x68, 0x61, 0x73, 0x68, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x73, 0x75, 0x5f, 0x69, 0x73, 0x73, 0x75, - 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x70, - 0x73, 0x75, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x0d, - 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x15, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x0c, 0x73, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, - 0x22, 0x40, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, - 0x64, 0x73, 0x22, 0x50, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, - 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x33, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x73, 0x22, 0x73, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x6f, 0x77, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x03, 0x72, 0x6f, 0x77, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, - 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x22, 0x2d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, - 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, - 0x22, 0x19, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, - 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x18, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, - 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x57, 0x0a, 0x08, 0x52, 0x61, 0x63, - 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, - 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x4f, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, - 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x72, - 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x79, - 0x70, 0x65, 0x73, 0x22, 0xca, 0x02, 0x0a, 0x0f, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0a, - 0xba, 0x48, 0x07, 0xd8, 0x01, 0x01, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x05, - 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0c, 0xba, 0x48, 0x09, - 0xc8, 0x01, 0x01, 0x72, 0x04, 0x10, 0x01, 0x18, 0x64, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x12, 0x3c, 0x0a, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x06, 0xba, 0x48, - 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4a, - 0x0a, 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x42, 0x0a, 0x10, 0x73, 0x6c, - 0x6f, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x0f, 0x73, - 0x6c, 0x6f, 0x74, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x10, - 0x0a, 0x0e, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, - 0x22, 0xa6, 0x01, 0x0a, 0x10, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, - 0x0a, 0x0e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, - 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0x65, 0x0a, 0x0d, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x44, - 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, - 0x52, 0x4f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, - 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x10, 0x02, - 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, - 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, - 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, - 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, - 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, 0x43, 0x4b, 0x5f, - 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, - 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, - 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, - 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x41, 0x43, - 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, - 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a, 0x70, 0x0a, 0x0f, 0x52, 0x61, 0x63, - 0x6b, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x1d, - 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x19, 0x0a, 0x15, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x41, - 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x49, 0x4d, 0x4d, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x2a, 0xdd, 0x01, 0x0a, 0x10, + 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x22, 0x1f, 0x0a, 0x1d, + 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x0a, + 0x13, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, + 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x22, 0x02, 0x20, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, + 0x64, 0x22, 0x74, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x12, 0x2b, 0x0a, + 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, + 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, + 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2d, 0x0a, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x22, 0xde, + 0x07, 0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, + 0x2c, 0x0a, 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, + 0x79, 0x5f, 0x6a, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, + 0x45, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, + 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, + 0x72, 0x4b, 0x77, 0x12, 0x2a, 0x0a, 0x11, 0x6d, 0x69, 0x6e, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, + 0x6d, 0x69, 0x6e, 0x54, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x12, + 0x2a, 0x0a, 0x11, 0x6d, 0x61, 0x78, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x5f, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x54, + 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x12, 0x23, 0x0a, 0x0d, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, + 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, + 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x1a, 0x65, 0x66, 0x66, + 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, + 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x65, + 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x6f, 0x77, 0x65, 0x72, + 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1b, 0x74, + 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x19, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, 0x73, + 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, 0x75, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x69, 0x73, + 0x73, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0d, 0x66, 0x61, 0x6e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x33, + 0x0a, 0x16, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x73, 0x73, + 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, + 0x68, 0x61, 0x73, 0x68, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x73, 0x75, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x70, 0x73, + 0x75, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x0d, 0x73, + 0x6c, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x15, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x0c, 0x73, 0x6c, 0x6f, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x22, + 0x40, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x03, 0x52, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, + 0x73, 0x22, 0x50, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x22, 0x73, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x03, 0x72, 0x6f, 0x77, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, + 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, - 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4d, 0x50, 0x54, 0x59, - 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, - 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x53, 0x5f, 0x41, - 0x54, 0x54, 0x45, 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, - 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4c, - 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x53, 0x4c, 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x32, 0xd4, 0x0d, 0x0a, 0x10, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x60, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, - 0x65, 0x74, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, - 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x5d, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x73, 0x12, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, - 0x0a, 0x15, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x54, 0x6f, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0x2d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x22, + 0x19, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, + 0x65, 0x66, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x18, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x52, 0x05, 0x7a, 0x6f, 0x6e, 0x65, 0x73, + 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x57, 0x0a, 0x08, 0x52, 0x61, 0x63, 0x6b, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x22, 0x4f, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0a, 0x72, 0x61, + 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, + 0x65, 0x73, 0x22, 0xca, 0x02, 0x0a, 0x0f, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x0a, 0xba, + 0x48, 0x07, 0xd8, 0x01, 0x01, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0c, 0xba, 0x48, 0x09, 0xc8, + 0x01, 0x01, 0x72, 0x04, 0x10, 0x01, 0x18, 0x64, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, + 0x3c, 0x0a, 0x09, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x06, 0xba, 0x48, 0x03, + 0xc8, 0x01, 0x01, 0x52, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4a, 0x0a, + 0x0f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x42, 0x0a, 0x10, 0x73, 0x6c, 0x6f, + 0x74, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x0f, 0x73, 0x6c, + 0x6f, 0x74, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x10, 0x0a, + 0x0e, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x22, + 0xa6, 0x01, 0x0a, 0x10, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, + 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, + 0x65, 0x74, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, 0x0a, + 0x0e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xaf, 0x01, 0x0a, 0x1a, 0x41, 0x73, 0x73, + 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x4a, 0x0a, 0x0f, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, + 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x22, 0x9d, 0x01, 0x0a, 0x1b, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x2a, 0x65, 0x0a, 0x0d, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x44, + 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, + 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x44, 0x45, 0x56, 0x49, 0x43, + 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x10, + 0x02, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, + 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, + 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, + 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x41, 0x43, 0x4b, + 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, + 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, + 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42, 0x4f, 0x54, 0x54, + 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x41, + 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x54, + 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a, 0x70, 0x0a, 0x0f, 0x52, 0x61, + 0x63, 0x6b, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, + 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x19, 0x0a, 0x15, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x52, + 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x49, 0x4d, 0x4d, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x2a, 0xdd, 0x01, 0x0a, + 0x10, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x4d, 0x50, 0x54, + 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, + 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, + 0x59, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, + 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x53, 0x5f, + 0x41, 0x54, 0x54, 0x45, 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x53, + 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x53, + 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x53, 0x4c, 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x32, 0xa9, 0x0e, 0x0a, + 0x10, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x53, 0x65, 0x74, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, + 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, + 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x5d, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x73, 0x12, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x66, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, - 0x6f, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x81, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x74, 0x12, 0x30, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, + 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x12, 0x2c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, - 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2a, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x29, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, - 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, - 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, - 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, - 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, - 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, - 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x61, - 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, - 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, - 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, - 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, - 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, - 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, - 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x12, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, - 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, + 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, + 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, + 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, + 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x6c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, + 0x13, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, + 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, + 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, + 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2c, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, + 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, + 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, + 0x73, 0x12, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, + 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, + 0x12, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, - 0x6b, 0x12, 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0xc3, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x53, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, - 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x44, 0x58, 0x58, 0xaa, - 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, - 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x53, 0x65, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, + 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, + 0x65, 0x73, 0x12, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, + 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, + 0x08, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x41, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, + 0x6b, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, + 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, + 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xc3, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, + 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0e, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x4d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2f, + 0x76, 0x31, 0x3b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x76, 0x31, 0xa2, + 0x02, 0x03, 0x44, 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, + 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, + 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x18, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, + 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x0d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -3720,91 +3855,93 @@ func file_device_set_v1_device_set_proto_rawDescGZIP() []byte { } var file_device_set_v1_device_set_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_device_set_v1_device_set_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_device_set_v1_device_set_proto_msgTypes = make([]protoimpl.MessageInfo, 46) var file_device_set_v1_device_set_proto_goTypes = []any{ - (DeviceSetType)(0), // 0: device_set.v1.DeviceSetType - (RackOrderIndex)(0), // 1: device_set.v1.RackOrderIndex - (RackCoolingType)(0), // 2: device_set.v1.RackCoolingType - (SlotDeviceStatus)(0), // 3: device_set.v1.SlotDeviceStatus - (*DeviceSet)(nil), // 4: device_set.v1.DeviceSet - (*RackInfo)(nil), // 5: device_set.v1.RackInfo - (*GroupInfo)(nil), // 6: device_set.v1.GroupInfo - (*DeviceSetMember)(nil), // 7: device_set.v1.DeviceSetMember - (*RackMemberDetails)(nil), // 8: device_set.v1.RackMemberDetails - (*RackSlotPosition)(nil), // 9: device_set.v1.RackSlotPosition - (*CreateDeviceSetRequest)(nil), // 10: device_set.v1.CreateDeviceSetRequest - (*CreateDeviceSetResponse)(nil), // 11: device_set.v1.CreateDeviceSetResponse - (*GetDeviceSetRequest)(nil), // 12: device_set.v1.GetDeviceSetRequest - (*GetDeviceSetResponse)(nil), // 13: device_set.v1.GetDeviceSetResponse - (*UpdateDeviceSetRequest)(nil), // 14: device_set.v1.UpdateDeviceSetRequest - (*UpdateDeviceSetResponse)(nil), // 15: device_set.v1.UpdateDeviceSetResponse - (*DeleteDeviceSetRequest)(nil), // 16: device_set.v1.DeleteDeviceSetRequest - (*DeleteDeviceSetResponse)(nil), // 17: device_set.v1.DeleteDeviceSetResponse - (*ListDeviceSetsRequest)(nil), // 18: device_set.v1.ListDeviceSetsRequest - (*ListDeviceSetsResponse)(nil), // 19: device_set.v1.ListDeviceSetsResponse - (*AddDevicesToDeviceSetRequest)(nil), // 20: device_set.v1.AddDevicesToDeviceSetRequest - (*AddDevicesToDeviceSetResponse)(nil), // 21: device_set.v1.AddDevicesToDeviceSetResponse - (*RemoveDevicesFromDeviceSetRequest)(nil), // 22: device_set.v1.RemoveDevicesFromDeviceSetRequest - (*RemoveDevicesFromDeviceSetResponse)(nil), // 23: device_set.v1.RemoveDevicesFromDeviceSetResponse - (*ListDeviceSetMembersRequest)(nil), // 24: device_set.v1.ListDeviceSetMembersRequest - (*ListDeviceSetMembersResponse)(nil), // 25: device_set.v1.ListDeviceSetMembersResponse - (*GetDeviceDeviceSetsRequest)(nil), // 26: device_set.v1.GetDeviceDeviceSetsRequest - (*GetDeviceDeviceSetsResponse)(nil), // 27: device_set.v1.GetDeviceDeviceSetsResponse - (*SetRackSlotPositionRequest)(nil), // 28: device_set.v1.SetRackSlotPositionRequest - (*SetRackSlotPositionResponse)(nil), // 29: device_set.v1.SetRackSlotPositionResponse - (*ClearRackSlotPositionRequest)(nil), // 30: device_set.v1.ClearRackSlotPositionRequest - (*ClearRackSlotPositionResponse)(nil), // 31: device_set.v1.ClearRackSlotPositionResponse - (*GetRackSlotsRequest)(nil), // 32: device_set.v1.GetRackSlotsRequest - (*RackSlot)(nil), // 33: device_set.v1.RackSlot - (*GetRackSlotsResponse)(nil), // 34: device_set.v1.GetRackSlotsResponse - (*DeviceSetStats)(nil), // 35: device_set.v1.DeviceSetStats - (*GetDeviceSetStatsRequest)(nil), // 36: device_set.v1.GetDeviceSetStatsRequest - (*GetDeviceSetStatsResponse)(nil), // 37: device_set.v1.GetDeviceSetStatsResponse - (*RackSlotStatus)(nil), // 38: device_set.v1.RackSlotStatus - (*ListRackZonesRequest)(nil), // 39: device_set.v1.ListRackZonesRequest - (*ListRackZonesResponse)(nil), // 40: device_set.v1.ListRackZonesResponse - (*ListRackZoneRefsRequest)(nil), // 41: device_set.v1.ListRackZoneRefsRequest - (*ListRackZoneRefsResponse)(nil), // 42: device_set.v1.ListRackZoneRefsResponse - (*ListRackTypesRequest)(nil), // 43: device_set.v1.ListRackTypesRequest - (*RackType)(nil), // 44: device_set.v1.RackType - (*ListRackTypesResponse)(nil), // 45: device_set.v1.ListRackTypesResponse - (*SaveRackRequest)(nil), // 46: device_set.v1.SaveRackRequest - (*SaveRackResponse)(nil), // 47: device_set.v1.SaveRackResponse - (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp - (*v1.DeviceSelector)(nil), // 49: common.v1.DeviceSelector - (*v1.SortConfig)(nil), // 50: common.v1.SortConfig - (v11.ComponentType)(0), // 51: errors.v1.ComponentType - (*v1.ZoneKey)(nil), // 52: common.v1.ZoneKey - (*v1.ZoneRef)(nil), // 53: common.v1.ZoneRef + (DeviceSetType)(0), // 0: device_set.v1.DeviceSetType + (RackOrderIndex)(0), // 1: device_set.v1.RackOrderIndex + (RackCoolingType)(0), // 2: device_set.v1.RackCoolingType + (SlotDeviceStatus)(0), // 3: device_set.v1.SlotDeviceStatus + (*DeviceSet)(nil), // 4: device_set.v1.DeviceSet + (*RackInfo)(nil), // 5: device_set.v1.RackInfo + (*GroupInfo)(nil), // 6: device_set.v1.GroupInfo + (*DeviceSetMember)(nil), // 7: device_set.v1.DeviceSetMember + (*RackMemberDetails)(nil), // 8: device_set.v1.RackMemberDetails + (*RackSlotPosition)(nil), // 9: device_set.v1.RackSlotPosition + (*CreateDeviceSetRequest)(nil), // 10: device_set.v1.CreateDeviceSetRequest + (*CreateDeviceSetResponse)(nil), // 11: device_set.v1.CreateDeviceSetResponse + (*GetDeviceSetRequest)(nil), // 12: device_set.v1.GetDeviceSetRequest + (*GetDeviceSetResponse)(nil), // 13: device_set.v1.GetDeviceSetResponse + (*UpdateDeviceSetRequest)(nil), // 14: device_set.v1.UpdateDeviceSetRequest + (*UpdateDeviceSetResponse)(nil), // 15: device_set.v1.UpdateDeviceSetResponse + (*DeleteDeviceSetRequest)(nil), // 16: device_set.v1.DeleteDeviceSetRequest + (*DeleteDeviceSetResponse)(nil), // 17: device_set.v1.DeleteDeviceSetResponse + (*ListDeviceSetsRequest)(nil), // 18: device_set.v1.ListDeviceSetsRequest + (*ListDeviceSetsResponse)(nil), // 19: device_set.v1.ListDeviceSetsResponse + (*AddDevicesToGroupRequest)(nil), // 20: device_set.v1.AddDevicesToGroupRequest + (*AddDevicesToGroupResponse)(nil), // 21: device_set.v1.AddDevicesToGroupResponse + (*RemoveDevicesFromGroupRequest)(nil), // 22: device_set.v1.RemoveDevicesFromGroupRequest + (*RemoveDevicesFromGroupResponse)(nil), // 23: device_set.v1.RemoveDevicesFromGroupResponse + (*ListDeviceSetMembersRequest)(nil), // 24: device_set.v1.ListDeviceSetMembersRequest + (*ListDeviceSetMembersResponse)(nil), // 25: device_set.v1.ListDeviceSetMembersResponse + (*GetDeviceDeviceSetsRequest)(nil), // 26: device_set.v1.GetDeviceDeviceSetsRequest + (*GetDeviceDeviceSetsResponse)(nil), // 27: device_set.v1.GetDeviceDeviceSetsResponse + (*SetRackSlotPositionRequest)(nil), // 28: device_set.v1.SetRackSlotPositionRequest + (*SetRackSlotPositionResponse)(nil), // 29: device_set.v1.SetRackSlotPositionResponse + (*ClearRackSlotPositionRequest)(nil), // 30: device_set.v1.ClearRackSlotPositionRequest + (*ClearRackSlotPositionResponse)(nil), // 31: device_set.v1.ClearRackSlotPositionResponse + (*GetRackSlotsRequest)(nil), // 32: device_set.v1.GetRackSlotsRequest + (*RackSlot)(nil), // 33: device_set.v1.RackSlot + (*GetRackSlotsResponse)(nil), // 34: device_set.v1.GetRackSlotsResponse + (*DeviceSetStats)(nil), // 35: device_set.v1.DeviceSetStats + (*GetDeviceSetStatsRequest)(nil), // 36: device_set.v1.GetDeviceSetStatsRequest + (*GetDeviceSetStatsResponse)(nil), // 37: device_set.v1.GetDeviceSetStatsResponse + (*RackSlotStatus)(nil), // 38: device_set.v1.RackSlotStatus + (*ListRackZonesRequest)(nil), // 39: device_set.v1.ListRackZonesRequest + (*ListRackZonesResponse)(nil), // 40: device_set.v1.ListRackZonesResponse + (*ListRackZoneRefsRequest)(nil), // 41: device_set.v1.ListRackZoneRefsRequest + (*ListRackZoneRefsResponse)(nil), // 42: device_set.v1.ListRackZoneRefsResponse + (*ListRackTypesRequest)(nil), // 43: device_set.v1.ListRackTypesRequest + (*RackType)(nil), // 44: device_set.v1.RackType + (*ListRackTypesResponse)(nil), // 45: device_set.v1.ListRackTypesResponse + (*SaveRackRequest)(nil), // 46: device_set.v1.SaveRackRequest + (*SaveRackResponse)(nil), // 47: device_set.v1.SaveRackResponse + (*AssignDevicesToRackRequest)(nil), // 48: device_set.v1.AssignDevicesToRackRequest + (*AssignDevicesToRackResponse)(nil), // 49: device_set.v1.AssignDevicesToRackResponse + (*timestamppb.Timestamp)(nil), // 50: google.protobuf.Timestamp + (*v1.DeviceSelector)(nil), // 51: common.v1.DeviceSelector + (*v1.SortConfig)(nil), // 52: common.v1.SortConfig + (v11.ComponentType)(0), // 53: errors.v1.ComponentType + (*v1.ZoneKey)(nil), // 54: common.v1.ZoneKey + (*v1.ZoneRef)(nil), // 55: common.v1.ZoneRef } var file_device_set_v1_device_set_proto_depIdxs = []int32{ 0, // 0: device_set.v1.DeviceSet.type:type_name -> device_set.v1.DeviceSetType - 48, // 1: device_set.v1.DeviceSet.created_at:type_name -> google.protobuf.Timestamp - 48, // 2: device_set.v1.DeviceSet.updated_at:type_name -> google.protobuf.Timestamp + 50, // 1: device_set.v1.DeviceSet.created_at:type_name -> google.protobuf.Timestamp + 50, // 2: device_set.v1.DeviceSet.updated_at:type_name -> google.protobuf.Timestamp 5, // 3: device_set.v1.DeviceSet.rack_info:type_name -> device_set.v1.RackInfo 6, // 4: device_set.v1.DeviceSet.group_info:type_name -> device_set.v1.GroupInfo 1, // 5: device_set.v1.RackInfo.order_index:type_name -> device_set.v1.RackOrderIndex 2, // 6: device_set.v1.RackInfo.cooling_type:type_name -> device_set.v1.RackCoolingType - 48, // 7: device_set.v1.DeviceSetMember.added_at:type_name -> google.protobuf.Timestamp + 50, // 7: device_set.v1.DeviceSetMember.added_at:type_name -> google.protobuf.Timestamp 8, // 8: device_set.v1.DeviceSetMember.rack:type_name -> device_set.v1.RackMemberDetails 9, // 9: device_set.v1.RackMemberDetails.slot_position:type_name -> device_set.v1.RackSlotPosition 0, // 10: device_set.v1.CreateDeviceSetRequest.type:type_name -> device_set.v1.DeviceSetType 5, // 11: device_set.v1.CreateDeviceSetRequest.rack_info:type_name -> device_set.v1.RackInfo 6, // 12: device_set.v1.CreateDeviceSetRequest.group_info:type_name -> device_set.v1.GroupInfo - 49, // 13: device_set.v1.CreateDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector + 51, // 13: device_set.v1.CreateDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector 4, // 14: device_set.v1.CreateDeviceSetResponse.device_set:type_name -> device_set.v1.DeviceSet 4, // 15: device_set.v1.GetDeviceSetResponse.device_set:type_name -> device_set.v1.DeviceSet 5, // 16: device_set.v1.UpdateDeviceSetRequest.rack_info:type_name -> device_set.v1.RackInfo 6, // 17: device_set.v1.UpdateDeviceSetRequest.group_info:type_name -> device_set.v1.GroupInfo - 49, // 18: device_set.v1.UpdateDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector + 51, // 18: device_set.v1.UpdateDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector 4, // 19: device_set.v1.UpdateDeviceSetResponse.device_set:type_name -> device_set.v1.DeviceSet 0, // 20: device_set.v1.ListDeviceSetsRequest.type:type_name -> device_set.v1.DeviceSetType - 50, // 21: device_set.v1.ListDeviceSetsRequest.sort:type_name -> common.v1.SortConfig - 51, // 22: device_set.v1.ListDeviceSetsRequest.error_component_types:type_name -> errors.v1.ComponentType - 52, // 23: device_set.v1.ListDeviceSetsRequest.zone_keys:type_name -> common.v1.ZoneKey + 52, // 21: device_set.v1.ListDeviceSetsRequest.sort:type_name -> common.v1.SortConfig + 53, // 22: device_set.v1.ListDeviceSetsRequest.error_component_types:type_name -> errors.v1.ComponentType + 54, // 23: device_set.v1.ListDeviceSetsRequest.zone_keys:type_name -> common.v1.ZoneKey 4, // 24: device_set.v1.ListDeviceSetsResponse.device_sets:type_name -> device_set.v1.DeviceSet - 49, // 25: device_set.v1.AddDevicesToDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector - 49, // 26: device_set.v1.RemoveDevicesFromDeviceSetRequest.device_selector:type_name -> common.v1.DeviceSelector + 51, // 25: device_set.v1.AddDevicesToGroupRequest.device_selector:type_name -> common.v1.DeviceSelector + 51, // 26: device_set.v1.RemoveDevicesFromGroupRequest.device_selector:type_name -> common.v1.DeviceSelector 7, // 27: device_set.v1.ListDeviceSetMembersResponse.members:type_name -> device_set.v1.DeviceSetMember 0, // 28: device_set.v1.GetDeviceDeviceSetsRequest.type:type_name -> device_set.v1.DeviceSetType 4, // 29: device_set.v1.GetDeviceDeviceSetsResponse.device_sets:type_name -> device_set.v1.DeviceSet @@ -3815,51 +3952,54 @@ var file_device_set_v1_device_set_proto_depIdxs = []int32{ 38, // 34: device_set.v1.DeviceSetStats.slot_statuses:type_name -> device_set.v1.RackSlotStatus 35, // 35: device_set.v1.GetDeviceSetStatsResponse.stats:type_name -> device_set.v1.DeviceSetStats 3, // 36: device_set.v1.RackSlotStatus.status:type_name -> device_set.v1.SlotDeviceStatus - 53, // 37: device_set.v1.ListRackZoneRefsResponse.zones:type_name -> common.v1.ZoneRef + 55, // 37: device_set.v1.ListRackZoneRefsResponse.zones:type_name -> common.v1.ZoneRef 44, // 38: device_set.v1.ListRackTypesResponse.rack_types:type_name -> device_set.v1.RackType 5, // 39: device_set.v1.SaveRackRequest.rack_info:type_name -> device_set.v1.RackInfo - 49, // 40: device_set.v1.SaveRackRequest.device_selector:type_name -> common.v1.DeviceSelector + 51, // 40: device_set.v1.SaveRackRequest.device_selector:type_name -> common.v1.DeviceSelector 33, // 41: device_set.v1.SaveRackRequest.slot_assignments:type_name -> device_set.v1.RackSlot 4, // 42: device_set.v1.SaveRackResponse.device_set:type_name -> device_set.v1.DeviceSet - 10, // 43: device_set.v1.DeviceSetService.CreateDeviceSet:input_type -> device_set.v1.CreateDeviceSetRequest - 12, // 44: device_set.v1.DeviceSetService.GetDeviceSet:input_type -> device_set.v1.GetDeviceSetRequest - 14, // 45: device_set.v1.DeviceSetService.UpdateDeviceSet:input_type -> device_set.v1.UpdateDeviceSetRequest - 16, // 46: device_set.v1.DeviceSetService.DeleteDeviceSet:input_type -> device_set.v1.DeleteDeviceSetRequest - 18, // 47: device_set.v1.DeviceSetService.ListDeviceSets:input_type -> device_set.v1.ListDeviceSetsRequest - 20, // 48: device_set.v1.DeviceSetService.AddDevicesToDeviceSet:input_type -> device_set.v1.AddDevicesToDeviceSetRequest - 22, // 49: device_set.v1.DeviceSetService.RemoveDevicesFromDeviceSet:input_type -> device_set.v1.RemoveDevicesFromDeviceSetRequest - 24, // 50: device_set.v1.DeviceSetService.ListDeviceSetMembers:input_type -> device_set.v1.ListDeviceSetMembersRequest - 26, // 51: device_set.v1.DeviceSetService.GetDeviceDeviceSets:input_type -> device_set.v1.GetDeviceDeviceSetsRequest - 28, // 52: device_set.v1.DeviceSetService.SetRackSlotPosition:input_type -> device_set.v1.SetRackSlotPositionRequest - 30, // 53: device_set.v1.DeviceSetService.ClearRackSlotPosition:input_type -> device_set.v1.ClearRackSlotPositionRequest - 32, // 54: device_set.v1.DeviceSetService.GetRackSlots:input_type -> device_set.v1.GetRackSlotsRequest - 36, // 55: device_set.v1.DeviceSetService.GetDeviceSetStats:input_type -> device_set.v1.GetDeviceSetStatsRequest - 39, // 56: device_set.v1.DeviceSetService.ListRackZones:input_type -> device_set.v1.ListRackZonesRequest - 41, // 57: device_set.v1.DeviceSetService.ListRackZoneRefs:input_type -> device_set.v1.ListRackZoneRefsRequest - 43, // 58: device_set.v1.DeviceSetService.ListRackTypes:input_type -> device_set.v1.ListRackTypesRequest - 46, // 59: device_set.v1.DeviceSetService.SaveRack:input_type -> device_set.v1.SaveRackRequest - 11, // 60: device_set.v1.DeviceSetService.CreateDeviceSet:output_type -> device_set.v1.CreateDeviceSetResponse - 13, // 61: device_set.v1.DeviceSetService.GetDeviceSet:output_type -> device_set.v1.GetDeviceSetResponse - 15, // 62: device_set.v1.DeviceSetService.UpdateDeviceSet:output_type -> device_set.v1.UpdateDeviceSetResponse - 17, // 63: device_set.v1.DeviceSetService.DeleteDeviceSet:output_type -> device_set.v1.DeleteDeviceSetResponse - 19, // 64: device_set.v1.DeviceSetService.ListDeviceSets:output_type -> device_set.v1.ListDeviceSetsResponse - 21, // 65: device_set.v1.DeviceSetService.AddDevicesToDeviceSet:output_type -> device_set.v1.AddDevicesToDeviceSetResponse - 23, // 66: device_set.v1.DeviceSetService.RemoveDevicesFromDeviceSet:output_type -> device_set.v1.RemoveDevicesFromDeviceSetResponse - 25, // 67: device_set.v1.DeviceSetService.ListDeviceSetMembers:output_type -> device_set.v1.ListDeviceSetMembersResponse - 27, // 68: device_set.v1.DeviceSetService.GetDeviceDeviceSets:output_type -> device_set.v1.GetDeviceDeviceSetsResponse - 29, // 69: device_set.v1.DeviceSetService.SetRackSlotPosition:output_type -> device_set.v1.SetRackSlotPositionResponse - 31, // 70: device_set.v1.DeviceSetService.ClearRackSlotPosition:output_type -> device_set.v1.ClearRackSlotPositionResponse - 34, // 71: device_set.v1.DeviceSetService.GetRackSlots:output_type -> device_set.v1.GetRackSlotsResponse - 37, // 72: device_set.v1.DeviceSetService.GetDeviceSetStats:output_type -> device_set.v1.GetDeviceSetStatsResponse - 40, // 73: device_set.v1.DeviceSetService.ListRackZones:output_type -> device_set.v1.ListRackZonesResponse - 42, // 74: device_set.v1.DeviceSetService.ListRackZoneRefs:output_type -> device_set.v1.ListRackZoneRefsResponse - 45, // 75: device_set.v1.DeviceSetService.ListRackTypes:output_type -> device_set.v1.ListRackTypesResponse - 47, // 76: device_set.v1.DeviceSetService.SaveRack:output_type -> device_set.v1.SaveRackResponse - 60, // [60:77] is the sub-list for method output_type - 43, // [43:60] is the sub-list for method input_type - 43, // [43:43] is the sub-list for extension type_name - 43, // [43:43] is the sub-list for extension extendee - 0, // [0:43] is the sub-list for field type_name + 51, // 43: device_set.v1.AssignDevicesToRackRequest.device_selector:type_name -> common.v1.DeviceSelector + 10, // 44: device_set.v1.DeviceSetService.CreateDeviceSet:input_type -> device_set.v1.CreateDeviceSetRequest + 12, // 45: device_set.v1.DeviceSetService.GetDeviceSet:input_type -> device_set.v1.GetDeviceSetRequest + 14, // 46: device_set.v1.DeviceSetService.UpdateDeviceSet:input_type -> device_set.v1.UpdateDeviceSetRequest + 16, // 47: device_set.v1.DeviceSetService.DeleteDeviceSet:input_type -> device_set.v1.DeleteDeviceSetRequest + 18, // 48: device_set.v1.DeviceSetService.ListDeviceSets:input_type -> device_set.v1.ListDeviceSetsRequest + 20, // 49: device_set.v1.DeviceSetService.AddDevicesToGroup:input_type -> device_set.v1.AddDevicesToGroupRequest + 22, // 50: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:input_type -> device_set.v1.RemoveDevicesFromGroupRequest + 24, // 51: device_set.v1.DeviceSetService.ListDeviceSetMembers:input_type -> device_set.v1.ListDeviceSetMembersRequest + 26, // 52: device_set.v1.DeviceSetService.GetDeviceDeviceSets:input_type -> device_set.v1.GetDeviceDeviceSetsRequest + 28, // 53: device_set.v1.DeviceSetService.SetRackSlotPosition:input_type -> device_set.v1.SetRackSlotPositionRequest + 30, // 54: device_set.v1.DeviceSetService.ClearRackSlotPosition:input_type -> device_set.v1.ClearRackSlotPositionRequest + 32, // 55: device_set.v1.DeviceSetService.GetRackSlots:input_type -> device_set.v1.GetRackSlotsRequest + 36, // 56: device_set.v1.DeviceSetService.GetDeviceSetStats:input_type -> device_set.v1.GetDeviceSetStatsRequest + 39, // 57: device_set.v1.DeviceSetService.ListRackZones:input_type -> device_set.v1.ListRackZonesRequest + 41, // 58: device_set.v1.DeviceSetService.ListRackZoneRefs:input_type -> device_set.v1.ListRackZoneRefsRequest + 43, // 59: device_set.v1.DeviceSetService.ListRackTypes:input_type -> device_set.v1.ListRackTypesRequest + 46, // 60: device_set.v1.DeviceSetService.SaveRack:input_type -> device_set.v1.SaveRackRequest + 48, // 61: device_set.v1.DeviceSetService.AssignDevicesToRack:input_type -> device_set.v1.AssignDevicesToRackRequest + 11, // 62: device_set.v1.DeviceSetService.CreateDeviceSet:output_type -> device_set.v1.CreateDeviceSetResponse + 13, // 63: device_set.v1.DeviceSetService.GetDeviceSet:output_type -> device_set.v1.GetDeviceSetResponse + 15, // 64: device_set.v1.DeviceSetService.UpdateDeviceSet:output_type -> device_set.v1.UpdateDeviceSetResponse + 17, // 65: device_set.v1.DeviceSetService.DeleteDeviceSet:output_type -> device_set.v1.DeleteDeviceSetResponse + 19, // 66: device_set.v1.DeviceSetService.ListDeviceSets:output_type -> device_set.v1.ListDeviceSetsResponse + 21, // 67: device_set.v1.DeviceSetService.AddDevicesToGroup:output_type -> device_set.v1.AddDevicesToGroupResponse + 23, // 68: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:output_type -> device_set.v1.RemoveDevicesFromGroupResponse + 25, // 69: device_set.v1.DeviceSetService.ListDeviceSetMembers:output_type -> device_set.v1.ListDeviceSetMembersResponse + 27, // 70: device_set.v1.DeviceSetService.GetDeviceDeviceSets:output_type -> device_set.v1.GetDeviceDeviceSetsResponse + 29, // 71: device_set.v1.DeviceSetService.SetRackSlotPosition:output_type -> device_set.v1.SetRackSlotPositionResponse + 31, // 72: device_set.v1.DeviceSetService.ClearRackSlotPosition:output_type -> device_set.v1.ClearRackSlotPositionResponse + 34, // 73: device_set.v1.DeviceSetService.GetRackSlots:output_type -> device_set.v1.GetRackSlotsResponse + 37, // 74: device_set.v1.DeviceSetService.GetDeviceSetStats:output_type -> device_set.v1.GetDeviceSetStatsResponse + 40, // 75: device_set.v1.DeviceSetService.ListRackZones:output_type -> device_set.v1.ListRackZonesResponse + 42, // 76: device_set.v1.DeviceSetService.ListRackZoneRefs:output_type -> device_set.v1.ListRackZoneRefsResponse + 45, // 77: device_set.v1.DeviceSetService.ListRackTypes:output_type -> device_set.v1.ListRackTypesResponse + 47, // 78: device_set.v1.DeviceSetService.SaveRack:output_type -> device_set.v1.SaveRackResponse + 49, // 79: device_set.v1.DeviceSetService.AssignDevicesToRack:output_type -> device_set.v1.AssignDevicesToRackResponse + 62, // [62:80] is the sub-list for method output_type + 44, // [44:62] is the sub-list for method input_type + 44, // [44:44] is the sub-list for extension type_name + 44, // [44:44] is the sub-list for extension extendee + 0, // [0:44] is the sub-list for field type_name } func init() { file_device_set_v1_device_set_proto_init() } @@ -3884,13 +4024,14 @@ func file_device_set_v1_device_set_proto_init() { (*UpdateDeviceSetRequest_GroupInfo)(nil), } file_device_set_v1_device_set_proto_msgTypes[42].OneofWrappers = []any{} + file_device_set_v1_device_set_proto_msgTypes[44].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_device_set_v1_device_set_proto_rawDesc), len(file_device_set_v1_device_set_proto_rawDesc)), NumEnums: 4, - NumMessages: 44, + NumMessages: 46, NumExtensions: 0, NumServices: 1, }, diff --git a/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go b/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go index 40441be54..ff1c76bb7 100644 --- a/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go +++ b/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go @@ -49,12 +49,12 @@ const ( // DeviceSetServiceListDeviceSetsProcedure is the fully-qualified name of the DeviceSetService's // ListDeviceSets RPC. DeviceSetServiceListDeviceSetsProcedure = "/device_set.v1.DeviceSetService/ListDeviceSets" - // DeviceSetServiceAddDevicesToDeviceSetProcedure is the fully-qualified name of the - // DeviceSetService's AddDevicesToDeviceSet RPC. - DeviceSetServiceAddDevicesToDeviceSetProcedure = "/device_set.v1.DeviceSetService/AddDevicesToDeviceSet" - // DeviceSetServiceRemoveDevicesFromDeviceSetProcedure is the fully-qualified name of the - // DeviceSetService's RemoveDevicesFromDeviceSet RPC. - DeviceSetServiceRemoveDevicesFromDeviceSetProcedure = "/device_set.v1.DeviceSetService/RemoveDevicesFromDeviceSet" + // DeviceSetServiceAddDevicesToGroupProcedure is the fully-qualified name of the DeviceSetService's + // AddDevicesToGroup RPC. + DeviceSetServiceAddDevicesToGroupProcedure = "/device_set.v1.DeviceSetService/AddDevicesToGroup" + // DeviceSetServiceRemoveDevicesFromGroupProcedure is the fully-qualified name of the + // DeviceSetService's RemoveDevicesFromGroup RPC. + DeviceSetServiceRemoveDevicesFromGroupProcedure = "/device_set.v1.DeviceSetService/RemoveDevicesFromGroup" // DeviceSetServiceListDeviceSetMembersProcedure is the fully-qualified name of the // DeviceSetService's ListDeviceSetMembers RPC. DeviceSetServiceListDeviceSetMembersProcedure = "/device_set.v1.DeviceSetService/ListDeviceSetMembers" @@ -85,6 +85,9 @@ const ( // DeviceSetServiceSaveRackProcedure is the fully-qualified name of the DeviceSetService's SaveRack // RPC. DeviceSetServiceSaveRackProcedure = "/device_set.v1.DeviceSetService/SaveRack" + // DeviceSetServiceAssignDevicesToRackProcedure is the fully-qualified name of the + // DeviceSetService's AssignDevicesToRack RPC. + DeviceSetServiceAssignDevicesToRackProcedure = "/device_set.v1.DeviceSetService/AssignDevicesToRack" ) // DeviceSetServiceClient is a client for the device_set.v1.DeviceSetService service. @@ -99,10 +102,13 @@ type DeviceSetServiceClient interface { DeleteDeviceSet(context.Context, *connect.Request[v1.DeleteDeviceSetRequest]) (*connect.Response[v1.DeleteDeviceSetResponse], error) // Lists all device sets for the organization ListDeviceSets(context.Context, *connect.Request[v1.ListDeviceSetsRequest]) (*connect.Response[v1.ListDeviceSetsResponse], error) - // Adds devices to a device set - AddDevicesToDeviceSet(context.Context, *connect.Request[v1.AddDevicesToDeviceSetRequest]) (*connect.Response[v1.AddDevicesToDeviceSetResponse], error) - // Removes devices from a device set - RemoveDevicesFromDeviceSet(context.Context, *connect.Request[v1.RemoveDevicesFromDeviceSetRequest]) (*connect.Response[v1.RemoveDevicesFromDeviceSetResponse], error) + // Adds devices to a group (many-to-many). Rejects non-group targets + // with InvalidArgument; use AssignDevicesToRack for racks. + AddDevicesToGroup(context.Context, *connect.Request[v1.AddDevicesToGroupRequest]) (*connect.Response[v1.AddDevicesToGroupResponse], error) + // Removes devices from a group. Rejects non-group targets with + // InvalidArgument; rack membership is cleared via + // AssignDevicesToRack with target_rack_id unset. + RemoveDevicesFromGroup(context.Context, *connect.Request[v1.RemoveDevicesFromGroupRequest]) (*connect.Response[v1.RemoveDevicesFromGroupResponse], error) // Lists members of a device set ListDeviceSetMembers(context.Context, *connect.Request[v1.ListDeviceSetMembersRequest]) (*connect.Response[v1.ListDeviceSetMembersResponse], error) // Gets device sets that a device belongs to @@ -129,6 +135,23 @@ type DeviceSetServiceClient interface { // Atomically creates or updates a rack with its membership and slot assignments. // All operations (metadata, membership, slot positions) are applied in a single transaction. SaveRack(context.Context, *connect.Request[v1.SaveRackRequest]) (*connect.Response[v1.SaveRackResponse], error) + // AssignDevicesToRack atomically moves devices into the target rack + // in one transaction: removes any existing rack membership for each + // device, inserts new membership at target_rack_id, and cascades + // device.site_id when the target rack's site differs from the + // device's current site. target_rack_id unset clears rack + // membership without re-assigning (site/building stay intact). + // + // Closes the orphan window where a client-side remove + add pair + // could leave devices without rack membership on transport failure + // between the two calls, and the cross-rack rename race where the + // source rack's id resolution drifts between the resolve and the + // remove. + // + // device_selector accepts only the device_list variant. all_devices + // is rejected with InvalidArgument because moving every paired + // device into a single rack is never the intended operation. + AssignDevicesToRack(context.Context, *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error) } // NewDeviceSetServiceClient constructs a client for the device_set.v1.DeviceSetService service. By @@ -166,14 +189,14 @@ func NewDeviceSetServiceClient(httpClient connect.HTTPClient, baseURL string, op baseURL+DeviceSetServiceListDeviceSetsProcedure, opts..., ), - addDevicesToDeviceSet: connect.NewClient[v1.AddDevicesToDeviceSetRequest, v1.AddDevicesToDeviceSetResponse]( + addDevicesToGroup: connect.NewClient[v1.AddDevicesToGroupRequest, v1.AddDevicesToGroupResponse]( httpClient, - baseURL+DeviceSetServiceAddDevicesToDeviceSetProcedure, + baseURL+DeviceSetServiceAddDevicesToGroupProcedure, opts..., ), - removeDevicesFromDeviceSet: connect.NewClient[v1.RemoveDevicesFromDeviceSetRequest, v1.RemoveDevicesFromDeviceSetResponse]( + removeDevicesFromGroup: connect.NewClient[v1.RemoveDevicesFromGroupRequest, v1.RemoveDevicesFromGroupResponse]( httpClient, - baseURL+DeviceSetServiceRemoveDevicesFromDeviceSetProcedure, + baseURL+DeviceSetServiceRemoveDevicesFromGroupProcedure, opts..., ), listDeviceSetMembers: connect.NewClient[v1.ListDeviceSetMembersRequest, v1.ListDeviceSetMembersResponse]( @@ -226,28 +249,34 @@ func NewDeviceSetServiceClient(httpClient connect.HTTPClient, baseURL string, op baseURL+DeviceSetServiceSaveRackProcedure, opts..., ), + assignDevicesToRack: connect.NewClient[v1.AssignDevicesToRackRequest, v1.AssignDevicesToRackResponse]( + httpClient, + baseURL+DeviceSetServiceAssignDevicesToRackProcedure, + opts..., + ), } } // deviceSetServiceClient implements DeviceSetServiceClient. type deviceSetServiceClient struct { - createDeviceSet *connect.Client[v1.CreateDeviceSetRequest, v1.CreateDeviceSetResponse] - getDeviceSet *connect.Client[v1.GetDeviceSetRequest, v1.GetDeviceSetResponse] - updateDeviceSet *connect.Client[v1.UpdateDeviceSetRequest, v1.UpdateDeviceSetResponse] - deleteDeviceSet *connect.Client[v1.DeleteDeviceSetRequest, v1.DeleteDeviceSetResponse] - listDeviceSets *connect.Client[v1.ListDeviceSetsRequest, v1.ListDeviceSetsResponse] - addDevicesToDeviceSet *connect.Client[v1.AddDevicesToDeviceSetRequest, v1.AddDevicesToDeviceSetResponse] - removeDevicesFromDeviceSet *connect.Client[v1.RemoveDevicesFromDeviceSetRequest, v1.RemoveDevicesFromDeviceSetResponse] - listDeviceSetMembers *connect.Client[v1.ListDeviceSetMembersRequest, v1.ListDeviceSetMembersResponse] - getDeviceDeviceSets *connect.Client[v1.GetDeviceDeviceSetsRequest, v1.GetDeviceDeviceSetsResponse] - setRackSlotPosition *connect.Client[v1.SetRackSlotPositionRequest, v1.SetRackSlotPositionResponse] - clearRackSlotPosition *connect.Client[v1.ClearRackSlotPositionRequest, v1.ClearRackSlotPositionResponse] - getRackSlots *connect.Client[v1.GetRackSlotsRequest, v1.GetRackSlotsResponse] - getDeviceSetStats *connect.Client[v1.GetDeviceSetStatsRequest, v1.GetDeviceSetStatsResponse] - listRackZones *connect.Client[v1.ListRackZonesRequest, v1.ListRackZonesResponse] - listRackZoneRefs *connect.Client[v1.ListRackZoneRefsRequest, v1.ListRackZoneRefsResponse] - listRackTypes *connect.Client[v1.ListRackTypesRequest, v1.ListRackTypesResponse] - saveRack *connect.Client[v1.SaveRackRequest, v1.SaveRackResponse] + createDeviceSet *connect.Client[v1.CreateDeviceSetRequest, v1.CreateDeviceSetResponse] + getDeviceSet *connect.Client[v1.GetDeviceSetRequest, v1.GetDeviceSetResponse] + updateDeviceSet *connect.Client[v1.UpdateDeviceSetRequest, v1.UpdateDeviceSetResponse] + deleteDeviceSet *connect.Client[v1.DeleteDeviceSetRequest, v1.DeleteDeviceSetResponse] + listDeviceSets *connect.Client[v1.ListDeviceSetsRequest, v1.ListDeviceSetsResponse] + addDevicesToGroup *connect.Client[v1.AddDevicesToGroupRequest, v1.AddDevicesToGroupResponse] + removeDevicesFromGroup *connect.Client[v1.RemoveDevicesFromGroupRequest, v1.RemoveDevicesFromGroupResponse] + listDeviceSetMembers *connect.Client[v1.ListDeviceSetMembersRequest, v1.ListDeviceSetMembersResponse] + getDeviceDeviceSets *connect.Client[v1.GetDeviceDeviceSetsRequest, v1.GetDeviceDeviceSetsResponse] + setRackSlotPosition *connect.Client[v1.SetRackSlotPositionRequest, v1.SetRackSlotPositionResponse] + clearRackSlotPosition *connect.Client[v1.ClearRackSlotPositionRequest, v1.ClearRackSlotPositionResponse] + getRackSlots *connect.Client[v1.GetRackSlotsRequest, v1.GetRackSlotsResponse] + getDeviceSetStats *connect.Client[v1.GetDeviceSetStatsRequest, v1.GetDeviceSetStatsResponse] + listRackZones *connect.Client[v1.ListRackZonesRequest, v1.ListRackZonesResponse] + listRackZoneRefs *connect.Client[v1.ListRackZoneRefsRequest, v1.ListRackZoneRefsResponse] + listRackTypes *connect.Client[v1.ListRackTypesRequest, v1.ListRackTypesResponse] + saveRack *connect.Client[v1.SaveRackRequest, v1.SaveRackResponse] + assignDevicesToRack *connect.Client[v1.AssignDevicesToRackRequest, v1.AssignDevicesToRackResponse] } // CreateDeviceSet calls device_set.v1.DeviceSetService.CreateDeviceSet. @@ -275,14 +304,14 @@ func (c *deviceSetServiceClient) ListDeviceSets(ctx context.Context, req *connec return c.listDeviceSets.CallUnary(ctx, req) } -// AddDevicesToDeviceSet calls device_set.v1.DeviceSetService.AddDevicesToDeviceSet. -func (c *deviceSetServiceClient) AddDevicesToDeviceSet(ctx context.Context, req *connect.Request[v1.AddDevicesToDeviceSetRequest]) (*connect.Response[v1.AddDevicesToDeviceSetResponse], error) { - return c.addDevicesToDeviceSet.CallUnary(ctx, req) +// AddDevicesToGroup calls device_set.v1.DeviceSetService.AddDevicesToGroup. +func (c *deviceSetServiceClient) AddDevicesToGroup(ctx context.Context, req *connect.Request[v1.AddDevicesToGroupRequest]) (*connect.Response[v1.AddDevicesToGroupResponse], error) { + return c.addDevicesToGroup.CallUnary(ctx, req) } -// RemoveDevicesFromDeviceSet calls device_set.v1.DeviceSetService.RemoveDevicesFromDeviceSet. -func (c *deviceSetServiceClient) RemoveDevicesFromDeviceSet(ctx context.Context, req *connect.Request[v1.RemoveDevicesFromDeviceSetRequest]) (*connect.Response[v1.RemoveDevicesFromDeviceSetResponse], error) { - return c.removeDevicesFromDeviceSet.CallUnary(ctx, req) +// RemoveDevicesFromGroup calls device_set.v1.DeviceSetService.RemoveDevicesFromGroup. +func (c *deviceSetServiceClient) RemoveDevicesFromGroup(ctx context.Context, req *connect.Request[v1.RemoveDevicesFromGroupRequest]) (*connect.Response[v1.RemoveDevicesFromGroupResponse], error) { + return c.removeDevicesFromGroup.CallUnary(ctx, req) } // ListDeviceSetMembers calls device_set.v1.DeviceSetService.ListDeviceSetMembers. @@ -335,6 +364,11 @@ func (c *deviceSetServiceClient) SaveRack(ctx context.Context, req *connect.Requ return c.saveRack.CallUnary(ctx, req) } +// AssignDevicesToRack calls device_set.v1.DeviceSetService.AssignDevicesToRack. +func (c *deviceSetServiceClient) AssignDevicesToRack(ctx context.Context, req *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error) { + return c.assignDevicesToRack.CallUnary(ctx, req) +} + // DeviceSetServiceHandler is an implementation of the device_set.v1.DeviceSetService service. type DeviceSetServiceHandler interface { // Creates a new device set @@ -347,10 +381,13 @@ type DeviceSetServiceHandler interface { DeleteDeviceSet(context.Context, *connect.Request[v1.DeleteDeviceSetRequest]) (*connect.Response[v1.DeleteDeviceSetResponse], error) // Lists all device sets for the organization ListDeviceSets(context.Context, *connect.Request[v1.ListDeviceSetsRequest]) (*connect.Response[v1.ListDeviceSetsResponse], error) - // Adds devices to a device set - AddDevicesToDeviceSet(context.Context, *connect.Request[v1.AddDevicesToDeviceSetRequest]) (*connect.Response[v1.AddDevicesToDeviceSetResponse], error) - // Removes devices from a device set - RemoveDevicesFromDeviceSet(context.Context, *connect.Request[v1.RemoveDevicesFromDeviceSetRequest]) (*connect.Response[v1.RemoveDevicesFromDeviceSetResponse], error) + // Adds devices to a group (many-to-many). Rejects non-group targets + // with InvalidArgument; use AssignDevicesToRack for racks. + AddDevicesToGroup(context.Context, *connect.Request[v1.AddDevicesToGroupRequest]) (*connect.Response[v1.AddDevicesToGroupResponse], error) + // Removes devices from a group. Rejects non-group targets with + // InvalidArgument; rack membership is cleared via + // AssignDevicesToRack with target_rack_id unset. + RemoveDevicesFromGroup(context.Context, *connect.Request[v1.RemoveDevicesFromGroupRequest]) (*connect.Response[v1.RemoveDevicesFromGroupResponse], error) // Lists members of a device set ListDeviceSetMembers(context.Context, *connect.Request[v1.ListDeviceSetMembersRequest]) (*connect.Response[v1.ListDeviceSetMembersResponse], error) // Gets device sets that a device belongs to @@ -377,6 +414,23 @@ type DeviceSetServiceHandler interface { // Atomically creates or updates a rack with its membership and slot assignments. // All operations (metadata, membership, slot positions) are applied in a single transaction. SaveRack(context.Context, *connect.Request[v1.SaveRackRequest]) (*connect.Response[v1.SaveRackResponse], error) + // AssignDevicesToRack atomically moves devices into the target rack + // in one transaction: removes any existing rack membership for each + // device, inserts new membership at target_rack_id, and cascades + // device.site_id when the target rack's site differs from the + // device's current site. target_rack_id unset clears rack + // membership without re-assigning (site/building stay intact). + // + // Closes the orphan window where a client-side remove + add pair + // could leave devices without rack membership on transport failure + // between the two calls, and the cross-rack rename race where the + // source rack's id resolution drifts between the resolve and the + // remove. + // + // device_selector accepts only the device_list variant. all_devices + // is rejected with InvalidArgument because moving every paired + // device into a single rack is never the intended operation. + AssignDevicesToRack(context.Context, *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error) } // NewDeviceSetServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -410,14 +464,14 @@ func NewDeviceSetServiceHandler(svc DeviceSetServiceHandler, opts ...connect.Han svc.ListDeviceSets, opts..., ) - deviceSetServiceAddDevicesToDeviceSetHandler := connect.NewUnaryHandler( - DeviceSetServiceAddDevicesToDeviceSetProcedure, - svc.AddDevicesToDeviceSet, + deviceSetServiceAddDevicesToGroupHandler := connect.NewUnaryHandler( + DeviceSetServiceAddDevicesToGroupProcedure, + svc.AddDevicesToGroup, opts..., ) - deviceSetServiceRemoveDevicesFromDeviceSetHandler := connect.NewUnaryHandler( - DeviceSetServiceRemoveDevicesFromDeviceSetProcedure, - svc.RemoveDevicesFromDeviceSet, + deviceSetServiceRemoveDevicesFromGroupHandler := connect.NewUnaryHandler( + DeviceSetServiceRemoveDevicesFromGroupProcedure, + svc.RemoveDevicesFromGroup, opts..., ) deviceSetServiceListDeviceSetMembersHandler := connect.NewUnaryHandler( @@ -470,6 +524,11 @@ func NewDeviceSetServiceHandler(svc DeviceSetServiceHandler, opts ...connect.Han svc.SaveRack, opts..., ) + deviceSetServiceAssignDevicesToRackHandler := connect.NewUnaryHandler( + DeviceSetServiceAssignDevicesToRackProcedure, + svc.AssignDevicesToRack, + opts..., + ) return "/device_set.v1.DeviceSetService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case DeviceSetServiceCreateDeviceSetProcedure: @@ -482,10 +541,10 @@ func NewDeviceSetServiceHandler(svc DeviceSetServiceHandler, opts ...connect.Han deviceSetServiceDeleteDeviceSetHandler.ServeHTTP(w, r) case DeviceSetServiceListDeviceSetsProcedure: deviceSetServiceListDeviceSetsHandler.ServeHTTP(w, r) - case DeviceSetServiceAddDevicesToDeviceSetProcedure: - deviceSetServiceAddDevicesToDeviceSetHandler.ServeHTTP(w, r) - case DeviceSetServiceRemoveDevicesFromDeviceSetProcedure: - deviceSetServiceRemoveDevicesFromDeviceSetHandler.ServeHTTP(w, r) + case DeviceSetServiceAddDevicesToGroupProcedure: + deviceSetServiceAddDevicesToGroupHandler.ServeHTTP(w, r) + case DeviceSetServiceRemoveDevicesFromGroupProcedure: + deviceSetServiceRemoveDevicesFromGroupHandler.ServeHTTP(w, r) case DeviceSetServiceListDeviceSetMembersProcedure: deviceSetServiceListDeviceSetMembersHandler.ServeHTTP(w, r) case DeviceSetServiceGetDeviceDeviceSetsProcedure: @@ -506,6 +565,8 @@ func NewDeviceSetServiceHandler(svc DeviceSetServiceHandler, opts ...connect.Han deviceSetServiceListRackTypesHandler.ServeHTTP(w, r) case DeviceSetServiceSaveRackProcedure: deviceSetServiceSaveRackHandler.ServeHTTP(w, r) + case DeviceSetServiceAssignDevicesToRackProcedure: + deviceSetServiceAssignDevicesToRackHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -535,12 +596,12 @@ func (UnimplementedDeviceSetServiceHandler) ListDeviceSets(context.Context, *con return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.ListDeviceSets is not implemented")) } -func (UnimplementedDeviceSetServiceHandler) AddDevicesToDeviceSet(context.Context, *connect.Request[v1.AddDevicesToDeviceSetRequest]) (*connect.Response[v1.AddDevicesToDeviceSetResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.AddDevicesToDeviceSet is not implemented")) +func (UnimplementedDeviceSetServiceHandler) AddDevicesToGroup(context.Context, *connect.Request[v1.AddDevicesToGroupRequest]) (*connect.Response[v1.AddDevicesToGroupResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.AddDevicesToGroup is not implemented")) } -func (UnimplementedDeviceSetServiceHandler) RemoveDevicesFromDeviceSet(context.Context, *connect.Request[v1.RemoveDevicesFromDeviceSetRequest]) (*connect.Response[v1.RemoveDevicesFromDeviceSetResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.RemoveDevicesFromDeviceSet is not implemented")) +func (UnimplementedDeviceSetServiceHandler) RemoveDevicesFromGroup(context.Context, *connect.Request[v1.RemoveDevicesFromGroupRequest]) (*connect.Response[v1.RemoveDevicesFromGroupResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.RemoveDevicesFromGroup is not implemented")) } func (UnimplementedDeviceSetServiceHandler) ListDeviceSetMembers(context.Context, *connect.Request[v1.ListDeviceSetMembersRequest]) (*connect.Response[v1.ListDeviceSetMembersResponse], error) { @@ -582,3 +643,7 @@ func (UnimplementedDeviceSetServiceHandler) ListRackTypes(context.Context, *conn func (UnimplementedDeviceSetServiceHandler) SaveRack(context.Context, *connect.Request[v1.SaveRackRequest]) (*connect.Response[v1.SaveRackResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.SaveRack is not implemented")) } + +func (UnimplementedDeviceSetServiceHandler) AssignDevicesToRack(context.Context, *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("device_set.v1.DeviceSetService.AssignDevicesToRack is not implemented")) +} diff --git a/server/generated/grpc/sites/v1/sites.pb.go b/server/generated/grpc/sites/v1/sites.pb.go index cd630b1df..f25b93937 100644 --- a/server/generated/grpc/sites/v1/sites.pb.go +++ b/server/generated/grpc/sites/v1/sites.pb.go @@ -25,7 +25,7 @@ const ( ) // PerDeviceConflictReason enumerates the reasons a device can be -// rejected by ReassignDevicesToSite. +// rejected by AssignDevicesToSite. type PerDeviceConflictReason int32 const ( @@ -840,29 +840,41 @@ func (x *DeleteSiteResponse) GetUnassignedRackCount() int64 { return 0 } -type ReassignDevicesToSiteRequest struct { +type AssignDevicesToSiteRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Unset = move to the "Unassigned" bucket. When present, must be > 0. TargetSiteId *int64 `protobuf:"varint,1,opt,name=target_site_id,json=targetSiteId,proto3,oneof" json:"target_site_id,omitempty"` DeviceIdentifiers []string `protobuf:"bytes,2,rep,name=device_identifiers,json=deviceIdentifiers,proto3" json:"device_identifiers,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ReassignDevicesToSiteRequest) Reset() { - *x = ReassignDevicesToSiteRequest{} + // When true, the server — inside the same transaction as the site + // write — removes any conflicting rack memberships for the listed + // devices before applying the site assignment. This closes the + // cross-site reparent orphan window the client-side + // remove-then-assign loop in MinerReparentPicker had: a transport + // failure between the per-rack unassign and the site reassign + // would leave miners rack-less but still pinned to the old site. + // + // When false (default), preserves today's behavior: any device + // currently in a rack at a different site rejects the whole batch + // with a PerDeviceConflict[] of DEVICE_IN_RACK_AT_OTHER_SITE. + ForceClearConflictingRackMembership *bool `protobuf:"varint,3,opt,name=force_clear_conflicting_rack_membership,json=forceClearConflictingRackMembership,proto3,oneof" json:"force_clear_conflicting_rack_membership,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignDevicesToSiteRequest) Reset() { + *x = AssignDevicesToSiteRequest{} mi := &file_sites_v1_sites_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ReassignDevicesToSiteRequest) String() string { +func (x *AssignDevicesToSiteRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ReassignDevicesToSiteRequest) ProtoMessage() {} +func (*AssignDevicesToSiteRequest) ProtoMessage() {} -func (x *ReassignDevicesToSiteRequest) ProtoReflect() protoreflect.Message { +func (x *AssignDevicesToSiteRequest) ProtoReflect() protoreflect.Message { mi := &file_sites_v1_sites_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -874,27 +886,34 @@ func (x *ReassignDevicesToSiteRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ReassignDevicesToSiteRequest.ProtoReflect.Descriptor instead. -func (*ReassignDevicesToSiteRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use AssignDevicesToSiteRequest.ProtoReflect.Descriptor instead. +func (*AssignDevicesToSiteRequest) Descriptor() ([]byte, []int) { return file_sites_v1_sites_proto_rawDescGZIP(), []int{10} } -func (x *ReassignDevicesToSiteRequest) GetTargetSiteId() int64 { +func (x *AssignDevicesToSiteRequest) GetTargetSiteId() int64 { if x != nil && x.TargetSiteId != nil { return *x.TargetSiteId } return 0 } -func (x *ReassignDevicesToSiteRequest) GetDeviceIdentifiers() []string { +func (x *AssignDevicesToSiteRequest) GetDeviceIdentifiers() []string { if x != nil { return x.DeviceIdentifiers } return nil } +func (x *AssignDevicesToSiteRequest) GetForceClearConflictingRackMembership() bool { + if x != nil && x.ForceClearConflictingRackMembership != nil { + return *x.ForceClearConflictingRackMembership + } + return false +} + // PerDeviceConflict carries the per-device error details surfaced -// when ReassignDevicesToSite rejects. The whole batch is rejected; +// when AssignDevicesToSite rejects. The whole batch is rejected; // no rows are mutated. type PerDeviceConflict struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -958,7 +977,7 @@ func (x *PerDeviceConflict) GetConflictingSiteId() int64 { return 0 } -type ReassignDevicesToSiteResponse struct { +type AssignDevicesToSiteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ReassignedCount int64 `protobuf:"varint,1,opt,name=reassigned_count,json=reassignedCount,proto3" json:"reassigned_count,omitempty"` // Populated only on rejection. Empty on success. @@ -967,20 +986,20 @@ type ReassignDevicesToSiteResponse struct { sizeCache protoimpl.SizeCache } -func (x *ReassignDevicesToSiteResponse) Reset() { - *x = ReassignDevicesToSiteResponse{} +func (x *AssignDevicesToSiteResponse) Reset() { + *x = AssignDevicesToSiteResponse{} mi := &file_sites_v1_sites_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ReassignDevicesToSiteResponse) String() string { +func (x *AssignDevicesToSiteResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ReassignDevicesToSiteResponse) ProtoMessage() {} +func (*AssignDevicesToSiteResponse) ProtoMessage() {} -func (x *ReassignDevicesToSiteResponse) ProtoReflect() protoreflect.Message { +func (x *AssignDevicesToSiteResponse) ProtoReflect() protoreflect.Message { mi := &file_sites_v1_sites_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -992,48 +1011,48 @@ func (x *ReassignDevicesToSiteResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ReassignDevicesToSiteResponse.ProtoReflect.Descriptor instead. -func (*ReassignDevicesToSiteResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AssignDevicesToSiteResponse.ProtoReflect.Descriptor instead. +func (*AssignDevicesToSiteResponse) Descriptor() ([]byte, []int) { return file_sites_v1_sites_proto_rawDescGZIP(), []int{12} } -func (x *ReassignDevicesToSiteResponse) GetReassignedCount() int64 { +func (x *AssignDevicesToSiteResponse) GetReassignedCount() int64 { if x != nil { return x.ReassignedCount } return 0 } -func (x *ReassignDevicesToSiteResponse) GetConflicts() []*PerDeviceConflict { +func (x *AssignDevicesToSiteResponse) GetConflicts() []*PerDeviceConflict { if x != nil { return x.Conflicts } return nil } -type AssignBuildingToSiteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - BuildingId int64 `protobuf:"varint,1,opt,name=building_id,json=buildingId,proto3" json:"building_id,omitempty"` - // Unset = move building to "Unassigned". When present, must be > 0. +type AssignBuildingsToSiteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BuildingIds []int64 `protobuf:"varint,1,rep,packed,name=building_ids,json=buildingIds,proto3" json:"building_ids,omitempty"` + // Unset = move buildings to "Unassigned". When present, must be > 0. TargetSiteId *int64 `protobuf:"varint,2,opt,name=target_site_id,json=targetSiteId,proto3,oneof" json:"target_site_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AssignBuildingToSiteRequest) Reset() { - *x = AssignBuildingToSiteRequest{} +func (x *AssignBuildingsToSiteRequest) Reset() { + *x = AssignBuildingsToSiteRequest{} mi := &file_sites_v1_sites_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AssignBuildingToSiteRequest) String() string { +func (x *AssignBuildingsToSiteRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AssignBuildingToSiteRequest) ProtoMessage() {} +func (*AssignBuildingsToSiteRequest) ProtoMessage() {} -func (x *AssignBuildingToSiteRequest) ProtoReflect() protoreflect.Message { +func (x *AssignBuildingsToSiteRequest) ProtoReflect() protoreflect.Message { mi := &file_sites_v1_sites_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1045,49 +1064,50 @@ func (x *AssignBuildingToSiteRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AssignBuildingToSiteRequest.ProtoReflect.Descriptor instead. -func (*AssignBuildingToSiteRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use AssignBuildingsToSiteRequest.ProtoReflect.Descriptor instead. +func (*AssignBuildingsToSiteRequest) Descriptor() ([]byte, []int) { return file_sites_v1_sites_proto_rawDescGZIP(), []int{13} } -func (x *AssignBuildingToSiteRequest) GetBuildingId() int64 { +func (x *AssignBuildingsToSiteRequest) GetBuildingIds() []int64 { if x != nil { - return x.BuildingId + return x.BuildingIds } - return 0 + return nil } -func (x *AssignBuildingToSiteRequest) GetTargetSiteId() int64 { +func (x *AssignBuildingsToSiteRequest) GetTargetSiteId() int64 { if x != nil && x.TargetSiteId != nil { return *x.TargetSiteId } return 0 } -type AssignBuildingToSiteResponse struct { +type AssignBuildingsToSiteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Cascade impact: how many racks and devices were re-stamped with - // the new site_id in the same transaction. + // the new site_id in the same transaction, aggregated across every + // building in the batch. ReassignedRackCount int64 `protobuf:"varint,1,opt,name=reassigned_rack_count,json=reassignedRackCount,proto3" json:"reassigned_rack_count,omitempty"` ReassignedDeviceCount int64 `protobuf:"varint,2,opt,name=reassigned_device_count,json=reassignedDeviceCount,proto3" json:"reassigned_device_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AssignBuildingToSiteResponse) Reset() { - *x = AssignBuildingToSiteResponse{} +func (x *AssignBuildingsToSiteResponse) Reset() { + *x = AssignBuildingsToSiteResponse{} mi := &file_sites_v1_sites_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AssignBuildingToSiteResponse) String() string { +func (x *AssignBuildingsToSiteResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AssignBuildingToSiteResponse) ProtoMessage() {} +func (*AssignBuildingsToSiteResponse) ProtoMessage() {} -func (x *AssignBuildingToSiteResponse) ProtoReflect() protoreflect.Message { +func (x *AssignBuildingsToSiteResponse) ProtoReflect() protoreflect.Message { mi := &file_sites_v1_sites_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1099,25 +1119,136 @@ func (x *AssignBuildingToSiteResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AssignBuildingToSiteResponse.ProtoReflect.Descriptor instead. -func (*AssignBuildingToSiteResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AssignBuildingsToSiteResponse.ProtoReflect.Descriptor instead. +func (*AssignBuildingsToSiteResponse) Descriptor() ([]byte, []int) { return file_sites_v1_sites_proto_rawDescGZIP(), []int{14} } -func (x *AssignBuildingToSiteResponse) GetReassignedRackCount() int64 { +func (x *AssignBuildingsToSiteResponse) GetReassignedRackCount() int64 { if x != nil { return x.ReassignedRackCount } return 0 } -func (x *AssignBuildingToSiteResponse) GetReassignedDeviceCount() int64 { +func (x *AssignBuildingsToSiteResponse) GetReassignedDeviceCount() int64 { if x != nil { return x.ReassignedDeviceCount } return 0 } +type AssignRacksToSiteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RackIds []int64 `protobuf:"varint,1,rep,packed,name=rack_ids,json=rackIds,proto3" json:"rack_ids,omitempty"` + // Unset = move racks to "Unassigned" (site_id NULL). When present, + // must be > 0. + TargetSiteId *int64 `protobuf:"varint,2,opt,name=target_site_id,json=targetSiteId,proto3,oneof" json:"target_site_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignRacksToSiteRequest) Reset() { + *x = AssignRacksToSiteRequest{} + mi := &file_sites_v1_sites_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssignRacksToSiteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssignRacksToSiteRequest) ProtoMessage() {} + +func (x *AssignRacksToSiteRequest) ProtoReflect() protoreflect.Message { + mi := &file_sites_v1_sites_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssignRacksToSiteRequest.ProtoReflect.Descriptor instead. +func (*AssignRacksToSiteRequest) Descriptor() ([]byte, []int) { + return file_sites_v1_sites_proto_rawDescGZIP(), []int{15} +} + +func (x *AssignRacksToSiteRequest) GetRackIds() []int64 { + if x != nil { + return x.RackIds + } + return nil +} + +func (x *AssignRacksToSiteRequest) GetTargetSiteId() int64 { + if x != nil && x.TargetSiteId != nil { + return *x.TargetSiteId + } + return 0 +} + +type AssignRacksToSiteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Cascade impact: how many devices had their site_id re-stamped + // across every rack in the batch. + ReassignedDeviceCount int64 `protobuf:"varint,1,opt,name=reassigned_device_count,json=reassignedDeviceCount,proto3" json:"reassigned_device_count,omitempty"` + // Number of racks whose building_id was cleared because the move + // crossed site boundaries. UI can use this to prompt for + // re-assignment to a building in the new site. + ClearedBuildingCount int64 `protobuf:"varint,2,opt,name=cleared_building_count,json=clearedBuildingCount,proto3" json:"cleared_building_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AssignRacksToSiteResponse) Reset() { + *x = AssignRacksToSiteResponse{} + mi := &file_sites_v1_sites_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AssignRacksToSiteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssignRacksToSiteResponse) ProtoMessage() {} + +func (x *AssignRacksToSiteResponse) ProtoReflect() protoreflect.Message { + mi := &file_sites_v1_sites_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssignRacksToSiteResponse.ProtoReflect.Descriptor instead. +func (*AssignRacksToSiteResponse) Descriptor() ([]byte, []int) { + return file_sites_v1_sites_proto_rawDescGZIP(), []int{16} +} + +func (x *AssignRacksToSiteResponse) GetReassignedDeviceCount() int64 { + if x != nil { + return x.ReassignedDeviceCount + } + return 0 +} + +func (x *AssignRacksToSiteResponse) GetClearedBuildingCount() int64 { + if x != nil { + return x.ClearedBuildingCount + } + return 0 +} + type GetSiteStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SiteId int64 `protobuf:"varint,1,opt,name=site_id,json=siteId,proto3" json:"site_id,omitempty"` @@ -1127,7 +1258,7 @@ type GetSiteStatsRequest struct { func (x *GetSiteStatsRequest) Reset() { *x = GetSiteStatsRequest{} - mi := &file_sites_v1_sites_proto_msgTypes[15] + mi := &file_sites_v1_sites_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1139,7 +1270,7 @@ func (x *GetSiteStatsRequest) String() string { func (*GetSiteStatsRequest) ProtoMessage() {} func (x *GetSiteStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sites_v1_sites_proto_msgTypes[15] + mi := &file_sites_v1_sites_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1152,7 +1283,7 @@ func (x *GetSiteStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSiteStatsRequest.ProtoReflect.Descriptor instead. func (*GetSiteStatsRequest) Descriptor() ([]byte, []int) { - return file_sites_v1_sites_proto_rawDescGZIP(), []int{15} + return file_sites_v1_sites_proto_rawDescGZIP(), []int{17} } func (x *GetSiteStatsRequest) GetSiteId() int64 { @@ -1194,7 +1325,7 @@ type GetSiteStatsResponse struct { func (x *GetSiteStatsResponse) Reset() { *x = GetSiteStatsResponse{} - mi := &file_sites_v1_sites_proto_msgTypes[16] + mi := &file_sites_v1_sites_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1206,7 +1337,7 @@ func (x *GetSiteStatsResponse) String() string { func (*GetSiteStatsResponse) ProtoMessage() {} func (x *GetSiteStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_sites_v1_sites_proto_msgTypes[16] + mi := &file_sites_v1_sites_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1219,7 +1350,7 @@ func (x *GetSiteStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSiteStatsResponse.ProtoReflect.Descriptor instead. func (*GetSiteStatsResponse) Descriptor() ([]byte, []int) { - return file_sites_v1_sites_proto_rawDescGZIP(), []int{16} + return file_sites_v1_sites_proto_rawDescGZIP(), []int{18} } func (x *GetSiteStatsResponse) GetSiteId() int64 { @@ -1466,156 +1597,188 @@ var file_sites_v1_sites_proto_rawDesc = string([]byte{ 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x75, 0x6e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x22, 0xaa, 0x01, 0x0a, 0x1c, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x32, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, - 0x20, 0x00, 0x48, 0x00, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, - 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x09, 0x42, 0x14, 0xba, 0x48, 0x11, 0x92, 0x01, 0x0e, 0x08, 0x01, 0x10, 0x90, 0x4e, 0x22, 0x07, - 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x02, 0x52, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x22, 0xab, 0x01, - 0x0a, 0x11, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x6c, - 0x69, 0x63, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x12, 0x39, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x21, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x63, - 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, - 0x63, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x74, 0x65, 0x49, 0x64, 0x22, 0x85, 0x01, 0x0a, 0x1d, - 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, - 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, - 0x10, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, - 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, - 0x6c, 0x69, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, 0x69, - 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, - 0x63, 0x74, 0x73, 0x22, 0x8e, 0x01, 0x0a, 0x1b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, - 0x00, 0x52, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, 0x32, 0x0a, + 0x22, 0xaf, 0x02, 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x32, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, + 0x48, 0x00, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x49, 0x64, + 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, + 0x14, 0xba, 0x48, 0x11, 0x92, 0x01, 0x0e, 0x08, 0x01, 0x10, 0x90, 0x4e, 0x22, 0x07, 0x72, 0x05, + 0x10, 0x01, 0x18, 0x80, 0x02, 0x52, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x59, 0x0a, 0x27, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x5f, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, + 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, + 0x68, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x23, 0x66, 0x6f, 0x72, + 0x63, 0x65, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, + 0x6e, 0x67, 0x52, 0x61, 0x63, 0x6b, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, + 0x88, 0x01, 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, + 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x42, 0x2a, 0x0a, 0x28, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x5f, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, + 0x69, 0x70, 0x22, 0xab, 0x01, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, + 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x5f, + 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x63, + 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x74, 0x65, 0x49, 0x64, + 0x22, 0x83, 0x01, 0x0a, 0x1b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x61, 0x73, + 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, + 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x09, 0x63, 0x6f, 0x6e, + 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x1c, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x42, 0x11, 0xba, + 0x48, 0x0e, 0x92, 0x01, 0x0b, 0x08, 0x01, 0x10, 0xe8, 0x07, 0x22, 0x04, 0x22, 0x02, 0x20, 0x00, + 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, - 0x65, 0x5f, 0x69, 0x64, 0x22, 0x8a, 0x01, 0x0a, 0x1c, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, - 0x6e, 0x65, 0x64, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, - 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x72, 0x65, 0x61, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x72, 0x65, 0x61, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x22, 0x37, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x07, 0x73, 0x69, 0x74, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, - 0x20, 0x00, 0x52, 0x06, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x22, 0xe4, 0x04, 0x0a, 0x14, 0x47, - 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x0e, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x2c, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, - 0x65, 0x5f, 0x74, 0x68, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, 0x2c, 0x0a, - 0x12, 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, - 0x6a, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, 0x45, 0x66, - 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x4b, - 0x77, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, - 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, - 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, - 0x0a, 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, - 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x3c, 0x0a, 0x1a, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x18, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, - 0x15, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, - 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, - 0x77, 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x2a, 0xb3, 0x01, 0x0a, 0x17, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, - 0x26, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, - 0x4c, 0x49, 0x43, 0x54, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2f, 0x0a, 0x2b, 0x50, 0x45, 0x52, + 0x65, 0x5f, 0x69, 0x64, 0x22, 0x8b, 0x01, 0x0a, 0x1d, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, + 0x64, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x72, 0x65, + 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x72, 0x65, 0x61, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x22, 0x8f, 0x01, 0x0a, 0x18, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, + 0x6b, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2c, 0x0a, 0x08, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x03, 0x42, 0x11, 0xba, 0x48, 0x0e, 0x92, 0x01, 0x0b, 0x08, 0x01, 0x10, 0xe8, 0x07, 0x22, 0x04, + 0x22, 0x02, 0x20, 0x00, 0x52, 0x07, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x64, 0x73, 0x12, 0x32, 0x0a, + 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, 0x00, 0x48, 0x00, + 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x49, 0x64, 0x88, 0x01, + 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x74, + 0x65, 0x5f, 0x69, 0x64, 0x22, 0x89, 0x01, 0x0a, 0x19, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, + 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x15, 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x63, 0x6c, + 0x65, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x63, 0x6c, 0x65, 0x61, + 0x72, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x37, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x07, 0x73, 0x69, 0x74, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07, 0xba, 0x48, 0x04, 0x22, 0x02, 0x20, + 0x00, 0x52, 0x06, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x22, 0xe4, 0x04, 0x0a, 0x14, 0x47, 0x65, + 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x69, 0x74, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0d, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, + 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, + 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x68, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x48, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, 0x54, 0x68, 0x73, 0x12, 0x2c, 0x0a, 0x12, + 0x61, 0x76, 0x67, 0x5f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6a, + 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x10, 0x61, 0x76, 0x67, 0x45, 0x66, 0x66, + 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x4a, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x6b, 0x77, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x01, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x4b, 0x77, + 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x69, 0x6e, 0x67, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62, 0x72, 0x6f, + 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x66, 0x66, 0x6c, + 0x69, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, + 0x0e, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x69, 0x6e, 0x67, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x18, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, + 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x16, 0x68, 0x61, 0x73, 0x68, 0x72, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, + 0x0a, 0x1a, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x18, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, + 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x70, 0x6f, 0x77, + 0x65, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x2a, 0xb3, 0x01, 0x0a, 0x17, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x26, + 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, + 0x49, 0x43, 0x54, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2f, 0x0a, 0x2b, 0x50, 0x45, 0x52, 0x5f, + 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, 0x5f, + 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4e, 0x4f, + 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x3b, 0x0a, 0x37, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, - 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4e, - 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x3b, 0x0a, 0x37, 0x50, 0x45, - 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, - 0x54, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, - 0x49, 0x4e, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x41, 0x54, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, - 0x5f, 0x53, 0x49, 0x54, 0x45, 0x10, 0x02, 0x32, 0xce, 0x04, 0x0a, 0x0b, 0x53, 0x69, 0x74, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x69, 0x74, 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, - 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x73, 0x69, - 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x47, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, - 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x52, 0x65, 0x61, 0x73, - 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, - 0x65, 0x12, 0x26, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, - 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, 0x69, 0x74, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x65, 0x0a, 0x14, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x25, 0x2e, 0x73, 0x69, 0x74, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x26, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, - 0x69, 0x67, 0x6e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x53, 0x69, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x47, 0x65, 0x74, - 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x73, 0x69, 0x74, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xa0, 0x01, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, - 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x53, 0x69, 0x74, 0x65, 0x73, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, - 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x69, 0x74, 0x65, - 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x69, 0x74, 0x65, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x53, - 0x58, 0x58, 0xaa, 0x02, 0x08, 0x53, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x08, - 0x53, 0x69, 0x74, 0x65, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x14, 0x53, 0x69, 0x74, 0x65, 0x73, - 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x09, 0x53, 0x69, 0x74, 0x65, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x49, + 0x4e, 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x41, 0x54, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x5f, + 0x53, 0x49, 0x54, 0x45, 0x10, 0x02, 0x32, 0xa9, 0x05, 0x0a, 0x0b, 0x53, 0x69, 0x74, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x69, + 0x74, 0x65, 0x73, 0x12, 0x1a, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1b, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0a, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x73, 0x69, 0x74, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, + 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x73, + 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x69, 0x74, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x69, 0x67, + 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x24, + 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x53, + 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x15, 0x41, + 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x54, 0x6f, + 0x53, 0x69, 0x74, 0x65, 0x12, 0x26, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x54, + 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, + 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5c, 0x0a, 0x11, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, + 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x22, 0x2e, 0x73, 0x69, 0x74, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x52, 0x61, 0x63, 0x6b, + 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, + 0x52, 0x61, 0x63, 0x6b, 0x73, 0x54, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x53, 0x69, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0xa0, 0x01, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x69, 0x74, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x53, 0x69, 0x74, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, + 0x69, 0x74, 0x65, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x53, 0x58, 0x58, 0xaa, 0x02, 0x08, 0x53, + 0x69, 0x74, 0x65, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x08, 0x53, 0x69, 0x74, 0x65, 0x73, 0x5c, + 0x56, 0x31, 0xe2, 0x02, 0x14, 0x53, 0x69, 0x74, 0x65, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x09, 0x53, 0x69, 0x74, 0x65, + 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1631,7 +1794,7 @@ func file_sites_v1_sites_proto_rawDescGZIP() []byte { } var file_sites_v1_sites_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_sites_v1_sites_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_sites_v1_sites_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_sites_v1_sites_proto_goTypes = []any{ (PerDeviceConflictReason)(0), // 0: sites.v1.PerDeviceConflictReason (*Site)(nil), // 1: sites.v1.Site @@ -1644,40 +1807,44 @@ var file_sites_v1_sites_proto_goTypes = []any{ (*UpdateSiteResponse)(nil), // 8: sites.v1.UpdateSiteResponse (*DeleteSiteRequest)(nil), // 9: sites.v1.DeleteSiteRequest (*DeleteSiteResponse)(nil), // 10: sites.v1.DeleteSiteResponse - (*ReassignDevicesToSiteRequest)(nil), // 11: sites.v1.ReassignDevicesToSiteRequest + (*AssignDevicesToSiteRequest)(nil), // 11: sites.v1.AssignDevicesToSiteRequest (*PerDeviceConflict)(nil), // 12: sites.v1.PerDeviceConflict - (*ReassignDevicesToSiteResponse)(nil), // 13: sites.v1.ReassignDevicesToSiteResponse - (*AssignBuildingToSiteRequest)(nil), // 14: sites.v1.AssignBuildingToSiteRequest - (*AssignBuildingToSiteResponse)(nil), // 15: sites.v1.AssignBuildingToSiteResponse - (*GetSiteStatsRequest)(nil), // 16: sites.v1.GetSiteStatsRequest - (*GetSiteStatsResponse)(nil), // 17: sites.v1.GetSiteStatsResponse - (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp + (*AssignDevicesToSiteResponse)(nil), // 13: sites.v1.AssignDevicesToSiteResponse + (*AssignBuildingsToSiteRequest)(nil), // 14: sites.v1.AssignBuildingsToSiteRequest + (*AssignBuildingsToSiteResponse)(nil), // 15: sites.v1.AssignBuildingsToSiteResponse + (*AssignRacksToSiteRequest)(nil), // 16: sites.v1.AssignRacksToSiteRequest + (*AssignRacksToSiteResponse)(nil), // 17: sites.v1.AssignRacksToSiteResponse + (*GetSiteStatsRequest)(nil), // 18: sites.v1.GetSiteStatsRequest + (*GetSiteStatsResponse)(nil), // 19: sites.v1.GetSiteStatsResponse + (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp } var file_sites_v1_sites_proto_depIdxs = []int32{ - 18, // 0: sites.v1.Site.created_at:type_name -> google.protobuf.Timestamp - 18, // 1: sites.v1.Site.updated_at:type_name -> google.protobuf.Timestamp + 20, // 0: sites.v1.Site.created_at:type_name -> google.protobuf.Timestamp + 20, // 1: sites.v1.Site.updated_at:type_name -> google.protobuf.Timestamp 1, // 2: sites.v1.SiteWithCounts.site:type_name -> sites.v1.Site 2, // 3: sites.v1.ListSitesResponse.sites:type_name -> sites.v1.SiteWithCounts 1, // 4: sites.v1.CreateSiteResponse.site:type_name -> sites.v1.Site 1, // 5: sites.v1.UpdateSiteResponse.site:type_name -> sites.v1.Site 0, // 6: sites.v1.PerDeviceConflict.reason:type_name -> sites.v1.PerDeviceConflictReason - 12, // 7: sites.v1.ReassignDevicesToSiteResponse.conflicts:type_name -> sites.v1.PerDeviceConflict + 12, // 7: sites.v1.AssignDevicesToSiteResponse.conflicts:type_name -> sites.v1.PerDeviceConflict 3, // 8: sites.v1.SiteService.ListSites:input_type -> sites.v1.ListSitesRequest 5, // 9: sites.v1.SiteService.CreateSite:input_type -> sites.v1.CreateSiteRequest 7, // 10: sites.v1.SiteService.UpdateSite:input_type -> sites.v1.UpdateSiteRequest 9, // 11: sites.v1.SiteService.DeleteSite:input_type -> sites.v1.DeleteSiteRequest - 11, // 12: sites.v1.SiteService.ReassignDevicesToSite:input_type -> sites.v1.ReassignDevicesToSiteRequest - 14, // 13: sites.v1.SiteService.AssignBuildingToSite:input_type -> sites.v1.AssignBuildingToSiteRequest - 16, // 14: sites.v1.SiteService.GetSiteStats:input_type -> sites.v1.GetSiteStatsRequest - 4, // 15: sites.v1.SiteService.ListSites:output_type -> sites.v1.ListSitesResponse - 6, // 16: sites.v1.SiteService.CreateSite:output_type -> sites.v1.CreateSiteResponse - 8, // 17: sites.v1.SiteService.UpdateSite:output_type -> sites.v1.UpdateSiteResponse - 10, // 18: sites.v1.SiteService.DeleteSite:output_type -> sites.v1.DeleteSiteResponse - 13, // 19: sites.v1.SiteService.ReassignDevicesToSite:output_type -> sites.v1.ReassignDevicesToSiteResponse - 15, // 20: sites.v1.SiteService.AssignBuildingToSite:output_type -> sites.v1.AssignBuildingToSiteResponse - 17, // 21: sites.v1.SiteService.GetSiteStats:output_type -> sites.v1.GetSiteStatsResponse - 15, // [15:22] is the sub-list for method output_type - 8, // [8:15] is the sub-list for method input_type + 11, // 12: sites.v1.SiteService.AssignDevicesToSite:input_type -> sites.v1.AssignDevicesToSiteRequest + 14, // 13: sites.v1.SiteService.AssignBuildingsToSite:input_type -> sites.v1.AssignBuildingsToSiteRequest + 16, // 14: sites.v1.SiteService.AssignRacksToSite:input_type -> sites.v1.AssignRacksToSiteRequest + 18, // 15: sites.v1.SiteService.GetSiteStats:input_type -> sites.v1.GetSiteStatsRequest + 4, // 16: sites.v1.SiteService.ListSites:output_type -> sites.v1.ListSitesResponse + 6, // 17: sites.v1.SiteService.CreateSite:output_type -> sites.v1.CreateSiteResponse + 8, // 18: sites.v1.SiteService.UpdateSite:output_type -> sites.v1.UpdateSiteResponse + 10, // 19: sites.v1.SiteService.DeleteSite:output_type -> sites.v1.DeleteSiteResponse + 13, // 20: sites.v1.SiteService.AssignDevicesToSite:output_type -> sites.v1.AssignDevicesToSiteResponse + 15, // 21: sites.v1.SiteService.AssignBuildingsToSite:output_type -> sites.v1.AssignBuildingsToSiteResponse + 17, // 22: sites.v1.SiteService.AssignRacksToSite:output_type -> sites.v1.AssignRacksToSiteResponse + 19, // 23: sites.v1.SiteService.GetSiteStats:output_type -> sites.v1.GetSiteStatsResponse + 16, // [16:24] is the sub-list for method output_type + 8, // [8:16] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -1690,13 +1857,14 @@ func file_sites_v1_sites_proto_init() { } file_sites_v1_sites_proto_msgTypes[10].OneofWrappers = []any{} file_sites_v1_sites_proto_msgTypes[13].OneofWrappers = []any{} + file_sites_v1_sites_proto_msgTypes[15].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sites_v1_sites_proto_rawDesc), len(file_sites_v1_sites_proto_rawDesc)), NumEnums: 1, - NumMessages: 17, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/server/generated/grpc/sites/v1/sitesv1connect/sites.connect.go b/server/generated/grpc/sites/v1/sitesv1connect/sites.connect.go index 223d0db21..7667f981c 100644 --- a/server/generated/grpc/sites/v1/sitesv1connect/sites.connect.go +++ b/server/generated/grpc/sites/v1/sitesv1connect/sites.connect.go @@ -42,12 +42,15 @@ const ( SiteServiceUpdateSiteProcedure = "/sites.v1.SiteService/UpdateSite" // SiteServiceDeleteSiteProcedure is the fully-qualified name of the SiteService's DeleteSite RPC. SiteServiceDeleteSiteProcedure = "/sites.v1.SiteService/DeleteSite" - // SiteServiceReassignDevicesToSiteProcedure is the fully-qualified name of the SiteService's - // ReassignDevicesToSite RPC. - SiteServiceReassignDevicesToSiteProcedure = "/sites.v1.SiteService/ReassignDevicesToSite" - // SiteServiceAssignBuildingToSiteProcedure is the fully-qualified name of the SiteService's - // AssignBuildingToSite RPC. - SiteServiceAssignBuildingToSiteProcedure = "/sites.v1.SiteService/AssignBuildingToSite" + // SiteServiceAssignDevicesToSiteProcedure is the fully-qualified name of the SiteService's + // AssignDevicesToSite RPC. + SiteServiceAssignDevicesToSiteProcedure = "/sites.v1.SiteService/AssignDevicesToSite" + // SiteServiceAssignBuildingsToSiteProcedure is the fully-qualified name of the SiteService's + // AssignBuildingsToSite RPC. + SiteServiceAssignBuildingsToSiteProcedure = "/sites.v1.SiteService/AssignBuildingsToSite" + // SiteServiceAssignRacksToSiteProcedure is the fully-qualified name of the SiteService's + // AssignRacksToSite RPC. + SiteServiceAssignRacksToSiteProcedure = "/sites.v1.SiteService/AssignRacksToSite" // SiteServiceGetSiteStatsProcedure is the fully-qualified name of the SiteService's GetSiteStats // RPC. SiteServiceGetSiteStatsProcedure = "/sites.v1.SiteService/GetSiteStats" @@ -72,17 +75,28 @@ type SiteServiceClient interface { // Cascade impact counts are returned so the UI can surface the // result in a toast/activity row. DeleteSite(context.Context, *connect.Request[v1.DeleteSiteRequest]) (*connect.Response[v1.DeleteSiteResponse], error) - // ReassignDevicesToSite is an all-or-nothing bulk update of + // AssignDevicesToSite is an all-or-nothing bulk update of // device.site_id. If any device is in a rack whose site_id differs // from the target, the entire batch rejects with per-device error // details and no row is touched. Omit target_site_id (or leave it // unset) to move devices to the "Unassigned" bucket. - ReassignDevicesToSite(context.Context, *connect.Request[v1.ReassignDevicesToSiteRequest]) (*connect.Response[v1.ReassignDevicesToSiteResponse], error) - // AssignBuildingToSite moves a building to a different site - // (or to "Unassigned" when target_site_id is unset). The same - // transaction cascades site_id down to the building's racks and - // their devices. Returns the cascade counts. - AssignBuildingToSite(context.Context, *connect.Request[v1.AssignBuildingToSiteRequest]) (*connect.Response[v1.AssignBuildingToSiteResponse], error) + AssignDevicesToSite(context.Context, *connect.Request[v1.AssignDevicesToSiteRequest]) (*connect.Response[v1.AssignDevicesToSiteResponse], error) + // AssignBuildingsToSite moves one or more buildings to a target site + // (or to "Unassigned" when target_site_id is unset). All updates run + // in a single transaction; if any building fails, the batch rolls + // back. The same transaction cascades site_id down to each + // building's racks and their devices. Returns the aggregate cascade + // counts across every building in the batch. + AssignBuildingsToSite(context.Context, *connect.Request[v1.AssignBuildingsToSiteRequest]) (*connect.Response[v1.AssignBuildingsToSiteResponse], error) + // AssignRacksToSite moves one or more racks to a target site (or + // to "Unassigned" when target_site_id is unset) as a partial + // update — the rack's label, layout, members, and slot + // assignments stay untouched, only site_id changes. building_id is + // auto-cleared on any site transition since a building belongs to + // a single site; the response carries the count of racks whose + // building was cleared so the UI can prompt for re-assignment. + // Same transaction cascades device.site_id for every rack member. + AssignRacksToSite(context.Context, *connect.Request[v1.AssignRacksToSiteRequest]) (*connect.Response[v1.AssignRacksToSiteResponse], error) // GetSiteStats returns server-rolled telemetry + miner-state counts // for every device assigned to the site, including devices whose // rack has no building set and devices that have no rack at all. @@ -120,14 +134,19 @@ func NewSiteServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. baseURL+SiteServiceDeleteSiteProcedure, opts..., ), - reassignDevicesToSite: connect.NewClient[v1.ReassignDevicesToSiteRequest, v1.ReassignDevicesToSiteResponse]( + assignDevicesToSite: connect.NewClient[v1.AssignDevicesToSiteRequest, v1.AssignDevicesToSiteResponse]( httpClient, - baseURL+SiteServiceReassignDevicesToSiteProcedure, + baseURL+SiteServiceAssignDevicesToSiteProcedure, opts..., ), - assignBuildingToSite: connect.NewClient[v1.AssignBuildingToSiteRequest, v1.AssignBuildingToSiteResponse]( + assignBuildingsToSite: connect.NewClient[v1.AssignBuildingsToSiteRequest, v1.AssignBuildingsToSiteResponse]( httpClient, - baseURL+SiteServiceAssignBuildingToSiteProcedure, + baseURL+SiteServiceAssignBuildingsToSiteProcedure, + opts..., + ), + assignRacksToSite: connect.NewClient[v1.AssignRacksToSiteRequest, v1.AssignRacksToSiteResponse]( + httpClient, + baseURL+SiteServiceAssignRacksToSiteProcedure, opts..., ), getSiteStats: connect.NewClient[v1.GetSiteStatsRequest, v1.GetSiteStatsResponse]( @@ -144,8 +163,9 @@ type siteServiceClient struct { createSite *connect.Client[v1.CreateSiteRequest, v1.CreateSiteResponse] updateSite *connect.Client[v1.UpdateSiteRequest, v1.UpdateSiteResponse] deleteSite *connect.Client[v1.DeleteSiteRequest, v1.DeleteSiteResponse] - reassignDevicesToSite *connect.Client[v1.ReassignDevicesToSiteRequest, v1.ReassignDevicesToSiteResponse] - assignBuildingToSite *connect.Client[v1.AssignBuildingToSiteRequest, v1.AssignBuildingToSiteResponse] + assignDevicesToSite *connect.Client[v1.AssignDevicesToSiteRequest, v1.AssignDevicesToSiteResponse] + assignBuildingsToSite *connect.Client[v1.AssignBuildingsToSiteRequest, v1.AssignBuildingsToSiteResponse] + assignRacksToSite *connect.Client[v1.AssignRacksToSiteRequest, v1.AssignRacksToSiteResponse] getSiteStats *connect.Client[v1.GetSiteStatsRequest, v1.GetSiteStatsResponse] } @@ -169,14 +189,19 @@ func (c *siteServiceClient) DeleteSite(ctx context.Context, req *connect.Request return c.deleteSite.CallUnary(ctx, req) } -// ReassignDevicesToSite calls sites.v1.SiteService.ReassignDevicesToSite. -func (c *siteServiceClient) ReassignDevicesToSite(ctx context.Context, req *connect.Request[v1.ReassignDevicesToSiteRequest]) (*connect.Response[v1.ReassignDevicesToSiteResponse], error) { - return c.reassignDevicesToSite.CallUnary(ctx, req) +// AssignDevicesToSite calls sites.v1.SiteService.AssignDevicesToSite. +func (c *siteServiceClient) AssignDevicesToSite(ctx context.Context, req *connect.Request[v1.AssignDevicesToSiteRequest]) (*connect.Response[v1.AssignDevicesToSiteResponse], error) { + return c.assignDevicesToSite.CallUnary(ctx, req) +} + +// AssignBuildingsToSite calls sites.v1.SiteService.AssignBuildingsToSite. +func (c *siteServiceClient) AssignBuildingsToSite(ctx context.Context, req *connect.Request[v1.AssignBuildingsToSiteRequest]) (*connect.Response[v1.AssignBuildingsToSiteResponse], error) { + return c.assignBuildingsToSite.CallUnary(ctx, req) } -// AssignBuildingToSite calls sites.v1.SiteService.AssignBuildingToSite. -func (c *siteServiceClient) AssignBuildingToSite(ctx context.Context, req *connect.Request[v1.AssignBuildingToSiteRequest]) (*connect.Response[v1.AssignBuildingToSiteResponse], error) { - return c.assignBuildingToSite.CallUnary(ctx, req) +// AssignRacksToSite calls sites.v1.SiteService.AssignRacksToSite. +func (c *siteServiceClient) AssignRacksToSite(ctx context.Context, req *connect.Request[v1.AssignRacksToSiteRequest]) (*connect.Response[v1.AssignRacksToSiteResponse], error) { + return c.assignRacksToSite.CallUnary(ctx, req) } // GetSiteStats calls sites.v1.SiteService.GetSiteStats. @@ -203,17 +228,28 @@ type SiteServiceHandler interface { // Cascade impact counts are returned so the UI can surface the // result in a toast/activity row. DeleteSite(context.Context, *connect.Request[v1.DeleteSiteRequest]) (*connect.Response[v1.DeleteSiteResponse], error) - // ReassignDevicesToSite is an all-or-nothing bulk update of + // AssignDevicesToSite is an all-or-nothing bulk update of // device.site_id. If any device is in a rack whose site_id differs // from the target, the entire batch rejects with per-device error // details and no row is touched. Omit target_site_id (or leave it // unset) to move devices to the "Unassigned" bucket. - ReassignDevicesToSite(context.Context, *connect.Request[v1.ReassignDevicesToSiteRequest]) (*connect.Response[v1.ReassignDevicesToSiteResponse], error) - // AssignBuildingToSite moves a building to a different site - // (or to "Unassigned" when target_site_id is unset). The same - // transaction cascades site_id down to the building's racks and - // their devices. Returns the cascade counts. - AssignBuildingToSite(context.Context, *connect.Request[v1.AssignBuildingToSiteRequest]) (*connect.Response[v1.AssignBuildingToSiteResponse], error) + AssignDevicesToSite(context.Context, *connect.Request[v1.AssignDevicesToSiteRequest]) (*connect.Response[v1.AssignDevicesToSiteResponse], error) + // AssignBuildingsToSite moves one or more buildings to a target site + // (or to "Unassigned" when target_site_id is unset). All updates run + // in a single transaction; if any building fails, the batch rolls + // back. The same transaction cascades site_id down to each + // building's racks and their devices. Returns the aggregate cascade + // counts across every building in the batch. + AssignBuildingsToSite(context.Context, *connect.Request[v1.AssignBuildingsToSiteRequest]) (*connect.Response[v1.AssignBuildingsToSiteResponse], error) + // AssignRacksToSite moves one or more racks to a target site (or + // to "Unassigned" when target_site_id is unset) as a partial + // update — the rack's label, layout, members, and slot + // assignments stay untouched, only site_id changes. building_id is + // auto-cleared on any site transition since a building belongs to + // a single site; the response carries the count of racks whose + // building was cleared so the UI can prompt for re-assignment. + // Same transaction cascades device.site_id for every rack member. + AssignRacksToSite(context.Context, *connect.Request[v1.AssignRacksToSiteRequest]) (*connect.Response[v1.AssignRacksToSiteResponse], error) // GetSiteStats returns server-rolled telemetry + miner-state counts // for every device assigned to the site, including devices whose // rack has no building set and devices that have no rack at all. @@ -247,14 +283,19 @@ func NewSiteServiceHandler(svc SiteServiceHandler, opts ...connect.HandlerOption svc.DeleteSite, opts..., ) - siteServiceReassignDevicesToSiteHandler := connect.NewUnaryHandler( - SiteServiceReassignDevicesToSiteProcedure, - svc.ReassignDevicesToSite, + siteServiceAssignDevicesToSiteHandler := connect.NewUnaryHandler( + SiteServiceAssignDevicesToSiteProcedure, + svc.AssignDevicesToSite, opts..., ) - siteServiceAssignBuildingToSiteHandler := connect.NewUnaryHandler( - SiteServiceAssignBuildingToSiteProcedure, - svc.AssignBuildingToSite, + siteServiceAssignBuildingsToSiteHandler := connect.NewUnaryHandler( + SiteServiceAssignBuildingsToSiteProcedure, + svc.AssignBuildingsToSite, + opts..., + ) + siteServiceAssignRacksToSiteHandler := connect.NewUnaryHandler( + SiteServiceAssignRacksToSiteProcedure, + svc.AssignRacksToSite, opts..., ) siteServiceGetSiteStatsHandler := connect.NewUnaryHandler( @@ -272,10 +313,12 @@ func NewSiteServiceHandler(svc SiteServiceHandler, opts ...connect.HandlerOption siteServiceUpdateSiteHandler.ServeHTTP(w, r) case SiteServiceDeleteSiteProcedure: siteServiceDeleteSiteHandler.ServeHTTP(w, r) - case SiteServiceReassignDevicesToSiteProcedure: - siteServiceReassignDevicesToSiteHandler.ServeHTTP(w, r) - case SiteServiceAssignBuildingToSiteProcedure: - siteServiceAssignBuildingToSiteHandler.ServeHTTP(w, r) + case SiteServiceAssignDevicesToSiteProcedure: + siteServiceAssignDevicesToSiteHandler.ServeHTTP(w, r) + case SiteServiceAssignBuildingsToSiteProcedure: + siteServiceAssignBuildingsToSiteHandler.ServeHTTP(w, r) + case SiteServiceAssignRacksToSiteProcedure: + siteServiceAssignRacksToSiteHandler.ServeHTTP(w, r) case SiteServiceGetSiteStatsProcedure: siteServiceGetSiteStatsHandler.ServeHTTP(w, r) default: @@ -303,12 +346,16 @@ func (UnimplementedSiteServiceHandler) DeleteSite(context.Context, *connect.Requ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.DeleteSite is not implemented")) } -func (UnimplementedSiteServiceHandler) ReassignDevicesToSite(context.Context, *connect.Request[v1.ReassignDevicesToSiteRequest]) (*connect.Response[v1.ReassignDevicesToSiteResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.ReassignDevicesToSite is not implemented")) +func (UnimplementedSiteServiceHandler) AssignDevicesToSite(context.Context, *connect.Request[v1.AssignDevicesToSiteRequest]) (*connect.Response[v1.AssignDevicesToSiteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.AssignDevicesToSite is not implemented")) +} + +func (UnimplementedSiteServiceHandler) AssignBuildingsToSite(context.Context, *connect.Request[v1.AssignBuildingsToSiteRequest]) (*connect.Response[v1.AssignBuildingsToSiteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.AssignBuildingsToSite is not implemented")) } -func (UnimplementedSiteServiceHandler) AssignBuildingToSite(context.Context, *connect.Request[v1.AssignBuildingToSiteRequest]) (*connect.Response[v1.AssignBuildingToSiteResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.AssignBuildingToSite is not implemented")) +func (UnimplementedSiteServiceHandler) AssignRacksToSite(context.Context, *connect.Request[v1.AssignRacksToSiteRequest]) (*connect.Response[v1.AssignRacksToSiteResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sites.v1.SiteService.AssignRacksToSite is not implemented")) } func (UnimplementedSiteServiceHandler) GetSiteStats(context.Context, *connect.Request[v1.GetSiteStatsRequest]) (*connect.Response[v1.GetSiteStatsResponse], error) { diff --git a/server/generated/sqlc/building.sql.go b/server/generated/sqlc/building.sql.go index ca0a75629..fd64081e1 100644 --- a/server/generated/sqlc/building.sql.go +++ b/server/generated/sqlc/building.sql.go @@ -40,6 +40,34 @@ func (q *Queries) AssignBuildingToSite(ctx context.Context, arg AssignBuildingTo return result.RowsAffected() } +const assignBuildingsToSiteBulk = `-- name: AssignBuildingsToSiteBulk :execrows +UPDATE building +SET site_id = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = ANY($2::bigint[]) + AND org_id = $3 + AND deleted_at IS NULL +` + +type AssignBuildingsToSiteBulkParams struct { + SiteID sql.NullInt64 + BuildingIds []int64 + OrgID int64 +} + +// Bulk variant of AssignBuildingToSite. Updates building.site_id for +// every building in @building_ids in one statement. Caller is +// expected to have row-locked each building first (canonical lock +// order: site → buildings). Returns the row count of buildings +// actually moved (skips soft-deleted rows). +func (q *Queries) AssignBuildingsToSiteBulk(ctx context.Context, arg AssignBuildingsToSiteBulkParams) (int64, error) { + result, err := q.exec(ctx, q.assignBuildingsToSiteBulkStmt, assignBuildingsToSiteBulk, arg.SiteID, pq.Array(arg.BuildingIds), arg.OrgID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const buildingBelongsToOrg = `-- name: BuildingBelongsToOrg :one SELECT EXISTS( SELECT 1 FROM building @@ -502,6 +530,74 @@ func (q *Queries) SetRackBuildingPosition(ctx context.Context, arg SetRackBuildi return err } +const setRackBuildingPositionBulkClear = `-- name: SetRackBuildingPositionBulkClear :exec +UPDATE device_set_rack +SET aisle_index = NULL, + position_in_aisle = NULL +WHERE device_set_id = ANY($1::bigint[]) + AND org_id = $2 +` + +type SetRackBuildingPositionBulkClearParams struct { + RackIds []int64 + OrgID int64 +} + +// Bulk variant of SetRackBuildingPosition that nulls (aisle_index, +// position_in_aisle) for every rack in @rack_ids. Used by +// AssignRacksToBuilding's pass-1 vacate so every rack in the batch +// holds NULL position before pass-2 reclaims cells. Mirrors the +// single-row query's intentional no-touch on building_id — +// UpdateRackPlacement already settled that. +func (q *Queries) SetRackBuildingPositionBulkClear(ctx context.Context, arg SetRackBuildingPositionBulkClearParams) error { + _, err := q.exec(ctx, q.setRackBuildingPositionBulkClearStmt, setRackBuildingPositionBulkClear, pq.Array(arg.RackIds), arg.OrgID) + return err +} + +const setRackBuildingPositionBulkPlace = `-- name: SetRackBuildingPositionBulkPlace :exec +UPDATE device_set_rack dsr +SET aisle_index = u.aisle_index, + position_in_aisle = u.position_in_aisle +FROM ( + SELECT + rack_ids[i] AS rack_id, + aisle_indexes[i] AS aisle_index, + position_in_aisles[i] AS position_in_aisle + FROM ( + SELECT + $2::bigint[] AS rack_ids, + $3::int[] AS aisle_indexes, + $4::int[] AS position_in_aisles + ) arrs + CROSS JOIN generate_subscripts(arrs.rack_ids, 1) AS i +) AS u +WHERE dsr.device_set_id = u.rack_id + AND dsr.org_id = $1 +` + +type SetRackBuildingPositionBulkPlaceParams struct { + OrgID int64 + RackIds []int64 + AisleIndexes []int32 + PositionInAisles []int32 +} + +// Bulk variant of SetRackBuildingPosition that writes per-rack +// (aisle_index, position_in_aisle) via parallel arrays joined on +// ordinal position. Used by AssignRacksToBuilding's pass-2 after +// pass-1 has vacated every cell touched by the batch. Arrays must be +// the same length and parallel-aligned with @rack_ids; callers that +// have a "place nothing" pass-2 should skip the query entirely. +func (q *Queries) SetRackBuildingPositionBulkPlace(ctx context.Context, arg SetRackBuildingPositionBulkPlaceParams) error { + _, err := q.exec(ctx, q.setRackBuildingPositionBulkPlaceStmt, setRackBuildingPositionBulkPlace, + arg.OrgID, + pq.Array(arg.RackIds), + pq.Array(arg.AisleIndexes), + pq.Array(arg.PositionInAisles), + ) + return err +} + const softDeleteBuilding = `-- name: SoftDeleteBuilding :execrows UPDATE building SET deleted_at = CURRENT_TIMESTAMP diff --git a/server/generated/sqlc/db.go b/server/generated/sqlc/db.go index 44f07ac72..5139f15c4 100644 --- a/server/generated/sqlc/db.go +++ b/server/generated/sqlc/db.go @@ -42,6 +42,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.assignBuildingToSiteStmt, err = db.PrepareContext(ctx, assignBuildingToSite); err != nil { return nil, fmt.Errorf("error preparing query AssignBuildingToSite: %w", err) } + if q.assignBuildingsToSiteBulkStmt, err = db.PrepareContext(ctx, assignBuildingsToSiteBulk); err != nil { + return nil, fmt.Errorf("error preparing query AssignBuildingsToSiteBulk: %w", err) + } + if q.assignDevicesToSiteStmt, err = db.PrepareContext(ctx, assignDevicesToSite); err != nil { + return nil, fmt.Errorf("error preparing query AssignDevicesToSite: %w", err) + } if q.assignPermissionToRoleStmt, err = db.PrepareContext(ctx, assignPermissionToRole); err != nil { return nil, fmt.Errorf("error preparing query AssignPermissionToRole: %w", err) } @@ -78,6 +84,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.cascadeRackDeviceSitesStmt, err = db.PrepareContext(ctx, cascadeRackDeviceSites); err != nil { return nil, fmt.Errorf("error preparing query CascadeRackDeviceSites: %w", err) } + if q.cascadeRackDeviceSitesBulkStmt, err = db.PrepareContext(ctx, cascadeRackDeviceSitesBulk); err != nil { + return nil, fmt.Errorf("error preparing query CascadeRackDeviceSitesBulk: %w", err) + } if q.claimMessageForProcessingStmt, err = db.PrepareContext(ctx, claimMessageForProcessing); err != nil { return nil, fmt.Errorf("error preparing query ClaimMessageForProcessing: %w", err) } @@ -810,6 +819,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.lockRackPlacementForWriteStmt, err = db.PrepareContext(ctx, lockRackPlacementForWrite); err != nil { return nil, fmt.Errorf("error preparing query LockRackPlacementForWrite: %w", err) } + if q.lockRacksForReparentStmt, err = db.PrepareContext(ctx, lockRacksForReparent); err != nil { + return nil, fmt.Errorf("error preparing query LockRacksForReparent: %w", err) + } if q.lockSchedulePriorityStmt, err = db.PrepareContext(ctx, lockSchedulePriority); err != nil { return nil, fmt.Errorf("error preparing query LockSchedulePriority: %w", err) } @@ -855,18 +867,24 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.reapStuckProcessingMessagesStmt, err = db.PrepareContext(ctx, reapStuckProcessingMessages); err != nil { return nil, fmt.Errorf("error preparing query ReapStuckProcessingMessages: %w", err) } - if q.reassignDevicesToSiteStmt, err = db.PrepareContext(ctx, reassignDevicesToSite); err != nil { - return nil, fmt.Errorf("error preparing query ReassignDevicesToSite: %w", err) - } if q.reassignDevicesUnderBuildingStmt, err = db.PrepareContext(ctx, reassignDevicesUnderBuilding); err != nil { return nil, fmt.Errorf("error preparing query ReassignDevicesUnderBuilding: %w", err) } + if q.reassignDevicesUnderBuildingsBulkStmt, err = db.PrepareContext(ctx, reassignDevicesUnderBuildingsBulk); err != nil { + return nil, fmt.Errorf("error preparing query ReassignDevicesUnderBuildingsBulk: %w", err) + } if q.reassignRacksUnderBuildingStmt, err = db.PrepareContext(ctx, reassignRacksUnderBuilding); err != nil { return nil, fmt.Errorf("error preparing query ReassignRacksUnderBuilding: %w", err) } + if q.reassignRacksUnderBuildingsBulkStmt, err = db.PrepareContext(ctx, reassignRacksUnderBuildingsBulk); err != nil { + return nil, fmt.Errorf("error preparing query ReassignRacksUnderBuildingsBulk: %w", err) + } if q.removeAllDevicesFromDeviceSetStmt, err = db.PrepareContext(ctx, removeAllDevicesFromDeviceSet); err != nil { return nil, fmt.Errorf("error preparing query RemoveAllDevicesFromDeviceSet: %w", err) } + if q.removeDevicesFromAnyRackStmt, err = db.PrepareContext(ctx, removeDevicesFromAnyRack); err != nil { + return nil, fmt.Errorf("error preparing query RemoveDevicesFromAnyRack: %w", err) + } if q.removeDevicesFromDeviceSetStmt, err = db.PrepareContext(ctx, removeDevicesFromDeviceSet); err != nil { return nil, fmt.Errorf("error preparing query RemoveDevicesFromDeviceSet: %w", err) } @@ -924,6 +942,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.setRackBuildingPositionStmt, err = db.PrepareContext(ctx, setRackBuildingPosition); err != nil { return nil, fmt.Errorf("error preparing query SetRackBuildingPosition: %w", err) } + if q.setRackBuildingPositionBulkClearStmt, err = db.PrepareContext(ctx, setRackBuildingPositionBulkClear); err != nil { + return nil, fmt.Errorf("error preparing query SetRackBuildingPositionBulkClear: %w", err) + } + if q.setRackBuildingPositionBulkPlaceStmt, err = db.PrepareContext(ctx, setRackBuildingPositionBulkPlace); err != nil { + return nil, fmt.Errorf("error preparing query SetRackBuildingPositionBulkPlace: %w", err) + } if q.setRackSlotPositionStmt, err = db.PrepareContext(ctx, setRackSlotPosition); err != nil { return nil, fmt.Errorf("error preparing query SetRackSlotPosition: %w", err) } @@ -1113,6 +1137,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.updateRackPlacementStmt, err = db.PrepareContext(ctx, updateRackPlacement); err != nil { return nil, fmt.Errorf("error preparing query UpdateRackPlacement: %w", err) } + if q.updateRackPlacementBulkForBuildingStmt, err = db.PrepareContext(ctx, updateRackPlacementBulkForBuilding); err != nil { + return nil, fmt.Errorf("error preparing query UpdateRackPlacementBulkForBuilding: %w", err) + } + if q.updateRackPlacementBulkForSiteStmt, err = db.PrepareContext(ctx, updateRackPlacementBulkForSite); err != nil { + return nil, fmt.Errorf("error preparing query UpdateRackPlacementBulkForSite: %w", err) + } if q.updateRoleStmt, err = db.PrepareContext(ctx, updateRole); err != nil { return nil, fmt.Errorf("error preparing query UpdateRole: %w", err) } @@ -1217,6 +1247,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing assignBuildingToSiteStmt: %w", cerr) } } + if q.assignBuildingsToSiteBulkStmt != nil { + if cerr := q.assignBuildingsToSiteBulkStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing assignBuildingsToSiteBulkStmt: %w", cerr) + } + } + if q.assignDevicesToSiteStmt != nil { + if cerr := q.assignDevicesToSiteStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing assignDevicesToSiteStmt: %w", cerr) + } + } if q.assignPermissionToRoleStmt != nil { if cerr := q.assignPermissionToRoleStmt.Close(); cerr != nil { err = fmt.Errorf("error closing assignPermissionToRoleStmt: %w", cerr) @@ -1277,6 +1317,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing cascadeRackDeviceSitesStmt: %w", cerr) } } + if q.cascadeRackDeviceSitesBulkStmt != nil { + if cerr := q.cascadeRackDeviceSitesBulkStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing cascadeRackDeviceSitesBulkStmt: %w", cerr) + } + } if q.claimMessageForProcessingStmt != nil { if cerr := q.claimMessageForProcessingStmt.Close(); cerr != nil { err = fmt.Errorf("error closing claimMessageForProcessingStmt: %w", cerr) @@ -2497,6 +2542,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing lockRackPlacementForWriteStmt: %w", cerr) } } + if q.lockRacksForReparentStmt != nil { + if cerr := q.lockRacksForReparentStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing lockRacksForReparentStmt: %w", cerr) + } + } if q.lockSchedulePriorityStmt != nil { if cerr := q.lockSchedulePriorityStmt.Close(); cerr != nil { err = fmt.Errorf("error closing lockSchedulePriorityStmt: %w", cerr) @@ -2572,26 +2622,36 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing reapStuckProcessingMessagesStmt: %w", cerr) } } - if q.reassignDevicesToSiteStmt != nil { - if cerr := q.reassignDevicesToSiteStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing reassignDevicesToSiteStmt: %w", cerr) - } - } if q.reassignDevicesUnderBuildingStmt != nil { if cerr := q.reassignDevicesUnderBuildingStmt.Close(); cerr != nil { err = fmt.Errorf("error closing reassignDevicesUnderBuildingStmt: %w", cerr) } } + if q.reassignDevicesUnderBuildingsBulkStmt != nil { + if cerr := q.reassignDevicesUnderBuildingsBulkStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing reassignDevicesUnderBuildingsBulkStmt: %w", cerr) + } + } if q.reassignRacksUnderBuildingStmt != nil { if cerr := q.reassignRacksUnderBuildingStmt.Close(); cerr != nil { err = fmt.Errorf("error closing reassignRacksUnderBuildingStmt: %w", cerr) } } + if q.reassignRacksUnderBuildingsBulkStmt != nil { + if cerr := q.reassignRacksUnderBuildingsBulkStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing reassignRacksUnderBuildingsBulkStmt: %w", cerr) + } + } if q.removeAllDevicesFromDeviceSetStmt != nil { if cerr := q.removeAllDevicesFromDeviceSetStmt.Close(); cerr != nil { err = fmt.Errorf("error closing removeAllDevicesFromDeviceSetStmt: %w", cerr) } } + if q.removeDevicesFromAnyRackStmt != nil { + if cerr := q.removeDevicesFromAnyRackStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing removeDevicesFromAnyRackStmt: %w", cerr) + } + } if q.removeDevicesFromDeviceSetStmt != nil { if cerr := q.removeDevicesFromDeviceSetStmt.Close(); cerr != nil { err = fmt.Errorf("error closing removeDevicesFromDeviceSetStmt: %w", cerr) @@ -2687,6 +2747,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing setRackBuildingPositionStmt: %w", cerr) } } + if q.setRackBuildingPositionBulkClearStmt != nil { + if cerr := q.setRackBuildingPositionBulkClearStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing setRackBuildingPositionBulkClearStmt: %w", cerr) + } + } + if q.setRackBuildingPositionBulkPlaceStmt != nil { + if cerr := q.setRackBuildingPositionBulkPlaceStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing setRackBuildingPositionBulkPlaceStmt: %w", cerr) + } + } if q.setRackSlotPositionStmt != nil { if cerr := q.setRackSlotPositionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing setRackSlotPositionStmt: %w", cerr) @@ -3002,6 +3072,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing updateRackPlacementStmt: %w", cerr) } } + if q.updateRackPlacementBulkForBuildingStmt != nil { + if cerr := q.updateRackPlacementBulkForBuildingStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateRackPlacementBulkForBuildingStmt: %w", cerr) + } + } + if q.updateRackPlacementBulkForSiteStmt != nil { + if cerr := q.updateRackPlacementBulkForSiteStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateRackPlacementBulkForSiteStmt: %w", cerr) + } + } if q.updateRoleStmt != nil { if cerr := q.updateRoleStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateRoleStmt: %w", cerr) @@ -3162,6 +3242,8 @@ type Queries struct { adminTerminateCurtailmentEventStmt *sql.Stmt allDevicesBelongToOrgStmt *sql.Stmt assignBuildingToSiteStmt *sql.Stmt + assignBuildingsToSiteBulkStmt *sql.Stmt + assignDevicesToSiteStmt *sql.Stmt assignPermissionToRoleStmt *sql.Stmt assignRoleStmt *sql.Stmt beginCurtailmentRestorationStmt *sql.Stmt @@ -3174,6 +3256,7 @@ type Queries struct { cancelPendingEnrollmentStmt *sql.Stmt cascadeAddedDeviceSitesStmt *sql.Stmt cascadeRackDeviceSitesStmt *sql.Stmt + cascadeRackDeviceSitesBulkStmt *sql.Stmt claimMessageForProcessingStmt *sql.Stmt clearCurtailmentAutomationActiveEventStmt *sql.Stmt clearRackPlacementForSoftDeleteStmt *sql.Stmt @@ -3418,6 +3501,7 @@ type Queries struct { lockDevicesForReassignStmt *sql.Stmt lockFleetNodeByIDStmt *sql.Stmt lockRackPlacementForWriteStmt *sql.Stmt + lockRacksForReparentStmt *sql.Stmt lockSchedulePriorityStmt *sql.Stmt lockSiteForWriteStmt *sql.Stmt markCommandBatchFinishedStmt *sql.Stmt @@ -3433,10 +3517,12 @@ type Queries struct { queryErrorsStmt *sql.Stmt reapStuckFirmwareUpdateMessagesStmt *sql.Stmt reapStuckProcessingMessagesStmt *sql.Stmt - reassignDevicesToSiteStmt *sql.Stmt reassignDevicesUnderBuildingStmt *sql.Stmt + reassignDevicesUnderBuildingsBulkStmt *sql.Stmt reassignRacksUnderBuildingStmt *sql.Stmt + reassignRacksUnderBuildingsBulkStmt *sql.Stmt removeAllDevicesFromDeviceSetStmt *sql.Stmt + removeDevicesFromAnyRackStmt *sql.Stmt removeDevicesFromDeviceSetStmt *sql.Stmt resetCurtailmentTargetsForRecurtailStmt *sql.Stmt resetCurtailmentTargetsForRestoreStmt *sql.Stmt @@ -3456,6 +3542,8 @@ type Queries struct { setFleetNodeEnrollmentStatusStmt *sql.Stmt setMQTTSourceConfigEnabledStmt *sql.Stmt setRackBuildingPositionStmt *sql.Stmt + setRackBuildingPositionBulkClearStmt *sql.Stmt + setRackBuildingPositionBulkPlaceStmt *sql.Stmt setRackSlotPositionStmt *sql.Stmt setSchedulePrioritiesStmt *sql.Stmt setScheduleRunningStmt *sql.Stmt @@ -3519,6 +3607,8 @@ type Queries struct { updatePoolStmt *sql.Stmt updateRackInfoStmt *sql.Stmt updateRackPlacementStmt *sql.Stmt + updateRackPlacementBulkForBuildingStmt *sql.Stmt + updateRackPlacementBulkForSiteStmt *sql.Stmt updateRoleStmt *sql.Stmt updateScheduleStmt *sql.Stmt updateScheduleAfterRunStmt *sql.Stmt @@ -3554,6 +3644,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { adminTerminateCurtailmentEventStmt: q.adminTerminateCurtailmentEventStmt, allDevicesBelongToOrgStmt: q.allDevicesBelongToOrgStmt, assignBuildingToSiteStmt: q.assignBuildingToSiteStmt, + assignBuildingsToSiteBulkStmt: q.assignBuildingsToSiteBulkStmt, + assignDevicesToSiteStmt: q.assignDevicesToSiteStmt, assignPermissionToRoleStmt: q.assignPermissionToRoleStmt, assignRoleStmt: q.assignRoleStmt, beginCurtailmentRestorationStmt: q.beginCurtailmentRestorationStmt, @@ -3566,6 +3658,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { cancelPendingEnrollmentStmt: q.cancelPendingEnrollmentStmt, cascadeAddedDeviceSitesStmt: q.cascadeAddedDeviceSitesStmt, cascadeRackDeviceSitesStmt: q.cascadeRackDeviceSitesStmt, + cascadeRackDeviceSitesBulkStmt: q.cascadeRackDeviceSitesBulkStmt, claimMessageForProcessingStmt: q.claimMessageForProcessingStmt, clearCurtailmentAutomationActiveEventStmt: q.clearCurtailmentAutomationActiveEventStmt, clearRackPlacementForSoftDeleteStmt: q.clearRackPlacementForSoftDeleteStmt, @@ -3810,6 +3903,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { lockDevicesForReassignStmt: q.lockDevicesForReassignStmt, lockFleetNodeByIDStmt: q.lockFleetNodeByIDStmt, lockRackPlacementForWriteStmt: q.lockRackPlacementForWriteStmt, + lockRacksForReparentStmt: q.lockRacksForReparentStmt, lockSchedulePriorityStmt: q.lockSchedulePriorityStmt, lockSiteForWriteStmt: q.lockSiteForWriteStmt, markCommandBatchFinishedStmt: q.markCommandBatchFinishedStmt, @@ -3825,10 +3919,12 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { queryErrorsStmt: q.queryErrorsStmt, reapStuckFirmwareUpdateMessagesStmt: q.reapStuckFirmwareUpdateMessagesStmt, reapStuckProcessingMessagesStmt: q.reapStuckProcessingMessagesStmt, - reassignDevicesToSiteStmt: q.reassignDevicesToSiteStmt, reassignDevicesUnderBuildingStmt: q.reassignDevicesUnderBuildingStmt, + reassignDevicesUnderBuildingsBulkStmt: q.reassignDevicesUnderBuildingsBulkStmt, reassignRacksUnderBuildingStmt: q.reassignRacksUnderBuildingStmt, + reassignRacksUnderBuildingsBulkStmt: q.reassignRacksUnderBuildingsBulkStmt, removeAllDevicesFromDeviceSetStmt: q.removeAllDevicesFromDeviceSetStmt, + removeDevicesFromAnyRackStmt: q.removeDevicesFromAnyRackStmt, removeDevicesFromDeviceSetStmt: q.removeDevicesFromDeviceSetStmt, resetCurtailmentTargetsForRecurtailStmt: q.resetCurtailmentTargetsForRecurtailStmt, resetCurtailmentTargetsForRestoreStmt: q.resetCurtailmentTargetsForRestoreStmt, @@ -3848,6 +3944,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { setFleetNodeEnrollmentStatusStmt: q.setFleetNodeEnrollmentStatusStmt, setMQTTSourceConfigEnabledStmt: q.setMQTTSourceConfigEnabledStmt, setRackBuildingPositionStmt: q.setRackBuildingPositionStmt, + setRackBuildingPositionBulkClearStmt: q.setRackBuildingPositionBulkClearStmt, + setRackBuildingPositionBulkPlaceStmt: q.setRackBuildingPositionBulkPlaceStmt, setRackSlotPositionStmt: q.setRackSlotPositionStmt, setSchedulePrioritiesStmt: q.setSchedulePrioritiesStmt, setScheduleRunningStmt: q.setScheduleRunningStmt, @@ -3911,6 +4009,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { updatePoolStmt: q.updatePoolStmt, updateRackInfoStmt: q.updateRackInfoStmt, updateRackPlacementStmt: q.updateRackPlacementStmt, + updateRackPlacementBulkForBuildingStmt: q.updateRackPlacementBulkForBuildingStmt, + updateRackPlacementBulkForSiteStmt: q.updateRackPlacementBulkForSiteStmt, updateRoleStmt: q.updateRoleStmt, updateScheduleStmt: q.updateScheduleStmt, updateScheduleAfterRunStmt: q.updateScheduleAfterRunStmt, diff --git a/server/generated/sqlc/device_set.sql.go b/server/generated/sqlc/device_set.sql.go index 1e892f773..972616fc0 100644 --- a/server/generated/sqlc/device_set.sql.go +++ b/server/generated/sqlc/device_set.sql.go @@ -102,6 +102,37 @@ func (q *Queries) CascadeRackDeviceSites(ctx context.Context, arg CascadeRackDev return result.RowsAffected() } +const cascadeRackDeviceSitesBulk = `-- name: CascadeRackDeviceSitesBulk :execrows +UPDATE device d +SET site_id = $1::bigint, + updated_at = CURRENT_TIMESTAMP +FROM device_set_membership dsm +WHERE dsm.device_set_id = ANY($2::bigint[]) + AND dsm.org_id = $3 + AND dsm.device_set_type = 'rack' + AND dsm.device_id = d.id + AND d.deleted_at IS NULL + AND d.site_id IS DISTINCT FROM $1::bigint +` + +type CascadeRackDeviceSitesBulkParams struct { + TargetSiteID sql.NullInt64 + RackIds []int64 + OrgID int64 +} + +// Bulk variant of CascadeRackDeviceSites. Rewrites device.site_id to +// target_site_id for every paired member of every rack in @rack_ids. +// NULL target unassigns. IS DISTINCT FROM skips no-op rows. Returns +// the total affected row count across all racks. +func (q *Queries) CascadeRackDeviceSitesBulk(ctx context.Context, arg CascadeRackDeviceSitesBulkParams) (int64, error) { + result, err := q.exec(ctx, q.cascadeRackDeviceSitesBulkStmt, cascadeRackDeviceSitesBulk, arg.TargetSiteID, pq.Array(arg.RackIds), arg.OrgID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const clearRackPlacementForSoftDelete = `-- name: ClearRackPlacementForSoftDelete :exec UPDATE device_set_rack SET aisle_index = NULL, @@ -1145,6 +1176,69 @@ func (q *Queries) LockRackPlacementForWrite(ctx context.Context, arg LockRackPla return i, err } +const lockRacksForReparent = `-- name: LockRacksForReparent :many +SELECT ds.id AS device_set_id +FROM device_set ds +WHERE ds.id IN ( + SELECT dsm.device_set_id + FROM device_set_membership dsm + WHERE dsm.org_id = $1 + AND dsm.device_set_type = 'rack' + AND dsm.device_identifier = ANY($2::text[]) + UNION + SELECT $3::bigint + WHERE $3::bigint > 0 + ) + AND ds.org_id = $1 + AND ds.type = 'rack' + AND ds.deleted_at IS NULL +ORDER BY ds.id ASC +FOR UPDATE +` + +type LockRacksForReparentParams struct { + OrgID int64 + DeviceIdentifiers []string + TargetRackID int64 +} + +// Locks every rack involved in a reparent (sources + target) FOR UPDATE +// in ascending id order. Used by AssignDevicesToRack as the FIRST tx +// operation. Sorting source and target together in a single lock +// acquisition is what prevents the deadlock two concurrent +// AssignDevicesToRack calls moving devices in opposite directions +// between the same pair of racks would otherwise hit: each tx locks +// {sourceA, sourceB, ..., target} in id order, so any two txs touching +// the same rack pair always agree on the global lock order regardless +// of which side is source vs target. Pass 0 for @target_rack_id on the +// unassign path (clear-rack -- no target to include). The membership +// DELETE in RemoveDevicesFromAnyRack still excludes the target via its +// own predicate; this query is purely about lock order. Distinct ids +// are derived in a subquery so the outer locking SELECT can use +// FOR UPDATE (Postgres rejects DISTINCT + FOR UPDATE at runtime). +func (q *Queries) LockRacksForReparent(ctx context.Context, arg LockRacksForReparentParams) ([]int64, error) { + rows, err := q.query(ctx, q.lockRacksForReparentStmt, lockRacksForReparent, arg.OrgID, pq.Array(arg.DeviceIdentifiers), arg.TargetRackID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var device_set_id int64 + if err := rows.Scan(&device_set_id); err != nil { + return nil, err + } + items = append(items, device_set_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const removeAllDevicesFromDeviceSet = `-- name: RemoveAllDevicesFromDeviceSet :execrows DELETE FROM device_set_membership WHERE device_set_id = $1 @@ -1164,6 +1258,37 @@ func (q *Queries) RemoveAllDevicesFromDeviceSet(ctx context.Context, arg RemoveA return result.RowsAffected() } +const removeDevicesFromAnyRack = `-- name: RemoveDevicesFromAnyRack :execrows +DELETE FROM device_set_membership +WHERE org_id = $1 + AND device_identifier = ANY($2::text[]) + AND device_set_type = 'rack' + AND device_set_id != $3::bigint +` + +type RemoveDevicesFromAnyRackParams struct { + OrgID int64 + DeviceIdentifiers []string + TargetRackID int64 +} + +// Removes the given devices from whatever rack they're currently in, +// EXCEPT the target rack (@target_rack_id). AssignDevicesToRack uses +// this to clear prior rack membership inside the same transaction as +// the new-rack insert, closing the orphan window the client-side +// remove + add orchestration had. Skipping the target rack preserves +// the existing membership row (and its rack_slot child) when a device +// is reassigned to the same rack it's already in -- otherwise the +// DELETE would cascade rack_slot rows that we'd silently lose. Pass +// 0 for an unconditional clear (caller intends to unassign). +func (q *Queries) RemoveDevicesFromAnyRack(ctx context.Context, arg RemoveDevicesFromAnyRackParams) (int64, error) { + result, err := q.exec(ctx, q.removeDevicesFromAnyRackStmt, removeDevicesFromAnyRack, arg.OrgID, pq.Array(arg.DeviceIdentifiers), arg.TargetRackID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const removeDevicesFromDeviceSet = `-- name: RemoveDevicesFromDeviceSet :execrows DELETE FROM device_set_membership WHERE device_set_id = $1 @@ -1402,3 +1527,101 @@ func (q *Queries) UpdateRackPlacement(ctx context.Context, arg UpdateRackPlaceme ) return err } + +const updateRackPlacementBulkForBuilding = `-- name: UpdateRackPlacementBulkForBuilding :exec +UPDATE device_set_rack dsr +SET site_id = CASE + WHEN $1::bigint IS NULL THEN dsr.site_id + ELSE $2::bigint + END, + building_id = $1::bigint, + zone = CASE + WHEN dsr.building_id IS NOT NULL + AND dsr.building_id IS DISTINCT FROM $1::bigint + THEN '' + ELSE dsr.zone + END, + aisle_index = CASE + WHEN $1::bigint IS DISTINCT FROM dsr.building_id THEN NULL + ELSE dsr.aisle_index + END, + position_in_aisle = CASE + WHEN $1::bigint IS DISTINCT FROM dsr.building_id THEN NULL + ELSE dsr.position_in_aisle + END +WHERE dsr.device_set_id = ANY($3::bigint[]) + AND dsr.org_id = $4 + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.org_id = $4 + AND ds.deleted_at IS NULL + ) +` + +type UpdateRackPlacementBulkForBuildingParams struct { + TargetBuildingID sql.NullInt64 + TargetSiteID sql.NullInt64 + RackIds []int64 + OrgID int64 +} + +// Bulk variant of UpdateRackPlacement scoped to AssignRacksToBuilding. +// Sets site_id, building_id, and zone for every rack in @rack_ids in a +// single update. +// +// Semantics match the per-row UpdateRackPlacement + service-layer +// zone/site rules: +// +// - When @target_building_id IS NULL (unassign branch), each rack +// keeps its current site_id (no cascade fires later). Otherwise +// every rack is stamped with @target_site_id. +// - Zone clears to ” for any rack that had a building and is +// transitioning to a different (or NULL) building. Racks staying in +// the same building preserve their zone. +// - aisle_index / position_in_aisle clear when building_id changes, +// matching the single-row CASE. +func (q *Queries) UpdateRackPlacementBulkForBuilding(ctx context.Context, arg UpdateRackPlacementBulkForBuildingParams) error { + _, err := q.exec(ctx, q.updateRackPlacementBulkForBuildingStmt, updateRackPlacementBulkForBuilding, + arg.TargetBuildingID, + arg.TargetSiteID, + pq.Array(arg.RackIds), + arg.OrgID, + ) + return err +} + +const updateRackPlacementBulkForSite = `-- name: UpdateRackPlacementBulkForSite :exec +UPDATE device_set_rack dsr +SET site_id = $1::bigint, + building_id = NULL, + zone = '', + aisle_index = NULL, + position_in_aisle = NULL +WHERE dsr.device_set_id = ANY($2::bigint[]) + AND dsr.org_id = $3 + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.org_id = $3 + AND ds.deleted_at IS NULL + ) +` + +type UpdateRackPlacementBulkForSiteParams struct { + TargetSiteID sql.NullInt64 + RackIds []int64 + OrgID int64 +} + +// Bulk variant used by AssignRacksToSite. Stamps every rack in +// @rack_ids with the target site, clears building_id and zone (because +// a building belongs to one site, the rack's building membership is +// invalidated by any site transition), and clears the grid placement +// as a downstream effect of building_id changing. Caller is expected +// to only pass racks whose current site differs from the target so the +// response counts stay accurate. +func (q *Queries) UpdateRackPlacementBulkForSite(ctx context.Context, arg UpdateRackPlacementBulkForSiteParams) error { + _, err := q.exec(ctx, q.updateRackPlacementBulkForSiteStmt, updateRackPlacementBulkForSite, arg.TargetSiteID, pq.Array(arg.RackIds), arg.OrgID) + return err +} diff --git a/server/generated/sqlc/site.sql.go b/server/generated/sqlc/site.sql.go index e2bf52d21..40a6417ac 100644 --- a/server/generated/sqlc/site.sql.go +++ b/server/generated/sqlc/site.sql.go @@ -13,6 +13,33 @@ import ( "github.com/lib/pq" ) +const assignDevicesToSite = `-- name: AssignDevicesToSite :execrows +UPDATE device +SET site_id = $1, + updated_at = CURRENT_TIMESTAMP +WHERE org_id = $2 + AND device_identifier = ANY($3::text[]) + AND deleted_at IS NULL +` + +type AssignDevicesToSiteParams struct { + TargetSiteID sql.NullInt64 + OrgID int64 + DeviceIdentifiers []string +} + +// Bulk update of device.site_id for the given identifiers within the +// org. Caller is expected to have already validated that no device is +// in a rack at a different site (see FindDeviceSiteConflicts). +// target_site_id NULL = move to Unassigned. +func (q *Queries) AssignDevicesToSite(ctx context.Context, arg AssignDevicesToSiteParams) (int64, error) { + result, err := q.exec(ctx, q.assignDevicesToSiteStmt, assignDevicesToSite, arg.TargetSiteID, arg.OrgID, pq.Array(arg.DeviceIdentifiers)) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const createSite = `-- name: CreateSite :one INSERT INTO site ( org_id, @@ -226,7 +253,7 @@ type ListExistingDeviceIdentifiersParams struct { // Filters the requested identifier list down to those that actually // exist as live devices in the org. Used to surface "device_not_found" -// conflicts in ReassignDevicesToSite without an N+1 lookup. +// conflicts in AssignDevicesToSite without an N+1 lookup. func (q *Queries) ListExistingDeviceIdentifiers(ctx context.Context, arg ListExistingDeviceIdentifiersParams) ([]string, error) { rows, err := q.query(ctx, q.listExistingDeviceIdentifiersStmt, listExistingDeviceIdentifiers, arg.OrgID, pq.Array(arg.DeviceIdentifiers)) if err != nil { @@ -412,7 +439,7 @@ type LockBuildingForWriteParams struct { } // Row-locks a specific building so concurrent mutations (DeleteSite, -// AssignBuildingToSite, DeleteBuilding) serialize. Returns the building +// AssignBuildingsToSite, DeleteBuilding) serialize. Returns the building // id when alive; sql.ErrNoRows when soft-deleted or missing. func (q *Queries) LockBuildingForWrite(ctx context.Context, arg LockBuildingForWriteParams) (int64, error) { row := q.queryRow(ctx, q.lockBuildingForWriteStmt, lockBuildingForWrite, arg.ID, arg.OrgID) @@ -436,7 +463,7 @@ type LockBuildingsBySiteForWriteParams struct { // Row-locks every live building under the given site so DeleteSite's // cascade can rewrite their racks without a concurrent -// AssignBuildingToSite slipping a building out from under it. Returns +// AssignBuildingsToSite slipping a building out from under it. Returns // the locked ids (result is informational; the FOR UPDATE side-effect // is what matters). func (q *Queries) LockBuildingsBySiteForWrite(ctx context.Context, arg LockBuildingsBySiteForWriteParams) ([]int64, error) { @@ -525,34 +552,45 @@ func (q *Queries) LockSiteForWrite(ctx context.Context, arg LockSiteForWritePara return id, err } -const reassignDevicesToSite = `-- name: ReassignDevicesToSite :execrows -UPDATE device +const reassignDevicesUnderBuilding = `-- name: ReassignDevicesUnderBuilding :execrows +UPDATE device d SET site_id = $1, updated_at = CURRENT_TIMESTAMP -WHERE org_id = $2 - AND device_identifier = ANY($3::text[]) - AND deleted_at IS NULL +FROM device_set_membership dsm +JOIN device_set ds + ON ds.id = dsm.device_set_id + AND ds.deleted_at IS NULL +JOIN device_set_rack dsr + ON dsr.device_set_id = dsm.device_set_id + AND dsr.org_id = dsm.org_id +WHERE d.id = dsm.device_id + AND d.org_id = dsm.org_id + AND dsm.device_set_type = 'rack' + AND d.org_id = $2 + AND dsr.building_id = $3 + AND d.deleted_at IS NULL ` -type ReassignDevicesToSiteParams struct { - TargetSiteID sql.NullInt64 - OrgID int64 - DeviceIdentifiers []string +type ReassignDevicesUnderBuildingParams struct { + TargetSiteID sql.NullInt64 + OrgID int64 + BuildingID sql.NullInt64 } -// Bulk update of device.site_id for the given identifiers within the -// org. Caller is expected to have already validated that no device is -// in a rack at a different site (see FindDeviceSiteConflicts). -// target_site_id NULL = move to Unassigned. -func (q *Queries) ReassignDevicesToSite(ctx context.Context, arg ReassignDevicesToSiteParams) (int64, error) { - result, err := q.exec(ctx, q.reassignDevicesToSiteStmt, reassignDevicesToSite, arg.TargetSiteID, arg.OrgID, pq.Array(arg.DeviceIdentifiers)) +// Sets device.site_id = $target for every device in any live rack of +// the given building. Caller wraps this in the same tx as the building +// UPDATE. The JOIN on device_set with deleted_at IS NULL skips +// soft-deleted rack collections so a stale membership can't rewrite +// live devices through a rack users no longer see. +func (q *Queries) ReassignDevicesUnderBuilding(ctx context.Context, arg ReassignDevicesUnderBuildingParams) (int64, error) { + result, err := q.exec(ctx, q.reassignDevicesUnderBuildingStmt, reassignDevicesUnderBuilding, arg.TargetSiteID, arg.OrgID, arg.BuildingID) if err != nil { return 0, err } return result.RowsAffected() } -const reassignDevicesUnderBuilding = `-- name: ReassignDevicesUnderBuilding :execrows +const reassignDevicesUnderBuildingsBulk = `-- name: ReassignDevicesUnderBuildingsBulk :execrows UPDATE device d SET site_id = $1, updated_at = CURRENT_TIMESTAMP @@ -567,23 +605,21 @@ WHERE d.id = dsm.device_id AND d.org_id = dsm.org_id AND dsm.device_set_type = 'rack' AND d.org_id = $2 - AND dsr.building_id = $3 + AND dsr.building_id = ANY($3::bigint[]) AND d.deleted_at IS NULL ` -type ReassignDevicesUnderBuildingParams struct { +type ReassignDevicesUnderBuildingsBulkParams struct { TargetSiteID sql.NullInt64 OrgID int64 - BuildingID sql.NullInt64 + BuildingIds []int64 } -// Sets device.site_id = $target for every device in any live rack of -// the given building. Caller wraps this in the same tx as the building -// UPDATE. The JOIN on device_set with deleted_at IS NULL skips -// soft-deleted rack collections so a stale membership can't rewrite -// live devices through a rack users no longer see. -func (q *Queries) ReassignDevicesUnderBuilding(ctx context.Context, arg ReassignDevicesUnderBuildingParams) (int64, error) { - result, err := q.exec(ctx, q.reassignDevicesUnderBuildingStmt, reassignDevicesUnderBuilding, arg.TargetSiteID, arg.OrgID, arg.BuildingID) +// Bulk variant of ReassignDevicesUnderBuilding. Sets device.site_id = +// $target for every device in any live rack of any building in +// @building_ids. Caller wraps in the same tx as the building UPDATE. +func (q *Queries) ReassignDevicesUnderBuildingsBulk(ctx context.Context, arg ReassignDevicesUnderBuildingsBulkParams) (int64, error) { + result, err := q.exec(ctx, q.reassignDevicesUnderBuildingsBulkStmt, reassignDevicesUnderBuildingsBulk, arg.TargetSiteID, arg.OrgID, pq.Array(arg.BuildingIds)) if err != nil { return 0, err } @@ -621,6 +657,36 @@ func (q *Queries) ReassignRacksUnderBuilding(ctx context.Context, arg ReassignRa return result.RowsAffected() } +const reassignRacksUnderBuildingsBulk = `-- name: ReassignRacksUnderBuildingsBulk :execrows +UPDATE device_set_rack dsr +SET site_id = $1 +WHERE dsr.org_id = $2 + AND dsr.building_id = ANY($3::bigint[]) + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.deleted_at IS NULL + ) +` + +type ReassignRacksUnderBuildingsBulkParams struct { + TargetSiteID sql.NullInt64 + OrgID int64 + BuildingIds []int64 +} + +// Bulk variant of ReassignRacksUnderBuilding. Sets rack.site_id = +// $target for every live rack pointing at any of @building_ids in one +// statement. Caller wraps in the same tx as the building UPDATE so +// the building/rack/device site_ids stay in lockstep. +func (q *Queries) ReassignRacksUnderBuildingsBulk(ctx context.Context, arg ReassignRacksUnderBuildingsBulkParams) (int64, error) { + result, err := q.exec(ctx, q.reassignRacksUnderBuildingsBulkStmt, reassignRacksUnderBuildingsBulk, arg.TargetSiteID, arg.OrgID, pq.Array(arg.BuildingIds)) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + const siteBelongsToOrg = `-- name: SiteBelongsToOrg :one SELECT EXISTS( SELECT 1 FROM site diff --git a/server/internal/domain/buildings/models/models.go b/server/internal/domain/buildings/models/models.go index a3d0ffd53..01c5267e3 100644 --- a/server/internal/domain/buildings/models/models.go +++ b/server/internal/domain/buildings/models/models.go @@ -67,7 +67,7 @@ type CreateParams struct { // UpdateParams is the input shape for building updates. SiteID is // intentionally NOT updated here; that flow lives on -// SiteService.AssignBuildingToSite, which carries the cross-collection +// SiteService.AssignBuildingsToSite, which carries the cross-collection // invariant check. type UpdateParams struct { OrgID int64 @@ -110,13 +110,11 @@ type BuildingRack struct { PositionInAisle *int32 } -// AssignRackToBuildingParams is the input shape for assigning (or -// unassigning) a rack to a building, optionally with a grid cell. -type AssignRackToBuildingParams struct { - OrgID int64 +// RackPlacementParam carries one rack's identity plus its optional +// grid placement inside the target building. Used by +// AssignRacksToBuilding for bulk updates. +type RackPlacementParam struct { RackID int64 - // BuildingID is nil when unassigning the rack from any building. - BuildingID *int64 // AisleIndex / PositionInAisle are nil when the caller is not // positioning the rack at a specific cell. Must be paired (both // nil or both set); enforced at the service edge. @@ -124,9 +122,20 @@ type AssignRackToBuildingParams struct { PositionInAisle *int32 } -// AssignRackToBuildingResult is the response shape carrying the -// cascade impact count for the activity log + UI confirmation. -type AssignRackToBuildingResult struct { +// AssignRacksToBuildingParams is the input shape for the bulk +// rack→building assignment flow. TargetBuildingID is nil when +// unassigning every rack in the batch from any building. Each entry +// in Racks may carry its own grid placement (or leave it nil to clear +// the cell). +type AssignRacksToBuildingParams struct { + OrgID int64 + Racks []RackPlacementParam + TargetBuildingID *int64 +} + +// AssignRacksToBuildingResult is the aggregate response carrying the +// total cascade impact count across every rack in the batch. +type AssignRacksToBuildingResult struct { SiteReassignedDeviceCount int64 } diff --git a/server/internal/domain/buildings/service.go b/server/internal/domain/buildings/service.go index b2af855cc..1c54ac105 100644 --- a/server/internal/domain/buildings/service.go +++ b/server/internal/domain/buildings/service.go @@ -1,12 +1,13 @@ // Package buildings is the domain layer for the BuildingService RPC // surface. CRUD + cascade-unassign-on-delete; site assignment lives on -// SiteService.AssignBuildingToSite where the cross-collection +// SiteService.AssignBuildingsToSite where the cross-collection // invariant is enforced. package buildings import ( "context" "fmt" + "sort" fm "github.com/block/proto-fleet/server/generated/grpc/fleetmanagement/v1" "github.com/block/proto-fleet/server/internal/domain/activity" @@ -154,7 +155,7 @@ func (s *Service) UpdateBuilding(ctx context.Context, params models.UpdateParams var b *models.Building err := s.transactor.RunInTx(ctx, func(txCtx context.Context) error { // Lock the building row first so a concurrent - // AssignRackToBuilding can't race us into orphaned-position + // AssignRacksToBuilding can't race us into orphaned-position // state between the bounds check and the update. if err := s.siteStore.LockBuildingForWrite(txCtx, params.OrgID, params.ID); err != nil { return err @@ -259,142 +260,230 @@ func (s *Service) ListBuildingRacks(ctx context.Context, orgID, buildingID int64 return s.store.ListBuildingRacks(ctx, orgID, buildingID, pageSize, pageToken) } -// AssignRackToBuilding sets a rack's building_id and, optionally, its -// grid placement (aisle_index, position_in_aisle). Runs in a single -// transaction: +// AssignRacksToBuilding sets the building_id (and optional grid +// placement) of every rack in the batch. Runs in a single transaction: // -// 1. Lock the target building (when assigning) so a concurrent -// DeleteBuilding can't race the placement write. -// 2. Lock the rack row and read current placement. -// 3. Resolve the new site_id from the target building (or NULL when -// unassigning). -// 4. Validate the optional grid cell against the target building's -// aisles / racks_per_aisle. -// 5. Call collectionStore.UpdateRackPlacement to write site_id + -// building_id + zone atomically (zone is cleared on cross/leave -// building, mirroring the existing SaveRack cascade rule). -// 6. Cascade descendant device.site_id when the rack's site changes. -// 7. When the request includes a grid cell, write it via -// SetRackBuildingPosition. -func (s *Service) AssignRackToBuilding(ctx context.Context, params models.AssignRackToBuildingParams) (*models.AssignRackToBuildingResult, error) { - // Position fields must be paired. The proto CEL rule enforces - // this at the wire boundary; this is the defense-in-depth check - // for non-proto callers. - if (params.AisleIndex == nil) != (params.PositionInAisle == nil) { - return nil, fleeterror.NewInvalidArgumentError("aisle_index and position_in_aisle must both be set or both unset") - } - if params.AisleIndex != nil && params.BuildingID == nil { - return nil, fleeterror.NewInvalidArgumentError("a grid cell (aisle_index, position_in_aisle) requires a building_id") - } - if params.AisleIndex != nil && *params.AisleIndex < 0 { - return nil, fleeterror.NewInvalidArgumentError("aisle_index must be >= 0") - } - if params.PositionInAisle != nil && *params.PositionInAisle < 0 { - return nil, fleeterror.NewInvalidArgumentError("position_in_aisle must be >= 0") +// 1. Lock the target building once (when assigning), canonical lock +// order is building -> rack(s). +// 2. Validate every entry up-front (paired position fields, in-bounds +// aisle/position). The whole batch rejects on any invalid entry. +// 3. Pass 1 — for each rack (sorted by id for deadlock-safe lock +// order): +// a. Lock the rack row and read current placement. +// b. Resolve the new site_id from the target building (or preserve +// current.SiteID on building-only unassign). +// c. Compute final zone (clear on leave/cross building). +// d. Persist site_id + building_id + zone via UpdateRackPlacement. +// e. Cascade descendant device.site_id when the site changes; sum +// the per-rack counts into the aggregate result. +// f. When assigning (TargetBuildingID != nil), clear the rack's +// grid cell to (NULL, NULL) via SetRackBuildingPosition. +// 4. Pass 2 — for each rack that carries an explicit (aisle, position), +// write the final cell via SetRackBuildingPosition. +// +// The two-pass split is what lets a single batch contain heterogeneous +// position changes (swaps, "move into occupied cell", clear + reuse) +// without tripping uk_device_set_rack_building_position. By the time +// any rack tries to claim a cell in pass 2, every rack in the batch is +// guaranteed to hold NULL position — so no partial-unique-index +// collision can fire mid-batch. +// +// If any rack fails, the whole tx rolls back and no row is touched. +func (s *Service) AssignRacksToBuilding(ctx context.Context, params models.AssignRacksToBuildingParams) (*models.AssignRacksToBuildingResult, error) { + if len(params.Racks) == 0 { + return nil, fleeterror.NewInvalidArgumentError("racks must not be empty") + } + + // Reject duplicate rack_ids up front. The handler / proto layers + // don't enforce uniqueness, and the per-entry grid-cell write would + // silently clobber an earlier same-rack entry inside the tx — + // surface the inconsistency to the caller instead. + seenRackIDs := make(map[int64]struct{}, len(params.Racks)) + for _, rp := range params.Racks { + if _, dup := seenRackIDs[rp.RackID]; dup { + return nil, fleeterror.NewInvalidArgumentErrorf("duplicate rack_id %d in racks", rp.RackID) + } + seenRackIDs[rp.RackID] = struct{}{} } + // Per-entry validation runs before any I/O so a bad request fails + // fast without partial work. Defense-in-depth — the proto CEL rule + // also enforces position pairing. + for _, rp := range params.Racks { + if rp.RackID <= 0 { + return nil, fleeterror.NewInvalidArgumentError("rack_id must be > 0") + } + if (rp.AisleIndex == nil) != (rp.PositionInAisle == nil) { + return nil, fleeterror.NewInvalidArgumentError("aisle_index and position_in_aisle must both be set or both unset") + } + if rp.AisleIndex != nil && params.TargetBuildingID == nil { + return nil, fleeterror.NewInvalidArgumentError("a grid cell (aisle_index, position_in_aisle) requires a target_building_id") + } + if rp.AisleIndex != nil && *rp.AisleIndex < 0 { + return nil, fleeterror.NewInvalidArgumentError("aisle_index must be >= 0") + } + if rp.PositionInAisle != nil && *rp.PositionInAisle < 0 { + return nil, fleeterror.NewInvalidArgumentError("position_in_aisle must be >= 0") + } + } + + // Sort rack entries by id for stable lock order so two concurrent + // AssignRacksToBuilding calls overlapping on a rack set can't + // deadlock. + racks := make([]models.RackPlacementParam, len(params.Racks)) + copy(racks, params.Racks) + sort.Slice(racks, func(i, j int) bool { return racks[i].RackID < racks[j].RackID }) + var ( - out models.AssignRackToBuildingResult - newSiteID *int64 - cascadeRan bool + out models.AssignRacksToBuildingResult + targetSiteID *int64 + cascadeRackIDs []int64 + positionedRackIDs []int64 + // For a building-only unassign (TargetBuildingID == nil) of a + // single rack, capture the rack's current SiteID so the activity + // log preserves "the site this rack lives in" instead of nil + // (which would make the event look site-less). + fallbackSiteID *int64 ) err := s.transactor.RunInTx(ctx, func(txCtx context.Context) error { // Lock the target building first (canonical lock order: // building -> rack). Skip when unassigning — there is no - // building row to lock — but we still lock the rack below. + // building row to lock — but each rack still gets row-locked + // below. var targetBuilding *models.Building - if params.BuildingID != nil { - if err := s.siteStore.LockBuildingForWrite(txCtx, params.OrgID, *params.BuildingID); err != nil { + if params.TargetBuildingID != nil { + if err := s.siteStore.LockBuildingForWrite(txCtx, params.OrgID, *params.TargetBuildingID); err != nil { return err } - b, err := s.store.GetBuilding(txCtx, params.OrgID, *params.BuildingID) + b, err := s.store.GetBuilding(txCtx, params.OrgID, *params.TargetBuildingID) if err != nil { return err } targetBuilding = b - newSiteID = b.SiteID + targetSiteID = b.SiteID } // Grid-cell upper-bound validation has to run after we know // the target building's layout dimensions. - if params.AisleIndex != nil && targetBuilding != nil { - if targetBuilding.Aisles <= 0 || *params.AisleIndex >= targetBuilding.Aisles { - return fleeterror.NewInvalidArgumentErrorf("aisle_index %d is out of bounds (building has %d aisles)", *params.AisleIndex, targetBuilding.Aisles) - } - if targetBuilding.RacksPerAisle <= 0 || *params.PositionInAisle >= targetBuilding.RacksPerAisle { - return fleeterror.NewInvalidArgumentErrorf("position_in_aisle %d is out of bounds (building allows %d racks per aisle)", *params.PositionInAisle, targetBuilding.RacksPerAisle) + if targetBuilding != nil { + for _, rp := range racks { + if rp.AisleIndex == nil { + continue + } + if targetBuilding.Aisles <= 0 || *rp.AisleIndex >= targetBuilding.Aisles { + return fleeterror.NewInvalidArgumentErrorf("aisle_index %d is out of bounds (building has %d aisles)", *rp.AisleIndex, targetBuilding.Aisles) + } + if targetBuilding.RacksPerAisle <= 0 || *rp.PositionInAisle >= targetBuilding.RacksPerAisle { + return fleeterror.NewInvalidArgumentErrorf("position_in_aisle %d is out of bounds (building allows %d racks per aisle)", *rp.PositionInAisle, targetBuilding.RacksPerAisle) + } } } - // Lock the rack row and read its current placement so we can - // decide whether the cascade needs to run + what zone value - // to persist. - current, err := s.collectionStore.LockRackPlacementForWrite(txCtx, params.RackID, params.OrgID) - if err != nil { - return err - } - - // Building-only unassign must NOT cascade-clear the rack's site - // (and, transitively, every descendant device.site_id). Removing - // a rack from a building is a building-membership change; the - // rack and its devices stay in their current site until an - // explicit site-level unassign happens elsewhere. Preserve - // current.SiteID in that branch so the siteChanged check below - // reads false and the cascade stays inert. - if params.BuildingID == nil { - newSiteID = current.SiteID - } + // Phase A: sequential per-rack lock acquisition in sorted order. + // Locks must be acquired one-by-one to avoid the deadlock that + // would happen if two concurrent calls grabbed an overlapping + // rack set in different orders. Only locks + snapshot reads + // run here — every write happens in Phase B as a bulk + // statement once every row lock is held. + allRackIDs := make([]int64, 0, len(racks)) + // cascadeRackIDs is populated in this phase so the bulk + // CascadeRackDeviceSites call in Phase B knows which racks + // actually transitioned sites. It's still appended in + // sorted-rack order to keep the activity-log metadata stable. + for _, rp := range racks { + // Lock the rack row and read its current placement so we + // can decide whether the cascade needs to run later and + // capture per-rack state for the activity-log fallback. + current, err := s.collectionStore.LockRackPlacementForWrite(txCtx, rp.RackID, params.OrgID) + if err != nil { + return err + } + allRackIDs = append(allRackIDs, rp.RackID) + + // Capture the source SiteID for a single-rack building-only + // unassign so the activity log carries the rack's site + // instead of nil. Only meaningful when the batch is exactly + // one rack — multi-rack batches may straddle sites and + // surfacing the first rack's site would be misleading. + if params.TargetBuildingID == nil && len(racks) == 1 { + fallbackSiteID = current.SiteID + } - // Mirror SaveRack's zone-clear cascade: clear zone when the - // rack leaves a building or crosses to a different one. - // Preserve the current zone on a no-op building transition - // so legacy callers don't strip zone unintentionally. - finalZone := current.Zone - leavingBuilding := current.BuildingID != nil && params.BuildingID == nil - crossingBuildings := current.BuildingID != nil && params.BuildingID != nil && *current.BuildingID != *params.BuildingID - if leavingBuilding || crossingBuildings { - finalZone = "" + // Building-only unassign must NOT cascade-clear the rack's + // site (and, transitively, every descendant device.site_id). + // Preserve current.SiteID in that branch so siteChanged + // reads false and the cascade stays inert. + newSiteID := targetSiteID + if params.TargetBuildingID == nil { + newSiteID = current.SiteID + } + if !int64PtrEqual(current.SiteID, newSiteID) { + cascadeRackIDs = append(cascadeRackIDs, rp.RackID) + } } - // Persist site_id + building_id + zone in one write. The - // query also clears the grid position on building transition - // via a CASE expression, so a stale (aisle_index, - // position_in_aisle) never outlives its parent building. - if err := s.collectionStore.UpdateRackPlacement(txCtx, params.RackID, params.OrgID, newSiteID, params.BuildingID, finalZone); err != nil { + // Phase B1: single bulk write for site_id + building_id + zone + // + grid-position-on-building-change across every rack. The + // SQL CASE expressions mirror the per-row UpdateRackPlacement + + // service-layer zone rules so the swap/mixed-clear-and-place + // cases still behave like the F5 two-pass shape. + if err := s.collectionStore.UpdateRackPlacementBulkForBuilding( + txCtx, params.OrgID, allRackIDs, targetSiteID, params.TargetBuildingID, + ); err != nil { return err } - // Cascade descendant device.site_id when the rack's site - // changed. CascadeRackDeviceSites returns the row count. - siteChanged := !int64PtrEqual(current.SiteID, newSiteID) - if siteChanged { - count, err := s.collectionStore.CascadeRackDeviceSites(txCtx, params.RackID, params.OrgID, newSiteID) + // Phase B2: single bulk cascade for the subset of racks whose + // site actually changed. CascadeRackDeviceSitesBulk no-ops on + // an empty rack set, but skip the call to keep the wire log + // clean. + if len(cascadeRackIDs) > 0 { + count, err := s.collectionStore.CascadeRackDeviceSitesBulk( + txCtx, params.OrgID, cascadeRackIDs, targetSiteID, + ) if err != nil { return err } - out.SiteReassignedDeviceCount = count - cascadeRan = true + out.SiteReassignedDeviceCount += count } - // Grid-cell write. Two cases land here: - // - // - Both fields set → write the explicit (aisle, position). - // - Both fields nil + building_id is set → operator is - // unplacing the rack within the same building (or moving - // it across with no chosen cell yet). Write NULL/NULL so - // the cell on the rack row matches the operator's intent. - // UpdateRackPlacement's CASE only clears when building_id - // changes, so without this explicit write a same-building - // unplace would silently no-op and the old position would - // survive. - // - // When BuildingID is nil (full unassign) we skip this call — - // UpdateRackPlacement's CASE already nulls the position via - // the building-id-changed branch. - if params.BuildingID != nil { - if err := s.store.SetRackBuildingPosition(txCtx, params.OrgID, params.RackID, params.AisleIndex, params.PositionInAisle); err != nil { + // Phase B3: single bulk pass-1 vacate. Force (aisle, position) + // to (NULL, NULL) for every rack in the batch so pass-2 below + // can reclaim any cell without colliding mid-batch on the + // partial unique index uk_device_set_rack_building_position. + // When TargetBuildingID is nil the UpdateRackPlacement bulk + // already nulled positions via its CASE — skip here. + if params.TargetBuildingID != nil { + if err := s.store.SetRackBuildingPositionBulkClear(txCtx, params.OrgID, allRackIDs); err != nil { return err } + positionedRackIDs = append(positionedRackIDs, allRackIDs...) + } + + // Phase B4: single bulk pass-2 place for racks carrying a + // (aisle, position). Pass-1 vacated every cell touched by the + // batch so no two writes can collide. + if params.TargetBuildingID != nil { + var ( + placeRackIDs []int64 + placeAisles []int32 + placePos []int32 + ) + for _, rp := range racks { + if rp.AisleIndex == nil || rp.PositionInAisle == nil { + continue + } + placeRackIDs = append(placeRackIDs, rp.RackID) + placeAisles = append(placeAisles, *rp.AisleIndex) + placePos = append(placePos, *rp.PositionInAisle) + } + if len(placeRackIDs) > 0 { + if err := s.store.SetRackBuildingPositionBulkPlace( + txCtx, params.OrgID, placeRackIDs, placeAisles, placePos, + ); err != nil { + return err + } + } } return nil }) @@ -402,43 +491,44 @@ func (s *Service) AssignRackToBuilding(ctx context.Context, params models.Assign return nil, err } - // Activity log fires AFTER tx commits. SiteID is the rack's final - // site after the write — same as newSiteID, which now equals - // current.SiteID on building-only unassign (so we don't lose the - // site filter on building-removal events) and the target - // building's site otherwise. Using cascadeSite here would only - // populate when CascadeRackDeviceSites ran, hiding same-site - // assigns from site-scoped activity queries. + // Activity log fires AFTER tx commits. orgIDVal := params.OrgID - // Dereference the building id stored in metadata so JSON shape - // matches DeleteBuilding (int64, not *int64). Downstream consumers - // doing `.(int64)` on the metadata field would crash on the - // pointer variant. var buildingIDMeta any - if params.BuildingID != nil { - buildingIDMeta = *params.BuildingID + if params.TargetBuildingID != nil { + buildingIDMeta = *params.TargetBuildingID + } + rackIDs := make([]int64, len(racks)) + for i, rp := range racks { + rackIDs[i] = rp.RackID + } + // For a single-rack building-only unassign, fall back to the rack's + // own SiteID captured during the lock so the event still records + // which site the operator was working in. + eventSiteID := targetSiteID + if eventSiteID == nil && fallbackSiteID != nil { + eventSiteID = fallbackSiteID } event := activitymodels.Event{ Category: activitymodels.CategoryFleetManagement, Type: eventRackAssignedBuilding, OrganizationID: &orgIDVal, - SiteID: newSiteID, + SiteID: eventSiteID, Description: fmt.Sprintf( - "Assigned rack %d to building %v", - params.RackID, derefInt64(params.BuildingID), + "Assigned %d rack(s) to building %v", + len(racks), derefInt64(params.TargetBuildingID), ), Metadata: map[string]any{ - "rack_id": params.RackID, + "rack_ids": rackIDs, "building_id": buildingIDMeta, }, } - if cascadeRan { + if len(cascadeRackIDs) > 0 { event.Metadata["site_cascade"] = true + event.Metadata["site_cascaded_rack_ids"] = cascadeRackIDs event.Metadata["site_reassigned_device_count"] = out.SiteReassignedDeviceCount } - if params.AisleIndex != nil { - event.Metadata["aisle_index"] = *params.AisleIndex - event.Metadata["position_in_aisle"] = *params.PositionInAisle + if len(positionedRackIDs) > 0 { + event.Metadata["positioned_rack_ids"] = positionedRackIDs } activity.StampActor(ctx, &event) s.activitySvc.Log(ctx, event) @@ -531,7 +621,7 @@ func (s *Service) DeleteBuilding(ctx context.Context, orgID, id int64) (*models. // the org. // // `expectedSiteID` carries the site the handler resolved at authz time: -// if a concurrent AssignBuildingToSite moves the building between the +// if a concurrent AssignBuildingsToSite moves the building between the // handler's pre-authz lookup and this read, the building's current // site will diverge from what the caller was authorized for. We // surface that as NotFound rather than leaking telemetry into the @@ -579,7 +669,7 @@ func (s *Service) GetBuildingStats(ctx context.Context, orgID, buildingID int64, // Resolve floor-plan bounds for the out-of-range filter below. A rack // with aisle_index >= aisles or position_in_aisle >= racks_per_aisle - // shouldn't normally exist (AssignRackToBuilding + UpdateBuilding both + // shouldn't normally exist (AssignRacksToBuilding + UpdateBuilding both // validate), but the FE silently drops cells outside the rendered // grid, so we clear the position fields server-side here for defense // in depth — the rack still appears in rack_health[] without a cell. @@ -587,7 +677,7 @@ func (s *Service) GetBuildingStats(ctx context.Context, orgID, buildingID int64, if err != nil { return nil, err } - // Guard against the AssignBuildingToSite race: if the building has + // Guard against the AssignBuildingsToSite race: if the building has // moved to a different site since the handler's pre-authz lookup, // the permission grant we ran against doesn't match the current // scope. NotFound is the safe surface here — the caller was never @@ -607,7 +697,7 @@ func (s *Service) GetBuildingStats(ctx context.Context, orgID, buildingID int64, // Per-rack state counts via the existing collection-membership query. // // Residual race window (intentionally not guarded): if - // AssignRackToBuilding moves a rack out of this building between + // AssignRacksToBuilding moves a rack out of this building between // the ListBuildingRacks above and this counts read, the response // still includes per-rack state counts (hashing/broken/offline/ // sleeping totals) for that rack. The post-read building.SiteID @@ -663,7 +753,7 @@ func (s *Service) GetBuildingStats(ctx context.Context, orgID, buildingID int64, // Pass PAIRED + AUTHENTICATION_NEEDED explicitly so the stats roll-up // counts AUTH_NEEDED devices the same way the miner list does. // - // Also constrain by expectedSiteID so a concurrent AssignBuildingToSite + // Also constrain by expectedSiteID so a concurrent AssignBuildingsToSite // that commits between the building re-read and the device fetch can't // leak the new site's device set: the cascade stamps device.site_id // onto every device under the moved building, so requiring @@ -729,7 +819,7 @@ func (s *Service) GetBuildingStats(ctx context.Context, orgID, buildingID int64, // Belt-and-braces: re-read the building after all the rollup queries. // The device fetch is already scoped to expectedSiteID, but the rack // and per-rack state queries join on building_id alone — if - // AssignBuildingToSite committed between the initial GetBuilding check + // AssignBuildingsToSite committed between the initial GetBuilding check // and these reads, the rack/state data would still be that of the // moved building (which now belongs to a site the caller wasn't // authorized for). Catch that here and surface NotFound rather than diff --git a/server/internal/domain/buildings/service_stats_test.go b/server/internal/domain/buildings/service_stats_test.go index 6d520f2ab..52269a8f5 100644 --- a/server/internal/domain/buildings/service_stats_test.go +++ b/server/internal/domain/buildings/service_stats_test.go @@ -72,7 +72,7 @@ func TestGetBuildingStats_notFound(t *testing.T) { func TestGetBuildingStats_notFoundWhenSiteMovedDuringAuthz(t *testing.T) { // Race: handler resolved the building at site A, but a concurrent - // AssignBuildingToSite moved it to site B before the service read. + // AssignBuildingsToSite moved it to site B before the service read. // Expectation: surface NotFound rather than return stats the caller // wasn't authorized for in the new site-scope. ctrl := gomock.NewController(t) @@ -93,7 +93,7 @@ func TestGetBuildingStats_notFoundWhenSiteMovedDuringAuthz(t *testing.T) { func TestGetBuildingStats_notFoundWhenSiteMovedAfterReads(t *testing.T) { // Sharper race: handler + initial site read both saw site A, so the - // rollup proceeded. AssignBuildingToSite then commits to site B + // rollup proceeded. AssignBuildingsToSite then commits to site B // before the service hits its post-read re-check. Expectation: the // post-read guard catches the move and returns NotFound rather than // handing the caller a snapshot built under stale authz. diff --git a/server/internal/domain/buildings/service_test.go b/server/internal/domain/buildings/service_test.go index 3d3ffe26b..436ab22cf 100644 --- a/server/internal/domain/buildings/service_test.go +++ b/server/internal/domain/buildings/service_test.go @@ -185,7 +185,7 @@ func TestListBuildings_rejectsExclusiveFilters(t *testing.T) { } } -// Helper: assemble the full mock set for AssignRackToBuilding tests. +// Helper: assemble the full mock set for AssignRacksToBuilding tests. type assignHarness struct { store *mocks.MockBuildingStore siteStore *mocks.MockSiteStore @@ -212,8 +212,9 @@ func newAssignHarness(t *testing.T) *assignHarness { } // Assign with a grid cell: lock building, lock rack, write placement, -// write grid cell, no site cascade because target site matches current. -func TestAssignRackToBuilding_placesRackWithGridCell(t *testing.T) { +// vacate cell in pass 1 (NULL/NULL), then write the actual cell in +// pass 2. No site cascade because target site matches current. +func TestAssignRacksToBuilding_placesRackWithGridCell(t *testing.T) { h := newAssignHarness(t) buildingID := int64(11) rackID := int64(99) @@ -223,20 +224,27 @@ func TestAssignRackToBuilding_placesRackWithGridCell(t *testing.T) { h.siteStore.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, buildingID).Return(nil), h.store.EXPECT().GetBuilding(inTxCtx, testOrgID, buildingID). Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil), + // Phase A: lock + read. h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). Return(interfaces.RackPlacement{SiteID: nil, BuildingID: nil, Zone: ""}, nil), - h.collectionStore.EXPECT().UpdateRackPlacement(inTxCtx, rackID, testOrgID, &siteID, &buildingID, "").Return(nil), - // siteChanged is true (nil -> &siteID); cascade fires. - h.collectionStore.EXPECT().CascadeRackDeviceSites(inTxCtx, rackID, testOrgID, &siteID).Return(int64(2), nil), - h.store.EXPECT().SetRackBuildingPosition(inTxCtx, testOrgID, rackID, ptrInt32(1), ptrInt32(2)).Return(nil), + // Phase B1: single bulk placement update. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackID}, &siteID, &buildingID).Return(nil), + // Phase B2: single bulk cascade — siteChanged (nil -> &siteID). + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, []int64{rackID}, &siteID).Return(int64(2), nil), + // Phase B3: bulk pass-1 vacate. + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackID}).Return(nil), + // Phase B4: bulk pass-2 place. + h.store.EXPECT().SetRackBuildingPositionBulkPlace(inTxCtx, testOrgID, []int64{rackID}, []int32{1}, []int32{2}).Return(nil), ) - out, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: &buildingID, - AisleIndex: ptrInt32(1), - PositionInAisle: ptrInt32(2), + out, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{{ + RackID: rackID, + AisleIndex: ptrInt32(1), + PositionInAisle: ptrInt32(2), + }}, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -253,7 +261,7 @@ func TestAssignRackToBuilding_placesRackWithGridCell(t *testing.T) { // SetRackBuildingPosition(nil, nil). The explicit clear is what makes // same-building unplace work — without it, UpdateRackPlacement's CASE // preserves the old position whenever building_id doesn't change. -func TestAssignRackToBuilding_membersWithoutPositionClearsCell(t *testing.T) { +func TestAssignRacksToBuilding_membersWithoutPositionClearsCell(t *testing.T) { h := newAssignHarness(t) buildingID := int64(11) rackID := int64(99) @@ -264,13 +272,16 @@ func TestAssignRackToBuilding_membersWithoutPositionClearsCell(t *testing.T) { Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil) h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). Return(interfaces.RackPlacement{SiteID: &siteID}, nil) - h.collectionStore.EXPECT().UpdateRackPlacement(inTxCtx, rackID, testOrgID, &siteID, &buildingID, "").Return(nil) - h.store.EXPECT().SetRackBuildingPosition(inTxCtx, testOrgID, rackID, (*int32)(nil), (*int32)(nil)).Return(nil) - - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: &buildingID, + // No cascade — site unchanged. Bulk placement update + bulk pass-1 + // vacate fire; pass-2 place is skipped because no positions were + // requested. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackID}, &siteID, &buildingID).Return(nil) + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackID}).Return(nil) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{{RackID: rackID}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -278,11 +289,11 @@ func TestAssignRackToBuilding_membersWithoutPositionClearsCell(t *testing.T) { } // Same-building unplace: rack already in this building at a known cell, -// caller resends building_id with no position. SetRackBuildingPosition -// must fire with nil/nil so the prior (aisle, position) is cleared from -// the rack row. Guards against the "unplace within building silently -// no-ops" regression. -func TestAssignRackToBuilding_sameBuildingUnplaceClearsPosition(t *testing.T) { +// caller resends building_id with no position. The bulk pass-1 vacate +// is what clears the prior (aisle, position) so the unplace doesn't +// silently no-op. Guards against the "unplace within building silently +// no-ops" regression on the post-bulk refactor. +func TestAssignRacksToBuilding_sameBuildingUnplaceClearsPosition(t *testing.T) { h := newAssignHarness(t) buildingID := int64(11) rackID := int64(99) @@ -293,16 +304,16 @@ func TestAssignRackToBuilding_sameBuildingUnplaceClearsPosition(t *testing.T) { Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil) h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(buildingID), Zone: "Z1"}, nil) - // Same building → zone preserved → finalZone is "Z1". - h.collectionStore.EXPECT().UpdateRackPlacement(inTxCtx, rackID, testOrgID, &siteID, &buildingID, "Z1").Return(nil) - // No site change → no cascade. - // Critical: explicit position clear fires. - h.store.EXPECT().SetRackBuildingPosition(inTxCtx, testOrgID, rackID, (*int32)(nil), (*int32)(nil)).Return(nil) - - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: &buildingID, + // Bulk placement update — zone preservation is now decided in SQL + // per-row, so the bulk call only carries (target_site, target_building). + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackID}, &siteID, &buildingID).Return(nil) + // Critical: explicit bulk pass-1 vacate fires. + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackID}).Return(nil) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{{RackID: rackID}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -312,7 +323,7 @@ func TestAssignRackToBuilding_sameBuildingUnplaceClearsPosition(t *testing.T) { // Building-only unassign must preserve the rack's site_id — the cascade // from the device level was the bug this test guards against. siteChanged // is false, so CascadeRackDeviceSites must never be called. -func TestAssignRackToBuilding_unassignPreservesSiteAndSkipsCascade(t *testing.T) { +func TestAssignRacksToBuilding_unassignPreservesSiteAndSkipsCascade(t *testing.T) { h := newAssignHarness(t) const rackID = int64(99) const priorBuildingID = int64(11) @@ -321,14 +332,16 @@ func TestAssignRackToBuilding_unassignPreservesSiteAndSkipsCascade(t *testing.T) // No LockBuildingForWrite / GetBuilding expected — params.BuildingID is nil. h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(priorBuildingID), Zone: "Z1"}, nil) - // site stays &siteID; zone clears (leavingBuilding). - h.collectionStore.EXPECT().UpdateRackPlacement(inTxCtx, rackID, testOrgID, &siteID, (*int64)(nil), "").Return(nil) - // CascadeRackDeviceSites must NOT fire. - - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: nil, + // Bulk placement update: site target nil (preserve), building target nil. + // CascadeRackDeviceSitesBulk must NOT fire — site is preserved. + // Bulk pass-1 vacate is skipped — building_id change inside SQL CASE + // nulls aisle/position automatically. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackID}, (*int64)(nil), (*int64)(nil)).Return(nil) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: nil, + Racks: []models.RackPlacementParam{{RackID: rackID}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -337,7 +350,7 @@ func TestAssignRackToBuilding_unassignPreservesSiteAndSkipsCascade(t *testing.T) // Cross-building move into a different site: zone clears, site cascade // runs with the new site. -func TestAssignRackToBuilding_crossBuildingClearsZoneAndCascadesSite(t *testing.T) { +func TestAssignRacksToBuilding_crossBuildingClearsZoneAndCascadesSite(t *testing.T) { h := newAssignHarness(t) targetBuildingID := int64(22) priorBuildingID := int64(11) @@ -350,17 +363,17 @@ func TestAssignRackToBuilding_crossBuildingClearsZoneAndCascadesSite(t *testing. Return(&models.Building{ID: targetBuildingID, SiteID: &newSite, Aisles: 4, RacksPerAisle: 6}, nil) h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). Return(interfaces.RackPlacement{SiteID: &priorSite, BuildingID: ptrInt64(priorBuildingID), Zone: "Z1"}, nil) - // crossingBuildings ⇒ zone clears. - h.collectionStore.EXPECT().UpdateRackPlacement(inTxCtx, rackID, testOrgID, &newSite, &targetBuildingID, "").Return(nil) - h.collectionStore.EXPECT().CascadeRackDeviceSites(inTxCtx, rackID, testOrgID, &newSite).Return(int64(4), nil) - // Cross-building move with no chosen cell — explicit nil/nil - // position write confirms the new row carries no stale placement. - h.store.EXPECT().SetRackBuildingPosition(inTxCtx, testOrgID, rackID, (*int32)(nil), (*int32)(nil)).Return(nil) - - out, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: ptrInt64(targetBuildingID), + // Bulk placement update — crossingBuildings zone clear runs in SQL. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackID}, &newSite, &targetBuildingID).Return(nil) + // Bulk cascade fires for site-changed racks. + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, []int64{rackID}, &newSite).Return(int64(4), nil) + // Bulk pass-1 vacate confirms the new row carries no stale placement. + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackID}).Return(nil) + + out, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: ptrInt64(targetBuildingID), + Racks: []models.RackPlacementParam{{RackID: rackID}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -371,7 +384,7 @@ func TestAssignRackToBuilding_crossBuildingClearsZoneAndCascadesSite(t *testing. } // Grid cell out of bounds: validated after GetBuilding, before any write. -func TestAssignRackToBuilding_rejectsOutOfBoundsAisle(t *testing.T) { +func TestAssignRacksToBuilding_rejectsOutOfBoundsAisle(t *testing.T) { h := newAssignHarness(t) buildingID := int64(11) rackID := int64(99) @@ -382,12 +395,14 @@ func TestAssignRackToBuilding_rejectsOutOfBoundsAisle(t *testing.T) { // Reach LockRackPlacementForWrite via the closure ordering, but no // write or cascade fires because validation rejects first. - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: rackID, - BuildingID: ptrInt64(buildingID), - AisleIndex: ptrInt32(2), // out of bounds (Aisles=2 means valid 0,1) - PositionInAisle: ptrInt32(0), + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: ptrInt64(buildingID), + Racks: []models.RackPlacementParam{{ + RackID: rackID, + AisleIndex: ptrInt32(2), // out of bounds (Aisles=2 means valid 0,1) + PositionInAisle: ptrInt32(0), + }}, }) if err == nil { t.Fatal("expected InvalidArgument, got nil") @@ -395,13 +410,15 @@ func TestAssignRackToBuilding_rejectsOutOfBoundsAisle(t *testing.T) { } // Position pairing: aisle_index set, position_in_aisle absent. -func TestAssignRackToBuilding_rejectsHalfSetPosition(t *testing.T) { +func TestAssignRacksToBuilding_rejectsHalfSetPosition(t *testing.T) { h := newAssignHarness(t) - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: 1, - BuildingID: ptrInt64(11), - AisleIndex: ptrInt32(0), + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: ptrInt64(11), + Racks: []models.RackPlacementParam{{ + RackID: 1, + AisleIndex: ptrInt32(0), + }}, }) if err == nil { t.Fatal("expected InvalidArgument for half-set position pair, got nil") @@ -409,20 +426,267 @@ func TestAssignRackToBuilding_rejectsHalfSetPosition(t *testing.T) { } // Position-requires-building guard: grid cell set, building_id nil. -func TestAssignRackToBuilding_rejectsPositionWithoutBuilding(t *testing.T) { +func TestAssignRacksToBuilding_rejectsPositionWithoutBuilding(t *testing.T) { h := newAssignHarness(t) - _, err := h.svc.AssignRackToBuilding(context.Background(), models.AssignRackToBuildingParams{ - OrgID: testOrgID, - RackID: 1, - BuildingID: nil, - AisleIndex: ptrInt32(0), - PositionInAisle: ptrInt32(0), + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: nil, + Racks: []models.RackPlacementParam{{ + RackID: 1, + AisleIndex: ptrInt32(0), + PositionInAisle: ptrInt32(0), + }}, }) if err == nil { t.Fatal("expected InvalidArgument for grid cell without building_id, got nil") } } +// TestAssignRacksToBuilding_emptyRejected guards the len(Racks) == 0 +// pre-check so callers learn up front instead of getting a 0-row +// response. +func TestAssignRacksToBuilding_emptyRejected(t *testing.T) { + h := newAssignHarness(t) + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: ptrInt64(11), + Racks: nil, + }) + if err == nil { + t.Fatal("expected InvalidArgument for empty racks, got nil") + } + if h.tx.calls != 0 { + t.Fatalf("guard must reject before opening tx, got %d", h.tx.calls) + } +} + +// TestAssignRacksToBuilding_rejectsDuplicateRackIDs covers F19: bulk +// requests with the same rack id repeated must fail up-front so the +// per-entry grid-cell write doesn't silently clobber an earlier entry. +func TestAssignRacksToBuilding_rejectsDuplicateRackIDs(t *testing.T) { + h := newAssignHarness(t) + buildingID := int64(11) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{ + {RackID: 1, AisleIndex: ptrInt32(0), PositionInAisle: ptrInt32(0)}, + {RackID: 1, AisleIndex: ptrInt32(0), PositionInAisle: ptrInt32(1)}, + }, + }) + if err == nil { + t.Fatal("expected InvalidArgument for duplicate rack_ids, got nil") + } + if h.tx.calls != 0 { + t.Fatalf("guard must reject before opening tx, got %d", h.tx.calls) + } +} + +// TestAssignRacksToBuilding_bulkRollsBackOnLaterFailure mirrors the +// sites batch rollback case: first rack succeeds, second errors on the +// lock, the tx aborts, and the closure ran exactly once with the error +// propagating. +func TestAssignRacksToBuilding_bulkRollsBackOnLaterFailure(t *testing.T) { + h := newAssignHarness(t) + buildingID := int64(11) + siteID := int64(3) + + h.siteStore.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, buildingID).Return(nil) + h.store.EXPECT().GetBuilding(inTxCtx, testOrgID, buildingID). + Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil) + // Phase A walks both rack ids in order; the second lock errors so + // the closure exits before any bulk write fires. + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, int64(100), testOrgID). + Return(interfaces.RackPlacement{}, nil) + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, int64(101), testOrgID). + Return(interfaces.RackPlacement{}, fleeterror.NewNotFoundErrorf("rack %d not found", 101)) + // No bulk UpdateRackPlacementBulkForBuilding / CascadeRackDeviceSitesBulk / + // SetRackBuildingPosition{Bulk}* calls — the closure aborts in Phase A. + _ = siteID + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{ + {RackID: 100}, + {RackID: 101}, + }, + }) + if !fleeterror.IsNotFoundError(err) { + t.Fatalf("expected NotFound, got %v", err) + } + if h.tx.calls != 1 { + t.Fatalf("expected exactly 1 tx closure run, got %d", h.tx.calls) + } +} + +// TestAssignRacksToBuilding_swapsPositionsInSingleBatch covers F5: +// a single batch that swaps two racks' positions inside the same +// building must succeed in one tx. The service pre-clears every +// rack's position (pass 1) before writing any new positions (pass 2), +// so the partial unique index uk_device_set_rack_building_position +// can't see two rows trying to hold the same (building, aisle, pos) +// simultaneously. +func TestAssignRacksToBuilding_swapsPositionsInSingleBatch(t *testing.T) { + h := newAssignHarness(t) + buildingID := int64(11) + siteID := int64(3) + rackA := int64(100) + rackB := int64(101) + + // Racks are sorted by id, so rackA(100) is processed before rackB(101) + // during Phase A (lock acquisition). Phase B issues one bulk + // placement update, then one bulk pass-1 vacate covering both racks, + // then one bulk pass-2 place that writes the swapped positions in a + // single statement. + gomock.InOrder( + // Building lock + load. + h.siteStore.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, buildingID).Return(nil), + h.store.EXPECT().GetBuilding(inTxCtx, testOrgID, buildingID). + Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil), + // Phase A: locks in sorted order, no writes. + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackA, testOrgID). + Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(buildingID), Zone: ""}, nil), + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackB, testOrgID). + Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(buildingID), Zone: ""}, nil), + // Phase B1: single bulk placement update across both racks. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackA, rackB}, &siteID, &buildingID).Return(nil), + // Phase B2: no cascade — both racks stay in the same site. + // Phase B3: single bulk pass-1 vacate — critical for the swap. + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackA, rackB}).Return(nil), + // Phase B4: single bulk pass-2 place — both racks in one statement. + h.store.EXPECT().SetRackBuildingPositionBulkPlace( + inTxCtx, testOrgID, []int64{rackA, rackB}, []int32{0, 0}, []int32{1, 0}, + ).Return(nil), + ) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{ + // rackA: (0,0) -> (0,1) + {RackID: rackA, AisleIndex: ptrInt32(0), PositionInAisle: ptrInt32(1)}, + // rackB: (0,1) -> (0,0) + {RackID: rackB, AisleIndex: ptrInt32(0), PositionInAisle: ptrInt32(0)}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h.tx.calls != 1 { + t.Fatalf("expected one tx run, got %d", h.tx.calls) + } +} + +// TestAssignRacksToBuilding_mixedClearAndPlaceInSingleBatch covers F5: +// one rack being unplaced + another rack moving into the freshly +// vacated cell must succeed in one batch. The clear write fires in +// pass 1 strictly before the place write in pass 2. +func TestAssignRacksToBuilding_mixedClearAndPlaceInSingleBatch(t *testing.T) { + h := newAssignHarness(t) + buildingID := int64(11) + siteID := int64(3) + rackClearer := int64(100) // was at (0,0), going to NULL + rackPlacer := int64(101) // was unplaced, going to (0,0) + + gomock.InOrder( + h.siteStore.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, buildingID).Return(nil), + h.store.EXPECT().GetBuilding(inTxCtx, testOrgID, buildingID). + Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil), + // Phase A: locks in sorted order. + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackClearer, testOrgID). + Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(buildingID), Zone: ""}, nil), + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackPlacer, testOrgID). + Return(interfaces.RackPlacement{SiteID: &siteID, BuildingID: ptrInt64(buildingID), Zone: ""}, nil), + // Phase B1: single bulk placement update across both racks. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, []int64{rackClearer, rackPlacer}, &siteID, &buildingID).Return(nil), + // Phase B2: no cascade — both stay in the same site. + // Phase B3: bulk vacate covers both racks unconditionally + // (swap-safe invariant). + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, []int64{rackClearer, rackPlacer}).Return(nil), + // Phase B4: bulk pass-2 places only rackPlacer — rackClearer + // has no requested position and stays vacated. + h.store.EXPECT().SetRackBuildingPositionBulkPlace( + inTxCtx, testOrgID, []int64{rackPlacer}, []int32{0}, []int32{0}, + ).Return(nil), + ) + + _, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: []models.RackPlacementParam{ + {RackID: rackClearer}, // clear cell + {RackID: rackPlacer, AisleIndex: ptrInt32(0), PositionInAisle: ptrInt32(0)}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h.tx.calls != 1 { + t.Fatalf("expected one tx run, got %d", h.tx.calls) + } +} + +// TestAssignRacksToBuilding_largeBatchIssuesSingleBulkWrites guards the +// F7 bulk refactor: a 100-rack batch must produce exactly one +// UpdateRackPlacementBulkForBuilding + one CascadeRackDeviceSitesBulk +// + one SetRackBuildingPositionBulkClear + one SetRackBuildingPositionBulkPlace +// call regardless of N. +func TestAssignRacksToBuilding_largeBatchIssuesSingleBulkWrites(t *testing.T) { + h := newAssignHarness(t) + buildingID := int64(11) + siteID := int64(3) + + const N = 100 + wantRackIDs := make([]int64, N) + wantAisles := make([]int32, N) + wantPositions := make([]int32, N) + racks := make([]models.RackPlacementParam, N) + // Build distinct (aisle, position) values that fit in a 10×10 grid. + for i := 0; i < N; i++ { + id := int64(1000 + i) + wantRackIDs[i] = id + aisle := int32(i / 10) + pos := int32(i % 10) + wantAisles[i] = aisle + wantPositions[i] = pos + racks[i] = models.RackPlacementParam{ + RackID: id, AisleIndex: ptrInt32(aisle), PositionInAisle: ptrInt32(pos), + } + } + + h.siteStore.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, buildingID).Return(nil) + h.store.EXPECT().GetBuilding(inTxCtx, testOrgID, buildingID). + Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 10, RacksPerAisle: 10}, nil) + // Phase A: N per-rack lock acquisitions in sorted order — these are + // the only writes that fan out by N. + for _, id := range wantRackIDs { + h.collectionStore.EXPECT().LockRackPlacementForWrite(inTxCtx, id, testOrgID). + Return(interfaces.RackPlacement{}, nil) + } + // Phase B writes: exactly four bulk calls, regardless of N. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(inTxCtx, testOrgID, wantRackIDs, &siteID, &buildingID).Return(nil) + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, wantRackIDs, &siteID).Return(int64(200), nil) + h.store.EXPECT().SetRackBuildingPositionBulkClear(inTxCtx, testOrgID, wantRackIDs).Return(nil) + h.store.EXPECT().SetRackBuildingPositionBulkPlace(inTxCtx, testOrgID, wantRackIDs, wantAisles, wantPositions).Return(nil) + + out, err := h.svc.AssignRacksToBuilding(context.Background(), models.AssignRacksToBuildingParams{ + OrgID: testOrgID, + TargetBuildingID: &buildingID, + Racks: racks, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.SiteReassignedDeviceCount != 200 { + t.Fatalf("expected 200 cascaded devices, got %d", out.SiteReassignedDeviceCount) + } + if h.tx.calls != 1 { + t.Fatalf("expected one tx closure run, got %d", h.tx.calls) + } +} + // ListBuildingRacks just delegates to the store after an org-scoped // building existence check. func TestListBuildingRacks_returnsStoreResult(t *testing.T) { diff --git a/server/internal/domain/collection/service.go b/server/internal/domain/collection/service.go index 2e250d5f1..a8743cfeb 100644 --- a/server/internal/domain/collection/service.go +++ b/server/internal/domain/collection/service.go @@ -695,194 +695,333 @@ func (s *Service) ListCollections(ctx context.Context, req *pb.ListCollectionsRe return &pb.ListCollectionsResponse{Collections: collections, NextPageToken: nextPageToken, TotalCount: totalCount}, nil } -type membershipChangeResult struct { - collection *pb.DeviceCollection - count int64 - conflicts []interfaces.AddedDeviceSiteConflict - finalSiteID *int64 - cascadeCount int64 +// AddDevicesToGroupParams is the domain-layer input shape for adding +// devices to a group device set. TargetGroupID must point at a group; +// rack adds must go through AssignDevicesToRack to get atomic prior-rack +// removal + site cascade. +type AddDevicesToGroupParams struct { + TargetGroupID int64 + DeviceSelector *commonpb.DeviceSelector } -// AddDevicesToCollection adds devices to a collection. -func (s *Service) AddDevicesToCollection(ctx context.Context, req *pb.AddDevicesToCollectionRequest) (*pb.AddDevicesToCollectionResponse, error) { +// AddDevicesToGroupResult carries the added-row count for the activity +// log + handler response surface. +type AddDevicesToGroupResult struct { + AddedCount int64 +} + +// AddDevicesToGroup adds devices to a group device set. Groups are +// org-scoped (devices may span sites) so this skips the rack site +// cascade entirely. Rack targets are rejected with InvalidArgument. +func (s *Service) AddDevicesToGroup(ctx context.Context, params AddDevicesToGroupParams) (*AddDevicesToGroupResult, error) { info, err := session.GetInfo(ctx) if err != nil { return nil, err } - deviceIdentifiers, err := s.resolveDeviceIdentifiers(ctx, req.DeviceSelector, info.OrganizationID) + deviceIdentifiers, err := s.resolveDeviceIdentifiers(ctx, params.DeviceSelector, info.OrganizationID) if err != nil { return nil, err } + type txOut struct { + added int64 + label string + } result, err := s.transactor.RunInTxWithResult(ctx, func(ctx context.Context) (any, error) { - coll, err := s.collectionStore.GetCollection(ctx, info.OrganizationID, req.CollectionId) + coll, err := s.collectionStore.GetCollection(ctx, info.OrganizationID, params.TargetGroupID) if err != nil { return nil, err } - - // Lock rack FOR UPDATE so the cascade reads rack.site_id under a - // write lock that serializes against SiteService writers. Skip the - // site lock — that would invert canonical lock order and deadlock - // against concurrent site moves. Groups skip lock and cascade. - var ( - conflicts []interfaces.AddedDeviceSiteConflict - finalSiteID *int64 - cascadeCount int64 - ) - if coll.Type == pb.CollectionType_COLLECTION_TYPE_RACK { - placement, err := s.collectionStore.LockRackPlacementForWrite(ctx, req.CollectionId, info.OrganizationID) - if err != nil { - return nil, err - } - finalSiteID = placement.SiteID - if placement.SiteID != nil { - conflicts, err = s.collectionStore.GetAddedDeviceSiteConflicts(ctx, info.OrganizationID, req.CollectionId, deviceIdentifiers) - if err != nil { - return nil, err - } - } + if coll.Type != pb.CollectionType_COLLECTION_TYPE_GROUP { + return nil, fleeterror.NewInvalidArgumentErrorf("target_group_id %d is not a group", params.TargetGroupID) } - addedCount, err := s.collectionStore.AddDevicesToCollection(ctx, info.OrganizationID, req.CollectionId, deviceIdentifiers) + addedCount, err := s.collectionStore.AddDevicesToCollection(ctx, info.OrganizationID, params.TargetGroupID, deviceIdentifiers) if err != nil { return nil, err } - if coll.Type == pb.CollectionType_COLLECTION_TYPE_RACK && finalSiteID != nil { - n, err := s.collectionStore.CascadeAddedDeviceSites(ctx, info.OrganizationID, req.CollectionId, deviceIdentifiers) - if err != nil { - return nil, err - } - cascadeCount = n - } - - return &membershipChangeResult{ - collection: coll, - count: addedCount, - conflicts: conflicts, - finalSiteID: finalSiteID, - cascadeCount: cascadeCount, - }, nil + return &txOut{added: addedCount, label: coll.Label}, nil }) if err != nil { return nil, err } - - txResult, ok := result.(*membershipChangeResult) + out, ok := result.(*txOut) if !ok { return nil, fleeterror.NewInternalErrorf("unexpected result type: %T", result) } - addedCountInt := int(txResult.count) - scopeType := collectionScopeType(txResult.collection.Type) - label := txResult.collection.Label - addEvent := activitymodels.Event{ + addedCountInt := int(out.added) + scopeType := collectionScopeType(pb.CollectionType_COLLECTION_TYPE_GROUP) + s.logActivity(ctx, activitymodels.Event{ Category: activitymodels.CategoryCollection, Type: "add_devices", - Description: fmt.Sprintf("Add devices to %s: %s", scopeType, label), + Description: fmt.Sprintf("Add devices to %s: %s", scopeType, out.label), ScopeType: &scopeType, - ScopeLabel: &label, + ScopeLabel: &out.label, ScopeCount: &addedCountInt, UserID: &info.ExternalUserID, Username: &info.Username, OrganizationID: &info.OrganizationID, - SiteID: txResult.finalSiteID, - } - if len(txResult.conflicts) > 0 { - total := len(txResult.conflicts) - capacity := total - if capacity > maxCascadeAuditEntries { - capacity = maxCascadeAuditEntries - } - priors := make([]map[string]any, 0, capacity) - for i, c := range txResult.conflicts { - if i >= maxCascadeAuditEntries { - break - } - row := map[string]any{ - "device_identifier": c.DeviceIdentifier, - "target_site_id": c.TargetSiteID, - } - if c.PriorSiteID != nil { - row["prior_site_id"] = *c.PriorSiteID - } - priors = append(priors, row) - } - meta := map[string]any{ - "site_cascade": true, - "final_site_id": txResult.finalSiteID, - "site_reassigned_count": txResult.cascadeCount, - "device_site_changes": priors, - "total_affected": total, - } - if total > maxCascadeAuditEntries { - meta["truncated"] = true - } - addEvent.Metadata = meta - } - s.logActivity(ctx, addEvent) + }) - // #nosec G115 -- addedCount is bounded by request size which is limited by gRPC message size - return &pb.AddDevicesToCollectionResponse{ - CollectionId: req.CollectionId, - AddedCount: int32(txResult.count), - // #nosec G115 -- cascadeCount bounded by added member count - SiteReassignedCount: int32(txResult.cascadeCount), - }, nil + return &AddDevicesToGroupResult{AddedCount: out.added}, nil +} + +// RemoveDevicesFromGroupParams is the domain-layer input shape for +// removing devices from a group device set. Rack targets are rejected +// with InvalidArgument; use AssignDevicesToRack (target_rack_id unset) +// to clear rack membership. +type RemoveDevicesFromGroupParams struct { + TargetGroupID int64 + DeviceSelector *commonpb.DeviceSelector } -// RemoveDevicesFromCollection removes devices from a collection. -func (s *Service) RemoveDevicesFromCollection(ctx context.Context, req *pb.RemoveDevicesFromCollectionRequest) (*pb.RemoveDevicesFromCollectionResponse, error) { +// RemoveDevicesFromGroupResult carries the removed-row count for the +// activity log + handler response surface. +type RemoveDevicesFromGroupResult struct { + RemovedCount int64 +} + +// RemoveDevicesFromGroup removes devices from a group device set. +func (s *Service) RemoveDevicesFromGroup(ctx context.Context, params RemoveDevicesFromGroupParams) (*RemoveDevicesFromGroupResult, error) { info, err := session.GetInfo(ctx) if err != nil { return nil, err } - deviceIdentifiers, err := s.resolveDeviceIdentifiers(ctx, req.DeviceSelector, info.OrganizationID) + deviceIdentifiers, err := s.resolveDeviceIdentifiers(ctx, params.DeviceSelector, info.OrganizationID) if err != nil { return nil, err } + type txOut struct { + removed int64 + label string + } result, err := s.transactor.RunInTxWithResult(ctx, func(ctx context.Context) (any, error) { - coll, err := s.collectionStore.GetCollection(ctx, info.OrganizationID, req.CollectionId) + coll, err := s.collectionStore.GetCollection(ctx, info.OrganizationID, params.TargetGroupID) if err != nil { return nil, err } + if coll.Type != pb.CollectionType_COLLECTION_TYPE_GROUP { + return nil, fleeterror.NewInvalidArgumentErrorf("target_group_id %d is not a group", params.TargetGroupID) + } - removedCount, err := s.collectionStore.RemoveDevicesFromCollection(ctx, info.OrganizationID, req.CollectionId, deviceIdentifiers) + removedCount, err := s.collectionStore.RemoveDevicesFromCollection(ctx, info.OrganizationID, params.TargetGroupID, deviceIdentifiers) if err != nil { return nil, err } - return &membershipChangeResult{collection: coll, count: removedCount}, nil + return &txOut{removed: removedCount, label: coll.Label}, nil }) if err != nil { return nil, err } - - txResult, ok := result.(*membershipChangeResult) + out, ok := result.(*txOut) if !ok { return nil, fleeterror.NewInternalErrorf("unexpected result type: %T", result) } - removedCountInt := int(txResult.count) - scopeType := collectionScopeType(txResult.collection.Type) - label := txResult.collection.Label + removedCountInt := int(out.removed) + scopeType := collectionScopeType(pb.CollectionType_COLLECTION_TYPE_GROUP) s.logActivity(ctx, activitymodels.Event{ Category: activitymodels.CategoryCollection, Type: "remove_devices", - Description: fmt.Sprintf("Remove devices from %s: %s", scopeType, label), + Description: fmt.Sprintf("Remove devices from %s: %s", scopeType, out.label), ScopeType: &scopeType, - ScopeLabel: &label, + ScopeLabel: &out.label, ScopeCount: &removedCountInt, UserID: &info.ExternalUserID, Username: &info.Username, OrganizationID: &info.OrganizationID, }) - // #nosec G115 -- removedCount is bounded by request size which is limited by gRPC message size - return &pb.RemoveDevicesFromCollectionResponse{RemovedCount: int32(txResult.count)}, nil + return &RemoveDevicesFromGroupResult{RemovedCount: out.removed}, nil +} + +// AssignDevicesToRackParams is the domain-layer input shape for the +// atomic rack reassignment flow. TargetRackID == nil means "clear +// rack membership without re-assigning" (site/building stay intact). +type AssignDevicesToRackParams struct { + OrgID int64 + TargetRackID *int64 + DeviceIdentifiers []string +} + +// AssignDevicesToRackResult carries the per-step row counts the +// activity log + handler response surface. +type AssignDevicesToRackResult struct { + AssignedCount int64 + RemovedCount int64 + SiteReassignedCount int64 +} + +// AssignDevicesToRack atomically moves devices into target_rack_id (or +// clears their rack membership when target is unset) inside one +// transaction. The two-step "remove from prior rack, insert into new +// rack" happens under a single tx so a server error / network blip +// can't leave devices without rack membership (the orphan window the +// client-side orchestration had). Cascades device.site_id when the +// target rack's site differs from the device's current site, matching +// AddDevicesToCollection's cascade. +// +// Empty DeviceIdentifiers rejects with InvalidArgument so the caller +// learns up-front instead of getting a 0-row response. +func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesToRackParams) (*AssignDevicesToRackResult, error) { + if len(params.DeviceIdentifiers) == 0 { + return nil, fleeterror.NewInvalidArgumentError("device_identifiers must not be empty") + } + + type txOut struct { + assigned int64 + removed int64 + siteReassigned int64 + targetLabel string + } + result, err := s.transactor.RunInTxWithResult(ctx, func(ctx context.Context) (any, error) { + var ( + targetSiteID *int64 + targetLabel string + ) + // Canonical lock order: lock every rack involved in the + // reparent -- sources + target -- together in ascending + // device_set.id order via LockRacksForReparent. Locking source + // and target as one globally sorted set is what keeps two + // concurrent AssignDevicesToRack calls moving devices in + // opposite directions between the same rack pair from + // deadlocking: each tx acquires {rack1, rack2} in the same + // {1, 2} order regardless of which side is source vs target. + // Without the pre-pass, the calls race the + // device_set_membership unique constraint and the loser trips + // uk_device_set_membership during the INSERT. The subsequent + // LockRackPlacementForWrite call below still fires for its + // device_set_rack row + placement read; the parent device_set + // row it locks is already held by this tx from the pre-pass. + // Pass 0 in the clear-rack path so the UNION contributes no + // target row. + var targetRackID int64 + if params.TargetRackID != nil { + targetRackID = *params.TargetRackID + } + if _, err := s.collectionStore.LockRacksForReparent(ctx, params.OrgID, params.DeviceIdentifiers, targetRackID); err != nil { + return nil, err + } + + // Lock + verify target rack so concurrent SaveRack / + // DeleteCollection on the target can't race us. Site/building + // locks would invert against AddDevicesToCollection's cascade, + // so we stay rack-only here. + // + // Order: take the row lock BEFORE the label/type read so a + // concurrent rename can't slip a stale label into the activity + // log we emit downstream. + if params.TargetRackID != nil { + placement, err := s.collectionStore.LockRackPlacementForWrite(ctx, *params.TargetRackID, params.OrgID) + if err != nil { + return nil, err + } + coll, err := s.collectionStore.GetCollection(ctx, params.OrgID, *params.TargetRackID) + if err != nil { + return nil, err + } + if coll.Type != pb.CollectionType_COLLECTION_TYPE_RACK { + return nil, fleeterror.NewInvalidArgumentErrorf("target_rack_id %d is not a rack", *params.TargetRackID) + } + targetSiteID = placement.SiteID + targetLabel = coll.Label + } + + // Clear existing rack membership for the given devices regardless + // of which rack they sit in EXCEPT the target rack. Excluding + // the target preserves the membership row + rack_slot child for + // devices that are already in the target rack (a same-rack + // re-add would otherwise cascade-drop the rack_slot row). This + // is the half of the operation that previously lived in the + // client-side RemoveDevicesFromDeviceSet call; bundling it into + // the tx is what closes the orphan window described in #420. + removed, err := s.collectionStore.RemoveDevicesFromAnyRack(ctx, params.OrgID, params.DeviceIdentifiers, targetRackID) + if err != nil { + return nil, err + } + + var ( + assigned int64 + siteReassigned int64 + ) + if params.TargetRackID != nil { + n, err := s.collectionStore.AddDevicesToCollection(ctx, params.OrgID, *params.TargetRackID, params.DeviceIdentifiers) + if err != nil { + return nil, err + } + assigned = n + if targetSiteID != nil { + c, err := s.collectionStore.CascadeAddedDeviceSites(ctx, params.OrgID, *params.TargetRackID, params.DeviceIdentifiers) + if err != nil { + return nil, err + } + siteReassigned = c + } + } + + return &txOut{ + assigned: assigned, + removed: removed, + siteReassigned: siteReassigned, + targetLabel: targetLabel, + }, nil + }) + if err != nil { + return nil, err + } + out, ok := result.(*txOut) + if !ok { + return nil, fleeterror.NewInternalErrorf("unexpected result type: %T", result) + } + + info, _ := session.GetInfo(ctx) + var ( + userID, username *string + orgIDPtr *int64 + eventDescription, eventScopeStr string + ) + if info != nil { + userID = &info.ExternalUserID + username = &info.Username + orgIDPtr = &info.OrganizationID + } + if params.TargetRackID != nil { + eventScopeStr = "rack" + eventDescription = fmt.Sprintf("Assigned devices to rack: %s", out.targetLabel) + } else { + eventDescription = "Cleared devices from rack" + } + // ScopeCount is the number of devices the caller asked to touch, + // not the sum of per-step row counts (assigned + removed can + // double-count devices that were both removed from a prior rack + // and added to a new one). + assignedInt := len(params.DeviceIdentifiers) + event := activitymodels.Event{ + Category: activitymodels.CategoryCollection, + Type: "assign_devices_to_rack", + Description: eventDescription, + ScopeCount: &assignedInt, + UserID: userID, + Username: username, + OrganizationID: orgIDPtr, + } + if eventScopeStr != "" { + event.ScopeType = &eventScopeStr + event.ScopeLabel = &out.targetLabel + } + s.logActivity(ctx, event) + + return &AssignDevicesToRackResult{ + AssignedCount: out.assigned, + RemovedCount: out.removed, + SiteReassignedCount: out.siteReassigned, + }, nil } // ListCollectionMembers returns all members of a collection. diff --git a/server/internal/domain/collection/service_test.go b/server/internal/domain/collection/service_test.go index ed161681f..d511a4fb1 100644 --- a/server/internal/domain/collection/service_test.go +++ b/server/internal/domain/collection/service_test.go @@ -292,7 +292,7 @@ func TestService_DeleteCollection_NotFoundWhenZeroRows(t *testing.T) { assert.True(t, fleeterror.IsNotFoundError(err)) } -func TestService_AddDevicesToCollection_NotFoundWhenNotOwnedByOrg(t *testing.T) { +func TestService_AddDevicesToGroup_NotFoundWhenNotOwnedByOrg(t *testing.T) { ctrl := gomock.NewController(t) mockStore := mocks.NewMockCollectionStore(ctrl) mockTransactor := mocks.NewMockTransactor(ctrl) @@ -313,8 +313,8 @@ func TestService_AddDevicesToCollection_NotFoundWhenNotOwnedByOrg(t *testing.T) Return(nil, fleeterror.NewNotFoundErrorf("collection not found")) // Act - _, err := svc.AddDevicesToCollection(ctx, &pb.AddDevicesToCollectionRequest{ - CollectionId: testCollectionID, + _, err := svc.AddDevicesToGroup(ctx, AddDevicesToGroupParams{ + TargetGroupID: testCollectionID, DeviceSelector: &commonpb.DeviceSelector{ SelectionType: &commonpb.DeviceSelector_DeviceList{ DeviceList: &commonpb.DeviceIdentifierList{DeviceIdentifiers: []string{"device-1"}}, @@ -400,7 +400,7 @@ func TestService_GetRackSlots_RejectsGroupCollection(t *testing.T) { assert.True(t, fleeterror.IsInvalidArgumentError(err)) } -func TestService_AddDevicesToCollection_ResolverError(t *testing.T) { +func TestService_AddDevicesToGroup_ResolverError(t *testing.T) { ctrl := gomock.NewController(t) mockStore := mocks.NewMockCollectionStore(ctrl) mockTransactor := mocks.NewMockTransactor(ctrl) @@ -413,8 +413,8 @@ func TestService_AddDevicesToCollection_ResolverError(t *testing.T) { svc := NewService(mockStore, &mockDeviceQueryer{}, nil, nil, mockTransactor, resolver, nil, newStubActivityService(ctrl)) // Act - _, err := svc.AddDevicesToCollection(ctx, &pb.AddDevicesToCollectionRequest{ - CollectionId: testCollectionID, + _, err := svc.AddDevicesToGroup(ctx, AddDevicesToGroupParams{ + TargetGroupID: testCollectionID, DeviceSelector: &commonpb.DeviceSelector{ SelectionType: &commonpb.DeviceSelector_DeviceList{ DeviceList: &commonpb.DeviceIdentifierList{DeviceIdentifiers: []string{"device-1"}}, @@ -1536,7 +1536,7 @@ func TestService_SaveRack_MoveBetweenBuildingsCascadesSite(t *testing.T) { mockStore.EXPECT().GetCollectionType(gomock.Any(), testOrgID, collectionID).Return(pb.CollectionType_COLLECTION_TYPE_RACK, nil) // resolveAndLockRackPlacement peeks building→site, locks site, locks // building, then re-reads building→site under the lock to detect - // concurrent AssignBuildingToSite. Both reads return the same value + // concurrent AssignBuildingsToSite. Both reads return the same value // here, so the tx proceeds without abort/retry. mockStore.EXPECT().GetBuildingSite(gomock.Any(), testOrgID, newBuilding).Return(&newSiteID, nil).Times(2) mockSiteStore.EXPECT().LockSiteForWrite(gomock.Any(), testOrgID, newSiteID).Return(nil) @@ -1785,67 +1785,11 @@ func TestService_SaveRack_OmittedPlacementPreservesZone(t *testing.T) { _ = mockSiteStore } -// TestService_AddDevicesToCollection_CascadesRackSite covers the -// AddDevicesToDeviceSet cascade flow (issue #220): when devices are -// added to a rack that has a site stamped, every paired device whose -// current site_id differs is rewritten to the rack's site_id in the -// same transaction. Group targets remain org-scoped. -func TestService_AddDevicesToCollection_CascadesRackSite(t *testing.T) { - deviceIDs := []string{"device-1", "device-2"} - resolver := func(_ context.Context, _ *commonpb.DeviceSelector, _ int64) ([]string, error) { - return deviceIDs, nil - } - svc, mockStore, _, captured := newTestServiceWithSitesRecordingActivity(t, resolver) - ctx := testCtx(t) - - collectionID := int64(42) - rackSite := int64(7) - priorSite := int64(11) - - mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, collectionID). - Return(&pb.DeviceCollection{Id: collectionID, Label: "Rack A", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil) - mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), collectionID, testOrgID). - Return(interfaces.RackPlacement{SiteID: &rackSite}, nil) - mockStore.EXPECT().GetAddedDeviceSiteConflicts(gomock.Any(), testOrgID, collectionID, deviceIDs). - Return([]interfaces.AddedDeviceSiteConflict{ - {DeviceIdentifier: "device-1", PriorSiteID: &priorSite, TargetSiteID: rackSite}, - }, nil) - mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, collectionID, deviceIDs).Return(int64(2), nil) - mockStore.EXPECT().CascadeAddedDeviceSites(gomock.Any(), testOrgID, collectionID, deviceIDs).Return(int64(1), nil) - - resp, err := svc.AddDevicesToCollection(ctx, &pb.AddDevicesToCollectionRequest{ - CollectionId: collectionID, - DeviceSelector: &commonpb.DeviceSelector{ - SelectionType: &commonpb.DeviceSelector_DeviceList{ - DeviceList: &commonpb.DeviceIdentifierList{DeviceIdentifiers: deviceIDs}, - }, - }, - }) - require.NoError(t, err) - assert.Equal(t, int32(2), resp.AddedCount) - assert.Equal(t, int32(1), resp.SiteReassignedCount, "response carries the cascade row count") - - // Assert cascade metadata on the activity event. - require.Len(t, *captured, 1) - event := (*captured)[0] - assert.Equal(t, "add_devices", event.Type) - require.NotNil(t, event.SiteID) - assert.Equal(t, rackSite, *event.SiteID) - require.NotNil(t, event.Metadata) - assert.Equal(t, true, event.Metadata["site_cascade"]) - assert.Equal(t, int64(1), event.Metadata["site_reassigned_count"]) - priors, ok := event.Metadata["device_site_changes"].([]map[string]any) - require.True(t, ok) - require.Len(t, priors, 1) - assert.Equal(t, "device-1", priors[0]["device_identifier"]) - assert.Equal(t, priorSite, priors[0]["prior_site_id"]) - assert.Equal(t, rackSite, priors[0]["target_site_id"]) -} - -// TestService_AddDevicesToCollection_GroupTargetSkipsCascade asserts -// the cascade exemption for groups: plan §"Cross-collection consistency -// rule" — groups are org-scoped and may span sites by design. -func TestService_AddDevicesToCollection_GroupTargetSkipsCascade(t *testing.T) { +// TestService_AddDevicesToGroup_HappyPath covers the group add flow: +// groups are org-scoped (cross-site allowed) so there is no rack lock, +// no LockSiteForWrite, and no cascade — just the membership insert +// plus an activity event. +func TestService_AddDevicesToGroup_HappyPath(t *testing.T) { deviceIDs := []string{"device-1"} resolver := func(_ context.Context, _ *commonpb.DeviceSelector, _ int64) ([]string, error) { return deviceIDs, nil @@ -1856,12 +1800,10 @@ func TestService_AddDevicesToCollection_GroupTargetSkipsCascade(t *testing.T) { collectionID := int64(43) mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, collectionID). Return(&pb.DeviceCollection{Id: collectionID, Label: "G1", Type: pb.CollectionType_COLLECTION_TYPE_GROUP}, nil) - // No LockRackPlacementForWrite, no LockSiteForWrite, no cascade - // expectations — groups skip the rack-site invariant entirely. mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, collectionID, deviceIDs).Return(int64(1), nil) - resp, err := svc.AddDevicesToCollection(ctx, &pb.AddDevicesToCollectionRequest{ - CollectionId: collectionID, + resp, err := svc.AddDevicesToGroup(ctx, AddDevicesToGroupParams{ + TargetGroupID: collectionID, DeviceSelector: &commonpb.DeviceSelector{ SelectionType: &commonpb.DeviceSelector_DeviceList{ DeviceList: &commonpb.DeviceIdentifierList{DeviceIdentifiers: deviceIDs}, @@ -1869,13 +1811,14 @@ func TestService_AddDevicesToCollection_GroupTargetSkipsCascade(t *testing.T) { }, }) require.NoError(t, err) - assert.Equal(t, int32(1), resp.AddedCount) + assert.Equal(t, int64(1), resp.AddedCount) } -// TestService_AddDevicesToCollection_RackWithoutSiteSkipsCascade asserts -// that adding devices to a rack whose site_id is NULL still inserts the -// membership but does not run the cascade — there is no site to enforce. -func TestService_AddDevicesToCollection_RackWithoutSiteSkipsCascade(t *testing.T) { +// TestService_AddDevicesToGroup_RejectsRackTarget covers the type +// guard: rack targets must go through AssignDevicesToRack to get the +// atomic prior-rack removal + site cascade. The group endpoint rejects +// rack targets with InvalidArgument before any store mutation. +func TestService_AddDevicesToGroup_RejectsRackTarget(t *testing.T) { deviceIDs := []string{"device-1"} resolver := func(_ context.Context, _ *commonpb.DeviceSelector, _ int64) ([]string, error) { return deviceIDs, nil @@ -1885,20 +1828,204 @@ func TestService_AddDevicesToCollection_RackWithoutSiteSkipsCascade(t *testing.T collectionID := int64(44) mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, collectionID). - Return(&pb.DeviceCollection{Id: collectionID, Label: "Site-less Rack", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil) - mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), collectionID, testOrgID). - Return(interfaces.RackPlacement{}, nil) // no site stamped - // No GetAddedDeviceSiteConflicts, no CascadeAddedDeviceSites. - mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, collectionID, deviceIDs).Return(int64(1), nil) + Return(&pb.DeviceCollection{Id: collectionID, Label: "Rack-X", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil) - resp, err := svc.AddDevicesToCollection(ctx, &pb.AddDevicesToCollectionRequest{ - CollectionId: collectionID, + _, err := svc.AddDevicesToGroup(ctx, AddDevicesToGroupParams{ + TargetGroupID: collectionID, DeviceSelector: &commonpb.DeviceSelector{ SelectionType: &commonpb.DeviceSelector_DeviceList{ DeviceList: &commonpb.DeviceIdentifierList{DeviceIdentifiers: deviceIDs}, }, }, }) + require.Error(t, err) + assert.True(t, fleeterror.IsInvalidArgumentError(err)) +} + +// TestService_AssignDevicesToRack_atomicReassign covers the issue +// #420 atomic-rack-reassign happy path: the prior rack membership is +// cleared and the new membership written inside one transaction, with +// the cascade firing when the target rack has a stamped site. +func TestService_AssignDevicesToRack_atomicReassign(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + targetRackID := int64(42) + rackSite := int64(7) + deviceIDs := []string{"d1", "d2"} + + gomock.InOrder( + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, targetRackID). + Return([]int64{11, 23}, nil), + mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID). + Return(interfaces.RackPlacement{SiteID: &rackSite}, nil), + mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID). + Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil), + mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(2), nil), + mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(2), nil), + mockStore.EXPECT().CascadeAddedDeviceSites(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil), + ) + + out, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: &targetRackID, + DeviceIdentifiers: deviceIDs, + }) require.NoError(t, err) - assert.Equal(t, int32(1), resp.AddedCount) + assert.Equal(t, int64(2), out.AssignedCount) + assert.Equal(t, int64(2), out.RemovedCount) + assert.Equal(t, int64(1), out.SiteReassignedCount) +} + +// TestService_AssignDevicesToRack_unassignClearsWithoutAdd covers the +// target_rack_id-unset branch: removes prior rack membership without +// touching site/building. No GetCollection/Lock/AddDevices call. +func TestService_AssignDevicesToRack_unassignClearsWithoutAdd(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + deviceIDs := []string{"d1"} + gomock.InOrder( + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, int64(0)). + Return([]int64{17}, nil), + mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, int64(0)).Return(int64(1), nil), + ) + + out, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: nil, + DeviceIdentifiers: deviceIDs, + }) + require.NoError(t, err) + assert.Equal(t, int64(0), out.AssignedCount) + assert.Equal(t, int64(1), out.RemovedCount) + assert.Equal(t, int64(0), out.SiteReassignedCount) +} + +// TestService_AssignDevicesToRack_targetMustBeRack rejects when the +// target collection exists but isn't a rack. +func TestService_AssignDevicesToRack_targetMustBeRack(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + targetID := int64(99) + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, []string{"d1"}, targetID). + Return(nil, nil) + mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetID, testOrgID). + Return(interfaces.RackPlacement{}, nil) + mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetID). + Return(&pb.DeviceCollection{Id: targetID, Label: "G1", Type: pb.CollectionType_COLLECTION_TYPE_GROUP}, nil) + + _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: &targetID, + DeviceIdentifiers: []string{"d1"}, + }) + require.Error(t, err) +} + +// TestService_AssignDevicesToRack_acquiresSourceRackLocksBeforeWrites +// pins F9's lock-order invariant: LockRacksForReparent must run +// BEFORE LockRackPlacementForWrite and BEFORE RemoveDevicesFromAnyRack. +// Reversing this risks deadlock against a concurrent call that takes +// the locks in the opposite order, and skipping it altogether is what +// lets concurrent overlapping reparent calls race the +// device_set_membership unique constraint. +func TestService_AssignDevicesToRack_acquiresSourceRackLocksBeforeWrites(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + targetRackID := int64(42) + deviceIDs := []string{"d1", "d2"} + + gomock.InOrder( + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, targetRackID). + Return([]int64{11, 23}, nil), + mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID). + Return(interfaces.RackPlacement{}, nil), + mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID). + Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil), + mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(2), nil), + mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(2), nil), + ) + + _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: &targetRackID, + DeviceIdentifiers: deviceIDs, + }) + require.NoError(t, err) +} + +// TestService_AssignDevicesToRack_unassignPathLocksSourceRacks pins +// that the rack-lock pre-pass ALSO fires on the clear-rack path. +// targetRackID is 0 so the UNION arm contributes no target row and +// only the source racks holding any of the requested devices are +// locked. +func TestService_AssignDevicesToRack_unassignPathLocksSourceRacks(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + deviceIDs := []string{"d1", "d2"} + + gomock.InOrder( + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, int64(0)). + Return([]int64{5, 12}, nil), + mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, int64(0)).Return(int64(2), nil), + ) + + _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: nil, + DeviceIdentifiers: deviceIDs, + }) + require.NoError(t, err) +} + +// TestService_AssignDevicesToRack_locksIncludeTargetRack pins the +// deadlock-prevention contract: when a target rack is supplied, the +// pre-pass lock call MUST receive the target rack id (not 0) so the +// SQL UNION includes the target in the same globally sorted FOR +// UPDATE acquisition as the source racks. Two concurrent reparents +// moving devices in opposite directions between rack A and rack B +// would otherwise lock {sourceA} then {B} in one tx and {sourceB} +// then {A} in the other, producing the classic A→B / B→A deadlock. +func TestService_AssignDevicesToRack_locksIncludeTargetRack(t *testing.T) { + svc, mockStore, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + targetRackID := int64(77) + deviceIDs := []string{"d1"} + + // Assert by argument: LockRacksForReparent receives targetRackID, + // not 0. The mock matcher rejects any other value. + mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, targetRackID). + Return([]int64{77}, nil) + mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID). + Return(interfaces.RackPlacement{}, nil) + mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID). + Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-Target", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil) + mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(0), nil) + mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil) + + _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + TargetRackID: &targetRackID, + DeviceIdentifiers: deviceIDs, + }) + require.NoError(t, err) +} + +// TestService_AssignDevicesToRack_emptyDevicesRejected guards the +// empty-input edge so callers get InvalidArgument instead of a +// silent 0-row response. +func TestService_AssignDevicesToRack_emptyDevicesRejected(t *testing.T) { + svc, _, _ := newTestServiceWithSites(t, nil) + ctx := testCtx(t) + + _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{ + OrgID: testOrgID, + DeviceIdentifiers: nil, + }) + require.Error(t, err) } diff --git a/server/internal/domain/fleetimport/importer.go b/server/internal/domain/fleetimport/importer.go index cd1ab4340..c870c3c79 100644 --- a/server/internal/domain/fleetimport/importer.go +++ b/server/internal/domain/fleetimport/importer.go @@ -12,6 +12,7 @@ import ( collectionpb "github.com/block/proto-fleet/server/generated/grpc/collection/v1" commonpb "github.com/block/proto-fleet/server/generated/grpc/common/v1" poolspb "github.com/block/proto-fleet/server/generated/grpc/pools/v1" + "github.com/block/proto-fleet/server/internal/domain/collection" "github.com/block/proto-fleet/server/internal/domain/fleeterror" models "github.com/block/proto-fleet/server/internal/domain/miner/models" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces" @@ -42,7 +43,8 @@ type PoolCreator interface { // collection service (with validation, transactions, and activity logging). type CollectionManager interface { CreateCollection(ctx context.Context, req *collectionpb.CreateCollectionRequest) (*collectionpb.CreateCollectionResponse, error) - AddDevicesToCollection(ctx context.Context, req *collectionpb.AddDevicesToCollectionRequest) (*collectionpb.AddDevicesToCollectionResponse, error) + AddDevicesToGroup(ctx context.Context, params collection.AddDevicesToGroupParams) (*collection.AddDevicesToGroupResult, error) + AssignDevicesToRack(ctx context.Context, params collection.AssignDevicesToRackParams) (*collection.AssignDevicesToRackResult, error) ListCollections(ctx context.Context, req *collectionpb.ListCollectionsRequest) (*collectionpb.ListCollectionsResponse, error) } @@ -77,7 +79,7 @@ func (imp *Importer) Import(ctx context.Context, orgID int64, data *ImportData) macToDevice := imp.resolveMACToDevice(ctx, orgID, data.Miners) result.PoolsCreated = imp.createPools(ctx, data.Pools) - result.GroupsCreated, result.RacksCreated, result.DevicesAssigned = imp.createGroupsAndRacks(ctx, data, macToDevice) + result.GroupsCreated, result.RacksCreated, result.DevicesAssigned = imp.createGroupsAndRacks(ctx, orgID, data, macToDevice) result.WorkerNamesSet = imp.setWorkerNames(ctx, data, macToDevice) result.MinerNamesSet = imp.setMinerNames(ctx, orgID, data, macToDevice) return result @@ -232,7 +234,7 @@ func deviceSelector(deviceIDs []string) *commonpb.DeviceSelector { } } -func (imp *Importer) createGroupsAndRacks(ctx context.Context, data *ImportData, macToDevice map[string]*interfaces.PairedDeviceInfo) (int32, int32, int32) { +func (imp *Importer) createGroupsAndRacks(ctx context.Context, orgID int64, data *ImportData, macToDevice map[string]*interfaces.PairedDeviceInfo) (int32, int32, int32) { // Pre-fetch existing collections so we can reconcile duplicates existingCollections := imp.buildExistingCollectionMap(ctx) @@ -301,14 +303,14 @@ func (imp *Importer) createGroupsAndRacks(ctx context.Context, data *ImportData, if existingID, ok := existingCollections[key]; ok { // Existing group — just add devices if len(deviceIDs) > 0 { - resp, err := imp.collectionManager.AddDevicesToCollection(ctx, &collectionpb.AddDevicesToCollectionRequest{ - CollectionId: existingID, + resp, err := imp.collectionManager.AddDevicesToGroup(ctx, collection.AddDevicesToGroupParams{ + TargetGroupID: existingID, DeviceSelector: deviceSelector(deviceIDs), }) if err != nil { slog.Warn("failed to add devices to group", "group", g.Name, "error", err) } else { - devicesAssigned += resp.AddedCount + devicesAssigned += int32(resp.AddedCount) //nolint:gosec // bounded by device count } } } else { @@ -326,13 +328,13 @@ func (imp *Importer) createGroupsAndRacks(ctx context.Context, data *ImportData, // Race: group was created after we built existingCollections — refresh and add devices. existingCollections = imp.buildExistingCollectionMap(ctx) if existingID, ok := existingCollections[key]; ok && len(deviceIDs) > 0 { - if addResp, addErr := imp.collectionManager.AddDevicesToCollection(ctx, &collectionpb.AddDevicesToCollectionRequest{ - CollectionId: existingID, + if addResp, addErr := imp.collectionManager.AddDevicesToGroup(ctx, collection.AddDevicesToGroupParams{ + TargetGroupID: existingID, DeviceSelector: deviceSelector(deviceIDs), }); addErr != nil { slog.Warn("failed to add devices to group after duplicate", "group", g.Name, "error", addErr) } else { - devicesAssigned += addResp.AddedCount + devicesAssigned += int32(addResp.AddedCount) //nolint:gosec // bounded by device count } } continue @@ -374,16 +376,20 @@ func (imp *Importer) createGroupsAndRacks(ctx context.Context, data *ImportData, } if existingID, ok := existingCollections[key]; ok { - // Existing rack — just add devices + // Existing rack — assign devices atomically. Bulk import + // targets fresh devices with no prior rack, so the atomic + // prior-rack-clear in AssignDevicesToRack is a no-op. if len(rackDeviceIDs) > 0 { - resp, err := imp.collectionManager.AddDevicesToCollection(ctx, &collectionpb.AddDevicesToCollectionRequest{ - CollectionId: existingID, - DeviceSelector: deviceSelector(rackDeviceIDs), + rackID := existingID + resp, err := imp.collectionManager.AssignDevicesToRack(ctx, collection.AssignDevicesToRackParams{ + OrgID: orgID, + TargetRackID: &rackID, + DeviceIdentifiers: rackDeviceIDs, }) if err != nil { - slog.Warn("failed to add devices to rack", "rack", r.Name, "error", err) + slog.Warn("failed to assign devices to rack", "rack", r.Name, "error", err) } else { - devicesAssigned += resp.AddedCount + devicesAssigned += int32(resp.AssignedCount) //nolint:gosec // bounded by device count } } } else { @@ -407,16 +413,18 @@ func (imp *Importer) createGroupsAndRacks(ctx context.Context, data *ImportData, slog.Warn("failed to create rack", "name", r.Name, "error", err) continue } - // Race: rack was created after we built existingCollections — refresh and add devices. + // Race: rack was created after we built existingCollections — refresh and assign devices. existingCollections = imp.buildExistingCollectionMap(ctx) if existingID, ok := existingCollections[key]; ok && len(rackDeviceIDs) > 0 { - if addResp, addErr := imp.collectionManager.AddDevicesToCollection(ctx, &collectionpb.AddDevicesToCollectionRequest{ - CollectionId: existingID, - DeviceSelector: deviceSelector(rackDeviceIDs), + rackID := existingID + if addResp, addErr := imp.collectionManager.AssignDevicesToRack(ctx, collection.AssignDevicesToRackParams{ + OrgID: orgID, + TargetRackID: &rackID, + DeviceIdentifiers: rackDeviceIDs, }); addErr != nil { - slog.Warn("failed to add devices to rack after duplicate", "rack", r.Name, "error", addErr) + slog.Warn("failed to assign devices to rack after duplicate", "rack", r.Name, "error", addErr) } else { - devicesAssigned += addResp.AddedCount + devicesAssigned += int32(addResp.AssignedCount) //nolint:gosec // bounded by device count } } continue diff --git a/server/internal/domain/fleetmanagement/collection_test.go b/server/internal/domain/fleetmanagement/collection_test.go index 4ccfdc0e0..921d214fc 100644 --- a/server/internal/domain/fleetmanagement/collection_test.go +++ b/server/internal/domain/fleetmanagement/collection_test.go @@ -254,7 +254,7 @@ func TestService_ListMinerStateSnapshots_ShouldPopulateSiteIDAndLabel(t *testing require.NoError(t, err) // Reassign only device 0 to the site; device 1 stays unassigned. - _, err = siteStore.ReassignDevicesToSite(t.Context(), orgID, &site.ID, deviceIDs[:1]) + _, err = siteStore.AssignDevicesToSite(t.Context(), orgID, &site.ID, deviceIDs[:1]) require.NoError(t, err) ctx := testutil.MockAuthContextForTesting(t.Context(), testUser.DatabaseID, orgID) diff --git a/server/internal/domain/foremanimport/service_test.go b/server/internal/domain/foremanimport/service_test.go index 0b3b05247..428b1d864 100644 --- a/server/internal/domain/foremanimport/service_test.go +++ b/server/internal/domain/foremanimport/service_test.go @@ -12,6 +12,7 @@ import ( collectionpb "github.com/block/proto-fleet/server/generated/grpc/collection/v1" pb "github.com/block/proto-fleet/server/generated/grpc/foremanimport/v1" poolspb "github.com/block/proto-fleet/server/generated/grpc/pools/v1" + "github.com/block/proto-fleet/server/internal/domain/collection" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces/mocks" "github.com/block/proto-fleet/server/internal/infrastructure/foreman" @@ -64,20 +65,25 @@ func (f *fakeCollectionManager) CreateCollection(_ context.Context, _ *collectio }, nil } -func (f *fakeCollectionManager) AddDevicesToCollection(_ context.Context, req *collectionpb.AddDevicesToCollectionRequest) (*collectionpb.AddDevicesToCollectionResponse, error) { +func (f *fakeCollectionManager) AddDevicesToGroup(_ context.Context, params collection.AddDevicesToGroupParams) (*collection.AddDevicesToGroupResult, error) { f.mu.Lock() defer f.mu.Unlock() var count int32 - if sel := req.GetDeviceSelector(); sel != nil { + if sel := params.DeviceSelector; sel != nil { if dl := sel.GetDeviceList(); dl != nil { count = int32(len(dl.DeviceIdentifiers)) //nolint:gosec } } f.devicesAdded += count - return &collectionpb.AddDevicesToCollectionResponse{ - CollectionId: req.CollectionId, - AddedCount: count, - }, nil + return &collection.AddDevicesToGroupResult{AddedCount: int64(count)}, nil +} + +func (f *fakeCollectionManager) AssignDevicesToRack(_ context.Context, params collection.AssignDevicesToRackParams) (*collection.AssignDevicesToRackResult, error) { + f.mu.Lock() + defer f.mu.Unlock() + count := int32(len(params.DeviceIdentifiers)) //nolint:gosec + f.devicesAdded += count + return &collection.AssignDevicesToRackResult{AssignedCount: int64(count)}, nil } func (f *fakeCollectionManager) ListCollections(_ context.Context, _ *collectionpb.ListCollectionsRequest) (*collectionpb.ListCollectionsResponse, error) { diff --git a/server/internal/domain/sites/models/models.go b/server/internal/domain/sites/models/models.go index 5fa0ce142..6dd113908 100644 --- a/server/internal/domain/sites/models/models.go +++ b/server/internal/domain/sites/models/models.go @@ -95,29 +95,55 @@ type PerDeviceConflict struct { ConflictingSiteID int64 } -// ReassignDevicesToSiteParams is the input shape for the bulk reassign +// AssignDevicesToSiteParams is the input shape for the bulk assign // flow. TargetSiteID == nil means "Unassigned". -type ReassignDevicesToSiteParams struct { - OrgID int64 - TargetSiteID *int64 - DeviceIdentifiers []string +// +// When ForceClearConflictingRackMembership is true the service, inside +// the same transaction as the site write, drops any existing rack +// membership for the listed devices before applying the site update. +// This closes the cross-site reparent orphan window the client-side +// remove-then-reassign loop in MinerReparentPicker had. When false +// (default), a device sitting in a rack at a different site rejects +// the whole batch with PerDeviceConflict[]. +type AssignDevicesToSiteParams struct { + OrgID int64 + TargetSiteID *int64 + DeviceIdentifiers []string + ForceClearConflictingRackMembership bool } -// AssignBuildingToSiteParams is the input shape for the building site -// reassignment flow. TargetSiteID == nil means "Unassigned". -type AssignBuildingToSiteParams struct { +// AssignBuildingsToSiteParams is the input shape for the bulk +// building→site assignment flow. TargetSiteID == nil means "Unassigned"; +// the entire batch is applied in one transaction. +type AssignBuildingsToSiteParams struct { OrgID int64 - BuildingID int64 + BuildingIDs []int64 TargetSiteID *int64 } -// AssignBuildingToSiteResult is the cascade-impact tally for the -// building → site move. -type AssignBuildingToSiteResult struct { +// AssignBuildingsToSiteResult is the aggregate cascade-impact tally +// across every building in the batch. +type AssignBuildingsToSiteResult struct { ReassignedRackCount int64 ReassignedDeviceCount int64 } +// AssignRacksToSiteParams is the input shape for the bulk rack→site +// partial-update flow. TargetSiteID == nil means "Unassigned"; the +// entire batch applies in one transaction. +type AssignRacksToSiteParams struct { + OrgID int64 + RackIDs []int64 + TargetSiteID *int64 +} + +// AssignRacksToSiteResult carries cascade impact + the count of racks +// whose building_id was cleared on the site transition. +type AssignRacksToSiteResult struct { + ReassignedDeviceCount int64 + ClearedBuildingCount int64 +} + // SiteNetworkConfigEntry is a (name, network_config) tuple used by the // service when computing cross-site overlap warnings on save. type SiteNetworkConfigEntry struct { diff --git a/server/internal/domain/sites/service.go b/server/internal/domain/sites/service.go index 7bcf46151..286fb6418 100644 --- a/server/internal/domain/sites/service.go +++ b/server/internal/domain/sites/service.go @@ -29,6 +29,7 @@ const ( eventSiteDeleted = "site.deleted" eventDevicesReassignedToSite = "devices.reassigned_to_site" eventBuildingAssignedToSite = "building.assigned_to_site" + eventRacksAssignedToSite = "racks.assigned_to_site" ) // maxDeviceIdentifiersInMetadata bounds how many identifiers we keep in @@ -50,12 +51,13 @@ const MaxDevicesPerSiteStatsRequest = 100_000 // site delete cascade and the bulk-reassign all-or-nothing semantics // both depend on it. type Service struct { - store interfaces.SiteStore - buildingStore interfaces.BuildingStore - deviceQueryer devicerollup.DeviceQueryer - telemetry devicerollup.TelemetryCollector - transactor interfaces.Transactor - activitySvc *activity.Service + store interfaces.SiteStore + buildingStore interfaces.BuildingStore + collectionStore interfaces.CollectionStore + deviceQueryer devicerollup.DeviceQueryer + telemetry devicerollup.TelemetryCollector + transactor interfaces.Transactor + activitySvc *activity.Service } // NewService wires a SiteStore, Transactor, and the activity Service @@ -66,21 +68,27 @@ type Service struct { // buildingStore, deviceQueryer, and telemetry power GetSiteStats only. // Any of them may be nil in test setups where the stats RPC isn't // exercised; GetSiteStats returns an internal error in that case. +// +// collectionStore powers AssignRacksToSite (it owns the rack +// placement read/write path shared with SaveRack). Nil collectionStore +// causes AssignRacksToSite to return an internal error. func NewService( store interfaces.SiteStore, buildingStore interfaces.BuildingStore, + collectionStore interfaces.CollectionStore, deviceQueryer devicerollup.DeviceQueryer, telemetry devicerollup.TelemetryCollector, transactor interfaces.Transactor, activitySvc *activity.Service, ) *Service { return &Service{ - store: store, - buildingStore: buildingStore, - deviceQueryer: deviceQueryer, - telemetry: telemetry, - transactor: transactor, - activitySvc: activitySvc, + store: store, + buildingStore: buildingStore, + collectionStore: collectionStore, + deviceQueryer: deviceQueryer, + telemetry: telemetry, + transactor: transactor, + activitySvc: activitySvc, } } @@ -273,13 +281,13 @@ func (s *Service) DeleteSite(ctx context.Context, orgID, id int64) (*models.Dele return &out, nil } -// ReassignDevicesToSite enforces the cross-collection invariant and, +// AssignDevicesToSite enforces the cross-collection invariant and, // on success, bulk-updates device.site_id for every identifier in one // transaction. Per the plan, the entire batch rejects if *any* device // fails the check; no partial writes. The conflict check and the // UPDATE run inside the same row-locked transaction so a concurrent -// reassign can't slip between them. -func (s *Service) ReassignDevicesToSite(ctx context.Context, params models.ReassignDevicesToSiteParams) (int64, []models.PerDeviceConflict, error) { +// assign can't slip between them. +func (s *Service) AssignDevicesToSite(ctx context.Context, params models.AssignDevicesToSiteParams) (int64, []models.PerDeviceConflict, error) { identifiers := dedupeStrings(params.DeviceIdentifiers) if len(identifiers) == 0 { return 0, nil, fleeterror.NewInvalidArgumentError("device_identifiers must not be empty") @@ -310,6 +318,49 @@ func (s *Service) ReassignDevicesToSite(ctx context.Context, params models.Reass if err != nil { return err } + // When the caller opted into the force-clear branch, treat + // DEVICE_IN_RACK_AT_OTHER_SITE conflicts as a cascade-clear + // signal instead of a rejection. DEVICE_NOT_FOUND still aborts + // — the caller can't move what doesn't exist. Partition + // conflicts: only identifiers whose blocker is rack-at-other- + // site get their rack memberships cleared. Devices already at + // the target site (no conflict) keep their rack rows, so we + // don't cascade-delete unrelated rack_slot rows. + if params.ForceClearConflictingRackMembership && len(conflicts) > 0 { + var ( + clearableIDs []string + residual []models.PerDeviceConflict + ) + for _, c := range conflicts { + if c.Reason == models.ReasonDeviceInRackAtOtherSite { + clearableIDs = append(clearableIDs, c.DeviceIdentifier) + continue + } + residual = append(residual, c) + } + // Abort BEFORE any deletion when residual non-clearable + // conflicts remain. Otherwise the tx would commit the + // rack-membership delete for clearable devices and then + // return without the site move, leaving rack-stripped + // devices on their old site. + if len(residual) > 0 { + txConflicts = residual + return nil + } + if len(clearableIDs) > 0 { + if s.collectionStore == nil { + return fleeterror.NewInternalErrorf("force-clear branch requires a collection store") + } + // Clear only the rack memberships for devices that + // actually had the cross-site conflict. targetRackID=0 + // means "exclude nothing", i.e. drop every rack row + // for the listed devices. + if _, err := s.collectionStore.RemoveDevicesFromAnyRack(txCtx, params.OrgID, clearableIDs, 0); err != nil { + return err + } + conflicts = nil + } + } if len(conflicts) > 0 { // Don't return a sentinel error — SQLTransactor wraps non- // FleetError errors as Internal, which would surface as a @@ -318,7 +369,7 @@ func (s *Service) ReassignDevicesToSite(ctx context.Context, params models.Reass txConflicts = conflicts return nil } - n, txErr := s.store.ReassignDevicesToSite(txCtx, params.OrgID, targetSiteID, identifiers) + n, txErr := s.store.AssignDevicesToSite(txCtx, params.OrgID, targetSiteID, identifiers) if txErr != nil { return txErr } @@ -361,11 +412,26 @@ func (s *Service) ReassignDevicesToSite(ctx context.Context, params models.Reass return rowsAffected, nil, nil } -// AssignBuildingToSite moves a building to a different site (or to -// "Unassigned" when TargetSiteID is nil) and cascades site_id down to -// the building's racks and their devices in one transaction. Returns -// the cascade counts. -func (s *Service) AssignBuildingToSite(ctx context.Context, params models.AssignBuildingToSiteParams) (*models.AssignBuildingToSiteResult, error) { +// AssignBuildingsToSite moves one or more buildings to a target site +// (or to "Unassigned" when TargetSiteID is nil) and cascades site_id +// down to each building's racks and their devices. Everything runs in +// one transaction; if any building fails, the batch rolls back. +// Returns the aggregate cascade counts across every building. +func (s *Service) AssignBuildingsToSite(ctx context.Context, params models.AssignBuildingsToSiteParams) (*models.AssignBuildingsToSiteResult, error) { + buildingIDs := dedupeInt64s(params.BuildingIDs) + if len(buildingIDs) == 0 { + return nil, fleeterror.NewInvalidArgumentError("building_ids must not be empty") + } + // Reject an explicit target_site_id == 0 so callers don't confuse + // "Unassigned" (TargetSiteID == nil) with a zero-valued site row + // they forgot to populate. nil stays the sentinel for unassign. + if params.TargetSiteID != nil && *params.TargetSiteID == 0 { + return nil, fleeterror.NewInvalidArgumentError("target_site_id must be > 0 (use nil for Unassigned)") + } + // Sort for stable lock order: deadlock-safe against concurrent + // AssignBuildingsToSite touching an overlapping building set. + sort.Slice(buildingIDs, func(i, j int) bool { return buildingIDs[i] < buildingIDs[j] }) + var ( rackCount int64 deviceCount int64 @@ -381,27 +447,45 @@ func (s *Service) AssignBuildingToSite(ctx context.Context, params models.Assign return err } } - // Lock the building so a concurrent DeleteSite that owns the - // source site can't clear this building's racks while we - // reassign them. Same site→building lock order DeleteSite uses. - if err := s.store.LockBuildingForWrite(txCtx, params.OrgID, params.BuildingID); err != nil { - return err + // Phase A: sequential per-building lock acquisition in sorted + // order so a concurrent DeleteSite owning the source site can't + // clear racks under any of these buildings between our reads + // and writes. Locks must be acquired one-by-one — bulk lock + // acquisition would risk a deadlock against another + // AssignBuildingsToSite touching an overlapping building set. + for _, buildingID := range buildingIDs { + if err := s.store.LockBuildingForWrite(txCtx, params.OrgID, buildingID); err != nil { + return err + } } - rowsAffected, err := s.store.AssignBuildingToSite(txCtx, params.OrgID, params.BuildingID, params.TargetSiteID) + + // Phase B1: single bulk write moving every locked building to + // the target site. The row count tells us whether every + // requested building is live; mismatch surfaces as NotFound + // (matches the per-row check the old loop performed). + rowsAffected, err := s.store.AssignBuildingsToSiteBulk(txCtx, params.OrgID, buildingIDs, params.TargetSiteID) if err != nil { return err } - if rowsAffected == 0 { - return fleeterror.NewNotFoundErrorf("building %d not found", params.BuildingID) + if rowsAffected != int64(len(buildingIDs)) { + return fleeterror.NewNotFoundErrorf("one or more buildings not found (expected %d, updated %d)", len(buildingIDs), rowsAffected) } - rackCount, err = s.store.ReassignRacksUnderBuilding(txCtx, params.OrgID, params.BuildingID, params.TargetSiteID) + + // Phase B2: single bulk rack cascade across every building in + // the batch. + racks, err := s.store.ReassignRacksUnderBuildingsBulk(txCtx, params.OrgID, buildingIDs, params.TargetSiteID) if err != nil { return err } - deviceCount, err = s.store.ReassignDevicesUnderBuilding(txCtx, params.OrgID, params.BuildingID, params.TargetSiteID) + rackCount = racks + + // Phase B3: single bulk device cascade across every building + // in the batch. + devices, err := s.store.ReassignDevicesUnderBuildingsBulk(txCtx, params.OrgID, buildingIDs, params.TargetSiteID) if err != nil { return err } + deviceCount = devices return nil }) if err != nil { @@ -409,18 +493,17 @@ func (s *Service) AssignBuildingToSite(ctx context.Context, params models.Assign } orgIDVal := params.OrgID - buildingIDVal := params.BuildingID event := activitymodels.Event{ Category: activitymodels.CategoryFleetManagement, Type: eventBuildingAssignedToSite, OrganizationID: &orgIDVal, SiteID: params.TargetSiteID, Description: fmt.Sprintf( - "Assigned building %d to site %s (%d racks, %d devices cascaded)", - buildingIDVal, formatSiteIDForDescription(params.TargetSiteID), rackCount, deviceCount, + "Assigned %d building(s) to site %s (%d racks, %d devices cascaded)", + len(buildingIDs), formatSiteIDForDescription(params.TargetSiteID), rackCount, deviceCount, ), Metadata: map[string]any{ - "building_id": buildingIDVal, + "building_ids": buildingIDs, "target_site_id": params.TargetSiteID, "reassigned_rack_count": rackCount, "reassigned_device_count": deviceCount, @@ -429,12 +512,145 @@ func (s *Service) AssignBuildingToSite(ctx context.Context, params models.Assign activity.StampActor(ctx, &event) s.activitySvc.Log(ctx, event) - return &models.AssignBuildingToSiteResult{ + return &models.AssignBuildingsToSiteResult{ ReassignedRackCount: rackCount, ReassignedDeviceCount: deviceCount, }, nil } +// AssignRacksToSite moves one or more racks to a target site (or to +// "Unassigned" when TargetSiteID is nil) as a partial update — label, +// layout, members, and slot assignments stay untouched. building_id is +// auto-cleared on any site transition because a building belongs to a +// single site; the response carries the count of racks whose building +// was cleared so the UI can prompt the operator to pick a building in +// the new site. Same transaction cascades device.site_id for every +// rack member. +// +// Lock order: target site → each rack id ascending. Matches +// AssignBuildingsToSite + AssignDevicesToSite so concurrent site-scope +// writers can't deadlock against an overlapping rack set. +func (s *Service) AssignRacksToSite(ctx context.Context, params models.AssignRacksToSiteParams) (*models.AssignRacksToSiteResult, error) { + if s.collectionStore == nil { + return nil, fleeterror.NewInternalErrorf("collection store not configured") + } + rackIDs := dedupeInt64s(params.RackIDs) + if len(rackIDs) == 0 { + return nil, fleeterror.NewInvalidArgumentError("rack_ids must not be empty") + } + // Reject an explicit target_site_id == 0 so callers don't confuse + // "Unassigned" (TargetSiteID == nil) with a zero-valued site row + // they forgot to populate. nil stays the sentinel for unassign. + if params.TargetSiteID != nil && *params.TargetSiteID == 0 { + return nil, fleeterror.NewInvalidArgumentError("target_site_id must be > 0 (use nil for Unassigned)") + } + sort.Slice(rackIDs, func(i, j int) bool { return rackIDs[i] < rackIDs[j] }) + + var ( + deviceCount int64 + clearedCount int64 + cascadedRackIDs []int64 + ) + err := s.transactor.RunInTx(ctx, func(txCtx context.Context) error { + // Lock target site first if assigning so a concurrent + // DeleteSite can't soft-delete it between the check and the + // cascade writes. target=nil/0 (Unassigned) needs no lock. + if params.TargetSiteID != nil && *params.TargetSiteID > 0 { + if err := s.store.LockSiteForWrite(txCtx, params.OrgID, *params.TargetSiteID); err != nil { + return err + } + } + // Phase A: sequential per-rack lock acquisition in sorted order + // (deadlock-safe). Per-rack reads classify each rack into the + // "site changed" bucket (which drives both the SQL write set + // and the activity-log metadata) or the same-site no-op + // bucket. + var changedRackIDs []int64 + for _, rackID := range rackIDs { + current, err := s.collectionStore.LockRackPlacementForWrite(txCtx, rackID, params.OrgID) + if err != nil { + return err + } + siteChanged := !int64PtrEqual(current.SiteID, params.TargetSiteID) + if !siteChanged { + continue + } + changedRackIDs = append(changedRackIDs, rackID) + cascadedRackIDs = append(cascadedRackIDs, rackID) + if current.BuildingID != nil { + clearedCount++ + } + } + + // Phase B1: single bulk update for every rack whose site + // actually changes. building_id, zone, and grid placement + // clear together because a building is bound to one site and + // the partial unique index would otherwise leave stale cells + // behind. Skip the round-trip when no rack changes site. + if len(changedRackIDs) > 0 { + if err := s.collectionStore.UpdateRackPlacementBulkForSite( + txCtx, params.OrgID, changedRackIDs, params.TargetSiteID, + ); err != nil { + return err + } + + // Phase B2: single bulk cascade for the same set. + n, err := s.collectionStore.CascadeRackDeviceSitesBulk( + txCtx, params.OrgID, changedRackIDs, params.TargetSiteID, + ) + if err != nil { + return err + } + deviceCount += n + } + return nil + }) + if err != nil { + return nil, err + } + + orgIDVal := params.OrgID + event := activitymodels.Event{ + Category: activitymodels.CategoryFleetManagement, + Type: eventRacksAssignedToSite, + OrganizationID: &orgIDVal, + SiteID: params.TargetSiteID, + Description: fmt.Sprintf( + "Assigned %d rack(s) to site %s (%d devices cascaded, %d building(s) cleared)", + len(rackIDs), formatSiteIDForDescription(params.TargetSiteID), deviceCount, clearedCount, + ), + Metadata: map[string]any{ + "rack_ids": rackIDs, + "target_site_id": params.TargetSiteID, + "reassigned_device_count": deviceCount, + "cleared_building_count": clearedCount, + }, + } + if len(cascadedRackIDs) > 0 { + event.Metadata["site_cascaded_rack_ids"] = cascadedRackIDs + } + activity.StampActor(ctx, &event) + s.activitySvc.Log(ctx, event) + + return &models.AssignRacksToSiteResult{ + ReassignedDeviceCount: deviceCount, + ClearedBuildingCount: clearedCount, + }, nil +} + +// int64PtrEqual treats two *int64 as equal when both are nil or both +// dereference to the same value. Local helper since this is the only +// caller in the sites domain; collection.Service has its own copy. +func int64PtrEqual(a, b *int64) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return *a == *b +} + // --- helpers --- func (s *Service) computeReassignConflicts(ctx context.Context, orgID int64, targetSiteID *int64, identifiers []string) ([]models.PerDeviceConflict, error) { @@ -529,6 +745,19 @@ func dedupeStrings(in []string) []string { return out } +func dedupeInt64s(in []int64) []int64 { + seen := make(map[int64]struct{}, len(in)) + out := make([]int64, 0, len(in)) + for _, v := range in { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} + func formatSiteIDForDescription(target *int64) string { if target == nil { return "Unassigned" diff --git a/server/internal/domain/sites/service_stats_test.go b/server/internal/domain/sites/service_stats_test.go index 95e989bc7..dd14a1576 100644 --- a/server/internal/domain/sites/service_stats_test.go +++ b/server/internal/domain/sites/service_stats_test.go @@ -64,7 +64,7 @@ func TestGetSiteStats_notFoundWhenSiteMissing(t *testing.T) { store := mocks.NewMockSiteStore(ctrl) store.EXPECT().SiteBelongsToOrg(gomock.Any(), testOrgID, int64(99)).Return(false, nil) - svc := NewService(store, mocks.NewMockBuildingStore(ctrl), &fakeDeviceQueryer{}, &fakeTelemetryCollector{}, &fakeTransactor{}, nil) + svc := NewService(store, mocks.NewMockBuildingStore(ctrl), nil, &fakeDeviceQueryer{}, &fakeTelemetryCollector{}, &fakeTransactor{}, nil) _, err := svc.GetSiteStats(context.Background(), testOrgID, 99) var fe fleeterror.FleetError if !errors.As(err, &fe) || fe.GRPCCode != connect.CodeNotFound { @@ -108,7 +108,7 @@ func TestGetSiteStats_rollsUpEverything(t *testing.T) { }, } - svc := NewService(store, buildingStore, devices, telemetry, &fakeTransactor{}, nil) + svc := NewService(store, buildingStore, nil, devices, telemetry, &fakeTransactor{}, nil) stats, err := svc.GetSiteStats(context.Background(), testOrgID, 1) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -150,7 +150,7 @@ func TestGetSiteStats_includesAuthNeededInFilter(t *testing.T) { buildingStore.EXPECT().ListBuildings(gomock.Any(), gomock.Any()).Return(nil, nil) devices := &fakeDeviceQueryer{deviceIDs: nil} // no devices → telemetry not called - svc := NewService(store, buildingStore, devices, &fakeTelemetryCollector{}, &fakeTransactor{}, nil) + svc := NewService(store, buildingStore, nil, devices, &fakeTelemetryCollector{}, &fakeTransactor{}, nil) _, err := svc.GetSiteStats(context.Background(), testOrgID, 1) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -194,7 +194,7 @@ func TestGetSiteStats_failsFastOverCap(t *testing.T) { telemetry := &fakeTelemetryCollector{err: errors.New("should not be called")} devices := &fakeDeviceQueryer{deviceIDs: overCap} - svc := NewService(store, buildingStore, devices, telemetry, &fakeTransactor{}, nil) + svc := NewService(store, buildingStore, nil, devices, telemetry, &fakeTransactor{}, nil) _, err := svc.GetSiteStats(context.Background(), testOrgID, 1) var fe fleeterror.FleetError if !errors.As(err, &fe) || fe.GRPCCode != connect.CodeInternal { @@ -214,7 +214,7 @@ func TestGetSiteStats_emptyDevicesShortCircuits(t *testing.T) { telemetry := &fakeTelemetryCollector{err: errors.New("should not be called")} devices := &fakeDeviceQueryer{deviceIDs: nil} - svc := NewService(store, buildingStore, devices, telemetry, &fakeTransactor{}, nil) + svc := NewService(store, buildingStore, nil, devices, telemetry, &fakeTransactor{}, nil) stats, err := svc.GetSiteStats(context.Background(), testOrgID, 1) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -228,7 +228,7 @@ func TestGetSiteStats_internalErrorWhenStatsDepsMissing(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) // Nil deviceQueryer should short-circuit before any store call. - svc := NewService(store, nil, nil, nil, &fakeTransactor{}, nil) + svc := NewService(store, nil, nil, nil, nil, &fakeTransactor{}, nil) _, err := svc.GetSiteStats(context.Background(), testOrgID, 1) var fe fleeterror.FleetError if !errors.As(err, &fe) || fe.GRPCCode != connect.CodeInternal { diff --git a/server/internal/domain/sites/service_test.go b/server/internal/domain/sites/service_test.go index d963da205..583429ea6 100644 --- a/server/internal/domain/sites/service_test.go +++ b/server/internal/domain/sites/service_test.go @@ -109,7 +109,7 @@ func TestDeleteSite_cascadeInOneTransaction(t *testing.T) { tx := &fakeTransactor{} // activitySvc is nil; the service's logActivity is nil-safe. Production // wires a real *activity.Service from main.go. - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) gomock.InOrder( store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, int64(11)).Return(nil), @@ -142,7 +142,7 @@ func TestDeleteSite_notFoundWhenSoftDeleteAffectsZeroRows(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) // LockSiteForWrite succeeds (row exists at start of tx) but the // final SoftDeleteSite affects 0 rows because nothing matched the @@ -163,7 +163,7 @@ func TestDeleteSite_notFoundWhenSoftDeleteAffectsZeroRows(t *testing.T) { } } -func TestReassignDevicesToSite_rejectsCrossCollectionConflict(t *testing.T) { +func TestAssignDevicesToSite_rejectsCrossCollectionConflict(t *testing.T) { // Table-driven across both transactors: the production-shaped // wrapping transactor pins Fix #1 (a sentinel error would get // wrapped as Internal in prod, breaking the conflict path). @@ -172,7 +172,7 @@ func TestReassignDevicesToSite_rejectsCrossCollectionConflict(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := tf.make() - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) identifiers := []string{"d1", "d2"} target := int64(20) @@ -190,7 +190,7 @@ func TestReassignDevicesToSite_rejectsCrossCollectionConflict(t *testing.T) { }, nil) // No update call — entire batch rejected. - count, conflicts, err := svc.ReassignDevicesToSite(context.Background(), models.ReassignDevicesToSiteParams{ + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ OrgID: testOrgID, TargetSiteID: &target, DeviceIdentifiers: identifiers, @@ -217,11 +217,11 @@ func TestReassignDevicesToSite_rejectsCrossCollectionConflict(t *testing.T) { } } -func TestReassignDevicesToSite_reportsMissingDevices(t *testing.T) { +func TestAssignDevicesToSite_reportsMissingDevices(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) identifiers := []string{"d1", "d-missing"} target := int64(20) @@ -232,7 +232,7 @@ func TestReassignDevicesToSite_reportsMissingDevices(t *testing.T) { store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return([]string{"d1"}, nil) store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{}, nil) - _, conflicts, err := svc.ReassignDevicesToSite(context.Background(), models.ReassignDevicesToSiteParams{ + _, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ OrgID: testOrgID, TargetSiteID: &target, DeviceIdentifiers: identifiers, @@ -248,11 +248,11 @@ func TestReassignDevicesToSite_reportsMissingDevices(t *testing.T) { } } -func TestReassignDevicesToSite_writesOnSuccess(t *testing.T) { +func TestAssignDevicesToSite_writesOnSuccess(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) identifiers := []string{"d1", "d2"} target := int64(20) @@ -262,9 +262,9 @@ func TestReassignDevicesToSite_writesOnSuccess(t *testing.T) { store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{}, nil) - store.EXPECT().ReassignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(2), nil) + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(2), nil) - count, conflicts, err := svc.ReassignDevicesToSite(context.Background(), models.ReassignDevicesToSiteParams{ + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ OrgID: testOrgID, TargetSiteID: &target, DeviceIdentifiers: identifiers, @@ -283,11 +283,11 @@ func TestReassignDevicesToSite_writesOnSuccess(t *testing.T) { } } -func TestReassignDevicesToSite_unassignedTargetSkipsBelongsCheck(t *testing.T) { +func TestAssignDevicesToSite_unassignedTargetSkipsBelongsCheck(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) identifiers := []string{"d1"} @@ -296,9 +296,9 @@ func TestReassignDevicesToSite_unassignedTargetSkipsBelongsCheck(t *testing.T) { store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{}, nil) - store.EXPECT().ReassignDevicesToSite(inTxCtx, testOrgID, gomock.Nil(), identifiers).Return(int64(1), nil) + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.Nil(), identifiers).Return(int64(1), nil) - _, _, err := svc.ReassignDevicesToSite(context.Background(), models.ReassignDevicesToSiteParams{ + _, _, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ OrgID: testOrgID, TargetSiteID: nil, DeviceIdentifiers: identifiers, @@ -308,11 +308,11 @@ func TestReassignDevicesToSite_unassignedTargetSkipsBelongsCheck(t *testing.T) { } } -func TestReassignDevicesToSite_targetMatchesCurrentRackSiteIsNotAConflict(t *testing.T) { +func TestAssignDevicesToSite_targetMatchesCurrentRackSiteIsNotAConflict(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) identifiers := []string{"d1"} target := int64(42) @@ -324,9 +324,9 @@ func TestReassignDevicesToSite_targetMatchesCurrentRackSiteIsNotAConflict(t *tes store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{ "d1": target, }, nil) - store.EXPECT().ReassignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(1), nil) + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(1), nil) - _, conflicts, err := svc.ReassignDevicesToSite(context.Background(), models.ReassignDevicesToSiteParams{ + _, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ OrgID: testOrgID, TargetSiteID: &target, DeviceIdentifiers: identifiers, @@ -339,11 +339,265 @@ func TestReassignDevicesToSite_targetMatchesCurrentRackSiteIsNotAConflict(t *tes } } -func TestAssignBuildingToSite_cascadeOnSuccess(t *testing.T) { +// TestAssignDevicesToSite_forceClearCascadesRackMembership pins the +// cross-site reparent path: when the caller passes the force-clear +// flag and devices live in racks at other sites, the service drops +// every rack membership for those devices inside the same tx and +// then applies the site write. No conflicts surface to the caller. +func TestAssignDevicesToSite_forceClearCascadesRackMembership(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + identifiers := []string{"d1", "d2"} + target := int64(20) + conflictingSite := int64(30) + + store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) + store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{ + "d1": conflictingSite, + }, nil) + // Only d1 had a rack-at-other-site conflict, so only d1's rack + // memberships get cleared. d2 (no conflict, already at target) + // keeps its rack row so its rack_slot child isn't cascade-dropped. + // targetRackID=0 means "exclude nothing" — drop every rack row + // for the listed devices. + collStore.EXPECT().RemoveDevicesFromAnyRack(inTxCtx, testOrgID, []string{"d1"}, int64(0)).Return(int64(1), nil) + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(2), nil) + + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ + OrgID: testOrgID, + TargetSiteID: &target, + DeviceIdentifiers: identifiers, + ForceClearConflictingRackMembership: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if count != 2 { + t.Fatalf("expected 2 rows updated, got %d", count) + } + if len(conflicts) != 0 { + t.Fatalf("expected zero conflicts on cascade-clear success, got %v", conflicts) + } + if tx.calls != 1 { + t.Fatalf("expected one tx run, got %d", tx.calls) + } +} + +// TestAssignDevicesToSite_forceClearWithoutConflicts is the no-op +// branch: when the flag is true but nothing conflicts, the service +// must not call RemoveDevicesFromAnyRack — the cascade clear is only +// for the rack-at-other-site case. +func TestAssignDevicesToSite_forceClearWithoutConflicts(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + identifiers := []string{"d1"} + target := int64(20) + + store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) + store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{}, nil) + // No RemoveDevicesFromAnyRack expectation: gomock fails the test + // if it's called. + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(1), nil) + + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ + OrgID: testOrgID, + TargetSiteID: &target, + DeviceIdentifiers: identifiers, + ForceClearConflictingRackMembership: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if count != 1 { + t.Fatalf("expected 1 row updated, got %d", count) + } + if len(conflicts) != 0 { + t.Fatalf("expected zero conflicts, got %v", conflicts) + } +} + +// TestAssignDevicesToSite_forceClearMissingDeviceStillRejects covers +// the partial-conflict case: cascade-clear handles the rack-site +// mismatch, but a DEVICE_NOT_FOUND still aborts the batch (you can't +// move a device that doesn't exist). The store's AssignDevicesToSite +// must NOT be called when at least one device-not-found conflict +// remains. +func TestAssignDevicesToSite_forceClearMissingDeviceStillRejects(t *testing.T) { + for _, tf := range transactorFactories { + t.Run(tf.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := tf.make() + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + identifiers := []string{"d1", "d-missing"} + target := int64(20) + conflictingSite := int64(30) + + store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + // Only d1 exists; d-missing is reported as not found. + store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return([]string{"d1"}, nil) + store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{ + "d1": conflictingSite, + }, nil) + // Residual DEVICE_NOT_FOUND aborts the tx BEFORE any + // deletion runs. Otherwise the cascade-clear delete would + // commit and the tx would still return without the site + // move, leaving d1 rack-stripped on its old site. + // No RemoveDevicesFromAnyRack expectation: gomock fails + // the test if it's called. + // No AssignDevicesToSite expectation: batch must reject. + + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ + OrgID: testOrgID, + TargetSiteID: &target, + DeviceIdentifiers: identifiers, + ForceClearConflictingRackMembership: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if count != 0 { + t.Fatalf("expected zero rows on rejection, got %d", count) + } + if len(conflicts) != 1 { + t.Fatalf("expected one remaining conflict, got %d", len(conflicts)) + } + if conflicts[0].Reason != models.ReasonDeviceNotFound { + t.Fatalf("expected ReasonDeviceNotFound, got %v", conflicts[0].Reason) + } + }) + } +} + +// TestAssignDevicesToSite_forceClearRollsBackOnSiteWriteFailure pins +// the rollback contract: if AssignDevicesToSite fails after the +// cascade clear, the whole tx aborts — the cascade-clear write is +// undone alongside the failed site write. The wrappingFakeTransactor +// also pins that a non-FleetError surfaces as Internal in prod. +func TestAssignDevicesToSite_forceClearRollsBackOnSiteWriteFailure(t *testing.T) { + for _, tf := range transactorFactories { + t.Run(tf.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := tf.make() + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + identifiers := []string{"d1"} + target := int64(20) + conflictingSite := int64(30) + sentinel := errors.New("site write boom") + + store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) + store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{ + "d1": conflictingSite, + }, nil) + collStore.EXPECT().RemoveDevicesFromAnyRack(inTxCtx, testOrgID, identifiers, int64(0)).Return(int64(1), nil) + // Site write fails. The transactor returns the error, which + // rolls back the cascade clear write that just happened. + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(0), sentinel) + + _, _, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ + OrgID: testOrgID, + TargetSiteID: &target, + DeviceIdentifiers: identifiers, + ForceClearConflictingRackMembership: true, + }) + if err == nil { + t.Fatalf("expected error, got nil") + } + // Exactly one tx attempt — RunInTx surfaces the error so the + // outer caller sees the failure path. No retries are wired + // into either fake transactor. + switch ttx := tx.(type) { + case *fakeTransactor: + if ttx.calls != 1 { + t.Fatalf("expected one tx run, got %d", ttx.calls) + } + case *wrappingFakeTransactor: + if ttx.calls != 1 { + t.Fatalf("expected one tx run, got %d", ttx.calls) + } + default: + t.Fatalf("unexpected transactor type %T", tx) + } + }) + } +} + +// TestAssignDevicesToSite_forceClearOnlyConflictingDevices pins the +// scoped-clear contract: when only a subset of devices have a +// rack-at-other-site conflict, RemoveDevicesFromAnyRack is called +// only with the conflicting identifiers. Devices already at the +// target site (no conflict) keep their rack rows so their rack_slot +// children aren't cascade-dropped. Regression test for codex PR +// review (issue-420): the original implementation passed the full +// identifier list, over-deleting unrelated rack memberships. +func TestAssignDevicesToSite_forceClearOnlyConflictingDevices(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + identifiers := []string{"d-conflict", "d-already-here"} + target := int64(20) + conflictingSite := int64(30) + + store.EXPECT().LockDevicesForReassign(inTxCtx, testOrgID, identifiers).Return(nil) + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + store.EXPECT().ListExistingDeviceIdentifiers(inTxCtx, testOrgID, identifiers).Return(identifiers, nil) + // Only d-conflict is at a different site; d-already-here returns + // no rack-site row (already at target, would be filtered by the + // target==site equality check anyway). + store.EXPECT().FindDeviceSiteConflicts(inTxCtx, testOrgID, identifiers).Return(map[string]int64{ + "d-conflict": conflictingSite, + }, nil) + // Critical: only d-conflict's rack rows get dropped. Passing + // the full identifier list here would delete d-already-here's + // rack membership and cascade its rack_slot row. + collStore.EXPECT().RemoveDevicesFromAnyRack(inTxCtx, testOrgID, []string{"d-conflict"}, int64(0)).Return(int64(1), nil) + store.EXPECT().AssignDevicesToSite(inTxCtx, testOrgID, gomock.AssignableToTypeOf(ptrInt64(0)), identifiers).Return(int64(2), nil) + + count, conflicts, err := svc.AssignDevicesToSite(context.Background(), models.AssignDevicesToSiteParams{ + OrgID: testOrgID, + TargetSiteID: &target, + DeviceIdentifiers: identifiers, + ForceClearConflictingRackMembership: true, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if count != 2 { + t.Fatalf("expected 2 rows updated, got %d", count) + } + if len(conflicts) != 0 { + t.Fatalf("expected zero conflicts after scoped clear, got %v", conflicts) + } +} + +func TestAssignBuildingsToSite_cascadeOnSuccess(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, nil, nil, nil, tx, nil) target := int64(20) // The TOCTOU fix moved the site-alive check inside the tx and @@ -353,13 +607,13 @@ func TestAssignBuildingToSite_cascadeOnSuccess(t *testing.T) { // LockBuildingsBySiteForWrite for the source-site race. store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) store.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, int64(50)).Return(nil) - store.EXPECT().AssignBuildingToSite(inTxCtx, testOrgID, int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(1), nil) - store.EXPECT().ReassignRacksUnderBuilding(inTxCtx, testOrgID, int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(3), nil) - store.EXPECT().ReassignDevicesUnderBuilding(inTxCtx, testOrgID, int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(15), nil) + store.EXPECT().AssignBuildingsToSiteBulk(inTxCtx, testOrgID, []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(1), nil) + store.EXPECT().ReassignRacksUnderBuildingsBulk(inTxCtx, testOrgID, []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(3), nil) + store.EXPECT().ReassignDevicesUnderBuildingsBulk(inTxCtx, testOrgID, []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(15), nil) - out, err := svc.AssignBuildingToSite(context.Background(), models.AssignBuildingToSiteParams{ + out, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ OrgID: testOrgID, - BuildingID: 50, + BuildingIDs: []int64{50}, TargetSiteID: &target, }) if err != nil { @@ -370,11 +624,11 @@ func TestAssignBuildingToSite_cascadeOnSuccess(t *testing.T) { } } -func TestAssignBuildingToSite_notFoundWhenBuildingMissing(t *testing.T) { +func TestAssignBuildingsToSite_notFoundWhenBuildingMissing(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) target := int64(20) // Both calls now live inside the tx after the TOCTOU fix. With the @@ -385,9 +639,9 @@ func TestAssignBuildingToSite_notFoundWhenBuildingMissing(t *testing.T) { store.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, int64(50)). Return(fleeterror.NewNotFoundErrorf("building %d not found", 50)) - _, err := svc.AssignBuildingToSite(context.Background(), models.AssignBuildingToSiteParams{ + _, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ OrgID: testOrgID, - BuildingID: 50, + BuildingIDs: []int64{50}, TargetSiteID: &target, }) if !fleeterror.IsNotFoundError(err) { @@ -395,11 +649,39 @@ func TestAssignBuildingToSite_notFoundWhenBuildingMissing(t *testing.T) { } } +func TestAssignBuildingsToSite_bulkRollsBackOnLaterFailure(t *testing.T) { + // First building succeeds, second fails — tx must roll back, no + // AssignBuildingToSite call on the second building's cascade phase. + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, nil, nil, nil, tx, nil) + + target := int64(20) + // All per-building locks run in Phase A in sorted order. The second + // building's lock errors so no bulk write fires (cleaner mock log + // than the pre-refactor per-building loop, where the first building + // went through update+cascade before the second's lock failed). + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + store.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, int64(50)).Return(nil) + store.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, int64(51)). + Return(fleeterror.NewNotFoundErrorf("building %d not found", 51)) + + _, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ + OrgID: testOrgID, + BuildingIDs: []int64{50, 51}, + TargetSiteID: &target, + }) + if !fleeterror.IsNotFoundError(err) { + t.Fatalf("expected NotFound for second building, got %v", err) + } +} + func TestCreateSite_invalidNetworkConfigBlocksWrite(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) // CreateSite must NOT be called when network_config validation fails. _, err := svc.CreateSite(context.Background(), models.CreateSiteParams{ @@ -416,7 +698,7 @@ func TestCreateSite_canonicalizesAndPersists(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) store.EXPECT().ListAllSiteNetworkConfigs(gomock.Any(), testOrgID, int64(0)).Return(nil, nil) store.EXPECT().CreateSite(gomock.Any(), gomock.AssignableToTypeOf(models.CreateSiteParams{})). @@ -444,7 +726,7 @@ func TestCreateSite_crossSiteOverlapSurfacesAsWarning(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) store.EXPECT().ListAllSiteNetworkConfigs(gomock.Any(), testOrgID, int64(0)).Return([]models.SiteNetworkConfigEntry{ {ID: 99, Name: "siteB", NetworkConfig: "10.0.0.0/22"}, @@ -468,7 +750,7 @@ func TestUpdateSite_canonicalizesAndPersists(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) store.EXPECT().ListAllSiteNetworkConfigs(gomock.Any(), testOrgID, int64(11)).Return(nil, nil) store.EXPECT().UpdateSite(gomock.Any(), gomock.AssignableToTypeOf(models.UpdateSiteParams{})). @@ -497,7 +779,7 @@ func TestUpdateSite_excludesSelfFromOverlapWarnings(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) store.EXPECT().ListAllSiteNetworkConfigs(gomock.Any(), testOrgID, int64(11)).Return(nil, nil) store.EXPECT().UpdateSite(gomock.Any(), gomock.Any()).Return(&models.Site{ID: 11}, nil) @@ -520,7 +802,7 @@ func TestUpdateSite_overlapWithDifferentSiteSurfacesWarning(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) store.EXPECT().ListAllSiteNetworkConfigs(gomock.Any(), testOrgID, int64(11)).Return([]models.SiteNetworkConfigEntry{ {ID: 99, Name: "siteB", NetworkConfig: "10.0.0.0/22"}, @@ -545,7 +827,7 @@ func TestUpdateSite_invalidNetworkConfigBlocksWrite(t *testing.T) { ctrl := gomock.NewController(t) store := mocks.NewMockSiteStore(ctrl) tx := &fakeTransactor{} - svc := NewService(store, nil, nil, nil, tx, nil) + svc := NewService(store, nil, nil, nil, nil, tx, nil) // UpdateSite must NOT be called when validation fails. _, err := svc.UpdateSite(context.Background(), models.UpdateSiteParams{ @@ -558,3 +840,324 @@ func TestUpdateSite_invalidNetworkConfigBlocksWrite(t *testing.T) { t.Fatal("expected validation error, got nil") } } + +func TestAssignRacksToSite_emptyRejected(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, mocks.NewMockCollectionStore(ctrl), nil, nil, tx, nil) + + _, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: nil, + }) + if err == nil { + t.Fatal("expected InvalidArgument for empty rack_ids, got nil") + } +} + +func TestAssignBuildingsToSite_emptyRejected(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, mocks.NewMockCollectionStore(ctrl), nil, nil, tx, nil) + + _, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ + OrgID: testOrgID, + BuildingIDs: nil, + }) + if err == nil { + t.Fatal("expected InvalidArgument for empty building_ids, got nil") + } +} + +func TestAssignRacksToSite_nilCollectionStoreRejected(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, nil, nil, nil, tx, nil) + + _, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{1}, + }) + if err == nil { + t.Fatal("expected internal error when collection store is unconfigured") + } +} + +// TestAssignRacksToSite_clearsBuildingOnSiteChange covers the cascade +// happy path: a rack moves to a new site, so building_id + zone must +// clear, clearedCount increments, and CascadeRackDeviceSites fires. +func TestAssignRacksToSite_clearsBuildingOnSiteChange(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + rackID := int64(100) + oldSite := int64(1) + oldBuilding := int64(50) + newSite := int64(2) + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, newSite).Return(nil) + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). + Return(interfaces.RackPlacement{SiteID: &oldSite, BuildingID: &oldBuilding, Zone: "Z"}, nil) + // Bulk placement update writes site + clears building/zone in one + // statement. Bulk cascade follows for the same set. + collStore.EXPECT(). + UpdateRackPlacementBulkForSite(inTxCtx, testOrgID, []int64{rackID}, &newSite). + Return(nil) + collStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, []int64{rackID}, &newSite).Return(int64(4), nil) + _ = oldBuilding + + out, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{rackID}, + TargetSiteID: &newSite, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ReassignedDeviceCount != 4 { + t.Fatalf("expected ReassignedDeviceCount=4, got %d", out.ReassignedDeviceCount) + } + if out.ClearedBuildingCount != 1 { + t.Fatalf("expected ClearedBuildingCount=1, got %d", out.ClearedBuildingCount) + } +} + +// TestAssignRacksToSite_sameSiteIsNoop covers the no-op branch: a rack +// already on the target site stays intact — no cascade, no clear. +func TestAssignRacksToSite_sameSiteIsNoop(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + rackID := int64(100) + site := int64(7) + building := int64(50) + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, site).Return(nil) + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). + Return(interfaces.RackPlacement{SiteID: &site, BuildingID: &building, Zone: "Z"}, nil) + // Site unchanged → rack is filtered out of the bulk write set; no + // UpdateRackPlacementBulkForSite or CascadeRackDeviceSitesBulk call + // fires. + _ = building + + out, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{rackID}, + TargetSiteID: &site, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ReassignedDeviceCount != 0 || out.ClearedBuildingCount != 0 { + t.Fatalf("expected zero cascade counts, got %+v", out) + } +} + +// TestAssignRacksToSite_noPriorBuildingStaysIntact ensures the +// clearedCount only ticks when there *was* a building to clear. +func TestAssignRacksToSite_noPriorBuildingStaysIntact(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + rackID := int64(100) + oldSite := int64(1) + newSite := int64(2) + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, newSite).Return(nil) + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rackID, testOrgID). + Return(interfaces.RackPlacement{SiteID: &oldSite, BuildingID: nil, Zone: ""}, nil) + collStore.EXPECT(). + UpdateRackPlacementBulkForSite(inTxCtx, testOrgID, []int64{rackID}, &newSite). + Return(nil) + collStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, []int64{rackID}, &newSite).Return(int64(0), nil) + + out, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{rackID}, + TargetSiteID: &newSite, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ClearedBuildingCount != 0 { + t.Fatalf("expected ClearedBuildingCount=0 (no prior building), got %d", out.ClearedBuildingCount) + } +} + +// TestAssignRacksToSite_bulkRollsBackOnLaterFailure mirrors the +// building-batch rollback case: first rack succeeds, second fails on +// the lock, the tx aborts, and the wrapping transactor records exactly +// one closure run with the error propagating up. +func TestAssignRacksToSite_bulkRollsBackOnLaterFailure(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + oldSite := int64(1) + newSite := int64(2) + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, newSite).Return(nil) + // Phase A locks both racks in sorted order. The second lock errors + // so the closure aborts before any bulk write fires. + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, int64(100), testOrgID). + Return(interfaces.RackPlacement{SiteID: &oldSite}, nil) + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, int64(101), testOrgID). + Return(interfaces.RackPlacement{}, fleeterror.NewNotFoundErrorf("rack %d not found", 101)) + + _, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{100, 101}, + TargetSiteID: &newSite, + }) + if !fleeterror.IsNotFoundError(err) { + t.Fatalf("expected NotFound, got %v", err) + } + if tx.calls != 1 { + t.Fatalf("expected exactly 1 tx closure run, got %d", tx.calls) + } +} + +// TestAssignBuildingsToSite_largeBatchIssuesSingleBulkWrites guards +// the F13 bulk refactor: a 100-building batch must produce exactly one +// AssignBuildingsToSiteBulk + one ReassignRacksUnderBuildingsBulk + +// one ReassignDevicesUnderBuildingsBulk call regardless of N. Per- +// building lock acquisitions stay sequential for deadlock safety. +func TestAssignBuildingsToSite_largeBatchIssuesSingleBulkWrites(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, nil, nil, nil, tx, nil) + + const N = 100 + target := int64(20) + buildingIDs := make([]int64, N) + for i := 0; i < N; i++ { + buildingIDs[i] = int64(1000 + i) + } + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, target).Return(nil) + for _, bid := range buildingIDs { + store.EXPECT().LockBuildingForWrite(inTxCtx, testOrgID, bid).Return(nil) + } + store.EXPECT().AssignBuildingsToSiteBulk(inTxCtx, testOrgID, buildingIDs, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(N), nil) + store.EXPECT().ReassignRacksUnderBuildingsBulk(inTxCtx, testOrgID, buildingIDs, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(300), nil) + store.EXPECT().ReassignDevicesUnderBuildingsBulk(inTxCtx, testOrgID, buildingIDs, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(2000), nil) + + out, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ + OrgID: testOrgID, + BuildingIDs: buildingIDs, + TargetSiteID: &target, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ReassignedRackCount != 300 || out.ReassignedDeviceCount != 2000 { + t.Fatalf("unexpected cascade counts: %+v", out) + } + if tx.calls != 1 { + t.Fatalf("expected one tx closure run, got %d", tx.calls) + } +} + +// TestAssignRacksToSite_largeBatchIssuesSingleBulkWrites guards the +// F14 bulk refactor: a 100-rack site-move batch must produce at most +// one UpdateRackPlacementBulkForSite + one CascadeRackDeviceSitesBulk +// call regardless of N, after the per-rack sequential lock phase. +func TestAssignRacksToSite_largeBatchIssuesSingleBulkWrites(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + collStore := mocks.NewMockCollectionStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, collStore, nil, nil, tx, nil) + + const N = 100 + oldSite := int64(9) + newSite := int64(20) + rackIDs := make([]int64, N) + for i := 0; i < N; i++ { + rackIDs[i] = int64(1000 + i) + } + + store.EXPECT().LockSiteForWrite(inTxCtx, testOrgID, newSite).Return(nil) + for _, rid := range rackIDs { + collStore.EXPECT().LockRackPlacementForWrite(inTxCtx, rid, testOrgID). + Return(interfaces.RackPlacement{SiteID: &oldSite}, nil) + } + // Exactly one bulk placement update + one bulk cascade. + collStore.EXPECT().UpdateRackPlacementBulkForSite(inTxCtx, testOrgID, rackIDs, &newSite).Return(nil) + collStore.EXPECT().CascadeRackDeviceSitesBulk(inTxCtx, testOrgID, rackIDs, &newSite).Return(int64(500), nil) + + out, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: rackIDs, + TargetSiteID: &newSite, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ReassignedDeviceCount != 500 { + t.Fatalf("unexpected cascade count: %d", out.ReassignedDeviceCount) + } + if tx.calls != 1 { + t.Fatalf("expected one tx closure run, got %d", tx.calls) + } +} + +// TestAssignBuildingsToSite_rejectsZeroTargetSite guards F12: an +// explicit *TargetSiteID == 0 must return InvalidArgument so callers +// can't confuse "Unassigned" (nil) with a zero-valued site. +func TestAssignBuildingsToSite_rejectsZeroTargetSite(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, mocks.NewMockCollectionStore(ctrl), nil, nil, tx, nil) + + zero := int64(0) + _, err := svc.AssignBuildingsToSite(context.Background(), models.AssignBuildingsToSiteParams{ + OrgID: testOrgID, + BuildingIDs: []int64{1}, + TargetSiteID: &zero, + }) + if err == nil { + t.Fatal("expected InvalidArgument for target_site_id=0, got nil") + } + if tx.calls != 0 { + t.Fatalf("guard must reject before opening tx, got %d", tx.calls) + } +} + +// TestAssignRacksToSite_rejectsZeroTargetSite covers the matching F12 +// guard on the rack-batch RPC. +func TestAssignRacksToSite_rejectsZeroTargetSite(t *testing.T) { + ctrl := gomock.NewController(t) + store := mocks.NewMockSiteStore(ctrl) + tx := &fakeTransactor{} + svc := NewService(store, nil, mocks.NewMockCollectionStore(ctrl), nil, nil, tx, nil) + + zero := int64(0) + _, err := svc.AssignRacksToSite(context.Background(), models.AssignRacksToSiteParams{ + OrgID: testOrgID, + RackIDs: []int64{1}, + TargetSiteID: &zero, + }) + if err == nil { + t.Fatal("expected InvalidArgument for target_site_id=0, got nil") + } + if tx.calls != 0 { + t.Fatalf("guard must reject before opening tx, got %d", tx.calls) + } +} diff --git a/server/internal/domain/stores/interfaces/building.go b/server/internal/domain/stores/interfaces/building.go index 13f655efe..a9a5089c0 100644 --- a/server/internal/domain/stores/interfaces/building.go +++ b/server/internal/domain/stores/interfaces/building.go @@ -28,7 +28,7 @@ type BuildingStore interface { ListBuildings(ctx context.Context, filter models.ListFilter) ([]models.BuildingWithCounts, error) // UpdateBuilding mutates the row's mutable fields (excluding - // site_id — that lives on SiteService.AssignBuildingToSite for + // site_id — that lives on SiteService.AssignBuildingsToSite for // cross-collection enforcement). Returns NotFound when row gone. UpdateBuilding(ctx context.Context, params models.UpdateParams) (*models.Building, error) @@ -72,4 +72,15 @@ type BuildingStore interface { // building_id via the collection store's UpdateRackPlacement in // the same transaction. SetRackBuildingPosition(ctx context.Context, orgID, rackID int64, aisleIndex, positionInAisle *int32) error + + // SetRackBuildingPositionBulkClear nulls (aisle_index, + // position_in_aisle) for every rack in rackIDs in one statement. + // Used by AssignRacksToBuilding's pass-1 vacate. + SetRackBuildingPositionBulkClear(ctx context.Context, orgID int64, rackIDs []int64) error + + // SetRackBuildingPositionBulkPlace writes per-rack + // (aisleIndexes[i], positionInAisles[i]) for every rack in + // rackIDs (parallel-aligned arrays). Used by + // AssignRacksToBuilding's pass-2 after pass-1 cleared cells. + SetRackBuildingPositionBulkPlace(ctx context.Context, orgID int64, rackIDs []int64, aisleIndexes, positionInAisles []int32) error } diff --git a/server/internal/domain/stores/interfaces/collection.go b/server/internal/domain/stores/interfaces/collection.go index 66956afd0..07855d51f 100644 --- a/server/internal/domain/stores/interfaces/collection.go +++ b/server/internal/domain/stores/interfaces/collection.go @@ -100,6 +100,24 @@ type CollectionStore interface { // atomically. UpdateRackPlacement(ctx context.Context, collectionID, orgID int64, siteID, buildingID *int64, zone string) error + // UpdateRackPlacementBulkForBuilding writes site_id, building_id, + // and zone in one statement for every rack in rackIDs. Semantics + // mirror the per-row UpdateRackPlacement with the + // AssignRacksToBuilding-specific rules in SQL: + // * targetBuildingID == nil keeps each rack's current site_id. + // * Zone clears to '' for racks transitioning to a different (or + // NULL) building; preserved otherwise. + // * Grid position clears when building_id changes. + // Caller is expected to have locked every rack via + // LockRackPlacementForWrite before invoking. + UpdateRackPlacementBulkForBuilding(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID, targetBuildingID *int64) error + + // UpdateRackPlacementBulkForSite stamps every rack in rackIDs with + // the target site, clears building_id + zone + grid placement. + // Caller is expected to pass only racks whose site is actually + // changing. + UpdateRackPlacementBulkForSite(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) error + // UnassignDeviceSitesByRack nulls device.site_id for paired rack // members that match the rack's stamped site. No-op when the rack // has no site or no members. @@ -109,6 +127,11 @@ type CollectionStore interface { // rack members where the value differs. Returns the affected count. CascadeRackDeviceSites(ctx context.Context, collectionID, orgID int64, targetSiteID *int64) (int64, error) + // CascadeRackDeviceSitesBulk is the multi-rack variant: rewrites + // device.site_id to targetSiteID for every paired member of every + // rack in rackIDs where the current value differs. + CascadeRackDeviceSitesBulk(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) (int64, error) + // GetDeviceSiteIDsByMembership returns device_identifier + current // site_id for every rack member. GetDeviceSiteIDsByMembership(ctx context.Context, collectionID, orgID int64) (map[string]*int64, error) @@ -169,6 +192,34 @@ type CollectionStore interface { // Returns the number of devices actually removed. RemoveDevicesFromCollection(ctx context.Context, orgID int64, collectionID int64, deviceIdentifiers []string) (int64, error) + // RemoveDevicesFromAnyRack deletes the given devices' rack + // membership rows regardless of which rack they currently sit in, + // EXCEPT the target rack (targetRackID). Used by AssignDevicesToRack + // to clear prior rack membership inside the same transaction as the + // new-rack insert, closing the orphan window the client-side + // RemoveDevicesFromDeviceSet + AddDevicesToDeviceSet orchestration + // had. Excluding targetRackID preserves the membership row (and its + // rack_slot child) for devices already in the target rack -- a + // re-add inside the same transaction would silently drop the + // rack_slot via the FK cascade. Pass 0 to clear unconditionally + // (caller intends to unassign). + RemoveDevicesFromAnyRack(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) (int64, error) + + // LockRacksForReparent takes FOR UPDATE locks on every rack involved + // in a reparent -- every source rack currently holding any of the + // given devices PLUS targetRackID (when non-zero) -- in ascending + // device_set_id order, and returns the locked ids. + // AssignDevicesToRack calls this as the FIRST tx operation. Locking + // source and target together in one globally sorted acquisition is + // what prevents two concurrent reparent calls moving devices in + // opposite directions between the same rack pair from deadlocking + // (tx A locking source 1 then target 2 while tx B locks source 2 + // then target 1). Pass 0 for targetRackID in the clear-rack path + // where there is no target. The subsequent + // LockRackPlacementForWrite call on the target still happens for + // its placement read; this query handles the rack-id locks. + LockRacksForReparent(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) ([]int64, error) + // ListCollectionMembers returns paginated members of a collection ordered by when they were added (newest first). // Returns the members and a next page token (empty if no more results). ListCollectionMembers(ctx context.Context, orgID int64, collectionID int64, pageSize int32, pageToken string) ([]*pb.CollectionMember, string, error) diff --git a/server/internal/domain/stores/interfaces/mocks/mock_building_store.go b/server/internal/domain/stores/interfaces/mocks/mock_building_store.go index c75ed6f84..07fa740d5 100644 --- a/server/internal/domain/stores/interfaces/mocks/mock_building_store.go +++ b/server/internal/domain/stores/interfaces/mocks/mock_building_store.go @@ -161,6 +161,34 @@ func (mr *MockBuildingStoreMockRecorder) SetRackBuildingPosition(ctx, orgID, rac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRackBuildingPosition", reflect.TypeOf((*MockBuildingStore)(nil).SetRackBuildingPosition), ctx, orgID, rackID, aisleIndex, positionInAisle) } +// SetRackBuildingPositionBulkClear mocks base method. +func (m *MockBuildingStore) SetRackBuildingPositionBulkClear(ctx context.Context, orgID int64, rackIDs []int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRackBuildingPositionBulkClear", ctx, orgID, rackIDs) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRackBuildingPositionBulkClear indicates an expected call of SetRackBuildingPositionBulkClear. +func (mr *MockBuildingStoreMockRecorder) SetRackBuildingPositionBulkClear(ctx, orgID, rackIDs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRackBuildingPositionBulkClear", reflect.TypeOf((*MockBuildingStore)(nil).SetRackBuildingPositionBulkClear), ctx, orgID, rackIDs) +} + +// SetRackBuildingPositionBulkPlace mocks base method. +func (m *MockBuildingStore) SetRackBuildingPositionBulkPlace(ctx context.Context, orgID int64, rackIDs []int64, aisleIndexes, positionInAisles []int32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRackBuildingPositionBulkPlace", ctx, orgID, rackIDs, aisleIndexes, positionInAisles) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetRackBuildingPositionBulkPlace indicates an expected call of SetRackBuildingPositionBulkPlace. +func (mr *MockBuildingStoreMockRecorder) SetRackBuildingPositionBulkPlace(ctx, orgID, rackIDs, aisleIndexes, positionInAisles any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRackBuildingPositionBulkPlace", reflect.TypeOf((*MockBuildingStore)(nil).SetRackBuildingPositionBulkPlace), ctx, orgID, rackIDs, aisleIndexes, positionInAisles) +} + // SoftDeleteBuilding mocks base method. func (m *MockBuildingStore) SoftDeleteBuilding(ctx context.Context, orgID, id int64) (int64, error) { m.ctrl.T.Helper() diff --git a/server/internal/domain/stores/interfaces/mocks/mock_collection_store.go b/server/internal/domain/stores/interfaces/mocks/mock_collection_store.go index a1b35df4f..c2ba89742 100644 --- a/server/internal/domain/stores/interfaces/mocks/mock_collection_store.go +++ b/server/internal/domain/stores/interfaces/mocks/mock_collection_store.go @@ -87,6 +87,21 @@ func (mr *MockCollectionStoreMockRecorder) CascadeRackDeviceSites(ctx, collectio return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CascadeRackDeviceSites", reflect.TypeOf((*MockCollectionStore)(nil).CascadeRackDeviceSites), ctx, collectionID, orgID, targetSiteID) } +// CascadeRackDeviceSitesBulk mocks base method. +func (m *MockCollectionStore) CascadeRackDeviceSitesBulk(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CascadeRackDeviceSitesBulk", ctx, orgID, rackIDs, targetSiteID) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CascadeRackDeviceSitesBulk indicates an expected call of CascadeRackDeviceSitesBulk. +func (mr *MockCollectionStoreMockRecorder) CascadeRackDeviceSitesBulk(ctx, orgID, rackIDs, targetSiteID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CascadeRackDeviceSitesBulk", reflect.TypeOf((*MockCollectionStore)(nil).CascadeRackDeviceSitesBulk), ctx, orgID, rackIDs, targetSiteID) +} + // ClearRackPlacementForSoftDelete mocks base method. func (m *MockCollectionStore) ClearRackPlacementForSoftDelete(ctx context.Context, orgID, collectionID int64) error { m.ctrl.T.Helper() @@ -447,6 +462,21 @@ func (mr *MockCollectionStoreMockRecorder) LockRackPlacementForWrite(ctx, collec return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockRackPlacementForWrite", reflect.TypeOf((*MockCollectionStore)(nil).LockRackPlacementForWrite), ctx, collectionID, orgID) } +// LockRacksForReparent mocks base method. +func (m *MockCollectionStore) LockRacksForReparent(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) ([]int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LockRacksForReparent", ctx, orgID, deviceIdentifiers, targetRackID) + ret0, _ := ret[0].([]int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LockRacksForReparent indicates an expected call of LockRacksForReparent. +func (mr *MockCollectionStoreMockRecorder) LockRacksForReparent(ctx, orgID, deviceIdentifiers, targetRackID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockRacksForReparent", reflect.TypeOf((*MockCollectionStore)(nil).LockRacksForReparent), ctx, orgID, deviceIdentifiers, targetRackID) +} + // RemoveAllDevicesFromCollection mocks base method. func (m *MockCollectionStore) RemoveAllDevicesFromCollection(ctx context.Context, orgID, collectionID int64) (int64, error) { m.ctrl.T.Helper() @@ -462,6 +492,21 @@ func (mr *MockCollectionStoreMockRecorder) RemoveAllDevicesFromCollection(ctx, o return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAllDevicesFromCollection", reflect.TypeOf((*MockCollectionStore)(nil).RemoveAllDevicesFromCollection), ctx, orgID, collectionID) } +// RemoveDevicesFromAnyRack mocks base method. +func (m *MockCollectionStore) RemoveDevicesFromAnyRack(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveDevicesFromAnyRack", ctx, orgID, deviceIdentifiers, targetRackID) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveDevicesFromAnyRack indicates an expected call of RemoveDevicesFromAnyRack. +func (mr *MockCollectionStoreMockRecorder) RemoveDevicesFromAnyRack(ctx, orgID, deviceIdentifiers, targetRackID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDevicesFromAnyRack", reflect.TypeOf((*MockCollectionStore)(nil).RemoveDevicesFromAnyRack), ctx, orgID, deviceIdentifiers, targetRackID) +} + // RemoveDevicesFromCollection mocks base method. func (m *MockCollectionStore) RemoveDevicesFromCollection(ctx context.Context, orgID, collectionID int64, deviceIdentifiers []string) (int64, error) { m.ctrl.T.Helper() @@ -562,3 +607,31 @@ func (mr *MockCollectionStoreMockRecorder) UpdateRackPlacement(ctx, collectionID mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRackPlacement", reflect.TypeOf((*MockCollectionStore)(nil).UpdateRackPlacement), ctx, collectionID, orgID, siteID, buildingID, zone) } + +// UpdateRackPlacementBulkForBuilding mocks base method. +func (m *MockCollectionStore) UpdateRackPlacementBulkForBuilding(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID, targetBuildingID *int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRackPlacementBulkForBuilding", ctx, orgID, rackIDs, targetSiteID, targetBuildingID) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateRackPlacementBulkForBuilding indicates an expected call of UpdateRackPlacementBulkForBuilding. +func (mr *MockCollectionStoreMockRecorder) UpdateRackPlacementBulkForBuilding(ctx, orgID, rackIDs, targetSiteID, targetBuildingID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRackPlacementBulkForBuilding", reflect.TypeOf((*MockCollectionStore)(nil).UpdateRackPlacementBulkForBuilding), ctx, orgID, rackIDs, targetSiteID, targetBuildingID) +} + +// UpdateRackPlacementBulkForSite mocks base method. +func (m *MockCollectionStore) UpdateRackPlacementBulkForSite(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRackPlacementBulkForSite", ctx, orgID, rackIDs, targetSiteID) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateRackPlacementBulkForSite indicates an expected call of UpdateRackPlacementBulkForSite. +func (mr *MockCollectionStoreMockRecorder) UpdateRackPlacementBulkForSite(ctx, orgID, rackIDs, targetSiteID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRackPlacementBulkForSite", reflect.TypeOf((*MockCollectionStore)(nil).UpdateRackPlacementBulkForSite), ctx, orgID, rackIDs, targetSiteID) +} diff --git a/server/internal/domain/stores/interfaces/mocks/mock_site_store.go b/server/internal/domain/stores/interfaces/mocks/mock_site_store.go index ae44eb0a3..47b52abe9 100644 --- a/server/internal/domain/stores/interfaces/mocks/mock_site_store.go +++ b/server/internal/domain/stores/interfaces/mocks/mock_site_store.go @@ -56,6 +56,36 @@ func (mr *MockSiteStoreMockRecorder) AssignBuildingToSite(ctx, orgID, buildingID return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignBuildingToSite", reflect.TypeOf((*MockSiteStore)(nil).AssignBuildingToSite), ctx, orgID, buildingID, targetSiteID) } +// AssignBuildingsToSiteBulk mocks base method. +func (m *MockSiteStore) AssignBuildingsToSiteBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssignBuildingsToSiteBulk", ctx, orgID, buildingIDs, targetSiteID) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssignBuildingsToSiteBulk indicates an expected call of AssignBuildingsToSiteBulk. +func (mr *MockSiteStoreMockRecorder) AssignBuildingsToSiteBulk(ctx, orgID, buildingIDs, targetSiteID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignBuildingsToSiteBulk", reflect.TypeOf((*MockSiteStore)(nil).AssignBuildingsToSiteBulk), ctx, orgID, buildingIDs, targetSiteID) +} + +// AssignDevicesToSite mocks base method. +func (m *MockSiteStore) AssignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssignDevicesToSite", ctx, orgID, targetSiteID, deviceIdentifiers) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssignDevicesToSite indicates an expected call of AssignDevicesToSite. +func (mr *MockSiteStoreMockRecorder) AssignDevicesToSite(ctx, orgID, targetSiteID, deviceIdentifiers any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignDevicesToSite", reflect.TypeOf((*MockSiteStore)(nil).AssignDevicesToSite), ctx, orgID, targetSiteID, deviceIdentifiers) +} + // CreateSite mocks base method. func (m *MockSiteStore) CreateSite(ctx context.Context, params models.CreateSiteParams) (*models.Site, error) { m.ctrl.T.Helper() @@ -217,34 +247,34 @@ func (mr *MockSiteStoreMockRecorder) LockSiteForWrite(ctx, orgID, siteID any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockSiteForWrite", reflect.TypeOf((*MockSiteStore)(nil).LockSiteForWrite), ctx, orgID, siteID) } -// ReassignDevicesToSite mocks base method. -func (m *MockSiteStore) ReassignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) { +// ReassignDevicesUnderBuilding mocks base method. +func (m *MockSiteStore) ReassignDevicesUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReassignDevicesToSite", ctx, orgID, targetSiteID, deviceIdentifiers) + ret := m.ctrl.Call(m, "ReassignDevicesUnderBuilding", ctx, orgID, buildingID, targetSiteID) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } -// ReassignDevicesToSite indicates an expected call of ReassignDevicesToSite. -func (mr *MockSiteStoreMockRecorder) ReassignDevicesToSite(ctx, orgID, targetSiteID, deviceIdentifiers any) *gomock.Call { +// ReassignDevicesUnderBuilding indicates an expected call of ReassignDevicesUnderBuilding. +func (mr *MockSiteStoreMockRecorder) ReassignDevicesUnderBuilding(ctx, orgID, buildingID, targetSiteID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignDevicesToSite", reflect.TypeOf((*MockSiteStore)(nil).ReassignDevicesToSite), ctx, orgID, targetSiteID, deviceIdentifiers) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignDevicesUnderBuilding", reflect.TypeOf((*MockSiteStore)(nil).ReassignDevicesUnderBuilding), ctx, orgID, buildingID, targetSiteID) } -// ReassignDevicesUnderBuilding mocks base method. -func (m *MockSiteStore) ReassignDevicesUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) { +// ReassignDevicesUnderBuildingsBulk mocks base method. +func (m *MockSiteStore) ReassignDevicesUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReassignDevicesUnderBuilding", ctx, orgID, buildingID, targetSiteID) + ret := m.ctrl.Call(m, "ReassignDevicesUnderBuildingsBulk", ctx, orgID, buildingIDs, targetSiteID) ret0, _ := ret[0].(int64) ret1, _ := ret[1].(error) return ret0, ret1 } -// ReassignDevicesUnderBuilding indicates an expected call of ReassignDevicesUnderBuilding. -func (mr *MockSiteStoreMockRecorder) ReassignDevicesUnderBuilding(ctx, orgID, buildingID, targetSiteID any) *gomock.Call { +// ReassignDevicesUnderBuildingsBulk indicates an expected call of ReassignDevicesUnderBuildingsBulk. +func (mr *MockSiteStoreMockRecorder) ReassignDevicesUnderBuildingsBulk(ctx, orgID, buildingIDs, targetSiteID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignDevicesUnderBuilding", reflect.TypeOf((*MockSiteStore)(nil).ReassignDevicesUnderBuilding), ctx, orgID, buildingID, targetSiteID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignDevicesUnderBuildingsBulk", reflect.TypeOf((*MockSiteStore)(nil).ReassignDevicesUnderBuildingsBulk), ctx, orgID, buildingIDs, targetSiteID) } // ReassignRacksUnderBuilding mocks base method. @@ -262,6 +292,21 @@ func (mr *MockSiteStoreMockRecorder) ReassignRacksUnderBuilding(ctx, orgID, buil return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignRacksUnderBuilding", reflect.TypeOf((*MockSiteStore)(nil).ReassignRacksUnderBuilding), ctx, orgID, buildingID, targetSiteID) } +// ReassignRacksUnderBuildingsBulk mocks base method. +func (m *MockSiteStore) ReassignRacksUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReassignRacksUnderBuildingsBulk", ctx, orgID, buildingIDs, targetSiteID) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReassignRacksUnderBuildingsBulk indicates an expected call of ReassignRacksUnderBuildingsBulk. +func (mr *MockSiteStoreMockRecorder) ReassignRacksUnderBuildingsBulk(ctx, orgID, buildingIDs, targetSiteID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReassignRacksUnderBuildingsBulk", reflect.TypeOf((*MockSiteStore)(nil).ReassignRacksUnderBuildingsBulk), ctx, orgID, buildingIDs, targetSiteID) +} + // SiteBelongsToOrg mocks base method. func (m *MockSiteStore) SiteBelongsToOrg(ctx context.Context, orgID, id int64) (bool, error) { m.ctrl.T.Helper() diff --git a/server/internal/domain/stores/interfaces/site.go b/server/internal/domain/stores/interfaces/site.go index 327b26885..ff2d45760 100644 --- a/server/internal/domain/stores/interfaces/site.go +++ b/server/internal/domain/stores/interfaces/site.go @@ -81,16 +81,16 @@ type SiteStore interface { // LockDevicesForReassign takes a row-lock on every matching live // device for the duration of the surrounding transaction so the - // conflict check + UPDATE in ReassignDevicesToSite are atomic - // against a concurrent reassign. + // conflict check + UPDATE in AssignDevicesToSite are atomic + // against a concurrent assign. LockDevicesForReassign(ctx context.Context, orgID int64, deviceIdentifiers []string) error - // ReassignDevicesToSite bulk-updates device.site_id for the + // AssignDevicesToSite bulk-updates device.site_id for the // given identifiers. The caller must have validated cross- // collection conflicts (see FindDeviceSiteConflicts) and that // every identifier exists (see ListExistingDeviceIdentifiers). // targetSiteID == nil means "Unassigned". - ReassignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) + AssignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) // FindDeviceSiteConflicts returns, for each requested device that // is in a rack with a site_id, the device identifier and that @@ -113,13 +113,28 @@ type SiteStore interface { // number of rows affected (0 == not found). AssignBuildingToSite(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) + // AssignBuildingsToSiteBulk is the multi-building variant of + // AssignBuildingToSite. Updates building.site_id for every building + // in buildingIDs in one statement. Returns the row count actually + // moved (skips soft-deleted rows). + AssignBuildingsToSiteBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) + // ReassignRacksUnderBuilding cascades site_id down to every rack // under the given building. Caller wraps it in the same tx as the // building UPDATE. ReassignRacksUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) + // ReassignRacksUnderBuildingsBulk cascades site_id down to every + // rack under any building in buildingIDs, in one statement. + ReassignRacksUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) + // ReassignDevicesUnderBuilding cascades site_id down to every // device in any rack of the given building. Caller wraps it in // the same tx as the building UPDATE. ReassignDevicesUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) + + // ReassignDevicesUnderBuildingsBulk cascades site_id down to every + // device in any rack under any building in buildingIDs, in one + // statement. + ReassignDevicesUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) } diff --git a/server/internal/domain/stores/sqlstores/building.go b/server/internal/domain/stores/sqlstores/building.go index aeb1e6674..749da9f36 100644 --- a/server/internal/domain/stores/sqlstores/building.go +++ b/server/internal/domain/stores/sqlstores/building.go @@ -237,6 +237,37 @@ func (s *SQLBuildingStore) SetRackBuildingPosition(ctx context.Context, orgID, r return nil } +func (s *SQLBuildingStore) SetRackBuildingPositionBulkClear(ctx context.Context, orgID int64, rackIDs []int64) error { + if len(rackIDs) == 0 { + return nil + } + if err := s.GetQueries(ctx).SetRackBuildingPositionBulkClear(ctx, sqlc.SetRackBuildingPositionBulkClearParams{ + RackIds: rackIDs, + OrgID: orgID, + }); err != nil { + return fleeterror.NewInternalErrorf("failed to bulk-clear rack building positions: %v", err) + } + return nil +} + +func (s *SQLBuildingStore) SetRackBuildingPositionBulkPlace(ctx context.Context, orgID int64, rackIDs []int64, aisleIndexes, positionInAisles []int32) error { + if len(rackIDs) == 0 { + return nil + } + if len(rackIDs) != len(aisleIndexes) || len(rackIDs) != len(positionInAisles) { + return fleeterror.NewInternalErrorf("SetRackBuildingPositionBulkPlace: array length mismatch (rackIDs=%d aisles=%d positions=%d)", len(rackIDs), len(aisleIndexes), len(positionInAisles)) + } + if err := s.GetQueries(ctx).SetRackBuildingPositionBulkPlace(ctx, sqlc.SetRackBuildingPositionBulkPlaceParams{ + OrgID: orgID, + RackIds: rackIDs, + AisleIndexes: aisleIndexes, + PositionInAisles: positionInAisles, + }); err != nil { + return fleeterror.NewInternalErrorf("failed to bulk-place rack building positions: %v", err) + } + return nil +} + func buildingFromRow(row sqlc.Building) models.Building { return models.Building{ ID: row.ID, diff --git a/server/internal/domain/stores/sqlstores/collection.go b/server/internal/domain/stores/sqlstores/collection.go index 8e832822c..6123ca56f 100644 --- a/server/internal/domain/stores/sqlstores/collection.go +++ b/server/internal/domain/stores/sqlstores/collection.go @@ -237,6 +237,35 @@ func (s *SQLCollectionStore) UpdateRackPlacement(ctx context.Context, collection return nil } +func (s *SQLCollectionStore) UpdateRackPlacementBulkForBuilding(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID, targetBuildingID *int64) error { + if len(rackIDs) == 0 { + return nil + } + if err := s.GetQueries(ctx).UpdateRackPlacementBulkForBuilding(ctx, sqlc.UpdateRackPlacementBulkForBuildingParams{ + TargetBuildingID: ptrToNullInt64(targetBuildingID), + TargetSiteID: ptrToNullInt64(targetSiteID), + RackIds: rackIDs, + OrgID: orgID, + }); err != nil { + return fleeterror.NewInternalErrorf("failed to bulk-update rack placement: %v", err) + } + return nil +} + +func (s *SQLCollectionStore) UpdateRackPlacementBulkForSite(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) error { + if len(rackIDs) == 0 { + return nil + } + if err := s.GetQueries(ctx).UpdateRackPlacementBulkForSite(ctx, sqlc.UpdateRackPlacementBulkForSiteParams{ + TargetSiteID: ptrToNullInt64(targetSiteID), + RackIds: rackIDs, + OrgID: orgID, + }); err != nil { + return fleeterror.NewInternalErrorf("failed to bulk-update rack placement (site): %v", err) + } + return nil +} + func (s *SQLCollectionStore) UnassignDeviceSitesByRack(ctx context.Context, collectionID, orgID int64) (int64, error) { n, err := s.GetQueries(ctx).UnassignDeviceSitesByRack(ctx, sqlc.UnassignDeviceSitesByRackParams{ DeviceSetID: collectionID, @@ -260,6 +289,21 @@ func (s *SQLCollectionStore) CascadeRackDeviceSites(ctx context.Context, collect return n, nil } +func (s *SQLCollectionStore) CascadeRackDeviceSitesBulk(ctx context.Context, orgID int64, rackIDs []int64, targetSiteID *int64) (int64, error) { + if len(rackIDs) == 0 { + return 0, nil + } + n, err := s.GetQueries(ctx).CascadeRackDeviceSitesBulk(ctx, sqlc.CascadeRackDeviceSitesBulkParams{ + TargetSiteID: ptrToNullInt64(targetSiteID), + RackIds: rackIDs, + OrgID: orgID, + }) + if err != nil { + return 0, fleeterror.NewInternalErrorf("failed to bulk-cascade rack device sites: %v", err) + } + return n, nil +} + func (s *SQLCollectionStore) GetDeviceSiteIDsByMembership(ctx context.Context, collectionID, orgID int64) (map[string]*int64, error) { rows, err := s.GetQueries(ctx).GetDeviceSiteIDsByMembership(ctx, sqlc.GetDeviceSiteIDsByMembershipParams{ DeviceSetID: collectionID, @@ -523,6 +567,30 @@ func (s *SQLCollectionStore) RemoveDevicesFromCollection(ctx context.Context, or return count, nil } +func (s *SQLCollectionStore) RemoveDevicesFromAnyRack(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) (int64, error) { + count, err := s.GetQueries(ctx).RemoveDevicesFromAnyRack(ctx, sqlc.RemoveDevicesFromAnyRackParams{ + OrgID: orgID, + DeviceIdentifiers: deviceIdentifiers, + TargetRackID: targetRackID, + }) + if err != nil { + return 0, fleeterror.NewInternalErrorf("failed to remove devices from rack: %v", err) + } + return count, nil +} + +func (s *SQLCollectionStore) LockRacksForReparent(ctx context.Context, orgID int64, deviceIdentifiers []string, targetRackID int64) ([]int64, error) { + ids, err := s.GetQueries(ctx).LockRacksForReparent(ctx, sqlc.LockRacksForReparentParams{ + OrgID: orgID, + DeviceIdentifiers: deviceIdentifiers, + TargetRackID: targetRackID, + }) + if err != nil { + return nil, fleeterror.NewInternalErrorf("failed to lock racks for reparent: %v", err) + } + return ids, nil +} + func (s *SQLCollectionStore) ListCollectionMembers(ctx context.Context, orgID int64, collectionID int64, pageSize int32, pageToken string) ([]*pb.CollectionMember, string, error) { cursor, err := decodeMemberCursor(pageToken) if err != nil { diff --git a/server/internal/domain/stores/sqlstores/collection_site_placement_integration_test.go b/server/internal/domain/stores/sqlstores/collection_site_placement_integration_test.go index d6b87eba6..c71695f85 100644 --- a/server/internal/domain/stores/sqlstores/collection_site_placement_integration_test.go +++ b/server/internal/domain/stores/sqlstores/collection_site_placement_integration_test.go @@ -111,7 +111,7 @@ func TestCascadeRackDeviceSites_RewritesMemberDevices(t *testing.T) { require.NoError(t, err) // Pre-assign all devices to site A. - _, err = siteStore.ReassignDevicesToSite(ctx, orgID, &siteA.ID, deviceIDs) + _, err = siteStore.AssignDevicesToSite(ctx, orgID, &siteA.ID, deviceIDs) require.NoError(t, err) rack, err := collectionStore.CreateCollection(ctx, orgID, pb.CollectionType_COLLECTION_TYPE_RACK, "Rack", "") @@ -163,9 +163,9 @@ func TestGetAddedDeviceSiteConflicts_ReturnsOnlyConflictingDevices(t *testing.T) require.NoError(t, err) // Device 0 already at site A (matches rack); 1 at site B (conflict); 2 unassigned (conflict). - _, err = siteStore.ReassignDevicesToSite(ctx, orgID, &siteA.ID, deviceIDs[:1]) + _, err = siteStore.AssignDevicesToSite(ctx, orgID, &siteA.ID, deviceIDs[:1]) require.NoError(t, err) - _, err = siteStore.ReassignDevicesToSite(ctx, orgID, &siteB.ID, deviceIDs[1:2]) + _, err = siteStore.AssignDevicesToSite(ctx, orgID, &siteB.ID, deviceIDs[1:2]) require.NoError(t, err) rack, err := collectionStore.CreateCollection(ctx, orgID, pb.CollectionType_COLLECTION_TYPE_RACK, "Rack", "") @@ -201,7 +201,7 @@ func TestCascadeAddedDeviceSites_RewritesOnlyDiffering(t *testing.T) { siteB, err := siteStore.CreateSite(ctx, sitesmodels.CreateSiteParams{OrgID: orgID, Name: "Site B"}) require.NoError(t, err) - _, err = siteStore.ReassignDevicesToSite(ctx, orgID, &siteB.ID, deviceIDs[1:2]) + _, err = siteStore.AssignDevicesToSite(ctx, orgID, &siteB.ID, deviceIDs[1:2]) require.NoError(t, err) rack, err := collectionStore.CreateCollection(ctx, orgID, pb.CollectionType_COLLECTION_TYPE_RACK, "Rack", "") @@ -314,7 +314,7 @@ func TestUnassignDeviceSitesByRack_ClearsRackMembersOnly(t *testing.T) { site, err := siteStore.CreateSite(ctx, sitesmodels.CreateSiteParams{OrgID: orgID, Name: "Site"}) require.NoError(t, err) - _, err = siteStore.ReassignDevicesToSite(ctx, orgID, &site.ID, deviceIDs) + _, err = siteStore.AssignDevicesToSite(ctx, orgID, &site.ID, deviceIDs) require.NoError(t, err) rack, err := collectionStore.CreateCollection(ctx, orgID, pb.CollectionType_COLLECTION_TYPE_RACK, "Rack", "") diff --git a/server/internal/domain/stores/sqlstores/site.go b/server/internal/domain/stores/sqlstores/site.go index 48d21ca99..a8010cdf0 100644 --- a/server/internal/domain/stores/sqlstores/site.go +++ b/server/internal/domain/stores/sqlstores/site.go @@ -228,8 +228,8 @@ func (s *SQLSiteStore) LockDevicesForReassign(ctx context.Context, orgID int64, return nil } -func (s *SQLSiteStore) ReassignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) { - rowsAffected, err := s.GetQueries(ctx).ReassignDevicesToSite(ctx, sqlc.ReassignDevicesToSiteParams{ +func (s *SQLSiteStore) AssignDevicesToSite(ctx context.Context, orgID int64, targetSiteID *int64, deviceIdentifiers []string) (int64, error) { + rowsAffected, err := s.GetQueries(ctx).AssignDevicesToSite(ctx, sqlc.AssignDevicesToSiteParams{ OrgID: orgID, TargetSiteID: ptrToNullInt64(targetSiteID), DeviceIdentifiers: deviceIdentifiers, @@ -285,6 +285,24 @@ func (s *SQLSiteStore) AssignBuildingToSite(ctx context.Context, orgID, building return rowsAffected, nil } +func (s *SQLSiteStore) AssignBuildingsToSiteBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { + if len(buildingIDs) == 0 { + return 0, nil + } + rowsAffected, err := s.GetQueries(ctx).AssignBuildingsToSiteBulk(ctx, sqlc.AssignBuildingsToSiteBulkParams{ + SiteID: ptrToNullInt64(targetSiteID), + BuildingIds: buildingIDs, + OrgID: orgID, + }) + if err != nil { + if isUniqueViolation(err) { + return 0, fleeterror.NewPlainError("a building with this name already exists in the target site", connect.CodeAlreadyExists).WithCallerStackTrace() + } + return 0, fleeterror.NewInternalErrorf("failed to bulk-assign buildings to site: %v", err) + } + return rowsAffected, nil +} + func (s *SQLSiteStore) ReassignRacksUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) { rowsAffected, err := s.GetQueries(ctx).ReassignRacksUnderBuilding(ctx, sqlc.ReassignRacksUnderBuildingParams{ TargetSiteID: ptrToNullInt64(targetSiteID), @@ -297,6 +315,21 @@ func (s *SQLSiteStore) ReassignRacksUnderBuilding(ctx context.Context, orgID, bu return rowsAffected, nil } +func (s *SQLSiteStore) ReassignRacksUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { + if len(buildingIDs) == 0 { + return 0, nil + } + rowsAffected, err := s.GetQueries(ctx).ReassignRacksUnderBuildingsBulk(ctx, sqlc.ReassignRacksUnderBuildingsBulkParams{ + TargetSiteID: ptrToNullInt64(targetSiteID), + OrgID: orgID, + BuildingIds: buildingIDs, + }) + if err != nil { + return 0, fleeterror.NewInternalErrorf("failed to bulk-reassign racks under buildings: %v", err) + } + return rowsAffected, nil +} + func (s *SQLSiteStore) ReassignDevicesUnderBuilding(ctx context.Context, orgID, buildingID int64, targetSiteID *int64) (int64, error) { rowsAffected, err := s.GetQueries(ctx).ReassignDevicesUnderBuilding(ctx, sqlc.ReassignDevicesUnderBuildingParams{ TargetSiteID: ptrToNullInt64(targetSiteID), @@ -309,6 +342,21 @@ func (s *SQLSiteStore) ReassignDevicesUnderBuilding(ctx context.Context, orgID, return rowsAffected, nil } +func (s *SQLSiteStore) ReassignDevicesUnderBuildingsBulk(ctx context.Context, orgID int64, buildingIDs []int64, targetSiteID *int64) (int64, error) { + if len(buildingIDs) == 0 { + return 0, nil + } + rowsAffected, err := s.GetQueries(ctx).ReassignDevicesUnderBuildingsBulk(ctx, sqlc.ReassignDevicesUnderBuildingsBulkParams{ + TargetSiteID: ptrToNullInt64(targetSiteID), + OrgID: orgID, + BuildingIds: buildingIDs, + }) + if err != nil { + return 0, fleeterror.NewInternalErrorf("failed to bulk-reassign devices under buildings: %v", err) + } + return rowsAffected, nil +} + func (s *SQLSiteStore) ListAllSiteNetworkConfigs(ctx context.Context, orgID, excludeID int64) ([]models.SiteNetworkConfigEntry, error) { rows, err := s.GetQueries(ctx).ListSiteNetworkConfigsForOverlap(ctx, sqlc.ListSiteNetworkConfigsForOverlapParams{ OrgID: orgID, diff --git a/server/internal/domain/stores/sqlstores/site_invariant_integration_test.go b/server/internal/domain/stores/sqlstores/site_invariant_integration_test.go index b711f156b..947790425 100644 --- a/server/internal/domain/stores/sqlstores/site_invariant_integration_test.go +++ b/server/internal/domain/stores/sqlstores/site_invariant_integration_test.go @@ -40,7 +40,7 @@ func TestDeleteSite_ClearsAllDeviceSitePointers(t *testing.T) { }) require.NoError(t, err) - _, err = siteStore.ReassignDevicesToSite(t.Context(), orgID, &site.ID, deviceIDs[:2]) + _, err = siteStore.AssignDevicesToSite(t.Context(), orgID, &site.ID, deviceIDs[:2]) require.NoError(t, err) // Sanity: the two reassigned devices now point at the site. diff --git a/server/internal/handlers/buildings/handler.go b/server/internal/handlers/buildings/handler.go index b8723230f..fb291b79c 100644 --- a/server/internal/handlers/buildings/handler.go +++ b/server/internal/handlers/buildings/handler.go @@ -113,16 +113,16 @@ func (h *Handler) ListBuildingRacks(ctx context.Context, req *connect.Request[pb return connect.NewResponse(toListBuildingRacksResponse(racks, nextPageToken)), nil } -func (h *Handler) AssignRackToBuilding(ctx context.Context, req *connect.Request[pb.AssignRackToBuildingRequest]) (*connect.Response[pb.AssignRackToBuildingResponse], error) { +func (h *Handler) AssignRacksToBuilding(ctx context.Context, req *connect.Request[pb.AssignRacksToBuildingRequest]) (*connect.Response[pb.AssignRacksToBuildingResponse], error) { info, err := middleware.RequirePermission(ctx, authz.PermSiteManage, authz.ResourceContext{}) if err != nil { return nil, err } - out, err := h.service.AssignRackToBuilding(ctx, toAssignRackToBuildingParams(req.Msg, info.OrganizationID)) + out, err := h.service.AssignRacksToBuilding(ctx, toAssignRacksToBuildingParams(req.Msg, info.OrganizationID)) if err != nil { return nil, err } - return connect.NewResponse(&pb.AssignRackToBuildingResponse{ + return connect.NewResponse(&pb.AssignRacksToBuildingResponse{ SiteReassignedDeviceCount: out.SiteReassignedDeviceCount, }), nil } @@ -172,7 +172,7 @@ func (h *Handler) GetBuildingStats(ctx context.Context, req *connect.Request[pb. } // Pass the building's site as we saw it at authz time. The service // re-reads the building and rejects with NotFound if a concurrent - // AssignBuildingToSite moved it — otherwise a site-scoped caller + // AssignBuildingsToSite moved it — otherwise a site-scoped caller // could end up with telemetry for a site they're not authorized for. out, err := h.service.GetBuildingStats(ctx, info.OrganizationID, req.Msg.GetBuildingId(), building.SiteID) if err != nil { diff --git a/server/internal/handlers/buildings/handler_test.go b/server/internal/handlers/buildings/handler_test.go index fb18f7eef..354dab3cc 100644 --- a/server/internal/handlers/buildings/handler_test.go +++ b/server/internal/handlers/buildings/handler_test.go @@ -363,17 +363,17 @@ func TestHandler_ListBuildingRacks_happy(t *testing.T) { assert.Equal(t, "next-rack-page", resp.Msg.GetNextPageToken()) } -// AssignRackToBuilding requires PermSiteManage — callers without it +// AssignRacksToBuilding requires PermSiteManage — callers without it // are rejected before the service is touched. -func TestHandler_AssignRackToBuilding_rejectsCallerWithoutSiteManage(t *testing.T) { +func TestHandler_AssignRacksToBuilding_rejectsCallerWithoutSiteManage(t *testing.T) { t.Parallel() h := NewHandler(nil) // PermSiteRead alone does NOT satisfy PermSiteManage. ctx := handlerstest.CtxWithPermissions(t, 7, authz.PermSiteRead) buildingID := int64(11) - _, err := h.AssignRackToBuilding(ctx, connect.NewRequest(&pb.AssignRackToBuildingRequest{ - RackId: 99, - BuildingId: &buildingID, + _, err := h.AssignRacksToBuilding(ctx, connect.NewRequest(&pb.AssignRacksToBuildingRequest{ + Racks: []*pb.RackPlacement{{RackId: 99}}, + TargetBuildingId: &buildingID, })) require.Error(t, err) var fleetErr fleeterror.FleetError @@ -381,10 +381,10 @@ func TestHandler_AssignRackToBuilding_rejectsCallerWithoutSiteManage(t *testing. assert.Equal(t, connect.CodePermissionDenied, fleetErr.GRPCCode) } -// AssignRackToBuilding: PermSiteManage clears the gate and request +// AssignRacksToBuilding: PermSiteManage clears the gate and request // fields thread through to service params; response carries // SiteReassignedDeviceCount. -func TestHandler_AssignRackToBuilding_happy(t *testing.T) { +func TestHandler_AssignRacksToBuilding_happy(t *testing.T) { t.Parallel() h := newTestHandler(t) @@ -395,23 +395,32 @@ func TestHandler_AssignRackToBuilding_happy(t *testing.T) { h.siteStore.EXPECT().LockBuildingForWrite(gomock.Any(), int64(7), buildingID).Return(nil), h.buildingStore.EXPECT().GetBuilding(gomock.Any(), int64(7), buildingID). Return(&models.Building{ID: buildingID, SiteID: &siteID, Aisles: 4, RacksPerAisle: 6}, nil), + // Phase A: per-rack lock + read. h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), int64(99), int64(7)). Return(interfaces.RackPlacement{SiteID: nil}, nil), - h.collectionStore.EXPECT().UpdateRackPlacement(gomock.Any(), int64(99), int64(7), &siteID, &buildingID, ""). + // Phase B1: single bulk placement write. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForBuilding(gomock.Any(), int64(7), []int64{99}, &siteID, &buildingID). Return(nil), - h.collectionStore.EXPECT().CascadeRackDeviceSites(gomock.Any(), int64(99), int64(7), &siteID). + // Phase B2: single bulk cascade for site-changed rack. + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(gomock.Any(), int64(7), []int64{99}, &siteID). Return(int64(3), nil), - h.buildingStore.EXPECT().SetRackBuildingPosition(gomock.Any(), int64(7), int64(99), ptrInt32t(1), ptrInt32t(2)). + // Phase B3: bulk pass-1 vacate. + h.buildingStore.EXPECT().SetRackBuildingPositionBulkClear(gomock.Any(), int64(7), []int64{99}). + Return(nil), + // Phase B4: bulk pass-2 place. + h.buildingStore.EXPECT().SetRackBuildingPositionBulkPlace(gomock.Any(), int64(7), []int64{99}, []int32{1}, []int32{2}). Return(nil), ) aisle := int32(1) pos := int32(2) - resp, err := h.handler.AssignRackToBuilding(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRackToBuildingRequest{ - RackId: 99, - BuildingId: &buildingID, - AisleIndex: &aisle, - PositionInAisle: &pos, + resp, err := h.handler.AssignRacksToBuilding(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRacksToBuildingRequest{ + TargetBuildingId: &buildingID, + Racks: []*pb.RackPlacement{{ + RackId: 99, + AisleIndex: &aisle, + PositionInAisle: &pos, + }}, })) require.NoError(t, err) assert.Equal(t, int64(3), resp.Msg.GetSiteReassignedDeviceCount()) diff --git a/server/internal/handlers/buildings/translate.go b/server/internal/handlers/buildings/translate.go index f553c6ee1..2e8e8ad33 100644 --- a/server/internal/handlers/buildings/translate.go +++ b/server/internal/handlers/buildings/translate.go @@ -121,22 +121,26 @@ func toListBuildingRacksResponse(rows []models.BuildingRack, nextPageToken strin return &pb.ListBuildingRacksResponse{Racks: out, NextPageToken: nextPageToken} } -func toAssignRackToBuildingParams(req *pb.AssignRackToBuildingRequest, orgID int64) models.AssignRackToBuildingParams { - out := models.AssignRackToBuildingParams{ - OrgID: orgID, - RackID: req.GetRackId(), +func toAssignRacksToBuildingParams(req *pb.AssignRacksToBuildingRequest, orgID int64) models.AssignRacksToBuildingParams { + out := models.AssignRacksToBuildingParams{ + OrgID: orgID, + Racks: make([]models.RackPlacementParam, 0, len(req.GetRacks())), } - if req.BuildingId != nil { - v := req.GetBuildingId() - out.BuildingID = &v + if req.TargetBuildingId != nil { + v := req.GetTargetBuildingId() + out.TargetBuildingID = &v } - if req.AisleIndex != nil { - v := req.GetAisleIndex() - out.AisleIndex = &v - } - if req.PositionInAisle != nil { - v := req.GetPositionInAisle() - out.PositionInAisle = &v + for _, rp := range req.GetRacks() { + entry := models.RackPlacementParam{RackID: rp.GetRackId()} + if rp.AisleIndex != nil { + v := rp.GetAisleIndex() + entry.AisleIndex = &v + } + if rp.PositionInAisle != nil { + v := rp.GetPositionInAisle() + entry.PositionInAisle = &v + } + out.Racks = append(out.Racks, entry) } return out } diff --git a/server/internal/handlers/collection/handler.go b/server/internal/handlers/collection/handler.go index 32ad826de..57fd7ff82 100644 --- a/server/internal/handlers/collection/handler.go +++ b/server/internal/handlers/collection/handler.go @@ -85,30 +85,6 @@ func (h *Handler) ListCollections(ctx context.Context, r *connect.Request[pb.Lis return connect.NewResponse(result), nil } -// AddDevicesToCollection adds devices to a collection. -func (h *Handler) AddDevicesToCollection(ctx context.Context, r *connect.Request[pb.AddDevicesToCollectionRequest]) (*connect.Response[pb.AddDevicesToCollectionResponse], error) { - if _, err := middleware.RequirePermission(ctx, authz.PermRackManage, authz.ResourceContext{}); err != nil { - return nil, err - } - result, err := h.collectionSvc.AddDevicesToCollection(ctx, r.Msg) - if err != nil { - return nil, err - } - return connect.NewResponse(result), nil -} - -// RemoveDevicesFromCollection removes devices from a collection. -func (h *Handler) RemoveDevicesFromCollection(ctx context.Context, r *connect.Request[pb.RemoveDevicesFromCollectionRequest]) (*connect.Response[pb.RemoveDevicesFromCollectionResponse], error) { - if _, err := middleware.RequirePermission(ctx, authz.PermRackManage, authz.ResourceContext{}); err != nil { - return nil, err - } - result, err := h.collectionSvc.RemoveDevicesFromCollection(ctx, r.Msg) - if err != nil { - return nil, err - } - return connect.NewResponse(result), nil -} - // ListCollectionMembers returns all members of a collection. func (h *Handler) ListCollectionMembers(ctx context.Context, r *connect.Request[pb.ListCollectionMembersRequest]) (*connect.Response[pb.ListCollectionMembersResponse], error) { if _, err := middleware.RequirePermission(ctx, authz.PermRackRead, authz.ResourceContext{}); err != nil { diff --git a/server/internal/handlers/deviceset/handler.go b/server/internal/handlers/deviceset/handler.go index a6c92bf47..2781cd5fe 100644 --- a/server/internal/handlers/deviceset/handler.go +++ b/server/internal/handlers/deviceset/handler.go @@ -107,36 +107,42 @@ func (h *Handler) ListDeviceSets(ctx context.Context, r *connect.Request[dspb.Li }), nil } -func (h *Handler) AddDevicesToDeviceSet(ctx context.Context, r *connect.Request[dspb.AddDevicesToDeviceSetRequest]) (*connect.Response[dspb.AddDevicesToDeviceSetResponse], error) { +// AddDevicesToGroup adds devices to a group device set. The target +// must be a group; racks are rejected with InvalidArgument so callers +// can't smuggle rack mutations through the group endpoint (rack adds +// must go through AssignDevicesToRack to get the atomic prior-rack +// removal + site cascade). +func (h *Handler) AddDevicesToGroup(ctx context.Context, r *connect.Request[dspb.AddDevicesToGroupRequest]) (*connect.Response[dspb.AddDevicesToGroupResponse], error) { if _, err := middleware.RequirePermission(ctx, authz.PermRackManage, authz.ResourceContext{}); err != nil { return nil, err } - result, err := h.svc.AddDevicesToCollection(ctx, &collectionpb.AddDevicesToCollectionRequest{ - CollectionId: r.Msg.DeviceSetId, + result, err := h.svc.AddDevicesToGroup(ctx, collection.AddDevicesToGroupParams{ + TargetGroupID: r.Msg.TargetGroupId, DeviceSelector: r.Msg.DeviceSelector, }) if err != nil { return nil, err } - return connect.NewResponse(&dspb.AddDevicesToDeviceSetResponse{ - DeviceSetId: result.CollectionId, - AddedCount: result.AddedCount, - SiteReassignedCount: result.SiteReassignedCount, + return connect.NewResponse(&dspb.AddDevicesToGroupResponse{ + AddedCount: result.AddedCount, }), nil } -func (h *Handler) RemoveDevicesFromDeviceSet(ctx context.Context, r *connect.Request[dspb.RemoveDevicesFromDeviceSetRequest]) (*connect.Response[dspb.RemoveDevicesFromDeviceSetResponse], error) { +// RemoveDevicesFromGroup removes devices from a group device set. +// Racks are rejected with InvalidArgument; use AssignDevicesToRack +// (target_rack_id unset) to clear rack membership. +func (h *Handler) RemoveDevicesFromGroup(ctx context.Context, r *connect.Request[dspb.RemoveDevicesFromGroupRequest]) (*connect.Response[dspb.RemoveDevicesFromGroupResponse], error) { if _, err := middleware.RequirePermission(ctx, authz.PermRackManage, authz.ResourceContext{}); err != nil { return nil, err } - result, err := h.svc.RemoveDevicesFromCollection(ctx, &collectionpb.RemoveDevicesFromCollectionRequest{ - CollectionId: r.Msg.DeviceSetId, + result, err := h.svc.RemoveDevicesFromGroup(ctx, collection.RemoveDevicesFromGroupParams{ + TargetGroupID: r.Msg.TargetGroupId, DeviceSelector: r.Msg.DeviceSelector, }) if err != nil { return nil, err } - return connect.NewResponse(&dspb.RemoveDevicesFromDeviceSetResponse{ + return connect.NewResponse(&dspb.RemoveDevicesFromGroupResponse{ RemovedCount: result.RemovedCount, }), nil } @@ -325,3 +331,23 @@ func (h *Handler) SaveRack(ctx context.Context, r *connect.Request[dspb.SaveRack SiteReassignedCount: result.SiteReassignedCount, }), nil } + +func (h *Handler) AssignDevicesToRack(ctx context.Context, r *connect.Request[dspb.AssignDevicesToRackRequest]) (*connect.Response[dspb.AssignDevicesToRackResponse], error) { + info, err := middleware.RequirePermission(ctx, authz.PermRackManage, authz.ResourceContext{}) + if err != nil { + return nil, err + } + params, err := toAssignDevicesToRackParams(r.Msg, info.OrganizationID) + if err != nil { + return nil, err + } + result, err := h.svc.AssignDevicesToRack(ctx, params) + if err != nil { + return nil, err + } + return connect.NewResponse(&dspb.AssignDevicesToRackResponse{ + AssignedCount: result.AssignedCount, + RemovedCount: result.RemovedCount, + SiteReassignedCount: result.SiteReassignedCount, + }), nil +} diff --git a/server/internal/handlers/deviceset/handler_test.go b/server/internal/handlers/deviceset/handler_test.go index d2e0e081e..312ea7f55 100644 --- a/server/internal/handlers/deviceset/handler_test.go +++ b/server/internal/handlers/deviceset/handler_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + "connectrpc.com/authn" "connectrpc.com/connect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,10 +15,13 @@ import ( commonpb "github.com/block/proto-fleet/server/generated/grpc/common/v1" dspb "github.com/block/proto-fleet/server/generated/grpc/device_set/v1" "github.com/block/proto-fleet/server/internal/domain/activity" + "github.com/block/proto-fleet/server/internal/domain/authz" "github.com/block/proto-fleet/server/internal/domain/collection" "github.com/block/proto-fleet/server/internal/domain/fleeterror" + "github.com/block/proto-fleet/server/internal/domain/session" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces/mocks" + "github.com/block/proto-fleet/server/internal/handlers/middleware" "github.com/block/proto-fleet/server/internal/testutil" ) @@ -347,3 +351,327 @@ func TestListRackZoneRefs_StoreError(t *testing.T) { _, err := h.handler.ListRackZoneRefs(testCtx(t), connect.NewRequest(&dspb.ListRackZoneRefsRequest{})) require.Error(t, err) } + +// ctxWithPerms builds an auth context with an explicit permission set +// so we can assert AssignDevicesToRack's PermRackManage gate. +func ctxWithPerms(perms ...string) context.Context { + ctx := authn.SetInfo(context.Background(), &session.Info{ + AuthMethod: session.AuthMethodSession, + OrganizationID: testOrgID, + UserID: testUserID, + }) + return middleware.WithEffectivePermissions(ctx, authz.NewEffectivePermissions([]authz.Assignment{{ + AssignmentID: 1, + ScopeType: authz.ScopeOrg, + Permissions: perms, + }})) +} + +// deviceListSelector builds a DeviceSelector that selects the given +// identifiers (the device_list variant), the only variant accepted by +// AssignDevicesToRack and the only variant the noopResolver-backed +// group endpoint tests exercise. +func deviceListSelector(ids ...string) *commonpb.DeviceSelector { + return &commonpb.DeviceSelector{ + SelectionType: &commonpb.DeviceSelector_DeviceList{ + DeviceList: &commonpb.DeviceIdentifierList{ + DeviceIdentifiers: ids, + }, + }, + } +} + +// allDevicesSelector builds the all_devices DeviceSelector variant. +// AssignDevicesToRack must reject it with InvalidArgument. +func allDevicesSelector() *commonpb.DeviceSelector { + return &commonpb.DeviceSelector{ + SelectionType: &commonpb.DeviceSelector_AllDevices{AllDevices: true}, + } +} + +// TestAssignDevicesToRack_PermissionRequired confirms the +// PermRackManage gate rejects callers without rack:manage. No store +// calls should fire. +func TestAssignDevicesToRack_PermissionRequired(t *testing.T) { + h := newTestHandler(t) + + // Caller has *some* permission but not PermRackManage. + ctx := ctxWithPerms(authz.PermSiteRead) + req := connect.NewRequest(&dspb.AssignDevicesToRackRequest{ + TargetRackId: ptrInt64Local(42), + DeviceSelector: deviceListSelector("d1"), + }) + + _, err := h.handler.AssignDevicesToRack(ctx, req) + require.Error(t, err) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe, "expected FleetError, got %T", err) + assert.Equal(t, connect.CodePermissionDenied, fe.GRPCCode) +} + +// TestAssignDevicesToRack_RejectsAllDevicesSelector confirms that the +// all_devices selector variant is rejected with InvalidArgument at the +// handler boundary, before any store call fires. Moving every paired +// device into a single rack is never the intended operation and the +// filter-based variants are reserved for a future expansion. +func TestAssignDevicesToRack_RejectsAllDevicesSelector(t *testing.T) { + h := newTestHandler(t) + + resp, err := h.handler.AssignDevicesToRack(testCtx(t), connect.NewRequest(&dspb.AssignDevicesToRackRequest{ + TargetRackId: ptrInt64Local(42), + DeviceSelector: allDevicesSelector(), + })) + require.Error(t, err) + require.Nil(t, resp) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe) + assert.Equal(t, connect.CodeInvalidArgument, fe.GRPCCode) +} + +// TestAssignDevicesToRack_HappyPathAssigns covers the assign branch: +// target_rack_id set, devices flow through the lock → label-read → +// remove → add → cascade chain, and the handler round-trips the +// per-step counts onto the wire response. +func TestAssignDevicesToRack_HappyPathAssigns(t *testing.T) { + h := newTestHandler(t) + + targetRackID := int64(42) + rackSite := int64(7) + deviceIDs := []string{"d1", "d2"} + + gomock.InOrder( + h.collectionStore.EXPECT(). + LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, targetRackID). + Return([]int64{11, 23}, nil), + h.collectionStore.EXPECT(). + LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID). + Return(interfaces.RackPlacement{SiteID: &rackSite}, nil), + h.collectionStore.EXPECT(). + GetCollection(gomock.Any(), testOrgID, targetRackID). + Return(&collectionpb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: collectionpb.CollectionType_COLLECTION_TYPE_RACK}, nil), + h.collectionStore.EXPECT(). + RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID). + Return(int64(2), nil), + h.collectionStore.EXPECT(). + AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs). + Return(int64(2), nil), + h.collectionStore.EXPECT(). + CascadeAddedDeviceSites(gomock.Any(), testOrgID, targetRackID, deviceIDs). + Return(int64(1), nil), + ) + + resp, err := h.handler.AssignDevicesToRack(testCtx(t), connect.NewRequest(&dspb.AssignDevicesToRackRequest{ + TargetRackId: &targetRackID, + DeviceSelector: deviceListSelector(deviceIDs...), + })) + require.NoError(t, err) + assert.Equal(t, int64(2), resp.Msg.AssignedCount) + assert.Equal(t, int64(2), resp.Msg.RemovedCount) + assert.Equal(t, int64(1), resp.Msg.SiteReassignedCount) +} + +// TestAssignDevicesToRack_UnassignBranch covers target_rack_id unset: +// clears prior rack membership, no Get/Lock/Add/Cascade. +// RemoveDevicesFromAnyRack is called with targetRackID = 0 (sentinel +// meaning "don't exclude any rack"). +func TestAssignDevicesToRack_UnassignBranch(t *testing.T) { + h := newTestHandler(t) + + deviceIDs := []string{"d1"} + gomock.InOrder( + h.collectionStore.EXPECT(). + LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, int64(0)). + Return([]int64{17}, nil), + h.collectionStore.EXPECT(). + RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, int64(0)). + Return(int64(1), nil), + ) + + resp, err := h.handler.AssignDevicesToRack(testCtx(t), connect.NewRequest(&dspb.AssignDevicesToRackRequest{ + TargetRackId: nil, + DeviceSelector: deviceListSelector(deviceIDs...), + })) + require.NoError(t, err) + assert.Equal(t, int64(0), resp.Msg.AssignedCount) + assert.Equal(t, int64(1), resp.Msg.RemovedCount) + assert.Equal(t, int64(0), resp.Msg.SiteReassignedCount) +} + +// TestAddDevicesToGroup_HappyPath asserts that the handler verifies the +// target is a group, calls the underlying AddDevicesToGroup path, and +// round-trips the added_count onto the wire response. +// +// The default test harness uses a noopResolver that returns nil for +// every selector; this test wires a resolver that returns the supplied +// identifiers so the call threads through to the store mock. +func TestAddDevicesToGroup_HappyPath(t *testing.T) { + targetGroupID := int64(11) + deviceIDs := []string{"d1", "d2"} + + h := newGroupHandlerWithResolver(t, deviceIDs) + gomock.InOrder( + h.collectionStore.EXPECT(). + GetCollection(gomock.Any(), testOrgID, targetGroupID). + Return(&collectionpb.DeviceCollection{Id: targetGroupID, Label: "Group A", Type: collectionpb.CollectionType_COLLECTION_TYPE_GROUP}, nil), + h.collectionStore.EXPECT(). + AddDevicesToCollection(gomock.Any(), testOrgID, targetGroupID, deviceIDs). + Return(int64(2), nil), + ) + + resp, err := h.handler.AddDevicesToGroup(testCtx(t), connect.NewRequest(&dspb.AddDevicesToGroupRequest{ + TargetGroupId: targetGroupID, + DeviceSelector: deviceListSelector(deviceIDs...), + })) + require.NoError(t, err) + assert.Equal(t, int64(2), resp.Msg.AddedCount) +} + +// TestAddDevicesToGroup_RejectsRackTarget asserts the handler returns +// InvalidArgument when target_group_id points at a rack, so callers +// can't smuggle a rack mutation through the group endpoint and bypass +// AssignDevicesToRack's atomic prior-rack removal + site cascade. +func TestAddDevicesToGroup_RejectsRackTarget(t *testing.T) { + h := newTestHandler(t) + + targetID := int64(11) + h.collectionStore.EXPECT(). + GetCollection(gomock.Any(), testOrgID, targetID). + Return(&collectionpb.DeviceCollection{Id: targetID, Label: "Rack-X", Type: collectionpb.CollectionType_COLLECTION_TYPE_RACK}, nil) + + _, err := h.handler.AddDevicesToGroup(testCtx(t), connect.NewRequest(&dspb.AddDevicesToGroupRequest{ + TargetGroupId: targetID, + DeviceSelector: deviceListSelector("d1"), + })) + require.Error(t, err) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe) + assert.Equal(t, connect.CodeInvalidArgument, fe.GRPCCode) +} + +// TestAddDevicesToGroup_PermissionRequired confirms the PermRackManage +// gate. No store calls fire when the caller lacks the permission. +func TestAddDevicesToGroup_PermissionRequired(t *testing.T) { + h := newTestHandler(t) + + ctx := ctxWithPerms(authz.PermSiteRead) + _, err := h.handler.AddDevicesToGroup(ctx, connect.NewRequest(&dspb.AddDevicesToGroupRequest{ + TargetGroupId: 42, + DeviceSelector: deviceListSelector("d1"), + })) + require.Error(t, err) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe) + assert.Equal(t, connect.CodePermissionDenied, fe.GRPCCode) +} + +// TestRemoveDevicesFromGroup_HappyPath asserts the handler verifies the +// target is a group and forwards to the group remove path. +func TestRemoveDevicesFromGroup_HappyPath(t *testing.T) { + targetGroupID := int64(11) + deviceIDs := []string{"d1"} + + h := newGroupHandlerWithResolver(t, deviceIDs) + gomock.InOrder( + h.collectionStore.EXPECT(). + GetCollection(gomock.Any(), testOrgID, targetGroupID). + Return(&collectionpb.DeviceCollection{Id: targetGroupID, Label: "Group A", Type: collectionpb.CollectionType_COLLECTION_TYPE_GROUP}, nil), + h.collectionStore.EXPECT(). + RemoveDevicesFromCollection(gomock.Any(), testOrgID, targetGroupID, deviceIDs). + Return(int64(1), nil), + ) + + resp, err := h.handler.RemoveDevicesFromGroup(testCtx(t), connect.NewRequest(&dspb.RemoveDevicesFromGroupRequest{ + TargetGroupId: targetGroupID, + DeviceSelector: deviceListSelector(deviceIDs...), + })) + require.NoError(t, err) + assert.Equal(t, int64(1), resp.Msg.RemovedCount) +} + +// TestRemoveDevicesFromGroup_RejectsRackTarget asserts a rack target +// returns InvalidArgument with no follow-up store mutation. +func TestRemoveDevicesFromGroup_RejectsRackTarget(t *testing.T) { + h := newTestHandler(t) + + targetID := int64(11) + h.collectionStore.EXPECT(). + GetCollection(gomock.Any(), testOrgID, targetID). + Return(&collectionpb.DeviceCollection{Id: targetID, Label: "Rack-X", Type: collectionpb.CollectionType_COLLECTION_TYPE_RACK}, nil) + + _, err := h.handler.RemoveDevicesFromGroup(testCtx(t), connect.NewRequest(&dspb.RemoveDevicesFromGroupRequest{ + TargetGroupId: targetID, + DeviceSelector: deviceListSelector("d1"), + })) + require.Error(t, err) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe) + assert.Equal(t, connect.CodeInvalidArgument, fe.GRPCCode) +} + +// TestRemoveDevicesFromGroup_PermissionRequired confirms the +// PermRackManage gate. +func TestRemoveDevicesFromGroup_PermissionRequired(t *testing.T) { + h := newTestHandler(t) + + ctx := ctxWithPerms(authz.PermSiteRead) + _, err := h.handler.RemoveDevicesFromGroup(ctx, connect.NewRequest(&dspb.RemoveDevicesFromGroupRequest{ + TargetGroupId: 42, + DeviceSelector: deviceListSelector("d1"), + })) + require.Error(t, err) + var fe fleeterror.FleetError + require.ErrorAs(t, err, &fe) + assert.Equal(t, connect.CodePermissionDenied, fe.GRPCCode) +} + +// newGroupHandlerWithResolver builds a harness like newTestHandler but +// wires a resolver that returns the supplied identifiers for any +// selector. The default harness uses a noopResolver that returns nil, +// which is fine for handler-level rejection tests but not for the +// group happy-path tests that need to thread a non-empty identifier +// list through to AddDevicesToGroup / RemoveDevicesFromGroup. +func newGroupHandlerWithResolver(t *testing.T, ids []string) *testHarness { + t.Helper() + ctrl := gomock.NewController(t) + + collectionStore := mocks.NewMockCollectionStore(ctrl) + buildingStore := mocks.NewMockBuildingStore(ctrl) + tx := mocks.NewMockTransactor(ctrl) + tx.EXPECT().RunInTx(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( + func(ctx context.Context, fn func(context.Context) error) error { + return fn(ctx) + }, + ) + tx.EXPECT().RunInTxWithResult(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( + func(ctx context.Context, fn func(context.Context) (any, error)) (any, error) { + return fn(ctx) + }, + ) + + activityStore := mocks.NewMockActivityStore(ctrl) + activityStore.EXPECT().Insert(gomock.Any(), gomock.Any()).AnyTimes().Return(nil) + activitySvc := activity.NewService(activityStore) + + resolver := func(_ context.Context, _ *commonpb.DeviceSelector, _ int64) ([]string, error) { + return ids, nil + } + + svc := collection.NewService( + collectionStore, + nil, + nil, + buildingStore, + tx, + resolver, + nil, + activitySvc, + ) + return &testHarness{ + handler: NewHandler(svc), + collectionStore: collectionStore, + buildingStore: buildingStore, + ctrl: ctrl, + } +} + +func ptrInt64Local(v int64) *int64 { return &v } diff --git a/server/internal/handlers/deviceset/translate.go b/server/internal/handlers/deviceset/translate.go new file mode 100644 index 000000000..4dff4ee21 --- /dev/null +++ b/server/internal/handlers/deviceset/translate.go @@ -0,0 +1,55 @@ +package deviceset + +import ( + commonpb "github.com/block/proto-fleet/server/generated/grpc/common/v1" + dspb "github.com/block/proto-fleet/server/generated/grpc/device_set/v1" + "github.com/block/proto-fleet/server/internal/domain/collection" + "github.com/block/proto-fleet/server/internal/domain/fleeterror" +) + +// toAssignDevicesToRackParams translates the proto request into the +// domain-layer input shape. Mirrors the proto→domain helpers under +// server/internal/handlers/sites/translate.go so handler bodies stay +// focused on auth + wiring. +// +// Only the device_list selector variant is accepted. all_devices is +// rejected with InvalidArgument because moving every paired device +// into a single rack is never the intended operation; filter-based +// selectors are reserved for a future expansion. +func toAssignDevicesToRackParams(req *dspb.AssignDevicesToRackRequest, orgID int64) (collection.AssignDevicesToRackParams, error) { + var targetRackID *int64 + if req.TargetRackId != nil { + v := req.GetTargetRackId() + targetRackID = &v + } + identifiers, err := identifiersFromAssignSelector(req.GetDeviceSelector()) + if err != nil { + return collection.AssignDevicesToRackParams{}, err + } + return collection.AssignDevicesToRackParams{ + OrgID: orgID, + TargetRackID: targetRackID, + DeviceIdentifiers: identifiers, + }, nil +} + +// identifiersFromAssignSelector enforces the device_list-only contract +// for AssignDevicesToRack. The all_devices variant and the unset +// oneof are both rejected with InvalidArgument so callers learn +// up-front instead of getting a 0-row response or a panic downstream. +func identifiersFromAssignSelector(sel *commonpb.DeviceSelector) ([]string, error) { + if sel == nil { + return nil, fleeterror.NewInvalidArgumentError("device_selector is required") + } + switch v := sel.GetSelectionType().(type) { + case *commonpb.DeviceSelector_DeviceList: + if v.DeviceList == nil { + return nil, fleeterror.NewInvalidArgumentError("device_selector.device_list is required") + } + return v.DeviceList.GetDeviceIdentifiers(), nil + case *commonpb.DeviceSelector_AllDevices: + return nil, fleeterror.NewInvalidArgumentError("device_selector.all_devices is not supported for AssignDevicesToRack; pass an explicit device_list") + default: + return nil, fleeterror.NewInvalidArgumentError("device_selector must set device_list") + } +} diff --git a/server/internal/handlers/middleware/rpc_permissions.go b/server/internal/handlers/middleware/rpc_permissions.go index fdcd8ac56..ff8f93d42 100644 --- a/server/internal/handlers/middleware/rpc_permissions.go +++ b/server/internal/handlers/middleware/rpc_permissions.go @@ -89,15 +89,15 @@ var ProcedurePermissions = map[string]string{ authv1connect.AuthServiceListUsersProcedure: authz.PermUserRead, // Buildings CRUD — site:read for reads, site:manage for writes. - // ListBuildingRacks is a building-scoped read; AssignRackToBuilding + // ListBuildingRacks is a building-scoped read; AssignRacksToBuilding // mutates the rack's building/site/zone/grid placement. - buildingsv1connect.BuildingServiceListBuildingsProcedure: authz.PermSiteRead, - buildingsv1connect.BuildingServiceGetBuildingProcedure: authz.PermSiteRead, - buildingsv1connect.BuildingServiceListBuildingRacksProcedure: authz.PermSiteRead, - buildingsv1connect.BuildingServiceCreateBuildingProcedure: authz.PermSiteManage, - buildingsv1connect.BuildingServiceUpdateBuildingProcedure: authz.PermSiteManage, - buildingsv1connect.BuildingServiceDeleteBuildingProcedure: authz.PermSiteManage, - buildingsv1connect.BuildingServiceAssignRackToBuildingProcedure: authz.PermSiteManage, + buildingsv1connect.BuildingServiceListBuildingsProcedure: authz.PermSiteRead, + buildingsv1connect.BuildingServiceGetBuildingProcedure: authz.PermSiteRead, + buildingsv1connect.BuildingServiceListBuildingRacksProcedure: authz.PermSiteRead, + buildingsv1connect.BuildingServiceCreateBuildingProcedure: authz.PermSiteManage, + buildingsv1connect.BuildingServiceUpdateBuildingProcedure: authz.PermSiteManage, + buildingsv1connect.BuildingServiceDeleteBuildingProcedure: authz.PermSiteManage, + buildingsv1connect.BuildingServiceAssignRacksToBuildingProcedure: authz.PermSiteManage, // GetBuildingStats also calls RequirePermission(PermFleetRead) and // RequirePermission(PermMinerRead) inline — those gate the telemetry // rollup and the device_identifiers surface respectively. The map @@ -142,42 +142,41 @@ var ProcedurePermissions = map[string]string{ // Collections are the legacy name for racks; the wire surface still // carries Collection-prefixed names while the domain has been // renamed. - collectionv1connect.DeviceCollectionServiceGetCollectionProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceGetCollectionStatsProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceListCollectionsProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceListCollectionMembersProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceGetDeviceCollectionsProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceListRackTypesProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceListRackZonesProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceGetRackSlotsProcedure: authz.PermRackRead, - collectionv1connect.DeviceCollectionServiceCreateCollectionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceUpdateCollectionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceDeleteCollectionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceAddDevicesToCollectionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceRemoveDevicesFromCollectionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceSaveRackProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceSetRackSlotPositionProcedure: authz.PermRackManage, - collectionv1connect.DeviceCollectionServiceClearRackSlotPositionProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceGetCollectionProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceGetCollectionStatsProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceListCollectionsProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceListCollectionMembersProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceGetDeviceCollectionsProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceListRackTypesProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceListRackZonesProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceGetRackSlotsProcedure: authz.PermRackRead, + collectionv1connect.DeviceCollectionServiceCreateCollectionProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceUpdateCollectionProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceDeleteCollectionProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceSaveRackProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceSetRackSlotPositionProcedure: authz.PermRackManage, + collectionv1connect.DeviceCollectionServiceClearRackSlotPositionProcedure: authz.PermRackManage, // DeviceSetService (racks via the new wire surface) — same mapping // as DeviceCollectionService; the handler is a proto-adapter shim. - device_setv1connect.DeviceSetServiceGetDeviceSetProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceGetDeviceSetStatsProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceListDeviceSetsProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceListDeviceSetMembersProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceGetDeviceDeviceSetsProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceListRackTypesProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceListRackZonesProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceListRackZoneRefsProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceGetRackSlotsProcedure: authz.PermRackRead, - device_setv1connect.DeviceSetServiceCreateDeviceSetProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceUpdateDeviceSetProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceDeleteDeviceSetProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceAddDevicesToDeviceSetProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceRemoveDevicesFromDeviceSetProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceSaveRackProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceSetRackSlotPositionProcedure: authz.PermRackManage, - device_setv1connect.DeviceSetServiceClearRackSlotPositionProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceGetDeviceSetProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceGetDeviceSetStatsProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceListDeviceSetsProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceListDeviceSetMembersProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceGetDeviceDeviceSetsProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceListRackTypesProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceListRackZonesProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceListRackZoneRefsProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceGetRackSlotsProcedure: authz.PermRackRead, + device_setv1connect.DeviceSetServiceCreateDeviceSetProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceUpdateDeviceSetProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceDeleteDeviceSetProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceAddDevicesToGroupProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceRemoveDevicesFromGroupProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceSaveRackProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceAssignDevicesToRackProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceSetRackSlotPositionProcedure: authz.PermRackManage, + device_setv1connect.DeviceSetServiceClearRackSlotPositionProcedure: authz.PermRackManage, // ErrorQueryService — fleet:read; diagnostics are scoped to the org // and live alongside the fleet dashboard. @@ -287,8 +286,9 @@ var ProcedurePermissions = map[string]string{ sitesv1connect.SiteServiceCreateSiteProcedure: authz.PermSiteManage, sitesv1connect.SiteServiceUpdateSiteProcedure: authz.PermSiteManage, sitesv1connect.SiteServiceDeleteSiteProcedure: authz.PermSiteManage, - sitesv1connect.SiteServiceReassignDevicesToSiteProcedure: authz.PermSiteManage, - sitesv1connect.SiteServiceAssignBuildingToSiteProcedure: authz.PermSiteManage, + sitesv1connect.SiteServiceAssignDevicesToSiteProcedure: authz.PermSiteManage, + sitesv1connect.SiteServiceAssignBuildingsToSiteProcedure: authz.PermSiteManage, + sitesv1connect.SiteServiceAssignRacksToSiteProcedure: authz.PermSiteManage, // GetSiteStats also calls RequirePermission(PermFleetRead) inline to // cover the aggregate telemetry surface (matching the gate on // telemetry.GetCombinedMetrics). The map entry is the primary gate. diff --git a/server/internal/handlers/sites/handler.go b/server/internal/handlers/sites/handler.go index ea37ed5ec..f02bf0e85 100644 --- a/server/internal/handlers/sites/handler.go +++ b/server/internal/handlers/sites/handler.go @@ -86,36 +86,51 @@ func (h *Handler) DeleteSite(ctx context.Context, req *connect.Request[pb.Delete }), nil } -func (h *Handler) ReassignDevicesToSite(ctx context.Context, req *connect.Request[pb.ReassignDevicesToSiteRequest]) (*connect.Response[pb.ReassignDevicesToSiteResponse], error) { +func (h *Handler) AssignDevicesToSite(ctx context.Context, req *connect.Request[pb.AssignDevicesToSiteRequest]) (*connect.Response[pb.AssignDevicesToSiteResponse], error) { info, err := middleware.RequirePermission(ctx, authz.PermSiteManage, authz.ResourceContext{}) if err != nil { return nil, err } - count, conflicts, err := h.service.ReassignDevicesToSite(ctx, toReassignParams(req.Msg, info.OrganizationID)) + count, conflicts, err := h.service.AssignDevicesToSite(ctx, toAssignDevicesParams(req.Msg, info.OrganizationID)) if err != nil { return nil, err } - return connect.NewResponse(&pb.ReassignDevicesToSiteResponse{ + return connect.NewResponse(&pb.AssignDevicesToSiteResponse{ ReassignedCount: count, Conflicts: toProtoConflicts(conflicts), }), nil } -func (h *Handler) AssignBuildingToSite(ctx context.Context, req *connect.Request[pb.AssignBuildingToSiteRequest]) (*connect.Response[pb.AssignBuildingToSiteResponse], error) { +func (h *Handler) AssignBuildingsToSite(ctx context.Context, req *connect.Request[pb.AssignBuildingsToSiteRequest]) (*connect.Response[pb.AssignBuildingsToSiteResponse], error) { info, err := middleware.RequirePermission(ctx, authz.PermSiteManage, authz.ResourceContext{}) if err != nil { return nil, err } - out, err := h.service.AssignBuildingToSite(ctx, toAssignBuildingParams(req.Msg, info.OrganizationID)) + out, err := h.service.AssignBuildingsToSite(ctx, toAssignBuildingsParams(req.Msg, info.OrganizationID)) if err != nil { return nil, err } - return connect.NewResponse(&pb.AssignBuildingToSiteResponse{ + return connect.NewResponse(&pb.AssignBuildingsToSiteResponse{ ReassignedRackCount: out.ReassignedRackCount, ReassignedDeviceCount: out.ReassignedDeviceCount, }), nil } +func (h *Handler) AssignRacksToSite(ctx context.Context, req *connect.Request[pb.AssignRacksToSiteRequest]) (*connect.Response[pb.AssignRacksToSiteResponse], error) { + info, err := middleware.RequirePermission(ctx, authz.PermSiteManage, authz.ResourceContext{}) + if err != nil { + return nil, err + } + out, err := h.service.AssignRacksToSite(ctx, toAssignRacksToSiteParams(req.Msg, info.OrganizationID)) + if err != nil { + return nil, err + } + return connect.NewResponse(&pb.AssignRacksToSiteResponse{ + ReassignedDeviceCount: out.ReassignedDeviceCount, + ClearedBuildingCount: out.ClearedBuildingCount, + }), nil +} + func (h *Handler) GetSiteStats(ctx context.Context, req *connect.Request[pb.GetSiteStatsRequest]) (*connect.Response[pb.GetSiteStatsResponse], error) { // GetSiteStats returns telemetry rollups + miner health buckets, so // site:read alone isn't enough; we also gate on fleet:read. Both checks diff --git a/server/internal/handlers/sites/handler_stats_test.go b/server/internal/handlers/sites/handler_stats_test.go index 992d105d1..d15f2579b 100644 --- a/server/internal/handlers/sites/handler_stats_test.go +++ b/server/internal/handlers/sites/handler_stats_test.go @@ -69,7 +69,7 @@ func newStatsHandler(t *testing.T) *statsHarness { ) deviceQueryer := &fakeDeviceQueryer{} telemetry := &fakeTelemetryCollector{} - svc := sites.NewService(siteStore, buildingStore, deviceQueryer, telemetry, tx, nil) + svc := sites.NewService(siteStore, buildingStore, nil, deviceQueryer, telemetry, tx, nil) return &statsHarness{ handler: NewHandler(svc), siteStore: siteStore, diff --git a/server/internal/handlers/sites/handler_test.go b/server/internal/handlers/sites/handler_test.go index 8c48cf4be..00ec4df76 100644 --- a/server/internal/handlers/sites/handler_test.go +++ b/server/internal/handlers/sites/handler_test.go @@ -14,6 +14,7 @@ import ( "github.com/block/proto-fleet/server/internal/domain/fleeterror" "github.com/block/proto-fleet/server/internal/domain/sites" "github.com/block/proto-fleet/server/internal/domain/sites/models" + "github.com/block/proto-fleet/server/internal/domain/stores/interfaces" "github.com/block/proto-fleet/server/internal/domain/stores/interfaces/mocks" "github.com/block/proto-fleet/server/internal/handlers/handlerstest" ) @@ -23,16 +24,18 @@ import ( // the service's logActivity guards against that path so audit fire-and- // forget no-ops in tests. type testHarness struct { - handler *Handler - siteStore *mocks.MockSiteStore - tx *mocks.MockTransactor - ctrl *gomock.Controller + handler *Handler + siteStore *mocks.MockSiteStore + collectionStore *mocks.MockCollectionStore + tx *mocks.MockTransactor + ctrl *gomock.Controller } func newTestHandler(t *testing.T) *testHarness { t.Helper() ctrl := gomock.NewController(t) siteStore := mocks.NewMockSiteStore(ctrl) + collectionStore := mocks.NewMockCollectionStore(ctrl) tx := mocks.NewMockTransactor(ctrl) // RunInTx fake: runs the closure inline so cascade calls land // against the mock store without a real DB. @@ -43,12 +46,13 @@ func newTestHandler(t *testing.T) *testHarness { ) // GetSiteStats isn't exercised by these tests; pass nil for the // stats-only dependencies and rely on the service's nil-guard. - svc := sites.NewService(siteStore, nil, nil, nil, tx, nil) + svc := sites.NewService(siteStore, nil, collectionStore, nil, nil, tx, nil) return &testHarness{ - handler: NewHandler(svc), - siteStore: siteStore, - tx: tx, - ctrl: ctrl, + handler: NewHandler(svc), + siteStore: siteStore, + collectionStore: collectionStore, + tx: tx, + ctrl: ctrl, } } @@ -223,7 +227,7 @@ func TestHandler_DeleteSite_surfacesCascadeCounts(t *testing.T) { assert.Equal(t, int64(4), resp.Msg.GetUnassignedRackCount()) } -func TestHandler_ReassignDevicesToSite_success(t *testing.T) { +func TestHandler_AssignDevicesToSite_success(t *testing.T) { t.Parallel() h := newTestHandler(t) @@ -234,9 +238,9 @@ func TestHandler_ReassignDevicesToSite_success(t *testing.T) { h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) h.siteStore.EXPECT().ListExistingDeviceIdentifiers(gomock.Any(), int64(7), idents).Return(idents, nil) h.siteStore.EXPECT().FindDeviceSiteConflicts(gomock.Any(), int64(7), idents).Return(map[string]int64{}, nil) - h.siteStore.EXPECT().ReassignDevicesToSite(gomock.Any(), int64(7), gomock.AssignableToTypeOf(ptrInt64(0)), idents).Return(int64(2), nil) + h.siteStore.EXPECT().AssignDevicesToSite(gomock.Any(), int64(7), gomock.AssignableToTypeOf(ptrInt64(0)), idents).Return(int64(2), nil) - resp, err := h.handler.ReassignDevicesToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.ReassignDevicesToSiteRequest{ + resp, err := h.handler.AssignDevicesToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignDevicesToSiteRequest{ TargetSiteId: &target, DeviceIdentifiers: idents, })) @@ -245,7 +249,7 @@ func TestHandler_ReassignDevicesToSite_success(t *testing.T) { assert.Empty(t, resp.Msg.GetConflicts()) } -func TestHandler_ReassignDevicesToSite_conflictsReturnTypedReason(t *testing.T) { +func TestHandler_AssignDevicesToSite_conflictsReturnTypedReason(t *testing.T) { t.Parallel() h := newTestHandler(t) @@ -259,9 +263,9 @@ func TestHandler_ReassignDevicesToSite_conflictsReturnTypedReason(t *testing.T) h.siteStore.EXPECT().FindDeviceSiteConflicts(gomock.Any(), int64(7), idents).Return(map[string]int64{ "d1": conflictingSite, }, nil) - // No ReassignDevicesToSite store call — conflict path rejects entire batch. + // No AssignDevicesToSite store call — conflict path rejects entire batch. - resp, err := h.handler.ReassignDevicesToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.ReassignDevicesToSiteRequest{ + resp, err := h.handler.AssignDevicesToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignDevicesToSiteRequest{ TargetSiteId: &target, DeviceIdentifiers: idents, })) @@ -274,7 +278,7 @@ func TestHandler_ReassignDevicesToSite_conflictsReturnTypedReason(t *testing.T) assert.Equal(t, conflictingSite, c.GetConflictingSiteId()) } -func TestHandler_AssignBuildingToSite_surfacesCascadeCounts(t *testing.T) { +func TestHandler_AssignBuildingsToSite_surfacesCascadeCounts(t *testing.T) { t.Parallel() h := newTestHandler(t) @@ -282,12 +286,12 @@ func TestHandler_AssignBuildingToSite_surfacesCascadeCounts(t *testing.T) { h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) h.siteStore.EXPECT().LockBuildingForWrite(gomock.Any(), int64(7), int64(50)).Return(nil) - h.siteStore.EXPECT().AssignBuildingToSite(gomock.Any(), int64(7), int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(1), nil) - h.siteStore.EXPECT().ReassignRacksUnderBuilding(gomock.Any(), int64(7), int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(3), nil) - h.siteStore.EXPECT().ReassignDevicesUnderBuilding(gomock.Any(), int64(7), int64(50), gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(15), nil) + h.siteStore.EXPECT().AssignBuildingsToSiteBulk(gomock.Any(), int64(7), []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(1), nil) + h.siteStore.EXPECT().ReassignRacksUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(3), nil) + h.siteStore.EXPECT().ReassignDevicesUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(15), nil) - resp, err := h.handler.AssignBuildingToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignBuildingToSiteRequest{ - BuildingId: 50, + resp, err := h.handler.AssignBuildingsToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignBuildingsToSiteRequest{ + BuildingIds: []int64{50}, TargetSiteId: &target, })) require.NoError(t, err) @@ -295,22 +299,144 @@ func TestHandler_AssignBuildingToSite_surfacesCascadeCounts(t *testing.T) { assert.Equal(t, int64(15), resp.Msg.GetReassignedDeviceCount()) } -func TestHandler_AssignBuildingToSite_targetUnsetCascadesToUnassigned(t *testing.T) { +func TestHandler_AssignBuildingsToSite_targetUnsetCascadesToUnassigned(t *testing.T) { t.Parallel() h := newTestHandler(t) // target_site_id unset → service skips LockSiteForWrite (no target - // site to lock) but still locks the building before the cascade, - // then passes a nil targetSiteID through. + // site to lock) but still locks the building before the bulk cascade + // writes, then passes a nil targetSiteID through. h.siteStore.EXPECT().LockBuildingForWrite(gomock.Any(), int64(7), int64(50)).Return(nil) - h.siteStore.EXPECT().AssignBuildingToSite(gomock.Any(), int64(7), int64(50), gomock.Nil()).Return(int64(1), nil) - h.siteStore.EXPECT().ReassignRacksUnderBuilding(gomock.Any(), int64(7), int64(50), gomock.Nil()).Return(int64(0), nil) - h.siteStore.EXPECT().ReassignDevicesUnderBuilding(gomock.Any(), int64(7), int64(50), gomock.Nil()).Return(int64(0), nil) + h.siteStore.EXPECT().AssignBuildingsToSiteBulk(gomock.Any(), int64(7), []int64{50}, gomock.Nil()).Return(int64(1), nil) + h.siteStore.EXPECT().ReassignRacksUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50}, gomock.Nil()).Return(int64(0), nil) + h.siteStore.EXPECT().ReassignDevicesUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50}, gomock.Nil()).Return(int64(0), nil) - resp, err := h.handler.AssignBuildingToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignBuildingToSiteRequest{ - BuildingId: 50, + resp, err := h.handler.AssignBuildingsToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignBuildingsToSiteRequest{ + BuildingIds: []int64{50}, })) require.NoError(t, err) assert.Equal(t, int64(0), resp.Msg.GetReassignedRackCount()) assert.Equal(t, int64(0), resp.Msg.GetReassignedDeviceCount()) } + +func TestHandler_AssignBuildingsToSite_bulkAggregatesCascadeCounts(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + target := int64(20) + + // Phase A locks both buildings in sorted ID order; Phase B issues + // one bulk write per kind across both buildings. + h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) + h.siteStore.EXPECT().LockBuildingForWrite(gomock.Any(), int64(7), int64(50)).Return(nil) + h.siteStore.EXPECT().LockBuildingForWrite(gomock.Any(), int64(7), int64(51)).Return(nil) + h.siteStore.EXPECT().AssignBuildingsToSiteBulk(gomock.Any(), int64(7), []int64{50, 51}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(2), nil) + h.siteStore.EXPECT().ReassignRacksUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50, 51}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(6), nil) + h.siteStore.EXPECT().ReassignDevicesUnderBuildingsBulk(gomock.Any(), int64(7), []int64{50, 51}, gomock.AssignableToTypeOf(ptrInt64(0))).Return(int64(30), nil) + + // Pass IDs out of order to verify deterministic locking via sort. + resp, err := h.handler.AssignBuildingsToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignBuildingsToSiteRequest{ + BuildingIds: []int64{51, 50}, + TargetSiteId: &target, + })) + require.NoError(t, err) + assert.Equal(t, int64(6), resp.Msg.GetReassignedRackCount()) + assert.Equal(t, int64(30), resp.Msg.GetReassignedDeviceCount()) +} + +func TestHandler_AssignRacksToSite_partialUpdateCascadesAndClearsBuilding(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + target := int64(20) + rackID := int64(50) + priorSite := int64(9) + priorBuilding := int64(11) + + h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) + h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, int64(7)). + Return(interfaces.RackPlacement{SiteID: &priorSite, BuildingID: &priorBuilding, Zone: "Z1"}, nil) + // Bulk placement update clears building + zone in SQL; bulk cascade + // follows for the same rack set. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForSite(gomock.Any(), int64(7), []int64{rackID}, &target).Return(nil) + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(gomock.Any(), int64(7), []int64{rackID}, &target).Return(int64(8), nil) + + resp, err := h.handler.AssignRacksToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRacksToSiteRequest{ + RackIds: []int64{rackID}, + TargetSiteId: &target, + })) + require.NoError(t, err) + assert.Equal(t, int64(8), resp.Msg.GetReassignedDeviceCount()) + assert.Equal(t, int64(1), resp.Msg.GetClearedBuildingCount()) +} + +func TestHandler_AssignRacksToSite_targetUnsetUnassigns(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + rackID := int64(50) + priorSite := int64(9) + + // target_site_id unset → no LockSiteForWrite (no target to lock). + h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, int64(7)). + Return(interfaces.RackPlacement{SiteID: &priorSite}, nil) // no building set + // site changes (priorSite → nil) but no building to clear. + h.collectionStore.EXPECT().UpdateRackPlacementBulkForSite(gomock.Any(), int64(7), []int64{rackID}, gomock.Nil()).Return(nil) + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(gomock.Any(), int64(7), []int64{rackID}, gomock.Nil()).Return(int64(3), nil) + + resp, err := h.handler.AssignRacksToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRacksToSiteRequest{ + RackIds: []int64{rackID}, + })) + require.NoError(t, err) + assert.Equal(t, int64(3), resp.Msg.GetReassignedDeviceCount()) + assert.Equal(t, int64(0), resp.Msg.GetClearedBuildingCount()) +} + +func TestHandler_AssignRacksToSite_sameSiteIsNoOp(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + target := int64(20) + rackID := int64(50) + + h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) + // rack already at target site — filtered out of the bulk write set; + // no UpdateRackPlacementBulkForSite or CascadeRackDeviceSitesBulk call. + h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, int64(7)). + Return(interfaces.RackPlacement{SiteID: &target, BuildingID: ptrInt64(11), Zone: "Z1"}, nil) + + resp, err := h.handler.AssignRacksToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRacksToSiteRequest{ + RackIds: []int64{rackID}, + TargetSiteId: &target, + })) + require.NoError(t, err) + assert.Equal(t, int64(0), resp.Msg.GetReassignedDeviceCount()) + assert.Equal(t, int64(0), resp.Msg.GetClearedBuildingCount()) +} + +func TestHandler_AssignRacksToSite_bulkAggregates(t *testing.T) { + t.Parallel() + h := newTestHandler(t) + + target := int64(20) + priorSite := int64(9) + + h.siteStore.EXPECT().LockSiteForWrite(gomock.Any(), int64(7), target).Return(nil) + // Phase A locks both racks in sorted id order; Phase B issues one + // bulk placement update and one bulk cascade across both racks. + h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), int64(50), int64(7)). + Return(interfaces.RackPlacement{SiteID: &priorSite, BuildingID: ptrInt64(11)}, nil) + h.collectionStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), int64(51), int64(7)). + Return(interfaces.RackPlacement{SiteID: &priorSite}, nil) // no building + h.collectionStore.EXPECT().UpdateRackPlacementBulkForSite(gomock.Any(), int64(7), []int64{50, 51}, &target).Return(nil) + h.collectionStore.EXPECT().CascadeRackDeviceSitesBulk(gomock.Any(), int64(7), []int64{50, 51}, &target).Return(int64(6), nil) + + // IDs passed out-of-order to verify the sort happens. + resp, err := h.handler.AssignRacksToSite(sitePermsCtx(t, 7), connect.NewRequest(&pb.AssignRacksToSiteRequest{ + RackIds: []int64{51, 50}, + TargetSiteId: &target, + })) + require.NoError(t, err) + assert.Equal(t, int64(6), resp.Msg.GetReassignedDeviceCount()) + assert.Equal(t, int64(1), resp.Msg.GetClearedBuildingCount(), "only rack with prior building counts") +} diff --git a/server/internal/handlers/sites/translate.go b/server/internal/handlers/sites/translate.go index 964ea7401..725dd0ecd 100644 --- a/server/internal/handlers/sites/translate.go +++ b/server/internal/handlers/sites/translate.go @@ -41,28 +41,42 @@ func toUpdateSiteParams(req *pb.UpdateSiteRequest, orgID int64) models.UpdateSit } } -func toReassignParams(req *pb.ReassignDevicesToSiteRequest, orgID int64) models.ReassignDevicesToSiteParams { +func toAssignDevicesParams(req *pb.AssignDevicesToSiteRequest, orgID int64) models.AssignDevicesToSiteParams { var targetSiteID *int64 if req.TargetSiteId != nil { v := req.GetTargetSiteId() targetSiteID = &v } - return models.ReassignDevicesToSiteParams{ - OrgID: orgID, - TargetSiteID: targetSiteID, - DeviceIdentifiers: req.GetDeviceIdentifiers(), + return models.AssignDevicesToSiteParams{ + OrgID: orgID, + TargetSiteID: targetSiteID, + DeviceIdentifiers: req.GetDeviceIdentifiers(), + ForceClearConflictingRackMembership: req.GetForceClearConflictingRackMembership(), } } -func toAssignBuildingParams(req *pb.AssignBuildingToSiteRequest, orgID int64) models.AssignBuildingToSiteParams { +func toAssignBuildingsParams(req *pb.AssignBuildingsToSiteRequest, orgID int64) models.AssignBuildingsToSiteParams { var targetSiteID *int64 if req.TargetSiteId != nil { v := req.GetTargetSiteId() targetSiteID = &v } - return models.AssignBuildingToSiteParams{ + return models.AssignBuildingsToSiteParams{ OrgID: orgID, - BuildingID: req.GetBuildingId(), + BuildingIDs: req.GetBuildingIds(), + TargetSiteID: targetSiteID, + } +} + +func toAssignRacksToSiteParams(req *pb.AssignRacksToSiteRequest, orgID int64) models.AssignRacksToSiteParams { + var targetSiteID *int64 + if req.TargetSiteId != nil { + v := req.GetTargetSiteId() + targetSiteID = &v + } + return models.AssignRacksToSiteParams{ + OrgID: orgID, + RackIDs: req.GetRackIds(), TargetSiteID: targetSiteID, } } diff --git a/server/sqlc/queries/building.sql b/server/sqlc/queries/building.sql index 9ba52bf3d..5210d0dc8 100644 --- a/server/sqlc/queries/building.sql +++ b/server/sqlc/queries/building.sql @@ -195,6 +195,58 @@ SET aisle_index = sqlc.narg('aisle_index')::int, WHERE device_set_id = sqlc.arg('rack_id') AND org_id = sqlc.arg('org_id'); +-- name: SetRackBuildingPositionBulkClear :exec +-- Bulk variant of SetRackBuildingPosition that nulls (aisle_index, +-- position_in_aisle) for every rack in @rack_ids. Used by +-- AssignRacksToBuilding's pass-1 vacate so every rack in the batch +-- holds NULL position before pass-2 reclaims cells. Mirrors the +-- single-row query's intentional no-touch on building_id — +-- UpdateRackPlacement already settled that. +UPDATE device_set_rack +SET aisle_index = NULL, + position_in_aisle = NULL +WHERE device_set_id = ANY(sqlc.arg('rack_ids')::bigint[]) + AND org_id = sqlc.arg('org_id'); + +-- name: SetRackBuildingPositionBulkPlace :exec +-- Bulk variant of SetRackBuildingPosition that writes per-rack +-- (aisle_index, position_in_aisle) via parallel arrays joined on +-- ordinal position. Used by AssignRacksToBuilding's pass-2 after +-- pass-1 has vacated every cell touched by the batch. Arrays must be +-- the same length and parallel-aligned with @rack_ids; callers that +-- have a "place nothing" pass-2 should skip the query entirely. +UPDATE device_set_rack dsr +SET aisle_index = u.aisle_index, + position_in_aisle = u.position_in_aisle +FROM ( + SELECT + rack_ids[i] AS rack_id, + aisle_indexes[i] AS aisle_index, + position_in_aisles[i] AS position_in_aisle + FROM ( + SELECT + sqlc.arg('rack_ids')::bigint[] AS rack_ids, + sqlc.arg('aisle_indexes')::int[] AS aisle_indexes, + sqlc.arg('position_in_aisles')::int[] AS position_in_aisles + ) arrs + CROSS JOIN generate_subscripts(arrs.rack_ids, 1) AS i +) AS u +WHERE dsr.device_set_id = u.rack_id + AND dsr.org_id = sqlc.arg('org_id'); + +-- name: AssignBuildingsToSiteBulk :execrows +-- Bulk variant of AssignBuildingToSite. Updates building.site_id for +-- every building in @building_ids in one statement. Caller is +-- expected to have row-locked each building first (canonical lock +-- order: site → buildings). Returns the row count of buildings +-- actually moved (skips soft-deleted rows). +UPDATE building +SET site_id = sqlc.narg('site_id'), + updated_at = CURRENT_TIMESTAMP +WHERE id = ANY(sqlc.arg('building_ids')::bigint[]) + AND org_id = sqlc.arg('org_id') + AND deleted_at IS NULL; + -- name: BuildingBelongsToOrg :one SELECT EXISTS( SELECT 1 FROM building diff --git a/server/sqlc/queries/device_set.sql b/server/sqlc/queries/device_set.sql index 3378b7b6a..885d5ac8c 100644 --- a/server/sqlc/queries/device_set.sql +++ b/server/sqlc/queries/device_set.sql @@ -133,6 +133,90 @@ WHERE device_set_id = sqlc.arg('device_set_id') AND ds.deleted_at IS NULL ); +-- name: UpdateRackPlacementBulkForBuilding :exec +-- Bulk variant of UpdateRackPlacement scoped to AssignRacksToBuilding. +-- Sets site_id, building_id, and zone for every rack in @rack_ids in a +-- single update. +-- +-- Semantics match the per-row UpdateRackPlacement + service-layer +-- zone/site rules: +-- +-- * When @target_building_id IS NULL (unassign branch), each rack +-- keeps its current site_id (no cascade fires later). Otherwise +-- every rack is stamped with @target_site_id. +-- * Zone clears to '' for any rack that had a building and is +-- transitioning to a different (or NULL) building. Racks staying in +-- the same building preserve their zone. +-- * aisle_index / position_in_aisle clear when building_id changes, +-- matching the single-row CASE. +UPDATE device_set_rack dsr +SET site_id = CASE + WHEN sqlc.narg('target_building_id')::bigint IS NULL THEN dsr.site_id + ELSE sqlc.narg('target_site_id')::bigint + END, + building_id = sqlc.narg('target_building_id')::bigint, + zone = CASE + WHEN dsr.building_id IS NOT NULL + AND dsr.building_id IS DISTINCT FROM sqlc.narg('target_building_id')::bigint + THEN '' + ELSE dsr.zone + END, + aisle_index = CASE + WHEN sqlc.narg('target_building_id')::bigint IS DISTINCT FROM dsr.building_id THEN NULL + ELSE dsr.aisle_index + END, + position_in_aisle = CASE + WHEN sqlc.narg('target_building_id')::bigint IS DISTINCT FROM dsr.building_id THEN NULL + ELSE dsr.position_in_aisle + END +WHERE dsr.device_set_id = ANY(sqlc.arg('rack_ids')::bigint[]) + AND dsr.org_id = sqlc.arg('org_id') + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.org_id = sqlc.arg('org_id') + AND ds.deleted_at IS NULL + ); + +-- name: UpdateRackPlacementBulkForSite :exec +-- Bulk variant used by AssignRacksToSite. Stamps every rack in +-- @rack_ids with the target site, clears building_id and zone (because +-- a building belongs to one site, the rack's building membership is +-- invalidated by any site transition), and clears the grid placement +-- as a downstream effect of building_id changing. Caller is expected +-- to only pass racks whose current site differs from the target so the +-- response counts stay accurate. +UPDATE device_set_rack dsr +SET site_id = sqlc.narg('target_site_id')::bigint, + building_id = NULL, + zone = '', + aisle_index = NULL, + position_in_aisle = NULL +WHERE dsr.device_set_id = ANY(sqlc.arg('rack_ids')::bigint[]) + AND dsr.org_id = sqlc.arg('org_id') + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.org_id = sqlc.arg('org_id') + AND ds.deleted_at IS NULL + ); + +-- name: CascadeRackDeviceSitesBulk :execrows +-- Bulk variant of CascadeRackDeviceSites. Rewrites device.site_id to +-- target_site_id for every paired member of every rack in @rack_ids. +-- NULL target unassigns. IS DISTINCT FROM skips no-op rows. Returns +-- the total affected row count across all racks. +UPDATE device d +SET site_id = sqlc.narg('target_site_id')::bigint, + updated_at = CURRENT_TIMESTAMP +FROM device_set_membership dsm +WHERE dsm.device_set_id = ANY(sqlc.arg('rack_ids')::bigint[]) + AND dsm.org_id = sqlc.arg('org_id') + AND dsm.device_set_type = 'rack' + AND dsm.device_id = d.id + AND d.deleted_at IS NULL + AND d.site_id IS DISTINCT FROM sqlc.narg('target_site_id')::bigint; + -- name: SoftDeleteDeviceSet :execrows UPDATE device_set SET deleted_at = CURRENT_TIMESTAMP @@ -211,6 +295,55 @@ DELETE FROM device_set_membership WHERE device_set_id = $1 AND org_id = $2; +-- name: LockRacksForReparent :many +-- Locks every rack involved in a reparent (sources + target) FOR UPDATE +-- in ascending id order. Used by AssignDevicesToRack as the FIRST tx +-- operation. Sorting source and target together in a single lock +-- acquisition is what prevents the deadlock two concurrent +-- AssignDevicesToRack calls moving devices in opposite directions +-- between the same pair of racks would otherwise hit: each tx locks +-- {sourceA, sourceB, ..., target} in id order, so any two txs touching +-- the same rack pair always agree on the global lock order regardless +-- of which side is source vs target. Pass 0 for @target_rack_id on the +-- unassign path (clear-rack -- no target to include). The membership +-- DELETE in RemoveDevicesFromAnyRack still excludes the target via its +-- own predicate; this query is purely about lock order. Distinct ids +-- are derived in a subquery so the outer locking SELECT can use +-- FOR UPDATE (Postgres rejects DISTINCT + FOR UPDATE at runtime). +SELECT ds.id AS device_set_id +FROM device_set ds +WHERE ds.id IN ( + SELECT dsm.device_set_id + FROM device_set_membership dsm + WHERE dsm.org_id = @org_id + AND dsm.device_set_type = 'rack' + AND dsm.device_identifier = ANY(@device_identifiers::text[]) + UNION + SELECT @target_rack_id::bigint + WHERE @target_rack_id::bigint > 0 + ) + AND ds.org_id = @org_id + AND ds.type = 'rack' + AND ds.deleted_at IS NULL +ORDER BY ds.id ASC +FOR UPDATE; + +-- name: RemoveDevicesFromAnyRack :execrows +-- Removes the given devices from whatever rack they're currently in, +-- EXCEPT the target rack (@target_rack_id). AssignDevicesToRack uses +-- this to clear prior rack membership inside the same transaction as +-- the new-rack insert, closing the orphan window the client-side +-- remove + add orchestration had. Skipping the target rack preserves +-- the existing membership row (and its rack_slot child) when a device +-- is reassigned to the same rack it's already in -- otherwise the +-- DELETE would cascade rack_slot rows that we'd silently lose. Pass +-- 0 for an unconditional clear (caller intends to unassign). +DELETE FROM device_set_membership +WHERE org_id = $1 + AND device_identifier = ANY(@device_identifiers::text[]) + AND device_set_type = 'rack' + AND device_set_id != @target_rack_id::bigint; + -- name: RemoveDevicesFromDeviceSet :execrows DELETE FROM device_set_membership WHERE device_set_id = $1 diff --git a/server/sqlc/queries/site.sql b/server/sqlc/queries/site.sql index c7742d08e..bf2893fa2 100644 --- a/server/sqlc/queries/site.sql +++ b/server/sqlc/queries/site.sql @@ -189,7 +189,7 @@ FOR UPDATE; -- name: LockBuildingForWrite :one -- Row-locks a specific building so concurrent mutations (DeleteSite, --- AssignBuildingToSite, DeleteBuilding) serialize. Returns the building +-- AssignBuildingsToSite, DeleteBuilding) serialize. Returns the building -- id when alive; sql.ErrNoRows when soft-deleted or missing. SELECT id FROM building WHERE id = sqlc.arg('id') @@ -200,7 +200,7 @@ FOR UPDATE; -- name: LockBuildingsBySiteForWrite :many -- Row-locks every live building under the given site so DeleteSite's -- cascade can rewrite their racks without a concurrent --- AssignBuildingToSite slipping a building out from under it. Returns +-- AssignBuildingsToSite slipping a building out from under it. Returns -- the locked ids (result is informational; the FOR UPDATE side-effect -- is what matters). SELECT id FROM building @@ -209,7 +209,7 @@ WHERE org_id = sqlc.arg('org_id') AND deleted_at IS NULL FOR UPDATE; --- name: ReassignDevicesToSite :execrows +-- name: AssignDevicesToSite :execrows -- Bulk update of device.site_id for the given identifiers within the -- org. Caller is expected to have already validated that no device is -- in a rack at a different site (see FindDeviceSiteConflicts). @@ -261,7 +261,7 @@ WHERE d.org_id = sqlc.arg('org_id') -- name: ListExistingDeviceIdentifiers :many -- Filters the requested identifier list down to those that actually -- exist as live devices in the org. Used to surface "device_not_found" --- conflicts in ReassignDevicesToSite without an N+1 lookup. +-- conflicts in AssignDevicesToSite without an N+1 lookup. SELECT device_identifier FROM device WHERE org_id = sqlc.arg('org_id') @@ -295,6 +295,42 @@ WHERE dsr.org_id = sqlc.arg('org_id') AND ds.deleted_at IS NULL ); +-- name: ReassignRacksUnderBuildingsBulk :execrows +-- Bulk variant of ReassignRacksUnderBuilding. Sets rack.site_id = +-- $target for every live rack pointing at any of @building_ids in one +-- statement. Caller wraps in the same tx as the building UPDATE so +-- the building/rack/device site_ids stay in lockstep. +UPDATE device_set_rack dsr +SET site_id = sqlc.narg('target_site_id') +WHERE dsr.org_id = sqlc.arg('org_id') + AND dsr.building_id = ANY(sqlc.arg('building_ids')::bigint[]) + AND EXISTS ( + SELECT 1 FROM device_set ds + WHERE ds.id = dsr.device_set_id + AND ds.deleted_at IS NULL + ); + +-- name: ReassignDevicesUnderBuildingsBulk :execrows +-- Bulk variant of ReassignDevicesUnderBuilding. Sets device.site_id = +-- $target for every device in any live rack of any building in +-- @building_ids. Caller wraps in the same tx as the building UPDATE. +UPDATE device d +SET site_id = sqlc.narg('target_site_id'), + updated_at = CURRENT_TIMESTAMP +FROM device_set_membership dsm +JOIN device_set ds + ON ds.id = dsm.device_set_id + AND ds.deleted_at IS NULL +JOIN device_set_rack dsr + ON dsr.device_set_id = dsm.device_set_id + AND dsr.org_id = dsm.org_id +WHERE d.id = dsm.device_id + AND d.org_id = dsm.org_id + AND dsm.device_set_type = 'rack' + AND d.org_id = sqlc.arg('org_id') + AND dsr.building_id = ANY(sqlc.arg('building_ids')::bigint[]) + AND d.deleted_at IS NULL; + -- name: ReassignDevicesUnderBuilding :execrows -- Sets device.site_id = $target for every device in any live rack of -- the given building. Caller wraps this in the same tx as the building