From 539794607f7102258ba65d19d76ae01bd6c1936f Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 10:14:58 -0700 Subject: [PATCH 01/15] feat: download miner logs via fleet nodes --- .../v1/fleetnodegateway_pb.ts | 620 +++++----- .../v1/fleetnodegateway.proto | 5 + server/cmd/fleetnode/control.go | 2 +- server/cmd/fleetnode/fake_gateway_test.go | 40 + server/cmd/fleetnode/minercommand.go | 120 +- server/cmd/fleetnode/minercommand_test.go | 134 ++- .../v1/fleetnodegateway.pb.go | 1009 +++++++++-------- .../domain/fleetnode/control/command.go | 45 +- .../domain/fleetnode/control/registry_test.go | 77 ++ .../domain/miner/logformat/logformat.go | 98 ++ .../internal/domain/miner/remotenode/miner.go | 136 ++- .../domain/miner/remotenode/miner_test.go | 104 +- server/internal/domain/miner/service.go | 1 + .../internal/domain/plugins/plugin_miner.go | 114 +- .../domain/plugins/plugin_miner_test.go | 3 +- .../internal/infrastructure/files/service.go | 70 +- .../infrastructure/files/service_test.go | 96 ++ 17 files changed, 1682 insertions(+), 992 deletions(-) create mode 100644 server/internal/domain/miner/logformat/logformat.go diff --git a/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts b/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts index 5bfad9123..cfcfbb168 100644 --- a/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts +++ b/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts @@ -30,21 +30,8 @@ import type { Message } from "@bufbuild/protobuf"; /** * Describes the file fleetnodegateway/v1/fleetnodegateway.proto. */ -export const file_fleetnodegateway_v1_fleetnodegateway: GenFile = - /*@__PURE__*/ - fileDesc( - "CipmbGVldG5vZGVnYXRld2F5L3YxL2ZsZWV0bm9kZWdhdGV3YXkucHJvdG8SE2ZsZWV0bm9kZWdhdGV3YXkudjEilwEKD1JlZ2lzdGVyUmVxdWVzdBIkChBlbnJvbGxtZW50X3Rva2VuGAEgASgJQgq6SAdyBRAUGIAEEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESIAoPaWRlbnRpdHlfcHVia2V5GAMgASgMQge6SAR6AmggEiIKEWVuY3J5cHRpb25fcHVia2V5GAQgASgMQge6SAR6AmggIokBChBSZWdpc3RlclJlc3BvbnNlEhUKDWZsZWV0X25vZGVfaWQYASABKAMSQAoRZW5yb2xsbWVudF9zdGF0dXMYAiABKA4yJS5mbGVldG5vZGVnYXRld2F5LnYxLkVucm9sbG1lbnRTdGF0dXMSHAoUaWRlbnRpdHlfZmluZ2VycHJpbnQYAyABKAkiWgoZQmVnaW5BdXRoSGFuZHNoYWtlUmVxdWVzdBIbCgdhcGlfa2V5GAEgASgJQgq6SAdyBRAUGIAEEiAKD2lkZW50aXR5X3B1YmtleRgCIAEoDEIHukgEegJoICJfChpCZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRIRCgljaGFsbGVuZ2UYASABKAwSLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWAocQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVxdWVzdBIcCgljaGFsbGVuZ2UYASABKAxCCbpIBnoEEBAYQBIaCglzaWduYXR1cmUYAiABKAxCB7pIBHoCaEAiZgodQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVzcG9uc2USFQoNc2Vzc2lvbl90b2tlbhgBIAEoCRIuCgpleHBpcmVzX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJtChZVcGxvYWRUZWxlbWV0cnlSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIxChdVcGxvYWRUZWxlbWV0cnlSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJqChNVcGxvYWRFdmVudHNSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIuChRVcGxvYWRFdmVudHNSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJNChZVcGxvYWRIZWFydGJlYXRSZXF1ZXN0EjMKB3NlbnRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEiSgoXVXBsb2FkSGVhcnRiZWF0UmVzcG9uc2USLwoLcmVjZWl2ZWRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIuQBChJDb21tYW5kQXJ0aWZhY3RSZWYSHwoLYXJ0aWZhY3RfaWQYASABKAlCCrpIB3IFEAEYgAESSAoHcHVycG9zZRgCIAEoDjIrLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UHVycG9zZUIKukgHggEEEAEgABIcCghmaWxlbmFtZRgDIAEoCUIKukgHcgUQARj/ARIbCgpzaXplX2J5dGVzGAQgASgDQge6SAQiAiAAEigKBnNoYTI1NhgFIAEoCUIYukgVchMyDl5bYS1mMC05XXs2NH0kmAFAIpECChtDb21tYW5kQXJ0aWZhY3RVcGxvYWRIZWFkZXISHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJICgdwdXJwb3NlGAIgASgOMisuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RQdXJwb3NlQgq6SAeCAQQQASAAEhwKCGZpbGVuYW1lGAMgASgJQgq6SAdyBRABGP8BEhsKCnNpemVfYnl0ZXMYBCABKANCB7pIBCICIAASKAoGc2hhMjU2GAUgASgJQhi6SBVyEzIOXlthLWYwLTldezY0fSSYAUASIwoRZGV2aWNlX2lkZW50aWZpZXIYBiABKAlCCLpIBXIDGP8BIjEKFENvbW1hbmRBcnRpZmFjdENodW5rEhkKBGRhdGEYASABKAxCC7pICHoGEAEYgIBAIq0BChxVcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0EkIKBmhlYWRlchgBIAEoCzIwLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0VXBsb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiYgodVXBsb2FkQ29tbWFuZEFydGlmYWN0UmVzcG9uc2USQQoIYXJ0aWZhY3QYASABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdFJlZkIGukgDyAEBIqgBCh5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJBCghhcnRpZmFjdBgCIAEoCzInLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UmVmQga6SAPIAQESIwoRZGV2aWNlX2lkZW50aWZpZXIYAyABKAlCCLpIBXIDGP8BImIKHUNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVyEkEKCGFydGlmYWN0GAEgASgLMicuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RSZWZCBrpIA8gBASKyAQofRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZRJECgZoZWFkZXIYASABKAsyMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiiQEKHlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVxdWVzdBJHCgdkZXZpY2VzGAEgAygLMisuZmxlZXRub2RlZ2F0ZXdheS52MS5EaXNjb3ZlcmVkRGV2aWNlUmVwb3J0Qgm6SAaSAQMQgAgSHgoKY29tbWFuZF9pZBgCIAEoCUIKukgHcgUQARiAASKDAwoWRGlzY292ZXJlZERldmljZVJlcG9ydBIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIfCgppcF9hZGRyZXNzGAIgASgJQgu6SAhyBhABGC1wARKGAQoEcG9ydBgDIAEoCUJ4ukh1ugFsCgpwb3J0LnJhbmdlEilwb3J0IG11c3QgYmUgYSBkZWNpbWFsIG51bWJlciBpbiAxLi42NTUzNRozdGhpcy5tYXRjaGVzKCdeWzEtOV1bMC05XSokJykgJiYgaW50KHRoaXMpIDw9IDY1NTM1cgQQARgFEhsKCnVybF9zY2hlbWUYBCABKAlCB7pIBHICGCASHgoLZHJpdmVyX25hbWUYBSABKAlCCbpIBnIEEAEYMhIXCgVtb2RlbBgGIAEoCUIIukgFcgMY/wESHgoMbWFudWZhY3R1cmVyGAcgASgJQgi6SAVyAxj/ARIiChBmaXJtd2FyZV92ZXJzaW9uGAggASgJQgi6SAVyAxj/ASJRCh9SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIvoDChNGbGVldE5vZGVQYWlyUmVzdWx0EiUKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQgq6SAdyBRABGP8BEjEKB291dGNvbWUYAiABKA4yIC5mbGVldG5vZGVnYXRld2F5LnYxLlBhaXJPdXRjb21lEh8KDXNlcmlhbF9udW1iZXIYAyABKAlCCLpIBXIDGP8BEhwKC21hY19hZGRyZXNzGAQgASgJQge6SARyAhhAEhcKBW1vZGVsGAUgASgJQgi6SAVyAxj/ARIeCgxtYW51ZmFjdHVyZXIYBiABKAlCCLpIBXIDGP8BEiIKEGZpcm13YXJlX3ZlcnNpb24YByABKAlCCLpIBXIDGP8BEh8KDWVycm9yX21lc3NhZ2UYCCABKAlCCLpIBXIDGIAgEiQKF2RlZmF1bHRfcGFzc3dvcmRfYWN0aXZlGAwgASgISACIAQESSAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGA0gASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IaChhfZGVmYXVsdF9wYXNzd29yZF9hY3RpdmVKBAgJEApKBAgKEAtKBAgLEAxSDXVzZWRfdXNlcm5hbWVSDXVzZWRfcGFzc3dvcmRSEHVzZWRfY3JlZGVudGlhbHMiTgoURW5jcnlwdGVkQ3JlZGVudGlhbHMSGgoIdXNlcm5hbWUYASABKAxCCLpIBXoDGIAgEhoKCHBhc3N3b3JkGAIgASgMQgi6SAV6AxiAICKCAQoaUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJECgdyZXN1bHRzGAIgAygLMiguZmxlZXRub2RlZ2F0ZXdheS52MS5GbGVldE5vZGVQYWlyUmVzdWx0Qgm6SAaSAQMQgAgiTQobUmVwb3J0UGFpcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIokBChRDb250cm9sU3RyZWFtUmVxdWVzdBIyCgVoZWxsbxgBIAEoCzIhLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEhlbGxvSAASLgoDYWNrGAIgASgLMh8uZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sQWNrSABCDQoEa2luZBIFukgCCAEimAEKFUNvbnRyb2xTdHJlYW1SZXNwb25zZRI4CghhY2NlcHRlZBgBIAEoCzIkLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEFjY2VwdGVkSAASNgoHY29tbWFuZBgCIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbENvbW1hbmRIAEINCgRraW5kEgW6SAIIASIOCgxDb250cm9sSGVsbG8iQgoPQ29udHJvbEFjY2VwdGVkEi8KC3NlcnZlcl90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJMCg5Db250cm9sQ29tbWFuZBIeCgpjb21tYW5kX2lkGAEgASgJQgq6SAdyBRABGIABEhoKB3BheWxvYWQYAiABKAxCCbpIBnoEGICAQCK3AwoZTWluZXJDb25uZWN0aW9uRGVzY3JpcHRvchIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIeCgtkcml2ZXJfbmFtZRgCIAEoCUIJukgGcgQQARgyEh8KCmlwX2FkZHJlc3MYAyABKAlCC7pICHIGEAEYLXABEoYBCgRwb3J0GAQgASgJQni6SHW6AWwKCnBvcnQucmFuZ2USKXBvcnQgbXVzdCBiZSBhIGRlY2ltYWwgbnVtYmVyIGluIDEuLjY1NTM1GjN0aGlzLm1hdGNoZXMoJ15bMS05XVswLTldKiQnKSAmJiBpbnQodGhpcykgPD0gNjU1MzVyBBABGAUSGwoKdXJsX3NjaGVtZRgFIAEoCUIHukgEcgIYIBIfCg1zZXJpYWxfbnVtYmVyGAYgASgJQgi6SAVyAxj/ARIdCgttYWNfYWRkcmVzcxgHIAEoCUIIukgFcgMY/wESJQoTY3JlZGVudGlhbF91c2VybmFtZRgIIAEoDEIIukgFegMYgCASJQoTY3JlZGVudGlhbF9wYXNzd29yZBgJIAEoDEIIukgFegMYgCAi9QYKDE1pbmVyQ29tbWFuZBJGCgZ0YXJnZXQYASABKAsyLi5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29ubmVjdGlvbkRlc2NyaXB0b3JCBrpIA8gBARIzCgZyZWJvb3QYAiABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLlJlYm9vdEFjdGlvbkgAEj4KDHN0YXJ0X21pbmluZxgDIAEoCzImLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RhcnRNaW5pbmdBY3Rpb25IABI8CgtzdG9wX21pbmluZxgEIAEoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RvcE1pbmluZ0FjdGlvbkgAEjgKCWJsaW5rX2xlZBgFIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQmxpbmtMZWRBY3Rpb25IABI1CgdjdXJ0YWlsGAYgASgLMiIuZmxlZXRub2RlZ2F0ZXdheS52MS5DdXJ0YWlsQWN0aW9uSAASOQoJdW5jdXJ0YWlsGAcgASgLMiQuZmxlZXRub2RlZ2F0ZXdheS52MS5VbmN1cnRhaWxBY3Rpb25IABJFChBzZXRfY29vbGluZ19tb2RlGAggASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5TZXRDb29saW5nTW9kZUFjdGlvbkgAEkUKEHNldF9wb3dlcl90YXJnZXQYCSABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLlNldFBvd2VyVGFyZ2V0QWN0aW9uSAASSwoTdXBkYXRlX21pbmluZ19wb29scxgKIAEoCzIsLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluaW5nUG9vbHNBY3Rpb25IABJFChBnZXRfbWluaW5nX3Bvb2xzGAsgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5HZXRNaW5pbmdQb29sc0FjdGlvbkgAEjoKCmdldF9lcnJvcnMYDCABKAsyJC5mbGVldG5vZGVnYXRld2F5LnYxLkdldEVycm9yc0FjdGlvbkgAEk8KFXVwZGF0ZV9taW5lcl9wYXNzd29yZBgNIAEoCzIuLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbkgAQg8KBmFjdGlvbhIFukgCCAEiDgoMUmVib290QWN0aW9uIhMKEVN0YXJ0TWluaW5nQWN0aW9uIhIKEFN0b3BNaW5pbmdBY3Rpb24iEAoOQmxpbmtMZWRBY3Rpb24iEQoPVW5jdXJ0YWlsQWN0aW9uIhYKFEdldE1pbmluZ1Bvb2xzQWN0aW9uIhEKD0dldEVycm9yc0FjdGlvbiKaAgoUTm9kZUVuY3J5cHRlZFBheWxvYWQSpgEKCWFsZ29yaXRobRgBIAEoCUKSAbpIjgG6AYQBCiBub2RlX2VuY3J5cHRlZF9wYXlsb2FkLmFsZ29yaXRobRIzYWxnb3JpdGhtIG11c3QgYmUgeDI1NTE5LWhrZGYtc2hhMjU2LWFlcy0yNTYtZ2NtLXYxGit0aGlzID09ICd4MjU1MTktaGtkZi1zaGEyNTYtYWVzLTI1Ni1nY20tdjEncgQQARhAEiEKEGVwaGVtZXJhbF9wdWJrZXkYAiABKAxCB7pIBHoCaCASFgoFbm9uY2UYAyABKAxCB7pIBHoCaAwSHgoKY2lwaGVydGV4dBgEIAEoDEIKukgHegUQERiAQCJxChlVcGRhdGVNaW5lclBhc3N3b3JkQWN0aW9uElQKGWVuY3J5cHRlZF9wYXNzd29yZF91cGRhdGUYASABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLk5vZGVFbmNyeXB0ZWRQYXlsb2FkQga6SAPIAQEibQoZVXBkYXRlTWluZXJQYXNzd29yZFJlc3VsdBJQChVlbmNyeXB0ZWRfY3JlZGVudGlhbHMYASABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkVuY3J5cHRlZENyZWRlbnRpYWxzQga6SAPIAQEiQAoNQ3VydGFpbEFjdGlvbhIvCgVsZXZlbBgBIAEoDjIgLmN1cnRhaWxtZW50LnYxLkN1cnRhaWxtZW50TGV2ZWwiPAoUU2V0Q29vbGluZ01vZGVBY3Rpb24SJAoEbW9kZRgBIAEoDjIWLmNvbW1vbi52MS5Db29saW5nTW9kZSJSChRTZXRQb3dlclRhcmdldEFjdGlvbhI6ChBwZXJmb3JtYW5jZV9tb2RlGAEgASgOMiAubWluZXJjb21tYW5kLnYxLlBlcmZvcm1hbmNlTW9kZSKFAwoQTWluaW5nUG9vbENvbmZpZxIbCghwcmlvcml0eRgBIAEoBUIJukgGGgQYAigAErcCCgN1cmwYAiABKAlCqQK6SKUCcqICEAwYgAIymgJeKHN0cmF0dW1cKyh0Y3B8c3NsfHdzKTpcL1wvKChbYS16QS1aMC05XVthLXpBLVowLTkuLV0qW2EtekEtWjAtOV1cLlthLXpBLVpdezIsfSl8KFxkezEsM31cLil7M31cZHsxLDN9fFxbKFswLTlhLWZBLUY6XSspXF0pKDpcZHsxLDV9KT98c3RyYXR1bTJcK3RjcDpcL1wvKChbYS16QS1aMC05XVthLXpBLVowLTkuLV0qW2EtekEtWjAtOV1cLlthLXpBLVpdezIsfSl8KFxkezEsM31cLil7M31cZHsxLDN9fFxbKFswLTlhLWZBLUY6XSspXF0pOlxkezEsNX1cL1tBLVphLXowLTlfK1wvPTouLV0rKSQSGgoIdXNlcm5hbWUYAyABKAlCCLpIBXIDGIAEIlsKF1VwZGF0ZU1pbmluZ1Bvb2xzQWN0aW9uEkAKBXBvb2xzGAEgAygLMiUuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5pbmdQb29sQ29uZmlnQgq6SAeSAQQIARADIlYKFEdldE1pbmluZ1Bvb2xzUmVzdWx0Ej4KBXBvb2xzGAEgAygLMiUuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5pbmdQb29sQ29uZmlnQgi6SAWSAQIQAyLFBQoQTWluZXJFcnJvclJlcG9ydBI0CgttaW5lcl9lcnJvchgBIAEoDjIVLmVycm9ycy52MS5NaW5lckVycm9yQgi6SAWCAQIQARIfCg1jYXVzZV9zdW1tYXJ5GAIgASgJQgi6SAVyAxiAIBIkChJyZWNvbW1lbmRlZF9hY3Rpb24YAyABKAlCCLpIBXIDGIAgEi8KCHNldmVyaXR5GAQgASgOMhMuZXJyb3JzLnYxLlNldmVyaXR5Qgi6SAWCAQIQARIxCg1maXJzdF9zZWVuX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgxsYXN0X3NlZW5fYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi0KCWNsb3NlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAScAoRdmVuZG9yX2F0dHJpYnV0ZXMYCCADKAsyOy5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyRXJyb3JSZXBvcnQuVmVuZG9yQXR0cmlidXRlc0VudHJ5Qhi6SBWaARIQICIHcgUQARiAASoFcgMYgAgSHQoJZGV2aWNlX2lkGAkgASgJQgq6SAdyBRABGP8BEiMKDGNvbXBvbmVudF9pZBgKIAEoCUIIukgFcgMY/wFIAIgBARIYCgZpbXBhY3QYCyABKAlCCLpIBXIDGIAgEhkKB3N1bW1hcnkYDCABKAlCCLpIBXIDGIAgEjoKDmNvbXBvbmVudF90eXBlGA0gASgOMhguZXJyb3JzLnYxLkNvbXBvbmVudFR5cGVCCLpIBYIBAhABGjcKFVZlbmRvckF0dHJpYnV0ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQg8KDV9jb21wb25lbnRfaWQiowEKD0dldEVycm9yc1Jlc3VsdBIdCglkZXZpY2VfaWQYASABKAlCCrpIB3IFEAEY/wESQAoGZXJyb3JzGAIgAygLMiUuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5lckVycm9yUmVwb3J0Qgm6SAaSAQMQgAQSEQoJdHJ1bmNhdGVkGAMgASgIEhwKFG9taXR0ZWRfcmVwb3J0X2NvdW50GAQgASgNIv0BCgxBZ2VudENvbW1hbmQSLwoIZGlzY292ZXIYASABKAsyGy5wYWlyaW5nLnYxLkRpc2NvdmVyUmVxdWVzdEgAEjAKBHBhaXIYAiABKAsyIC5wYWlyaW5nLnYxLkZsZWV0Tm9kZVBhaXJSZXF1ZXN0SAASOgoNbWluZXJfY29tbWFuZBgDIAEoCzIhLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluZXJDb21tYW5kSAASPAoJdGVsZW1ldHJ5GAQgASgLMicudGVsZW1ldHJ5LnYxLkZsZWV0Tm9kZVRlbGVtZXRyeVJlcXVlc3RIAEIQCgdjb21tYW5kEgW6SAIIASKoAQoKQ29udHJvbEFjaxIeCgpjb21tYW5kX2lkGAEgASgJQgq6SAdyBRABGIABEhEKCXN1Y2NlZWRlZBgCIAEoCBIfCg1lcnJvcl9tZXNzYWdlGAMgASgJQgi6SAVyAxiAIBIqCgRjb2RlGAQgASgOMhwuZmxlZXRub2RlZ2F0ZXdheS52MS5BY2tDb2RlEhoKB3BheWxvYWQYBSABKAxCCbpIBnoEGICAQCqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIfChtFTlJPTExNRU5UX1NUQVRVU19DT05GSVJNRUQQAhIdChlFTlJPTExNRU5UX1NUQVRVU19SRVZPS0VEEAMqmgEKFkNvbW1hbmRBcnRpZmFjdFB1cnBvc2USKAokQ09NTUFORF9BUlRJRkFDVF9QVVJQT1NFX1VOU1BFQ0lGSUVEEAASJwojQ09NTUFORF9BUlRJRkFDVF9QVVJQT1NFX01JTkVSX0xPR1MQARItCilDT01NQU5EX0FSVElGQUNUX1BVUlBPU0VfRklSTVdBUkVfUEFZTE9BRBACKpgBCgtQYWlyT3V0Y29tZRIcChhQQUlSX09VVENPTUVfVU5TUEVDSUZJRUQQABIXChNQQUlSX09VVENPTUVfUEFJUkVEEAESHAoYUEFJUl9PVVRDT01FX0FVVEhfTkVFREVEEAISHAoYUEFJUl9PVVRDT01FX0FVVEhfRkFJTEVEEAMSFgoSUEFJUl9PVVRDT01FX0VSUk9SEAQqtAIKB0Fja0NvZGUSGAoUQUNLX0NPREVfVU5TUEVDSUZJRUQQABIPCgtBQ0tfQ09ERV9PSxABEhQKEEFDS19DT0RFX1BBUlRJQUwQAhIYChRBQ0tfQ09ERV9CQURfUkVRVUVTVBADEhwKGEFDS19DT0RFX0FHRU5UX0lOQ0FQQUJMRRAEEhgKFEFDS19DT0RFX1NDQU5fRkFJTEVEEAUSGgoWQUNLX0NPREVfUkVQT1JUX0ZBSUxFRBAGEhUKEUFDS19DT0RFX0lOVEVSTkFMEAcSEQoNQUNLX0NPREVfQlVTWRAIEhwKGEFDS19DT0RFX1VOQVVUSEVOVElDQVRFRBAJEhoKFkFDS19DT0RFX1VOSU1QTEVNRU5URUQQChIWChJBQ0tfQ09ERV9GT1JCSURERU4QCzKnCgoXRmxlZXROb2RlR2F0ZXdheVNlcnZpY2USVwoIUmVnaXN0ZXISJC5mbGVldG5vZGVnYXRld2F5LnYxLlJlZ2lzdGVyUmVxdWVzdBolLmZsZWV0bm9kZWdhdGV3YXkudjEuUmVnaXN0ZXJSZXNwb25zZRJ1ChJCZWdpbkF1dGhIYW5kc2hha2USLi5mbGVldG5vZGVnYXRld2F5LnYxLkJlZ2luQXV0aEhhbmRzaGFrZVJlcXVlc3QaLy5mbGVldG5vZGVnYXRld2F5LnYxLkJlZ2luQXV0aEhhbmRzaGFrZVJlc3BvbnNlEn4KFUNvbXBsZXRlQXV0aEhhbmRzaGFrZRIxLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVxdWVzdBoyLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVzcG9uc2USbgoPVXBsb2FkVGVsZW1ldHJ5EisuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRUZWxlbWV0cnlSZXF1ZXN0GiwuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRUZWxlbWV0cnlSZXNwb25zZSgBEmUKDFVwbG9hZEV2ZW50cxIoLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkRXZlbnRzUmVxdWVzdBopLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkRXZlbnRzUmVzcG9uc2UoARJsCg9VcGxvYWRIZWFydGJlYXQSKy5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEhlYXJ0YmVhdFJlcXVlc3QaLC5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEhlYXJ0YmVhdFJlc3BvbnNlEoABChVVcGxvYWRDb21tYW5kQXJ0aWZhY3QSMS5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QaMi5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZENvbW1hbmRBcnRpZmFjdFJlc3BvbnNlKAEShgEKF0Rvd25sb2FkQ29tbWFuZEFydGlmYWN0EjMuZmxlZXRub2RlZ2F0ZXdheS52MS5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QaNC5mbGVldG5vZGVnYXRld2F5LnYxLkRvd25sb2FkQ29tbWFuZEFydGlmYWN0UmVzcG9uc2UwARKEAQoXUmVwb3J0RGlzY292ZXJlZERldmljZXMSMy5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVxdWVzdBo0LmZsZWV0bm9kZWdhdGV3YXkudjEuUmVwb3J0RGlzY292ZXJlZERldmljZXNSZXNwb25zZRJ4ChNSZXBvcnRQYWlyZWREZXZpY2VzEi8uZmxlZXRub2RlZ2F0ZXdheS52MS5SZXBvcnRQYWlyZWREZXZpY2VzUmVxdWVzdBowLmZsZWV0bm9kZWdhdGV3YXkudjEuUmVwb3J0UGFpcmVkRGV2aWNlc1Jlc3BvbnNlEmoKDUNvbnRyb2xTdHJlYW0SKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbnRyb2xTdHJlYW1SZXF1ZXN0GiouZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sU3RyZWFtUmVzcG9uc2UoATABQvgBChdjb20uZmxlZXRub2RlZ2F0ZXdheS52MUIVRmxlZXRub2RlZ2F0ZXdheVByb3RvUAFaWWdpdGh1Yi5jb20vYmxvY2svcHJvdG8tZmxlZXQvc2VydmVyL2dlbmVyYXRlZC9ncnBjL2ZsZWV0bm9kZWdhdGV3YXkvdjE7ZmxlZXRub2RlZ2F0ZXdheXYxogIDRlhYqgITRmxlZXRub2RlZ2F0ZXdheS5WMcoCE0ZsZWV0bm9kZWdhdGV3YXlcVjHiAh9GbGVldG5vZGVnYXRld2F5XFYxXEdQQk1ldGFkYXRh6gIURmxlZXRub2RlZ2F0ZXdheTo6VjFiBnByb3RvMw", - [ - file_buf_validate_validate, - file_common_v1_cooling, - file_curtailment_v1_curtailment, - file_errors_v1_errors, - file_google_protobuf_timestamp, - file_minercommand_v1_command, - file_pairing_v1_pairing, - file_telemetry_v1_telemetry, - ], - ); +export const file_fleetnodegateway_v1_fleetnodegateway: GenFile = /*@__PURE__*/ + fileDesc("CipmbGVldG5vZGVnYXRld2F5L3YxL2ZsZWV0bm9kZWdhdGV3YXkucHJvdG8SE2ZsZWV0bm9kZWdhdGV3YXkudjEilwEKD1JlZ2lzdGVyUmVxdWVzdBIkChBlbnJvbGxtZW50X3Rva2VuGAEgASgJQgq6SAdyBRAUGIAEEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESIAoPaWRlbnRpdHlfcHVia2V5GAMgASgMQge6SAR6AmggEiIKEWVuY3J5cHRpb25fcHVia2V5GAQgASgMQge6SAR6AmggIokBChBSZWdpc3RlclJlc3BvbnNlEhUKDWZsZWV0X25vZGVfaWQYASABKAMSQAoRZW5yb2xsbWVudF9zdGF0dXMYAiABKA4yJS5mbGVldG5vZGVnYXRld2F5LnYxLkVucm9sbG1lbnRTdGF0dXMSHAoUaWRlbnRpdHlfZmluZ2VycHJpbnQYAyABKAkiWgoZQmVnaW5BdXRoSGFuZHNoYWtlUmVxdWVzdBIbCgdhcGlfa2V5GAEgASgJQgq6SAdyBRAUGIAEEiAKD2lkZW50aXR5X3B1YmtleRgCIAEoDEIHukgEegJoICJfChpCZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRIRCgljaGFsbGVuZ2UYASABKAwSLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWAocQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVxdWVzdBIcCgljaGFsbGVuZ2UYASABKAxCCbpIBnoEEBAYQBIaCglzaWduYXR1cmUYAiABKAxCB7pIBHoCaEAiZgodQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVzcG9uc2USFQoNc2Vzc2lvbl90b2tlbhgBIAEoCRIuCgpleHBpcmVzX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJtChZVcGxvYWRUZWxlbWV0cnlSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIxChdVcGxvYWRUZWxlbWV0cnlSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJqChNVcGxvYWRFdmVudHNSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIuChRVcGxvYWRFdmVudHNSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJNChZVcGxvYWRIZWFydGJlYXRSZXF1ZXN0EjMKB3NlbnRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEiSgoXVXBsb2FkSGVhcnRiZWF0UmVzcG9uc2USLwoLcmVjZWl2ZWRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIuQBChJDb21tYW5kQXJ0aWZhY3RSZWYSHwoLYXJ0aWZhY3RfaWQYASABKAlCCrpIB3IFEAEYgAESSAoHcHVycG9zZRgCIAEoDjIrLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UHVycG9zZUIKukgHggEEEAEgABIcCghmaWxlbmFtZRgDIAEoCUIKukgHcgUQARj/ARIbCgpzaXplX2J5dGVzGAQgASgDQge6SAQiAiAAEigKBnNoYTI1NhgFIAEoCUIYukgVchMyDl5bYS1mMC05XXs2NH0kmAFAIpECChtDb21tYW5kQXJ0aWZhY3RVcGxvYWRIZWFkZXISHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJICgdwdXJwb3NlGAIgASgOMisuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RQdXJwb3NlQgq6SAeCAQQQASAAEhwKCGZpbGVuYW1lGAMgASgJQgq6SAdyBRABGP8BEhsKCnNpemVfYnl0ZXMYBCABKANCB7pIBCICIAASKAoGc2hhMjU2GAUgASgJQhi6SBVyEzIOXlthLWYwLTldezY0fSSYAUASIwoRZGV2aWNlX2lkZW50aWZpZXIYBiABKAlCCLpIBXIDGP8BIjEKFENvbW1hbmRBcnRpZmFjdENodW5rEhkKBGRhdGEYASABKAxCC7pICHoGEAEYgIBAIq0BChxVcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0EkIKBmhlYWRlchgBIAEoCzIwLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0VXBsb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiYgodVXBsb2FkQ29tbWFuZEFydGlmYWN0UmVzcG9uc2USQQoIYXJ0aWZhY3QYASABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdFJlZkIGukgDyAEBIqgBCh5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJBCghhcnRpZmFjdBgCIAEoCzInLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UmVmQga6SAPIAQESIwoRZGV2aWNlX2lkZW50aWZpZXIYAyABKAlCCLpIBXIDGP8BImIKHUNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVyEkEKCGFydGlmYWN0GAEgASgLMicuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RSZWZCBrpIA8gBASKyAQofRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZRJECgZoZWFkZXIYASABKAsyMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiiQEKHlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVxdWVzdBJHCgdkZXZpY2VzGAEgAygLMisuZmxlZXRub2RlZ2F0ZXdheS52MS5EaXNjb3ZlcmVkRGV2aWNlUmVwb3J0Qgm6SAaSAQMQgAgSHgoKY29tbWFuZF9pZBgCIAEoCUIKukgHcgUQARiAASKDAwoWRGlzY292ZXJlZERldmljZVJlcG9ydBIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIfCgppcF9hZGRyZXNzGAIgASgJQgu6SAhyBhABGC1wARKGAQoEcG9ydBgDIAEoCUJ4ukh1ugFsCgpwb3J0LnJhbmdlEilwb3J0IG11c3QgYmUgYSBkZWNpbWFsIG51bWJlciBpbiAxLi42NTUzNRozdGhpcy5tYXRjaGVzKCdeWzEtOV1bMC05XSokJykgJiYgaW50KHRoaXMpIDw9IDY1NTM1cgQQARgFEhsKCnVybF9zY2hlbWUYBCABKAlCB7pIBHICGCASHgoLZHJpdmVyX25hbWUYBSABKAlCCbpIBnIEEAEYMhIXCgVtb2RlbBgGIAEoCUIIukgFcgMY/wESHgoMbWFudWZhY3R1cmVyGAcgASgJQgi6SAVyAxj/ARIiChBmaXJtd2FyZV92ZXJzaW9uGAggASgJQgi6SAVyAxj/ASJRCh9SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIvoDChNGbGVldE5vZGVQYWlyUmVzdWx0EiUKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQgq6SAdyBRABGP8BEjEKB291dGNvbWUYAiABKA4yIC5mbGVldG5vZGVnYXRld2F5LnYxLlBhaXJPdXRjb21lEh8KDXNlcmlhbF9udW1iZXIYAyABKAlCCLpIBXIDGP8BEhwKC21hY19hZGRyZXNzGAQgASgJQge6SARyAhhAEhcKBW1vZGVsGAUgASgJQgi6SAVyAxj/ARIeCgxtYW51ZmFjdHVyZXIYBiABKAlCCLpIBXIDGP8BEiIKEGZpcm13YXJlX3ZlcnNpb24YByABKAlCCLpIBXIDGP8BEh8KDWVycm9yX21lc3NhZ2UYCCABKAlCCLpIBXIDGIAgEiQKF2RlZmF1bHRfcGFzc3dvcmRfYWN0aXZlGAwgASgISACIAQESSAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGA0gASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IaChhfZGVmYXVsdF9wYXNzd29yZF9hY3RpdmVKBAgJEApKBAgKEAtKBAgLEAxSDXVzZWRfdXNlcm5hbWVSDXVzZWRfcGFzc3dvcmRSEHVzZWRfY3JlZGVudGlhbHMiTgoURW5jcnlwdGVkQ3JlZGVudGlhbHMSGgoIdXNlcm5hbWUYASABKAxCCLpIBXoDGIAgEhoKCHBhc3N3b3JkGAIgASgMQgi6SAV6AxiAICKCAQoaUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJECgdyZXN1bHRzGAIgAygLMiguZmxlZXRub2RlZ2F0ZXdheS52MS5GbGVldE5vZGVQYWlyUmVzdWx0Qgm6SAaSAQMQgAgiTQobUmVwb3J0UGFpcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIokBChRDb250cm9sU3RyZWFtUmVxdWVzdBIyCgVoZWxsbxgBIAEoCzIhLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEhlbGxvSAASLgoDYWNrGAIgASgLMh8uZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sQWNrSABCDQoEa2luZBIFukgCCAEimAEKFUNvbnRyb2xTdHJlYW1SZXNwb25zZRI4CghhY2NlcHRlZBgBIAEoCzIkLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEFjY2VwdGVkSAASNgoHY29tbWFuZBgCIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbENvbW1hbmRIAEINCgRraW5kEgW6SAIIASIOCgxDb250cm9sSGVsbG8iQgoPQ29udHJvbEFjY2VwdGVkEi8KC3NlcnZlcl90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJMCg5Db250cm9sQ29tbWFuZBIeCgpjb21tYW5kX2lkGAEgASgJQgq6SAdyBRABGIABEhoKB3BheWxvYWQYAiABKAxCCbpIBnoEGICAQCK3AwoZTWluZXJDb25uZWN0aW9uRGVzY3JpcHRvchIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIeCgtkcml2ZXJfbmFtZRgCIAEoCUIJukgGcgQQARgyEh8KCmlwX2FkZHJlc3MYAyABKAlCC7pICHIGEAEYLXABEoYBCgRwb3J0GAQgASgJQni6SHW6AWwKCnBvcnQucmFuZ2USKXBvcnQgbXVzdCBiZSBhIGRlY2ltYWwgbnVtYmVyIGluIDEuLjY1NTM1GjN0aGlzLm1hdGNoZXMoJ15bMS05XVswLTldKiQnKSAmJiBpbnQodGhpcykgPD0gNjU1MzVyBBABGAUSGwoKdXJsX3NjaGVtZRgFIAEoCUIHukgEcgIYIBIfCg1zZXJpYWxfbnVtYmVyGAYgASgJQgi6SAVyAxj/ARIdCgttYWNfYWRkcmVzcxgHIAEoCUIIukgFcgMY/wESJQoTY3JlZGVudGlhbF91c2VybmFtZRgIIAEoDEIIukgFegMYgCASJQoTY3JlZGVudGlhbF9wYXNzd29yZBgJIAEoDEIIukgFegMYgCAitwcKDE1pbmVyQ29tbWFuZBJGCgZ0YXJnZXQYASABKAsyLi5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29ubmVjdGlvbkRlc2NyaXB0b3JCBrpIA8gBARIzCgZyZWJvb3QYAiABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLlJlYm9vdEFjdGlvbkgAEj4KDHN0YXJ0X21pbmluZxgDIAEoCzImLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RhcnRNaW5pbmdBY3Rpb25IABI8CgtzdG9wX21pbmluZxgEIAEoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RvcE1pbmluZ0FjdGlvbkgAEjgKCWJsaW5rX2xlZBgFIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQmxpbmtMZWRBY3Rpb25IABI1CgdjdXJ0YWlsGAYgASgLMiIuZmxlZXRub2RlZ2F0ZXdheS52MS5DdXJ0YWlsQWN0aW9uSAASOQoJdW5jdXJ0YWlsGAcgASgLMiQuZmxlZXRub2RlZ2F0ZXdheS52MS5VbmN1cnRhaWxBY3Rpb25IABJFChBzZXRfY29vbGluZ19tb2RlGAggASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5TZXRDb29saW5nTW9kZUFjdGlvbkgAEkUKEHNldF9wb3dlcl90YXJnZXQYCSABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLlNldFBvd2VyVGFyZ2V0QWN0aW9uSAASSwoTdXBkYXRlX21pbmluZ19wb29scxgKIAEoCzIsLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluaW5nUG9vbHNBY3Rpb25IABJFChBnZXRfbWluaW5nX3Bvb2xzGAsgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5HZXRNaW5pbmdQb29sc0FjdGlvbkgAEjoKCmdldF9lcnJvcnMYDCABKAsyJC5mbGVldG5vZGVnYXRld2F5LnYxLkdldEVycm9yc0FjdGlvbkgAEk8KFXVwZGF0ZV9taW5lcl9wYXNzd29yZBgNIAEoCzIuLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbkgAEkAKDWRvd25sb2FkX2xvZ3MYDiABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkRvd25sb2FkTG9nc0FjdGlvbkgAQg8KBmFjdGlvbhIFukgCCAEiDgoMUmVib290QWN0aW9uIhMKEVN0YXJ0TWluaW5nQWN0aW9uIhIKEFN0b3BNaW5pbmdBY3Rpb24iEAoOQmxpbmtMZWRBY3Rpb24iEQoPVW5jdXJ0YWlsQWN0aW9uIhYKFEdldE1pbmluZ1Bvb2xzQWN0aW9uIhEKD0dldEVycm9yc0FjdGlvbiI4ChJEb3dubG9hZExvZ3NBY3Rpb24SIgoOYmF0Y2hfbG9nX3V1aWQYASABKAlCCrpIB3IFEAEYgAEimgIKFE5vZGVFbmNyeXB0ZWRQYXlsb2FkEqYBCglhbGdvcml0aG0YASABKAlCkgG6SI4BugGEAQogbm9kZV9lbmNyeXB0ZWRfcGF5bG9hZC5hbGdvcml0aG0SM2FsZ29yaXRobSBtdXN0IGJlIHgyNTUxOS1oa2RmLXNoYTI1Ni1hZXMtMjU2LWdjbS12MRordGhpcyA9PSAneDI1NTE5LWhrZGYtc2hhMjU2LWFlcy0yNTYtZ2NtLXYxJ3IEEAEYQBIhChBlcGhlbWVyYWxfcHVia2V5GAIgASgMQge6SAR6AmggEhYKBW5vbmNlGAMgASgMQge6SAR6AmgMEh4KCmNpcGhlcnRleHQYBCABKAxCCrpIB3oFEBEYgEAicQoZVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbhJUChllbmNyeXB0ZWRfcGFzc3dvcmRfdXBkYXRlGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5Ob2RlRW5jcnlwdGVkUGF5bG9hZEIGukgDyAEBIm0KGVVwZGF0ZU1pbmVyUGFzc3dvcmRSZXN1bHQSUAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IGukgDyAEBIkAKDUN1cnRhaWxBY3Rpb24SLwoFbGV2ZWwYASABKA4yIC5jdXJ0YWlsbWVudC52MS5DdXJ0YWlsbWVudExldmVsIjwKFFNldENvb2xpbmdNb2RlQWN0aW9uEiQKBG1vZGUYASABKA4yFi5jb21tb24udjEuQ29vbGluZ01vZGUiUgoUU2V0UG93ZXJUYXJnZXRBY3Rpb24SOgoQcGVyZm9ybWFuY2VfbW9kZRgBIAEoDjIgLm1pbmVyY29tbWFuZC52MS5QZXJmb3JtYW5jZU1vZGUihQMKEE1pbmluZ1Bvb2xDb25maWcSGwoIcHJpb3JpdHkYASABKAVCCbpIBhoEGAIoABK3AgoDdXJsGAIgASgJQqkCukilAnKiAhAMGIACMpoCXihzdHJhdHVtXCsodGNwfHNzbHx3cyk6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKSg6XGR7MSw1fSk/fHN0cmF0dW0yXCt0Y3A6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKTpcZHsxLDV9XC9bQS1aYS16MC05XytcLz06Li1dKykkEhoKCHVzZXJuYW1lGAMgASgJQgi6SAVyAxiABCJbChdVcGRhdGVNaW5pbmdQb29sc0FjdGlvbhJACgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IKukgHkgEECAEQAyJWChRHZXRNaW5pbmdQb29sc1Jlc3VsdBI+CgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IIukgFkgECEAMixQUKEE1pbmVyRXJyb3JSZXBvcnQSNAoLbWluZXJfZXJyb3IYASABKA4yFS5lcnJvcnMudjEuTWluZXJFcnJvckIIukgFggECEAESHwoNY2F1c2Vfc3VtbWFyeRgCIAEoCUIIukgFcgMYgCASJAoScmVjb21tZW5kZWRfYWN0aW9uGAMgASgJQgi6SAVyAxiAIBIvCghzZXZlcml0eRgEIAEoDjITLmVycm9ycy52MS5TZXZlcml0eUIIukgFggECEAESMQoNZmlyc3Rfc2Vlbl9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMAoMbGFzdF9zZWVuX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBItCgljbG9zZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEnAKEXZlbmRvcl9hdHRyaWJ1dGVzGAggAygLMjsuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5lckVycm9yUmVwb3J0LlZlbmRvckF0dHJpYnV0ZXNFbnRyeUIYukgVmgESECAiB3IFEAEYgAEqBXIDGIAIEh0KCWRldmljZV9pZBgJIAEoCUIKukgHcgUQARj/ARIjCgxjb21wb25lbnRfaWQYCiABKAlCCLpIBXIDGP8BSACIAQESGAoGaW1wYWN0GAsgASgJQgi6SAVyAxiAIBIZCgdzdW1tYXJ5GAwgASgJQgi6SAVyAxiAIBI6Cg5jb21wb25lbnRfdHlwZRgNIAEoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlQgi6SAWCAQIQARo3ChVWZW5kb3JBdHRyaWJ1dGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIPCg1fY29tcG9uZW50X2lkIqMBCg9HZXRFcnJvcnNSZXN1bHQSHQoJZGV2aWNlX2lkGAEgASgJQgq6SAdyBRABGP8BEkAKBmVycm9ycxgCIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluZXJFcnJvclJlcG9ydEIJukgGkgEDEIAEEhEKCXRydW5jYXRlZBgDIAEoCBIcChRvbWl0dGVkX3JlcG9ydF9jb3VudBgEIAEoDSL9AQoMQWdlbnRDb21tYW5kEi8KCGRpc2NvdmVyGAEgASgLMhsucGFpcmluZy52MS5EaXNjb3ZlclJlcXVlc3RIABIwCgRwYWlyGAIgASgLMiAucGFpcmluZy52MS5GbGVldE5vZGVQYWlyUmVxdWVzdEgAEjoKDW1pbmVyX2NvbW1hbmQYAyABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29tbWFuZEgAEjwKCXRlbGVtZXRyeRgEIAEoCzInLnRlbGVtZXRyeS52MS5GbGVldE5vZGVUZWxlbWV0cnlSZXF1ZXN0SABCEAoHY29tbWFuZBIFukgCCAEiqAEKCkNvbnRyb2xBY2sSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARIRCglzdWNjZWVkZWQYAiABKAgSHwoNZXJyb3JfbWVzc2FnZRgDIAEoCUIIukgFcgMYgCASKgoEY29kZRgEIAEoDjIcLmZsZWV0bm9kZWdhdGV3YXkudjEuQWNrQ29kZRIaCgdwYXlsb2FkGAUgASgMQgm6SAZ6BBiAgEAqlAEKEEVucm9sbG1lbnRTdGF0dXMSIQodRU5ST0xMTUVOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIdChlFTlJPTExNRU5UX1NUQVRVU19QRU5ESU5HEAESHwobRU5ST0xMTUVOVF9TVEFUVVNfQ09ORklSTUVEEAISHQoZRU5ST0xMTUVOVF9TVEFUVVNfUkVWT0tFRBADKpoBChZDb21tYW5kQXJ0aWZhY3RQdXJwb3NlEigKJENPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9VTlNQRUNJRklFRBAAEicKI0NPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9NSU5FUl9MT0dTEAESLQopQ09NTUFORF9BUlRJRkFDVF9QVVJQT1NFX0ZJUk1XQVJFX1BBWUxPQUQQAiqYAQoLUGFpck91dGNvbWUSHAoYUEFJUl9PVVRDT01FX1VOU1BFQ0lGSUVEEAASFwoTUEFJUl9PVVRDT01FX1BBSVJFRBABEhwKGFBBSVJfT1VUQ09NRV9BVVRIX05FRURFRBACEhwKGFBBSVJfT1VUQ09NRV9BVVRIX0ZBSUxFRBADEhYKElBBSVJfT1VUQ09NRV9FUlJPUhAEKrQCCgdBY2tDb2RlEhgKFEFDS19DT0RFX1VOU1BFQ0lGSUVEEAASDwoLQUNLX0NPREVfT0sQARIUChBBQ0tfQ09ERV9QQVJUSUFMEAISGAoUQUNLX0NPREVfQkFEX1JFUVVFU1QQAxIcChhBQ0tfQ09ERV9BR0VOVF9JTkNBUEFCTEUQBBIYChRBQ0tfQ09ERV9TQ0FOX0ZBSUxFRBAFEhoKFkFDS19DT0RFX1JFUE9SVF9GQUlMRUQQBhIVChFBQ0tfQ09ERV9JTlRFUk5BTBAHEhEKDUFDS19DT0RFX0JVU1kQCBIcChhBQ0tfQ09ERV9VTkFVVEhFTlRJQ0FURUQQCRIaChZBQ0tfQ09ERV9VTklNUExFTUVOVEVEEAoSFgoSQUNLX0NPREVfRk9SQklEREVOEAsypwoKF0ZsZWV0Tm9kZUdhdGV3YXlTZXJ2aWNlElcKCFJlZ2lzdGVyEiQuZmxlZXRub2RlZ2F0ZXdheS52MS5SZWdpc3RlclJlcXVlc3QaJS5mbGVldG5vZGVnYXRld2F5LnYxLlJlZ2lzdGVyUmVzcG9uc2USdQoSQmVnaW5BdXRoSGFuZHNoYWtlEi4uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXF1ZXN0Gi8uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRJ+ChVDb21wbGV0ZUF1dGhIYW5kc2hha2USMS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlcXVlc3QaMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlc3BvbnNlEm4KD1VwbG9hZFRlbGVtZXRyeRIrLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVxdWVzdBosLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVzcG9uc2UoARJlCgxVcGxvYWRFdmVudHMSKC5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1JlcXVlc3QaKS5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1Jlc3BvbnNlKAESbAoPVXBsb2FkSGVhcnRiZWF0EisuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXF1ZXN0GiwuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXNwb25zZRKAAQoVVXBsb2FkQ29tbWFuZEFydGlmYWN0EjEuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjIuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZSgBEoYBChdEb3dubG9hZENvbW1hbmRBcnRpZmFjdBIzLmZsZWV0bm9kZWdhdGV3YXkudjEuRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjQuZmxlZXRub2RlZ2F0ZXdheS52MS5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlc3BvbnNlMAEShAEKF1JlcG9ydERpc2NvdmVyZWREZXZpY2VzEjMuZmxlZXRub2RlZ2F0ZXdheS52MS5SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1JlcXVlc3QaNC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVzcG9uc2USeAoTUmVwb3J0UGFpcmVkRGV2aWNlcxIvLmZsZWV0bm9kZWdhdGV3YXkudjEuUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QaMC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydFBhaXJlZERldmljZXNSZXNwb25zZRJqCg1Db250cm9sU3RyZWFtEikuZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sU3RyZWFtUmVxdWVzdBoqLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbFN0cmVhbVJlc3BvbnNlKAEwAUL4AQoXY29tLmZsZWV0bm9kZWdhdGV3YXkudjFCFUZsZWV0bm9kZWdhdGV3YXlQcm90b1ABWllnaXRodWIuY29tL2Jsb2NrL3Byb3RvLWZsZWV0L3NlcnZlci9nZW5lcmF0ZWQvZ3JwYy9mbGVldG5vZGVnYXRld2F5L3YxO2ZsZWV0bm9kZWdhdGV3YXl2MaICA0ZYWKoCE0ZsZWV0bm9kZWdhdGV3YXkuVjHKAhNGbGVldG5vZGVnYXRld2F5XFYx4gIfRmxlZXRub2RlZ2F0ZXdheVxWMVxHUEJNZXRhZGF0YeoCFEZsZWV0bm9kZWdhdGV3YXk6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_common_v1_cooling, file_curtailment_v1_curtailment, file_errors_v1_errors, file_google_protobuf_timestamp, file_minercommand_v1_command, file_pairing_v1_pairing, file_telemetry_v1_telemetry]); /** * @generated from message fleetnodegateway.v1.RegisterRequest @@ -77,8 +64,7 @@ export type RegisterRequest = Message<"fleetnodegateway.v1.RegisterRequest"> & { * Describes the message fleetnodegateway.v1.RegisterRequest. * Use `create(RegisterRequestSchema)` to create a new message. */ -export const RegisterRequestSchema: GenMessage = - /*@__PURE__*/ +export const RegisterRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); /** @@ -105,8 +91,7 @@ export type RegisterResponse = Message<"fleetnodegateway.v1.RegisterResponse"> & * Describes the message fleetnodegateway.v1.RegisterResponse. * Use `create(RegisterResponseSchema)` to create a new message. */ -export const RegisterResponseSchema: GenMessage = - /*@__PURE__*/ +export const RegisterResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 1); /** @@ -128,8 +113,7 @@ export type BeginAuthHandshakeRequest = Message<"fleetnodegateway.v1.BeginAuthHa * Describes the message fleetnodegateway.v1.BeginAuthHandshakeRequest. * Use `create(BeginAuthHandshakeRequestSchema)` to create a new message. */ -export const BeginAuthHandshakeRequestSchema: GenMessage = - /*@__PURE__*/ +export const BeginAuthHandshakeRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 2); /** @@ -151,8 +135,7 @@ export type BeginAuthHandshakeResponse = Message<"fleetnodegateway.v1.BeginAuthH * Describes the message fleetnodegateway.v1.BeginAuthHandshakeResponse. * Use `create(BeginAuthHandshakeResponseSchema)` to create a new message. */ -export const BeginAuthHandshakeResponseSchema: GenMessage = - /*@__PURE__*/ +export const BeginAuthHandshakeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); /** @@ -174,8 +157,7 @@ export type CompleteAuthHandshakeRequest = Message<"fleetnodegateway.v1.Complete * Describes the message fleetnodegateway.v1.CompleteAuthHandshakeRequest. * Use `create(CompleteAuthHandshakeRequestSchema)` to create a new message. */ -export const CompleteAuthHandshakeRequestSchema: GenMessage = - /*@__PURE__*/ +export const CompleteAuthHandshakeRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 4); /** @@ -197,8 +179,7 @@ export type CompleteAuthHandshakeResponse = Message<"fleetnodegateway.v1.Complet * Describes the message fleetnodegateway.v1.CompleteAuthHandshakeResponse. * Use `create(CompleteAuthHandshakeResponseSchema)` to create a new message. */ -export const CompleteAuthHandshakeResponseSchema: GenMessage = - /*@__PURE__*/ +export const CompleteAuthHandshakeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 5); /** @@ -220,8 +201,7 @@ export type UploadTelemetryRequest = Message<"fleetnodegateway.v1.UploadTelemetr * Describes the message fleetnodegateway.v1.UploadTelemetryRequest. * Use `create(UploadTelemetryRequestSchema)` to create a new message. */ -export const UploadTelemetryRequestSchema: GenMessage = - /*@__PURE__*/ +export const UploadTelemetryRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 6); /** @@ -238,8 +218,7 @@ export type UploadTelemetryResponse = Message<"fleetnodegateway.v1.UploadTelemet * Describes the message fleetnodegateway.v1.UploadTelemetryResponse. * Use `create(UploadTelemetryResponseSchema)` to create a new message. */ -export const UploadTelemetryResponseSchema: GenMessage = - /*@__PURE__*/ +export const UploadTelemetryResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 7); /** @@ -261,8 +240,7 @@ export type UploadEventsRequest = Message<"fleetnodegateway.v1.UploadEventsReque * Describes the message fleetnodegateway.v1.UploadEventsRequest. * Use `create(UploadEventsRequestSchema)` to create a new message. */ -export const UploadEventsRequestSchema: GenMessage = - /*@__PURE__*/ +export const UploadEventsRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 8); /** @@ -279,8 +257,7 @@ export type UploadEventsResponse = Message<"fleetnodegateway.v1.UploadEventsResp * Describes the message fleetnodegateway.v1.UploadEventsResponse. * Use `create(UploadEventsResponseSchema)` to create a new message. */ -export const UploadEventsResponseSchema: GenMessage = - /*@__PURE__*/ +export const UploadEventsResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 9); /** @@ -297,8 +274,7 @@ export type UploadHeartbeatRequest = Message<"fleetnodegateway.v1.UploadHeartbea * Describes the message fleetnodegateway.v1.UploadHeartbeatRequest. * Use `create(UploadHeartbeatRequestSchema)` to create a new message. */ -export const UploadHeartbeatRequestSchema: GenMessage = - /*@__PURE__*/ +export const UploadHeartbeatRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 10); /** @@ -315,8 +291,7 @@ export type UploadHeartbeatResponse = Message<"fleetnodegateway.v1.UploadHeartbe * Describes the message fleetnodegateway.v1.UploadHeartbeatResponse. * Use `create(UploadHeartbeatResponseSchema)` to create a new message. */ -export const UploadHeartbeatResponseSchema: GenMessage = - /*@__PURE__*/ +export const UploadHeartbeatResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 11); /** @@ -353,8 +328,7 @@ export type CommandArtifactRef = Message<"fleetnodegateway.v1.CommandArtifactRef * Describes the message fleetnodegateway.v1.CommandArtifactRef. * Use `create(CommandArtifactRefSchema)` to create a new message. */ -export const CommandArtifactRefSchema: GenMessage = - /*@__PURE__*/ +export const CommandArtifactRefSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 12); /** @@ -396,8 +370,7 @@ export type CommandArtifactUploadHeader = Message<"fleetnodegateway.v1.CommandAr * Describes the message fleetnodegateway.v1.CommandArtifactUploadHeader. * Use `create(CommandArtifactUploadHeaderSchema)` to create a new message. */ -export const CommandArtifactUploadHeaderSchema: GenMessage = - /*@__PURE__*/ +export const CommandArtifactUploadHeaderSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 13); /** @@ -414,8 +387,7 @@ export type CommandArtifactChunk = Message<"fleetnodegateway.v1.CommandArtifactC * Describes the message fleetnodegateway.v1.CommandArtifactChunk. * Use `create(CommandArtifactChunkSchema)` to create a new message. */ -export const CommandArtifactChunkSchema: GenMessage = - /*@__PURE__*/ +export const CommandArtifactChunkSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 14); /** @@ -425,30 +397,26 @@ export type UploadCommandArtifactRequest = Message<"fleetnodegateway.v1.UploadCo /** * @generated from oneof fleetnodegateway.v1.UploadCommandArtifactRequest.part */ - part: - | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactUploadHeader header = 1; - */ - value: CommandArtifactUploadHeader; - case: "header"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; - */ - value: CommandArtifactChunk; - case: "chunk"; - } - | { case: undefined; value?: undefined }; + part: { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactUploadHeader header = 1; + */ + value: CommandArtifactUploadHeader; + case: "header"; + } | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; + */ + value: CommandArtifactChunk; + case: "chunk"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.UploadCommandArtifactRequest. * Use `create(UploadCommandArtifactRequestSchema)` to create a new message. */ -export const UploadCommandArtifactRequestSchema: GenMessage = - /*@__PURE__*/ +export const UploadCommandArtifactRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 15); /** @@ -465,8 +433,7 @@ export type UploadCommandArtifactResponse = Message<"fleetnodegateway.v1.UploadC * Describes the message fleetnodegateway.v1.UploadCommandArtifactResponse. * Use `create(UploadCommandArtifactResponseSchema)` to create a new message. */ -export const UploadCommandArtifactResponseSchema: GenMessage = - /*@__PURE__*/ +export const UploadCommandArtifactResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 16); /** @@ -493,8 +460,7 @@ export type DownloadCommandArtifactRequest = Message<"fleetnodegateway.v1.Downlo * Describes the message fleetnodegateway.v1.DownloadCommandArtifactRequest. * Use `create(DownloadCommandArtifactRequestSchema)` to create a new message. */ -export const DownloadCommandArtifactRequestSchema: GenMessage = - /*@__PURE__*/ +export const DownloadCommandArtifactRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 17); /** @@ -511,8 +477,7 @@ export type CommandArtifactDownloadHeader = Message<"fleetnodegateway.v1.Command * Describes the message fleetnodegateway.v1.CommandArtifactDownloadHeader. * Use `create(CommandArtifactDownloadHeaderSchema)` to create a new message. */ -export const CommandArtifactDownloadHeaderSchema: GenMessage = - /*@__PURE__*/ +export const CommandArtifactDownloadHeaderSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 18); /** @@ -522,30 +487,26 @@ export type DownloadCommandArtifactResponse = Message<"fleetnodegateway.v1.Downl /** * @generated from oneof fleetnodegateway.v1.DownloadCommandArtifactResponse.part */ - part: - | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactDownloadHeader header = 1; - */ - value: CommandArtifactDownloadHeader; - case: "header"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; - */ - value: CommandArtifactChunk; - case: "chunk"; - } - | { case: undefined; value?: undefined }; + part: { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactDownloadHeader header = 1; + */ + value: CommandArtifactDownloadHeader; + case: "header"; + } | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; + */ + value: CommandArtifactChunk; + case: "chunk"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.DownloadCommandArtifactResponse. * Use `create(DownloadCommandArtifactResponseSchema)` to create a new message. */ -export const DownloadCommandArtifactResponseSchema: GenMessage = - /*@__PURE__*/ +export const DownloadCommandArtifactResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 19); /** @@ -572,8 +533,7 @@ export type ReportDiscoveredDevicesRequest = Message<"fleetnodegateway.v1.Report * Describes the message fleetnodegateway.v1.ReportDiscoveredDevicesRequest. * Use `create(ReportDiscoveredDevicesRequestSchema)` to create a new message. */ -export const ReportDiscoveredDevicesRequestSchema: GenMessage = - /*@__PURE__*/ +export const ReportDiscoveredDevicesRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 20); /** @@ -630,8 +590,7 @@ export type DiscoveredDeviceReport = Message<"fleetnodegateway.v1.DiscoveredDevi * Describes the message fleetnodegateway.v1.DiscoveredDeviceReport. * Use `create(DiscoveredDeviceReportSchema)` to create a new message. */ -export const DiscoveredDeviceReportSchema: GenMessage = - /*@__PURE__*/ +export const DiscoveredDeviceReportSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 21); /** @@ -657,8 +616,7 @@ export type ReportDiscoveredDevicesResponse = Message<"fleetnodegateway.v1.Repor * Describes the message fleetnodegateway.v1.ReportDiscoveredDevicesResponse. * Use `create(ReportDiscoveredDevicesResponseSchema)` to create a new message. */ -export const ReportDiscoveredDevicesResponseSchema: GenMessage = - /*@__PURE__*/ +export const ReportDiscoveredDevicesResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 22); /** @@ -731,8 +689,7 @@ export type FleetNodePairResult = Message<"fleetnodegateway.v1.FleetNodePairResu * Describes the message fleetnodegateway.v1.FleetNodePairResult. * Use `create(FleetNodePairResultSchema)` to create a new message. */ -export const FleetNodePairResultSchema: GenMessage = - /*@__PURE__*/ +export const FleetNodePairResultSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 23); /** @@ -754,8 +711,7 @@ export type EncryptedCredentials = Message<"fleetnodegateway.v1.EncryptedCredent * Describes the message fleetnodegateway.v1.EncryptedCredentials. * Use `create(EncryptedCredentialsSchema)` to create a new message. */ -export const EncryptedCredentialsSchema: GenMessage = - /*@__PURE__*/ +export const EncryptedCredentialsSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 24); /** @@ -781,8 +737,7 @@ export type ReportPairedDevicesRequest = Message<"fleetnodegateway.v1.ReportPair * Describes the message fleetnodegateway.v1.ReportPairedDevicesRequest. * Use `create(ReportPairedDevicesRequestSchema)` to create a new message. */ -export const ReportPairedDevicesRequestSchema: GenMessage = - /*@__PURE__*/ +export const ReportPairedDevicesRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 25); /** @@ -804,8 +759,7 @@ export type ReportPairedDevicesResponse = Message<"fleetnodegateway.v1.ReportPai * Describes the message fleetnodegateway.v1.ReportPairedDevicesResponse. * Use `create(ReportPairedDevicesResponseSchema)` to create a new message. */ -export const ReportPairedDevicesResponseSchema: GenMessage = - /*@__PURE__*/ +export const ReportPairedDevicesResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 26); /** @@ -815,30 +769,26 @@ export type ControlStreamRequest = Message<"fleetnodegateway.v1.ControlStreamReq /** * @generated from oneof fleetnodegateway.v1.ControlStreamRequest.kind */ - kind: - | { - /** - * @generated from field: fleetnodegateway.v1.ControlHello hello = 1; - */ - value: ControlHello; - case: "hello"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.ControlAck ack = 2; - */ - value: ControlAck; - case: "ack"; - } - | { case: undefined; value?: undefined }; + kind: { + /** + * @generated from field: fleetnodegateway.v1.ControlHello hello = 1; + */ + value: ControlHello; + case: "hello"; + } | { + /** + * @generated from field: fleetnodegateway.v1.ControlAck ack = 2; + */ + value: ControlAck; + case: "ack"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.ControlStreamRequest. * Use `create(ControlStreamRequestSchema)` to create a new message. */ -export const ControlStreamRequestSchema: GenMessage = - /*@__PURE__*/ +export const ControlStreamRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 27); /** @@ -848,43 +798,39 @@ export type ControlStreamResponse = Message<"fleetnodegateway.v1.ControlStreamRe /** * @generated from oneof fleetnodegateway.v1.ControlStreamResponse.kind */ - kind: - | { - /** - * @generated from field: fleetnodegateway.v1.ControlAccepted accepted = 1; - */ - value: ControlAccepted; - case: "accepted"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.ControlCommand command = 2; - */ - value: ControlCommand; - case: "command"; - } - | { case: undefined; value?: undefined }; + kind: { + /** + * @generated from field: fleetnodegateway.v1.ControlAccepted accepted = 1; + */ + value: ControlAccepted; + case: "accepted"; + } | { + /** + * @generated from field: fleetnodegateway.v1.ControlCommand command = 2; + */ + value: ControlCommand; + case: "command"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.ControlStreamResponse. * Use `create(ControlStreamResponseSchema)` to create a new message. */ -export const ControlStreamResponseSchema: GenMessage = - /*@__PURE__*/ +export const ControlStreamResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 28); /** * @generated from message fleetnodegateway.v1.ControlHello */ -export type ControlHello = Message<"fleetnodegateway.v1.ControlHello"> & {}; +export type ControlHello = Message<"fleetnodegateway.v1.ControlHello"> & { +}; /** * Describes the message fleetnodegateway.v1.ControlHello. * Use `create(ControlHelloSchema)` to create a new message. */ -export const ControlHelloSchema: GenMessage = - /*@__PURE__*/ +export const ControlHelloSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 29); /** @@ -901,8 +847,7 @@ export type ControlAccepted = Message<"fleetnodegateway.v1.ControlAccepted"> & { * Describes the message fleetnodegateway.v1.ControlAccepted. * Use `create(ControlAcceptedSchema)` to create a new message. */ -export const ControlAcceptedSchema: GenMessage = - /*@__PURE__*/ +export const ControlAcceptedSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 30); /** @@ -924,8 +869,7 @@ export type ControlCommand = Message<"fleetnodegateway.v1.ControlCommand"> & { * Describes the message fleetnodegateway.v1.ControlCommand. * Use `create(ControlCommandSchema)` to create a new message. */ -export const ControlCommandSchema: GenMessage = - /*@__PURE__*/ +export const ControlCommandSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 31); /** @@ -992,8 +936,7 @@ export type MinerConnectionDescriptor = Message<"fleetnodegateway.v1.MinerConnec * Describes the message fleetnodegateway.v1.MinerConnectionDescriptor. * Use `create(MinerConnectionDescriptorSchema)` to create a new message. */ -export const MinerConnectionDescriptorSchema: GenMessage = - /*@__PURE__*/ +export const MinerConnectionDescriptorSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 32); /** @@ -1012,193 +955,202 @@ export type MinerCommand = Message<"fleetnodegateway.v1.MinerCommand"> & { /** * @generated from oneof fleetnodegateway.v1.MinerCommand.action */ - action: - | { - /** - * @generated from field: fleetnodegateway.v1.RebootAction reboot = 2; - */ - value: RebootAction; - case: "reboot"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.StartMiningAction start_mining = 3; - */ - value: StartMiningAction; - case: "startMining"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.StopMiningAction stop_mining = 4; - */ - value: StopMiningAction; - case: "stopMining"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.BlinkLedAction blink_led = 5; - */ - value: BlinkLedAction; - case: "blinkLed"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.CurtailAction curtail = 6; - */ - value: CurtailAction; - case: "curtail"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.UncurtailAction uncurtail = 7; - */ - value: UncurtailAction; - case: "uncurtail"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.SetCoolingModeAction set_cooling_mode = 8; - */ - value: SetCoolingModeAction; - case: "setCoolingMode"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.SetPowerTargetAction set_power_target = 9; - */ - value: SetPowerTargetAction; - case: "setPowerTarget"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.UpdateMiningPoolsAction update_mining_pools = 10; - */ - value: UpdateMiningPoolsAction; - case: "updateMiningPools"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.GetMiningPoolsAction get_mining_pools = 11; - */ - value: GetMiningPoolsAction; - case: "getMiningPools"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.GetErrorsAction get_errors = 12; - */ - value: GetErrorsAction; - case: "getErrors"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.UpdateMinerPasswordAction update_miner_password = 13; - */ - value: UpdateMinerPasswordAction; - case: "updateMinerPassword"; - } - | { case: undefined; value?: undefined }; + action: { + /** + * @generated from field: fleetnodegateway.v1.RebootAction reboot = 2; + */ + value: RebootAction; + case: "reboot"; + } | { + /** + * @generated from field: fleetnodegateway.v1.StartMiningAction start_mining = 3; + */ + value: StartMiningAction; + case: "startMining"; + } | { + /** + * @generated from field: fleetnodegateway.v1.StopMiningAction stop_mining = 4; + */ + value: StopMiningAction; + case: "stopMining"; + } | { + /** + * @generated from field: fleetnodegateway.v1.BlinkLedAction blink_led = 5; + */ + value: BlinkLedAction; + case: "blinkLed"; + } | { + /** + * @generated from field: fleetnodegateway.v1.CurtailAction curtail = 6; + */ + value: CurtailAction; + case: "curtail"; + } | { + /** + * @generated from field: fleetnodegateway.v1.UncurtailAction uncurtail = 7; + */ + value: UncurtailAction; + case: "uncurtail"; + } | { + /** + * @generated from field: fleetnodegateway.v1.SetCoolingModeAction set_cooling_mode = 8; + */ + value: SetCoolingModeAction; + case: "setCoolingMode"; + } | { + /** + * @generated from field: fleetnodegateway.v1.SetPowerTargetAction set_power_target = 9; + */ + value: SetPowerTargetAction; + case: "setPowerTarget"; + } | { + /** + * @generated from field: fleetnodegateway.v1.UpdateMiningPoolsAction update_mining_pools = 10; + */ + value: UpdateMiningPoolsAction; + case: "updateMiningPools"; + } | { + /** + * @generated from field: fleetnodegateway.v1.GetMiningPoolsAction get_mining_pools = 11; + */ + value: GetMiningPoolsAction; + case: "getMiningPools"; + } | { + /** + * @generated from field: fleetnodegateway.v1.GetErrorsAction get_errors = 12; + */ + value: GetErrorsAction; + case: "getErrors"; + } | { + /** + * @generated from field: fleetnodegateway.v1.UpdateMinerPasswordAction update_miner_password = 13; + */ + value: UpdateMinerPasswordAction; + case: "updateMinerPassword"; + } | { + /** + * @generated from field: fleetnodegateway.v1.DownloadLogsAction download_logs = 14; + */ + value: DownloadLogsAction; + case: "downloadLogs"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.MinerCommand. * Use `create(MinerCommandSchema)` to create a new message. */ -export const MinerCommandSchema: GenMessage = - /*@__PURE__*/ +export const MinerCommandSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 33); /** * @generated from message fleetnodegateway.v1.RebootAction */ -export type RebootAction = Message<"fleetnodegateway.v1.RebootAction"> & {}; +export type RebootAction = Message<"fleetnodegateway.v1.RebootAction"> & { +}; /** * Describes the message fleetnodegateway.v1.RebootAction. * Use `create(RebootActionSchema)` to create a new message. */ -export const RebootActionSchema: GenMessage = - /*@__PURE__*/ +export const RebootActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 34); /** * @generated from message fleetnodegateway.v1.StartMiningAction */ -export type StartMiningAction = Message<"fleetnodegateway.v1.StartMiningAction"> & {}; +export type StartMiningAction = Message<"fleetnodegateway.v1.StartMiningAction"> & { +}; /** * Describes the message fleetnodegateway.v1.StartMiningAction. * Use `create(StartMiningActionSchema)` to create a new message. */ -export const StartMiningActionSchema: GenMessage = - /*@__PURE__*/ +export const StartMiningActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 35); /** * @generated from message fleetnodegateway.v1.StopMiningAction */ -export type StopMiningAction = Message<"fleetnodegateway.v1.StopMiningAction"> & {}; +export type StopMiningAction = Message<"fleetnodegateway.v1.StopMiningAction"> & { +}; /** * Describes the message fleetnodegateway.v1.StopMiningAction. * Use `create(StopMiningActionSchema)` to create a new message. */ -export const StopMiningActionSchema: GenMessage = - /*@__PURE__*/ +export const StopMiningActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 36); /** * @generated from message fleetnodegateway.v1.BlinkLedAction */ -export type BlinkLedAction = Message<"fleetnodegateway.v1.BlinkLedAction"> & {}; +export type BlinkLedAction = Message<"fleetnodegateway.v1.BlinkLedAction"> & { +}; /** * Describes the message fleetnodegateway.v1.BlinkLedAction. * Use `create(BlinkLedActionSchema)` to create a new message. */ -export const BlinkLedActionSchema: GenMessage = - /*@__PURE__*/ +export const BlinkLedActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 37); /** * @generated from message fleetnodegateway.v1.UncurtailAction */ -export type UncurtailAction = Message<"fleetnodegateway.v1.UncurtailAction"> & {}; +export type UncurtailAction = Message<"fleetnodegateway.v1.UncurtailAction"> & { +}; /** * Describes the message fleetnodegateway.v1.UncurtailAction. * Use `create(UncurtailActionSchema)` to create a new message. */ -export const UncurtailActionSchema: GenMessage = - /*@__PURE__*/ +export const UncurtailActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 38); /** * @generated from message fleetnodegateway.v1.GetMiningPoolsAction */ -export type GetMiningPoolsAction = Message<"fleetnodegateway.v1.GetMiningPoolsAction"> & {}; +export type GetMiningPoolsAction = Message<"fleetnodegateway.v1.GetMiningPoolsAction"> & { +}; /** * Describes the message fleetnodegateway.v1.GetMiningPoolsAction. * Use `create(GetMiningPoolsActionSchema)` to create a new message. */ -export const GetMiningPoolsActionSchema: GenMessage = - /*@__PURE__*/ +export const GetMiningPoolsActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 39); /** * @generated from message fleetnodegateway.v1.GetErrorsAction */ -export type GetErrorsAction = Message<"fleetnodegateway.v1.GetErrorsAction"> & {}; +export type GetErrorsAction = Message<"fleetnodegateway.v1.GetErrorsAction"> & { +}; /** * Describes the message fleetnodegateway.v1.GetErrorsAction. * Use `create(GetErrorsActionSchema)` to create a new message. */ -export const GetErrorsActionSchema: GenMessage = - /*@__PURE__*/ +export const GetErrorsActionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 40); +/** + * @generated from message fleetnodegateway.v1.DownloadLogsAction + */ +export type DownloadLogsAction = Message<"fleetnodegateway.v1.DownloadLogsAction"> & { + /** + * @generated from field: string batch_log_uuid = 1; + */ + batchLogUuid: string; +}; + +/** + * Describes the message fleetnodegateway.v1.DownloadLogsAction. + * Use `create(DownloadLogsActionSchema)` to create a new message. + */ +export const DownloadLogsActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 41); + /** * @generated from message fleetnodegateway.v1.NodeEncryptedPayload */ @@ -1228,9 +1180,8 @@ export type NodeEncryptedPayload = Message<"fleetnodegateway.v1.NodeEncryptedPay * Describes the message fleetnodegateway.v1.NodeEncryptedPayload. * Use `create(NodeEncryptedPayloadSchema)` to create a new message. */ -export const NodeEncryptedPayloadSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 41); +export const NodeEncryptedPayloadSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 42); /** * @generated from message fleetnodegateway.v1.UpdateMinerPasswordAction @@ -1246,9 +1197,8 @@ export type UpdateMinerPasswordAction = Message<"fleetnodegateway.v1.UpdateMiner * Describes the message fleetnodegateway.v1.UpdateMinerPasswordAction. * Use `create(UpdateMinerPasswordActionSchema)` to create a new message. */ -export const UpdateMinerPasswordActionSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 42); +export const UpdateMinerPasswordActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 43); /** * @generated from message fleetnodegateway.v1.UpdateMinerPasswordResult @@ -1264,9 +1214,8 @@ export type UpdateMinerPasswordResult = Message<"fleetnodegateway.v1.UpdateMiner * Describes the message fleetnodegateway.v1.UpdateMinerPasswordResult. * Use `create(UpdateMinerPasswordResultSchema)` to create a new message. */ -export const UpdateMinerPasswordResultSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 43); +export const UpdateMinerPasswordResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 44); /** * @generated from message fleetnodegateway.v1.CurtailAction @@ -1282,9 +1231,8 @@ export type CurtailAction = Message<"fleetnodegateway.v1.CurtailAction"> & { * Describes the message fleetnodegateway.v1.CurtailAction. * Use `create(CurtailActionSchema)` to create a new message. */ -export const CurtailActionSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 44); +export const CurtailActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 45); /** * @generated from message fleetnodegateway.v1.SetCoolingModeAction @@ -1300,9 +1248,8 @@ export type SetCoolingModeAction = Message<"fleetnodegateway.v1.SetCoolingModeAc * Describes the message fleetnodegateway.v1.SetCoolingModeAction. * Use `create(SetCoolingModeActionSchema)` to create a new message. */ -export const SetCoolingModeActionSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 45); +export const SetCoolingModeActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 46); /** * @generated from message fleetnodegateway.v1.SetPowerTargetAction @@ -1318,9 +1265,8 @@ export type SetPowerTargetAction = Message<"fleetnodegateway.v1.SetPowerTargetAc * Describes the message fleetnodegateway.v1.SetPowerTargetAction. * Use `create(SetPowerTargetActionSchema)` to create a new message. */ -export const SetPowerTargetActionSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 46); +export const SetPowerTargetActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 47); /** * @generated from message fleetnodegateway.v1.MiningPoolConfig @@ -1346,9 +1292,8 @@ export type MiningPoolConfig = Message<"fleetnodegateway.v1.MiningPoolConfig"> & * Describes the message fleetnodegateway.v1.MiningPoolConfig. * Use `create(MiningPoolConfigSchema)` to create a new message. */ -export const MiningPoolConfigSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 47); +export const MiningPoolConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 48); /** * @generated from message fleetnodegateway.v1.UpdateMiningPoolsAction @@ -1364,9 +1309,8 @@ export type UpdateMiningPoolsAction = Message<"fleetnodegateway.v1.UpdateMiningP * Describes the message fleetnodegateway.v1.UpdateMiningPoolsAction. * Use `create(UpdateMiningPoolsActionSchema)` to create a new message. */ -export const UpdateMiningPoolsActionSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 48); +export const UpdateMiningPoolsActionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 49); /** * @generated from message fleetnodegateway.v1.GetMiningPoolsResult @@ -1382,9 +1326,8 @@ export type GetMiningPoolsResult = Message<"fleetnodegateway.v1.GetMiningPoolsRe * Describes the message fleetnodegateway.v1.GetMiningPoolsResult. * Use `create(GetMiningPoolsResultSchema)` to create a new message. */ -export const GetMiningPoolsResultSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 49); +export const GetMiningPoolsResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 50); /** * @generated from message fleetnodegateway.v1.MinerErrorReport @@ -1460,9 +1403,8 @@ export type MinerErrorReport = Message<"fleetnodegateway.v1.MinerErrorReport"> & * Describes the message fleetnodegateway.v1.MinerErrorReport. * Use `create(MinerErrorReportSchema)` to create a new message. */ -export const MinerErrorReportSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 50); +export const MinerErrorReportSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 51); /** * @generated from message fleetnodegateway.v1.GetErrorsResult @@ -1493,9 +1435,8 @@ export type GetErrorsResult = Message<"fleetnodegateway.v1.GetErrorsResult"> & { * Describes the message fleetnodegateway.v1.GetErrorsResult. * Use `create(GetErrorsResultSchema)` to create a new message. */ -export const GetErrorsResultSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 51); +export const GetErrorsResultSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 52); /** * AgentCommand is the typed envelope marshaled into ControlCommand.payload so a @@ -1515,45 +1456,39 @@ export type AgentCommand = Message<"fleetnodegateway.v1.AgentCommand"> & { /** * @generated from oneof fleetnodegateway.v1.AgentCommand.command */ - command: - | { - /** - * @generated from field: pairing.v1.DiscoverRequest discover = 1; - */ - value: DiscoverRequest; - case: "discover"; - } - | { - /** - * @generated from field: pairing.v1.FleetNodePairRequest pair = 2; - */ - value: FleetNodePairRequest; - case: "pair"; - } - | { - /** - * @generated from field: fleetnodegateway.v1.MinerCommand miner_command = 3; - */ - value: MinerCommand; - case: "minerCommand"; - } - | { - /** - * @generated from field: telemetry.v1.FleetNodeTelemetryRequest telemetry = 4; - */ - value: FleetNodeTelemetryRequest; - case: "telemetry"; - } - | { case: undefined; value?: undefined }; + command: { + /** + * @generated from field: pairing.v1.DiscoverRequest discover = 1; + */ + value: DiscoverRequest; + case: "discover"; + } | { + /** + * @generated from field: pairing.v1.FleetNodePairRequest pair = 2; + */ + value: FleetNodePairRequest; + case: "pair"; + } | { + /** + * @generated from field: fleetnodegateway.v1.MinerCommand miner_command = 3; + */ + value: MinerCommand; + case: "minerCommand"; + } | { + /** + * @generated from field: telemetry.v1.FleetNodeTelemetryRequest telemetry = 4; + */ + value: FleetNodeTelemetryRequest; + case: "telemetry"; + } | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.AgentCommand. * Use `create(AgentCommandSchema)` to create a new message. */ -export const AgentCommandSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 52); +export const AgentCommandSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 53); /** * @generated from message fleetnodegateway.v1.ControlAck @@ -1591,9 +1526,8 @@ export type ControlAck = Message<"fleetnodegateway.v1.ControlAck"> & { * Describes the message fleetnodegateway.v1.ControlAck. * Use `create(ControlAckSchema)` to create a new message. */ -export const ControlAckSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 53); +export const ControlAckSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 54); /** * @generated from enum fleetnodegateway.v1.EnrollmentStatus @@ -1623,8 +1557,7 @@ export enum EnrollmentStatus { /** * Describes the enum fleetnodegateway.v1.EnrollmentStatus. */ -export const EnrollmentStatusSchema: GenEnum = - /*@__PURE__*/ +export const EnrollmentStatusSchema: GenEnum = /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); /** @@ -1654,8 +1587,7 @@ export enum CommandArtifactPurpose { /** * Describes the enum fleetnodegateway.v1.CommandArtifactPurpose. */ -export const CommandArtifactPurposeSchema: GenEnum = - /*@__PURE__*/ +export const CommandArtifactPurposeSchema: GenEnum = /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 1); /** @@ -1699,8 +1631,7 @@ export enum PairOutcome { /** * Describes the enum fleetnodegateway.v1.PairOutcome. */ -export const PairOutcomeSchema: GenEnum = - /*@__PURE__*/ +export const PairOutcomeSchema: GenEnum = /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 2); /** @@ -1797,7 +1728,8 @@ export enum AckCode { /** * Describes the enum fleetnodegateway.v1.AckCode. */ -export const AckCodeSchema: GenEnum = /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); +export const AckCodeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); /** * @generated from service fleetnodegateway.v1.FleetNodeGatewayService @@ -1810,7 +1742,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof RegisterRequestSchema; output: typeof RegisterResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake */ @@ -1818,7 +1750,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof BeginAuthHandshakeRequestSchema; output: typeof BeginAuthHandshakeResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake */ @@ -1826,7 +1758,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof CompleteAuthHandshakeRequestSchema; output: typeof CompleteAuthHandshakeResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry */ @@ -1834,7 +1766,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadTelemetryRequestSchema; output: typeof UploadTelemetryResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents */ @@ -1842,7 +1774,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadEventsRequestSchema; output: typeof UploadEventsResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat */ @@ -1850,7 +1782,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof UploadHeartbeatRequestSchema; output: typeof UploadHeartbeatResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact */ @@ -1858,7 +1790,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadCommandArtifactRequestSchema; output: typeof UploadCommandArtifactResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact */ @@ -1866,7 +1798,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "server_streaming"; input: typeof DownloadCommandArtifactRequestSchema; output: typeof DownloadCommandArtifactResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices */ @@ -1874,7 +1806,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof ReportDiscoveredDevicesRequestSchema; output: typeof ReportDiscoveredDevicesResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices */ @@ -1882,7 +1814,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof ReportPairedDevicesRequestSchema; output: typeof ReportPairedDevicesResponseSchema; - }; + }, /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ControlStream */ @@ -1890,5 +1822,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "bidi_streaming"; input: typeof ControlStreamRequestSchema; output: typeof ControlStreamResponseSchema; - }; -}> = /*@__PURE__*/ serviceDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); + }, +}> = /*@__PURE__*/ + serviceDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); + diff --git a/proto/fleetnodegateway/v1/fleetnodegateway.proto b/proto/fleetnodegateway/v1/fleetnodegateway.proto index 15d9732dd..4e2ac372a 100644 --- a/proto/fleetnodegateway/v1/fleetnodegateway.proto +++ b/proto/fleetnodegateway/v1/fleetnodegateway.proto @@ -329,6 +329,7 @@ message MinerCommand { GetMiningPoolsAction get_mining_pools = 11; GetErrorsAction get_errors = 12; UpdateMinerPasswordAction update_miner_password = 13; + DownloadLogsAction download_logs = 14; } } @@ -340,6 +341,10 @@ message UncurtailAction {} message GetMiningPoolsAction {} message GetErrorsAction {} +message DownloadLogsAction { + string batch_log_uuid = 1 [(buf.validate.field).string = {min_len: 1, max_len: 128}]; +} + message NodeEncryptedPayload { string algorithm = 1 [ (buf.validate.field).string = {min_len: 1, max_len: 64}, diff --git a/server/cmd/fleetnode/control.go b/server/cmd/fleetnode/control.go index 244183336..2882a4ec6 100644 --- a/server/cmd/fleetnode/control.go +++ b/server/cmd/fleetnode/control.go @@ -269,7 +269,7 @@ func (r *RunCmd) handleCommand(ctx context.Context, client gatewayClient, stream case *pb.AgentCommand_Discover: r.handleDiscover(ctx, client, stream, commandID, k.Discover, logger) case *pb.AgentCommand_MinerCommand: - r.handleMinerCommand(ctx, stream, commandID, k.MinerCommand, logger) + r.handleMinerCommand(ctx, client, stream, commandID, k.MinerCommand, logger) case *pb.AgentCommand_Pair: r.handlePairCommand(ctx, client, stream, commandID, k.Pair, logger) case *pb.AgentCommand_Telemetry: diff --git a/server/cmd/fleetnode/fake_gateway_test.go b/server/cmd/fleetnode/fake_gateway_test.go index c99302207..e3b9bb1a4 100644 --- a/server/cmd/fleetnode/fake_gateway_test.go +++ b/server/cmd/fleetnode/fake_gateway_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/ed25519" "errors" + "fmt" "net/http" "net/http/httptest" "sync" @@ -12,6 +13,7 @@ import ( "time" "connectrpc.com/connect" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" pb "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1" @@ -40,6 +42,11 @@ type fakeFleetNodeGateway struct { heartbeatsReceived []heartbeatRecord expectedSessionToken string onHeartbeat func(count int) + + artifactMu sync.Mutex + commandArtifactUploads []*pb.UploadCommandArtifactRequest + commandArtifactRef *pb.CommandArtifactRef + commandArtifactErr error } type heartbeatRecord struct { @@ -106,6 +113,31 @@ func (f *fakeFleetNodeGateway) UploadHeartbeat(_ context.Context, req *connect.R return connect.NewResponse(&pb.UploadHeartbeatResponse{ReceivedAt: timestamppb.Now()}), nil } +func (f *fakeFleetNodeGateway) UploadCommandArtifact(_ context.Context, stream *connect.ClientStream[pb.UploadCommandArtifactRequest]) (*connect.Response[pb.UploadCommandArtifactResponse], error) { + requests := []*pb.UploadCommandArtifactRequest{} + for stream.Receive() { + cloned := proto.Clone(stream.Msg()) + clonedReq, ok := cloned.(*pb.UploadCommandArtifactRequest) + if !ok { + return nil, fmt.Errorf("clone command artifact upload request: got %T", cloned) + } + requests = append(requests, clonedReq) + } + if err := stream.Err(); err != nil { + return nil, fmt.Errorf("receive command artifact upload: %w", err) + } + + f.artifactMu.Lock() + f.commandArtifactUploads = append(f.commandArtifactUploads, requests...) + ref := f.commandArtifactRef + err := f.commandArtifactErr + f.artifactMu.Unlock() + if err != nil { + return nil, err + } + return connect.NewResponse(&pb.UploadCommandArtifactResponse{Artifact: ref}), nil +} + func (f *fakeFleetNodeGateway) heartbeatCount() int { f.heartbeatMu.Lock() defer f.heartbeatMu.Unlock() @@ -120,6 +152,14 @@ func (f *fakeFleetNodeGateway) heartbeats() []heartbeatRecord { return out } +func (f *fakeFleetNodeGateway) artifactUploads() []*pb.UploadCommandArtifactRequest { + f.artifactMu.Lock() + defer f.artifactMu.Unlock() + out := make([]*pb.UploadCommandArtifactRequest, len(f.commandArtifactUploads)) + copy(out, f.commandArtifactUploads) + return out +} + func newFakeServer(t *testing.T, fake *fakeFleetNodeGateway) *httptest.Server { t.Helper() mux := http.NewServeMux() diff --git a/server/cmd/fleetnode/minercommand.go b/server/cmd/fleetnode/minercommand.go index 9b0cf3dee..66d0db01d 100644 --- a/server/cmd/fleetnode/minercommand.go +++ b/server/cmd/fleetnode/minercommand.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "context" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -22,6 +24,7 @@ import ( minercommandpb "github.com/block/proto-fleet/server/generated/grpc/minercommand/v1" "github.com/block/proto-fleet/server/internal/domain/fleetnode/commandresult" "github.com/block/proto-fleet/server/internal/domain/fleetnode/passwordupdate" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" minermodels "github.com/block/proto-fleet/server/internal/domain/miner/models" "github.com/block/proto-fleet/server/internal/domain/sv2" "github.com/block/proto-fleet/server/internal/fleetnode/bootstrap" @@ -38,6 +41,8 @@ const ( supportedMiningPoolSlots = 3 maxSupportedMiningPoolPriority = supportedMiningPoolSlots - 1 maxGetErrorsReports = 512 + minerLogsArtifactFilename = "miner-logs.csv" + commandArtifactChunkSize = 1 << 20 ) // driverGetter is the plugin-manager seam the executor needs; *plugins.Manager satisfies it. @@ -64,7 +69,7 @@ func (nodeSecretProvider) Seal(_ sdk.SecretBundle) (*pb.EncryptedCredentials, er return nil, cmdErr(pb.AckCode_ACK_CODE_AGENT_INCAPABLE, "fleet node has no credential sealer configured") } -func (r *RunCmd) handleMinerCommand(ctx context.Context, stream acker, commandID string, mc *pb.MinerCommand, logger *slog.Logger) { +func (r *RunCmd) handleMinerCommand(ctx context.Context, client gatewayClient, stream acker, commandID string, mc *pb.MinerCommand, logger *slog.Logger) { if r.driverGetter == nil || r.minerSecrets == nil { r.sendAck(stream, commandID, pb.AckCode_ACK_CODE_AGENT_INCAPABLE, "fleet node has no plugins loaded", logger) return @@ -128,11 +133,18 @@ func (r *RunCmd) handleMinerCommand(ctx context.Context, stream acker, commandID } }() + caps, err := commandCapabilities(cmdCtx, driver, mc) + if err != nil { + code, msg := classifyMinerCommandError("load driver capabilities", err) + r.sendAck(stream, commandID, code, msg, logger) + return + } + var payload []byte if passwordUpdate != nil { payload, err = passwordUpdate.run(cmdCtx, dev, bundle, r.minerSecrets) } else { - payload, err = runMinerAction(cmdCtx, dev, mc) + payload, err = runMinerAction(cmdCtx, client, commandID, caps, dev, mc) } if err != nil { code, msg := classifyMinerActionError("execute command", mc, err) @@ -218,7 +230,15 @@ func validateDialTarget(t *pb.MinerConnectionDescriptor) error { return nil } -func runMinerAction(ctx context.Context, dev sdk.Device, mc *pb.MinerCommand) ([]byte, error) { +func commandCapabilities(ctx context.Context, driver sdk.Driver, mc *pb.MinerCommand) (sdk.Capabilities, error) { + if _, ok := mc.GetAction().(*pb.MinerCommand_DownloadLogs); !ok { + return nil, nil + } + _, caps, err := driver.DescribeDriver(ctx) + return caps, err +} + +func runMinerAction(ctx context.Context, client gatewayClient, commandID string, caps sdk.Capabilities, dev sdk.Device, mc *pb.MinerCommand) ([]byte, error) { switch a := mc.GetAction().(type) { case *pb.MinerCommand_Reboot: return nil, dev.Reboot(ctx) @@ -274,11 +294,105 @@ func runMinerAction(ctx context.Context, dev sdk.Device, mc *pb.MinerCommand) ([ return nil, err } return getErrorsResultPayload(mc.GetTarget().GetDeviceIdentifier(), deviceErrors) + case *pb.MinerCommand_DownloadLogs: + logData, moreData, err := dev.DownloadLogs(ctx, nil, a.DownloadLogs.GetBatchLogUuid()) + if err != nil { + return nil, err + } + if moreData { + return nil, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log download returned partial data; retry after partial log pagination is supported") + } + payload, err := minerLogsArtifactPayload(logData, caps[sdk.CapabilityLogLevels]) + if err != nil { + return nil, err + } + if _, err := uploadMinerLogsArtifact(ctx, client, commandID, mc.GetTarget().GetDeviceIdentifier(), payload); err != nil { + return nil, err + } + return nil, nil default: return nil, cmdErr(pb.AckCode_ACK_CODE_BAD_REQUEST, "unrecognized miner command action") } } +func uploadMinerLogsArtifact(ctx context.Context, client gatewayClient, commandID string, deviceIdentifier string, payload []byte) (*pb.CommandArtifactRef, error) { + if client == nil { + return nil, fmt.Errorf("gateway client unavailable for miner log upload") + } + sum := sha256.Sum256(payload) + sha := hex.EncodeToString(sum[:]) + + stream := client.UploadCommandArtifact(ctx) + if stream == nil { + return nil, fmt.Errorf("gateway client returned nil command artifact upload stream") + } + if err := stream.Send(&pb.UploadCommandArtifactRequest{Part: &pb.UploadCommandArtifactRequest_Header{ + Header: &pb.CommandArtifactUploadHeader{ + CommandId: commandID, + Purpose: pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: minerLogsArtifactFilename, + SizeBytes: int64(len(payload)), + Sha256: sha, + DeviceIdentifier: deviceIdentifier, + }, + }}); err != nil { + return nil, fmt.Errorf("upload miner logs header: %w", err) + } + for offset := 0; offset < len(payload); offset += commandArtifactChunkSize { + end := offset + commandArtifactChunkSize + if end > len(payload) { + end = len(payload) + } + if err := stream.Send(&pb.UploadCommandArtifactRequest{Part: &pb.UploadCommandArtifactRequest_Chunk{ + Chunk: &pb.CommandArtifactChunk{Data: payload[offset:end]}, + }}); err != nil { + return nil, fmt.Errorf("upload miner logs chunk: %w", err) + } + } + resp, err := stream.CloseAndReceive() + if err != nil { + return nil, fmt.Errorf("finish miner logs upload: %w", err) + } + if resp == nil || resp.Msg == nil || resp.Msg.GetArtifact() == nil { + return nil, fmt.Errorf("miner logs upload returned no artifact") + } + ref := resp.Msg.GetArtifact() + if ref.GetPurpose() != pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS { + return nil, fmt.Errorf("miner logs upload returned artifact purpose %s", ref.GetPurpose()) + } + if ref.GetSizeBytes() != int64(len(payload)) { + return nil, fmt.Errorf("miner logs upload returned size %d, want %d", ref.GetSizeBytes(), len(payload)) + } + if ref.GetSha256() != sha { + return nil, fmt.Errorf("miner logs upload returned sha256 %q, want %q", ref.GetSha256(), sha) + } + return ref, nil +} + +func minerLogsArtifactPayload(logData string, includeType bool) ([]byte, error) { + if int64(len(logData)) > logformat.MaxArtifactBytes { + return nil, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log data exceeds %d byte download limit", logformat.MaxArtifactBytes) + } + payload := &limitedBuffer{limit: logformat.MaxArtifactBytes} + if err := logformat.WriteTextToCSV(payload, logData, includeType); err != nil { + return nil, err + } + return payload.Bytes(), nil +} + +type limitedBuffer struct { + bytes.Buffer + limit int64 +} + +func (b *limitedBuffer) Write(p []byte) (int, error) { + if int64(b.Len()+len(p)) > b.limit { + return 0, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log artifact exceeds %d byte download limit", b.limit) + } + n, _ := b.Buffer.Write(p) + return n, nil +} + func decryptUpdateMinerPasswordSecret(privateKey []byte, target *pb.MinerConnectionDescriptor, action *pb.UpdateMinerPasswordAction) (passwordupdate.Secret, error) { if len(privateKey) == 0 { return passwordupdate.Secret{}, cmdErr(pb.AckCode_ACK_CODE_AGENT_INCAPABLE, "fleet node has no password update decryption key configured") diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index 3871c8d5a..ba5e46223 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "strings" @@ -11,6 +13,7 @@ import ( "time" "buf.build/go/protovalidate" + "connectrpc.com/connect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -22,6 +25,7 @@ import ( curtailmentpb "github.com/block/proto-fleet/server/generated/grpc/curtailment/v1" errorspb "github.com/block/proto-fleet/server/generated/grpc/errors/v1" pb "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1" + "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1/fleetnodegatewayv1connect" minercommandpb "github.com/block/proto-fleet/server/generated/grpc/minercommand/v1" "github.com/block/proto-fleet/server/internal/domain/fleetnode/passwordupdate" minermodels "github.com/block/proto-fleet/server/internal/domain/miner/models" @@ -92,7 +96,7 @@ func TestHandleMinerCommand_ExecutesAndAcksOK(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_Reboot{Reboot: &pb.RebootAction{}}}), discardLogger(t)) // Assert @@ -127,7 +131,7 @@ func TestHandleMinerCommand_DecryptsTargetCredential(t *testing.T) { mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert assert.Equal(t, pb.AckCode_ACK_CODE_OK, ack.only(t).GetCode()) @@ -146,7 +150,7 @@ func TestHandleMinerCommand_InvalidTargetCredentialAcksUnauthenticated(t *testin mc.Target.CredentialPassword = []byte("not-a-valid-credential") // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert: rejected before the driver is dialed. assert.Equal(t, pb.AckCode_ACK_CODE_UNAUTHENTICATED, ack.only(t).GetCode()) @@ -171,7 +175,7 @@ func TestHandleMinerCommand_WrongKeyTargetCredentialAcksUnauthenticated(t *testi mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert: rejected before the driver is dialed. assert.Equal(t, pb.AckCode_ACK_CODE_UNAUTHENTICATED, ack.only(t).GetCode()) @@ -189,7 +193,7 @@ func TestHandleMinerCommand_ConvertsCoolingMode(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_SetCoolingMode{SetCoolingMode: &pb.SetCoolingModeAction{Mode: commonpb.CoolingMode_COOLING_MODE_IMMERSION_COOLED}}}), discardLogger(t)) // Assert @@ -219,7 +223,7 @@ func TestHandleMinerCommand_UpdatesMiningPools(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_UpdateMiningPools{UpdateMiningPools: &pb.UpdateMiningPoolsAction{ Pools: []*pb.MiningPoolConfig{ {Priority: 0, Url: "stratum+tcp://pool1.example.com:3333", Username: "worker1"}, @@ -261,7 +265,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordReturnsEncryptedCredentials(t *te mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert got := ack.only(t) @@ -305,7 +309,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordDialsWithCurrentPassword(t *testi mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert got := ack.only(t) @@ -344,7 +348,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordAllowsPasswordOnlyCredentials(t * mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert got := ack.only(t) @@ -381,7 +385,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordUsesCurrentPasswordWhenProtoCrede mc.Target.DriverName = minermodels.DriverNameProto // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert got := ack.only(t) @@ -415,7 +419,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordSealsBeforeUpdatingDevice(t *test ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_UpdateMinerPassword{ UpdateMinerPassword: action, }}), discardLogger(t)) @@ -451,7 +455,7 @@ func TestHandleMinerCommand_UpdateMinerPasswordFailedPreconditionAcksUnauthentic mc.Target.CredentialPassword = encrypted.GetPassword() // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert assert.Equal(t, pb.AckCode_ACK_CODE_UNAUTHENTICATED, ack.only(t).GetCode()) @@ -485,7 +489,7 @@ func TestHandleMinerCommand_GetMiningPoolsReturnsPayload(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_GetMiningPools{GetMiningPools: &pb.GetMiningPoolsAction{}}}), discardLogger(t)) // Assert @@ -536,7 +540,7 @@ func TestHandleMinerCommand_GetErrorsReturnsPayload(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_GetErrors{GetErrors: &pb.GetErrorsAction{}}}), discardLogger(t)) // Assert @@ -563,6 +567,96 @@ func TestHandleMinerCommand_GetErrorsReturnsPayload(t *testing.T) { assert.Equal(t, errorspb.ComponentType_COMPONENT_TYPE_PSU, errReport.GetComponentType()) } +func TestHandleMinerCommand_DownloadLogsUploadsCSVAndAcksOK(t *testing.T) { + cases := []struct { + name string + caps sdk.Capabilities + logData string + wantBody string + }{ + { + name: "with log levels", + caps: sdk.Capabilities{sdk.CapabilityLogLevels: true}, + logData: "2024-06-14 16:01:58.470952 | INFO | mcdd::temp | stable\n", + wantBody: "Time,Type,Message\n\"2024-06-14 16:01:58\",\"INFO\",\"mcdd::temp | stable\"\n", + }, + { + name: "without log levels", + caps: sdk.Capabilities{}, + logData: "2026-02-24 07:52:12 30m avg rate is 84933.16 in 30 mins\n", + wantBody: "Time,Message\n\"2026-02-24 07:52:12\",\"30m avg rate is 84933.16 in 30 mins\"\n", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return(tc.logData, false, nil) + dev.EXPECT().Close(gomock.Any()).Return(nil) + drv := mocks.NewMockDriver(ctrl) + drv.EXPECT().NewDevice(gomock.Any(), "dev-1", gomock.Any(), gomock.Any()).Return(sdk.NewDeviceResult{Device: dev}, nil) + drv.EXPECT().DescribeDriver(gomock.Any()).Return(sdk.DriverIdentifier{}, tc.caps, nil) + + sum := sha256.Sum256([]byte(tc.wantBody)) + sha := hex.EncodeToString(sum[:]) + fake := &fakeFleetNodeGateway{commandArtifactRef: &pb.CommandArtifactRef{ + ArtifactId: "artifact-1", + Purpose: pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: minerLogsArtifactFilename, + SizeBytes: int64(len(tc.wantBody)), + Sha256: sha, + }} + server := newFakeServer(t, fake) + client := fleetnodegatewayv1connect.NewFleetNodeGatewayServiceClient(server.Client(), server.URL) + r := &RunCmd{driverGetter: fakeDriverGetter{d: drv}, minerSecrets: nodeSecretProvider{}} + ack := &captureAcker{} + + r.handleMinerCommand(context.Background(), client, ack, "cmd-1", + withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_DownloadLogs{DownloadLogs: &pb.DownloadLogsAction{BatchLogUuid: "batch-1"}}}), discardLogger(t)) + + got := ack.only(t) + assert.Equal(t, pb.AckCode_ACK_CODE_OK, got.GetCode()) + assert.True(t, got.GetSucceeded()) + uploads := fake.artifactUploads() + require.Len(t, uploads, 2) + header := uploads[0].GetHeader() + require.NotNil(t, header) + assert.Equal(t, "cmd-1", header.GetCommandId()) + assert.Equal(t, pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, header.GetPurpose()) + assert.Equal(t, minerLogsArtifactFilename, header.GetFilename()) + assert.Equal(t, "dev-1", header.GetDeviceIdentifier()) + assert.Equal(t, int64(len(tc.wantBody)), header.GetSizeBytes()) + assert.Equal(t, sha, header.GetSha256()) + assert.Equal(t, tc.wantBody, string(uploads[1].GetChunk().GetData())) + }) + } +} + +func TestHandleMinerCommand_DownloadLogsAcksFailureWhenUploadFails(t *testing.T) { + ctrl := gomock.NewController(t) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return("2026-02-24 07:52:12 log line\n", false, nil) + dev.EXPECT().Close(gomock.Any()).Return(nil) + drv := mocks.NewMockDriver(ctrl) + drv.EXPECT().NewDevice(gomock.Any(), "dev-1", gomock.Any(), gomock.Any()).Return(sdk.NewDeviceResult{Device: dev}, nil) + drv.EXPECT().DescribeDriver(gomock.Any()).Return(sdk.DriverIdentifier{}, sdk.Capabilities{}, nil) + fake := &fakeFleetNodeGateway{commandArtifactErr: connect.NewError(connect.CodeInternal, errors.New("upload failed"))} + server := newFakeServer(t, fake) + client := fleetnodegatewayv1connect.NewFleetNodeGatewayServiceClient(server.Client(), server.URL) + r := &RunCmd{driverGetter: fakeDriverGetter{d: drv}, minerSecrets: nodeSecretProvider{}} + ack := &captureAcker{} + + r.handleMinerCommand(context.Background(), client, ack, "cmd-1", + withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_DownloadLogs{DownloadLogs: &pb.DownloadLogsAction{BatchLogUuid: "batch-1"}}}), discardLogger(t)) + + got := ack.only(t) + assert.Equal(t, pb.AckCode_ACK_CODE_INTERNAL, got.GetCode()) + assert.False(t, got.GetSucceeded()) + assert.Contains(t, got.GetErrorMessage(), "finish miner logs upload") + assert.NotEmpty(t, fake.artifactUploads()) +} + func TestGetErrorsResultFromSDKRejectsInvalidPluginErrorData(t *testing.T) { validError := func() sdk.DeviceError { return sdk.DeviceError{ @@ -688,7 +782,7 @@ func TestHandleMinerCommand_GetErrorsReturnsTruncatedPayloadForOversizedPayload( ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_GetErrors{GetErrors: &pb.GetErrorsAction{}}}), discardLogger(t)) // Assert @@ -723,7 +817,7 @@ func TestHandleMinerCommand_GetMiningPoolsTrimsUnsupportedPoolSlots(t *testing.T ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_GetMiningPools{GetMiningPools: &pb.GetMiningPoolsAction{}}}), discardLogger(t)) // Assert @@ -779,7 +873,7 @@ func TestHandleMinerCommand_GetMiningPoolsRejectsInvalidPluginResult(t *testing. ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_GetMiningPools{GetMiningPools: &pb.GetMiningPoolsAction{}}}), discardLogger(t)) // Assert @@ -804,7 +898,7 @@ func TestHandleMinerCommand_DeviceErrorClassifiesToUnimplemented(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_Reboot{Reboot: &pb.RebootAction{}}}), discardLogger(t)) // Assert @@ -860,7 +954,7 @@ func TestHandleMinerCommand_RejectsUndiallableTarget(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", mc, discardLogger(t)) + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", mc, discardLogger(t)) // Assert: rejected at the control boundary; the driver is never dialed. assert.Equal(t, pb.AckCode_ACK_CODE_BAD_REQUEST, ack.only(t).GetCode()) @@ -874,7 +968,7 @@ func TestHandleMinerCommand_UnknownDriverAcksAgentIncapable(t *testing.T) { ack := &captureAcker{} // Act - r.handleMinerCommand(context.Background(), ack, "cmd-1", + r.handleMinerCommand(context.Background(), nil, ack, "cmd-1", withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_Reboot{Reboot: &pb.RebootAction{}}}), discardLogger(t)) // Assert diff --git a/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go b/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go index 948289570..96ccaff57 100644 --- a/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go +++ b/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go @@ -13,10 +13,6 @@ package fleetnodegatewayv1 import ( - reflect "reflect" - sync "sync" - unsafe "unsafe" - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" v11 "github.com/block/proto-fleet/server/generated/grpc/common/v1" v1 "github.com/block/proto-fleet/server/generated/grpc/curtailment/v1" @@ -27,6 +23,9 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" ) const ( @@ -2353,6 +2352,7 @@ type MinerCommand struct { // *MinerCommand_GetMiningPools // *MinerCommand_GetErrors // *MinerCommand_UpdateMinerPassword + // *MinerCommand_DownloadLogs Action isMinerCommand_Action `protobuf_oneof:"action"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2510,6 +2510,15 @@ func (x *MinerCommand) GetUpdateMinerPassword() *UpdateMinerPasswordAction { return nil } +func (x *MinerCommand) GetDownloadLogs() *DownloadLogsAction { + if x != nil { + if x, ok := x.Action.(*MinerCommand_DownloadLogs); ok { + return x.DownloadLogs + } + } + return nil +} + type isMinerCommand_Action interface { isMinerCommand_Action() } @@ -2562,6 +2571,10 @@ type MinerCommand_UpdateMinerPassword struct { UpdateMinerPassword *UpdateMinerPasswordAction `protobuf:"bytes,13,opt,name=update_miner_password,json=updateMinerPassword,proto3,oneof"` } +type MinerCommand_DownloadLogs struct { + DownloadLogs *DownloadLogsAction `protobuf:"bytes,14,opt,name=download_logs,json=downloadLogs,proto3,oneof"` +} + func (*MinerCommand_Reboot) isMinerCommand_Action() {} func (*MinerCommand_StartMining) isMinerCommand_Action() {} @@ -2586,6 +2599,8 @@ func (*MinerCommand_GetErrors) isMinerCommand_Action() {} func (*MinerCommand_UpdateMinerPassword) isMinerCommand_Action() {} +func (*MinerCommand_DownloadLogs) isMinerCommand_Action() {} + type RebootAction struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -2838,6 +2853,50 @@ func (*GetErrorsAction) Descriptor() ([]byte, []int) { return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{40} } +type DownloadLogsAction struct { + state protoimpl.MessageState `protogen:"open.v1"` + BatchLogUuid string `protobuf:"bytes,1,opt,name=batch_log_uuid,json=batchLogUuid,proto3" json:"batch_log_uuid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DownloadLogsAction) Reset() { + *x = DownloadLogsAction{} + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DownloadLogsAction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DownloadLogsAction) ProtoMessage() {} + +func (x *DownloadLogsAction) ProtoReflect() protoreflect.Message { + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[41] + 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 DownloadLogsAction.ProtoReflect.Descriptor instead. +func (*DownloadLogsAction) Descriptor() ([]byte, []int) { + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{41} +} + +func (x *DownloadLogsAction) GetBatchLogUuid() string { + if x != nil { + return x.BatchLogUuid + } + return "" +} + type NodeEncryptedPayload struct { state protoimpl.MessageState `protogen:"open.v1"` Algorithm string `protobuf:"bytes,1,opt,name=algorithm,proto3" json:"algorithm,omitempty"` @@ -2850,7 +2909,7 @@ type NodeEncryptedPayload struct { func (x *NodeEncryptedPayload) Reset() { *x = NodeEncryptedPayload{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[41] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2862,7 +2921,7 @@ func (x *NodeEncryptedPayload) String() string { func (*NodeEncryptedPayload) ProtoMessage() {} func (x *NodeEncryptedPayload) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[41] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2875,7 +2934,7 @@ func (x *NodeEncryptedPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use NodeEncryptedPayload.ProtoReflect.Descriptor instead. func (*NodeEncryptedPayload) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{41} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{42} } func (x *NodeEncryptedPayload) GetAlgorithm() string { @@ -2915,7 +2974,7 @@ type UpdateMinerPasswordAction struct { func (x *UpdateMinerPasswordAction) Reset() { *x = UpdateMinerPasswordAction{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[42] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2927,7 +2986,7 @@ func (x *UpdateMinerPasswordAction) String() string { func (*UpdateMinerPasswordAction) ProtoMessage() {} func (x *UpdateMinerPasswordAction) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[42] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2940,7 +2999,7 @@ func (x *UpdateMinerPasswordAction) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateMinerPasswordAction.ProtoReflect.Descriptor instead. func (*UpdateMinerPasswordAction) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{42} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{43} } func (x *UpdateMinerPasswordAction) GetEncryptedPasswordUpdate() *NodeEncryptedPayload { @@ -2959,7 +3018,7 @@ type UpdateMinerPasswordResult struct { func (x *UpdateMinerPasswordResult) Reset() { *x = UpdateMinerPasswordResult{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[43] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2971,7 +3030,7 @@ func (x *UpdateMinerPasswordResult) String() string { func (*UpdateMinerPasswordResult) ProtoMessage() {} func (x *UpdateMinerPasswordResult) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[43] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2984,7 +3043,7 @@ func (x *UpdateMinerPasswordResult) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateMinerPasswordResult.ProtoReflect.Descriptor instead. func (*UpdateMinerPasswordResult) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{43} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{44} } func (x *UpdateMinerPasswordResult) GetEncryptedCredentials() *EncryptedCredentials { @@ -3003,7 +3062,7 @@ type CurtailAction struct { func (x *CurtailAction) Reset() { *x = CurtailAction{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[44] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3015,7 +3074,7 @@ func (x *CurtailAction) String() string { func (*CurtailAction) ProtoMessage() {} func (x *CurtailAction) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[44] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3028,7 +3087,7 @@ func (x *CurtailAction) ProtoReflect() protoreflect.Message { // Deprecated: Use CurtailAction.ProtoReflect.Descriptor instead. func (*CurtailAction) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{44} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{45} } func (x *CurtailAction) GetLevel() v1.CurtailmentLevel { @@ -3047,7 +3106,7 @@ type SetCoolingModeAction struct { func (x *SetCoolingModeAction) Reset() { *x = SetCoolingModeAction{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[45] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3059,7 +3118,7 @@ func (x *SetCoolingModeAction) String() string { func (*SetCoolingModeAction) ProtoMessage() {} func (x *SetCoolingModeAction) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[45] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3072,7 +3131,7 @@ func (x *SetCoolingModeAction) ProtoReflect() protoreflect.Message { // Deprecated: Use SetCoolingModeAction.ProtoReflect.Descriptor instead. func (*SetCoolingModeAction) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{45} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{46} } func (x *SetCoolingModeAction) GetMode() v11.CoolingMode { @@ -3091,7 +3150,7 @@ type SetPowerTargetAction struct { func (x *SetPowerTargetAction) Reset() { *x = SetPowerTargetAction{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[46] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3103,7 +3162,7 @@ func (x *SetPowerTargetAction) String() string { func (*SetPowerTargetAction) ProtoMessage() {} func (x *SetPowerTargetAction) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[46] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3116,7 +3175,7 @@ func (x *SetPowerTargetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use SetPowerTargetAction.ProtoReflect.Descriptor instead. func (*SetPowerTargetAction) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{46} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{47} } func (x *SetPowerTargetAction) GetPerformanceMode() v12.PerformanceMode { @@ -3137,7 +3196,7 @@ type MiningPoolConfig struct { func (x *MiningPoolConfig) Reset() { *x = MiningPoolConfig{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[47] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3149,7 +3208,7 @@ func (x *MiningPoolConfig) String() string { func (*MiningPoolConfig) ProtoMessage() {} func (x *MiningPoolConfig) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[47] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3162,7 +3221,7 @@ func (x *MiningPoolConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use MiningPoolConfig.ProtoReflect.Descriptor instead. func (*MiningPoolConfig) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{47} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{48} } func (x *MiningPoolConfig) GetPriority() int32 { @@ -3195,7 +3254,7 @@ type UpdateMiningPoolsAction struct { func (x *UpdateMiningPoolsAction) Reset() { *x = UpdateMiningPoolsAction{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[48] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3207,7 +3266,7 @@ func (x *UpdateMiningPoolsAction) String() string { func (*UpdateMiningPoolsAction) ProtoMessage() {} func (x *UpdateMiningPoolsAction) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[48] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3220,7 +3279,7 @@ func (x *UpdateMiningPoolsAction) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateMiningPoolsAction.ProtoReflect.Descriptor instead. func (*UpdateMiningPoolsAction) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{48} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{49} } func (x *UpdateMiningPoolsAction) GetPools() []*MiningPoolConfig { @@ -3239,7 +3298,7 @@ type GetMiningPoolsResult struct { func (x *GetMiningPoolsResult) Reset() { *x = GetMiningPoolsResult{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[49] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3251,7 +3310,7 @@ func (x *GetMiningPoolsResult) String() string { func (*GetMiningPoolsResult) ProtoMessage() {} func (x *GetMiningPoolsResult) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[49] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3264,7 +3323,7 @@ func (x *GetMiningPoolsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMiningPoolsResult.ProtoReflect.Descriptor instead. func (*GetMiningPoolsResult) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{49} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{50} } func (x *GetMiningPoolsResult) GetPools() []*MiningPoolConfig { @@ -3295,7 +3354,7 @@ type MinerErrorReport struct { func (x *MinerErrorReport) Reset() { *x = MinerErrorReport{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[50] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3307,7 +3366,7 @@ func (x *MinerErrorReport) String() string { func (*MinerErrorReport) ProtoMessage() {} func (x *MinerErrorReport) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[50] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3320,7 +3379,7 @@ func (x *MinerErrorReport) ProtoReflect() protoreflect.Message { // Deprecated: Use MinerErrorReport.ProtoReflect.Descriptor instead. func (*MinerErrorReport) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{50} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{51} } func (x *MinerErrorReport) GetMinerError() v13.MinerError { @@ -3426,7 +3485,7 @@ type GetErrorsResult struct { func (x *GetErrorsResult) Reset() { *x = GetErrorsResult{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[51] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3438,7 +3497,7 @@ func (x *GetErrorsResult) String() string { func (*GetErrorsResult) ProtoMessage() {} func (x *GetErrorsResult) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[51] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3451,7 +3510,7 @@ func (x *GetErrorsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetErrorsResult.ProtoReflect.Descriptor instead. func (*GetErrorsResult) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{51} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{52} } func (x *GetErrorsResult) GetDeviceId() string { @@ -3508,7 +3567,7 @@ type AgentCommand struct { func (x *AgentCommand) Reset() { *x = AgentCommand{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[52] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3520,7 +3579,7 @@ func (x *AgentCommand) String() string { func (*AgentCommand) ProtoMessage() {} func (x *AgentCommand) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[52] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3533,7 +3592,7 @@ func (x *AgentCommand) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentCommand.ProtoReflect.Descriptor instead. func (*AgentCommand) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{52} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{53} } func (x *AgentCommand) GetCommand() isAgentCommand_Command { @@ -3621,7 +3680,7 @@ type ControlAck struct { func (x *ControlAck) Reset() { *x = ControlAck{} - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[53] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3633,7 +3692,7 @@ func (x *ControlAck) String() string { func (*ControlAck) ProtoMessage() {} func (x *ControlAck) ProtoReflect() protoreflect.Message { - mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[53] + mi := &file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3646,7 +3705,7 @@ func (x *ControlAck) ProtoReflect() protoreflect.Message { // Deprecated: Use ControlAck.ProtoReflect.Descriptor instead. func (*ControlAck) Descriptor() ([]byte, []int) { - return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{53} + return file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP(), []int{54} } func (x *ControlAck) GetCommandId() string { @@ -4068,7 +4127,7 @@ var file_fleetnodegateway_v1_fleetnodegateway_proto_rawDesc = string([]byte{ 0x39, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x08, 0xba, 0x48, 0x05, 0x7a, 0x03, 0x18, 0x80, 0x20, 0x52, 0x12, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x61, 0x6c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x9f, 0x08, 0x0a, 0x0c, 0x4d, + 0x61, 0x6c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xef, 0x08, 0x0a, 0x0c, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x4e, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, @@ -4133,7 +4192,12 @@ var file_fleetnodegateway_v1_fleetnodegateway_proto_rawDesc = string([]byte{ 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x13, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, - 0x6e, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x42, 0x0f, 0x0a, 0x06, 0x61, + 0x6e, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x4e, 0x0a, 0x0d, 0x64, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x4c, 0x6f, 0x67, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0c, 0x64, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x0f, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x62, 0x6f, 0x6f, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x13, 0x0a, 0x11, 0x53, 0x74, 0x61, 0x72, 0x74, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, 0x69, 0x6f, @@ -4143,347 +4207,351 @@ var file_fleetnodegateway_v1_fleetnodegateway_proto_rawDesc = string([]byte{ 0x74, 0x61, 0x69, 0x6c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x16, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x11, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc9, 0x02, 0x0a, 0x14, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0xb1, - 0x01, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x92, 0x01, 0xba, 0x48, 0x8e, 0x01, 0xba, 0x01, 0x84, 0x01, 0x0a, 0x20, 0x6e, - 0x6f, 0x64, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, - 0x33, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x78, 0x32, 0x35, 0x35, 0x31, 0x39, 0x2d, 0x68, 0x6b, 0x64, 0x66, 0x2d, 0x73, - 0x68, 0x61, 0x32, 0x35, 0x36, 0x2d, 0x61, 0x65, 0x73, 0x2d, 0x32, 0x35, 0x36, 0x2d, 0x67, 0x63, - 0x6d, 0x2d, 0x76, 0x31, 0x1a, 0x2b, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x78, - 0x32, 0x35, 0x35, 0x31, 0x39, 0x2d, 0x68, 0x6b, 0x64, 0x66, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x35, - 0x36, 0x2d, 0x61, 0x65, 0x73, 0x2d, 0x32, 0x35, 0x36, 0x2d, 0x67, 0x63, 0x6d, 0x2d, 0x76, 0x31, - 0x27, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, - 0x68, 0x6d, 0x12, 0x32, 0x0a, 0x10, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, - 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, - 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x0f, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, - 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x0c, 0x52, 0x05, - 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, - 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x7a, 0x05, - 0x10, 0x11, 0x18, 0x80, 0x40, 0x52, 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, - 0x74, 0x22, 0x8a, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, - 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x6d, 0x0a, 0x19, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x06, 0xba, - 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x17, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x83, - 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x66, 0x0a, 0x15, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, - 0x31, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x14, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x73, 0x22, 0x47, 0x0a, 0x0d, 0x43, 0x75, 0x72, 0x74, 0x61, 0x69, 0x6c, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x63, 0x75, 0x72, 0x74, 0x61, 0x69, 0x6c, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x74, 0x61, 0x69, 0x6c, 0x6d, 0x65, 0x6e, - 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x42, 0x0a, - 0x14, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x41, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, - 0x65, 0x22, 0x63, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x54, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x10, 0x70, 0x65, 0x72, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, - 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, - 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x9e, 0x03, 0x0a, 0x10, 0x4d, 0x69, 0x6e, 0x69, 0x6e, - 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x08, 0x70, - 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xba, - 0x48, 0x06, 0x1a, 0x04, 0x18, 0x02, 0x28, 0x00, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, - 0x74, 0x79, 0x12, 0xbc, 0x02, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0xa9, 0x02, 0xba, 0x48, 0xa5, 0x02, 0x72, 0xa2, 0x02, 0x10, 0x0c, 0x18, 0x80, 0x02, 0x32, - 0x9a, 0x02, 0x5e, 0x28, 0x73, 0x74, 0x72, 0x61, 0x74, 0x75, 0x6d, 0x5c, 0x2b, 0x28, 0x74, 0x63, - 0x70, 0x7c, 0x73, 0x73, 0x6c, 0x7c, 0x77, 0x73, 0x29, 0x3a, 0x5c, 0x2f, 0x5c, 0x2f, 0x28, 0x28, - 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, - 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x2e, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, - 0x30, 0x2d, 0x39, 0x5d, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5d, 0x7b, 0x32, - 0x2c, 0x7d, 0x29, 0x7c, 0x28, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, 0x7d, 0x5c, 0x2e, 0x29, 0x7b, - 0x33, 0x7d, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, 0x7d, 0x7c, 0x5c, 0x5b, 0x28, 0x5b, 0x30, 0x2d, - 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x3a, 0x5d, 0x2b, 0x29, 0x5c, 0x5d, 0x29, 0x28, 0x3a, - 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x35, 0x7d, 0x29, 0x3f, 0x7c, 0x73, 0x74, 0x72, 0x61, 0x74, 0x75, - 0x6d, 0x32, 0x5c, 0x2b, 0x74, 0x63, 0x70, 0x3a, 0x5c, 0x2f, 0x5c, 0x2f, 0x28, 0x28, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, - 0x30, 0x2d, 0x39, 0x2e, 0x2d, 0x5d, 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, - 0x39, 0x5d, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5d, 0x7b, 0x32, 0x2c, 0x7d, - 0x29, 0x7c, 0x28, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, 0x7d, 0x5c, 0x2e, 0x29, 0x7b, 0x33, 0x7d, - 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, 0x7d, 0x7c, 0x5c, 0x5b, 0x28, 0x5b, 0x30, 0x2d, 0x39, 0x61, - 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x3a, 0x5d, 0x2b, 0x29, 0x5c, 0x5d, 0x29, 0x3a, 0x5c, 0x64, 0x7b, - 0x31, 0x2c, 0x35, 0x7d, 0x5c, 0x2f, 0x5b, 0x41, 0x2d, 0x5a, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, - 0x5f, 0x2b, 0x5c, 0x2f, 0x3d, 0x3a, 0x2e, 0x2d, 0x5d, 0x2b, 0x29, 0x24, 0x52, 0x03, 0x75, 0x72, - 0x6c, 0x12, 0x24, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x04, 0x52, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x62, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x73, 0x41, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x47, 0x0a, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, - 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x92, 0x01, 0x04, - 0x08, 0x01, 0x10, 0x03, 0x52, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x22, 0x5d, 0x0a, 0x14, 0x47, - 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x73, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, - 0x6f, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, - 0x02, 0x10, 0x03, 0x52, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x22, 0xf5, 0x06, 0x0a, 0x10, 0x4d, - 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, - 0x40, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x12, 0x2d, 0x0a, 0x0d, 0x63, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, - 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, - 0x80, 0x20, 0x52, 0x0c, 0x63, 0x61, 0x75, 0x73, 0x65, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x12, 0x37, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x11, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, - 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, - 0x72, 0x69, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, - 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, - 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, - 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, - 0x41, 0x74, 0x12, 0x37, 0x0a, 0x09, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x08, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x41, 0x74, 0x12, 0x82, 0x01, 0x0a, 0x11, - 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, - 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, - 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, - 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x42, 0x18, 0xba, 0x48, 0x15, 0x9a, 0x01, 0x12, 0x10, 0x20, 0x22, 0x07, - 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x2a, 0x05, 0x72, 0x03, 0x18, 0x80, 0x08, 0x52, 0x10, - 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x27, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, - 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, - 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0xff, 0x01, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x6f, 0x6d, - 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x06, 0x69, - 0x6d, 0x70, 0x61, 0x63, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, - 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x06, 0x69, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x12, 0x22, 0x0a, - 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, - 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0d, 0x63, - 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x1a, 0x43, 0x0a, 0x15, - 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x22, 0xd4, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x27, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, - 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, - 0x48, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x46, 0x0a, 0x12, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x4c, 0x6f, 0x67, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x0e, 0x62, + 0x61, 0x74, 0x63, 0x68, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, + 0x0c, 0x62, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x67, 0x55, 0x75, 0x69, 0x64, 0x22, 0xc9, 0x02, + 0x0a, 0x14, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0xb1, 0x01, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, + 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x92, 0x01, 0xba, 0x48, 0x8e, + 0x01, 0xba, 0x01, 0x84, 0x01, 0x0a, 0x20, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x61, 0x6c, + 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x33, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, + 0x68, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x78, 0x32, 0x35, 0x35, 0x31, + 0x39, 0x2d, 0x68, 0x6b, 0x64, 0x66, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x2d, 0x61, 0x65, + 0x73, 0x2d, 0x32, 0x35, 0x36, 0x2d, 0x67, 0x63, 0x6d, 0x2d, 0x76, 0x31, 0x1a, 0x2b, 0x74, 0x68, + 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x78, 0x32, 0x35, 0x35, 0x31, 0x39, 0x2d, 0x68, 0x6b, + 0x64, 0x66, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x2d, 0x61, 0x65, 0x73, 0x2d, 0x32, 0x35, + 0x36, 0x2d, 0x67, 0x63, 0x6d, 0x2d, 0x76, 0x31, 0x27, 0x72, 0x04, 0x10, 0x01, 0x18, 0x40, 0x52, + 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x32, 0x0a, 0x10, 0x65, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, 0x48, 0x04, 0x7a, 0x02, 0x68, 0x20, 0x52, 0x0f, 0x65, + 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x1d, + 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x07, 0xba, + 0x48, 0x04, 0x7a, 0x02, 0x68, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x2a, 0x0a, + 0x0a, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0c, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x7a, 0x05, 0x10, 0x11, 0x18, 0x80, 0x40, 0x52, 0x0a, 0x63, + 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x8a, 0x01, 0x0a, 0x19, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x6d, 0x0a, 0x19, 0x65, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x17, 0x65, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x83, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x12, 0x66, 0x0a, 0x15, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x06, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x14, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x47, 0x0a, 0x0d, + 0x43, 0x75, 0x72, 0x74, 0x61, 0x69, 0x6c, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, + 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x63, + 0x75, 0x72, 0x74, 0x61, 0x69, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, + 0x72, 0x74, 0x61, 0x69, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, + 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x42, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6f, 0x6c, + 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, + 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x4d, + 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x63, 0x0a, 0x14, 0x53, 0x65, 0x74, + 0x50, 0x6f, 0x77, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x4b, 0x0a, 0x10, 0x70, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6d, 0x69, + 0x6e, 0x65, 0x72, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, + 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0f, 0x70, + 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x9e, + 0x03, 0x0a, 0x10, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, 0xba, 0x48, 0x06, 0x1a, 0x04, 0x18, 0x02, 0x28, 0x00, + 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0xbc, 0x02, 0x0a, 0x03, 0x75, + 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0xa9, 0x02, 0xba, 0x48, 0xa5, 0x02, 0x72, + 0xa2, 0x02, 0x10, 0x0c, 0x18, 0x80, 0x02, 0x32, 0x9a, 0x02, 0x5e, 0x28, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x75, 0x6d, 0x5c, 0x2b, 0x28, 0x74, 0x63, 0x70, 0x7c, 0x73, 0x73, 0x6c, 0x7c, 0x77, 0x73, + 0x29, 0x3a, 0x5c, 0x2f, 0x5c, 0x2f, 0x28, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, + 0x2d, 0x39, 0x5d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x2e, 0x2d, 0x5d, + 0x2a, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x5c, 0x2e, 0x5b, 0x61, + 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5d, 0x7b, 0x32, 0x2c, 0x7d, 0x29, 0x7c, 0x28, 0x5c, 0x64, 0x7b, + 0x31, 0x2c, 0x33, 0x7d, 0x5c, 0x2e, 0x29, 0x7b, 0x33, 0x7d, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, + 0x7d, 0x7c, 0x5c, 0x5b, 0x28, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x3a, + 0x5d, 0x2b, 0x29, 0x5c, 0x5d, 0x29, 0x28, 0x3a, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x35, 0x7d, 0x29, + 0x3f, 0x7c, 0x73, 0x74, 0x72, 0x61, 0x74, 0x75, 0x6d, 0x32, 0x5c, 0x2b, 0x74, 0x63, 0x70, 0x3a, + 0x5c, 0x2f, 0x5c, 0x2f, 0x28, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, + 0x5d, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x2e, 0x2d, 0x5d, 0x2a, 0x5b, + 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x30, 0x2d, 0x39, 0x5d, 0x5c, 0x2e, 0x5b, 0x61, 0x2d, 0x7a, + 0x41, 0x2d, 0x5a, 0x5d, 0x7b, 0x32, 0x2c, 0x7d, 0x29, 0x7c, 0x28, 0x5c, 0x64, 0x7b, 0x31, 0x2c, + 0x33, 0x7d, 0x5c, 0x2e, 0x29, 0x7b, 0x33, 0x7d, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x33, 0x7d, 0x7c, + 0x5c, 0x5b, 0x28, 0x5b, 0x30, 0x2d, 0x39, 0x61, 0x2d, 0x66, 0x41, 0x2d, 0x46, 0x3a, 0x5d, 0x2b, + 0x29, 0x5c, 0x5d, 0x29, 0x3a, 0x5c, 0x64, 0x7b, 0x31, 0x2c, 0x35, 0x7d, 0x5c, 0x2f, 0x5b, 0x41, + 0x2d, 0x5a, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5f, 0x2b, 0x5c, 0x2f, 0x3d, 0x3a, 0x2e, 0x2d, + 0x5d, 0x2b, 0x29, 0x24, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x24, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x72, 0x03, 0x18, 0x80, 0x04, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, + 0x62, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, + 0x6f, 0x6f, 0x6c, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x47, 0x0a, 0x05, 0x70, 0x6f, + 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x42, 0x0a, 0xba, 0x48, 0x07, 0x92, 0x01, 0x04, 0x08, 0x01, 0x10, 0x03, 0x52, 0x05, 0x70, 0x6f, + 0x6f, 0x6c, 0x73, 0x22, 0x5d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x50, 0x6f, 0x6f, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x70, + 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x4d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x50, 0x6f, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x42, 0x08, 0xba, 0x48, 0x05, 0x92, 0x01, 0x02, 0x10, 0x03, 0x52, 0x05, 0x70, 0x6f, 0x6f, + 0x6c, 0x73, 0x22, 0xf5, 0x06, 0x0a, 0x10, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x40, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x65, 0x72, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x6d, + 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x0d, 0x63, 0x61, 0x75, + 0x73, 0x65, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x0c, 0x63, 0x61, 0x75, 0x73, + 0x65, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x6f, + 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x11, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x42, 0x08, 0xba, 0x48, 0x05, 0x82, 0x01, 0x02, + 0x10, 0x01, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0d, + 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x3c, 0x0a, 0x0c, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, + 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x41, 0x74, 0x12, 0x37, 0x0a, 0x09, 0x63, 0x6c, + 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x63, 0x6c, 0x6f, 0x73, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x82, 0x01, 0x0a, 0x11, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x3b, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x09, 0xba, 0x48, 0x06, 0x92, 0x01, 0x03, 0x10, 0x80, - 0x04, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x72, 0x75, - 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x72, - 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x6f, 0x6d, 0x69, 0x74, 0x74, - 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x6f, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xa6, 0x02, 0x0a, 0x0c, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x08, 0x64, 0x69, - 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, - 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, - 0x63, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x36, 0x0a, 0x04, 0x70, 0x61, 0x69, 0x72, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, - 0x2e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x61, 0x69, 0x72, 0x12, 0x48, 0x0a, - 0x0d, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x65, 0x72, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x47, 0x0a, 0x09, 0x74, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4e, - 0x6f, 0x64, 0x65, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x09, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x42, 0x10, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x05, 0xba, 0x48, 0x02, - 0x08, 0x01, 0x22, 0xdb, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x41, 0x63, - 0x6b, 0x12, 0x29, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, - 0x01, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x0d, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x0c, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x63, 0x6f, 0x64, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, - 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, - 0x6b, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x07, 0x70, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x09, 0xba, 0x48, - 0x06, 0x7a, 0x04, 0x18, 0x80, 0x80, 0x40, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x2a, 0x94, 0x01, 0x0a, 0x10, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, - 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x4e, 0x52, 0x4f, - 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, - 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x4e, 0x52, 0x4f, 0x4c, - 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4e, - 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x4e, 0x52, 0x4f, - 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, - 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x03, 0x2a, 0x9a, 0x01, 0x0a, 0x16, 0x43, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x50, 0x75, 0x72, 0x70, 0x6f, - 0x73, 0x65, 0x12, 0x28, 0x0a, 0x24, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x41, 0x52, - 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, 0x5f, 0x50, 0x55, 0x52, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x27, 0x0a, 0x23, - 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x41, 0x52, 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, - 0x5f, 0x50, 0x55, 0x52, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4c, - 0x4f, 0x47, 0x53, 0x10, 0x01, 0x12, 0x2d, 0x0a, 0x29, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, - 0x5f, 0x41, 0x52, 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, 0x5f, 0x50, 0x55, 0x52, 0x50, 0x4f, 0x53, - 0x45, 0x5f, 0x46, 0x49, 0x52, 0x4d, 0x57, 0x41, 0x52, 0x45, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, - 0x41, 0x44, 0x10, 0x02, 0x2a, 0x98, 0x01, 0x0a, 0x0b, 0x50, 0x61, 0x69, 0x72, 0x4f, 0x75, 0x74, - 0x63, 0x6f, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, - 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, - 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, - 0x4d, 0x45, 0x5f, 0x50, 0x41, 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x50, - 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x48, - 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x49, - 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x41, 0x49, 0x52, 0x5f, - 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, - 0xb4, 0x02, 0x0a, 0x07, 0x41, 0x63, 0x6b, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x41, - 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, - 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, - 0x45, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, - 0x44, 0x45, 0x5f, 0x50, 0x41, 0x52, 0x54, 0x49, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, - 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x42, 0x41, 0x44, 0x5f, 0x52, 0x45, 0x51, - 0x55, 0x45, 0x53, 0x54, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, - 0x44, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x43, 0x41, 0x50, 0x41, 0x42, - 0x4c, 0x45, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, - 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x1a, - 0x0a, 0x16, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x50, 0x4f, 0x52, - 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x43, - 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, - 0x07, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x42, 0x55, - 0x53, 0x59, 0x10, 0x08, 0x12, 0x1c, 0x0a, 0x18, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, - 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x09, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x55, - 0x4e, 0x49, 0x4d, 0x50, 0x4c, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x16, - 0x0a, 0x12, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x42, 0x49, - 0x44, 0x44, 0x45, 0x4e, 0x10, 0x0b, 0x32, 0xa7, 0x0a, 0x0a, 0x17, 0x46, 0x6c, 0x65, 0x65, 0x74, - 0x4e, 0x6f, 0x64, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x24, - 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x12, 0x42, - 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, - 0x65, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, - 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, - 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x31, 0x2e, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, - 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, - 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x0f, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x74, 0x72, 0x79, 0x12, 0x2b, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, - 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x28, 0x01, 0x12, 0x65, 0x0a, 0x0c, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x28, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x66, + 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x18, 0xba, 0x48, + 0x15, 0x9a, 0x01, 0x12, 0x10, 0x20, 0x22, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x2a, + 0x05, 0x72, 0x03, 0x18, 0x80, 0x08, 0x52, 0x10, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, + 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x30, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0xff, + 0x01, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x06, 0x69, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, 0x52, 0x06, 0x69, + 0x6d, 0x70, 0x61, 0x63, 0x74, 0x12, 0x22, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0x18, 0x80, 0x20, + 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x18, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xba, 0x48, 0x05, + 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x1a, 0x43, 0x0a, 0x15, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x22, 0xd4, 0x01, 0x0a, 0x0f, 0x47, + 0x65, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x27, + 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0xff, 0x01, 0x52, 0x08, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x48, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, + 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, + 0x6e, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x09, + 0xba, 0x48, 0x06, 0x92, 0x01, 0x03, 0x10, 0x80, 0x04, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, + 0x30, 0x0a, 0x14, 0x6f, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x6f, + 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x22, 0xa6, 0x02, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x61, 0x69, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x48, 0x00, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x36, 0x0a, + 0x04, 0x70, 0x61, 0x69, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x61, + 0x69, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4e, 0x6f, + 0x64, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, + 0x04, 0x70, 0x61, 0x69, 0x72, 0x12, 0x48, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x6c, 0x0a, 0x0f, 0x55, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x2b, 0x2e, 0x66, + 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, + 0x00, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x47, 0x0a, 0x09, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x76, + 0x31, 0x2e, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x65, 0x6c, 0x65, 0x6d, + 0x65, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x09, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x42, 0x10, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x05, 0xba, 0x48, 0x02, 0x08, 0x01, 0x22, 0xdb, 0x01, 0x0a, 0x0a, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x41, 0x63, 0x6b, 0x12, 0x29, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, + 0x48, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x01, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, + 0x65, 0x64, 0x12, 0x2d, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, + 0x18, 0x80, 0x20, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x30, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x1c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x6b, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, + 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0c, 0x42, 0x09, 0xba, 0x48, 0x06, 0x7a, 0x04, 0x18, 0x80, 0x80, 0x40, 0x52, + 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2a, 0x94, 0x01, 0x0a, 0x10, 0x45, 0x6e, 0x72, + 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, + 0x1d, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, + 0x1f, 0x0a, 0x1b, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x52, 0x4d, 0x45, 0x44, 0x10, 0x02, + 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x4e, 0x52, 0x4f, 0x4c, 0x4c, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x03, 0x2a, + 0x9a, 0x01, 0x0a, 0x16, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x50, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x24, 0x43, 0x4f, + 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x41, 0x52, 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, 0x5f, 0x50, + 0x55, 0x52, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x27, 0x0a, 0x23, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, + 0x41, 0x52, 0x54, 0x49, 0x46, 0x41, 0x43, 0x54, 0x5f, 0x50, 0x55, 0x52, 0x50, 0x4f, 0x53, 0x45, + 0x5f, 0x4d, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x01, 0x12, 0x2d, 0x0a, + 0x29, 0x43, 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0x44, 0x5f, 0x41, 0x52, 0x54, 0x49, 0x46, 0x41, 0x43, + 0x54, 0x5f, 0x50, 0x55, 0x52, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x46, 0x49, 0x52, 0x4d, 0x57, 0x41, + 0x52, 0x45, 0x5f, 0x50, 0x41, 0x59, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x02, 0x2a, 0x98, 0x01, 0x0a, + 0x0b, 0x50, 0x61, 0x69, 0x72, 0x4f, 0x75, 0x74, 0x63, 0x6f, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x18, + 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, + 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, 0x50, 0x41, 0x49, 0x52, 0x45, + 0x44, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, + 0x4f, 0x4d, 0x45, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4e, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, + 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, + 0x45, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, + 0x16, 0x0a, 0x12, 0x50, 0x41, 0x49, 0x52, 0x5f, 0x4f, 0x55, 0x54, 0x43, 0x4f, 0x4d, 0x45, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0xb4, 0x02, 0x0a, 0x07, 0x41, 0x63, 0x6b, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, + 0x0b, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4f, 0x4b, 0x10, 0x01, 0x12, 0x14, + 0x0a, 0x10, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x41, 0x52, 0x54, 0x49, + 0x41, 0x4c, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, + 0x5f, 0x42, 0x41, 0x44, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x03, 0x12, 0x1c, + 0x0a, 0x18, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x41, 0x47, 0x45, 0x4e, 0x54, + 0x5f, 0x49, 0x4e, 0x43, 0x41, 0x50, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, + 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x53, 0x43, 0x41, 0x4e, 0x5f, 0x46, 0x41, + 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, + 0x44, 0x45, 0x5f, 0x52, 0x45, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, + 0x10, 0x06, 0x12, 0x15, 0x0a, 0x11, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, + 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x07, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x43, 0x4b, + 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x42, 0x55, 0x53, 0x59, 0x10, 0x08, 0x12, 0x1c, 0x0a, 0x18, + 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x41, 0x55, 0x54, 0x48, 0x45, + 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x09, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x43, + 0x4b, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x4d, 0x50, 0x4c, 0x45, 0x4d, 0x45, + 0x4e, 0x54, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x16, 0x0a, 0x12, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, + 0x44, 0x45, 0x5f, 0x46, 0x4f, 0x52, 0x42, 0x49, 0x44, 0x44, 0x45, 0x4e, 0x10, 0x0b, 0x32, 0xa7, + 0x0a, 0x0a, 0x17, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x57, 0x0a, 0x08, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, + 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, - 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x12, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, 0x68, + 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x2e, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x12, 0x31, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, + 0x42, 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, + 0x6b, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x42, 0x65, 0x67, 0x69, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, + 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x15, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, + 0x61, 0x6b, 0x65, 0x12, 0x31, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, + 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, + 0x6b, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x0f, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x12, 0x2b, 0x2e, + 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, + 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x65, 0x0a, 0x0c, 0x55, 0x70, + 0x6c, 0x6f, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x28, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, - 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x86, 0x01, 0x0a, 0x17, 0x44, - 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x33, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, - 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x77, - 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x30, 0x01, 0x12, 0x84, 0x01, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x69, - 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, - 0x33, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, - 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x13, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x50, 0x61, 0x69, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x12, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x61, - 0x69, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, - 0x61, 0x69, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x29, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2a, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, - 0x42, 0xf8, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, - 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x42, 0x15, 0x46, 0x6c, - 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x59, 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, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, - 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6c, - 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x76, 0x31, - 0xa2, 0x02, 0x03, 0x46, 0x58, 0x58, 0xaa, 0x02, 0x13, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, - 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x13, 0x46, - 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5c, - 0x56, 0x31, 0xe2, 0x02, 0x1f, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x77, 0x61, 0x79, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x14, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, + 0x01, 0x12, 0x6c, 0x0a, 0x0f, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, + 0x62, 0x65, 0x61, 0x74, 0x12, 0x2b, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2c, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x48, 0x65, + 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x80, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x31, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, + 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x66, + 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x28, 0x01, 0x12, 0x86, 0x01, 0x0a, 0x17, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x33, + 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, + 0x61, 0x64, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x84, 0x01, 0x0a, 0x17, + 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x33, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, + 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x66, + 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, + 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x78, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x61, 0x69, 0x72, + 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x2f, 0x2e, 0x66, 0x6c, 0x65, 0x65, + 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x61, 0x69, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x66, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x61, 0x69, 0x72, 0x65, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x0d, + 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x29, 0x2e, + 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, + 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0xf8, 0x01, 0x0a, 0x17, 0x63, 0x6f, 0x6d, + 0x2e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x2e, 0x76, 0x31, 0x42, 0x15, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x59, 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, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x46, 0x58, 0x58, 0xaa, 0x02, + 0x13, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x13, 0x46, 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1f, 0x46, 0x6c, 0x65, + 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5c, 0x56, 0x31, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x14, 0x46, + 0x6c, 0x65, 0x65, 0x74, 0x6e, 0x6f, 0x64, 0x65, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -4499,7 +4567,7 @@ func file_fleetnodegateway_v1_fleetnodegateway_proto_rawDescGZIP() []byte { } var file_fleetnodegateway_v1_fleetnodegateway_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes = make([]protoimpl.MessageInfo, 55) +var file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes = make([]protoimpl.MessageInfo, 56) var file_fleetnodegateway_v1_fleetnodegateway_proto_goTypes = []any{ (EnrollmentStatus)(0), // 0: fleetnodegateway.v1.EnrollmentStatus (CommandArtifactPurpose)(0), // 1: fleetnodegateway.v1.CommandArtifactPurpose @@ -4546,39 +4614,40 @@ var file_fleetnodegateway_v1_fleetnodegateway_proto_goTypes = []any{ (*UncurtailAction)(nil), // 42: fleetnodegateway.v1.UncurtailAction (*GetMiningPoolsAction)(nil), // 43: fleetnodegateway.v1.GetMiningPoolsAction (*GetErrorsAction)(nil), // 44: fleetnodegateway.v1.GetErrorsAction - (*NodeEncryptedPayload)(nil), // 45: fleetnodegateway.v1.NodeEncryptedPayload - (*UpdateMinerPasswordAction)(nil), // 46: fleetnodegateway.v1.UpdateMinerPasswordAction - (*UpdateMinerPasswordResult)(nil), // 47: fleetnodegateway.v1.UpdateMinerPasswordResult - (*CurtailAction)(nil), // 48: fleetnodegateway.v1.CurtailAction - (*SetCoolingModeAction)(nil), // 49: fleetnodegateway.v1.SetCoolingModeAction - (*SetPowerTargetAction)(nil), // 50: fleetnodegateway.v1.SetPowerTargetAction - (*MiningPoolConfig)(nil), // 51: fleetnodegateway.v1.MiningPoolConfig - (*UpdateMiningPoolsAction)(nil), // 52: fleetnodegateway.v1.UpdateMiningPoolsAction - (*GetMiningPoolsResult)(nil), // 53: fleetnodegateway.v1.GetMiningPoolsResult - (*MinerErrorReport)(nil), // 54: fleetnodegateway.v1.MinerErrorReport - (*GetErrorsResult)(nil), // 55: fleetnodegateway.v1.GetErrorsResult - (*AgentCommand)(nil), // 56: fleetnodegateway.v1.AgentCommand - (*ControlAck)(nil), // 57: fleetnodegateway.v1.ControlAck - nil, // 58: fleetnodegateway.v1.MinerErrorReport.VendorAttributesEntry - (*timestamppb.Timestamp)(nil), // 59: google.protobuf.Timestamp - (v1.CurtailmentLevel)(0), // 60: curtailment.v1.CurtailmentLevel - (v11.CoolingMode)(0), // 61: common.v1.CoolingMode - (v12.PerformanceMode)(0), // 62: minercommand.v1.PerformanceMode - (v13.MinerError)(0), // 63: errors.v1.MinerError - (v13.Severity)(0), // 64: errors.v1.Severity - (v13.ComponentType)(0), // 65: errors.v1.ComponentType - (*v14.DiscoverRequest)(nil), // 66: pairing.v1.DiscoverRequest - (*v14.FleetNodePairRequest)(nil), // 67: pairing.v1.FleetNodePairRequest - (*v15.FleetNodeTelemetryRequest)(nil), // 68: telemetry.v1.FleetNodeTelemetryRequest + (*DownloadLogsAction)(nil), // 45: fleetnodegateway.v1.DownloadLogsAction + (*NodeEncryptedPayload)(nil), // 46: fleetnodegateway.v1.NodeEncryptedPayload + (*UpdateMinerPasswordAction)(nil), // 47: fleetnodegateway.v1.UpdateMinerPasswordAction + (*UpdateMinerPasswordResult)(nil), // 48: fleetnodegateway.v1.UpdateMinerPasswordResult + (*CurtailAction)(nil), // 49: fleetnodegateway.v1.CurtailAction + (*SetCoolingModeAction)(nil), // 50: fleetnodegateway.v1.SetCoolingModeAction + (*SetPowerTargetAction)(nil), // 51: fleetnodegateway.v1.SetPowerTargetAction + (*MiningPoolConfig)(nil), // 52: fleetnodegateway.v1.MiningPoolConfig + (*UpdateMiningPoolsAction)(nil), // 53: fleetnodegateway.v1.UpdateMiningPoolsAction + (*GetMiningPoolsResult)(nil), // 54: fleetnodegateway.v1.GetMiningPoolsResult + (*MinerErrorReport)(nil), // 55: fleetnodegateway.v1.MinerErrorReport + (*GetErrorsResult)(nil), // 56: fleetnodegateway.v1.GetErrorsResult + (*AgentCommand)(nil), // 57: fleetnodegateway.v1.AgentCommand + (*ControlAck)(nil), // 58: fleetnodegateway.v1.ControlAck + nil, // 59: fleetnodegateway.v1.MinerErrorReport.VendorAttributesEntry + (*timestamppb.Timestamp)(nil), // 60: google.protobuf.Timestamp + (v1.CurtailmentLevel)(0), // 61: curtailment.v1.CurtailmentLevel + (v11.CoolingMode)(0), // 62: common.v1.CoolingMode + (v12.PerformanceMode)(0), // 63: minercommand.v1.PerformanceMode + (v13.MinerError)(0), // 64: errors.v1.MinerError + (v13.Severity)(0), // 65: errors.v1.Severity + (v13.ComponentType)(0), // 66: errors.v1.ComponentType + (*v14.DiscoverRequest)(nil), // 67: pairing.v1.DiscoverRequest + (*v14.FleetNodePairRequest)(nil), // 68: pairing.v1.FleetNodePairRequest + (*v15.FleetNodeTelemetryRequest)(nil), // 69: telemetry.v1.FleetNodeTelemetryRequest } var file_fleetnodegateway_v1_fleetnodegateway_proto_depIdxs = []int32{ 0, // 0: fleetnodegateway.v1.RegisterResponse.enrollment_status:type_name -> fleetnodegateway.v1.EnrollmentStatus - 59, // 1: fleetnodegateway.v1.BeginAuthHandshakeResponse.expires_at:type_name -> google.protobuf.Timestamp - 59, // 2: fleetnodegateway.v1.CompleteAuthHandshakeResponse.expires_at:type_name -> google.protobuf.Timestamp - 59, // 3: fleetnodegateway.v1.UploadTelemetryRequest.captured_at:type_name -> google.protobuf.Timestamp - 59, // 4: fleetnodegateway.v1.UploadEventsRequest.captured_at:type_name -> google.protobuf.Timestamp - 59, // 5: fleetnodegateway.v1.UploadHeartbeatRequest.sent_at:type_name -> google.protobuf.Timestamp - 59, // 6: fleetnodegateway.v1.UploadHeartbeatResponse.received_at:type_name -> google.protobuf.Timestamp + 60, // 1: fleetnodegateway.v1.BeginAuthHandshakeResponse.expires_at:type_name -> google.protobuf.Timestamp + 60, // 2: fleetnodegateway.v1.CompleteAuthHandshakeResponse.expires_at:type_name -> google.protobuf.Timestamp + 60, // 3: fleetnodegateway.v1.UploadTelemetryRequest.captured_at:type_name -> google.protobuf.Timestamp + 60, // 4: fleetnodegateway.v1.UploadEventsRequest.captured_at:type_name -> google.protobuf.Timestamp + 60, // 5: fleetnodegateway.v1.UploadHeartbeatRequest.sent_at:type_name -> google.protobuf.Timestamp + 60, // 6: fleetnodegateway.v1.UploadHeartbeatResponse.received_at:type_name -> google.protobuf.Timestamp 1, // 7: fleetnodegateway.v1.CommandArtifactRef.purpose:type_name -> fleetnodegateway.v1.CommandArtifactPurpose 1, // 8: fleetnodegateway.v1.CommandArtifactUploadHeader.purpose:type_name -> fleetnodegateway.v1.CommandArtifactPurpose 17, // 9: fleetnodegateway.v1.UploadCommandArtifactRequest.header:type_name -> fleetnodegateway.v1.CommandArtifactUploadHeader @@ -4593,70 +4662,71 @@ var file_fleetnodegateway_v1_fleetnodegateway_proto_depIdxs = []int32{ 28, // 18: fleetnodegateway.v1.FleetNodePairResult.encrypted_credentials:type_name -> fleetnodegateway.v1.EncryptedCredentials 27, // 19: fleetnodegateway.v1.ReportPairedDevicesRequest.results:type_name -> fleetnodegateway.v1.FleetNodePairResult 33, // 20: fleetnodegateway.v1.ControlStreamRequest.hello:type_name -> fleetnodegateway.v1.ControlHello - 57, // 21: fleetnodegateway.v1.ControlStreamRequest.ack:type_name -> fleetnodegateway.v1.ControlAck + 58, // 21: fleetnodegateway.v1.ControlStreamRequest.ack:type_name -> fleetnodegateway.v1.ControlAck 34, // 22: fleetnodegateway.v1.ControlStreamResponse.accepted:type_name -> fleetnodegateway.v1.ControlAccepted 35, // 23: fleetnodegateway.v1.ControlStreamResponse.command:type_name -> fleetnodegateway.v1.ControlCommand - 59, // 24: fleetnodegateway.v1.ControlAccepted.server_time:type_name -> google.protobuf.Timestamp + 60, // 24: fleetnodegateway.v1.ControlAccepted.server_time:type_name -> google.protobuf.Timestamp 36, // 25: fleetnodegateway.v1.MinerCommand.target:type_name -> fleetnodegateway.v1.MinerConnectionDescriptor 38, // 26: fleetnodegateway.v1.MinerCommand.reboot:type_name -> fleetnodegateway.v1.RebootAction 39, // 27: fleetnodegateway.v1.MinerCommand.start_mining:type_name -> fleetnodegateway.v1.StartMiningAction 40, // 28: fleetnodegateway.v1.MinerCommand.stop_mining:type_name -> fleetnodegateway.v1.StopMiningAction 41, // 29: fleetnodegateway.v1.MinerCommand.blink_led:type_name -> fleetnodegateway.v1.BlinkLedAction - 48, // 30: fleetnodegateway.v1.MinerCommand.curtail:type_name -> fleetnodegateway.v1.CurtailAction + 49, // 30: fleetnodegateway.v1.MinerCommand.curtail:type_name -> fleetnodegateway.v1.CurtailAction 42, // 31: fleetnodegateway.v1.MinerCommand.uncurtail:type_name -> fleetnodegateway.v1.UncurtailAction - 49, // 32: fleetnodegateway.v1.MinerCommand.set_cooling_mode:type_name -> fleetnodegateway.v1.SetCoolingModeAction - 50, // 33: fleetnodegateway.v1.MinerCommand.set_power_target:type_name -> fleetnodegateway.v1.SetPowerTargetAction - 52, // 34: fleetnodegateway.v1.MinerCommand.update_mining_pools:type_name -> fleetnodegateway.v1.UpdateMiningPoolsAction + 50, // 32: fleetnodegateway.v1.MinerCommand.set_cooling_mode:type_name -> fleetnodegateway.v1.SetCoolingModeAction + 51, // 33: fleetnodegateway.v1.MinerCommand.set_power_target:type_name -> fleetnodegateway.v1.SetPowerTargetAction + 53, // 34: fleetnodegateway.v1.MinerCommand.update_mining_pools:type_name -> fleetnodegateway.v1.UpdateMiningPoolsAction 43, // 35: fleetnodegateway.v1.MinerCommand.get_mining_pools:type_name -> fleetnodegateway.v1.GetMiningPoolsAction 44, // 36: fleetnodegateway.v1.MinerCommand.get_errors:type_name -> fleetnodegateway.v1.GetErrorsAction - 46, // 37: fleetnodegateway.v1.MinerCommand.update_miner_password:type_name -> fleetnodegateway.v1.UpdateMinerPasswordAction - 45, // 38: fleetnodegateway.v1.UpdateMinerPasswordAction.encrypted_password_update:type_name -> fleetnodegateway.v1.NodeEncryptedPayload - 28, // 39: fleetnodegateway.v1.UpdateMinerPasswordResult.encrypted_credentials:type_name -> fleetnodegateway.v1.EncryptedCredentials - 60, // 40: fleetnodegateway.v1.CurtailAction.level:type_name -> curtailment.v1.CurtailmentLevel - 61, // 41: fleetnodegateway.v1.SetCoolingModeAction.mode:type_name -> common.v1.CoolingMode - 62, // 42: fleetnodegateway.v1.SetPowerTargetAction.performance_mode:type_name -> minercommand.v1.PerformanceMode - 51, // 43: fleetnodegateway.v1.UpdateMiningPoolsAction.pools:type_name -> fleetnodegateway.v1.MiningPoolConfig - 51, // 44: fleetnodegateway.v1.GetMiningPoolsResult.pools:type_name -> fleetnodegateway.v1.MiningPoolConfig - 63, // 45: fleetnodegateway.v1.MinerErrorReport.miner_error:type_name -> errors.v1.MinerError - 64, // 46: fleetnodegateway.v1.MinerErrorReport.severity:type_name -> errors.v1.Severity - 59, // 47: fleetnodegateway.v1.MinerErrorReport.first_seen_at:type_name -> google.protobuf.Timestamp - 59, // 48: fleetnodegateway.v1.MinerErrorReport.last_seen_at:type_name -> google.protobuf.Timestamp - 59, // 49: fleetnodegateway.v1.MinerErrorReport.closed_at:type_name -> google.protobuf.Timestamp - 58, // 50: fleetnodegateway.v1.MinerErrorReport.vendor_attributes:type_name -> fleetnodegateway.v1.MinerErrorReport.VendorAttributesEntry - 65, // 51: fleetnodegateway.v1.MinerErrorReport.component_type:type_name -> errors.v1.ComponentType - 54, // 52: fleetnodegateway.v1.GetErrorsResult.errors:type_name -> fleetnodegateway.v1.MinerErrorReport - 66, // 53: fleetnodegateway.v1.AgentCommand.discover:type_name -> pairing.v1.DiscoverRequest - 67, // 54: fleetnodegateway.v1.AgentCommand.pair:type_name -> pairing.v1.FleetNodePairRequest - 37, // 55: fleetnodegateway.v1.AgentCommand.miner_command:type_name -> fleetnodegateway.v1.MinerCommand - 68, // 56: fleetnodegateway.v1.AgentCommand.telemetry:type_name -> telemetry.v1.FleetNodeTelemetryRequest - 3, // 57: fleetnodegateway.v1.ControlAck.code:type_name -> fleetnodegateway.v1.AckCode - 4, // 58: fleetnodegateway.v1.FleetNodeGatewayService.Register:input_type -> fleetnodegateway.v1.RegisterRequest - 6, // 59: fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake:input_type -> fleetnodegateway.v1.BeginAuthHandshakeRequest - 8, // 60: fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake:input_type -> fleetnodegateway.v1.CompleteAuthHandshakeRequest - 10, // 61: fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry:input_type -> fleetnodegateway.v1.UploadTelemetryRequest - 12, // 62: fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents:input_type -> fleetnodegateway.v1.UploadEventsRequest - 14, // 63: fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat:input_type -> fleetnodegateway.v1.UploadHeartbeatRequest - 19, // 64: fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact:input_type -> fleetnodegateway.v1.UploadCommandArtifactRequest - 21, // 65: fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact:input_type -> fleetnodegateway.v1.DownloadCommandArtifactRequest - 24, // 66: fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices:input_type -> fleetnodegateway.v1.ReportDiscoveredDevicesRequest - 29, // 67: fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices:input_type -> fleetnodegateway.v1.ReportPairedDevicesRequest - 31, // 68: fleetnodegateway.v1.FleetNodeGatewayService.ControlStream:input_type -> fleetnodegateway.v1.ControlStreamRequest - 5, // 69: fleetnodegateway.v1.FleetNodeGatewayService.Register:output_type -> fleetnodegateway.v1.RegisterResponse - 7, // 70: fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake:output_type -> fleetnodegateway.v1.BeginAuthHandshakeResponse - 9, // 71: fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake:output_type -> fleetnodegateway.v1.CompleteAuthHandshakeResponse - 11, // 72: fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry:output_type -> fleetnodegateway.v1.UploadTelemetryResponse - 13, // 73: fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents:output_type -> fleetnodegateway.v1.UploadEventsResponse - 15, // 74: fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat:output_type -> fleetnodegateway.v1.UploadHeartbeatResponse - 20, // 75: fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact:output_type -> fleetnodegateway.v1.UploadCommandArtifactResponse - 23, // 76: fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact:output_type -> fleetnodegateway.v1.DownloadCommandArtifactResponse - 26, // 77: fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices:output_type -> fleetnodegateway.v1.ReportDiscoveredDevicesResponse - 30, // 78: fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices:output_type -> fleetnodegateway.v1.ReportPairedDevicesResponse - 32, // 79: fleetnodegateway.v1.FleetNodeGatewayService.ControlStream:output_type -> fleetnodegateway.v1.ControlStreamResponse - 69, // [69:80] is the sub-list for method output_type - 58, // [58:69] is the sub-list for method input_type - 58, // [58:58] is the sub-list for extension type_name - 58, // [58:58] is the sub-list for extension extendee - 0, // [0:58] is the sub-list for field type_name + 47, // 37: fleetnodegateway.v1.MinerCommand.update_miner_password:type_name -> fleetnodegateway.v1.UpdateMinerPasswordAction + 45, // 38: fleetnodegateway.v1.MinerCommand.download_logs:type_name -> fleetnodegateway.v1.DownloadLogsAction + 46, // 39: fleetnodegateway.v1.UpdateMinerPasswordAction.encrypted_password_update:type_name -> fleetnodegateway.v1.NodeEncryptedPayload + 28, // 40: fleetnodegateway.v1.UpdateMinerPasswordResult.encrypted_credentials:type_name -> fleetnodegateway.v1.EncryptedCredentials + 61, // 41: fleetnodegateway.v1.CurtailAction.level:type_name -> curtailment.v1.CurtailmentLevel + 62, // 42: fleetnodegateway.v1.SetCoolingModeAction.mode:type_name -> common.v1.CoolingMode + 63, // 43: fleetnodegateway.v1.SetPowerTargetAction.performance_mode:type_name -> minercommand.v1.PerformanceMode + 52, // 44: fleetnodegateway.v1.UpdateMiningPoolsAction.pools:type_name -> fleetnodegateway.v1.MiningPoolConfig + 52, // 45: fleetnodegateway.v1.GetMiningPoolsResult.pools:type_name -> fleetnodegateway.v1.MiningPoolConfig + 64, // 46: fleetnodegateway.v1.MinerErrorReport.miner_error:type_name -> errors.v1.MinerError + 65, // 47: fleetnodegateway.v1.MinerErrorReport.severity:type_name -> errors.v1.Severity + 60, // 48: fleetnodegateway.v1.MinerErrorReport.first_seen_at:type_name -> google.protobuf.Timestamp + 60, // 49: fleetnodegateway.v1.MinerErrorReport.last_seen_at:type_name -> google.protobuf.Timestamp + 60, // 50: fleetnodegateway.v1.MinerErrorReport.closed_at:type_name -> google.protobuf.Timestamp + 59, // 51: fleetnodegateway.v1.MinerErrorReport.vendor_attributes:type_name -> fleetnodegateway.v1.MinerErrorReport.VendorAttributesEntry + 66, // 52: fleetnodegateway.v1.MinerErrorReport.component_type:type_name -> errors.v1.ComponentType + 55, // 53: fleetnodegateway.v1.GetErrorsResult.errors:type_name -> fleetnodegateway.v1.MinerErrorReport + 67, // 54: fleetnodegateway.v1.AgentCommand.discover:type_name -> pairing.v1.DiscoverRequest + 68, // 55: fleetnodegateway.v1.AgentCommand.pair:type_name -> pairing.v1.FleetNodePairRequest + 37, // 56: fleetnodegateway.v1.AgentCommand.miner_command:type_name -> fleetnodegateway.v1.MinerCommand + 69, // 57: fleetnodegateway.v1.AgentCommand.telemetry:type_name -> telemetry.v1.FleetNodeTelemetryRequest + 3, // 58: fleetnodegateway.v1.ControlAck.code:type_name -> fleetnodegateway.v1.AckCode + 4, // 59: fleetnodegateway.v1.FleetNodeGatewayService.Register:input_type -> fleetnodegateway.v1.RegisterRequest + 6, // 60: fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake:input_type -> fleetnodegateway.v1.BeginAuthHandshakeRequest + 8, // 61: fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake:input_type -> fleetnodegateway.v1.CompleteAuthHandshakeRequest + 10, // 62: fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry:input_type -> fleetnodegateway.v1.UploadTelemetryRequest + 12, // 63: fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents:input_type -> fleetnodegateway.v1.UploadEventsRequest + 14, // 64: fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat:input_type -> fleetnodegateway.v1.UploadHeartbeatRequest + 19, // 65: fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact:input_type -> fleetnodegateway.v1.UploadCommandArtifactRequest + 21, // 66: fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact:input_type -> fleetnodegateway.v1.DownloadCommandArtifactRequest + 24, // 67: fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices:input_type -> fleetnodegateway.v1.ReportDiscoveredDevicesRequest + 29, // 68: fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices:input_type -> fleetnodegateway.v1.ReportPairedDevicesRequest + 31, // 69: fleetnodegateway.v1.FleetNodeGatewayService.ControlStream:input_type -> fleetnodegateway.v1.ControlStreamRequest + 5, // 70: fleetnodegateway.v1.FleetNodeGatewayService.Register:output_type -> fleetnodegateway.v1.RegisterResponse + 7, // 71: fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake:output_type -> fleetnodegateway.v1.BeginAuthHandshakeResponse + 9, // 72: fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake:output_type -> fleetnodegateway.v1.CompleteAuthHandshakeResponse + 11, // 73: fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry:output_type -> fleetnodegateway.v1.UploadTelemetryResponse + 13, // 74: fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents:output_type -> fleetnodegateway.v1.UploadEventsResponse + 15, // 75: fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat:output_type -> fleetnodegateway.v1.UploadHeartbeatResponse + 20, // 76: fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact:output_type -> fleetnodegateway.v1.UploadCommandArtifactResponse + 23, // 77: fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact:output_type -> fleetnodegateway.v1.DownloadCommandArtifactResponse + 26, // 78: fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices:output_type -> fleetnodegateway.v1.ReportDiscoveredDevicesResponse + 30, // 79: fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices:output_type -> fleetnodegateway.v1.ReportPairedDevicesResponse + 32, // 80: fleetnodegateway.v1.FleetNodeGatewayService.ControlStream:output_type -> fleetnodegateway.v1.ControlStreamResponse + 70, // [70:81] is the sub-list for method output_type + 59, // [59:70] is the sub-list for method input_type + 59, // [59:59] is the sub-list for extension type_name + 59, // [59:59] is the sub-list for extension extendee + 0, // [0:59] is the sub-list for field type_name } func init() { file_fleetnodegateway_v1_fleetnodegateway_proto_init() } @@ -4694,9 +4764,10 @@ func file_fleetnodegateway_v1_fleetnodegateway_proto_init() { (*MinerCommand_GetMiningPools)(nil), (*MinerCommand_GetErrors)(nil), (*MinerCommand_UpdateMinerPassword)(nil), + (*MinerCommand_DownloadLogs)(nil), } - file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[50].OneofWrappers = []any{} - file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[52].OneofWrappers = []any{ + file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[51].OneofWrappers = []any{} + file_fleetnodegateway_v1_fleetnodegateway_proto_msgTypes[53].OneofWrappers = []any{ (*AgentCommand_Discover)(nil), (*AgentCommand_Pair)(nil), (*AgentCommand_MinerCommand)(nil), @@ -4708,7 +4779,7 @@ func file_fleetnodegateway_v1_fleetnodegateway_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_fleetnodegateway_v1_fleetnodegateway_proto_rawDesc), len(file_fleetnodegateway_v1_fleetnodegateway_proto_rawDesc)), NumEnums: 4, - NumMessages: 55, + NumMessages: 56, NumExtensions: 0, NumServices: 1, }, diff --git a/server/internal/domain/fleetnode/control/command.go b/server/internal/domain/fleetnode/control/command.go index f070f3331..22098bdd2 100644 --- a/server/internal/domain/fleetnode/control/command.go +++ b/server/internal/domain/fleetnode/control/command.go @@ -24,6 +24,14 @@ func (r *Registry) SendCommand(ctx context.Context, fleetNodeID int64, cmd *gate // SendCommandWithArtifacts dispatches an ack-only command with optional // artifact-transfer expectations attached to its command_id. func (r *Registry) SendCommandWithArtifacts(ctx context.Context, fleetNodeID int64, cmd *gatewaypb.ControlCommand, artifacts []ArtifactExpectation) (*gatewaypb.ControlAck, error) { + ack, _, err := r.SendCommandWithArtifactResults(ctx, fleetNodeID, cmd, artifacts) + return ack, err +} + +// SendCommandWithArtifactResults dispatches an ack-only command with optional +// artifact-transfer expectations and returns any completed upload refs alongside +// the terminal ack. Refs are snapshotted before the in-flight command is removed. +func (r *Registry) SendCommandWithArtifactResults(ctx context.Context, fleetNodeID int64, cmd *gatewaypb.ControlCommand, artifacts []ArtifactExpectation) (*gatewaypb.ControlAck, []*gatewaypb.CommandArtifactRef, error) { c := &inflightCommand{ id: cmd.GetCommandId(), ack: make(chan *gatewaypb.ControlAck, 1), // never closed @@ -33,30 +41,53 @@ func (r *Registry) SendCommandWithArtifacts(ctx context.Context, fleetNodeID int outgoing, connDone, err := r.addCmd(fleetNodeID, c) if err != nil { if errors.Is(err, errDuplicateCommandID) { - return nil, fleeterror.NewInternalError(err.Error()) + return nil, nil, fleeterror.NewInternalError(err.Error()) } - return nil, err // ErrNoActiveStream + return nil, nil, err // ErrNoActiveStream } // Always free the slot: on ack, disconnect, or ctx expiry. defer r.removeCmd(fleetNodeID, c) if err := r.enqueue(ctx, outgoing, connDone, cmd); err != nil { - return nil, err + return nil, nil, err } select { case ack := <-c.ack: - return ack, nil + refs, resultErr := r.completedUploadRefs(ack, c) + return ack, refs, resultErr case <-c.done: // teardown raced the ack; drain a late ack before giving up so select // randomness can't drop a terminal result that landed with the teardown. select { case ack := <-c.ack: - return ack, nil + refs, resultErr := r.completedUploadRefs(ack, c) + return ack, refs, resultErr default: - return nil, ErrNoActiveStream + return nil, nil, ErrNoActiveStream } case <-ctx.Done(): - return nil, fleeterror.NewInternalErrorf("await ack: %w", ctx.Err()) + return nil, nil, fleeterror.NewInternalErrorf("await ack: %w", ctx.Err()) + } +} + +func (r *Registry) completedUploadRefs(ack *gatewaypb.ControlAck, c *inflightCommand) ([]*gatewaypb.CommandArtifactRef, error) { + r.mu.Lock() + defer r.mu.Unlock() + refs := make([]*gatewaypb.CommandArtifactRef, 0, len(c.artifacts)) + missingUploads := 0 + for _, exp := range c.artifacts { + if exp.Direction != ArtifactDirectionUpload { + continue + } + if !exp.completed || exp.uploadRef == nil { + missingUploads++ + continue + } + refs = append(refs, cloneCommandArtifactRef(exp.uploadRef)) + } + if ack.GetCode() == gatewaypb.AckCode_ACK_CODE_OK && ack.GetSucceeded() && missingUploads > 0 { + return refs, fleeterror.NewInternalErrorf("fleet node reported command success before %d expected artifact upload(s) completed", missingUploads) } + return refs, nil } diff --git a/server/internal/domain/fleetnode/control/registry_test.go b/server/internal/domain/fleetnode/control/registry_test.go index f3cc67bf1..367f7b93f 100644 --- a/server/internal/domain/fleetnode/control/registry_test.go +++ b/server/internal/domain/fleetnode/control/registry_test.go @@ -287,6 +287,66 @@ func TestRegistry_SendCommandAckPayloadRoutesToMatchingCommand(t *testing.T) { assert.Equal(t, []byte("payload-first"), res.ack.GetPayload()) } +func TestRegistry_SendCommandWithArtifactResultsReturnsCompletedUploadRefs(t *testing.T) { + r := NewRegistry() + s := r.Register(1) + defer s.Unregister() + expectation := ArtifactExpectation{ + Direction: ArtifactDirectionUpload, + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + DeviceIdentifier: "dev-1", + } + results := make(chan artifactCmdResult, 1) + go func() { + ack, refs, err := r.SendCommandWithArtifactResults(context.Background(), 1, &gatewaypb.ControlCommand{CommandId: "logs"}, []ArtifactExpectation{expectation}) + results <- artifactCmdResult{ack: ack, refs: refs, err: err} + }() + require.Equal(t, "logs", recvCommandID(t, s)) + require.NoError(t, r.AdmitCommandArtifact(1, "logs", expectation)) + ref := &gatewaypb.CommandArtifactRef{ + ArtifactId: "artifact-1", + Purpose: expectation.Purpose, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + } + require.True(t, r.CompleteCommandArtifactUpload(1, "logs", expectation, ref)) + + s.PublishAck(&gatewaypb.ControlAck{CommandId: "logs", Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}) + + res := recvArtifactResult(t, results) + require.NoError(t, res.err) + require.NotNil(t, res.ack) + require.Len(t, res.refs, 1) + assert.Equal(t, ref.GetArtifactId(), res.refs[0].GetArtifactId()) + assert.Equal(t, ref.GetSha256(), res.refs[0].GetSha256()) +} + +func TestRegistry_SendCommandWithArtifactResultsRejectsOKAckWithoutCompletedUpload(t *testing.T) { + r := NewRegistry() + s := r.Register(1) + defer s.Unregister() + expectation := ArtifactExpectation{ + Direction: ArtifactDirectionUpload, + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + DeviceIdentifier: "dev-1", + } + results := make(chan artifactCmdResult, 1) + go func() { + ack, refs, err := r.SendCommandWithArtifactResults(context.Background(), 1, &gatewaypb.ControlCommand{CommandId: "logs"}, []ArtifactExpectation{expectation}) + results <- artifactCmdResult{ack: ack, refs: refs, err: err} + }() + require.Equal(t, "logs", recvCommandID(t, s)) + + s.PublishAck(&gatewaypb.ControlAck{CommandId: "logs", Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}) + + res := recvArtifactResult(t, results) + require.Error(t, res.err) + assert.Contains(t, res.err.Error(), "expected artifact upload") + require.NotNil(t, res.ack) + assert.Empty(t, res.refs) +} + func TestRegistry_TeardownClosesAllInFlightCommands(t *testing.T) { // Arrange: a discovery and an ack-only command are both in flight. r := NewRegistry() @@ -655,6 +715,12 @@ type cmdResult struct { err error } +type artifactCmdResult struct { + ack *gatewaypb.ControlAck + refs []*gatewaypb.CommandArtifactRef + err error +} + // recvCommandID drains one dispatched command off the agent's outgoing channel and // returns its command_id. Receiving it proves the command was registered (addCmd runs // before the enqueue), so a subsequent PublishAck routes deterministically. @@ -681,6 +747,17 @@ func recvResult(t *testing.T, ch <-chan cmdResult) cmdResult { } } +func recvArtifactResult(t *testing.T, ch <-chan artifactCmdResult) artifactCmdResult { + t.Helper() + select { + case res := <-ch: + return res + case <-time.After(time.Second): + t.Fatal("timed out waiting for SendCommandWithArtifactResults result") + return artifactCmdResult{} + } +} + func assertClosed(t *testing.T, ch <-chan struct{}) { t.Helper() select { diff --git a/server/internal/domain/miner/logformat/logformat.go b/server/internal/domain/miner/logformat/logformat.go new file mode 100644 index 000000000..8cd808804 --- /dev/null +++ b/server/internal/domain/miner/logformat/logformat.go @@ -0,0 +1,98 @@ +package logformat + +import ( + "fmt" + "strings" +) + +const csvLogHeaderWithType = "Time,Type,Message" +const csvLogHeaderNoType = "Time,Message" + +// logLevelSeparators maps Proto miner log-level separators to their display labels. +// Format: "{prefix}: {timestamp} | LEVEL | {message}" +var logLevelSeparators = []struct { + separator string + label string +}{ + {" | ERROR | ", "ERROR"}, + {" | WARN | ", "WARN"}, + {" | INFO | ", "INFO"}, + {" | DEBUG | ", "DEBUG"}, +} + +// FormatTextToCSV converts raw newline-delimited miner logs into CSV rows. +func FormatTextToCSV(logData string, includeType bool) []string { + return FormatLinesToCSV(strings.Split(strings.TrimRight(logData, "\n"), "\n"), includeType) +} + +// FormatLinesToCSV converts raw log lines into CSV rows. +// When includeType is true, the header is "Time,Type,Message" for logs that emit +// levels. When false, the header is "Time,Message". +func FormatLinesToCSV(logLines []string, includeType bool) []string { + header := csvLogHeaderWithType + if !includeType { + header = csvLogHeaderNoType + } + rows := make([]string, 0, len(logLines)+1) + rows = append(rows, header) + for _, line := range logLines { + if strings.TrimSpace(line) == "" { + continue + } + rows = append(rows, FormatLineToCSVRow(line, includeType)) + } + return rows +} + +// FormatLineToCSVRow parses a single log line into a CSV row. +func FormatLineToCSVRow(line string, includeType bool) string { + csvRow := func(ts, logType, message string) string { + esc := func(s string) string { return strings.ReplaceAll(s, `"`, `""`) } + if includeType { + return fmt.Sprintf(`"%s","%s","%s"`, esc(ts), esc(logType), esc(message)) + } + return fmt.Sprintf(`"%s","%s"`, esc(ts), esc(message)) + } + + for _, level := range logLevelSeparators { + idx := strings.Index(line, level.separator) + if idx < 0 { + continue + } + prefix := line[:idx] + message := line[idx+len(level.separator):] + + ts := prefix + if parts := strings.SplitN(prefix, ": ", 2); len(parts) == 2 { + ts = parts[1] + } else if fields := strings.Fields(prefix); len(fields) >= 3 { + ts = fields[0] + " " + fields[1] + " " + fields[2] + } + ts = strings.TrimSpace(ts) + if dotIdx := strings.Index(ts, "."); dotIdx >= 0 { + ts = ts[:dotIdx] + } + + return csvRow(ts, level.label, message) + } + + // Antminer bracketed calendar timestamps look like "[2026-01-01T00:00:00Z] message". + // Boot counters such as "[258.894452@1]" intentionally fall through. + if strings.HasPrefix(line, "[") { + if closeBracket := strings.Index(line, "]"); closeBracket > 0 { + potentialTS := strings.TrimSpace(line[1:closeBracket]) + if strings.ContainsAny(potentialTS, "0123456789") && strings.ContainsAny(potentialTS, "T-/") { + message := strings.TrimPrefix(line[closeBracket+1:], " ") + return csvRow(potentialTS, "", message) + } + } + } + + if len(line) > 19 && line[4] == '-' && line[7] == '-' && line[10] == ' ' && line[13] == ':' && line[16] == ':' { + timestamp := line[:19] + message := strings.TrimPrefix(line[19:], " ") + return csvRow(timestamp, "", message) + } + + return csvRow("", "", line) +} diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index 83662f747..3ecd9b055 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -42,12 +42,21 @@ type CommandSender interface { SendCommand(ctx context.Context, fleetNodeID int64, cmd *gatewaypb.ControlCommand) (*gatewaypb.ControlAck, error) } +type ArtifactCommandSender interface { + SendCommandWithArtifactResults(ctx context.Context, fleetNodeID int64, cmd *gatewaypb.ControlCommand, artifacts []control.ArtifactExpectation) (*gatewaypb.ControlAck, []*gatewaypb.CommandArtifactRef, error) +} + +type LogArtifactSaver interface { + SaveCommandArtifactLog(batchLogUUID string, macAddress string, artifactID string) (string, error) +} + // Config carries everything the adapter needs to address a fleet-node-paired miner. type Config struct { - Sender CommandSender - FleetNodeID int64 - OrgID int64 - SiteID int64 + Sender CommandSender + FleetNodeID int64 + OrgID int64 + SiteID int64 + LogArtifacts LogArtifactSaver // Gate, if set, bounds concurrent commands the server has in flight to this // fleet node so a large batch paces rather than oversubscribing the node. Gate Gate @@ -70,13 +79,14 @@ type Config struct { // value (no live connection), so caching the handle is safe; stream liveness is // resolved per command by the registry. type Miner struct { - sender CommandSender - gate Gate - fleetNodeID int64 - orgID int64 - siteID int64 - desc *gatewaypb.MinerConnectionDescriptor - connInfo networking.ConnectionInfo + sender CommandSender + gate Gate + logArtifacts LogArtifactSaver + fleetNodeID int64 + orgID int64 + siteID int64 + desc *gatewaypb.MinerConnectionDescriptor + connInfo networking.ConnectionInfo } var _ interfaces.Miner = (*Miner)(nil) @@ -100,11 +110,12 @@ func New(cfg Config) (*Miner, error) { return nil, fleeterror.NewInternalErrorf("remote-node miner: connection info: %v", err) } return &Miner{ - sender: cfg.Sender, - gate: cfg.Gate, - fleetNodeID: cfg.FleetNodeID, - orgID: cfg.OrgID, - siteID: cfg.SiteID, + sender: cfg.Sender, + gate: cfg.Gate, + logArtifacts: cfg.LogArtifacts, + fleetNodeID: cfg.FleetNodeID, + orgID: cfg.OrgID, + siteID: cfg.SiteID, desc: &gatewaypb.MinerConnectionDescriptor{ DeviceIdentifier: cfg.DeviceIdentifier, DriverName: cfg.DriverName, @@ -205,6 +216,34 @@ func (m *Miner) acquireGate(ctx context.Context) (func(), error) { } func (m *Miner) sendWithoutGate(ctx context.Context, mc *gatewaypb.MinerCommand) (*gatewaypb.ControlAck, error) { + cmd, err := m.controlCommandForMiner(id.GenerateID(), mc) + if err != nil { + return nil, err + } + ack, err := m.sender.SendCommand(ctx, m.fleetNodeID, cmd) + if err != nil { + return nil, mapSendCommandError(err) + } + return ack, nil +} + +func (m *Miner) sendWithoutGateWithArtifactResults(ctx context.Context, mc *gatewaypb.MinerCommand, artifacts []control.ArtifactExpectation) (*gatewaypb.ControlAck, []*gatewaypb.CommandArtifactRef, error) { + sender, ok := m.sender.(ArtifactCommandSender) + if !ok { + return nil, nil, fleeterror.NewInternalError("fleet node command sender does not support artifact results") + } + cmd, err := m.controlCommandForMiner(id.GenerateID(), mc) + if err != nil { + return nil, nil, err + } + ack, refs, err := sender.SendCommandWithArtifactResults(ctx, m.fleetNodeID, cmd, artifacts) + if err != nil { + return nil, nil, mapSendCommandError(err) + } + return ack, refs, nil +} + +func (m *Miner) controlCommandForMiner(commandID string, mc *gatewaypb.MinerCommand) (*gatewaypb.ControlCommand, error) { mc.Target = m.desc if err := protovalidate.Validate(mc); err != nil { return nil, fleeterror.NewInvalidArgumentErrorf("invalid fleet node miner command: %v", err) @@ -215,19 +254,19 @@ func (m *Miner) sendWithoutGate(ctx context.Context, mc *gatewaypb.MinerCommand) if err != nil { return nil, fleeterror.NewInternalErrorf("marshal miner command: %v", err) } - ack, err := m.sender.SendCommand(ctx, m.fleetNodeID, &gatewaypb.ControlCommand{ - CommandId: id.GenerateID(), + return &gatewaypb.ControlCommand{ + CommandId: commandID, Payload: payload, - }) - if err != nil { - if errors.Is(err, control.ErrNoActiveStream) { - // Retryable, not permanent (Unavailable is not in the queue's permanent-fail - // set), so a node mid-reconnect re-attempts rather than dropping the command. - return nil, fleeterror.NewUnavailableErrorf("fleet node has no active control stream; retry shortly") - } - return nil, err + }, nil +} + +func mapSendCommandError(err error) error { + if errors.Is(err, control.ErrNoActiveStream) { + // Retryable, not permanent (Unavailable is not in the queue's permanent-fail + // set), so a node mid-reconnect re-attempts rather than dropping the command. + return fleeterror.NewUnavailableErrorf("fleet node has no active control stream; retry shortly") } - return ack, nil + return err } // maxAckReasonBytes mirrors the node's send-side cap so a buggy/hostile node can't @@ -336,8 +375,38 @@ func dtoNodeEncryptedPayloadToProto(payload *dto.NodeEncryptedPayload) *gatewayp } } -func (m *Miner) DownloadLogs(_ context.Context, _ string) error { - return errUnsupported("DownloadLogs") +func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { + if m.logArtifacts == nil { + return fleeterror.NewInternalError("remote-node miner log artifact saver is not configured") + } + release, err := m.acquireGate(ctx) + if err != nil { + return err + } + defer release() + + ack, refs, err := m.sendWithoutGateWithArtifactResults(ctx, &gatewaypb.MinerCommand{Action: &gatewaypb.MinerCommand_DownloadLogs{ + DownloadLogs: &gatewaypb.DownloadLogsAction{BatchLogUuid: batchLogUUID}, + }}, []control.ArtifactExpectation{{ + Direction: control.ArtifactDirectionUpload, + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + DeviceIdentifier: m.desc.GetDeviceIdentifier(), + }}) + if err != nil { + return err + } + if err := ackToError(ack); err != nil { + return err + } + + ref, ok := minerLogsArtifactRef(refs) + if !ok { + return fleeterror.NewInternalError("fleet node reported log download success without uploaded log artifact") + } + if _, err := m.logArtifacts.SaveCommandArtifactLog(batchLogUUID, m.desc.GetMacAddress(), ref.GetArtifactId()); err != nil { + return fleeterror.NewInternalErrorf("failed to save fleet node miner logs: %v", err) + } + return nil } func (m *Miner) FirmwareUpdate(_ context.Context, _ sdk.FirmwareFile) error { @@ -441,6 +510,15 @@ func errUnsupported(op string) error { return fleeterror.NewUnimplementedErrorf("%s is not yet supported for fleet-node-paired miners", op) } +func minerLogsArtifactRef(refs []*gatewaypb.CommandArtifactRef) (*gatewaypb.CommandArtifactRef, bool) { + for _, ref := range refs { + if ref.GetPurpose() == gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS && ref.GetArtifactId() != "" { + return ref, true + } + } + return nil, false +} + func deviceErrorsFromResult(result *gatewaypb.GetErrorsResult) (models.DeviceErrors, error) { deviceID := result.GetDeviceId() out := models.DeviceErrors{ diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index e559ed813..3e43174a7 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -28,9 +28,11 @@ import ( ) type fakeSender struct { - cmd *gatewaypb.ControlCommand - ack *gatewaypb.ControlAck - err error + cmd *gatewaypb.ControlCommand + ack *gatewaypb.ControlAck + err error + artifacts []control.ArtifactExpectation + refs []*gatewaypb.CommandArtifactRef } func (f *fakeSender) SendCommand(_ context.Context, _ int64, cmd *gatewaypb.ControlCommand) (*gatewaypb.ControlAck, error) { @@ -38,6 +40,29 @@ func (f *fakeSender) SendCommand(_ context.Context, _ int64, cmd *gatewaypb.Cont return f.ack, f.err } +func (f *fakeSender) SendCommandWithArtifactResults(_ context.Context, _ int64, cmd *gatewaypb.ControlCommand, artifacts []control.ArtifactExpectation) (*gatewaypb.ControlAck, []*gatewaypb.CommandArtifactRef, error) { + f.cmd = cmd + f.artifacts = artifacts + return f.ack, f.refs, f.err +} + +type fakeLogArtifactSaver struct { + batchLogUUID string + macAddress string + artifactID string + err error +} + +func (f *fakeLogArtifactSaver) SaveCommandArtifactLog(batchLogUUID string, macAddress string, artifactID string) (string, error) { + f.batchLogUUID = batchLogUUID + f.macAddress = macAddress + f.artifactID = artifactID + if f.err != nil { + return "", f.err + } + return "logs/" + batchLogUUID + "/miner-logs.csv", nil +} + type blockingSender struct { started chan struct{} } @@ -64,6 +89,19 @@ func newTestMiner(t *testing.T, s CommandSender) *Miner { return m } +func newTestMinerWithLogSaver(t *testing.T, s CommandSender, saver LogArtifactSaver) *Miner { + t.Helper() + m, err := New(Config{ + Sender: s, FleetNodeID: 7, OrgID: 1, + LogArtifacts: saver, + DeviceIdentifier: "dev-1", DriverName: "virtual", + IPAddress: "10.0.0.5", Port: "4028", URLScheme: "http", + SerialNumber: "SN1", MacAddress: "AA:BB:CC:DD:EE:FF", + }) + require.NoError(t, err) + return m +} + func newTestMinerWithGate(t *testing.T, s CommandSender, gate Gate) *Miner { t.Helper() m, err := New(Config{ @@ -803,6 +841,65 @@ func TestMiner_NoActiveStreamIsRetryable(t *testing.T) { assert.False(t, fleeterror.IsFailedPreconditionError(err)) } +func TestMiner_DownloadLogsSendsActionAndMaterializesArtifact(t *testing.T) { + // Arrange + s := &fakeSender{ + ack: &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}, + refs: []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, + } + saver := &fakeLogArtifactSaver{} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.NoError(t, err) + mc := decodeSent(t, s) + assert.Equal(t, "batch-1", mc.GetDownloadLogs().GetBatchLogUuid()) + require.Len(t, s.artifacts, 1) + assert.Equal(t, control.ArtifactDirectionUpload, s.artifacts[0].Direction) + assert.Equal(t, gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, s.artifacts[0].Purpose) + assert.Equal(t, "dev-1", s.artifacts[0].DeviceIdentifier) + assert.Equal(t, "batch-1", saver.batchLogUUID) + assert.Equal(t, "AA:BB:CC:DD:EE:FF", saver.macAddress) + assert.Equal(t, "artifact-1", saver.artifactID) +} + +func TestMiner_DownloadLogsRequiresUploadedArtifactRef(t *testing.T) { + // Arrange + s := &fakeSender{ack: &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}} + saver := &fakeLogArtifactSaver{} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.Contains(t, err.Error(), "without uploaded log artifact") + assert.Empty(t, saver.artifactID) +} + +func TestMiner_DownloadLogsNoActiveStreamIsRetryable(t *testing.T) { + // Arrange + m := newTestMinerWithLogSaver(t, &fakeSender{err: control.ErrNoActiveStream}, &fakeLogArtifactSaver{}) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.True(t, fleeterror.IsUnavailableError(err)) + assert.False(t, fleeterror.IsFailedPreconditionError(err)) +} + func TestClampAckReason(t *testing.T) { // Arrange: an untrusted oversized ack message (a well-behaved node caps at the limit). oversized := strings.Repeat("x", maxAckReasonBytes*2) @@ -823,7 +920,6 @@ func TestMiner_UnsupportedMethodsReturnUnimplemented(t *testing.T) { // Assert: not-yet-supported methods return Unimplemented. assert.True(t, fleeterror.IsUnimplementedError(m.Unpair(ctx))) - assert.True(t, fleeterror.IsUnimplementedError(m.DownloadLogs(ctx, "batch"))) _, err := m.GetDeviceStatus(ctx) assert.True(t, fleeterror.IsUnimplementedError(err)) } diff --git a/server/internal/domain/miner/service.go b/server/internal/domain/miner/service.go index 7ea476231..6c3ccb86e 100644 --- a/server/internal/domain/miner/service.go +++ b/server/internal/domain/miner/service.go @@ -249,6 +249,7 @@ func (s *Service) tryFleetNodeMiner(ctx context.Context, deviceID models.DeviceI remoteCommandMiner, err := remotenode.New(remotenode.Config{ Sender: s.commandSender, Gate: s.nodeLimiter, + LogArtifacts: s.filesService, FleetNodeID: telemetryRoute.FleetNodeID, OrgID: telemetryRoute.OrgID, SiteID: telemetryRoute.SiteID.Int64, diff --git a/server/internal/domain/plugins/plugin_miner.go b/server/internal/domain/plugins/plugin_miner.go index b26c1161c..2abe40cd2 100644 --- a/server/internal/domain/plugins/plugin_miner.go +++ b/server/internal/domain/plugins/plugin_miner.go @@ -17,6 +17,7 @@ import ( "github.com/block/proto-fleet/server/internal/domain/fleeterror" "github.com/block/proto-fleet/server/internal/domain/miner/dto" "github.com/block/proto-fleet/server/internal/domain/miner/interfaces" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" "github.com/block/proto-fleet/server/internal/domain/miner/models" "github.com/block/proto-fleet/server/internal/domain/plugins/mappers" modelsV2 "github.com/block/proto-fleet/server/internal/domain/telemetry/models/v2" @@ -373,125 +374,14 @@ func (p *PluginMiner) DownloadLogs(ctx context.Context, batchLogUUID string) err if err != nil { return fleeterror.NewInternalErrorf("failed to download logs: %v", err) } - logLines := strings.Split(strings.TrimRight(logData, "\n"), "\n") - csvRows := formatLogsToCSV(logLines, p.caps[sdk.CapabilityLogLevels]) + csvRows := logformat.FormatTextToCSV(logData, p.caps[sdk.CapabilityLogLevels]) if _, err := p.filesService.SaveLogs(batchLogUUID, p.deviceInfo.MacAddress, csvRows); err != nil { return fleeterror.NewInternalErrorf("failed to save logs: %v", err) } return nil } -const csvLogHeaderWithType = "Time,Type,Message" -const csvLogHeaderNoType = "Time,Message" - -// logLevelSeparators maps the separator strings used in Proto miner log lines to their display label. -// Format: "{prefix}: {timestamp} | LEVEL | {message}" -var logLevelSeparators = []struct { - separator string - label string -}{ - {" | ERROR | ", "ERROR"}, - {" | WARN | ", "WARN"}, - {" | INFO | ", "INFO"}, - {" | DEBUG | ", "DEBUG"}, -} - -// formatLogsToCSV converts raw log lines into CSV rows. -// When includeType is true, the header is "Time,Type,Message" (used for Proto miners that emit log levels). -// When false, the header is "Time,Message" (used for Antminer logs that have no log level field). -func formatLogsToCSV(logLines []string, includeType bool) []string { - header := csvLogHeaderWithType - if !includeType { - header = csvLogHeaderNoType - } - rows := make([]string, 0, len(logLines)+1) - rows = append(rows, header) - for _, line := range logLines { - if strings.TrimSpace(line) == "" { - continue - } - rows = append(rows, formatLogLineToCSVRow(line, includeType)) - } - return rows -} - -// formatLogLineToCSVRow parses a single log line into a CSV row. -// Handles three formats: -// - Proto miner syslog with mcdd timestamp: "{syslog_prefix}: {mcdd_timestamp} | LEVEL | {message}" -// - Proto miner syslog without mcdd timestamp (BX firmware): "{syslog_prefix}: | LEVEL | {message}" -// - Proto miner bare timestamp: "{mcdd_timestamp} | LEVEL | {message}" -// - Antminer calendar timestamp: "[2026-01-01T00:00:00Z] message" -// - Antminer application log: "YYYY-MM-DD HH:MM:SS message" -// - Antminer kernel boot log: "[seconds_since_boot] message" — no wall-clock time, falls through -// -// Matches the parsing logic in the ProtoOS frontend utility.ts formatLog function. -func formatLogLineToCSVRow(line string, includeType bool) string { - csvRow := func(ts, logType, message string) string { - esc := func(s string) string { return strings.ReplaceAll(s, `"`, `""`) } - if includeType { - return fmt.Sprintf(`"%s","%s","%s"`, esc(ts), esc(logType), esc(message)) - } - return fmt.Sprintf(`"%s","%s"`, esc(ts), esc(message)) - } - - for _, level := range logLevelSeparators { - idx := strings.Index(line, level.separator) - if idx < 0 { - continue - } - prefix := line[:idx] - message := line[idx+len(level.separator):] - - // Extract timestamp from the prefix. - // The level separator ` | LEVEL | ` includes a leading space, so the prefix ends just - // before that space (e.g. "...mcdd[664]:" with no trailing space). - // - // Three prefix shapes: - // 1. Syslog + mcdd timestamp: "Jun 14 16:01:58 miner mcdd[716]: 2024-06-14 16:01:58.470952" - // → SplitN on ": " → parts[1] = "2024-06-14 16:01:58.470952" - // 2. Syslog only (BX firmware): "Feb 23 12:33:24 proto-miner-D202 mcdd[664]:" - // → no ": " match → extract first 3 space-separated fields as syslog date/time - // 3. Bare mcdd timestamp: "2024-06-14 16:01:58.470952" - // → no ": " match, < 3 fields → use full prefix - ts := prefix - if parts := strings.SplitN(prefix, ": ", 2); len(parts) == 2 { - ts = parts[1] - } else if fields := strings.Fields(prefix); len(fields) >= 3 { - ts = fields[0] + " " + fields[1] + " " + fields[2] - } - ts = strings.TrimSpace(ts) - if dotIdx := strings.Index(ts, "."); dotIdx >= 0 { - ts = ts[:dotIdx] - } - - return csvRow(ts, level.label, message) - } - - // Try [timestamp] message format used by Antminer kernel logs. - // Only treat bracketed content as a real calendar timestamp when it contains date - // separators ('T', '-', '/'). Bare numbers like "[258.894452@1]" are seconds-since-boot - // counters with no wall-clock date, so they fall through to the raw message catch-all. - if strings.HasPrefix(line, "[") { - if closeBracket := strings.Index(line, "]"); closeBracket > 0 { - potentialTS := strings.TrimSpace(line[1:closeBracket]) - if strings.ContainsAny(potentialTS, "0123456789") && strings.ContainsAny(potentialTS, "T-/") { - message := strings.TrimPrefix(line[closeBracket+1:], " ") - return csvRow(potentialTS, "", message) - } - } - } - - // Try "YYYY-MM-DD HH:MM:SS message" format used by Antminer application logs. - if len(line) > 19 && line[4] == '-' && line[7] == '-' && line[10] == ' ' && line[13] == ':' && line[16] == ':' { - timestamp := line[:19] - message := strings.TrimPrefix(line[19:], " ") - return csvRow(timestamp, "", message) - } - - return csvRow("", "", line) -} - // FirmwareUpdate implements interfaces.Miner func (p *PluginMiner) FirmwareUpdate(ctx context.Context, firmware sdk.FirmwareFile) error { if err := p.sdkDevice.FirmwareUpdate(ctx, firmware); err != nil { diff --git a/server/internal/domain/plugins/plugin_miner_test.go b/server/internal/domain/plugins/plugin_miner_test.go index 65a74eae9..ea9ce263d 100644 --- a/server/internal/domain/plugins/plugin_miner_test.go +++ b/server/internal/domain/plugins/plugin_miner_test.go @@ -15,6 +15,7 @@ import ( pb "github.com/block/proto-fleet/server/generated/grpc/minercommand/v1" "github.com/block/proto-fleet/server/internal/domain/fleeterror" "github.com/block/proto-fleet/server/internal/domain/miner/dto" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" "github.com/block/proto-fleet/server/internal/domain/miner/models" "github.com/block/proto-fleet/server/internal/infrastructure/networking" sdk "github.com/block/proto-fleet/server/sdk/v1" @@ -594,7 +595,7 @@ func TestFormatLogLineToCSVRow(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := formatLogLineToCSVRow(tt.line, tt.includeType) + result := logformat.FormatLineToCSVRow(tt.line, tt.includeType) assert.Equal(t, tt.expected, result) }) } diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 6ff51e4cc..16231dd8f 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -4,6 +4,8 @@ import ( "archive/zip" "bufio" "context" + "crypto/sha256" + "encoding/hex" "fmt" "io" "log/slog" @@ -166,7 +168,7 @@ func (s *Service) CreateBatchDirIfNotExists(batchLogUUID string) (string, error) return batchDir, nil } -func (s *Service) SaveLogs(batchLogUUID string, macAddress string, logLines []string) (string, error) { +func (s *Service) batchLogFilePath(batchLogUUID string, macAddress string) (string, error) { batchDir, err := s.CreateBatchDirIfNotExists(batchLogUUID) if err != nil { return "", err @@ -175,11 +177,25 @@ func (s *Service) SaveLogs(batchLogUUID string, macAddress string, logLines []st normalizedMAC := sanitizeMACForFilename(macAddress) timestamp := time.Now().Format("2006-01-02_15-04-05") filename := fmt.Sprintf("miner-logs-%s-%s.csv", normalizedMAC, timestamp) - filePath := filepath.Join(batchDir, filename) + return filepath.Join(batchDir, filename), nil +} +func (s *Service) openBatchLogFile(batchLogUUID string, macAddress string) (string, *os.File, error) { + filePath, err := s.batchLogFilePath(batchLogUUID, macAddress) + if err != nil { + return "", nil, err + } file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { - return "", fleeterror.NewInternalErrorf("failed to create log file: %v", err) + return "", nil, fleeterror.NewInternalErrorf("failed to create log file: %v", err) + } + return filePath, file, nil +} + +func (s *Service) SaveLogs(batchLogUUID string, macAddress string, logLines []string) (string, error) { + filePath, file, err := s.openBatchLogFile(batchLogUUID, macAddress) + if err != nil { + return "", err } defer file.Close() @@ -203,6 +219,54 @@ func (s *Service) SaveLogs(batchLogUUID string, macAddress string, logLines []st return filePath, nil } +func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, artifactID string) (string, error) { + reader, info, err := s.OpenCommandArtifact(artifactID) + if err != nil { + return "", err + } + defer reader.Close() + + filePath, file, err := s.openBatchLogFile(batchLogUUID, macAddress) + if err != nil { + return "", err + } + keep := false + defer func() { + if file != nil { + if closeErr := file.Close(); closeErr != nil { + slog.Warn("failed to close command artifact log file", "path", filePath, "error", closeErr) + } + } + if !keep { + if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) { + slog.Warn("failed to remove partial command artifact log file", "path", filePath, "error", removeErr) + } + } + }() + + hasher := sha256.New() + written, err := io.CopyBuffer(file, io.TeeReader(reader, hasher), make([]byte, commandArtifactCopyBufferSize)) + if err != nil { + return "", fleeterror.NewInternalErrorf("failed to copy command artifact log: %v", err) + } + if written != info.Size { + return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: metadata size %d does not match copied size %d", info.ID, info.Size, written) + } + if actualSHA := hex.EncodeToString(hasher.Sum(nil)); actualSHA != info.SHA256 { + return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: sha256 mismatch", info.ID) + } + if err := file.Sync(); err != nil { + return "", fleeterror.NewInternalErrorf("failed to sync command artifact log: %v", err) + } + if err := file.Close(); err != nil { + return "", fleeterror.NewInternalErrorf("failed to close command artifact log: %v", err) + } + file = nil + keep = true + + return filePath, nil +} + func (s *Service) bundleLogs(batchLogUUID string) (string, error) { batchDir := getBatchLogsDirPath(batchLogUUID) logFiles, err := os.ReadDir(batchDir) diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index fbdb1a394..bdc2b19c3 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -2,6 +2,7 @@ package files import ( "archive/zip" + "io" "os" "path/filepath" "strings" @@ -75,6 +76,32 @@ func TestBundleLogs_SingleFile_MovesToTempWithNameSidecar(t *testing.T) { assert.Equal(t, originalName, string(sidecar)) } +func TestSaveCommandArtifactLog_MaterializesAndBundlesSingleCSV(t *testing.T) { + svc := setupService(t) + content := "Time,Message\n2026-01-01T00:00:00Z,\"hello\"\n" + info, err := svc.SaveCommandArtifact("../../remote-miner-logs.csv", int64(len(content)), checksumOf(content), strings.NewReader(content)) + require.NoError(t, err) + + filePath, err := svc.SaveCommandArtifactLog("batch-artifact-single", "AA:BB:CC:DD:EE:FF", info.ID) + require.NoError(t, err) + + assert.Equal(t, getBatchLogsDirPath("batch-artifact-single"), filepath.Dir(filePath)) + assert.True(t, strings.HasPrefix(filepath.Base(filePath), "miner-logs-aabbccddeeff-")) + assert.True(t, strings.HasSuffix(filepath.Base(filePath), ".csv")) + data, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, content, string(data)) + + bundlePath, err := svc.bundleLogs("batch-artifact-single") + require.NoError(t, err) + assert.Equal(t, getBatchLogsSingleFilePath("batch-artifact-single"), bundlePath) + + fsFile, err := svc.GetBatchLogBundleFile("batch-artifact-single") + require.NoError(t, err) + assert.Equal(t, filepath.Base(filePath), fsFile.Filename) + assert.Equal(t, content, string(fsFile.Data)) +} + // TestBundleLogs_MultipleFiles_CreatesZIPWithNameSidecar verifies that when logs from // multiple devices are present they are bundled into a ZIP, and a .name sidecar is // written with a human-readable filename matching the miner-logs-{timestamp}.zip pattern. @@ -126,6 +153,25 @@ func TestBundleLogs_MultipleFiles_ZIPContainsAllFiles(t *testing.T) { assert.Contains(t, names, filepath.Base(file2)) } +func TestSaveCommandArtifactLog_BundlesMixedDirectAndRemoteLogsAsZIP(t *testing.T) { + svc := setupService(t) + directPath, err := svc.SaveLogs("batch-mixed", "aa:bb:cc:dd:ee:01", []string{"direct-line"}) + require.NoError(t, err) + remoteContent := "remote-line\n" + info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(remoteContent)), checksumOf(remoteContent), strings.NewReader(remoteContent)) + require.NoError(t, err) + remotePath, err := svc.SaveCommandArtifactLog("batch-mixed", "aa:bb:cc:dd:ee:02", info.ID) + require.NoError(t, err) + + bundlePath, err := svc.bundleLogs("batch-mixed") + require.NoError(t, err) + assert.Equal(t, getBatchLogsZipFilePath("batch-mixed"), bundlePath) + + contents := readZipFileContents(t, bundlePath) + assert.Equal(t, "direct-line\n", contents[filepath.Base(directPath)]) + assert.Equal(t, remoteContent, contents[filepath.Base(remotePath)]) +} + // TestBundleLogs_NoFiles_ReturnsEmpty verifies that bundling a batch with no log files // returns an empty path without error (all devices may have failed). func TestBundleLogs_NoFiles_ReturnsEmpty(t *testing.T) { @@ -206,3 +252,53 @@ func TestBatchLogCleanup_RemovesAllFiles(t *testing.T) { assert.NoFileExists(t, getBatchLogsZipFilePath("batch-cleanup")) assert.NoFileExists(t, getBatchLogsZipFilePath("batch-cleanup")+".name") } + +func TestSaveCommandArtifactLog_RejectsMissingAndCorruptArtifacts(t *testing.T) { + t.Run("missing", func(t *testing.T) { + svc := setupService(t) + + _, err := svc.SaveCommandArtifactLog("batch-missing-artifact", "aa:bb:cc:dd:ee:ff", "00000000-0000-0000-0000-000000000000") + + require.Error(t, err) + assert.Contains(t, err.Error(), "command artifact not found") + assert.NoDirExists(t, getBatchLogsDirPath("batch-missing-artifact")) + }) + + t.Run("corrupt", func(t *testing.T) { + svc := setupService(t) + content := "aaaa" + info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(content)), checksumOf(content), strings.NewReader(content)) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(getCommandArtifactDirPath(info.ID), info.Filename), []byte("bbbb"), 0600)) + + filePath, err := svc.SaveCommandArtifactLog("batch-corrupt-artifact", "aa:bb:cc:dd:ee:ff", info.ID) + + require.Error(t, err) + assert.Contains(t, err.Error(), "sha256 mismatch") + assert.Empty(t, filePath) + entries, readErr := os.ReadDir(getBatchLogsDirPath("batch-corrupt-artifact")) + if !os.IsNotExist(readErr) { + require.NoError(t, readErr) + assert.Empty(t, entries) + } + }) +} + +func readZipFileContents(t *testing.T, zipPath string) map[string]string { + t.Helper() + zr, err := zip.OpenReader(zipPath) + require.NoError(t, err) + defer zr.Close() + + contents := make(map[string]string, len(zr.File)) + for _, f := range zr.File { + reader, err := f.Open() + require.NoError(t, err) + data, err := io.ReadAll(reader) + closeErr := reader.Close() + require.NoError(t, err) + require.NoError(t, closeErr) + contents[f.Name] = string(data) + } + return contents +} From 5bf74af4ca929824ccc76d952164c9ccdf714171 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 10:42:03 -0700 Subject: [PATCH 02/15] fix: reject partial fleet-node log downloads --- server/cmd/fleetnode/minercommand_test.go | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index ba5e46223..0e100eee6 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -657,6 +657,30 @@ func TestHandleMinerCommand_DownloadLogsAcksFailureWhenUploadFails(t *testing.T) assert.NotEmpty(t, fake.artifactUploads()) } +func TestHandleMinerCommand_DownloadLogsRejectsPartialData(t *testing.T) { + ctrl := gomock.NewController(t) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return("2026-02-24 07:52:12 partial log line\n", true, nil) + dev.EXPECT().Close(gomock.Any()).Return(nil) + drv := mocks.NewMockDriver(ctrl) + drv.EXPECT().NewDevice(gomock.Any(), "dev-1", gomock.Any(), gomock.Any()).Return(sdk.NewDeviceResult{Device: dev}, nil) + drv.EXPECT().DescribeDriver(gomock.Any()).Return(sdk.DriverIdentifier{}, sdk.Capabilities{}, nil) + fake := &fakeFleetNodeGateway{} + server := newFakeServer(t, fake) + client := fleetnodegatewayv1connect.NewFleetNodeGatewayServiceClient(server.Client(), server.URL) + r := &RunCmd{driverGetter: fakeDriverGetter{d: drv}, minerSecrets: nodeSecretProvider{}} + ack := &captureAcker{} + + r.handleMinerCommand(context.Background(), client, ack, "cmd-1", + withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_DownloadLogs{DownloadLogs: &pb.DownloadLogsAction{BatchLogUuid: "batch-1"}}}), discardLogger(t)) + + got := ack.only(t) + assert.Equal(t, pb.AckCode_ACK_CODE_PARTIAL, got.GetCode()) + assert.False(t, got.GetSucceeded()) + assert.Contains(t, got.GetErrorMessage(), "partial data") + assert.Empty(t, fake.artifactUploads()) +} + func TestGetErrorsResultFromSDKRejectsInvalidPluginErrorData(t *testing.T) { validError := func() sdk.DeviceError { return sdk.DeviceError{ From 31e7eb2ded9d5632d117323c6ed4813216b6d9f0 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 11:00:58 -0700 Subject: [PATCH 03/15] fix: bound fleet-node log artifact uploads --- server/cmd/fleetnode/minercommand.go | 1 + server/cmd/fleetnode/minercommand_test.go | 45 +++++++++++++++++++ .../domain/miner/logformat/logformat.go | 27 +++++++++++ .../domain/miner/logformat/logformat_test.go | 39 ++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 server/internal/domain/miner/logformat/logformat_test.go diff --git a/server/cmd/fleetnode/minercommand.go b/server/cmd/fleetnode/minercommand.go index 66d0db01d..c5864797d 100644 --- a/server/cmd/fleetnode/minercommand.go +++ b/server/cmd/fleetnode/minercommand.go @@ -43,6 +43,7 @@ const ( maxGetErrorsReports = 512 minerLogsArtifactFilename = "miner-logs.csv" commandArtifactChunkSize = 1 << 20 + maxMinerLogsArtifactBytes = 4 * 1024 * 1024 ) // driverGetter is the plugin-manager seam the executor needs; *plugins.Manager satisfies it. diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index 0e100eee6..1b9600e43 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -681,6 +681,51 @@ func TestHandleMinerCommand_DownloadLogsRejectsPartialData(t *testing.T) { assert.Empty(t, fake.artifactUploads()) } +func TestHandleMinerCommand_DownloadLogsRejectsOversizedLogs(t *testing.T) { + cases := []struct { + name string + logData string + wantErr string + }{ + { + name: "raw log exceeds limit", + logData: strings.Repeat("x", maxMinerLogsArtifactBytes+1), + wantErr: "log data exceeds", + }, + { + name: "formatted artifact exceeds limit", + logData: strings.Repeat(`"`, maxMinerLogsArtifactBytes/2+1), + wantErr: "log artifact exceeds", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + dev := mocks.NewMockDevice(ctrl) + dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return(tc.logData, false, nil) + dev.EXPECT().Close(gomock.Any()).Return(nil) + drv := mocks.NewMockDriver(ctrl) + drv.EXPECT().NewDevice(gomock.Any(), "dev-1", gomock.Any(), gomock.Any()).Return(sdk.NewDeviceResult{Device: dev}, nil) + drv.EXPECT().DescribeDriver(gomock.Any()).Return(sdk.DriverIdentifier{}, sdk.Capabilities{}, nil) + fake := &fakeFleetNodeGateway{} + server := newFakeServer(t, fake) + client := fleetnodegatewayv1connect.NewFleetNodeGatewayServiceClient(server.Client(), server.URL) + r := &RunCmd{driverGetter: fakeDriverGetter{d: drv}, minerSecrets: nodeSecretProvider{}} + ack := &captureAcker{} + + r.handleMinerCommand(context.Background(), client, ack, "cmd-1", + withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_DownloadLogs{DownloadLogs: &pb.DownloadLogsAction{BatchLogUuid: "batch-1"}}}), discardLogger(t)) + + got := ack.only(t) + assert.Equal(t, pb.AckCode_ACK_CODE_PARTIAL, got.GetCode()) + assert.False(t, got.GetSucceeded()) + assert.Contains(t, got.GetErrorMessage(), tc.wantErr) + assert.Empty(t, fake.artifactUploads()) + }) + } +} + func TestGetErrorsResultFromSDKRejectsInvalidPluginErrorData(t *testing.T) { validError := func() sdk.DeviceError { return sdk.DeviceError{ diff --git a/server/internal/domain/miner/logformat/logformat.go b/server/internal/domain/miner/logformat/logformat.go index 8cd808804..0347479a4 100644 --- a/server/internal/domain/miner/logformat/logformat.go +++ b/server/internal/domain/miner/logformat/logformat.go @@ -2,6 +2,7 @@ package logformat import ( "fmt" + "io" "strings" ) @@ -44,6 +45,32 @@ func FormatLinesToCSV(logLines []string, includeType bool) []string { return rows } +// WriteTextToCSV converts raw newline-delimited miner logs into CSV rows without +// materializing every formatted row. +func WriteTextToCSV(w io.Writer, logData string, includeType bool) error { + header := csvLogHeaderWithType + if !includeType { + header = csvLogHeaderNoType + } + if _, err := fmt.Fprintln(w, header); err != nil { + return fmt.Errorf("write csv header: %w", err) + } + + remaining := strings.TrimRight(logData, "\n") + for { + line, rest, found := strings.Cut(remaining, "\n") + if strings.TrimSpace(line) != "" { + if _, err := fmt.Fprintln(w, FormatLineToCSVRow(line, includeType)); err != nil { + return fmt.Errorf("write csv row: %w", err) + } + } + if !found { + return nil + } + remaining = rest + } +} + // FormatLineToCSVRow parses a single log line into a CSV row. func FormatLineToCSVRow(line string, includeType bool) string { csvRow := func(ts, logType, message string) string { diff --git a/server/internal/domain/miner/logformat/logformat_test.go b/server/internal/domain/miner/logformat/logformat_test.go new file mode 100644 index 000000000..7fa083e02 --- /dev/null +++ b/server/internal/domain/miner/logformat/logformat_test.go @@ -0,0 +1,39 @@ +package logformat + +import ( + "bytes" + "strings" + "testing" +) + +func TestWriteTextToCSVMatchesRowFormatter(t *testing.T) { + cases := []struct { + name string + logData string + includeType bool + }{ + { + name: "with log levels", + logData: "2024-06-14 16:01:58.470952 | INFO | mcdd::temp | stable\n", + includeType: true, + }, + { + name: "without log levels", + logData: "[2026-01-01T00:00:00Z] line one\n[2026-01-01T00:00:01Z] line two\n", + includeType: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var got bytes.Buffer + if err := WriteTextToCSV(&got, tc.logData, tc.includeType); err != nil { + t.Fatalf("WriteTextToCSV() error = %v", err) + } + want := strings.Join(FormatTextToCSV(tc.logData, tc.includeType), "\n") + "\n" + if got.String() != want { + t.Fatalf("WriteTextToCSV() = %q, want %q", got.String(), want) + } + }) + } +} From 92d50541bb7a26092f89080dbc7c2691b3721c57 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 11:24:08 -0700 Subject: [PATCH 04/15] fix: enforce miner log artifact boundaries --- server/cmd/fleetnode/minercommand.go | 1 - server/cmd/fleetnode/minercommand_test.go | 5 +-- .../domain/fleetnode/control/registry.go | 6 ++++ .../domain/fleetnode/control/stream.go | 3 ++ .../domain/miner/logformat/logformat.go | 17 ++++++++- .../domain/miner/logformat/logformat_test.go | 36 +++++++++++++++++++ .../internal/domain/miner/remotenode/miner.go | 2 ++ .../domain/miner/remotenode/miner_test.go | 2 ++ .../handlers/fleetnode/gateway/handler.go | 3 ++ .../gateway/handler_artifact_test.go | 32 +++++++++++++++++ .../internal/infrastructure/files/service.go | 9 +++++ .../infrastructure/files/service_test.go | 17 +++++++++ 12 files changed, 129 insertions(+), 4 deletions(-) diff --git a/server/cmd/fleetnode/minercommand.go b/server/cmd/fleetnode/minercommand.go index c5864797d..66d0db01d 100644 --- a/server/cmd/fleetnode/minercommand.go +++ b/server/cmd/fleetnode/minercommand.go @@ -43,7 +43,6 @@ const ( maxGetErrorsReports = 512 minerLogsArtifactFilename = "miner-logs.csv" commandArtifactChunkSize = 1 << 20 - maxMinerLogsArtifactBytes = 4 * 1024 * 1024 ) // driverGetter is the plugin-manager seam the executor needs; *plugins.Manager satisfies it. diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index 1b9600e43..11ef6e6b4 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -28,6 +28,7 @@ import ( "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1/fleetnodegatewayv1connect" minercommandpb "github.com/block/proto-fleet/server/generated/grpc/minercommand/v1" "github.com/block/proto-fleet/server/internal/domain/fleetnode/passwordupdate" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" minermodels "github.com/block/proto-fleet/server/internal/domain/miner/models" sdk "github.com/block/proto-fleet/server/sdk/v1" sdkerrors "github.com/block/proto-fleet/server/sdk/v1/errors" @@ -689,12 +690,12 @@ func TestHandleMinerCommand_DownloadLogsRejectsOversizedLogs(t *testing.T) { }{ { name: "raw log exceeds limit", - logData: strings.Repeat("x", maxMinerLogsArtifactBytes+1), + logData: strings.Repeat("x", int(logformat.MaxArtifactBytes)+1), wantErr: "log data exceeds", }, { name: "formatted artifact exceeds limit", - logData: strings.Repeat(`"`, maxMinerLogsArtifactBytes/2+1), + logData: strings.Repeat(`"`, int(logformat.MaxArtifactBytes)/2+1), wantErr: "log artifact exceeds", }, } diff --git a/server/internal/domain/fleetnode/control/registry.go b/server/internal/domain/fleetnode/control/registry.go index 2da5cd117..2beaba249 100644 --- a/server/internal/domain/fleetnode/control/registry.go +++ b/server/internal/domain/fleetnode/control/registry.go @@ -93,6 +93,10 @@ var ( // retry attempts for one artifact expectation. ErrArtifactTransferAttemptsExceeded = errors.New("artifact transfer attempts exceeded for command") + // ErrArtifactTooLarge: an artifact transfer exceeds the in-flight command's + // purpose-specific size expectation. + ErrArtifactTooLarge = errors.New("artifact transfer exceeds command size expectation") + // errDuplicateCommandID: a command_id is already in flight for the fleet_node. // id.GenerateID() makes this practically impossible; callers map it to Internal. errDuplicateCommandID = errors.New("duplicate command_id in flight for fleet_node") @@ -146,6 +150,8 @@ type ArtifactExpectation struct { Purpose gatewaypb.CommandArtifactPurpose ArtifactID string DeviceIdentifier string + SizeBytes int64 + MaxSizeBytes int64 } type artifactExpectation struct { diff --git a/server/internal/domain/fleetnode/control/stream.go b/server/internal/domain/fleetnode/control/stream.go index becc9805e..9dabcdbaf 100644 --- a/server/internal/domain/fleetnode/control/stream.go +++ b/server/internal/domain/fleetnode/control/stream.go @@ -153,6 +153,9 @@ func (r *Registry) AdmitCommandArtifactTransfer(fleetNodeID int64, commandID str if exp == nil { return nil, ErrArtifactNotExpected } + if exp.MaxSizeBytes > 0 && want.SizeBytes > exp.MaxSizeBytes { + return nil, ErrArtifactTooLarge + } if exp.inProgress || exp.completed { return nil, ErrArtifactAlreadyTransferred } diff --git a/server/internal/domain/miner/logformat/logformat.go b/server/internal/domain/miner/logformat/logformat.go index 0347479a4..36b22ac22 100644 --- a/server/internal/domain/miner/logformat/logformat.go +++ b/server/internal/domain/miner/logformat/logformat.go @@ -8,6 +8,7 @@ import ( const csvLogHeaderWithType = "Time,Type,Message" const csvLogHeaderNoType = "Time,Message" +const MaxArtifactBytes int64 = 4 * 1024 * 1024 // logLevelSeparators maps Proto miner log-level separators to their display labels. // Format: "{prefix}: {timestamp} | LEVEL | {message}" @@ -74,7 +75,9 @@ func WriteTextToCSV(w io.Writer, logData string, includeType bool) error { // FormatLineToCSVRow parses a single log line into a CSV row. func FormatLineToCSVRow(line string, includeType bool) string { csvRow := func(ts, logType, message string) string { - esc := func(s string) string { return strings.ReplaceAll(s, `"`, `""`) } + esc := func(s string) string { + return strings.ReplaceAll(neutralizeCSVFormula(s), `"`, `""`) + } if includeType { return fmt.Sprintf(`"%s","%s","%s"`, esc(ts), esc(logType), esc(message)) } @@ -123,3 +126,15 @@ func FormatLineToCSVRow(line string, includeType bool) string { return csvRow("", "", line) } + +func neutralizeCSVFormula(s string) string { + if s == "" { + return s + } + switch s[0] { + case '=', '+', '-', '@': + return "'" + s + default: + return s + } +} diff --git a/server/internal/domain/miner/logformat/logformat_test.go b/server/internal/domain/miner/logformat/logformat_test.go index 7fa083e02..4fc73002f 100644 --- a/server/internal/domain/miner/logformat/logformat_test.go +++ b/server/internal/domain/miner/logformat/logformat_test.go @@ -37,3 +37,39 @@ func TestWriteTextToCSVMatchesRowFormatter(t *testing.T) { }) } } + +func TestFormatLineToCSVRowNeutralizesFormulaCells(t *testing.T) { + tests := []struct { + name string + line string + includeType bool + want string + }{ + { + name: "message formula without timestamp", + line: `=HYPERLINK("https://example.invalid","open")`, + includeType: false, + want: `"","'=HYPERLINK(""https://example.invalid"",""open"")"`, + }, + { + name: "typed message formula", + line: "2024-06-14 16:01:58.470952 | INFO | +cmd", + includeType: true, + want: `"2024-06-14 16:01:58","INFO","'+cmd"`, + }, + { + name: "timestamp formula", + line: "-2026-01-01T00:00:00Z message", + includeType: false, + want: `"","'-2026-01-01T00:00:00Z message"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FormatLineToCSVRow(tt.line, tt.includeType); got != tt.want { + t.Fatalf("FormatLineToCSVRow() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index 3ecd9b055..4be822aae 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -28,6 +28,7 @@ import ( "github.com/block/proto-fleet/server/internal/domain/fleetnode/control" "github.com/block/proto-fleet/server/internal/domain/miner/dto" "github.com/block/proto-fleet/server/internal/domain/miner/interfaces" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" minermodels "github.com/block/proto-fleet/server/internal/domain/miner/models" "github.com/block/proto-fleet/server/internal/domain/sv2" modelsV2 "github.com/block/proto-fleet/server/internal/domain/telemetry/models/v2" @@ -391,6 +392,7 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { Direction: control.ArtifactDirectionUpload, Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, DeviceIdentifier: m.desc.GetDeviceIdentifier(), + MaxSizeBytes: logformat.MaxArtifactBytes, }}) if err != nil { return err diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index 3e43174a7..f42a2e58e 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -24,6 +24,7 @@ import ( "github.com/block/proto-fleet/server/internal/domain/fleetnode/control" "github.com/block/proto-fleet/server/internal/domain/fleetnode/passwordupdate" "github.com/block/proto-fleet/server/internal/domain/miner/dto" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" sdk "github.com/block/proto-fleet/server/sdk/v1" ) @@ -867,6 +868,7 @@ func TestMiner_DownloadLogsSendsActionAndMaterializesArtifact(t *testing.T) { assert.Equal(t, control.ArtifactDirectionUpload, s.artifacts[0].Direction) assert.Equal(t, gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, s.artifacts[0].Purpose) assert.Equal(t, "dev-1", s.artifacts[0].DeviceIdentifier) + assert.Equal(t, logformat.MaxArtifactBytes, s.artifacts[0].MaxSizeBytes) assert.Equal(t, "batch-1", saver.batchLogUUID) assert.Equal(t, "AA:BB:CC:DD:EE:FF", saver.macAddress) assert.Equal(t, "artifact-1", saver.artifactID) diff --git a/server/internal/handlers/fleetnode/gateway/handler.go b/server/internal/handlers/fleetnode/gateway/handler.go index 68b09c92e..babc2987e 100644 --- a/server/internal/handlers/fleetnode/gateway/handler.go +++ b/server/internal/handlers/fleetnode/gateway/handler.go @@ -150,6 +150,7 @@ func (h *Handler) UploadCommandArtifact(ctx context.Context, stream *connect.Cli Direction: control.ArtifactDirectionUpload, Purpose: header.GetPurpose(), DeviceIdentifier: header.GetDeviceIdentifier(), + SizeBytes: header.GetSizeBytes(), } commandDone, err := h.registry.AdmitCommandArtifactTransfer(subject.FleetNodeID, header.GetCommandId(), expectation) if err != nil { @@ -448,6 +449,8 @@ func mapArtifactAdmissionError(err error) error { return connect.NewError(connect.CodeAlreadyExists, err) case errors.Is(err, control.ErrArtifactTransferLimitExceeded): return connect.NewError(connect.CodeResourceExhausted, err) + case errors.Is(err, control.ErrArtifactTooLarge): + return connect.NewError(connect.CodeResourceExhausted, err) case errors.Is(err, control.ErrArtifactTransferAttemptsExceeded): return connect.NewError(connect.CodeResourceExhausted, err) case errors.Is(err, control.ErrArtifactNotExpected): diff --git a/server/internal/handlers/fleetnode/gateway/handler_artifact_test.go b/server/internal/handlers/fleetnode/gateway/handler_artifact_test.go index 908af094e..478a77b4a 100644 --- a/server/internal/handlers/fleetnode/gateway/handler_artifact_test.go +++ b/server/internal/handlers/fleetnode/gateway/handler_artifact_test.go @@ -15,6 +15,7 @@ import ( pb "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1" "github.com/block/proto-fleet/server/generated/grpc/fleetnodegateway/v1/fleetnodegatewayv1connect" "github.com/block/proto-fleet/server/internal/domain/fleetnode/control" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" "github.com/block/proto-fleet/server/internal/handlers/fleetnode/gateway" "github.com/block/proto-fleet/server/internal/infrastructure/files" ) @@ -43,6 +44,7 @@ func uploadExpectation() control.ArtifactExpectation { Direction: control.ArtifactDirectionUpload, Purpose: pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, DeviceIdentifier: "miner-a", + MaxSizeBytes: logformat.MaxArtifactBytes, } } @@ -68,6 +70,12 @@ func uploadHeaderRequest(commandID string, payload []byte) *pb.UploadCommandArti }} } +func oversizedUploadHeaderRequest(commandID string) *pb.UploadCommandArtifactRequest { + req := uploadHeaderRequest(commandID, []byte("x")) + req.GetHeader().SizeBytes = logformat.MaxArtifactBytes + 1 + return req +} + func uploadChunkRequest(payload []byte) *pb.UploadCommandArtifactRequest { return &pb.UploadCommandArtifactRequest{Part: &pb.UploadCommandArtifactRequest_Chunk{ Chunk: &pb.CommandArtifactChunk{Data: payload}, @@ -279,3 +287,27 @@ func TestCommandArtifactUploadReadLimitRejectsOversizedMessageBeforeChunkReader( finishAckOnlyCommand(t, uploadStream, commandID, uploadDone) } + +func TestCommandArtifactUploadRejectsMinerLogsOverExpectedSize(t *testing.T) { + h, client := newArtifactTestClient(t) + commandID := "oversized-miner-log-artifact-command" + uploadStream, uploadDone := startAckOnlyCommandWithArtifacts(t, h, commandID, []control.ArtifactExpectation{uploadExpectation()}) + + oversized := client.UploadCommandArtifact(context.Background()) + err := oversized.Send(oversizedUploadHeaderRequest(commandID)) + if err == nil { + _, err = oversized.CloseAndReceive() + } + require.Error(t, err) + assert.Equal(t, connect.CodeResourceExhausted, connect.CodeOf(err)) + + payload := []byte("bounded miner logs") + retry := client.UploadCommandArtifact(context.Background()) + require.NoError(t, retry.Send(uploadHeaderRequest(commandID, payload))) + require.NoError(t, retry.Send(uploadChunkRequest(payload))) + uploadResp, err := retry.CloseAndReceive() + require.NoError(t, err) + require.NotNil(t, uploadResp.Msg.GetArtifact()) + + finishAckOnlyCommand(t, uploadStream, commandID, uploadDone) +} diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 16231dd8f..08f8d1fdc 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -16,7 +16,10 @@ import ( "sync" "time" + "connectrpc.com/connect" + "github.com/block/proto-fleet/server/internal/domain/fleeterror" + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" ) const logsDir = "logs" @@ -225,6 +228,12 @@ func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, return "", err } defer reader.Close() + if info.Size > logformat.MaxArtifactBytes { + return "", fleeterror.NewPlainError( + fmt.Sprintf("miner log artifact too large: %d bytes (max: %d bytes)", info.Size, logformat.MaxArtifactBytes), + connect.CodeResourceExhausted, + ) + } filePath, file, err := s.openBatchLogFile(batchLogUUID, macAddress) if err != nil { diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index bdc2b19c3..bdf0f5601 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -2,6 +2,7 @@ package files import ( "archive/zip" + "bytes" "io" "os" "path/filepath" @@ -10,6 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/block/proto-fleet/server/internal/domain/miner/logformat" ) // setupService creates a Service backed by a temporary directory and restores the @@ -284,6 +287,20 @@ func TestSaveCommandArtifactLog_RejectsMissingAndCorruptArtifacts(t *testing.T) }) } +func TestSaveCommandArtifactLog_RejectsOversizedMinerLogs(t *testing.T) { + svc := setupService(t) + content := bytes.Repeat([]byte("x"), int(logformat.MaxArtifactBytes)+1) + info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(content)), checksumOf(string(content)), bytes.NewReader(content)) + require.NoError(t, err) + + filePath, err := svc.SaveCommandArtifactLog("batch-oversized-artifact", "aa:bb:cc:dd:ee:ff", info.ID) + + require.Error(t, err) + assert.Contains(t, err.Error(), "miner log artifact too large") + assert.Empty(t, filePath) + assert.NoDirExists(t, getBatchLogsDirPath("batch-oversized-artifact")) +} + func readZipFileContents(t *testing.T, zipPath string) map[string]string { t.Helper() zr, err := zip.OpenReader(zipPath) From 34b98bb0f010a335b5f2be3c29476e79a0bfcd41 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 11:42:46 -0700 Subject: [PATCH 05/15] fix: format generated fleetnode gateway proto outputs --- .../v1/fleetnodegateway_pb.ts | 587 ++++++++++-------- .../v1/fleetnodegateway.pb.go | 7 +- 2 files changed, 343 insertions(+), 251 deletions(-) diff --git a/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts b/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts index cfcfbb168..63e6198f0 100644 --- a/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts +++ b/client/src/protoFleet/api/generated/fleetnodegateway/v1/fleetnodegateway_pb.ts @@ -30,8 +30,21 @@ import type { Message } from "@bufbuild/protobuf"; /** * Describes the file fleetnodegateway/v1/fleetnodegateway.proto. */ -export const file_fleetnodegateway_v1_fleetnodegateway: GenFile = /*@__PURE__*/ - fileDesc("CipmbGVldG5vZGVnYXRld2F5L3YxL2ZsZWV0bm9kZWdhdGV3YXkucHJvdG8SE2ZsZWV0bm9kZWdhdGV3YXkudjEilwEKD1JlZ2lzdGVyUmVxdWVzdBIkChBlbnJvbGxtZW50X3Rva2VuGAEgASgJQgq6SAdyBRAUGIAEEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESIAoPaWRlbnRpdHlfcHVia2V5GAMgASgMQge6SAR6AmggEiIKEWVuY3J5cHRpb25fcHVia2V5GAQgASgMQge6SAR6AmggIokBChBSZWdpc3RlclJlc3BvbnNlEhUKDWZsZWV0X25vZGVfaWQYASABKAMSQAoRZW5yb2xsbWVudF9zdGF0dXMYAiABKA4yJS5mbGVldG5vZGVnYXRld2F5LnYxLkVucm9sbG1lbnRTdGF0dXMSHAoUaWRlbnRpdHlfZmluZ2VycHJpbnQYAyABKAkiWgoZQmVnaW5BdXRoSGFuZHNoYWtlUmVxdWVzdBIbCgdhcGlfa2V5GAEgASgJQgq6SAdyBRAUGIAEEiAKD2lkZW50aXR5X3B1YmtleRgCIAEoDEIHukgEegJoICJfChpCZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRIRCgljaGFsbGVuZ2UYASABKAwSLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWAocQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVxdWVzdBIcCgljaGFsbGVuZ2UYASABKAxCCbpIBnoEEBAYQBIaCglzaWduYXR1cmUYAiABKAxCB7pIBHoCaEAiZgodQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVzcG9uc2USFQoNc2Vzc2lvbl90b2tlbhgBIAEoCRIuCgpleHBpcmVzX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJtChZVcGxvYWRUZWxlbWV0cnlSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIxChdVcGxvYWRUZWxlbWV0cnlSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJqChNVcGxvYWRFdmVudHNSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIuChRVcGxvYWRFdmVudHNSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJNChZVcGxvYWRIZWFydGJlYXRSZXF1ZXN0EjMKB3NlbnRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEiSgoXVXBsb2FkSGVhcnRiZWF0UmVzcG9uc2USLwoLcmVjZWl2ZWRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIuQBChJDb21tYW5kQXJ0aWZhY3RSZWYSHwoLYXJ0aWZhY3RfaWQYASABKAlCCrpIB3IFEAEYgAESSAoHcHVycG9zZRgCIAEoDjIrLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UHVycG9zZUIKukgHggEEEAEgABIcCghmaWxlbmFtZRgDIAEoCUIKukgHcgUQARj/ARIbCgpzaXplX2J5dGVzGAQgASgDQge6SAQiAiAAEigKBnNoYTI1NhgFIAEoCUIYukgVchMyDl5bYS1mMC05XXs2NH0kmAFAIpECChtDb21tYW5kQXJ0aWZhY3RVcGxvYWRIZWFkZXISHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJICgdwdXJwb3NlGAIgASgOMisuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RQdXJwb3NlQgq6SAeCAQQQASAAEhwKCGZpbGVuYW1lGAMgASgJQgq6SAdyBRABGP8BEhsKCnNpemVfYnl0ZXMYBCABKANCB7pIBCICIAASKAoGc2hhMjU2GAUgASgJQhi6SBVyEzIOXlthLWYwLTldezY0fSSYAUASIwoRZGV2aWNlX2lkZW50aWZpZXIYBiABKAlCCLpIBXIDGP8BIjEKFENvbW1hbmRBcnRpZmFjdENodW5rEhkKBGRhdGEYASABKAxCC7pICHoGEAEYgIBAIq0BChxVcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0EkIKBmhlYWRlchgBIAEoCzIwLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0VXBsb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiYgodVXBsb2FkQ29tbWFuZEFydGlmYWN0UmVzcG9uc2USQQoIYXJ0aWZhY3QYASABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdFJlZkIGukgDyAEBIqgBCh5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJBCghhcnRpZmFjdBgCIAEoCzInLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UmVmQga6SAPIAQESIwoRZGV2aWNlX2lkZW50aWZpZXIYAyABKAlCCLpIBXIDGP8BImIKHUNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVyEkEKCGFydGlmYWN0GAEgASgLMicuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RSZWZCBrpIA8gBASKyAQofRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZRJECgZoZWFkZXIYASABKAsyMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiiQEKHlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVxdWVzdBJHCgdkZXZpY2VzGAEgAygLMisuZmxlZXRub2RlZ2F0ZXdheS52MS5EaXNjb3ZlcmVkRGV2aWNlUmVwb3J0Qgm6SAaSAQMQgAgSHgoKY29tbWFuZF9pZBgCIAEoCUIKukgHcgUQARiAASKDAwoWRGlzY292ZXJlZERldmljZVJlcG9ydBIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIfCgppcF9hZGRyZXNzGAIgASgJQgu6SAhyBhABGC1wARKGAQoEcG9ydBgDIAEoCUJ4ukh1ugFsCgpwb3J0LnJhbmdlEilwb3J0IG11c3QgYmUgYSBkZWNpbWFsIG51bWJlciBpbiAxLi42NTUzNRozdGhpcy5tYXRjaGVzKCdeWzEtOV1bMC05XSokJykgJiYgaW50KHRoaXMpIDw9IDY1NTM1cgQQARgFEhsKCnVybF9zY2hlbWUYBCABKAlCB7pIBHICGCASHgoLZHJpdmVyX25hbWUYBSABKAlCCbpIBnIEEAEYMhIXCgVtb2RlbBgGIAEoCUIIukgFcgMY/wESHgoMbWFudWZhY3R1cmVyGAcgASgJQgi6SAVyAxj/ARIiChBmaXJtd2FyZV92ZXJzaW9uGAggASgJQgi6SAVyAxj/ASJRCh9SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIvoDChNGbGVldE5vZGVQYWlyUmVzdWx0EiUKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQgq6SAdyBRABGP8BEjEKB291dGNvbWUYAiABKA4yIC5mbGVldG5vZGVnYXRld2F5LnYxLlBhaXJPdXRjb21lEh8KDXNlcmlhbF9udW1iZXIYAyABKAlCCLpIBXIDGP8BEhwKC21hY19hZGRyZXNzGAQgASgJQge6SARyAhhAEhcKBW1vZGVsGAUgASgJQgi6SAVyAxj/ARIeCgxtYW51ZmFjdHVyZXIYBiABKAlCCLpIBXIDGP8BEiIKEGZpcm13YXJlX3ZlcnNpb24YByABKAlCCLpIBXIDGP8BEh8KDWVycm9yX21lc3NhZ2UYCCABKAlCCLpIBXIDGIAgEiQKF2RlZmF1bHRfcGFzc3dvcmRfYWN0aXZlGAwgASgISACIAQESSAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGA0gASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IaChhfZGVmYXVsdF9wYXNzd29yZF9hY3RpdmVKBAgJEApKBAgKEAtKBAgLEAxSDXVzZWRfdXNlcm5hbWVSDXVzZWRfcGFzc3dvcmRSEHVzZWRfY3JlZGVudGlhbHMiTgoURW5jcnlwdGVkQ3JlZGVudGlhbHMSGgoIdXNlcm5hbWUYASABKAxCCLpIBXoDGIAgEhoKCHBhc3N3b3JkGAIgASgMQgi6SAV6AxiAICKCAQoaUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJECgdyZXN1bHRzGAIgAygLMiguZmxlZXRub2RlZ2F0ZXdheS52MS5GbGVldE5vZGVQYWlyUmVzdWx0Qgm6SAaSAQMQgAgiTQobUmVwb3J0UGFpcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIokBChRDb250cm9sU3RyZWFtUmVxdWVzdBIyCgVoZWxsbxgBIAEoCzIhLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEhlbGxvSAASLgoDYWNrGAIgASgLMh8uZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sQWNrSABCDQoEa2luZBIFukgCCAEimAEKFUNvbnRyb2xTdHJlYW1SZXNwb25zZRI4CghhY2NlcHRlZBgBIAEoCzIkLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEFjY2VwdGVkSAASNgoHY29tbWFuZBgCIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbENvbW1hbmRIAEINCgRraW5kEgW6SAIIASIOCgxDb250cm9sSGVsbG8iQgoPQ29udHJvbEFjY2VwdGVkEi8KC3NlcnZlcl90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJMCg5Db250cm9sQ29tbWFuZBIeCgpjb21tYW5kX2lkGAEgASgJQgq6SAdyBRABGIABEhoKB3BheWxvYWQYAiABKAxCCbpIBnoEGICAQCK3AwoZTWluZXJDb25uZWN0aW9uRGVzY3JpcHRvchIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIeCgtkcml2ZXJfbmFtZRgCIAEoCUIJukgGcgQQARgyEh8KCmlwX2FkZHJlc3MYAyABKAlCC7pICHIGEAEYLXABEoYBCgRwb3J0GAQgASgJQni6SHW6AWwKCnBvcnQucmFuZ2USKXBvcnQgbXVzdCBiZSBhIGRlY2ltYWwgbnVtYmVyIGluIDEuLjY1NTM1GjN0aGlzLm1hdGNoZXMoJ15bMS05XVswLTldKiQnKSAmJiBpbnQodGhpcykgPD0gNjU1MzVyBBABGAUSGwoKdXJsX3NjaGVtZRgFIAEoCUIHukgEcgIYIBIfCg1zZXJpYWxfbnVtYmVyGAYgASgJQgi6SAVyAxj/ARIdCgttYWNfYWRkcmVzcxgHIAEoCUIIukgFcgMY/wESJQoTY3JlZGVudGlhbF91c2VybmFtZRgIIAEoDEIIukgFegMYgCASJQoTY3JlZGVudGlhbF9wYXNzd29yZBgJIAEoDEIIukgFegMYgCAitwcKDE1pbmVyQ29tbWFuZBJGCgZ0YXJnZXQYASABKAsyLi5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29ubmVjdGlvbkRlc2NyaXB0b3JCBrpIA8gBARIzCgZyZWJvb3QYAiABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLlJlYm9vdEFjdGlvbkgAEj4KDHN0YXJ0X21pbmluZxgDIAEoCzImLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RhcnRNaW5pbmdBY3Rpb25IABI8CgtzdG9wX21pbmluZxgEIAEoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RvcE1pbmluZ0FjdGlvbkgAEjgKCWJsaW5rX2xlZBgFIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQmxpbmtMZWRBY3Rpb25IABI1CgdjdXJ0YWlsGAYgASgLMiIuZmxlZXRub2RlZ2F0ZXdheS52MS5DdXJ0YWlsQWN0aW9uSAASOQoJdW5jdXJ0YWlsGAcgASgLMiQuZmxlZXRub2RlZ2F0ZXdheS52MS5VbmN1cnRhaWxBY3Rpb25IABJFChBzZXRfY29vbGluZ19tb2RlGAggASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5TZXRDb29saW5nTW9kZUFjdGlvbkgAEkUKEHNldF9wb3dlcl90YXJnZXQYCSABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLlNldFBvd2VyVGFyZ2V0QWN0aW9uSAASSwoTdXBkYXRlX21pbmluZ19wb29scxgKIAEoCzIsLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluaW5nUG9vbHNBY3Rpb25IABJFChBnZXRfbWluaW5nX3Bvb2xzGAsgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5HZXRNaW5pbmdQb29sc0FjdGlvbkgAEjoKCmdldF9lcnJvcnMYDCABKAsyJC5mbGVldG5vZGVnYXRld2F5LnYxLkdldEVycm9yc0FjdGlvbkgAEk8KFXVwZGF0ZV9taW5lcl9wYXNzd29yZBgNIAEoCzIuLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbkgAEkAKDWRvd25sb2FkX2xvZ3MYDiABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkRvd25sb2FkTG9nc0FjdGlvbkgAQg8KBmFjdGlvbhIFukgCCAEiDgoMUmVib290QWN0aW9uIhMKEVN0YXJ0TWluaW5nQWN0aW9uIhIKEFN0b3BNaW5pbmdBY3Rpb24iEAoOQmxpbmtMZWRBY3Rpb24iEQoPVW5jdXJ0YWlsQWN0aW9uIhYKFEdldE1pbmluZ1Bvb2xzQWN0aW9uIhEKD0dldEVycm9yc0FjdGlvbiI4ChJEb3dubG9hZExvZ3NBY3Rpb24SIgoOYmF0Y2hfbG9nX3V1aWQYASABKAlCCrpIB3IFEAEYgAEimgIKFE5vZGVFbmNyeXB0ZWRQYXlsb2FkEqYBCglhbGdvcml0aG0YASABKAlCkgG6SI4BugGEAQogbm9kZV9lbmNyeXB0ZWRfcGF5bG9hZC5hbGdvcml0aG0SM2FsZ29yaXRobSBtdXN0IGJlIHgyNTUxOS1oa2RmLXNoYTI1Ni1hZXMtMjU2LWdjbS12MRordGhpcyA9PSAneDI1NTE5LWhrZGYtc2hhMjU2LWFlcy0yNTYtZ2NtLXYxJ3IEEAEYQBIhChBlcGhlbWVyYWxfcHVia2V5GAIgASgMQge6SAR6AmggEhYKBW5vbmNlGAMgASgMQge6SAR6AmgMEh4KCmNpcGhlcnRleHQYBCABKAxCCrpIB3oFEBEYgEAicQoZVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbhJUChllbmNyeXB0ZWRfcGFzc3dvcmRfdXBkYXRlGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5Ob2RlRW5jcnlwdGVkUGF5bG9hZEIGukgDyAEBIm0KGVVwZGF0ZU1pbmVyUGFzc3dvcmRSZXN1bHQSUAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IGukgDyAEBIkAKDUN1cnRhaWxBY3Rpb24SLwoFbGV2ZWwYASABKA4yIC5jdXJ0YWlsbWVudC52MS5DdXJ0YWlsbWVudExldmVsIjwKFFNldENvb2xpbmdNb2RlQWN0aW9uEiQKBG1vZGUYASABKA4yFi5jb21tb24udjEuQ29vbGluZ01vZGUiUgoUU2V0UG93ZXJUYXJnZXRBY3Rpb24SOgoQcGVyZm9ybWFuY2VfbW9kZRgBIAEoDjIgLm1pbmVyY29tbWFuZC52MS5QZXJmb3JtYW5jZU1vZGUihQMKEE1pbmluZ1Bvb2xDb25maWcSGwoIcHJpb3JpdHkYASABKAVCCbpIBhoEGAIoABK3AgoDdXJsGAIgASgJQqkCukilAnKiAhAMGIACMpoCXihzdHJhdHVtXCsodGNwfHNzbHx3cyk6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKSg6XGR7MSw1fSk/fHN0cmF0dW0yXCt0Y3A6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKTpcZHsxLDV9XC9bQS1aYS16MC05XytcLz06Li1dKykkEhoKCHVzZXJuYW1lGAMgASgJQgi6SAVyAxiABCJbChdVcGRhdGVNaW5pbmdQb29sc0FjdGlvbhJACgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IKukgHkgEECAEQAyJWChRHZXRNaW5pbmdQb29sc1Jlc3VsdBI+CgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IIukgFkgECEAMixQUKEE1pbmVyRXJyb3JSZXBvcnQSNAoLbWluZXJfZXJyb3IYASABKA4yFS5lcnJvcnMudjEuTWluZXJFcnJvckIIukgFggECEAESHwoNY2F1c2Vfc3VtbWFyeRgCIAEoCUIIukgFcgMYgCASJAoScmVjb21tZW5kZWRfYWN0aW9uGAMgASgJQgi6SAVyAxiAIBIvCghzZXZlcml0eRgEIAEoDjITLmVycm9ycy52MS5TZXZlcml0eUIIukgFggECEAESMQoNZmlyc3Rfc2Vlbl9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMAoMbGFzdF9zZWVuX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBItCgljbG9zZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEnAKEXZlbmRvcl9hdHRyaWJ1dGVzGAggAygLMjsuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5lckVycm9yUmVwb3J0LlZlbmRvckF0dHJpYnV0ZXNFbnRyeUIYukgVmgESECAiB3IFEAEYgAEqBXIDGIAIEh0KCWRldmljZV9pZBgJIAEoCUIKukgHcgUQARj/ARIjCgxjb21wb25lbnRfaWQYCiABKAlCCLpIBXIDGP8BSACIAQESGAoGaW1wYWN0GAsgASgJQgi6SAVyAxiAIBIZCgdzdW1tYXJ5GAwgASgJQgi6SAVyAxiAIBI6Cg5jb21wb25lbnRfdHlwZRgNIAEoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlQgi6SAWCAQIQARo3ChVWZW5kb3JBdHRyaWJ1dGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIPCg1fY29tcG9uZW50X2lkIqMBCg9HZXRFcnJvcnNSZXN1bHQSHQoJZGV2aWNlX2lkGAEgASgJQgq6SAdyBRABGP8BEkAKBmVycm9ycxgCIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluZXJFcnJvclJlcG9ydEIJukgGkgEDEIAEEhEKCXRydW5jYXRlZBgDIAEoCBIcChRvbWl0dGVkX3JlcG9ydF9jb3VudBgEIAEoDSL9AQoMQWdlbnRDb21tYW5kEi8KCGRpc2NvdmVyGAEgASgLMhsucGFpcmluZy52MS5EaXNjb3ZlclJlcXVlc3RIABIwCgRwYWlyGAIgASgLMiAucGFpcmluZy52MS5GbGVldE5vZGVQYWlyUmVxdWVzdEgAEjoKDW1pbmVyX2NvbW1hbmQYAyABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29tbWFuZEgAEjwKCXRlbGVtZXRyeRgEIAEoCzInLnRlbGVtZXRyeS52MS5GbGVldE5vZGVUZWxlbWV0cnlSZXF1ZXN0SABCEAoHY29tbWFuZBIFukgCCAEiqAEKCkNvbnRyb2xBY2sSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARIRCglzdWNjZWVkZWQYAiABKAgSHwoNZXJyb3JfbWVzc2FnZRgDIAEoCUIIukgFcgMYgCASKgoEY29kZRgEIAEoDjIcLmZsZWV0bm9kZWdhdGV3YXkudjEuQWNrQ29kZRIaCgdwYXlsb2FkGAUgASgMQgm6SAZ6BBiAgEAqlAEKEEVucm9sbG1lbnRTdGF0dXMSIQodRU5ST0xMTUVOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIdChlFTlJPTExNRU5UX1NUQVRVU19QRU5ESU5HEAESHwobRU5ST0xMTUVOVF9TVEFUVVNfQ09ORklSTUVEEAISHQoZRU5ST0xMTUVOVF9TVEFUVVNfUkVWT0tFRBADKpoBChZDb21tYW5kQXJ0aWZhY3RQdXJwb3NlEigKJENPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9VTlNQRUNJRklFRBAAEicKI0NPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9NSU5FUl9MT0dTEAESLQopQ09NTUFORF9BUlRJRkFDVF9QVVJQT1NFX0ZJUk1XQVJFX1BBWUxPQUQQAiqYAQoLUGFpck91dGNvbWUSHAoYUEFJUl9PVVRDT01FX1VOU1BFQ0lGSUVEEAASFwoTUEFJUl9PVVRDT01FX1BBSVJFRBABEhwKGFBBSVJfT1VUQ09NRV9BVVRIX05FRURFRBACEhwKGFBBSVJfT1VUQ09NRV9BVVRIX0ZBSUxFRBADEhYKElBBSVJfT1VUQ09NRV9FUlJPUhAEKrQCCgdBY2tDb2RlEhgKFEFDS19DT0RFX1VOU1BFQ0lGSUVEEAASDwoLQUNLX0NPREVfT0sQARIUChBBQ0tfQ09ERV9QQVJUSUFMEAISGAoUQUNLX0NPREVfQkFEX1JFUVVFU1QQAxIcChhBQ0tfQ09ERV9BR0VOVF9JTkNBUEFCTEUQBBIYChRBQ0tfQ09ERV9TQ0FOX0ZBSUxFRBAFEhoKFkFDS19DT0RFX1JFUE9SVF9GQUlMRUQQBhIVChFBQ0tfQ09ERV9JTlRFUk5BTBAHEhEKDUFDS19DT0RFX0JVU1kQCBIcChhBQ0tfQ09ERV9VTkFVVEhFTlRJQ0FURUQQCRIaChZBQ0tfQ09ERV9VTklNUExFTUVOVEVEEAoSFgoSQUNLX0NPREVfRk9SQklEREVOEAsypwoKF0ZsZWV0Tm9kZUdhdGV3YXlTZXJ2aWNlElcKCFJlZ2lzdGVyEiQuZmxlZXRub2RlZ2F0ZXdheS52MS5SZWdpc3RlclJlcXVlc3QaJS5mbGVldG5vZGVnYXRld2F5LnYxLlJlZ2lzdGVyUmVzcG9uc2USdQoSQmVnaW5BdXRoSGFuZHNoYWtlEi4uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXF1ZXN0Gi8uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRJ+ChVDb21wbGV0ZUF1dGhIYW5kc2hha2USMS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlcXVlc3QaMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlc3BvbnNlEm4KD1VwbG9hZFRlbGVtZXRyeRIrLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVxdWVzdBosLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVzcG9uc2UoARJlCgxVcGxvYWRFdmVudHMSKC5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1JlcXVlc3QaKS5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1Jlc3BvbnNlKAESbAoPVXBsb2FkSGVhcnRiZWF0EisuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXF1ZXN0GiwuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXNwb25zZRKAAQoVVXBsb2FkQ29tbWFuZEFydGlmYWN0EjEuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjIuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZSgBEoYBChdEb3dubG9hZENvbW1hbmRBcnRpZmFjdBIzLmZsZWV0bm9kZWdhdGV3YXkudjEuRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjQuZmxlZXRub2RlZ2F0ZXdheS52MS5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlc3BvbnNlMAEShAEKF1JlcG9ydERpc2NvdmVyZWREZXZpY2VzEjMuZmxlZXRub2RlZ2F0ZXdheS52MS5SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1JlcXVlc3QaNC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVzcG9uc2USeAoTUmVwb3J0UGFpcmVkRGV2aWNlcxIvLmZsZWV0bm9kZWdhdGV3YXkudjEuUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QaMC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydFBhaXJlZERldmljZXNSZXNwb25zZRJqCg1Db250cm9sU3RyZWFtEikuZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sU3RyZWFtUmVxdWVzdBoqLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbFN0cmVhbVJlc3BvbnNlKAEwAUL4AQoXY29tLmZsZWV0bm9kZWdhdGV3YXkudjFCFUZsZWV0bm9kZWdhdGV3YXlQcm90b1ABWllnaXRodWIuY29tL2Jsb2NrL3Byb3RvLWZsZWV0L3NlcnZlci9nZW5lcmF0ZWQvZ3JwYy9mbGVldG5vZGVnYXRld2F5L3YxO2ZsZWV0bm9kZWdhdGV3YXl2MaICA0ZYWKoCE0ZsZWV0bm9kZWdhdGV3YXkuVjHKAhNGbGVldG5vZGVnYXRld2F5XFYx4gIfRmxlZXRub2RlZ2F0ZXdheVxWMVxHUEJNZXRhZGF0YeoCFEZsZWV0bm9kZWdhdGV3YXk6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_common_v1_cooling, file_curtailment_v1_curtailment, file_errors_v1_errors, file_google_protobuf_timestamp, file_minercommand_v1_command, file_pairing_v1_pairing, file_telemetry_v1_telemetry]); +export const file_fleetnodegateway_v1_fleetnodegateway: GenFile = + /*@__PURE__*/ + fileDesc( + "CipmbGVldG5vZGVnYXRld2F5L3YxL2ZsZWV0bm9kZWdhdGV3YXkucHJvdG8SE2ZsZWV0bm9kZWdhdGV3YXkudjEilwEKD1JlZ2lzdGVyUmVxdWVzdBIkChBlbnJvbGxtZW50X3Rva2VuGAEgASgJQgq6SAdyBRAUGIAEEhgKBG5hbWUYAiABKAlCCrpIB3IFEAEY/wESIAoPaWRlbnRpdHlfcHVia2V5GAMgASgMQge6SAR6AmggEiIKEWVuY3J5cHRpb25fcHVia2V5GAQgASgMQge6SAR6AmggIokBChBSZWdpc3RlclJlc3BvbnNlEhUKDWZsZWV0X25vZGVfaWQYASABKAMSQAoRZW5yb2xsbWVudF9zdGF0dXMYAiABKA4yJS5mbGVldG5vZGVnYXRld2F5LnYxLkVucm9sbG1lbnRTdGF0dXMSHAoUaWRlbnRpdHlfZmluZ2VycHJpbnQYAyABKAkiWgoZQmVnaW5BdXRoSGFuZHNoYWtlUmVxdWVzdBIbCgdhcGlfa2V5GAEgASgJQgq6SAdyBRAUGIAEEiAKD2lkZW50aXR5X3B1YmtleRgCIAEoDEIHukgEegJoICJfChpCZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRIRCgljaGFsbGVuZ2UYASABKAwSLgoKZXhwaXJlc19hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWAocQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVxdWVzdBIcCgljaGFsbGVuZ2UYASABKAxCCbpIBnoEEBAYQBIaCglzaWduYXR1cmUYAiABKAxCB7pIBHoCaEAiZgodQ29tcGxldGVBdXRoSGFuZHNoYWtlUmVzcG9uc2USFQoNc2Vzc2lvbl90b2tlbhgBIAEoCRIuCgpleHBpcmVzX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJtChZVcGxvYWRUZWxlbWV0cnlSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIxChdVcGxvYWRUZWxlbWV0cnlSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJqChNVcGxvYWRFdmVudHNSZXF1ZXN0EhoKB3BheWxvYWQYASABKAxCCbpIBnoEGICAQBI3CgtjYXB0dXJlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCBrpIA8gBASIuChRVcGxvYWRFdmVudHNSZXNwb25zZRIWCg5hY2NlcHRlZF9jb3VudBgBIAEoAyJNChZVcGxvYWRIZWFydGJlYXRSZXF1ZXN0EjMKB3NlbnRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQga6SAPIAQEiSgoXVXBsb2FkSGVhcnRiZWF0UmVzcG9uc2USLwoLcmVjZWl2ZWRfYXQYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIuQBChJDb21tYW5kQXJ0aWZhY3RSZWYSHwoLYXJ0aWZhY3RfaWQYASABKAlCCrpIB3IFEAEYgAESSAoHcHVycG9zZRgCIAEoDjIrLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UHVycG9zZUIKukgHggEEEAEgABIcCghmaWxlbmFtZRgDIAEoCUIKukgHcgUQARj/ARIbCgpzaXplX2J5dGVzGAQgASgDQge6SAQiAiAAEigKBnNoYTI1NhgFIAEoCUIYukgVchMyDl5bYS1mMC05XXs2NH0kmAFAIpECChtDb21tYW5kQXJ0aWZhY3RVcGxvYWRIZWFkZXISHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJICgdwdXJwb3NlGAIgASgOMisuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RQdXJwb3NlQgq6SAeCAQQQASAAEhwKCGZpbGVuYW1lGAMgASgJQgq6SAdyBRABGP8BEhsKCnNpemVfYnl0ZXMYBCABKANCB7pIBCICIAASKAoGc2hhMjU2GAUgASgJQhi6SBVyEzIOXlthLWYwLTldezY0fSSYAUASIwoRZGV2aWNlX2lkZW50aWZpZXIYBiABKAlCCLpIBXIDGP8BIjEKFENvbW1hbmRBcnRpZmFjdENodW5rEhkKBGRhdGEYASABKAxCC7pICHoGEAEYgIBAIq0BChxVcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0EkIKBmhlYWRlchgBIAEoCzIwLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0VXBsb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiYgodVXBsb2FkQ29tbWFuZEFydGlmYWN0UmVzcG9uc2USQQoIYXJ0aWZhY3QYASABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdFJlZkIGukgDyAEBIqgBCh5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJBCghhcnRpZmFjdBgCIAEoCzInLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29tbWFuZEFydGlmYWN0UmVmQga6SAPIAQESIwoRZGV2aWNlX2lkZW50aWZpZXIYAyABKAlCCLpIBXIDGP8BImIKHUNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVyEkEKCGFydGlmYWN0GAEgASgLMicuZmxlZXRub2RlZ2F0ZXdheS52MS5Db21tYW5kQXJ0aWZhY3RSZWZCBrpIA8gBASKyAQofRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZRJECgZoZWFkZXIYASABKAsyMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdERvd25sb2FkSGVhZGVySAASOgoFY2h1bmsYAiABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbW1hbmRBcnRpZmFjdENodW5rSABCDQoEcGFydBIFukgCCAEiiQEKHlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVxdWVzdBJHCgdkZXZpY2VzGAEgAygLMisuZmxlZXRub2RlZ2F0ZXdheS52MS5EaXNjb3ZlcmVkRGV2aWNlUmVwb3J0Qgm6SAaSAQMQgAgSHgoKY29tbWFuZF9pZBgCIAEoCUIKukgHcgUQARiAASKDAwoWRGlzY292ZXJlZERldmljZVJlcG9ydBIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIfCgppcF9hZGRyZXNzGAIgASgJQgu6SAhyBhABGC1wARKGAQoEcG9ydBgDIAEoCUJ4ukh1ugFsCgpwb3J0LnJhbmdlEilwb3J0IG11c3QgYmUgYSBkZWNpbWFsIG51bWJlciBpbiAxLi42NTUzNRozdGhpcy5tYXRjaGVzKCdeWzEtOV1bMC05XSokJykgJiYgaW50KHRoaXMpIDw9IDY1NTM1cgQQARgFEhsKCnVybF9zY2hlbWUYBCABKAlCB7pIBHICGCASHgoLZHJpdmVyX25hbWUYBSABKAlCCbpIBnIEEAEYMhIXCgVtb2RlbBgGIAEoCUIIukgFcgMY/wESHgoMbWFudWZhY3R1cmVyGAcgASgJQgi6SAVyAxj/ARIiChBmaXJtd2FyZV92ZXJzaW9uGAggASgJQgi6SAVyAxj/ASJRCh9SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIvoDChNGbGVldE5vZGVQYWlyUmVzdWx0EiUKEWRldmljZV9pZGVudGlmaWVyGAEgASgJQgq6SAdyBRABGP8BEjEKB291dGNvbWUYAiABKA4yIC5mbGVldG5vZGVnYXRld2F5LnYxLlBhaXJPdXRjb21lEh8KDXNlcmlhbF9udW1iZXIYAyABKAlCCLpIBXIDGP8BEhwKC21hY19hZGRyZXNzGAQgASgJQge6SARyAhhAEhcKBW1vZGVsGAUgASgJQgi6SAVyAxj/ARIeCgxtYW51ZmFjdHVyZXIYBiABKAlCCLpIBXIDGP8BEiIKEGZpcm13YXJlX3ZlcnNpb24YByABKAlCCLpIBXIDGP8BEh8KDWVycm9yX21lc3NhZ2UYCCABKAlCCLpIBXIDGIAgEiQKF2RlZmF1bHRfcGFzc3dvcmRfYWN0aXZlGAwgASgISACIAQESSAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGA0gASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IaChhfZGVmYXVsdF9wYXNzd29yZF9hY3RpdmVKBAgJEApKBAgKEAtKBAgLEAxSDXVzZWRfdXNlcm5hbWVSDXVzZWRfcGFzc3dvcmRSEHVzZWRfY3JlZGVudGlhbHMiTgoURW5jcnlwdGVkQ3JlZGVudGlhbHMSGgoIdXNlcm5hbWUYASABKAxCCLpIBXoDGIAgEhoKCHBhc3N3b3JkGAIgASgMQgi6SAV6AxiAICKCAQoaUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARJECgdyZXN1bHRzGAIgAygLMiguZmxlZXRub2RlZ2F0ZXdheS52MS5GbGVldE5vZGVQYWlyUmVzdWx0Qgm6SAaSAQMQgAgiTQobUmVwb3J0UGFpcmVkRGV2aWNlc1Jlc3BvbnNlEhYKDmFjY2VwdGVkX2NvdW50GAEgASgDEhYKDnJlamVjdGVkX2NvdW50GAIgASgDIokBChRDb250cm9sU3RyZWFtUmVxdWVzdBIyCgVoZWxsbxgBIAEoCzIhLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEhlbGxvSAASLgoDYWNrGAIgASgLMh8uZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sQWNrSABCDQoEa2luZBIFukgCCAEimAEKFUNvbnRyb2xTdHJlYW1SZXNwb25zZRI4CghhY2NlcHRlZBgBIAEoCzIkLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbEFjY2VwdGVkSAASNgoHY29tbWFuZBgCIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbENvbW1hbmRIAEINCgRraW5kEgW6SAIIASIOCgxDb250cm9sSGVsbG8iQgoPQ29udHJvbEFjY2VwdGVkEi8KC3NlcnZlcl90aW1lGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJMCg5Db250cm9sQ29tbWFuZBIeCgpjb21tYW5kX2lkGAEgASgJQgq6SAdyBRABGIABEhoKB3BheWxvYWQYAiABKAxCCbpIBnoEGICAQCK3AwoZTWluZXJDb25uZWN0aW9uRGVzY3JpcHRvchIlChFkZXZpY2VfaWRlbnRpZmllchgBIAEoCUIKukgHcgUQARj/ARIeCgtkcml2ZXJfbmFtZRgCIAEoCUIJukgGcgQQARgyEh8KCmlwX2FkZHJlc3MYAyABKAlCC7pICHIGEAEYLXABEoYBCgRwb3J0GAQgASgJQni6SHW6AWwKCnBvcnQucmFuZ2USKXBvcnQgbXVzdCBiZSBhIGRlY2ltYWwgbnVtYmVyIGluIDEuLjY1NTM1GjN0aGlzLm1hdGNoZXMoJ15bMS05XVswLTldKiQnKSAmJiBpbnQodGhpcykgPD0gNjU1MzVyBBABGAUSGwoKdXJsX3NjaGVtZRgFIAEoCUIHukgEcgIYIBIfCg1zZXJpYWxfbnVtYmVyGAYgASgJQgi6SAVyAxj/ARIdCgttYWNfYWRkcmVzcxgHIAEoCUIIukgFcgMY/wESJQoTY3JlZGVudGlhbF91c2VybmFtZRgIIAEoDEIIukgFegMYgCASJQoTY3JlZGVudGlhbF9wYXNzd29yZBgJIAEoDEIIukgFegMYgCAitwcKDE1pbmVyQ29tbWFuZBJGCgZ0YXJnZXQYASABKAsyLi5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29ubmVjdGlvbkRlc2NyaXB0b3JCBrpIA8gBARIzCgZyZWJvb3QYAiABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLlJlYm9vdEFjdGlvbkgAEj4KDHN0YXJ0X21pbmluZxgDIAEoCzImLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RhcnRNaW5pbmdBY3Rpb25IABI8CgtzdG9wX21pbmluZxgEIAEoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuU3RvcE1pbmluZ0FjdGlvbkgAEjgKCWJsaW5rX2xlZBgFIAEoCzIjLmZsZWV0bm9kZWdhdGV3YXkudjEuQmxpbmtMZWRBY3Rpb25IABI1CgdjdXJ0YWlsGAYgASgLMiIuZmxlZXRub2RlZ2F0ZXdheS52MS5DdXJ0YWlsQWN0aW9uSAASOQoJdW5jdXJ0YWlsGAcgASgLMiQuZmxlZXRub2RlZ2F0ZXdheS52MS5VbmN1cnRhaWxBY3Rpb25IABJFChBzZXRfY29vbGluZ19tb2RlGAggASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5TZXRDb29saW5nTW9kZUFjdGlvbkgAEkUKEHNldF9wb3dlcl90YXJnZXQYCSABKAsyKS5mbGVldG5vZGVnYXRld2F5LnYxLlNldFBvd2VyVGFyZ2V0QWN0aW9uSAASSwoTdXBkYXRlX21pbmluZ19wb29scxgKIAEoCzIsLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluaW5nUG9vbHNBY3Rpb25IABJFChBnZXRfbWluaW5nX3Bvb2xzGAsgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5HZXRNaW5pbmdQb29sc0FjdGlvbkgAEjoKCmdldF9lcnJvcnMYDCABKAsyJC5mbGVldG5vZGVnYXRld2F5LnYxLkdldEVycm9yc0FjdGlvbkgAEk8KFXVwZGF0ZV9taW5lcl9wYXNzd29yZBgNIAEoCzIuLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbkgAEkAKDWRvd25sb2FkX2xvZ3MYDiABKAsyJy5mbGVldG5vZGVnYXRld2F5LnYxLkRvd25sb2FkTG9nc0FjdGlvbkgAQg8KBmFjdGlvbhIFukgCCAEiDgoMUmVib290QWN0aW9uIhMKEVN0YXJ0TWluaW5nQWN0aW9uIhIKEFN0b3BNaW5pbmdBY3Rpb24iEAoOQmxpbmtMZWRBY3Rpb24iEQoPVW5jdXJ0YWlsQWN0aW9uIhYKFEdldE1pbmluZ1Bvb2xzQWN0aW9uIhEKD0dldEVycm9yc0FjdGlvbiI4ChJEb3dubG9hZExvZ3NBY3Rpb24SIgoOYmF0Y2hfbG9nX3V1aWQYASABKAlCCrpIB3IFEAEYgAEimgIKFE5vZGVFbmNyeXB0ZWRQYXlsb2FkEqYBCglhbGdvcml0aG0YASABKAlCkgG6SI4BugGEAQogbm9kZV9lbmNyeXB0ZWRfcGF5bG9hZC5hbGdvcml0aG0SM2FsZ29yaXRobSBtdXN0IGJlIHgyNTUxOS1oa2RmLXNoYTI1Ni1hZXMtMjU2LWdjbS12MRordGhpcyA9PSAneDI1NTE5LWhrZGYtc2hhMjU2LWFlcy0yNTYtZ2NtLXYxJ3IEEAEYQBIhChBlcGhlbWVyYWxfcHVia2V5GAIgASgMQge6SAR6AmggEhYKBW5vbmNlGAMgASgMQge6SAR6AmgMEh4KCmNpcGhlcnRleHQYBCABKAxCCrpIB3oFEBEYgEAicQoZVXBkYXRlTWluZXJQYXNzd29yZEFjdGlvbhJUChllbmNyeXB0ZWRfcGFzc3dvcmRfdXBkYXRlGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5Ob2RlRW5jcnlwdGVkUGF5bG9hZEIGukgDyAEBIm0KGVVwZGF0ZU1pbmVyUGFzc3dvcmRSZXN1bHQSUAoVZW5jcnlwdGVkX2NyZWRlbnRpYWxzGAEgASgLMikuZmxlZXRub2RlZ2F0ZXdheS52MS5FbmNyeXB0ZWRDcmVkZW50aWFsc0IGukgDyAEBIkAKDUN1cnRhaWxBY3Rpb24SLwoFbGV2ZWwYASABKA4yIC5jdXJ0YWlsbWVudC52MS5DdXJ0YWlsbWVudExldmVsIjwKFFNldENvb2xpbmdNb2RlQWN0aW9uEiQKBG1vZGUYASABKA4yFi5jb21tb24udjEuQ29vbGluZ01vZGUiUgoUU2V0UG93ZXJUYXJnZXRBY3Rpb24SOgoQcGVyZm9ybWFuY2VfbW9kZRgBIAEoDjIgLm1pbmVyY29tbWFuZC52MS5QZXJmb3JtYW5jZU1vZGUihQMKEE1pbmluZ1Bvb2xDb25maWcSGwoIcHJpb3JpdHkYASABKAVCCbpIBhoEGAIoABK3AgoDdXJsGAIgASgJQqkCukilAnKiAhAMGIACMpoCXihzdHJhdHVtXCsodGNwfHNzbHx3cyk6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKSg6XGR7MSw1fSk/fHN0cmF0dW0yXCt0Y3A6XC9cLygoW2EtekEtWjAtOV1bYS16QS1aMC05Li1dKlthLXpBLVowLTldXC5bYS16QS1aXXsyLH0pfChcZHsxLDN9XC4pezN9XGR7MSwzfXxcWyhbMC05YS1mQS1GOl0rKVxdKTpcZHsxLDV9XC9bQS1aYS16MC05XytcLz06Li1dKykkEhoKCHVzZXJuYW1lGAMgASgJQgi6SAVyAxiABCJbChdVcGRhdGVNaW5pbmdQb29sc0FjdGlvbhJACgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IKukgHkgEECAEQAyJWChRHZXRNaW5pbmdQb29sc1Jlc3VsdBI+CgVwb29scxgBIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluaW5nUG9vbENvbmZpZ0IIukgFkgECEAMixQUKEE1pbmVyRXJyb3JSZXBvcnQSNAoLbWluZXJfZXJyb3IYASABKA4yFS5lcnJvcnMudjEuTWluZXJFcnJvckIIukgFggECEAESHwoNY2F1c2Vfc3VtbWFyeRgCIAEoCUIIukgFcgMYgCASJAoScmVjb21tZW5kZWRfYWN0aW9uGAMgASgJQgi6SAVyAxiAIBIvCghzZXZlcml0eRgEIAEoDjITLmVycm9ycy52MS5TZXZlcml0eUIIukgFggECEAESMQoNZmlyc3Rfc2Vlbl9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMAoMbGFzdF9zZWVuX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBItCgljbG9zZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEnAKEXZlbmRvcl9hdHRyaWJ1dGVzGAggAygLMjsuZmxlZXRub2RlZ2F0ZXdheS52MS5NaW5lckVycm9yUmVwb3J0LlZlbmRvckF0dHJpYnV0ZXNFbnRyeUIYukgVmgESECAiB3IFEAEYgAEqBXIDGIAIEh0KCWRldmljZV9pZBgJIAEoCUIKukgHcgUQARj/ARIjCgxjb21wb25lbnRfaWQYCiABKAlCCLpIBXIDGP8BSACIAQESGAoGaW1wYWN0GAsgASgJQgi6SAVyAxiAIBIZCgdzdW1tYXJ5GAwgASgJQgi6SAVyAxiAIBI6Cg5jb21wb25lbnRfdHlwZRgNIAEoDjIYLmVycm9ycy52MS5Db21wb25lbnRUeXBlQgi6SAWCAQIQARo3ChVWZW5kb3JBdHRyaWJ1dGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIPCg1fY29tcG9uZW50X2lkIqMBCg9HZXRFcnJvcnNSZXN1bHQSHQoJZGV2aWNlX2lkGAEgASgJQgq6SAdyBRABGP8BEkAKBmVycm9ycxgCIAMoCzIlLmZsZWV0bm9kZWdhdGV3YXkudjEuTWluZXJFcnJvclJlcG9ydEIJukgGkgEDEIAEEhEKCXRydW5jYXRlZBgDIAEoCBIcChRvbWl0dGVkX3JlcG9ydF9jb3VudBgEIAEoDSL9AQoMQWdlbnRDb21tYW5kEi8KCGRpc2NvdmVyGAEgASgLMhsucGFpcmluZy52MS5EaXNjb3ZlclJlcXVlc3RIABIwCgRwYWlyGAIgASgLMiAucGFpcmluZy52MS5GbGVldE5vZGVQYWlyUmVxdWVzdEgAEjoKDW1pbmVyX2NvbW1hbmQYAyABKAsyIS5mbGVldG5vZGVnYXRld2F5LnYxLk1pbmVyQ29tbWFuZEgAEjwKCXRlbGVtZXRyeRgEIAEoCzInLnRlbGVtZXRyeS52MS5GbGVldE5vZGVUZWxlbWV0cnlSZXF1ZXN0SABCEAoHY29tbWFuZBIFukgCCAEiqAEKCkNvbnRyb2xBY2sSHgoKY29tbWFuZF9pZBgBIAEoCUIKukgHcgUQARiAARIRCglzdWNjZWVkZWQYAiABKAgSHwoNZXJyb3JfbWVzc2FnZRgDIAEoCUIIukgFcgMYgCASKgoEY29kZRgEIAEoDjIcLmZsZWV0bm9kZWdhdGV3YXkudjEuQWNrQ29kZRIaCgdwYXlsb2FkGAUgASgMQgm6SAZ6BBiAgEAqlAEKEEVucm9sbG1lbnRTdGF0dXMSIQodRU5ST0xMTUVOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIdChlFTlJPTExNRU5UX1NUQVRVU19QRU5ESU5HEAESHwobRU5ST0xMTUVOVF9TVEFUVVNfQ09ORklSTUVEEAISHQoZRU5ST0xMTUVOVF9TVEFUVVNfUkVWT0tFRBADKpoBChZDb21tYW5kQXJ0aWZhY3RQdXJwb3NlEigKJENPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9VTlNQRUNJRklFRBAAEicKI0NPTU1BTkRfQVJUSUZBQ1RfUFVSUE9TRV9NSU5FUl9MT0dTEAESLQopQ09NTUFORF9BUlRJRkFDVF9QVVJQT1NFX0ZJUk1XQVJFX1BBWUxPQUQQAiqYAQoLUGFpck91dGNvbWUSHAoYUEFJUl9PVVRDT01FX1VOU1BFQ0lGSUVEEAASFwoTUEFJUl9PVVRDT01FX1BBSVJFRBABEhwKGFBBSVJfT1VUQ09NRV9BVVRIX05FRURFRBACEhwKGFBBSVJfT1VUQ09NRV9BVVRIX0ZBSUxFRBADEhYKElBBSVJfT1VUQ09NRV9FUlJPUhAEKrQCCgdBY2tDb2RlEhgKFEFDS19DT0RFX1VOU1BFQ0lGSUVEEAASDwoLQUNLX0NPREVfT0sQARIUChBBQ0tfQ09ERV9QQVJUSUFMEAISGAoUQUNLX0NPREVfQkFEX1JFUVVFU1QQAxIcChhBQ0tfQ09ERV9BR0VOVF9JTkNBUEFCTEUQBBIYChRBQ0tfQ09ERV9TQ0FOX0ZBSUxFRBAFEhoKFkFDS19DT0RFX1JFUE9SVF9GQUlMRUQQBhIVChFBQ0tfQ09ERV9JTlRFUk5BTBAHEhEKDUFDS19DT0RFX0JVU1kQCBIcChhBQ0tfQ09ERV9VTkFVVEhFTlRJQ0FURUQQCRIaChZBQ0tfQ09ERV9VTklNUExFTUVOVEVEEAoSFgoSQUNLX0NPREVfRk9SQklEREVOEAsypwoKF0ZsZWV0Tm9kZUdhdGV3YXlTZXJ2aWNlElcKCFJlZ2lzdGVyEiQuZmxlZXRub2RlZ2F0ZXdheS52MS5SZWdpc3RlclJlcXVlc3QaJS5mbGVldG5vZGVnYXRld2F5LnYxLlJlZ2lzdGVyUmVzcG9uc2USdQoSQmVnaW5BdXRoSGFuZHNoYWtlEi4uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXF1ZXN0Gi8uZmxlZXRub2RlZ2F0ZXdheS52MS5CZWdpbkF1dGhIYW5kc2hha2VSZXNwb25zZRJ+ChVDb21wbGV0ZUF1dGhIYW5kc2hha2USMS5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlcXVlc3QaMi5mbGVldG5vZGVnYXRld2F5LnYxLkNvbXBsZXRlQXV0aEhhbmRzaGFrZVJlc3BvbnNlEm4KD1VwbG9hZFRlbGVtZXRyeRIrLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVxdWVzdBosLmZsZWV0bm9kZWdhdGV3YXkudjEuVXBsb2FkVGVsZW1ldHJ5UmVzcG9uc2UoARJlCgxVcGxvYWRFdmVudHMSKC5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1JlcXVlc3QaKS5mbGVldG5vZGVnYXRld2F5LnYxLlVwbG9hZEV2ZW50c1Jlc3BvbnNlKAESbAoPVXBsb2FkSGVhcnRiZWF0EisuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXF1ZXN0GiwuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRIZWFydGJlYXRSZXNwb25zZRKAAQoVVXBsb2FkQ29tbWFuZEFydGlmYWN0EjEuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjIuZmxlZXRub2RlZ2F0ZXdheS52MS5VcGxvYWRDb21tYW5kQXJ0aWZhY3RSZXNwb25zZSgBEoYBChdEb3dubG9hZENvbW1hbmRBcnRpZmFjdBIzLmZsZWV0bm9kZWdhdGV3YXkudjEuRG93bmxvYWRDb21tYW5kQXJ0aWZhY3RSZXF1ZXN0GjQuZmxlZXRub2RlZ2F0ZXdheS52MS5Eb3dubG9hZENvbW1hbmRBcnRpZmFjdFJlc3BvbnNlMAEShAEKF1JlcG9ydERpc2NvdmVyZWREZXZpY2VzEjMuZmxlZXRub2RlZ2F0ZXdheS52MS5SZXBvcnREaXNjb3ZlcmVkRGV2aWNlc1JlcXVlc3QaNC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydERpc2NvdmVyZWREZXZpY2VzUmVzcG9uc2USeAoTUmVwb3J0UGFpcmVkRGV2aWNlcxIvLmZsZWV0bm9kZWdhdGV3YXkudjEuUmVwb3J0UGFpcmVkRGV2aWNlc1JlcXVlc3QaMC5mbGVldG5vZGVnYXRld2F5LnYxLlJlcG9ydFBhaXJlZERldmljZXNSZXNwb25zZRJqCg1Db250cm9sU3RyZWFtEikuZmxlZXRub2RlZ2F0ZXdheS52MS5Db250cm9sU3RyZWFtUmVxdWVzdBoqLmZsZWV0bm9kZWdhdGV3YXkudjEuQ29udHJvbFN0cmVhbVJlc3BvbnNlKAEwAUL4AQoXY29tLmZsZWV0bm9kZWdhdGV3YXkudjFCFUZsZWV0bm9kZWdhdGV3YXlQcm90b1ABWllnaXRodWIuY29tL2Jsb2NrL3Byb3RvLWZsZWV0L3NlcnZlci9nZW5lcmF0ZWQvZ3JwYy9mbGVldG5vZGVnYXRld2F5L3YxO2ZsZWV0bm9kZWdhdGV3YXl2MaICA0ZYWKoCE0ZsZWV0bm9kZWdhdGV3YXkuVjHKAhNGbGVldG5vZGVnYXRld2F5XFYx4gIfRmxlZXRub2RlZ2F0ZXdheVxWMVxHUEJNZXRhZGF0YeoCFEZsZWV0bm9kZWdhdGV3YXk6OlYxYgZwcm90bzM", + [ + file_buf_validate_validate, + file_common_v1_cooling, + file_curtailment_v1_curtailment, + file_errors_v1_errors, + file_google_protobuf_timestamp, + file_minercommand_v1_command, + file_pairing_v1_pairing, + file_telemetry_v1_telemetry, + ], + ); /** * @generated from message fleetnodegateway.v1.RegisterRequest @@ -64,7 +77,8 @@ export type RegisterRequest = Message<"fleetnodegateway.v1.RegisterRequest"> & { * Describes the message fleetnodegateway.v1.RegisterRequest. * Use `create(RegisterRequestSchema)` to create a new message. */ -export const RegisterRequestSchema: GenMessage = /*@__PURE__*/ +export const RegisterRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); /** @@ -91,7 +105,8 @@ export type RegisterResponse = Message<"fleetnodegateway.v1.RegisterResponse"> & * Describes the message fleetnodegateway.v1.RegisterResponse. * Use `create(RegisterResponseSchema)` to create a new message. */ -export const RegisterResponseSchema: GenMessage = /*@__PURE__*/ +export const RegisterResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 1); /** @@ -113,7 +128,8 @@ export type BeginAuthHandshakeRequest = Message<"fleetnodegateway.v1.BeginAuthHa * Describes the message fleetnodegateway.v1.BeginAuthHandshakeRequest. * Use `create(BeginAuthHandshakeRequestSchema)` to create a new message. */ -export const BeginAuthHandshakeRequestSchema: GenMessage = /*@__PURE__*/ +export const BeginAuthHandshakeRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 2); /** @@ -135,7 +151,8 @@ export type BeginAuthHandshakeResponse = Message<"fleetnodegateway.v1.BeginAuthH * Describes the message fleetnodegateway.v1.BeginAuthHandshakeResponse. * Use `create(BeginAuthHandshakeResponseSchema)` to create a new message. */ -export const BeginAuthHandshakeResponseSchema: GenMessage = /*@__PURE__*/ +export const BeginAuthHandshakeResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); /** @@ -157,7 +174,8 @@ export type CompleteAuthHandshakeRequest = Message<"fleetnodegateway.v1.Complete * Describes the message fleetnodegateway.v1.CompleteAuthHandshakeRequest. * Use `create(CompleteAuthHandshakeRequestSchema)` to create a new message. */ -export const CompleteAuthHandshakeRequestSchema: GenMessage = /*@__PURE__*/ +export const CompleteAuthHandshakeRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 4); /** @@ -179,7 +197,8 @@ export type CompleteAuthHandshakeResponse = Message<"fleetnodegateway.v1.Complet * Describes the message fleetnodegateway.v1.CompleteAuthHandshakeResponse. * Use `create(CompleteAuthHandshakeResponseSchema)` to create a new message. */ -export const CompleteAuthHandshakeResponseSchema: GenMessage = /*@__PURE__*/ +export const CompleteAuthHandshakeResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 5); /** @@ -201,7 +220,8 @@ export type UploadTelemetryRequest = Message<"fleetnodegateway.v1.UploadTelemetr * Describes the message fleetnodegateway.v1.UploadTelemetryRequest. * Use `create(UploadTelemetryRequestSchema)` to create a new message. */ -export const UploadTelemetryRequestSchema: GenMessage = /*@__PURE__*/ +export const UploadTelemetryRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 6); /** @@ -218,7 +238,8 @@ export type UploadTelemetryResponse = Message<"fleetnodegateway.v1.UploadTelemet * Describes the message fleetnodegateway.v1.UploadTelemetryResponse. * Use `create(UploadTelemetryResponseSchema)` to create a new message. */ -export const UploadTelemetryResponseSchema: GenMessage = /*@__PURE__*/ +export const UploadTelemetryResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 7); /** @@ -240,7 +261,8 @@ export type UploadEventsRequest = Message<"fleetnodegateway.v1.UploadEventsReque * Describes the message fleetnodegateway.v1.UploadEventsRequest. * Use `create(UploadEventsRequestSchema)` to create a new message. */ -export const UploadEventsRequestSchema: GenMessage = /*@__PURE__*/ +export const UploadEventsRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 8); /** @@ -257,7 +279,8 @@ export type UploadEventsResponse = Message<"fleetnodegateway.v1.UploadEventsResp * Describes the message fleetnodegateway.v1.UploadEventsResponse. * Use `create(UploadEventsResponseSchema)` to create a new message. */ -export const UploadEventsResponseSchema: GenMessage = /*@__PURE__*/ +export const UploadEventsResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 9); /** @@ -274,7 +297,8 @@ export type UploadHeartbeatRequest = Message<"fleetnodegateway.v1.UploadHeartbea * Describes the message fleetnodegateway.v1.UploadHeartbeatRequest. * Use `create(UploadHeartbeatRequestSchema)` to create a new message. */ -export const UploadHeartbeatRequestSchema: GenMessage = /*@__PURE__*/ +export const UploadHeartbeatRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 10); /** @@ -291,7 +315,8 @@ export type UploadHeartbeatResponse = Message<"fleetnodegateway.v1.UploadHeartbe * Describes the message fleetnodegateway.v1.UploadHeartbeatResponse. * Use `create(UploadHeartbeatResponseSchema)` to create a new message. */ -export const UploadHeartbeatResponseSchema: GenMessage = /*@__PURE__*/ +export const UploadHeartbeatResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 11); /** @@ -328,7 +353,8 @@ export type CommandArtifactRef = Message<"fleetnodegateway.v1.CommandArtifactRef * Describes the message fleetnodegateway.v1.CommandArtifactRef. * Use `create(CommandArtifactRefSchema)` to create a new message. */ -export const CommandArtifactRefSchema: GenMessage = /*@__PURE__*/ +export const CommandArtifactRefSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 12); /** @@ -370,7 +396,8 @@ export type CommandArtifactUploadHeader = Message<"fleetnodegateway.v1.CommandAr * Describes the message fleetnodegateway.v1.CommandArtifactUploadHeader. * Use `create(CommandArtifactUploadHeaderSchema)` to create a new message. */ -export const CommandArtifactUploadHeaderSchema: GenMessage = /*@__PURE__*/ +export const CommandArtifactUploadHeaderSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 13); /** @@ -387,7 +414,8 @@ export type CommandArtifactChunk = Message<"fleetnodegateway.v1.CommandArtifactC * Describes the message fleetnodegateway.v1.CommandArtifactChunk. * Use `create(CommandArtifactChunkSchema)` to create a new message. */ -export const CommandArtifactChunkSchema: GenMessage = /*@__PURE__*/ +export const CommandArtifactChunkSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 14); /** @@ -397,26 +425,30 @@ export type UploadCommandArtifactRequest = Message<"fleetnodegateway.v1.UploadCo /** * @generated from oneof fleetnodegateway.v1.UploadCommandArtifactRequest.part */ - part: { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactUploadHeader header = 1; - */ - value: CommandArtifactUploadHeader; - case: "header"; - } | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; - */ - value: CommandArtifactChunk; - case: "chunk"; - } | { case: undefined; value?: undefined }; + part: + | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactUploadHeader header = 1; + */ + value: CommandArtifactUploadHeader; + case: "header"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; + */ + value: CommandArtifactChunk; + case: "chunk"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.UploadCommandArtifactRequest. * Use `create(UploadCommandArtifactRequestSchema)` to create a new message. */ -export const UploadCommandArtifactRequestSchema: GenMessage = /*@__PURE__*/ +export const UploadCommandArtifactRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 15); /** @@ -433,7 +465,8 @@ export type UploadCommandArtifactResponse = Message<"fleetnodegateway.v1.UploadC * Describes the message fleetnodegateway.v1.UploadCommandArtifactResponse. * Use `create(UploadCommandArtifactResponseSchema)` to create a new message. */ -export const UploadCommandArtifactResponseSchema: GenMessage = /*@__PURE__*/ +export const UploadCommandArtifactResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 16); /** @@ -460,7 +493,8 @@ export type DownloadCommandArtifactRequest = Message<"fleetnodegateway.v1.Downlo * Describes the message fleetnodegateway.v1.DownloadCommandArtifactRequest. * Use `create(DownloadCommandArtifactRequestSchema)` to create a new message. */ -export const DownloadCommandArtifactRequestSchema: GenMessage = /*@__PURE__*/ +export const DownloadCommandArtifactRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 17); /** @@ -477,7 +511,8 @@ export type CommandArtifactDownloadHeader = Message<"fleetnodegateway.v1.Command * Describes the message fleetnodegateway.v1.CommandArtifactDownloadHeader. * Use `create(CommandArtifactDownloadHeaderSchema)` to create a new message. */ -export const CommandArtifactDownloadHeaderSchema: GenMessage = /*@__PURE__*/ +export const CommandArtifactDownloadHeaderSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 18); /** @@ -487,26 +522,30 @@ export type DownloadCommandArtifactResponse = Message<"fleetnodegateway.v1.Downl /** * @generated from oneof fleetnodegateway.v1.DownloadCommandArtifactResponse.part */ - part: { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactDownloadHeader header = 1; - */ - value: CommandArtifactDownloadHeader; - case: "header"; - } | { - /** - * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; - */ - value: CommandArtifactChunk; - case: "chunk"; - } | { case: undefined; value?: undefined }; + part: + | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactDownloadHeader header = 1; + */ + value: CommandArtifactDownloadHeader; + case: "header"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.CommandArtifactChunk chunk = 2; + */ + value: CommandArtifactChunk; + case: "chunk"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.DownloadCommandArtifactResponse. * Use `create(DownloadCommandArtifactResponseSchema)` to create a new message. */ -export const DownloadCommandArtifactResponseSchema: GenMessage = /*@__PURE__*/ +export const DownloadCommandArtifactResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 19); /** @@ -533,7 +572,8 @@ export type ReportDiscoveredDevicesRequest = Message<"fleetnodegateway.v1.Report * Describes the message fleetnodegateway.v1.ReportDiscoveredDevicesRequest. * Use `create(ReportDiscoveredDevicesRequestSchema)` to create a new message. */ -export const ReportDiscoveredDevicesRequestSchema: GenMessage = /*@__PURE__*/ +export const ReportDiscoveredDevicesRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 20); /** @@ -590,7 +630,8 @@ export type DiscoveredDeviceReport = Message<"fleetnodegateway.v1.DiscoveredDevi * Describes the message fleetnodegateway.v1.DiscoveredDeviceReport. * Use `create(DiscoveredDeviceReportSchema)` to create a new message. */ -export const DiscoveredDeviceReportSchema: GenMessage = /*@__PURE__*/ +export const DiscoveredDeviceReportSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 21); /** @@ -616,7 +657,8 @@ export type ReportDiscoveredDevicesResponse = Message<"fleetnodegateway.v1.Repor * Describes the message fleetnodegateway.v1.ReportDiscoveredDevicesResponse. * Use `create(ReportDiscoveredDevicesResponseSchema)` to create a new message. */ -export const ReportDiscoveredDevicesResponseSchema: GenMessage = /*@__PURE__*/ +export const ReportDiscoveredDevicesResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 22); /** @@ -689,7 +731,8 @@ export type FleetNodePairResult = Message<"fleetnodegateway.v1.FleetNodePairResu * Describes the message fleetnodegateway.v1.FleetNodePairResult. * Use `create(FleetNodePairResultSchema)` to create a new message. */ -export const FleetNodePairResultSchema: GenMessage = /*@__PURE__*/ +export const FleetNodePairResultSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 23); /** @@ -711,7 +754,8 @@ export type EncryptedCredentials = Message<"fleetnodegateway.v1.EncryptedCredent * Describes the message fleetnodegateway.v1.EncryptedCredentials. * Use `create(EncryptedCredentialsSchema)` to create a new message. */ -export const EncryptedCredentialsSchema: GenMessage = /*@__PURE__*/ +export const EncryptedCredentialsSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 24); /** @@ -737,7 +781,8 @@ export type ReportPairedDevicesRequest = Message<"fleetnodegateway.v1.ReportPair * Describes the message fleetnodegateway.v1.ReportPairedDevicesRequest. * Use `create(ReportPairedDevicesRequestSchema)` to create a new message. */ -export const ReportPairedDevicesRequestSchema: GenMessage = /*@__PURE__*/ +export const ReportPairedDevicesRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 25); /** @@ -759,7 +804,8 @@ export type ReportPairedDevicesResponse = Message<"fleetnodegateway.v1.ReportPai * Describes the message fleetnodegateway.v1.ReportPairedDevicesResponse. * Use `create(ReportPairedDevicesResponseSchema)` to create a new message. */ -export const ReportPairedDevicesResponseSchema: GenMessage = /*@__PURE__*/ +export const ReportPairedDevicesResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 26); /** @@ -769,26 +815,30 @@ export type ControlStreamRequest = Message<"fleetnodegateway.v1.ControlStreamReq /** * @generated from oneof fleetnodegateway.v1.ControlStreamRequest.kind */ - kind: { - /** - * @generated from field: fleetnodegateway.v1.ControlHello hello = 1; - */ - value: ControlHello; - case: "hello"; - } | { - /** - * @generated from field: fleetnodegateway.v1.ControlAck ack = 2; - */ - value: ControlAck; - case: "ack"; - } | { case: undefined; value?: undefined }; + kind: + | { + /** + * @generated from field: fleetnodegateway.v1.ControlHello hello = 1; + */ + value: ControlHello; + case: "hello"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.ControlAck ack = 2; + */ + value: ControlAck; + case: "ack"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.ControlStreamRequest. * Use `create(ControlStreamRequestSchema)` to create a new message. */ -export const ControlStreamRequestSchema: GenMessage = /*@__PURE__*/ +export const ControlStreamRequestSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 27); /** @@ -798,39 +848,43 @@ export type ControlStreamResponse = Message<"fleetnodegateway.v1.ControlStreamRe /** * @generated from oneof fleetnodegateway.v1.ControlStreamResponse.kind */ - kind: { - /** - * @generated from field: fleetnodegateway.v1.ControlAccepted accepted = 1; - */ - value: ControlAccepted; - case: "accepted"; - } | { - /** - * @generated from field: fleetnodegateway.v1.ControlCommand command = 2; - */ - value: ControlCommand; - case: "command"; - } | { case: undefined; value?: undefined }; + kind: + | { + /** + * @generated from field: fleetnodegateway.v1.ControlAccepted accepted = 1; + */ + value: ControlAccepted; + case: "accepted"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.ControlCommand command = 2; + */ + value: ControlCommand; + case: "command"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.ControlStreamResponse. * Use `create(ControlStreamResponseSchema)` to create a new message. */ -export const ControlStreamResponseSchema: GenMessage = /*@__PURE__*/ +export const ControlStreamResponseSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 28); /** * @generated from message fleetnodegateway.v1.ControlHello */ -export type ControlHello = Message<"fleetnodegateway.v1.ControlHello"> & { -}; +export type ControlHello = Message<"fleetnodegateway.v1.ControlHello"> & {}; /** * Describes the message fleetnodegateway.v1.ControlHello. * Use `create(ControlHelloSchema)` to create a new message. */ -export const ControlHelloSchema: GenMessage = /*@__PURE__*/ +export const ControlHelloSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 29); /** @@ -847,7 +901,8 @@ export type ControlAccepted = Message<"fleetnodegateway.v1.ControlAccepted"> & { * Describes the message fleetnodegateway.v1.ControlAccepted. * Use `create(ControlAcceptedSchema)` to create a new message. */ -export const ControlAcceptedSchema: GenMessage = /*@__PURE__*/ +export const ControlAcceptedSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 30); /** @@ -869,7 +924,8 @@ export type ControlCommand = Message<"fleetnodegateway.v1.ControlCommand"> & { * Describes the message fleetnodegateway.v1.ControlCommand. * Use `create(ControlCommandSchema)` to create a new message. */ -export const ControlCommandSchema: GenMessage = /*@__PURE__*/ +export const ControlCommandSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 31); /** @@ -936,7 +992,8 @@ export type MinerConnectionDescriptor = Message<"fleetnodegateway.v1.MinerConnec * Describes the message fleetnodegateway.v1.MinerConnectionDescriptor. * Use `create(MinerConnectionDescriptorSchema)` to create a new message. */ -export const MinerConnectionDescriptorSchema: GenMessage = /*@__PURE__*/ +export const MinerConnectionDescriptorSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 32); /** @@ -955,183 +1012,198 @@ export type MinerCommand = Message<"fleetnodegateway.v1.MinerCommand"> & { /** * @generated from oneof fleetnodegateway.v1.MinerCommand.action */ - action: { - /** - * @generated from field: fleetnodegateway.v1.RebootAction reboot = 2; - */ - value: RebootAction; - case: "reboot"; - } | { - /** - * @generated from field: fleetnodegateway.v1.StartMiningAction start_mining = 3; - */ - value: StartMiningAction; - case: "startMining"; - } | { - /** - * @generated from field: fleetnodegateway.v1.StopMiningAction stop_mining = 4; - */ - value: StopMiningAction; - case: "stopMining"; - } | { - /** - * @generated from field: fleetnodegateway.v1.BlinkLedAction blink_led = 5; - */ - value: BlinkLedAction; - case: "blinkLed"; - } | { - /** - * @generated from field: fleetnodegateway.v1.CurtailAction curtail = 6; - */ - value: CurtailAction; - case: "curtail"; - } | { - /** - * @generated from field: fleetnodegateway.v1.UncurtailAction uncurtail = 7; - */ - value: UncurtailAction; - case: "uncurtail"; - } | { - /** - * @generated from field: fleetnodegateway.v1.SetCoolingModeAction set_cooling_mode = 8; - */ - value: SetCoolingModeAction; - case: "setCoolingMode"; - } | { - /** - * @generated from field: fleetnodegateway.v1.SetPowerTargetAction set_power_target = 9; - */ - value: SetPowerTargetAction; - case: "setPowerTarget"; - } | { - /** - * @generated from field: fleetnodegateway.v1.UpdateMiningPoolsAction update_mining_pools = 10; - */ - value: UpdateMiningPoolsAction; - case: "updateMiningPools"; - } | { - /** - * @generated from field: fleetnodegateway.v1.GetMiningPoolsAction get_mining_pools = 11; - */ - value: GetMiningPoolsAction; - case: "getMiningPools"; - } | { - /** - * @generated from field: fleetnodegateway.v1.GetErrorsAction get_errors = 12; - */ - value: GetErrorsAction; - case: "getErrors"; - } | { - /** - * @generated from field: fleetnodegateway.v1.UpdateMinerPasswordAction update_miner_password = 13; - */ - value: UpdateMinerPasswordAction; - case: "updateMinerPassword"; - } | { - /** - * @generated from field: fleetnodegateway.v1.DownloadLogsAction download_logs = 14; - */ - value: DownloadLogsAction; - case: "downloadLogs"; - } | { case: undefined; value?: undefined }; + action: + | { + /** + * @generated from field: fleetnodegateway.v1.RebootAction reboot = 2; + */ + value: RebootAction; + case: "reboot"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.StartMiningAction start_mining = 3; + */ + value: StartMiningAction; + case: "startMining"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.StopMiningAction stop_mining = 4; + */ + value: StopMiningAction; + case: "stopMining"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.BlinkLedAction blink_led = 5; + */ + value: BlinkLedAction; + case: "blinkLed"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.CurtailAction curtail = 6; + */ + value: CurtailAction; + case: "curtail"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.UncurtailAction uncurtail = 7; + */ + value: UncurtailAction; + case: "uncurtail"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.SetCoolingModeAction set_cooling_mode = 8; + */ + value: SetCoolingModeAction; + case: "setCoolingMode"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.SetPowerTargetAction set_power_target = 9; + */ + value: SetPowerTargetAction; + case: "setPowerTarget"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.UpdateMiningPoolsAction update_mining_pools = 10; + */ + value: UpdateMiningPoolsAction; + case: "updateMiningPools"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.GetMiningPoolsAction get_mining_pools = 11; + */ + value: GetMiningPoolsAction; + case: "getMiningPools"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.GetErrorsAction get_errors = 12; + */ + value: GetErrorsAction; + case: "getErrors"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.UpdateMinerPasswordAction update_miner_password = 13; + */ + value: UpdateMinerPasswordAction; + case: "updateMinerPassword"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.DownloadLogsAction download_logs = 14; + */ + value: DownloadLogsAction; + case: "downloadLogs"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.MinerCommand. * Use `create(MinerCommandSchema)` to create a new message. */ -export const MinerCommandSchema: GenMessage = /*@__PURE__*/ +export const MinerCommandSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 33); /** * @generated from message fleetnodegateway.v1.RebootAction */ -export type RebootAction = Message<"fleetnodegateway.v1.RebootAction"> & { -}; +export type RebootAction = Message<"fleetnodegateway.v1.RebootAction"> & {}; /** * Describes the message fleetnodegateway.v1.RebootAction. * Use `create(RebootActionSchema)` to create a new message. */ -export const RebootActionSchema: GenMessage = /*@__PURE__*/ +export const RebootActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 34); /** * @generated from message fleetnodegateway.v1.StartMiningAction */ -export type StartMiningAction = Message<"fleetnodegateway.v1.StartMiningAction"> & { -}; +export type StartMiningAction = Message<"fleetnodegateway.v1.StartMiningAction"> & {}; /** * Describes the message fleetnodegateway.v1.StartMiningAction. * Use `create(StartMiningActionSchema)` to create a new message. */ -export const StartMiningActionSchema: GenMessage = /*@__PURE__*/ +export const StartMiningActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 35); /** * @generated from message fleetnodegateway.v1.StopMiningAction */ -export type StopMiningAction = Message<"fleetnodegateway.v1.StopMiningAction"> & { -}; +export type StopMiningAction = Message<"fleetnodegateway.v1.StopMiningAction"> & {}; /** * Describes the message fleetnodegateway.v1.StopMiningAction. * Use `create(StopMiningActionSchema)` to create a new message. */ -export const StopMiningActionSchema: GenMessage = /*@__PURE__*/ +export const StopMiningActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 36); /** * @generated from message fleetnodegateway.v1.BlinkLedAction */ -export type BlinkLedAction = Message<"fleetnodegateway.v1.BlinkLedAction"> & { -}; +export type BlinkLedAction = Message<"fleetnodegateway.v1.BlinkLedAction"> & {}; /** * Describes the message fleetnodegateway.v1.BlinkLedAction. * Use `create(BlinkLedActionSchema)` to create a new message. */ -export const BlinkLedActionSchema: GenMessage = /*@__PURE__*/ +export const BlinkLedActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 37); /** * @generated from message fleetnodegateway.v1.UncurtailAction */ -export type UncurtailAction = Message<"fleetnodegateway.v1.UncurtailAction"> & { -}; +export type UncurtailAction = Message<"fleetnodegateway.v1.UncurtailAction"> & {}; /** * Describes the message fleetnodegateway.v1.UncurtailAction. * Use `create(UncurtailActionSchema)` to create a new message. */ -export const UncurtailActionSchema: GenMessage = /*@__PURE__*/ +export const UncurtailActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 38); /** * @generated from message fleetnodegateway.v1.GetMiningPoolsAction */ -export type GetMiningPoolsAction = Message<"fleetnodegateway.v1.GetMiningPoolsAction"> & { -}; +export type GetMiningPoolsAction = Message<"fleetnodegateway.v1.GetMiningPoolsAction"> & {}; /** * Describes the message fleetnodegateway.v1.GetMiningPoolsAction. * Use `create(GetMiningPoolsActionSchema)` to create a new message. */ -export const GetMiningPoolsActionSchema: GenMessage = /*@__PURE__*/ +export const GetMiningPoolsActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 39); /** * @generated from message fleetnodegateway.v1.GetErrorsAction */ -export type GetErrorsAction = Message<"fleetnodegateway.v1.GetErrorsAction"> & { -}; +export type GetErrorsAction = Message<"fleetnodegateway.v1.GetErrorsAction"> & {}; /** * Describes the message fleetnodegateway.v1.GetErrorsAction. * Use `create(GetErrorsActionSchema)` to create a new message. */ -export const GetErrorsActionSchema: GenMessage = /*@__PURE__*/ +export const GetErrorsActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 40); /** @@ -1148,7 +1220,8 @@ export type DownloadLogsAction = Message<"fleetnodegateway.v1.DownloadLogsAction * Describes the message fleetnodegateway.v1.DownloadLogsAction. * Use `create(DownloadLogsActionSchema)` to create a new message. */ -export const DownloadLogsActionSchema: GenMessage = /*@__PURE__*/ +export const DownloadLogsActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 41); /** @@ -1180,7 +1253,8 @@ export type NodeEncryptedPayload = Message<"fleetnodegateway.v1.NodeEncryptedPay * Describes the message fleetnodegateway.v1.NodeEncryptedPayload. * Use `create(NodeEncryptedPayloadSchema)` to create a new message. */ -export const NodeEncryptedPayloadSchema: GenMessage = /*@__PURE__*/ +export const NodeEncryptedPayloadSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 42); /** @@ -1197,7 +1271,8 @@ export type UpdateMinerPasswordAction = Message<"fleetnodegateway.v1.UpdateMiner * Describes the message fleetnodegateway.v1.UpdateMinerPasswordAction. * Use `create(UpdateMinerPasswordActionSchema)` to create a new message. */ -export const UpdateMinerPasswordActionSchema: GenMessage = /*@__PURE__*/ +export const UpdateMinerPasswordActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 43); /** @@ -1214,7 +1289,8 @@ export type UpdateMinerPasswordResult = Message<"fleetnodegateway.v1.UpdateMiner * Describes the message fleetnodegateway.v1.UpdateMinerPasswordResult. * Use `create(UpdateMinerPasswordResultSchema)` to create a new message. */ -export const UpdateMinerPasswordResultSchema: GenMessage = /*@__PURE__*/ +export const UpdateMinerPasswordResultSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 44); /** @@ -1231,7 +1307,8 @@ export type CurtailAction = Message<"fleetnodegateway.v1.CurtailAction"> & { * Describes the message fleetnodegateway.v1.CurtailAction. * Use `create(CurtailActionSchema)` to create a new message. */ -export const CurtailActionSchema: GenMessage = /*@__PURE__*/ +export const CurtailActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 45); /** @@ -1248,7 +1325,8 @@ export type SetCoolingModeAction = Message<"fleetnodegateway.v1.SetCoolingModeAc * Describes the message fleetnodegateway.v1.SetCoolingModeAction. * Use `create(SetCoolingModeActionSchema)` to create a new message. */ -export const SetCoolingModeActionSchema: GenMessage = /*@__PURE__*/ +export const SetCoolingModeActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 46); /** @@ -1265,7 +1343,8 @@ export type SetPowerTargetAction = Message<"fleetnodegateway.v1.SetPowerTargetAc * Describes the message fleetnodegateway.v1.SetPowerTargetAction. * Use `create(SetPowerTargetActionSchema)` to create a new message. */ -export const SetPowerTargetActionSchema: GenMessage = /*@__PURE__*/ +export const SetPowerTargetActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 47); /** @@ -1292,7 +1371,8 @@ export type MiningPoolConfig = Message<"fleetnodegateway.v1.MiningPoolConfig"> & * Describes the message fleetnodegateway.v1.MiningPoolConfig. * Use `create(MiningPoolConfigSchema)` to create a new message. */ -export const MiningPoolConfigSchema: GenMessage = /*@__PURE__*/ +export const MiningPoolConfigSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 48); /** @@ -1309,7 +1389,8 @@ export type UpdateMiningPoolsAction = Message<"fleetnodegateway.v1.UpdateMiningP * Describes the message fleetnodegateway.v1.UpdateMiningPoolsAction. * Use `create(UpdateMiningPoolsActionSchema)` to create a new message. */ -export const UpdateMiningPoolsActionSchema: GenMessage = /*@__PURE__*/ +export const UpdateMiningPoolsActionSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 49); /** @@ -1326,7 +1407,8 @@ export type GetMiningPoolsResult = Message<"fleetnodegateway.v1.GetMiningPoolsRe * Describes the message fleetnodegateway.v1.GetMiningPoolsResult. * Use `create(GetMiningPoolsResultSchema)` to create a new message. */ -export const GetMiningPoolsResultSchema: GenMessage = /*@__PURE__*/ +export const GetMiningPoolsResultSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 50); /** @@ -1403,7 +1485,8 @@ export type MinerErrorReport = Message<"fleetnodegateway.v1.MinerErrorReport"> & * Describes the message fleetnodegateway.v1.MinerErrorReport. * Use `create(MinerErrorReportSchema)` to create a new message. */ -export const MinerErrorReportSchema: GenMessage = /*@__PURE__*/ +export const MinerErrorReportSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 51); /** @@ -1435,7 +1518,8 @@ export type GetErrorsResult = Message<"fleetnodegateway.v1.GetErrorsResult"> & { * Describes the message fleetnodegateway.v1.GetErrorsResult. * Use `create(GetErrorsResultSchema)` to create a new message. */ -export const GetErrorsResultSchema: GenMessage = /*@__PURE__*/ +export const GetErrorsResultSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 52); /** @@ -1456,38 +1540,44 @@ export type AgentCommand = Message<"fleetnodegateway.v1.AgentCommand"> & { /** * @generated from oneof fleetnodegateway.v1.AgentCommand.command */ - command: { - /** - * @generated from field: pairing.v1.DiscoverRequest discover = 1; - */ - value: DiscoverRequest; - case: "discover"; - } | { - /** - * @generated from field: pairing.v1.FleetNodePairRequest pair = 2; - */ - value: FleetNodePairRequest; - case: "pair"; - } | { - /** - * @generated from field: fleetnodegateway.v1.MinerCommand miner_command = 3; - */ - value: MinerCommand; - case: "minerCommand"; - } | { - /** - * @generated from field: telemetry.v1.FleetNodeTelemetryRequest telemetry = 4; - */ - value: FleetNodeTelemetryRequest; - case: "telemetry"; - } | { case: undefined; value?: undefined }; + command: + | { + /** + * @generated from field: pairing.v1.DiscoverRequest discover = 1; + */ + value: DiscoverRequest; + case: "discover"; + } + | { + /** + * @generated from field: pairing.v1.FleetNodePairRequest pair = 2; + */ + value: FleetNodePairRequest; + case: "pair"; + } + | { + /** + * @generated from field: fleetnodegateway.v1.MinerCommand miner_command = 3; + */ + value: MinerCommand; + case: "minerCommand"; + } + | { + /** + * @generated from field: telemetry.v1.FleetNodeTelemetryRequest telemetry = 4; + */ + value: FleetNodeTelemetryRequest; + case: "telemetry"; + } + | { case: undefined; value?: undefined }; }; /** * Describes the message fleetnodegateway.v1.AgentCommand. * Use `create(AgentCommandSchema)` to create a new message. */ -export const AgentCommandSchema: GenMessage = /*@__PURE__*/ +export const AgentCommandSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 53); /** @@ -1526,7 +1616,8 @@ export type ControlAck = Message<"fleetnodegateway.v1.ControlAck"> & { * Describes the message fleetnodegateway.v1.ControlAck. * Use `create(ControlAckSchema)` to create a new message. */ -export const ControlAckSchema: GenMessage = /*@__PURE__*/ +export const ControlAckSchema: GenMessage = + /*@__PURE__*/ messageDesc(file_fleetnodegateway_v1_fleetnodegateway, 54); /** @@ -1557,7 +1648,8 @@ export enum EnrollmentStatus { /** * Describes the enum fleetnodegateway.v1.EnrollmentStatus. */ -export const EnrollmentStatusSchema: GenEnum = /*@__PURE__*/ +export const EnrollmentStatusSchema: GenEnum = + /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); /** @@ -1587,7 +1679,8 @@ export enum CommandArtifactPurpose { /** * Describes the enum fleetnodegateway.v1.CommandArtifactPurpose. */ -export const CommandArtifactPurposeSchema: GenEnum = /*@__PURE__*/ +export const CommandArtifactPurposeSchema: GenEnum = + /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 1); /** @@ -1631,7 +1724,8 @@ export enum PairOutcome { /** * Describes the enum fleetnodegateway.v1.PairOutcome. */ -export const PairOutcomeSchema: GenEnum = /*@__PURE__*/ +export const PairOutcomeSchema: GenEnum = + /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 2); /** @@ -1728,8 +1822,7 @@ export enum AckCode { /** * Describes the enum fleetnodegateway.v1.AckCode. */ -export const AckCodeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); +export const AckCodeSchema: GenEnum = /*@__PURE__*/ enumDesc(file_fleetnodegateway_v1_fleetnodegateway, 3); /** * @generated from service fleetnodegateway.v1.FleetNodeGatewayService @@ -1742,7 +1835,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof RegisterRequestSchema; output: typeof RegisterResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.BeginAuthHandshake */ @@ -1750,7 +1843,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof BeginAuthHandshakeRequestSchema; output: typeof BeginAuthHandshakeResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.CompleteAuthHandshake */ @@ -1758,7 +1851,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof CompleteAuthHandshakeRequestSchema; output: typeof CompleteAuthHandshakeResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadTelemetry */ @@ -1766,7 +1859,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadTelemetryRequestSchema; output: typeof UploadTelemetryResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadEvents */ @@ -1774,7 +1867,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadEventsRequestSchema; output: typeof UploadEventsResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadHeartbeat */ @@ -1782,7 +1875,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof UploadHeartbeatRequestSchema; output: typeof UploadHeartbeatResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.UploadCommandArtifact */ @@ -1790,7 +1883,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "client_streaming"; input: typeof UploadCommandArtifactRequestSchema; output: typeof UploadCommandArtifactResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.DownloadCommandArtifact */ @@ -1798,7 +1891,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "server_streaming"; input: typeof DownloadCommandArtifactRequestSchema; output: typeof DownloadCommandArtifactResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ReportDiscoveredDevices */ @@ -1806,7 +1899,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof ReportDiscoveredDevicesRequestSchema; output: typeof ReportDiscoveredDevicesResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ReportPairedDevices */ @@ -1814,7 +1907,7 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "unary"; input: typeof ReportPairedDevicesRequestSchema; output: typeof ReportPairedDevicesResponseSchema; - }, + }; /** * @generated from rpc fleetnodegateway.v1.FleetNodeGatewayService.ControlStream */ @@ -1822,7 +1915,5 @@ export const FleetNodeGatewayService: GenService<{ methodKind: "bidi_streaming"; input: typeof ControlStreamRequestSchema; output: typeof ControlStreamResponseSchema; - }, -}> = /*@__PURE__*/ - serviceDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); - + }; +}> = /*@__PURE__*/ serviceDesc(file_fleetnodegateway_v1_fleetnodegateway, 0); diff --git a/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go b/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go index 96ccaff57..c83fdf552 100644 --- a/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go +++ b/server/generated/grpc/fleetnodegateway/v1/fleetnodegateway.pb.go @@ -13,6 +13,10 @@ package fleetnodegatewayv1 import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" v11 "github.com/block/proto-fleet/server/generated/grpc/common/v1" v1 "github.com/block/proto-fleet/server/generated/grpc/curtailment/v1" @@ -23,9 +27,6 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" ) const ( From 0136469c015e094ba74c2ad59cce6af9012a9872 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 12:05:35 -0700 Subject: [PATCH 06/15] fix: clean up materialized miner log artifacts --- .../domain/miner/logformat/logformat.go | 6 ++-- .../domain/miner/logformat/logformat_test.go | 30 +++++++++++++++++++ .../internal/infrastructure/files/service.go | 4 +++ .../infrastructure/files/service_test.go | 1 + 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/server/internal/domain/miner/logformat/logformat.go b/server/internal/domain/miner/logformat/logformat.go index 36b22ac22..6756a357e 100644 --- a/server/internal/domain/miner/logformat/logformat.go +++ b/server/internal/domain/miner/logformat/logformat.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "strings" + "unicode" ) const csvLogHeaderWithType = "Time,Type,Message" @@ -128,10 +129,11 @@ func FormatLineToCSVRow(line string, includeType bool) string { } func neutralizeCSVFormula(s string) string { - if s == "" { + trimmed := strings.TrimLeftFunc(s, unicode.IsSpace) + if trimmed == "" { return s } - switch s[0] { + switch trimmed[0] { case '=', '+', '-', '@': return "'" + s default: diff --git a/server/internal/domain/miner/logformat/logformat_test.go b/server/internal/domain/miner/logformat/logformat_test.go index 4fc73002f..c45f4ec76 100644 --- a/server/internal/domain/miner/logformat/logformat_test.go +++ b/server/internal/domain/miner/logformat/logformat_test.go @@ -63,6 +63,36 @@ func TestFormatLineToCSVRowNeutralizesFormulaCells(t *testing.T) { includeType: false, want: `"","'-2026-01-01T00:00:00Z message"`, }, + { + name: "message formula after tab", + line: "\t=cmd", + includeType: false, + want: "\"\",\"'\t=cmd\"", + }, + { + name: "message formula after carriage return", + line: "\r+cmd", + includeType: false, + want: "\"\",\"'\r+cmd\"", + }, + { + name: "message formula after newline", + line: "\n-cmd", + includeType: false, + want: "\"\",\"'\n-cmd\"", + }, + { + name: "message formula after leading spaces", + line: " @cmd", + includeType: false, + want: `"","' @cmd"`, + }, + { + name: "typed message formula after tab", + line: "2024-06-14 16:01:58.470952 | INFO | \t=cmd", + includeType: true, + want: "\"2024-06-14 16:01:58\",\"INFO\",\"'\t=cmd\"", + }, } for _, tt := range tests { diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 08f8d1fdc..4f35d9d67 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -273,6 +273,10 @@ func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, file = nil keep = true + if err := s.DeleteCommandArtifact(info.ID); err != nil { + return "", fleeterror.NewInternalErrorf("failed to delete materialized command artifact %s: %v", info.ID, err) + } + return filePath, nil } diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index bdf0f5601..565a8721e 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -94,6 +94,7 @@ func TestSaveCommandArtifactLog_MaterializesAndBundlesSingleCSV(t *testing.T) { data, err := os.ReadFile(filePath) require.NoError(t, err) assert.Equal(t, content, string(data)) + assert.NoDirExists(t, getCommandArtifactDirPath(info.ID)) bundlePath, err := svc.bundleLogs("batch-artifact-single") require.NoError(t, err) From 090eecdc352f768283f71d63e2c149d17b27897b Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 12:43:33 -0700 Subject: [PATCH 07/15] fix: sanitize uploaded miner log csv artifacts --- .../domain/miner/logformat/logformat.go | 65 ++++++++++++++++-- .../domain/miner/logformat/logformat_test.go | 56 +++++++++++++++ .../internal/infrastructure/files/service.go | 26 +++++-- .../infrastructure/files/service_test.go | 68 +++++++++++++++++-- 4 files changed, 198 insertions(+), 17 deletions(-) diff --git a/server/internal/domain/miner/logformat/logformat.go b/server/internal/domain/miner/logformat/logformat.go index 6756a357e..247d15856 100644 --- a/server/internal/domain/miner/logformat/logformat.go +++ b/server/internal/domain/miner/logformat/logformat.go @@ -1,6 +1,8 @@ package logformat import ( + "encoding/csv" + "errors" "fmt" "io" "strings" @@ -73,16 +75,49 @@ func WriteTextToCSV(w io.Writer, logData string, includeType bool) error { } } +// WriteSanitizedCSV parses an uploaded miner-log CSV, validates that it has one +// of the expected miner-log headers, and rewrites all data cells with the same +// spreadsheet-formula neutralization used by direct miner log formatting. +func WriteSanitizedCSV(w io.Writer, r io.Reader) error { + reader := csv.NewReader(r) + header, err := reader.Read() + if errors.Is(err, io.EOF) { + return fmt.Errorf("empty miner log csv") + } + if err != nil { + return fmt.Errorf("read csv header: %w", err) + } + if !isMinerLogCSVHeader(header) { + return fmt.Errorf("unexpected miner log csv header") + } + if _, err := fmt.Fprintln(w, strings.Join(header, ",")); err != nil { + return fmt.Errorf("write csv header: %w", err) + } + + for { + row, err := reader.Read() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("read csv row: %w", err) + } + for i := range row { + row[i] = neutralizeCSVFormula(row[i]) + } + if _, err := fmt.Fprintln(w, formatCSVFields(row)); err != nil { + return fmt.Errorf("write csv row: %w", err) + } + } +} + // FormatLineToCSVRow parses a single log line into a CSV row. func FormatLineToCSVRow(line string, includeType bool) string { csvRow := func(ts, logType, message string) string { - esc := func(s string) string { - return strings.ReplaceAll(neutralizeCSVFormula(s), `"`, `""`) - } if includeType { - return fmt.Sprintf(`"%s","%s","%s"`, esc(ts), esc(logType), esc(message)) + return formatCSVFields([]string{ts, logType, message}) } - return fmt.Sprintf(`"%s","%s"`, esc(ts), esc(message)) + return formatCSVFields([]string{ts, message}) } for _, level := range logLevelSeparators { @@ -128,6 +163,26 @@ func FormatLineToCSVRow(line string, includeType bool) string { return csvRow("", "", line) } +func isMinerLogCSVHeader(header []string) bool { + if len(header) == 2 { + return header[0] == "Time" && header[1] == "Message" + } + if len(header) == 3 { + return header[0] == "Time" && header[1] == "Type" && header[2] == "Message" + } + return false +} + +func formatCSVFields(fields []string) string { + escaped := make([]string, 0, len(fields)) + for _, field := range fields { + field = neutralizeCSVFormula(field) + field = strings.ReplaceAll(field, `"`, `""`) + escaped = append(escaped, `"`+field+`"`) + } + return strings.Join(escaped, ",") +} + func neutralizeCSVFormula(s string) string { trimmed := strings.TrimLeftFunc(s, unicode.IsSpace) if trimmed == "" { diff --git a/server/internal/domain/miner/logformat/logformat_test.go b/server/internal/domain/miner/logformat/logformat_test.go index c45f4ec76..724183623 100644 --- a/server/internal/domain/miner/logformat/logformat_test.go +++ b/server/internal/domain/miner/logformat/logformat_test.go @@ -103,3 +103,59 @@ func TestFormatLineToCSVRowNeutralizesFormulaCells(t *testing.T) { }) } } + +func TestWriteSanitizedCSVNeutralizesUploadedCells(t *testing.T) { + input := "Time,Type,Message\n=cmd,INFO,+message\n2026-01-01T00:00:00Z,WARN,\" \t@nested\"\n" + + var got bytes.Buffer + if err := WriteSanitizedCSV(&got, strings.NewReader(input)); err != nil { + t.Fatalf("WriteSanitizedCSV() error = %v", err) + } + + want := "Time,Type,Message\n\"'=cmd\",\"INFO\",\"'+message\"\n\"2026-01-01T00:00:00Z\",\"WARN\",\"' \t@nested\"\n" + if got.String() != want { + t.Fatalf("WriteSanitizedCSV() = %q, want %q", got.String(), want) + } +} + +func TestWriteSanitizedCSVRejectsMalformedInput(t *testing.T) { + tests := []struct { + name string + input string + wantErr string + }{ + { + name: "empty", + input: "", + wantErr: "empty miner log csv", + }, + { + name: "unexpected header", + input: "Timestamp,Message\n2026-01-01T00:00:00Z,hello\n", + wantErr: "unexpected miner log csv header", + }, + { + name: "wrong field count", + input: "Time,Message\n2026-01-01T00:00:00Z,hello,extra\n", + wantErr: "wrong number of fields", + }, + { + name: "malformed row", + input: "Time,Message\n\"unterminated\n", + wantErr: "extraneous or missing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got bytes.Buffer + err := WriteSanitizedCSV(&got, strings.NewReader(tt.input)) + if err == nil { + t.Fatal("WriteSanitizedCSV() error = nil") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("WriteSanitizedCSV() error = %q, want substring %q", err.Error(), tt.wantErr) + } + }) + } +} diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 4f35d9d67..2cbcd1a4b 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -3,6 +3,7 @@ package files import ( "archive/zip" "bufio" + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -253,17 +254,30 @@ func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, } }() - hasher := sha256.New() - written, err := io.CopyBuffer(file, io.TeeReader(reader, hasher), make([]byte, commandArtifactCopyBufferSize)) + data, err := io.ReadAll(reader) if err != nil { - return "", fleeterror.NewInternalErrorf("failed to copy command artifact log: %v", err) + return "", fleeterror.NewInternalErrorf("failed to read command artifact log: %v", err) } - if written != info.Size { - return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: metadata size %d does not match copied size %d", info.ID, info.Size, written) + if int64(len(data)) != info.Size { + return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: metadata size %d does not match copied size %d", info.ID, info.Size, len(data)) } - if actualSHA := hex.EncodeToString(hasher.Sum(nil)); actualSHA != info.SHA256 { + if actualSHA := sha256.Sum256(data); hex.EncodeToString(actualSHA[:]) != info.SHA256 { return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: sha256 mismatch", info.ID) } + + var sanitized bytes.Buffer + if err := logformat.WriteSanitizedCSV(&sanitized, bytes.NewReader(data)); err != nil { + return "", fleeterror.NewInternalErrorf("failed to sanitize command artifact log csv: %v", err) + } + if int64(sanitized.Len()) > logformat.MaxArtifactBytes { + return "", fleeterror.NewPlainError( + fmt.Sprintf("sanitized miner log artifact too large: %d bytes (max: %d bytes)", sanitized.Len(), logformat.MaxArtifactBytes), + connect.CodeResourceExhausted, + ) + } + if _, err := file.Write(sanitized.Bytes()); err != nil { + return "", fleeterror.NewInternalErrorf("failed to write sanitized command artifact log: %v", err) + } if err := file.Sync(); err != nil { return "", fleeterror.NewInternalErrorf("failed to sync command artifact log: %v", err) } diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index 565a8721e..d334be158 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -82,6 +82,7 @@ func TestBundleLogs_SingleFile_MovesToTempWithNameSidecar(t *testing.T) { func TestSaveCommandArtifactLog_MaterializesAndBundlesSingleCSV(t *testing.T) { svc := setupService(t) content := "Time,Message\n2026-01-01T00:00:00Z,\"hello\"\n" + wantContent := "Time,Message\n\"2026-01-01T00:00:00Z\",\"hello\"\n" info, err := svc.SaveCommandArtifact("../../remote-miner-logs.csv", int64(len(content)), checksumOf(content), strings.NewReader(content)) require.NoError(t, err) @@ -93,7 +94,7 @@ func TestSaveCommandArtifactLog_MaterializesAndBundlesSingleCSV(t *testing.T) { assert.True(t, strings.HasSuffix(filepath.Base(filePath), ".csv")) data, err := os.ReadFile(filePath) require.NoError(t, err) - assert.Equal(t, content, string(data)) + assert.Equal(t, wantContent, string(data)) assert.NoDirExists(t, getCommandArtifactDirPath(info.ID)) bundlePath, err := svc.bundleLogs("batch-artifact-single") @@ -103,7 +104,22 @@ func TestSaveCommandArtifactLog_MaterializesAndBundlesSingleCSV(t *testing.T) { fsFile, err := svc.GetBatchLogBundleFile("batch-artifact-single") require.NoError(t, err) assert.Equal(t, filepath.Base(filePath), fsFile.Filename) - assert.Equal(t, content, string(fsFile.Data)) + assert.Equal(t, wantContent, string(fsFile.Data)) +} + +func TestSaveCommandArtifactLog_SanitizesUploadedCSV(t *testing.T) { + svc := setupService(t) + content := "Time,Type,Message\n=cmd,INFO,+message\n2026-01-01T00:00:00Z,WARN,\" \t@nested\"\n" + info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(content)), checksumOf(content), strings.NewReader(content)) + require.NoError(t, err) + + filePath, err := svc.SaveCommandArtifactLog("batch-sanitize-artifact", "aa:bb:cc:dd:ee:ff", info.ID) + require.NoError(t, err) + + data, err := os.ReadFile(filePath) + require.NoError(t, err) + assert.Equal(t, "Time,Type,Message\n\"'=cmd\",\"INFO\",\"'+message\"\n\"2026-01-01T00:00:00Z\",\"WARN\",\"' \t@nested\"\n", string(data)) + assert.NoDirExists(t, getCommandArtifactDirPath(info.ID)) } // TestBundleLogs_MultipleFiles_CreatesZIPWithNameSidecar verifies that when logs from @@ -161,7 +177,8 @@ func TestSaveCommandArtifactLog_BundlesMixedDirectAndRemoteLogsAsZIP(t *testing. svc := setupService(t) directPath, err := svc.SaveLogs("batch-mixed", "aa:bb:cc:dd:ee:01", []string{"direct-line"}) require.NoError(t, err) - remoteContent := "remote-line\n" + remoteContent := "Time,Message\n2026-01-01T00:00:00Z,remote-line\n" + wantRemoteContent := "Time,Message\n\"2026-01-01T00:00:00Z\",\"remote-line\"\n" info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(remoteContent)), checksumOf(remoteContent), strings.NewReader(remoteContent)) require.NoError(t, err) remotePath, err := svc.SaveCommandArtifactLog("batch-mixed", "aa:bb:cc:dd:ee:02", info.ID) @@ -173,7 +190,7 @@ func TestSaveCommandArtifactLog_BundlesMixedDirectAndRemoteLogsAsZIP(t *testing. contents := readZipFileContents(t, bundlePath) assert.Equal(t, "direct-line\n", contents[filepath.Base(directPath)]) - assert.Equal(t, remoteContent, contents[filepath.Base(remotePath)]) + assert.Equal(t, wantRemoteContent, contents[filepath.Base(remotePath)]) } // TestBundleLogs_NoFiles_ReturnsEmpty verifies that bundling a batch with no log files @@ -270,10 +287,10 @@ func TestSaveCommandArtifactLog_RejectsMissingAndCorruptArtifacts(t *testing.T) t.Run("corrupt", func(t *testing.T) { svc := setupService(t) - content := "aaaa" + content := "Time,Message\n,aaaa\n" info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(content)), checksumOf(content), strings.NewReader(content)) require.NoError(t, err) - require.NoError(t, os.WriteFile(filepath.Join(getCommandArtifactDirPath(info.ID), info.Filename), []byte("bbbb"), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(getCommandArtifactDirPath(info.ID), info.Filename), []byte("Time,Message\n,bbbb\n"), 0600)) filePath, err := svc.SaveCommandArtifactLog("batch-corrupt-artifact", "aa:bb:cc:dd:ee:ff", info.ID) @@ -288,6 +305,45 @@ func TestSaveCommandArtifactLog_RejectsMissingAndCorruptArtifacts(t *testing.T) }) } +func TestSaveCommandArtifactLog_RejectsMalformedCSV(t *testing.T) { + cases := []struct { + name string + content string + wantErr string + }{ + { + name: "unexpected header", + content: "Timestamp,Message\n2026-01-01T00:00:00Z,hello\n", + wantErr: "unexpected miner log csv header", + }, + { + name: "malformed row", + content: "Time,Message\n\"unterminated\n", + wantErr: "read csv row", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + svc := setupService(t) + info, err := svc.SaveCommandArtifact("remote-miner-logs.csv", int64(len(tc.content)), checksumOf(tc.content), strings.NewReader(tc.content)) + require.NoError(t, err) + + filePath, err := svc.SaveCommandArtifactLog("batch-malformed-artifact", "aa:bb:cc:dd:ee:ff", info.ID) + + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + assert.Empty(t, filePath) + entries, readErr := os.ReadDir(getBatchLogsDirPath("batch-malformed-artifact")) + if !os.IsNotExist(readErr) { + require.NoError(t, readErr) + assert.Empty(t, entries) + } + assert.DirExists(t, getCommandArtifactDirPath(info.ID)) + }) + } +} + func TestSaveCommandArtifactLog_RejectsOversizedMinerLogs(t *testing.T) { svc := setupService(t) content := bytes.Repeat([]byte("x"), int(logformat.MaxArtifactBytes)+1) From 5dfa8f497ceb8b7a7502ecfccc5fc2b93986454b Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:06:53 -0700 Subject: [PATCH 08/15] fix: preserve partial fleet-node miner logs --- server/cmd/fleetnode/minercommand.go | 6 ++-- server/cmd/fleetnode/minercommand_test.go | 22 ++++++++++---- .../internal/domain/miner/remotenode/miner.go | 11 +++++-- .../domain/miner/remotenode/miner_test.go | 30 +++++++++++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/server/cmd/fleetnode/minercommand.go b/server/cmd/fleetnode/minercommand.go index 66d0db01d..6d48301fb 100644 --- a/server/cmd/fleetnode/minercommand.go +++ b/server/cmd/fleetnode/minercommand.go @@ -299,9 +299,6 @@ func runMinerAction(ctx context.Context, client gatewayClient, commandID string, if err != nil { return nil, err } - if moreData { - return nil, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log download returned partial data; retry after partial log pagination is supported") - } payload, err := minerLogsArtifactPayload(logData, caps[sdk.CapabilityLogLevels]) if err != nil { return nil, err @@ -309,6 +306,9 @@ func runMinerAction(ctx context.Context, client gatewayClient, commandID string, if _, err := uploadMinerLogsArtifact(ctx, client, commandID, mc.GetTarget().GetDeviceIdentifier(), payload); err != nil { return nil, err } + if moreData { + return nil, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "uploaded partial miner log data; retry after partial log pagination is supported") + } return nil, nil default: return nil, cmdErr(pb.AckCode_ACK_CODE_BAD_REQUEST, "unrecognized miner command action") diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index 11ef6e6b4..7f88a312b 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -658,15 +658,25 @@ func TestHandleMinerCommand_DownloadLogsAcksFailureWhenUploadFails(t *testing.T) assert.NotEmpty(t, fake.artifactUploads()) } -func TestHandleMinerCommand_DownloadLogsRejectsPartialData(t *testing.T) { +func TestHandleMinerCommand_DownloadLogsUploadsPartialDataThenAcksPartial(t *testing.T) { ctrl := gomock.NewController(t) dev := mocks.NewMockDevice(ctrl) - dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return("2026-02-24 07:52:12 partial log line\n", true, nil) + logData := "2026-02-24 07:52:12 partial log line\n" + wantBody := "Time,Message\n\"2026-02-24 07:52:12\",\"partial log line\"\n" + dev.EXPECT().DownloadLogs(gomock.Any(), nil, "batch-1").Return(logData, true, nil) dev.EXPECT().Close(gomock.Any()).Return(nil) drv := mocks.NewMockDriver(ctrl) drv.EXPECT().NewDevice(gomock.Any(), "dev-1", gomock.Any(), gomock.Any()).Return(sdk.NewDeviceResult{Device: dev}, nil) drv.EXPECT().DescribeDriver(gomock.Any()).Return(sdk.DriverIdentifier{}, sdk.Capabilities{}, nil) - fake := &fakeFleetNodeGateway{} + sum := sha256.Sum256([]byte(wantBody)) + sha := hex.EncodeToString(sum[:]) + fake := &fakeFleetNodeGateway{commandArtifactRef: &pb.CommandArtifactRef{ + ArtifactId: "artifact-1", + Purpose: pb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: minerLogsArtifactFilename, + SizeBytes: int64(len(wantBody)), + Sha256: sha, + }} server := newFakeServer(t, fake) client := fleetnodegatewayv1connect.NewFleetNodeGatewayServiceClient(server.Client(), server.URL) r := &RunCmd{driverGetter: fakeDriverGetter{d: drv}, minerSecrets: nodeSecretProvider{}} @@ -678,8 +688,10 @@ func TestHandleMinerCommand_DownloadLogsRejectsPartialData(t *testing.T) { got := ack.only(t) assert.Equal(t, pb.AckCode_ACK_CODE_PARTIAL, got.GetCode()) assert.False(t, got.GetSucceeded()) - assert.Contains(t, got.GetErrorMessage(), "partial data") - assert.Empty(t, fake.artifactUploads()) + assert.Contains(t, got.GetErrorMessage(), "uploaded partial") + uploads := fake.artifactUploads() + require.Len(t, uploads, 2) + assert.Equal(t, wantBody, string(uploads[1].GetChunk().GetData())) } func TestHandleMinerCommand_DownloadLogsRejectsOversizedLogs(t *testing.T) { diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index 4be822aae..5330c544e 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -397,18 +397,23 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { if err != nil { return err } - if err := ackToError(ack); err != nil { - return err + ackErr := ackToError(ack) + code := ack.GetCode() + if code != gatewaypb.AckCode_ACK_CODE_OK && code != gatewaypb.AckCode_ACK_CODE_PARTIAL { + return ackErr } ref, ok := minerLogsArtifactRef(refs) if !ok { + if ackErr != nil { + return ackErr + } return fleeterror.NewInternalError("fleet node reported log download success without uploaded log artifact") } if _, err := m.logArtifacts.SaveCommandArtifactLog(batchLogUUID, m.desc.GetMacAddress(), ref.GetArtifactId()); err != nil { return fleeterror.NewInternalErrorf("failed to save fleet node miner logs: %v", err) } - return nil + return ackErr } func (m *Miner) FirmwareUpdate(_ context.Context, _ sdk.FirmwareFile) error { diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index f42a2e58e..ef83a8275 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -889,6 +889,36 @@ func TestMiner_DownloadLogsRequiresUploadedArtifactRef(t *testing.T) { assert.Empty(t, saver.artifactID) } +func TestMiner_DownloadLogsMaterializesPartialArtifactBeforeReturningError(t *testing.T) { + // Arrange + s := &fakeSender{ + ack: &gatewaypb.ControlAck{ + Succeeded: false, + Code: gatewaypb.AckCode_ACK_CODE_PARTIAL, + ErrorMessage: "uploaded partial miner log data", + }, + refs: []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, + } + saver := &fakeLogArtifactSaver{} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.Contains(t, err.Error(), "uploaded partial miner log data") + assert.Equal(t, "batch-1", saver.batchLogUUID) + assert.Equal(t, "AA:BB:CC:DD:EE:FF", saver.macAddress) + assert.Equal(t, "artifact-1", saver.artifactID) +} + func TestMiner_DownloadLogsNoActiveStreamIsRetryable(t *testing.T) { // Arrange m := newTestMinerWithLogSaver(t, &fakeSender{err: control.ErrNoActiveStream}, &fakeLogArtifactSaver{}) From 1d2f65f457f5a69bf670618041b0c62946589aad Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:22:16 -0700 Subject: [PATCH 09/15] Address fleet-node log download review feedback (#599) - Treat materialized partial log artifacts as terminal to avoid duplicate retries - Gate remote log downloads to artifact upload capacity per fleet node --- .../domain/fleetnode/control/registry.go | 4 +- .../domain/fleetnode/control/registry_test.go | 4 +- .../domain/fleetnode/control/stream.go | 2 +- .../internal/domain/miner/remotenode/gate.go | 7 ++ .../internal/domain/miner/remotenode/miner.go | 27 +++++- .../domain/miner/remotenode/miner_test.go | 84 ++++++++++++++++++- server/internal/domain/miner/service.go | 5 +- 7 files changed, 121 insertions(+), 12 deletions(-) diff --git a/server/internal/domain/fleetnode/control/registry.go b/server/internal/domain/fleetnode/control/registry.go index 2beaba249..0c110aadc 100644 --- a/server/internal/domain/fleetnode/control/registry.go +++ b/server/internal/domain/fleetnode/control/registry.go @@ -51,9 +51,9 @@ const commandEventBuffer = 64 // one node enqueue without serializing behind the gateway's single drain loop. const outgoingBuffer = 64 -// maxConcurrentCommandArtifactUploadsPerFleetNode bounds upload streams before +// MaxConcurrentCommandArtifactUploadsPerFleetNode bounds upload streams before // their first message can be matched to a command expectation. -const maxConcurrentCommandArtifactUploadsPerFleetNode = 2 +const MaxConcurrentCommandArtifactUploadsPerFleetNode = 2 const maxConcurrentCommandArtifactDownloadsPerFleetNode = 2 diff --git a/server/internal/domain/fleetnode/control/registry_test.go b/server/internal/domain/fleetnode/control/registry_test.go index 367f7b93f..2dfab656d 100644 --- a/server/internal/domain/fleetnode/control/registry_test.go +++ b/server/internal/domain/fleetnode/control/registry_test.go @@ -1046,7 +1046,7 @@ func TestCommandArtifactUploadSlotsLimitConcurrentStreams(t *testing.T) { defer stream.Unregister() var releases []ArtifactTransferRelease - for range maxConcurrentCommandArtifactUploadsPerFleetNode { + for range MaxConcurrentCommandArtifactUploadsPerFleetNode { release, err := r.AcquireCommandArtifactUpload(fleetNodeID) require.NoError(t, err) releases = append(releases, release) @@ -1074,7 +1074,7 @@ func TestCommandArtifactUploadSlotSurvivesControlStreamReconnect(t *testing.T) { _ = r.Register(fleetNodeID) var releases []ArtifactTransferRelease - for range maxConcurrentCommandArtifactUploadsPerFleetNode - 1 { + for range MaxConcurrentCommandArtifactUploadsPerFleetNode - 1 { nextRelease, err := r.AcquireCommandArtifactUpload(fleetNodeID) require.NoError(t, err) releases = append(releases, nextRelease) diff --git a/server/internal/domain/fleetnode/control/stream.go b/server/internal/domain/fleetnode/control/stream.go index 9dabcdbaf..68526acf5 100644 --- a/server/internal/domain/fleetnode/control/stream.go +++ b/server/internal/domain/fleetnode/control/stream.go @@ -93,7 +93,7 @@ type ArtifactTransferRelease func() // stream's first message is read. The returned release is bound to this lease, // not to the fleet node's replaceable ControlStream connection. func (r *Registry) AcquireCommandArtifactUpload(fleetNodeID int64) (ArtifactTransferRelease, error) { - return r.acquireCommandArtifactSlot(fleetNodeID, r.commandArtifactUploads, maxConcurrentCommandArtifactUploadsPerFleetNode) + return r.acquireCommandArtifactSlot(fleetNodeID, r.commandArtifactUploads, MaxConcurrentCommandArtifactUploadsPerFleetNode) } // AcquireCommandArtifactDownload reserves one per-node download slot before the diff --git a/server/internal/domain/miner/remotenode/gate.go b/server/internal/domain/miner/remotenode/gate.go index 6ada26974..2c91b6cb9 100644 --- a/server/internal/domain/miner/remotenode/gate.go +++ b/server/internal/domain/miner/remotenode/gate.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "sync" + + "github.com/block/proto-fleet/server/internal/domain/fleetnode/control" ) // DefaultPerNodeCommandLimit caps in-flight commands to one fleet node, held below the @@ -11,6 +13,11 @@ import ( // backlog) rather than oversubscribing the node and being rejected BUSY. const DefaultPerNodeCommandLimit = 8 +// DefaultPerNodeLogDownloadLimit matches the gateway's per-node command artifact +// upload capacity so same-node log batches wait server-side instead of overrunning +// UploadCommandArtifact stream admission. +const DefaultPerNodeLogDownloadLimit = control.MaxConcurrentCommandArtifactUploadsPerFleetNode + // Gate bounds concurrent commands to a single fleet node. type Gate interface { Acquire(ctx context.Context, fleetNodeID int64) (release func(), err error) diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index 5330c544e..f1989d55c 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -61,6 +61,9 @@ type Config struct { // Gate, if set, bounds concurrent commands the server has in flight to this // fleet node so a large batch paces rather than oversubscribing the node. Gate Gate + // LogDownloadGate, if set, further bounds concurrent log downloads to this + // fleet node so artifact uploads stay within gateway admission capacity. + LogDownloadGate Gate DeviceIdentifier string DriverName string @@ -82,6 +85,7 @@ type Config struct { type Miner struct { sender CommandSender gate Gate + logGate Gate logArtifacts LogArtifactSaver fleetNodeID int64 orgID int64 @@ -113,6 +117,7 @@ func New(cfg Config) (*Miner, error) { return &Miner{ sender: cfg.Sender, gate: cfg.Gate, + logGate: cfg.LogDownloadGate, logArtifacts: cfg.LogArtifacts, fleetNodeID: cfg.FleetNodeID, orgID: cfg.OrgID, @@ -201,15 +206,23 @@ func (m *Miner) send(ctx context.Context, mc *gatewaypb.MinerCommand) (*gatewayp } func (m *Miner) acquireGate(ctx context.Context) (func(), error) { - if m.gate == nil { + return acquireFleetNodeGate(ctx, m.gate, m.fleetNodeID, "fleet node command") +} + +func (m *Miner) acquireLogDownloadGate(ctx context.Context) (func(), error) { + return acquireFleetNodeGate(ctx, m.logGate, m.fleetNodeID, "fleet node log download") +} + +func acquireFleetNodeGate(ctx context.Context, gate Gate, fleetNodeID int64, slotName string) (func(), error) { + if gate == nil { return func() {}, nil } // Pace per fleet node so a large batch can't oversubscribe the node (-> BUSY); // the DB command queue holds the backlog while this worker waits for a slot. - release, err := m.gate.Acquire(ctx, m.fleetNodeID) + release, err := gate.Acquire(ctx, fleetNodeID) if err != nil { return nil, fleeterror.NewPlainError( - fmt.Sprintf("timed out waiting for a fleet node command slot: %v", err), + fmt.Sprintf("timed out waiting for a %s slot: %v", slotName, err), connect.CodeResourceExhausted, ) } @@ -385,6 +398,11 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { return err } defer release() + releaseLogDownload, err := m.acquireLogDownloadGate(ctx) + if err != nil { + return err + } + defer releaseLogDownload() ack, refs, err := m.sendWithoutGateWithArtifactResults(ctx, &gatewaypb.MinerCommand{Action: &gatewaypb.MinerCommand_DownloadLogs{ DownloadLogs: &gatewaypb.DownloadLogsAction{BatchLogUuid: batchLogUUID}, @@ -413,6 +431,9 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { if _, err := m.logArtifacts.SaveCommandArtifactLog(batchLogUUID, m.desc.GetMacAddress(), ref.GetArtifactId()); err != nil { return fleeterror.NewInternalErrorf("failed to save fleet node miner logs: %v", err) } + if code == gatewaypb.AckCode_ACK_CODE_PARTIAL { + return nil + } return ackErr } diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index ef83a8275..22a3781a3 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -115,6 +115,48 @@ func newTestMinerWithGate(t *testing.T, s CommandSender, gate Gate) *Miner { return m } +func newTestMinerWithLogDownloadGate(t *testing.T, s CommandSender, gate Gate, logGate Gate, saver LogArtifactSaver) *Miner { + t.Helper() + m, err := New(Config{ + Sender: s, Gate: gate, LogDownloadGate: logGate, FleetNodeID: 7, OrgID: 1, + LogArtifacts: saver, + DeviceIdentifier: "dev-1", DriverName: "virtual", + IPAddress: "10.0.0.5", Port: "4028", URLScheme: "http", + SerialNumber: "SN1", MacAddress: "AA:BB:CC:DD:EE:FF", + }) + require.NoError(t, err) + return m +} + +type blockingArtifactSender struct { + started chan struct{} + release chan struct{} +} + +func (s *blockingArtifactSender) SendCommand(_ context.Context, _ int64, _ *gatewaypb.ControlCommand) (*gatewaypb.ControlAck, error) { + return nil, fmt.Errorf("unexpected SendCommand") +} + +func (s *blockingArtifactSender) SendCommandWithArtifactResults(ctx context.Context, _ int64, _ *gatewaypb.ControlCommand, _ []control.ArtifactExpectation) (*gatewaypb.ControlAck, []*gatewaypb.CommandArtifactRef, error) { + select { + case s.started <- struct{}{}: + case <-ctx.Done(): + return nil, nil, fmt.Errorf("record upload start: %w", ctx.Err()) + } + select { + case <-s.release: + case <-ctx.Done(): + return nil, nil, fmt.Errorf("wait for upload release: %w", ctx.Err()) + } + return &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}, []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, nil +} + func decodeSent(t *testing.T, s *fakeSender) *gatewaypb.MinerCommand { t.Helper() require.NotNil(t, s.cmd, "no command was sent") @@ -889,7 +931,7 @@ func TestMiner_DownloadLogsRequiresUploadedArtifactRef(t *testing.T) { assert.Empty(t, saver.artifactID) } -func TestMiner_DownloadLogsMaterializesPartialArtifactBeforeReturningError(t *testing.T) { +func TestMiner_DownloadLogsTreatsMaterializedPartialArtifactAsTerminal(t *testing.T) { // Arrange s := &fakeSender{ ack: &gatewaypb.ControlAck{ @@ -912,13 +954,49 @@ func TestMiner_DownloadLogsMaterializesPartialArtifactBeforeReturningError(t *te err := m.DownloadLogs(context.Background(), "batch-1") // Assert - require.Error(t, err) - assert.Contains(t, err.Error(), "uploaded partial miner log data") + require.NoError(t, err) assert.Equal(t, "batch-1", saver.batchLogUUID) assert.Equal(t, "AA:BB:CC:DD:EE:FF", saver.macAddress) assert.Equal(t, "artifact-1", saver.artifactID) } +func TestMiner_DownloadLogsUsesLogDownloadGate(t *testing.T) { + // Arrange + sender := &blockingArtifactSender{ + started: make(chan struct{}, 2), + release: make(chan struct{}), + } + logGate := NewPerNodeLimiter(1) + m1 := newTestMinerWithLogDownloadGate(t, sender, nil, logGate, &fakeLogArtifactSaver{}) + m2 := newTestMinerWithLogDownloadGate(t, sender, nil, logGate, &fakeLogArtifactSaver{}) + errCh := make(chan error, 2) + + // Act: first log download enters the artifact-sending path and holds the gate. + go func() { errCh <- m1.DownloadLogs(context.Background(), "batch-1") }() + select { + case <-sender.started: + case <-time.After(time.Second): + t.Fatal("first log download did not start") + } + go func() { errCh <- m2.DownloadLogs(context.Background(), "batch-1") }() + + // Assert: second log download waits at the log gate instead of starting another + // upload-capable command immediately. + select { + case <-sender.started: + t.Fatal("second log download should wait for the log download gate") + case <-time.After(50 * time.Millisecond): + } + close(sender.release) + select { + case <-sender.started: + case <-time.After(time.Second): + t.Fatal("second log download should start after the first releases the log gate") + } + require.NoError(t, <-errCh) + require.NoError(t, <-errCh) +} + func TestMiner_DownloadLogsNoActiveStreamIsRetryable(t *testing.T) { // Arrange m := newTestMinerWithLogSaver(t, &fakeSender{err: control.ErrNoActiveStream}, &fakeLogArtifactSaver{}) diff --git a/server/internal/domain/miner/service.go b/server/internal/domain/miner/service.go index 6c3ccb86e..efba26435 100644 --- a/server/internal/domain/miner/service.go +++ b/server/internal/domain/miner/service.go @@ -53,7 +53,8 @@ type Service struct { commandSender remotenode.CommandSender // nodeLimiter paces commands per fleet node so a large batch can't oversubscribe // a node. Shared across all remote-node miners (keyed by fleet_node id). - nodeLimiter remotenode.Gate + nodeLimiter remotenode.Gate + nodeLogDownloadLimiter remotenode.Gate // cache stores miner handles keyed by DeviceIdentifier (string). // Both GetMiner and GetMinerFromDeviceIdentifier read from and write to @@ -66,6 +67,7 @@ type Service struct { func (s *Service) WithCommandSender(sender remotenode.CommandSender) *Service { s.commandSender = sender s.nodeLimiter = remotenode.NewPerNodeLimiter(remotenode.DefaultPerNodeCommandLimit) + s.nodeLogDownloadLimiter = remotenode.NewPerNodeLimiter(remotenode.DefaultPerNodeLogDownloadLimit) return s } @@ -249,6 +251,7 @@ func (s *Service) tryFleetNodeMiner(ctx context.Context, deviceID models.DeviceI remoteCommandMiner, err := remotenode.New(remotenode.Config{ Sender: s.commandSender, Gate: s.nodeLimiter, + LogDownloadGate: s.nodeLogDownloadLimiter, LogArtifacts: s.filesService, FleetNodeID: telemetryRoute.FleetNodeID, OrgID: telemetryRoute.OrgID, From 01d9d2cc8aaceee7e1b59efb68f16a3d4b648cf9 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:31:07 -0700 Subject: [PATCH 10/15] Fix oversized fleet-node log ack handling (#599) Return BAD_REQUEST when log size limits are hit before artifact upload so the command fails terminally instead of retrying a PARTIAL ack with no artifact. --- server/cmd/fleetnode/minercommand.go | 4 ++-- server/cmd/fleetnode/minercommand_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/cmd/fleetnode/minercommand.go b/server/cmd/fleetnode/minercommand.go index 6d48301fb..378f1634a 100644 --- a/server/cmd/fleetnode/minercommand.go +++ b/server/cmd/fleetnode/minercommand.go @@ -371,7 +371,7 @@ func uploadMinerLogsArtifact(ctx context.Context, client gatewayClient, commandI func minerLogsArtifactPayload(logData string, includeType bool) ([]byte, error) { if int64(len(logData)) > logformat.MaxArtifactBytes { - return nil, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log data exceeds %d byte download limit", logformat.MaxArtifactBytes) + return nil, cmdErr(pb.AckCode_ACK_CODE_BAD_REQUEST, "miner log data exceeds %d byte download limit", logformat.MaxArtifactBytes) } payload := &limitedBuffer{limit: logformat.MaxArtifactBytes} if err := logformat.WriteTextToCSV(payload, logData, includeType); err != nil { @@ -387,7 +387,7 @@ type limitedBuffer struct { func (b *limitedBuffer) Write(p []byte) (int, error) { if int64(b.Len()+len(p)) > b.limit { - return 0, cmdErr(pb.AckCode_ACK_CODE_PARTIAL, "miner log artifact exceeds %d byte download limit", b.limit) + return 0, cmdErr(pb.AckCode_ACK_CODE_BAD_REQUEST, "miner log artifact exceeds %d byte download limit", b.limit) } n, _ := b.Buffer.Write(p) return n, nil diff --git a/server/cmd/fleetnode/minercommand_test.go b/server/cmd/fleetnode/minercommand_test.go index 7f88a312b..5c02058dc 100644 --- a/server/cmd/fleetnode/minercommand_test.go +++ b/server/cmd/fleetnode/minercommand_test.go @@ -731,7 +731,7 @@ func TestHandleMinerCommand_DownloadLogsRejectsOversizedLogs(t *testing.T) { withTarget(&pb.MinerCommand{Action: &pb.MinerCommand_DownloadLogs{DownloadLogs: &pb.DownloadLogsAction{BatchLogUuid: "batch-1"}}}), discardLogger(t)) got := ack.only(t) - assert.Equal(t, pb.AckCode_ACK_CODE_PARTIAL, got.GetCode()) + assert.Equal(t, pb.AckCode_ACK_CODE_BAD_REQUEST, got.GetCode()) assert.False(t, got.GetSucceeded()) assert.Contains(t, got.GetErrorMessage(), tc.wantErr) assert.Empty(t, fake.artifactUploads()) From 3003cf7a1710c88d0f6244f5f335e10dae734395 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:35:51 -0700 Subject: [PATCH 11/15] Fix fleetnode UI test helper rebuild (#599) Build the fleetnode-ui-test image before the one-shot enrollment helper runs so local manual tests cannot reuse a stale binary that omits the required encryption_pubkey. --- justfile | 3 ++- server/internal/fleetnode/bootstrap/enrollment_test.go | 3 +++ server/internal/fleetnode/bootstrap/handshake_test.go | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 7e2948569..87fb82995 100644 --- a/justfile +++ b/justfile @@ -201,6 +201,7 @@ fleetnode-ui-test-up: -H 'Content-Type: application/json' \ -d '{}' \ http://localhost:4000/onboarding.v1.OnboardingService/GetFleetInitStatus >/dev/null + "${COMPOSE[@]}" build fleetnode-ui-test "${COMPOSE[@]}" run --rm \ -e FLEET_ADMIN_USERNAME \ -e FLEET_ADMIN_PASSWORD \ @@ -208,7 +209,7 @@ fleetnode-ui-test-up: --api-url=http://fleet-api:4000 \ --node-server-url=http://fleet-api:4000 \ --state-dir=/state - "${COMPOSE[@]}" up -d --build fleetnode-ui-test + "${COMPOSE[@]}" up -d fleetnode-ui-test cd ../client GIT_VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") diff --git a/server/internal/fleetnode/bootstrap/enrollment_test.go b/server/internal/fleetnode/bootstrap/enrollment_test.go index 88d93c81d..33c4188a1 100644 --- a/server/internal/fleetnode/bootstrap/enrollment_test.go +++ b/server/internal/fleetnode/bootstrap/enrollment_test.go @@ -40,6 +40,9 @@ func TestRegister_HappyPath(t *testing.T) { assert.Len(t, result.State.IdentityFingerprint, 16) assert.Equal(t, ed25519.PublicKeySize*2, len(result.State.IdentityPublicKeyHex)) assert.Equal(t, ed25519.PrivateKeySize*2, len(result.State.IdentityPrivateKeyHex)) + assert.Equal(t, 32, len(fake.encryptionPub)) + assert.Equal(t, 32*2, len(result.State.EncryptionPublicKeyHex)) + assert.Equal(t, 32*2, len(result.State.EncryptionPrivateKeyHex)) assert.Empty(t, result.State.APIKey, "Register returns partial state without api_key") assert.Empty(t, result.State.SessionToken, "Register returns partial state without session_token") assert.True(t, result.State.AllowInsecureTransport) diff --git a/server/internal/fleetnode/bootstrap/handshake_test.go b/server/internal/fleetnode/bootstrap/handshake_test.go index e69f2998a..9910b0929 100644 --- a/server/internal/fleetnode/bootstrap/handshake_test.go +++ b/server/internal/fleetnode/bootstrap/handshake_test.go @@ -35,6 +35,7 @@ type fakeAgentGateway struct { registered bool signatureVerified bool + encryptionPub []byte } func (f *fakeAgentGateway) Register(_ context.Context, req *connect.Request[pb.RegisterRequest]) (*connect.Response[pb.RegisterResponse], error) { @@ -45,6 +46,7 @@ func (f *fakeAgentGateway) Register(_ context.Context, req *connect.Request[pb.R return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("invalid enrollment code")) } f.identityPub = ed25519.PublicKey(req.Msg.GetIdentityPubkey()) + f.encryptionPub = append([]byte(nil), req.Msg.GetEncryptionPubkey()...) f.registered = true return connect.NewResponse(&pb.RegisterResponse{ FleetNodeId: f.agentID, From 8f2fe03c02ace14ef5d44cf29472a391681cc643 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:51:14 -0700 Subject: [PATCH 12/15] Fail partial fleet-node log downloads terminally (#599) Materialize any uploaded partial artifact, but return a FailedPrecondition so incomplete miner logs are not marked successful or retried. --- .../internal/domain/miner/remotenode/miner.go | 13 ++++++++- .../domain/miner/remotenode/miner_test.go | 28 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index f1989d55c..e647894b1 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -423,6 +423,9 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { ref, ok := minerLogsArtifactRef(refs) if !ok { + if code == gatewaypb.AckCode_ACK_CODE_PARTIAL { + return partialLogDownloadError(ack) + } if ackErr != nil { return ackErr } @@ -432,11 +435,19 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { return fleeterror.NewInternalErrorf("failed to save fleet node miner logs: %v", err) } if code == gatewaypb.AckCode_ACK_CODE_PARTIAL { - return nil + return partialLogDownloadError(ack) } return ackErr } +func partialLogDownloadError(ack *gatewaypb.ControlAck) error { + reason := clampAckReason(ack.GetErrorMessage()) + if reason == "" { + reason = "fleet node reported partial miner log data" + } + return fleeterror.NewFailedPreconditionErrorf("fleet node uploaded incomplete miner logs: %s", reason) +} + func (m *Miner) FirmwareUpdate(_ context.Context, _ sdk.FirmwareFile) error { return errUnsupported("FirmwareUpdate") } diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index 22a3781a3..38a0f8047 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -931,7 +931,7 @@ func TestMiner_DownloadLogsRequiresUploadedArtifactRef(t *testing.T) { assert.Empty(t, saver.artifactID) } -func TestMiner_DownloadLogsTreatsMaterializedPartialArtifactAsTerminal(t *testing.T) { +func TestMiner_DownloadLogsMaterializesPartialArtifactAndFailsTerminally(t *testing.T) { // Arrange s := &fakeSender{ ack: &gatewaypb.ControlAck{ @@ -954,12 +954,36 @@ func TestMiner_DownloadLogsTreatsMaterializedPartialArtifactAsTerminal(t *testin err := m.DownloadLogs(context.Background(), "batch-1") // Assert - require.NoError(t, err) + require.Error(t, err) + assert.True(t, fleeterror.IsFailedPreconditionError(err)) + assert.Contains(t, err.Error(), "uploaded partial miner log data") assert.Equal(t, "batch-1", saver.batchLogUUID) assert.Equal(t, "AA:BB:CC:DD:EE:FF", saver.macAddress) assert.Equal(t, "artifact-1", saver.artifactID) } +func TestMiner_DownloadLogsPartialAckWithoutArtifactFailsTerminally(t *testing.T) { + // Arrange + s := &fakeSender{ + ack: &gatewaypb.ControlAck{ + Succeeded: false, + Code: gatewaypb.AckCode_ACK_CODE_PARTIAL, + ErrorMessage: "miner log data incomplete", + }, + } + saver := &fakeLogArtifactSaver{} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.True(t, fleeterror.IsFailedPreconditionError(err)) + assert.Contains(t, err.Error(), "miner log data incomplete") + assert.Empty(t, saver.artifactID) +} + func TestMiner_DownloadLogsUsesLogDownloadGate(t *testing.T) { // Arrange sender := &blockingArtifactSender{ From a02861cbb0e07ea21f008e0cd1eac86e06a08686 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 13:59:36 -0700 Subject: [PATCH 13/15] Fail oversized remote log downloads terminally (#599) Map DownloadLogs BAD_REQUEST acks to FailedPrecondition so miner-log size-limit failures do not burn queue retries. --- .../internal/domain/miner/remotenode/miner.go | 11 +++++++++ .../domain/miner/remotenode/miner_test.go | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index e647894b1..36306e0bf 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -417,6 +417,9 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { } ackErr := ackToError(ack) code := ack.GetCode() + if code == gatewaypb.AckCode_ACK_CODE_BAD_REQUEST { + return logDownloadRejectedError(ack) + } if code != gatewaypb.AckCode_ACK_CODE_OK && code != gatewaypb.AckCode_ACK_CODE_PARTIAL { return ackErr } @@ -448,6 +451,14 @@ func partialLogDownloadError(ack *gatewaypb.ControlAck) error { return fleeterror.NewFailedPreconditionErrorf("fleet node uploaded incomplete miner logs: %s", reason) } +func logDownloadRejectedError(ack *gatewaypb.ControlAck) error { + reason := clampAckReason(ack.GetErrorMessage()) + if reason == "" { + reason = "fleet node rejected miner log download" + } + return fleeterror.NewFailedPreconditionErrorf("fleet node rejected miner log download: %s", reason) +} + func (m *Miner) FirmwareUpdate(_ context.Context, _ sdk.FirmwareFile) error { return errUnsupported("FirmwareUpdate") } diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index 38a0f8047..806438d0d 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -984,6 +984,29 @@ func TestMiner_DownloadLogsPartialAckWithoutArtifactFailsTerminally(t *testing.T assert.Empty(t, saver.artifactID) } +func TestMiner_DownloadLogsBadRequestFailsTerminally(t *testing.T) { + // Arrange: the fleet node returns BAD_REQUEST for no-artifact log failures such + // as raw logs or formatted CSV exceeding the miner-log artifact cap. + s := &fakeSender{ + ack: &gatewaypb.ControlAck{ + Succeeded: false, + Code: gatewaypb.AckCode_ACK_CODE_BAD_REQUEST, + ErrorMessage: "miner log data exceeds 4194304 byte download limit", + }, + } + saver := &fakeLogArtifactSaver{} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.True(t, fleeterror.IsFailedPreconditionError(err)) + assert.Contains(t, err.Error(), "miner log data exceeds") + assert.Empty(t, saver.artifactID) +} + func TestMiner_DownloadLogsUsesLogDownloadGate(t *testing.T) { // Arrange sender := &blockingArtifactSender{ From 5cd978b5d4369e4ce2e7d4b3bfc8b169d2b08cc8 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 14:28:16 -0700 Subject: [PATCH 14/15] Prevent batch log filename collisions (#599) Open batch log files with O_EXCL and retry with a numeric suffix so same-second MAC collisions cannot truncate earlier miner logs. --- .../internal/infrastructure/files/service.go | 35 ++++++++++++------- .../infrastructure/files/service_test.go | 34 ++++++++++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 2cbcd1a4b..7e047ac56 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -32,6 +32,10 @@ const unknownMACPlaceholder = "unknown" // Used to whitelist-sanitize MAC addresses for use in filenames. var macSafeCharsRe = regexp.MustCompile(`[^0-9a-f]`) +var batchLogTimestamp = func() string { + return time.Now().Format("2006-01-02_15-04-05") +} + // sanitizeMACForFilename strips separators from a MAC address and retains only lowercase // hex characters. If the result is empty (malformed input), it falls back to a safe placeholder. func sanitizeMACForFilename(mac string) string { @@ -172,28 +176,33 @@ func (s *Service) CreateBatchDirIfNotExists(batchLogUUID string) (string, error) return batchDir, nil } -func (s *Service) batchLogFilePath(batchLogUUID string, macAddress string) (string, error) { - batchDir, err := s.CreateBatchDirIfNotExists(batchLogUUID) - if err != nil { - return "", err - } - +func (s *Service) batchLogFileName(macAddress string, attempt int) string { normalizedMAC := sanitizeMACForFilename(macAddress) - timestamp := time.Now().Format("2006-01-02_15-04-05") - filename := fmt.Sprintf("miner-logs-%s-%s.csv", normalizedMAC, timestamp) - return filepath.Join(batchDir, filename), nil + timestamp := batchLogTimestamp() + if attempt == 0 { + return fmt.Sprintf("miner-logs-%s-%s.csv", normalizedMAC, timestamp) + } + return fmt.Sprintf("miner-logs-%s-%s-%d.csv", normalizedMAC, timestamp, attempt) } func (s *Service) openBatchLogFile(batchLogUUID string, macAddress string) (string, *os.File, error) { - filePath, err := s.batchLogFilePath(batchLogUUID, macAddress) + batchDir, err := s.CreateBatchDirIfNotExists(batchLogUUID) if err != nil { return "", nil, err } - file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { + + for attempt := range 1000 { + filePath := filepath.Join(batchDir, s.batchLogFileName(macAddress, attempt)) + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err == nil { + return filePath, file, nil + } + if os.IsExist(err) { + continue + } return "", nil, fleeterror.NewInternalErrorf("failed to create log file: %v", err) } - return filePath, file, nil + return "", nil, fleeterror.NewInternalErrorf("failed to create unique log file for batch %s", batchLogUUID) } func (s *Service) SaveLogs(batchLogUUID string, macAddress string, logLines []string) (string, error) { diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index d334be158..e1aaa367a 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -122,6 +122,40 @@ func TestSaveCommandArtifactLog_SanitizesUploadedCSV(t *testing.T) { assert.NoDirExists(t, getCommandArtifactDirPath(info.ID)) } +func TestSaveCommandArtifactLog_DoesNotOverwriteCollidingBatchLogNames(t *testing.T) { + svc := setupService(t) + originalTimestamp := batchLogTimestamp + batchLogTimestamp = func() string { return "2026-01-01_00-00-00" } + t.Cleanup(func() { batchLogTimestamp = originalTimestamp }) + contentA := "Time,Message\n2026-01-01T00:00:00Z,first\n" + contentB := "Time,Message\n2026-01-01T00:00:01Z,second\n" + infoA, err := svc.SaveCommandArtifact("remote-a.csv", int64(len(contentA)), checksumOf(contentA), strings.NewReader(contentA)) + require.NoError(t, err) + infoB, err := svc.SaveCommandArtifact("remote-b.csv", int64(len(contentB)), checksumOf(contentB), strings.NewReader(contentB)) + require.NoError(t, err) + + pathA, err := svc.SaveCommandArtifactLog("batch-colliding-artifacts", "", infoA.ID) + require.NoError(t, err) + pathB, err := svc.SaveCommandArtifactLog("batch-colliding-artifacts", "", infoB.ID) + require.NoError(t, err) + + require.NotEqual(t, pathA, pathB) + assert.Equal(t, "miner-logs-unknown-2026-01-01_00-00-00.csv", filepath.Base(pathA)) + assert.Equal(t, "miner-logs-unknown-2026-01-01_00-00-00-1.csv", filepath.Base(pathB)) + dataA, err := os.ReadFile(pathA) + require.NoError(t, err) + dataB, err := os.ReadFile(pathB) + require.NoError(t, err) + assert.Equal(t, "Time,Message\n\"2026-01-01T00:00:00Z\",\"first\"\n", string(dataA)) + assert.Equal(t, "Time,Message\n\"2026-01-01T00:00:01Z\",\"second\"\n", string(dataB)) + + bundlePath, err := svc.bundleLogs("batch-colliding-artifacts") + require.NoError(t, err) + contents := readZipFileContents(t, bundlePath) + assert.Equal(t, string(dataA), contents[filepath.Base(pathA)]) + assert.Equal(t, string(dataB), contents[filepath.Base(pathB)]) +} + // TestBundleLogs_MultipleFiles_CreatesZIPWithNameSidecar verifies that when logs from // multiple devices are present they are bundled into a ZIP, and a .name sidecar is // written with a human-readable filename matching the miner-logs-{timestamp}.zip pattern. From bdc7600a0fc947300190eb401d2b647fe818d798 Mon Sep 17 00:00:00 2001 From: Ankit Goswami Date: Fri, 26 Jun 2026 14:40:08 -0700 Subject: [PATCH 15/15] Fix remote log artifact gate and cleanup (#599) --- .../internal/domain/miner/remotenode/miner.go | 19 +++- .../domain/miner/remotenode/miner_test.go | 107 +++++++++++++++++- .../internal/infrastructure/files/service.go | 18 +-- .../infrastructure/files/service_test.go | 4 + 4 files changed, 127 insertions(+), 21 deletions(-) diff --git a/server/internal/domain/miner/remotenode/miner.go b/server/internal/domain/miner/remotenode/miner.go index 36306e0bf..ceea812eb 100644 --- a/server/internal/domain/miner/remotenode/miner.go +++ b/server/internal/domain/miner/remotenode/miner.go @@ -49,6 +49,7 @@ type ArtifactCommandSender interface { type LogArtifactSaver interface { SaveCommandArtifactLog(batchLogUUID string, macAddress string, artifactID string) (string, error) + DeleteCommandArtifact(artifactID string) error } // Config carries everything the adapter needs to address a fleet-node-paired miner. @@ -393,16 +394,16 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { if m.logArtifacts == nil { return fleeterror.NewInternalError("remote-node miner log artifact saver is not configured") } - release, err := m.acquireGate(ctx) + releaseLogDownload, err := m.acquireLogDownloadGate(ctx) if err != nil { return err } - defer release() - releaseLogDownload, err := m.acquireLogDownloadGate(ctx) + defer releaseLogDownload() + release, err := m.acquireGate(ctx) if err != nil { return err } - defer releaseLogDownload() + defer release() ack, refs, err := m.sendWithoutGateWithArtifactResults(ctx, &gatewaypb.MinerCommand{Action: &gatewaypb.MinerCommand_DownloadLogs{ DownloadLogs: &gatewaypb.DownloadLogsAction{BatchLogUuid: batchLogUUID}, @@ -435,6 +436,10 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { return fleeterror.NewInternalError("fleet node reported log download success without uploaded log artifact") } if _, err := m.logArtifacts.SaveCommandArtifactLog(batchLogUUID, m.desc.GetMacAddress(), ref.GetArtifactId()); err != nil { + if fleeterror.IsFailedPreconditionError(err) { + m.deleteRejectedLogArtifact(ref.GetArtifactId()) + return fleeterror.NewFailedPreconditionErrorf("failed to save fleet node miner logs: %v", err) + } return fleeterror.NewInternalErrorf("failed to save fleet node miner logs: %v", err) } if code == gatewaypb.AckCode_ACK_CODE_PARTIAL { @@ -443,6 +448,12 @@ func (m *Miner) DownloadLogs(ctx context.Context, batchLogUUID string) error { return ackErr } +func (m *Miner) deleteRejectedLogArtifact(artifactID string) { + if err := m.logArtifacts.DeleteCommandArtifact(artifactID); err != nil { + slog.Warn("failed to delete rejected fleet node miner log artifact", "artifact_id", artifactID, "error", err) + } +} + func partialLogDownloadError(ack *gatewaypb.ControlAck) error { reason := clampAckReason(ack.GetErrorMessage()) if reason == "" { diff --git a/server/internal/domain/miner/remotenode/miner_test.go b/server/internal/domain/miner/remotenode/miner_test.go index 806438d0d..4c819d16a 100644 --- a/server/internal/domain/miner/remotenode/miner_test.go +++ b/server/internal/domain/miner/remotenode/miner_test.go @@ -48,10 +48,12 @@ func (f *fakeSender) SendCommandWithArtifactResults(_ context.Context, _ int64, } type fakeLogArtifactSaver struct { - batchLogUUID string - macAddress string - artifactID string - err error + batchLogUUID string + macAddress string + artifactID string + deletedArtifactID string + err error + deleteErr error } func (f *fakeLogArtifactSaver) SaveCommandArtifactLog(batchLogUUID string, macAddress string, artifactID string) (string, error) { @@ -64,6 +66,11 @@ func (f *fakeLogArtifactSaver) SaveCommandArtifactLog(batchLogUUID string, macAd return "logs/" + batchLogUUID + "/miner-logs.csv", nil } +func (f *fakeLogArtifactSaver) DeleteCommandArtifact(artifactID string) error { + f.deletedArtifactID = artifactID + return f.deleteErr +} + type blockingSender struct { started chan struct{} } @@ -1007,6 +1014,56 @@ func TestMiner_DownloadLogsBadRequestFailsTerminally(t *testing.T) { assert.Empty(t, saver.artifactID) } +func TestMiner_DownloadLogsDeletesRejectedArtifactAndFailsTerminally(t *testing.T) { + // Arrange + s := &fakeSender{ + ack: &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}, + refs: []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, + } + saver := &fakeLogArtifactSaver{err: fleeterror.NewFailedPreconditionError("malformed miner log artifact")} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.True(t, fleeterror.IsFailedPreconditionError(err)) + assert.Equal(t, "artifact-1", saver.artifactID) + assert.Equal(t, "artifact-1", saver.deletedArtifactID) +} + +func TestMiner_DownloadLogsKeepsArtifactForRetryableMaterializationError(t *testing.T) { + // Arrange + s := &fakeSender{ + ack: &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}, + refs: []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, + } + saver := &fakeLogArtifactSaver{err: fleeterror.NewInternalError("disk unavailable")} + m := newTestMinerWithLogSaver(t, s, saver) + + // Act + err := m.DownloadLogs(context.Background(), "batch-1") + + // Assert + require.Error(t, err) + assert.False(t, fleeterror.IsFailedPreconditionError(err)) + assert.Equal(t, "artifact-1", saver.artifactID) + assert.Empty(t, saver.deletedArtifactID) +} + func TestMiner_DownloadLogsUsesLogDownloadGate(t *testing.T) { // Arrange sender := &blockingArtifactSender{ @@ -1044,6 +1101,48 @@ func TestMiner_DownloadLogsUsesLogDownloadGate(t *testing.T) { require.NoError(t, <-errCh) } +func TestMiner_DownloadLogsWaitsForLogGateBeforeCommandGate(t *testing.T) { + // Arrange + logGate := NewPerNodeLimiter(1) + releaseHeldLogDownload, err := logGate.Acquire(context.Background(), 7) + require.NoError(t, err) + gate := &releasableGate{ + acquired: make(chan int64, 1), + release: make(chan struct{}), + } + s := &fakeSender{ + ack: &gatewaypb.ControlAck{Succeeded: true, Code: gatewaypb.AckCode_ACK_CODE_OK}, + refs: []*gatewaypb.CommandArtifactRef{{ + ArtifactId: "artifact-1", + Purpose: gatewaypb.CommandArtifactPurpose_COMMAND_ARTIFACT_PURPOSE_MINER_LOGS, + Filename: "logs.csv", + SizeBytes: 123, + Sha256: "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7", + }}, + } + m := newTestMinerWithLogDownloadGate(t, s, gate, logGate, &fakeLogArtifactSaver{}) + errCh := make(chan error, 1) + + // Act + go func() { errCh <- m.DownloadLogs(context.Background(), "batch-1") }() + + // Assert: waiting for log artifact capacity must not occupy a general command slot. + select { + case <-gate.acquired: + t.Fatal("general command gate should wait until log download gate is available") + case <-time.After(50 * time.Millisecond): + } + releaseHeldLogDownload() + select { + case fleetNodeID := <-gate.acquired: + assert.Equal(t, int64(7), fleetNodeID) + case <-time.After(time.Second): + t.Fatal("general command gate should be acquired after log download gate is available") + } + close(gate.release) + require.NoError(t, <-errCh) +} + func TestMiner_DownloadLogsNoActiveStreamIsRetryable(t *testing.T) { // Arrange m := newTestMinerWithLogSaver(t, &fakeSender{err: control.ErrNoActiveStream}, &fakeLogArtifactSaver{}) diff --git a/server/internal/infrastructure/files/service.go b/server/internal/infrastructure/files/service.go index 7e047ac56..bdc27fdfd 100644 --- a/server/internal/infrastructure/files/service.go +++ b/server/internal/infrastructure/files/service.go @@ -17,8 +17,6 @@ import ( "sync" "time" - "connectrpc.com/connect" - "github.com/block/proto-fleet/server/internal/domain/fleeterror" "github.com/block/proto-fleet/server/internal/domain/miner/logformat" ) @@ -239,10 +237,7 @@ func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, } defer reader.Close() if info.Size > logformat.MaxArtifactBytes { - return "", fleeterror.NewPlainError( - fmt.Sprintf("miner log artifact too large: %d bytes (max: %d bytes)", info.Size, logformat.MaxArtifactBytes), - connect.CodeResourceExhausted, - ) + return "", fleeterror.NewFailedPreconditionErrorf("miner log artifact too large: %d bytes (max: %d bytes)", info.Size, logformat.MaxArtifactBytes) } filePath, file, err := s.openBatchLogFile(batchLogUUID, macAddress) @@ -268,21 +263,18 @@ func (s *Service) SaveCommandArtifactLog(batchLogUUID string, macAddress string, return "", fleeterror.NewInternalErrorf("failed to read command artifact log: %v", err) } if int64(len(data)) != info.Size { - return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: metadata size %d does not match copied size %d", info.ID, info.Size, len(data)) + return "", fleeterror.NewFailedPreconditionErrorf("corrupt command artifact %s: metadata size %d does not match copied size %d", info.ID, info.Size, len(data)) } if actualSHA := sha256.Sum256(data); hex.EncodeToString(actualSHA[:]) != info.SHA256 { - return "", fleeterror.NewInternalErrorf("corrupt command artifact %s: sha256 mismatch", info.ID) + return "", fleeterror.NewFailedPreconditionErrorf("corrupt command artifact %s: sha256 mismatch", info.ID) } var sanitized bytes.Buffer if err := logformat.WriteSanitizedCSV(&sanitized, bytes.NewReader(data)); err != nil { - return "", fleeterror.NewInternalErrorf("failed to sanitize command artifact log csv: %v", err) + return "", fleeterror.NewFailedPreconditionErrorf("failed to sanitize command artifact log csv: %v", err) } if int64(sanitized.Len()) > logformat.MaxArtifactBytes { - return "", fleeterror.NewPlainError( - fmt.Sprintf("sanitized miner log artifact too large: %d bytes (max: %d bytes)", sanitized.Len(), logformat.MaxArtifactBytes), - connect.CodeResourceExhausted, - ) + return "", fleeterror.NewFailedPreconditionErrorf("sanitized miner log artifact too large: %d bytes (max: %d bytes)", sanitized.Len(), logformat.MaxArtifactBytes) } if _, err := file.Write(sanitized.Bytes()); err != nil { return "", fleeterror.NewInternalErrorf("failed to write sanitized command artifact log: %v", err) diff --git a/server/internal/infrastructure/files/service_test.go b/server/internal/infrastructure/files/service_test.go index e1aaa367a..47797f0f7 100644 --- a/server/internal/infrastructure/files/service_test.go +++ b/server/internal/infrastructure/files/service_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/block/proto-fleet/server/internal/domain/fleeterror" "github.com/block/proto-fleet/server/internal/domain/miner/logformat" ) @@ -330,6 +331,7 @@ func TestSaveCommandArtifactLog_RejectsMissingAndCorruptArtifacts(t *testing.T) require.Error(t, err) assert.Contains(t, err.Error(), "sha256 mismatch") + assert.True(t, fleeterror.IsFailedPreconditionError(err)) assert.Empty(t, filePath) entries, readErr := os.ReadDir(getBatchLogsDirPath("batch-corrupt-artifact")) if !os.IsNotExist(readErr) { @@ -367,6 +369,7 @@ func TestSaveCommandArtifactLog_RejectsMalformedCSV(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), tc.wantErr) + assert.True(t, fleeterror.IsFailedPreconditionError(err)) assert.Empty(t, filePath) entries, readErr := os.ReadDir(getBatchLogsDirPath("batch-malformed-artifact")) if !os.IsNotExist(readErr) { @@ -388,6 +391,7 @@ func TestSaveCommandArtifactLog_RejectsOversizedMinerLogs(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "miner log artifact too large") + assert.True(t, fleeterror.IsFailedPreconditionError(err)) assert.Empty(t, filePath) assert.NoDirExists(t, getBatchLogsDirPath("batch-oversized-artifact")) }