From c71db28bd059f557a042accdbfd9f32d500f30b6 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Thu, 15 Jun 2023 15:29:10 +0200 Subject: [PATCH] all: Backport end device batch deletion --- CHANGELOG.md | 2 + api/api.md | 83 +++ api/api.swagger.json | 181 +++++ api/applicationserver.proto | 12 + api/end_device.proto | 11 + api/end_device_services.proto | 20 + api/identifiers.proto | 4 + api/joinserver.proto | 12 + api/networkserver.proto | 11 + cmd/ttn-lw-cli/commands/end_devices.go | 136 ++++ config/messages.json | 36 + pkg/applicationserver/applicationserver.go | 10 + .../applicationserver_util_test.go | 20 +- pkg/applicationserver/grpc_deviceregistry.go | 44 ++ .../grpc_deviceregistry_test.go | 384 +++++++++++ pkg/applicationserver/redis/registry.go | 73 ++ pkg/applicationserver/registry.go | 16 + pkg/applicationserver/registry_test.go | 69 ++ .../bunstore/end_device_location_store.go | 4 +- .../bunstore/end_device_store.go | 70 ++ pkg/identityserver/bunstore/store_test.go | 1 + pkg/identityserver/end_device_registry.go | 57 ++ .../end_device_registry_test.go | 84 +++ pkg/identityserver/identityserver.go | 3 + pkg/identityserver/store/store_interfaces.go | 5 + .../storetest/end_device_store.go | 131 ++++ pkg/joinserver/grpc_deviceregistry.go | 57 ++ pkg/joinserver/grpc_deviceregistry_test.go | 454 +++++++++++++ pkg/joinserver/joinserver.go | 7 + pkg/joinserver/joinserver_internal_test.go | 61 +- pkg/joinserver/redis/registry.go | 160 ++++- pkg/joinserver/registry.go | 7 + pkg/joinserver/registry_test.go | 167 +++++ pkg/joinserver/util_test.go | 3 + pkg/networkserver/grpc_deviceregistry.go | 46 ++ pkg/networkserver/grpc_deviceregistry_test.go | 382 +++++++++++ .../internal/test/shared/device_registry.go | 85 ++- pkg/networkserver/networkserver.go | 9 +- .../networkserver_util_internal_test.go | 31 +- pkg/networkserver/redis/registry.go | 128 +++- pkg/networkserver/registry.go | 13 + pkg/ttnpb/applicationserver.pb.go | 73 +- pkg/ttnpb/applicationserver.pb.gw.go | 175 +++++ pkg/ttnpb/applicationserver_grpc.pb.go | 97 +++ pkg/ttnpb/end_device.pb.go | 643 ++++++++++-------- pkg/ttnpb/end_device.pb.paths.fm.go | 10 + pkg/ttnpb/end_device.pb.setters.fm.go | 45 ++ pkg/ttnpb/end_device.pb.validate.go | 131 ++++ pkg/ttnpb/end_device_services.pb.go | 54 +- pkg/ttnpb/end_device_services.pb.gw.go | 175 +++++ pkg/ttnpb/end_device_services_grpc.pb.go | 111 +++ pkg/ttnpb/identifiers.pb.go | 108 ++- pkg/ttnpb/identifiers.pb.paths.fm.go | 7 + pkg/ttnpb/identifiers.pb.setters.fm.go | 20 + pkg/ttnpb/identifiers.pb.validate.go | 99 +++ pkg/ttnpb/identifiers_json.pb.go | 63 ++ pkg/ttnpb/joinserver.pb.go | 175 ++--- pkg/ttnpb/joinserver.pb.gw.go | 175 +++++ pkg/ttnpb/joinserver_grpc.pb.go | 97 +++ pkg/ttnpb/networkserver.pb.go | 61 +- pkg/ttnpb/networkserver.pb.gw.go | 175 +++++ pkg/ttnpb/networkserver_grpc.pb.go | 97 +++ sdk/js/generated/api-definition.json | 56 ++ sdk/js/generated/api.json | 208 ++++++ 64 files changed, 5455 insertions(+), 489 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c029f8b112..8c79ae128a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ For details about compatibility between different releases, see the **Commitment ### Added - The `as-db purge` command to purge unused data from the Application Server database. +- RPCs and CLI command to delete a batch of end devices within an application. + - Check `ttn-lw-cli end-devices batch-delete` for more details. ### Changed diff --git a/api/api.md b/api/api.md index 389c2b3be3..088a682b35 100644 --- a/api/api.md +++ b/api/api.md @@ -45,6 +45,7 @@ - [Enum `AsConfiguration.PubSub.Providers.Status`](#ttn.lorawan.v3.AsConfiguration.PubSub.Providers.Status) - [Service `AppAs`](#ttn.lorawan.v3.AppAs) - [Service `As`](#ttn.lorawan.v3.As) + - [Service `AsEndDeviceBatchRegistry`](#ttn.lorawan.v3.AsEndDeviceBatchRegistry) - [Service `AsEndDeviceRegistry`](#ttn.lorawan.v3.AsEndDeviceRegistry) - [Service `NsAs`](#ttn.lorawan.v3.NsAs) - [File `lorawan-stack/api/applicationserver_integrations_alcsync.proto`](#lorawan-stack/api/applicationserver_integrations_alcsync.proto) @@ -222,6 +223,7 @@ - [Message `ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode`](#ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode) - [Message `ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode`](#ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode) - [Message `ADRSettings.StaticMode`](#ttn.lorawan.v3.ADRSettings.StaticMode) + - [Message `BatchDeleteEndDevicesRequest`](#ttn.lorawan.v3.BatchDeleteEndDevicesRequest) - [Message `BatchUpdateEndDeviceLastSeenRequest`](#ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest) - [Message `BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate`](#ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate) - [Message `BoolValue`](#ttn.lorawan.v3.BoolValue) @@ -264,6 +266,7 @@ - [Message `UpdateEndDeviceRequest`](#ttn.lorawan.v3.UpdateEndDeviceRequest) - [Enum `PowerState`](#ttn.lorawan.v3.PowerState) - [File `lorawan-stack/api/end_device_services.proto`](#lorawan-stack/api/end_device_services.proto) + - [Service `EndDeviceBatchRegistry`](#ttn.lorawan.v3.EndDeviceBatchRegistry) - [Service `EndDeviceRegistry`](#ttn.lorawan.v3.EndDeviceRegistry) - [Service `EndDeviceTemplateConverter`](#ttn.lorawan.v3.EndDeviceTemplateConverter) - [File `lorawan-stack/api/enums.proto`](#lorawan-stack/api/enums.proto) @@ -337,6 +340,7 @@ - [Message `ApplicationIdentifiers`](#ttn.lorawan.v3.ApplicationIdentifiers) - [Message `ClientIdentifiers`](#ttn.lorawan.v3.ClientIdentifiers) - [Message `EndDeviceIdentifiers`](#ttn.lorawan.v3.EndDeviceIdentifiers) + - [Message `EndDeviceIdentifiersList`](#ttn.lorawan.v3.EndDeviceIdentifiersList) - [Message `EndDeviceVersionIdentifiers`](#ttn.lorawan.v3.EndDeviceVersionIdentifiers) - [Message `EntityIdentifiers`](#ttn.lorawan.v3.EntityIdentifiers) - [Message `GatewayIdentifiers`](#ttn.lorawan.v3.GatewayIdentifiers) @@ -392,6 +396,7 @@ - [Service `ApplicationCryptoService`](#ttn.lorawan.v3.ApplicationCryptoService) - [Service `AsJs`](#ttn.lorawan.v3.AsJs) - [Service `Js`](#ttn.lorawan.v3.Js) + - [Service `JsEndDeviceBatchRegistry`](#ttn.lorawan.v3.JsEndDeviceBatchRegistry) - [Service `JsEndDeviceRegistry`](#ttn.lorawan.v3.JsEndDeviceRegistry) - [Service `NetworkCryptoService`](#ttn.lorawan.v3.NetworkCryptoService) - [Service `NsJs`](#ttn.lorawan.v3.NsJs) @@ -527,6 +532,7 @@ - [Service `AsNs`](#ttn.lorawan.v3.AsNs) - [Service `GsNs`](#ttn.lorawan.v3.GsNs) - [Service `Ns`](#ttn.lorawan.v3.Ns) + - [Service `NsEndDeviceBatchRegistry`](#ttn.lorawan.v3.NsEndDeviceBatchRegistry) - [Service `NsEndDeviceRegistry`](#ttn.lorawan.v3.NsEndDeviceRegistry) - [File `lorawan-stack/api/notification_service.proto`](#lorawan-stack/api/notification_service.proto) - [Message `CreateNotificationRequest`](#ttn.lorawan.v3.CreateNotificationRequest) @@ -1214,6 +1220,20 @@ The As service manages the Application Server. | `GetLinkStats` | `GET` | `/api/v3/as/applications/{application_id}/link/stats` | | | `GetConfiguration` | `GET` | `/api/v3/as/configuration` | | +### Service `AsEndDeviceBatchRegistry` + +The AsEndDeviceBatchRegistry service allows clients to manage batches end devices on the Application Server. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Delete` | [`BatchDeleteEndDevicesRequest`](#ttn.lorawan.v3.BatchDeleteEndDevicesRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete a list of devices within the same application. This operation is atomic; either all devices are deleted or none. Devices not found are skipped and no error is returned. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Delete` | `DELETE` | `/api/v3/as/applications/{application_ids.application_id}/devices/batch` | | + ### Service `AsEndDeviceRegistry` The AsEndDeviceRegistry service allows clients to manage their end devices on the Application Server. @@ -3437,6 +3457,20 @@ Configuration options for static ADR. | `tx_power_index` |

`uint32.lte`: `15`

| | `nb_trans` |

`uint32.lte`: `15`

`uint32.gte`: `1`

| +### Message `BatchDeleteEndDevicesRequest` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `application_ids` | [`ApplicationIdentifiers`](#ttn.lorawan.v3.ApplicationIdentifiers) | | | +| `device_ids` | [`string`](#string) | repeated | | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `application_ids` |

`message.required`: `true`

| +| `device_ids` |

`repeated.min_items`: `1`

`repeated.max_items`: `20`

`repeated.items.string.max_len`: `36`

`repeated.items.string.pattern`: `^[a-z0-9](?:[-]?[a-z0-9]){2,}$`

| + ### Message `BatchUpdateEndDeviceLastSeenRequest` | Field | Type | Label | Description | @@ -4155,6 +4189,21 @@ Power state of the device. ## File `lorawan-stack/api/end_device_services.proto` +### Service `EndDeviceBatchRegistry` + +The EndDeviceBatchRegistry service, exposed by the Identity Server, is used to manage +end device registrations in batches. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Delete` | [`BatchDeleteEndDevicesRequest`](#ttn.lorawan.v3.BatchDeleteEndDevicesRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete a batch of end devices with the given IDs. This operation is atomic; either all devices are deleted or none. Devices not found are skipped and no error is returned. Before calling this RPC, use the corresponding BatchDelete RPCs of NsEndDeviceRegistry, AsEndDeviceRegistry and optionally the JsEndDeviceRegistry to delete the end devices. If the devices were claimed on a Join Server, use the BatchUnclaim RPC of the DeviceClaimingServer. This is NOT done automatically. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Delete` | `DELETE` | `/api/v3/applications/{application_ids.application_id}/devices/batch` | | + ### Service `EndDeviceRegistry` The EndDeviceRegistry service, exposed by the Identity Server, is used to manage @@ -5065,6 +5114,12 @@ The NsGs service connects a Network Server to a Gateway Server. | `join_eui` |

`bytes.len`: `8`

| | `dev_addr` |

`bytes.len`: `4`

| +### Message `EndDeviceIdentifiersList` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `end_device_ids` | [`EndDeviceIdentifiers`](#ttn.lorawan.v3.EndDeviceIdentifiers) | repeated | | + ### Message `EndDeviceVersionIdentifiers` Identifies an end device model with version information. @@ -5705,6 +5760,20 @@ The AsJs service connects an Application Server to a Join Server. | `GetJoinEUIPrefixes` | `GET` | `/api/v3/js/join_eui_prefixes` | | | `GetDefaultJoinEUI` | `GET` | `/api/v3/js/default_join_eui` | | +### Service `JsEndDeviceBatchRegistry` + +JsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Join Server. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Delete` | [`BatchDeleteEndDevicesRequest`](#ttn.lorawan.v3.BatchDeleteEndDevicesRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete a list of devices within the same application. This operation is atomic; either all devices are deleted or none. Devices not found are skipped and no error is returned. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Delete` | `DELETE` | `/api/v3/js/applications/{application_ids.application_id}/devices/batch` | | + ### Service `JsEndDeviceRegistry` The JsEndDeviceRegistry service allows clients to manage their end devices on the Join Server. @@ -7604,6 +7673,20 @@ The Ns service manages the Network Server. | `GetNetID` | `GET` | `/api/v3/ns/net_id` | | | `GetDeviceAddressPrefixes` | `GET` | `/api/v3/ns/dev_addr_prefixes` | | +### Service `NsEndDeviceBatchRegistry` + +The NsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Network Server. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Delete` | [`BatchDeleteEndDevicesRequest`](#ttn.lorawan.v3.BatchDeleteEndDevicesRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete a list of devices within the same application. This operation is atomic; either all devices are deleted or none. Devices not found are skipped and no error is returned. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Delete` | `DELETE` | `/api/v3/ns/applications/{application_ids.application_id}/devices/batch` | | + ### Service `NsEndDeviceRegistry` The NsEndDeviceRegistry service allows clients to manage their end devices on the Network Server. diff --git a/api/api.swagger.json b/api/api.swagger.json index 291652bbc8..584c4bb88d 100644 --- a/api/api.swagger.json +++ b/api/api.swagger.json @@ -23,6 +23,9 @@ { "name": "AsEndDeviceRegistry" }, + { + "name": "AsEndDeviceBatchRegistry" + }, { "name": "ApplicationUpStorage" }, @@ -62,6 +65,9 @@ { "name": "EndDeviceTemplateConverter" }, + { + "name": "EndDeviceBatchRegistry" + }, { "name": "Events" }, @@ -110,6 +116,9 @@ { "name": "JsEndDeviceRegistry" }, + { + "name": "JsEndDeviceBatchRegistry" + }, { "name": "ApplicationActivationSettingRegistry" }, @@ -128,6 +137,9 @@ { "name": "NsEndDeviceRegistry" }, + { + "name": "NsEndDeviceBatchRegistry" + }, { "name": "NotificationService" }, @@ -984,6 +996,49 @@ ] } }, + "/applications/{application_ids.application_id}/devices/batch": { + "delete": { + "summary": "Delete a batch of end devices with the given IDs.", + "description": "This operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.\nBefore calling this RPC, use the corresponding BatchDelete RPCs\nof NsEndDeviceRegistry, AsEndDeviceRegistry and\noptionally the JsEndDeviceRegistry to delete the end devices.\nIf the devices were claimed on a Join Server, use the BatchUnclaim RPC\nof the DeviceClaimingServer.\nThis is NOT done automatically.", + "operationId": "EndDeviceBatchRegistry_Delete", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": {} + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "application_ids.application_id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "device_ids", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "EndDeviceBatchRegistry" + ] + } + }, "/applications/{application_ids.application_id}/devices/{device_id}": { "delete": { "summary": "Delete the end device with the given IDs.", @@ -1900,6 +1955,48 @@ ] } }, + "/as/applications/{application_ids.application_id}/devices/batch": { + "delete": { + "summary": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "operationId": "AsEndDeviceBatchRegistry_Delete", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": {} + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "application_ids.application_id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "device_ids", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "AsEndDeviceBatchRegistry" + ] + } + }, "/as/applications/{application_ids.application_id}/devices/{device_id}": { "delete": { "summary": "Delete deletes the device that matches the given identifiers.\nIf there are multiple matches, an error will be returned.", @@ -9717,6 +9814,48 @@ ] } }, + "/js/applications/{application_ids.application_id}/devices/batch": { + "delete": { + "summary": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "operationId": "JsEndDeviceBatchRegistry_Delete", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": {} + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "application_ids.application_id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "device_ids", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "JsEndDeviceBatchRegistry" + ] + } + }, "/js/applications/{application_ids.application_id}/devices/{device_id}": { "delete": { "summary": "Delete deletes the device that matches the given identifiers.\nIf there are multiple matches, an error will be returned.", @@ -10711,6 +10850,48 @@ ] } }, + "/ns/applications/{application_ids.application_id}/devices/batch": { + "delete": { + "summary": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "operationId": "NsEndDeviceBatchRegistry_Delete", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "type": "object", + "properties": {} + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "application_ids.application_id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "device_ids", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "NsEndDeviceBatchRegistry" + ] + } + }, "/ns/applications/{application_ids.application_id}/devices/{device_id}": { "delete": { "summary": "Delete deletes the device that matches the given identifiers.\nIf there are multiple matches, an error will be returned.", diff --git a/api/applicationserver.proto b/api/applicationserver.proto index 90bbc62253..67b87fb564 100644 --- a/api/applicationserver.proto +++ b/api/applicationserver.proto @@ -289,3 +289,15 @@ service AsEndDeviceRegistry { }; }; } + +// The AsEndDeviceBatchRegistry service allows clients to manage batches end devices on the Application Server. +service AsEndDeviceBatchRegistry { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + rpc Delete(BatchDeleteEndDevicesRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/as/applications/{application_ids.application_id}/devices/batch" + }; + }; +} diff --git a/api/end_device.proto b/api/end_device.proto index 81d9937c56..a52194bb53 100644 --- a/api/end_device.proto +++ b/api/end_device.proto @@ -930,3 +930,14 @@ message ConvertEndDeviceTemplateRequest { // End device profile identifiers. EndDeviceVersionIdentifiers end_device_version_ids = 3; } + +message BatchDeleteEndDevicesRequest { + ttn.lorawan.v3.ApplicationIdentifiers application_ids = 1 [(validate.rules).message.required = true]; + repeated string device_ids = 2 [ + (validate.rules).repeated = { + min_items: 1, + max_items: 20, + items: { string: { pattern: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$", max_len: 36 } } + } + ]; +} diff --git a/api/end_device_services.proto b/api/end_device_services.proto index c2fbe139d3..55792130a4 100644 --- a/api/end_device_services.proto +++ b/api/end_device_services.proto @@ -109,3 +109,23 @@ service EndDeviceTemplateConverter { }; }; } + +// The EndDeviceBatchRegistry service, exposed by the Identity Server, is used to manage +// end device registrations in batches. +service EndDeviceBatchRegistry { + // Delete a batch of end devices with the given IDs. + // + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + // Before calling this RPC, use the corresponding BatchDelete RPCs + // of NsEndDeviceRegistry, AsEndDeviceRegistry and + // optionally the JsEndDeviceRegistry to delete the end devices. + // If the devices were claimed on a Join Server, use the BatchUnclaim RPC + // of the DeviceClaimingServer. + // This is NOT done automatically. + rpc Delete(BatchDeleteEndDevicesRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/applications/{application_ids.application_id}/devices/batch" + }; + }; +} diff --git a/api/identifiers.proto b/api/identifiers.proto index 221f8ea899..9a1f3ef186 100644 --- a/api/identifiers.proto +++ b/api/identifiers.proto @@ -193,3 +193,7 @@ message LoRaAllianceProfileIdentifiers { // ID of the LoRaWAN end device profile assigned by the vendor. uint32 vendor_profile_id = 2; } + +message EndDeviceIdentifiersList { + repeated EndDeviceIdentifiers end_device_ids = 1; +} diff --git a/api/joinserver.proto b/api/joinserver.proto index 8b733a6d58..9d4d81a4c5 100644 --- a/api/joinserver.proto +++ b/api/joinserver.proto @@ -321,6 +321,18 @@ service JsEndDeviceRegistry { }; } +// JsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Join Server. +service JsEndDeviceBatchRegistry { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + rpc Delete(BatchDeleteEndDevicesRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/js/applications/{application_ids.application_id}/devices/batch" + }; + }; +} + message ApplicationActivationSettings { option (thethings.flags.message) = { select: true, set: true }; // The KEK label to use for wrapping application keys. diff --git a/api/networkserver.proto b/api/networkserver.proto index b2a64ea183..350de6ebaa 100644 --- a/api/networkserver.proto +++ b/api/networkserver.proto @@ -167,3 +167,14 @@ service NsEndDeviceRegistry { }; }; } +// The NsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Network Server. +service NsEndDeviceBatchRegistry { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + rpc Delete(BatchDeleteEndDevicesRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/ns/applications/{application_ids.application_id}/devices/batch" + }; + }; +} diff --git a/cmd/ttn-lw-cli/commands/end_devices.go b/cmd/ttn-lw-cli/commands/end_devices.go index 39560f8562..636cc2ae23 100644 --- a/cmd/ttn-lw-cli/commands/end_devices.go +++ b/cmd/ttn-lw-cli/commands/end_devices.go @@ -1123,6 +1123,127 @@ var ( return deleteEndDevice(ctx, devID, skipClusterJS) }, } + endDevicesBatchDeleteCommand = &cobra.Command{ + Use: "batch-delete [application-id] [device-ids]", + Short: "Delete a batch of end devices within the same application (EXPERIMENTAL).", + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkComponentsEnabled(); err != nil { + return err + } + var ( + appID *ttnpb.ApplicationIdentifiers + devIDs = make([]*ttnpb.EndDeviceIdentifiers, 0) + ) + if inputDecoder != nil { + dec := struct { + ApplicationID string `json:"application_id"` + DeviceIDs []string `json:"device_ids"` + }{} + err := inputDecoder.Decode(&dec) + if err != nil { + return err + } + appID = &ttnpb.ApplicationIdentifiers{ + ApplicationId: dec.ApplicationID, + } + for _, devID := range dec.DeviceIDs { + devIDs = append(devIDs, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appID, + DeviceId: devID, + }) + } + } else if len(args) < 2 { + return errNoIDs.New() + } else { + appID = &ttnpb.ApplicationIdentifiers{ + ApplicationId: args[0], + } + for _, arg := range args[1:] { + devIDs = append(devIDs, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appID, + DeviceId: arg, + }) + } + } + + is, err := api.Dial(ctx, config.IdentityServerGRPCAddress) + if err != nil { + return err + } + del := make([]string, 0) + for _, devID := range devIDs { + dev, err := ttnpb.NewEndDeviceRegistryClient(is).Get(ctx, &ttnpb.GetEndDeviceRequest{ + EndDeviceIds: devID, + FieldMask: ttnpb.FieldMask( + "ids", + "network_server_address", + "application_server_address", + "join_server_address", + ), + }) + if err != nil && !errors.IsNotFound(err) { + return err + } + // Check if the device is in the configured cluster. + nsMismatch, asMismatch, jsMismatch := compareServerAddressesEndDevice(dev, config) + if nsMismatch || asMismatch || jsMismatch { + return errAddressMismatchEndDevice.New() + } + del = append(del, devID.DeviceId) + } + + // Batch Delete from JS. + if len(del) > 0 { + js, err := api.Dial(ctx, config.JoinServerGRPCAddress) + if err != nil { + return err + } + _, err = ttnpb.NewJsEndDeviceBatchRegistryClient(js).Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: appID, + DeviceIds: del, + }) + if err != nil { + return err + } + } + + // Batch Delete from AS. + as, err := api.Dial(ctx, config.ApplicationServerGRPCAddress) + if err != nil { + return err + } + _, err = ttnpb.NewAsEndDeviceBatchRegistryClient(as).Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: appID, + DeviceIds: del, + }) + if err != nil { + return err + } + + // Batch Delete from NS. + ns, err := api.Dial(ctx, config.NetworkServerGRPCAddress) + if err != nil { + return err + } + _, err = ttnpb.NewNsEndDeviceBatchRegistryClient(ns).Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: appID, + DeviceIds: del, + }) + if err != nil { + return err + } + + // Delete from IS. + _, err = ttnpb.NewEndDeviceBatchRegistryClient(is).Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: appID, + DeviceIds: del, + }) + if err != nil { + return err + } + return nil + }, + } endDevicesClaimCommand = &cobra.Command{ Use: "claim [application-id]", Short: "Claim an end device (DEPRECATED)", @@ -1446,6 +1567,8 @@ func init() { endDevicesListPhyVersionsCommand.Flags().AddFlagSet(listPhyVersionFlags) endDevicesCommand.AddCommand(endDevicesListPhyVersionsCommand) + endDevicesCommand.AddCommand(endDevicesBatchDeleteCommand) + // Deprecate flags. util.DeprecateFlagWithoutForwarding( endDevicesCreateCommand.Flags(), @@ -1487,3 +1610,16 @@ func compareServerAddressesEndDevice(device *ttnpb.EndDevice, config *Config) (n } return } + +func checkComponentsEnabled() error { + if !config.NetworkServerEnabled { + return errNetworkServerDisabled.New() + } + if !config.ApplicationServerEnabled { + return errApplicationServerDisabled.New() + } + if !config.JoinServerEnabled { + return errJoinServerDisabled.New() + } + return nil +} diff --git a/config/messages.json b/config/messages.json index c4c04d92e2..8a995eb4c7 100644 --- a/config/messages.json +++ b/config/messages.json @@ -9314,6 +9314,15 @@ "file": "observability.go" } }, + "event:as.end_device.batch.delete": { + "translations": { + "en": "batch delete end devices" + }, + "description": { + "package": "pkg/applicationserver", + "file": "grpc_deviceregistry.go" + } + }, "event:as.end_device.create": { "translations": { "en": "create end device" @@ -9926,6 +9935,24 @@ "file": "invitation_registry.go" } }, + "event:is.end_device.batch.delete": { + "translations": { + "en": "batch delete end devices" + }, + "description": { + "package": "pkg/identityserver", + "file": "end_device_registry.go" + } + }, + "event:js.end_device.batch.delete": { + "translations": { + "en": "batch delete end devices" + }, + "description": { + "package": "pkg/joinserver", + "file": "grpc_deviceregistry.go" + } + }, "event:js.end_device.create": { "translations": { "en": "create end device" @@ -10079,6 +10106,15 @@ "file": "observability.go" } }, + "event:ns.end_device.batch.delete": { + "translations": { + "en": "batch delete end devices" + }, + "description": { + "package": "pkg/networkserver", + "file": "grpc_deviceregistry.go" + } + }, "event:ns.end_device.create": { "translations": { "en": "create end device" diff --git a/pkg/applicationserver/applicationserver.go b/pkg/applicationserver/applicationserver.go index 3aa834c525..b20ddf18ff 100644 --- a/pkg/applicationserver/applicationserver.go +++ b/pkg/applicationserver/applicationserver.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package applicationserver provides a LoRaWAN-compliant Application Server implementation. package applicationserver import ( @@ -90,6 +91,8 @@ type ApplicationServer struct { grpc struct { asDevices asEndDeviceRegistryServer appAs ttnpb.AppAsServer + + asBatchDevices asEndDeviceBatchRegistryServer } interopClient InteropClient @@ -224,6 +227,10 @@ func New(c *component.Component, conf *Config) (as *ApplicationServer, err error }), ) + as.grpc.asBatchDevices = asEndDeviceBatchRegistryServer{ + AS: as, + } + ctx, cancel := context.WithCancel(as.Context()) defer func() { if err != nil { @@ -304,6 +311,7 @@ func New(c *component.Component, conf *Config) (as *ApplicationServer, err error "/ttn.lorawan.v3.As", "/ttn.lorawan.v3.NsAs", "/ttn.lorawan.v3.AsEndDeviceRegistry", + "/ttn.lorawan.v3.AsEndDeviceBatchRegistry", "/ttn.lorawan.v3.AppAs", "/ttn.lorawan.v3.ApplicationWebhookRegistry", "/ttn.lorawan.v3.ApplicationPubSubRegistry", @@ -336,6 +344,7 @@ func (as *ApplicationServer) RegisterServices(s *grpc.Server) { if pkgs := as.appPackages; pkgs != nil { pkgs.RegisterServices(s) } + ttnpb.RegisterAsEndDeviceBatchRegistryServer(s, as.grpc.asBatchDevices) } // RegisterHandlers registers gRPC handlers. @@ -352,6 +361,7 @@ func (as *ApplicationServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.Cl if pkgs := as.appPackages; pkgs != nil { pkgs.RegisterHandlers(s, conn) } + ttnpb.RegisterAsEndDeviceBatchRegistryHandler(as.Context(), s, conn) // nolint:errcheck } // RegisterRoutes registers HTTP routes. diff --git a/pkg/applicationserver/applicationserver_util_test.go b/pkg/applicationserver/applicationserver_util_test.go index f4b1b07f74..cee416caea 100644 --- a/pkg/applicationserver/applicationserver_util_test.go +++ b/pkg/applicationserver/applicationserver_util_test.go @@ -29,7 +29,13 @@ var ( // MockDeviceRegistry is a mock DeviceRegistry used for testing. type MockDeviceRegistry struct { GetFunc func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, paths []string) (*ttnpb.EndDevice, error) - SetFunc func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, paths []string, f func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, error) + SetFunc func( + ctx context.Context, + ids *ttnpb.EndDeviceIdentifiers, + paths []string, + f func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, error) + BatchDeleteFunc func(context.Context, *ttnpb.ApplicationIdentifiers, []string) ([]*ttnpb.EndDeviceIdentifiers, error) } // Get calls GetFunc if set and panics otherwise. @@ -83,3 +89,15 @@ func (m MockLinkRegistry) Set(ctx context.Context, ids *ttnpb.ApplicationIdentif } return m.SetFunc(ctx, ids, paths, f) } + +// BatchDelete calls BatchDeleteFunc if set and panics otherwise. +func (r MockDeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + if r.BatchDeleteFunc == nil { + panic("BatchDelete called, but not set") + } + return r.BatchDeleteFunc(ctx, appIDs, deviceIDs) +} diff --git a/pkg/applicationserver/grpc_deviceregistry.go b/pkg/applicationserver/grpc_deviceregistry.go index cf87f05131..6eadc2f44d 100644 --- a/pkg/applicationserver/grpc_deviceregistry.go +++ b/pkg/applicationserver/grpc_deviceregistry.go @@ -53,6 +53,14 @@ var ( events.WithClientInfoFromContext(), events.WithPropagateToParent(), ) + evtBatchDeleteEndDevices = events.Define( + "as.end_device.batch.delete", "batch delete end devices", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ), + events.WithDataType(&ttnpb.EndDeviceIdentifiersList{}), + events.WithAuthFromContext(), + events.WithClientInfoFromContext(), + events.WithPropagateToParent(), + ) ) type asEndDeviceRegistryServer struct { @@ -307,3 +315,39 @@ func (r asEndDeviceRegistryServer) Delete(ctx context.Context, ids *ttnpb.EndDev } return ttnpb.Empty, nil } + +type asEndDeviceBatchRegistryServer struct { + ttnpb.UnimplementedAsEndDeviceBatchRegistryServer + + AS *ApplicationServer +} + +// Delete implements ttipb.AsEndDeviceBatchRegistryServer. +func (r asEndDeviceBatchRegistryServer) Delete( + ctx context.Context, + req *ttnpb.BatchDeleteEndDevicesRequest, +) (*emptypb.Empty, error) { + // Check if the user has rights on the application. + if err := rights.RequireApplication( + ctx, + req.ApplicationIds, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + + deleted, err := r.AS.deviceRegistry.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) + if err != nil { + return nil, err + } + if len(deleted) != 0 { + events.Publish( + evtBatchDeleteEndDevices.NewWithIdentifiersAndData( + ctx, req.ApplicationIds, &ttnpb.EndDeviceIdentifiersList{ + EndDeviceIds: deleted, + }, + ), + ) + } + return ttnpb.Empty, nil +} diff --git a/pkg/applicationserver/grpc_deviceregistry_test.go b/pkg/applicationserver/grpc_deviceregistry_test.go index b4a53985fb..a6224f75ef 100644 --- a/pkg/applicationserver/grpc_deviceregistry_test.go +++ b/pkg/applicationserver/grpc_deviceregistry_test.go @@ -832,3 +832,387 @@ func TestDeviceRegistryDelete(t *testing.T) { }) } } + +func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest + registeredApplicationID := "test-app" + registeredApplicationIDs := &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + } + dev1 := &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + }, + DeviceId: "test-device-1", + JoinEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + DevEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + }, + RootKeys: &ttnpb.RootKeys{ + RootKeyId: "testKey", + NwkKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0, + }.Bytes(), + KekLabel: "test", + }, + AppKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + }.Bytes(), + KekLabel: "test", + }, + }, + } + dev2 := ttnpb.Clone(dev1) + dev2.Ids.DeviceId = "test-device-2" + dev2.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev2.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + dev3 := ttnpb.Clone(dev1) + dev3.Ids.DeviceId = "test-device-3" + dev3.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev3.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + for _, tc := range []struct { + Name string + ContextFunc func(context.Context) context.Context + BatchDeleteFunc func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) + Request *ttnpb.BatchDeleteEndDevicesRequest + ErrorAssertion func(*testing.T, error) bool + BatchDeleteCalls uint64 + }{ + { + Name: "No device write rights", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Non-existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func(ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + // Devices not found are skipped. + return nil, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "Wrong application ID", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-unknown-app-id"}, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Invalid Device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + "test-dev-&*@(#)", + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 1) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + a.So(deviceIDs[0], should.Equal, dev1.GetIds().DeviceId) + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "One invalid device in batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 3) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + t.Log("Known device ID") + case "test-dev-unknown-id": + t.Log("Ignore expected unknown device ID") + default: + t.Log("Unexpected device ID") + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + "test-dev-unknown-id", + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "Valid Batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + a.So(deviceIDs, should.HaveLength, 3) + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + case dev3.GetIds().DeviceId: + // Known device ID + default: + t.Error("Unknown device ID: ", devID) + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + dev3.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + } { + tc := tc + test.RunSubtest(t, test.SubtestConfig{ + Name: tc.Name, + Parallel: true, + Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { + t.Helper() + var batchDeleteCalls uint64 + as := test.Must(applicationserver.New(componenttest.NewComponent(t, &component.Config{}), + &applicationserver.Config{ + Devices: &MockDeviceRegistry{ + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + atomic.AddUint64(&batchDeleteCalls, 1) + return tc.BatchDeleteFunc(ctx, appIDs, deviceIDs) + }, + }, + Downlinks: applicationserver.DownlinksConfig{ + ConfirmationConfig: applicationserver.ConfirmationConfig{ + DefaultRetryAttempts: 3, + MaxRetryAttempts: 10, + }, + }, + })) + + as.AddContextFiller(tc.ContextFunc) + as.AddContextFiller(func(ctx context.Context) context.Context { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(Timeout)) + _ = cancel + return ctx + }) + as.AddContextFiller(func(ctx context.Context) context.Context { + return test.ContextWithTB(ctx, t) + }) + componenttest.StartComponent(t, as.Component) + defer as.Close() + + ctx = as.FillContext(ctx) + req := ttnpb.Clone(tc.Request) + + _, err := ttnpb.NewAsEndDeviceBatchRegistryClient(as.LoopbackConn()).Delete(ctx, req) + a.So(batchDeleteCalls, should.Equal, tc.BatchDeleteCalls) + if tc.ErrorAssertion != nil { + a.So(tc.ErrorAssertion(t, err), should.BeTrue) + } else { + a.So(err, should.BeNil) + } + }, + }) + } +} diff --git a/pkg/applicationserver/redis/registry.go b/pkg/applicationserver/redis/registry.go index 7b278e4533..7ab17c1758 100644 --- a/pkg/applicationserver/redis/registry.go +++ b/pkg/applicationserver/redis/registry.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package redis provides interfaces to Redis. package redis import ( @@ -22,6 +23,7 @@ import ( "github.com/redis/go-redis/v9" "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" ttnredis "go.thethings.network/lorawan-stack/v3/pkg/redis" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" @@ -448,3 +450,74 @@ func (r *LinkRegistry) Set(ctx context.Context, ids *ttnpb.ApplicationIdentifier } return pb, nil } + +// BatchDelete implements Registry. +func (r *DeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + var ( + uidKeys = make([]string, 0, len(deviceIDs)) + ret = make([]*ttnpb.EndDeviceIdentifiers, 0) + ) + for _, devID := range deviceIDs { + uidKeys = append( + uidKeys, + r.uidKey( + unique.ID( + ctx, + &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appIDs, + DeviceId: devID, + }), + ), + ) + } + + // Delete the devices in a single transaction. + transaction := func(tx *redis.Tx) error { + // Read the devices to delete. + raw, err := tx.MGet(ctx, uidKeys...).Result() + if err != nil { + return err + } + euiKeys := make([]string, 0, len(raw)) + for _, raw := range raw { + switch val := raw.(type) { + case nil: + continue + case string: + dev := &ttnpb.EndDevice{} + if err := ttnredis.UnmarshalProto(val, dev); err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to decode stored end device") + continue + } + ret = append(ret, dev.Ids) + if dev.Ids.JoinEui != nil && dev.Ids.DevEui != nil { + euiKeys = append(euiKeys, r.euiKey( + types.MustEUI64(dev.GetIds().GetJoinEui()).OrZero(), + types.MustEUI64(dev.GetIds().GetDevEui()).OrZero(), + )) + } + } + } + if err := tx.Watch(ctx, euiKeys...).Err(); err != nil { + return err + } + if _, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.Del(ctx, append(uidKeys, euiKeys...)...) + return nil + }); err != nil { + return err + } + return nil + } + + defer trace.StartRegion(ctx, "batch delete end device").End() + err := r.Redis.Watch(ctx, transaction, uidKeys...) + if err != nil { + return nil, ttnredis.ConvertError(err) + } + return ret, nil +} diff --git a/pkg/applicationserver/registry.go b/pkg/applicationserver/registry.go index 3c8faefe5c..acac103ea8 100644 --- a/pkg/applicationserver/registry.go +++ b/pkg/applicationserver/registry.go @@ -31,6 +31,12 @@ type DeviceRegistry interface { Set(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, paths []string, f func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, error) // Range ranges over the end devices and calls the callback function, until false is returned. Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error + // BatchDelete deletes a batch of end devices. + BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } type replacedEndDeviceFieldRegistryWrapper struct { @@ -79,6 +85,14 @@ func (w replacedEndDeviceFieldRegistryWrapper) Set(ctx context.Context, ids *ttn return dev, nil } +func (w replacedEndDeviceFieldRegistryWrapper) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + return w.registry.BatchDelete(ctx, appIDs, deviceIDs) +} + func (w replacedEndDeviceFieldRegistryWrapper) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error { paths, replaced := registry.MatchReplacedEndDeviceFields(paths, w.fields) return w.registry.Range(ctx, paths, func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, dev *ttnpb.EndDevice) bool { @@ -145,4 +159,6 @@ type ApplicationUplinkRegistry interface { Push(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, up *ttnpb.ApplicationUplink) error // Clear empties the uplink messages storage by the end device identifiers. Clear(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers) error + // BatchClear empties the uplink messages storage of multiple end devices. + BatchClear(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error } diff --git a/pkg/applicationserver/registry_test.go b/pkg/applicationserver/registry_test.go index 4cde43eff8..938bfb5f41 100644 --- a/pkg/applicationserver/registry_test.go +++ b/pkg/applicationserver/registry_test.go @@ -168,6 +168,75 @@ func handleDeviceRegistryTest(t *testing.T, reg DeviceRegistry) { t.Fatalf("Error received: %v", err) } a.So(ret, should.BeNil) + + // Batch Operations. + pb1 := ttnpb.Clone(pb) + pb1.Ids.DeviceId = "test-dev-1" + pb1.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + pb2 := ttnpb.Clone(pb) + pb2.Ids.DeviceId = "test-dev-2" + pb2.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + pb3 := ttnpb.Clone(pb) + pb3.Ids.DeviceId = "test-dev-3" + pb3.Ids.DevEui = types.EUI64{0x42, 0x45, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + pb3.PendingSession = nil + + for _, dev := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + ret, err = reg.Set(ctx, dev.Ids, + []string{ + "ids.application_ids", + "ids.dev_eui", + "ids.device_id", + "ids.join_eui", + "session", + "skip_payload_crypto_override", + }, + func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { + if !a.So(stored, should.BeNil) { + t.Fatal("Registry is not empty") + } + return dev, []string{ + "ids.application_ids", + "ids.dev_eui", + "ids.device_id", + "ids.join_eui", + "pending_session", + "session", + "skip_payload_crypto_override", + }, nil + }, + ) + if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { + t.Fatalf("Failed to create device: %s", err) + } + ret, err = reg.Get(ctx, dev.Ids, ttnpb.EndDeviceFieldPathsTopLevel) + a.So(err, should.BeNil) + a.So(ret, should.HaveEmptyDiff, dev) + } + + deleted, err := reg.BatchDelete( + ctx, + pb1.Ids.ApplicationIds, // All the devices share the application identifiers. + []string{ + pb1.Ids.DeviceId, + pb2.Ids.DeviceId, + pb3.Ids.DeviceId, + }, + ) + if !a.So(err, should.BeNil) || !a.So(deleted, should.HaveLength, 3) { + t.Fatalf("Failed to delete devices: %s", err) + } + + // Make sure that the device is deleted. + for _, dev := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + ret, err = reg.Get(ctx, dev.Ids, ttnpb.EndDeviceFieldPathsTopLevel) + if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) { + t.Fatalf("Error received: %v", err) + } + a.So(ret, should.BeNil) + } } func TestDeviceRegistry(t *testing.T) { diff --git a/pkg/identityserver/bunstore/end_device_location_store.go b/pkg/identityserver/bunstore/end_device_location_store.go index 78633fea03..b9b74c0b07 100644 --- a/pkg/identityserver/bunstore/end_device_location_store.go +++ b/pkg/identityserver/bunstore/end_device_location_store.go @@ -70,11 +70,11 @@ func endDeviceLocationMap(models []*EndDeviceLocation) map[string]*EndDeviceLoca } func (s *endDeviceStore) replaceEndDeviceLocations( - ctx context.Context, current []*EndDeviceLocation, desired map[string]*ttnpb.Location, gatewayID string, + ctx context.Context, current []*EndDeviceLocation, desired map[string]*ttnpb.Location, endDeviceID string, ) ([]*EndDeviceLocation, error) { var ( oldMap = endDeviceLocationMap(current) - newMap = endDeviceLocationMap(endDeviceLocationSliceFromPB(desired, gatewayID)) + newMap = endDeviceLocationMap(endDeviceLocationSliceFromPB(desired, endDeviceID)) toCreate = make([]*EndDeviceLocation, 0, len(newMap)) toUpdate = make([]*EndDeviceLocation, 0, len(newMap)) toDelete = make([]*EndDeviceLocation, 0, len(oldMap)) diff --git a/pkg/identityserver/bunstore/end_device_store.go b/pkg/identityserver/bunstore/end_device_store.go index 25b61b6bab..05aa9a0a33 100644 --- a/pkg/identityserver/bunstore/end_device_store.go +++ b/pkg/identityserver/bunstore/end_device_store.go @@ -885,3 +885,73 @@ func (s *endDeviceStore) BatchUpdateEndDeviceLastSeen( return nil } + +// BatchDeleteEndDevices implements EndDeviceStore. +func (s *endDeviceStore) BatchDeleteEndDevices( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + ctx, span := tracer.StartFromContext(ctx, "BatchDeleteEndDevices", trace.WithAttributes( + attribute.String("application_ids", appIDs.String()), + attribute.Int("count", len(deviceIDs)), + )) + defer span.End() + + // Sort end devices by ID to avoid deadlocks. + sort.Strings(deviceIDs) + + deleted := make([]*ttnpb.EndDeviceIdentifiers, 0, len(deviceIDs)) + for _, devID := range deviceIDs { + model, err := s.getEndDeviceModelBy(ctx, s.selectWithID( + ctx, + appIDs.GetApplicationId(), + devID, + ), store.FieldMask{"ids", "attributes", "locations"}) + if err != nil { + if errors.IsNotFound(err) { + continue + } + return nil, storeutil.WrapDriverError(err) + } + if len(model.Attributes) > 0 { + _, err = s.replaceAttributes(ctx, model.Attributes, nil, "device", model.ID) + if err != nil { + return nil, storeutil.WrapDriverError(err) + } + } + _, err = s.DB.NewDelete(). + Model(model). + WherePK(). + Exec(ctx) + if err != nil { + return nil, storeutil.WrapDriverError(err) + } + + if len(model.Locations) > 0 { + _, err := s.DB.NewDelete(). + Model(&model.Locations). + WherePK(). + Exec(ctx) + if err != nil { + return nil, storeutil.WrapDriverError(err) + } + } + + if model.PictureID != nil { + _, err = s.DB.NewDelete(). + Model((*Picture)(nil)). + Where("id = ?", *model.PictureID). + Exec(ctx) + if err != nil { + return nil, storeutil.WrapDriverError(err) + } + } + + deleted = append(deleted, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appIDs, + DeviceId: devID, + }) + } + return deleted, nil +} diff --git a/pkg/identityserver/bunstore/store_test.go b/pkg/identityserver/bunstore/store_test.go index 8cea481e03..783707f257 100644 --- a/pkg/identityserver/bunstore/store_test.go +++ b/pkg/identityserver/bunstore/store_test.go @@ -90,6 +90,7 @@ func TestEndDeviceStore(t *testing.T) { st.TestEndDeviceStorePagination(t) st.TestEndDeviceBatchUpdate(t) st.TestEndDeviceCAC(t) + st.TestEndDeviceBatchOperations(t) } func TestGatewayStore(t *testing.T) { diff --git a/pkg/identityserver/end_device_registry.go b/pkg/identityserver/end_device_registry.go index c4a70f6acd..2e7954bad8 100644 --- a/pkg/identityserver/end_device_registry.go +++ b/pkg/identityserver/end_device_registry.go @@ -58,6 +58,14 @@ var ( events.WithClientInfoFromContext(), events.WithPropagateToParent(), ) + evtBatchDeleteEndDevices = events.Define( + "is.end_device.batch.delete", "batch delete end devices", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ), + events.WithDataType(&ttnpb.EndDeviceIdentifiersList{}), + events.WithAuthFromContext(), + events.WithClientInfoFromContext(), + events.WithPropagateToParent(), + ) ) var errEndDeviceEUIsTaken = errors.DefineAlreadyExists( @@ -460,6 +468,48 @@ type endDeviceRegistry struct { *IdentityServer } +type endDeviceBatchRegistry struct { + ttnpb.UnimplementedEndDeviceBatchRegistryServer + + *IdentityServer +} + +func (is *IdentityServer) batchDeleteEndDevice( + ctx context.Context, + req *ttnpb.BatchDeleteEndDevicesRequest, +) (*emptypb.Empty, error) { + if err := rights.RequireApplication(ctx, + req.ApplicationIds, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + var ( + err error + deleted = []*ttnpb.EndDeviceIdentifiers{} + ) + err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) error { + deleted, err = st.BatchDeleteEndDevices(ctx, req.ApplicationIds, req.DeviceIds) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + if len(deleted) != 0 { + events.Publish( + evtBatchDeleteEndDevices.NewWithIdentifiersAndData( + ctx, req.ApplicationIds, &ttnpb.EndDeviceIdentifiersList{ + EndDeviceIds: deleted, + }, + ), + ) + } + return ttnpb.Empty, nil +} + func (dr *endDeviceRegistry) Create(ctx context.Context, req *ttnpb.CreateEndDeviceRequest) (*ttnpb.EndDevice, error) { return dr.createEndDevice(ctx, req) } @@ -487,3 +537,10 @@ func (dr *endDeviceRegistry) BatchUpdateLastSeen(ctx context.Context, req *ttnpb func (dr *endDeviceRegistry) Delete(ctx context.Context, req *ttnpb.EndDeviceIdentifiers) (*emptypb.Empty, error) { return dr.deleteEndDevice(ctx, req) } + +func (reg *endDeviceBatchRegistry) Delete( + ctx context.Context, + req *ttnpb.BatchDeleteEndDevicesRequest, +) (*emptypb.Empty, error) { + return reg.batchDeleteEndDevice(ctx, req) +} diff --git a/pkg/identityserver/end_device_registry_test.go b/pkg/identityserver/end_device_registry_test.go index 056f740e78..81dd3498a3 100644 --- a/pkg/identityserver/end_device_registry_test.go +++ b/pkg/identityserver/end_device_registry_test.go @@ -26,6 +26,8 @@ import ( "google.golang.org/grpc/metadata" ) +const noOfDevices = 3 + func TestEndDevicesPermissionDenied(t *testing.T) { p := &storetest.Population{} usr1 := p.NewUser() @@ -214,3 +216,85 @@ func TestEndDevicesPagination(t *testing.T) { } }, withPrivateTestDatabase(p)) } + +func TestEndDevicesBatchDelete(t *testing.T) { + t.Parallel() + a, ctx := test.New(t) + p := &storetest.Population{} + usr1 := p.NewUser() + app1 := p.NewApplication(usr1.GetOrganizationOrUserIdentifiers()) + devIDs := make([]string, 0, noOfDevices) + for i := 0; i < noOfDevices; i++ { + dev := p.NewEndDevice(app1.GetIds()) + dev.Attributes = map[string]string{ + "foo": "bar", + } + dev.Locations = map[string]*ttnpb.Location{ + "foo": { + Latitude: 1, + Longitude: 2, + Altitude: 3, + }, + } + devIDs = append(devIDs, dev.GetIds().DeviceId) + } + readKey, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ) + readCreds := rpcCreds(readKey) + + writeKey, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE) + writeCreds := rpcCreds(writeKey) + + testWithIdentityServer(t, func(is *IdentityServer, cc *grpc.ClientConn) { + reg := ttnpb.NewEndDeviceBatchRegistryClient(cc) + + // ClusterAuth. + _, err := reg.Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: app1.GetIds(), + DeviceIds: devIDs, + }, is.WithClusterAuth()) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Insufficient rights. + _, err = reg.Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: app1.GetIds(), + DeviceIds: devIDs, + }, readCreds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Unknown application. + _, err = reg.Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "unknown"}, + DeviceIds: devIDs, + }, writeCreds) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + + // Unknown device ignored. + _, err = reg.Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: app1.GetIds(), + DeviceIds: []string{ + "unknown", + }, + }, writeCreds) + a.So(err, should.BeNil) + + // Valid Batch. + _, err = reg.Delete(ctx, &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: app1.GetIds(), + DeviceIds: devIDs, + }, writeCreds) + a.So(err, should.BeNil) + + // Read after delete. + edReg := ttnpb.NewEndDeviceRegistryClient(cc) + for _, devID := range devIDs { + got, err := edReg.Get(ctx, &ttnpb.GetEndDeviceRequest{ + EndDeviceIds: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: app1.GetIds(), + DeviceId: devID, + }, + }, readCreds) + a.So(got, should.BeNil) + a.So(errors.IsNotFound(err), should.BeTrue) + } + }, withPrivateTestDatabase(p)) +} diff --git a/pkg/identityserver/identityserver.go b/pkg/identityserver/identityserver.go index f2cb69c381..172446cbe9 100644 --- a/pkg/identityserver/identityserver.go +++ b/pkg/identityserver/identityserver.go @@ -194,6 +194,7 @@ func New(c *component.Component, config *Config) (is *IdentityServer, err error) "/ttn.lorawan.v3.ClientRegistry", "/ttn.lorawan.v3.ClientAccess", "/ttn.lorawan.v3.EndDeviceRegistry", + "/ttn.lorawan.v3.EndDeviceBatchRegistry", "/ttn.lorawan.v3.GatewayRegistry", "/ttn.lorawan.v3.GatewayAccess", "/ttn.lorawan.v3.OrganizationRegistry", @@ -254,6 +255,7 @@ func (is *IdentityServer) RegisterServices(s *grpc.Server) { ttnpb.RegisterOAuthAuthorizationRegistryServer(s, &oauthRegistry{IdentityServer: is}) ttnpb.RegisterContactInfoRegistryServer(s, &contactInfoRegistry{IdentityServer: is}) ttnpb.RegisterNotificationServiceServer(s, ¬ificationRegistry{IdentityServer: is}) + ttnpb.RegisterEndDeviceBatchRegistryServer(s, &endDeviceBatchRegistry{IdentityServer: is}) } // RegisterHandlers registers gRPC handlers. @@ -278,6 +280,7 @@ func (is *IdentityServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.Clien ttnpb.RegisterOAuthAuthorizationRegistryHandler(is.Context(), s, conn) ttnpb.RegisterContactInfoRegistryHandler(is.Context(), s, conn) ttnpb.RegisterNotificationServiceHandler(is.Context(), s, conn) + ttnpb.RegisterEndDeviceBatchRegistryHandler(is.Context(), s, conn) // nolint:errcheck } // RegisterInterop registers the LoRaWAN Backend Interfaces interoperability services. diff --git a/pkg/identityserver/store/store_interfaces.go b/pkg/identityserver/store/store_interfaces.go index 7d5f421ab0..6f7adba424 100644 --- a/pkg/identityserver/store/store_interfaces.go +++ b/pkg/identityserver/store/store_interfaces.go @@ -87,6 +87,11 @@ type EndDeviceStore interface { ctx context.Context, devsLastSeen []*ttnpb.BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate, ) error + BatchDeleteEndDevices( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + devIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } // GatewayStore interface for storing Gateways. diff --git a/pkg/identityserver/storetest/end_device_store.go b/pkg/identityserver/storetest/end_device_store.go index fb8e0715fd..ff5fd0e35e 100644 --- a/pkg/identityserver/storetest/end_device_store.go +++ b/pkg/identityserver/storetest/end_device_store.go @@ -15,9 +15,11 @@ package storetest import ( + "context" . "testing" "time" + "github.com/smartystreets/assertions" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/identityserver/store" is "go.thethings.network/lorawan-stack/v3/pkg/identityserver/store" @@ -730,3 +732,132 @@ func (st *StoreTest) TestEndDeviceCAC(t *T) { //nolint:revive } }) } + +// TestEndDeviceBatchOperations tests the EndDeviceBatchStore implementation. +func (st *StoreTest) TestEndDeviceBatchOperations(t *T) { // nolint:gocyclo + a, ctx := test.New(t) + + s, ok := st.PrepareDB(t).(interface { + Store + is.EndDeviceStore + is.ApplicationStore + }) + defer st.DestroyDB(t, false) + if !ok { + t.Skip("Store does not implement TestEndDeviceBatchOperations") + } + defer s.Close() + + for _, ctx := range []context.Context{ + ctx, + } { + application, err := s.CreateApplication(ctx, &ttnpb.Application{ + Ids: &ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}, + }) + if !a.So(err, should.BeNil) { + t.FailNow() + } + + dev1, err := s.CreateEndDevice(ctx, &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: application.GetIds(), + DeviceId: "foo-1", + }, + }) + if !a.So(err, should.BeNil) { + t.FailNow() + } + + dev2, err := s.CreateEndDevice(ctx, &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: application.GetIds(), + DeviceId: "bar-1", + }, + }) + if !a.So(err, should.BeNil) { + t.FailNow() + } + + dev1.LastSeenAt = timestamppb.New(time.Now().Truncate(time.Millisecond)) + dev2.LastSeenAt = timestamppb.New(time.Now().Add(-1 * time.Second).Truncate(time.Millisecond)) + + batch := []*ttnpb.BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate{ + {Ids: dev1.Ids, LastSeenAt: dev1.LastSeenAt}, + {Ids: dev2.Ids, LastSeenAt: dev2.LastSeenAt}, + } + err = s.BatchUpdateEndDeviceLastSeen(ctx, batch) + a.So(err, should.BeNil) + + devs, err := s.ListEndDevices(ctx, application.Ids, []string{"last_seen_at"}) + if !a.So(err, should.BeNil) { + t.FailNow() + } + a.So(devs, should.HaveLength, 2) + + for _, dev := range devs { + if dev.Ids.DeviceId == dev1.Ids.DeviceId { + a.So(dev.LastSeenAt, should.Resemble, dev1.LastSeenAt) + } else if dev.Ids.DeviceId == dev2.Ids.DeviceId { + a.So(dev.LastSeenAt, should.Resemble, dev2.LastSeenAt) + } else { + t.FailNow() + } + } + + // Batch Delete + for _, tc := range []struct { // nolint:paralleltest + Name string + Context context.Context + BatchDeleteFunc func(context.Context, []*ttnpb.EndDeviceIdentifiers) ([]*ttnpb.EndDeviceIdentifiers, error) + ApplicationIDs *ttnpb.ApplicationIdentifiers + DeviceIDs []string + Response []*ttnpb.EndDeviceIdentifiers + ErrorAssertion func(*T, error) bool + }{ + { + Name: "Not Found", + Context: ctx, + ApplicationIDs: application.Ids, + DeviceIDs: []string{ + "unknown", + }, + Response: []*ttnpb.EndDeviceIdentifiers{}, + }, + { + Name: "Valid Batch", + Context: ctx, + ApplicationIDs: application.Ids, + DeviceIDs: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + }, + Response: []*ttnpb.EndDeviceIdentifiers{ + dev2.Ids, + dev1.Ids, + }, + }, + } { + tc := tc + t.Run(tc.Name, func(t *T) { + a := assertions.New(t) + deleted, err := s.BatchDeleteEndDevices(tc.Context, tc.ApplicationIDs, tc.DeviceIDs) + if tc.ErrorAssertion != nil && a.So(tc.ErrorAssertion(t, err), should.BeTrue) { + a.So(deleted, should.BeNil) + } else if a.So(err, should.BeNil) { + if tc.Response != nil { + a.So(deleted, should.Resemble, tc.Response) + } else { + a.So(deleted, should.BeNil) + } + } + }) + } + + // Check that the devices are deleted. + devs, err = s.ListEndDevices(ctx, application.Ids, []string{"last_seen_at", "locations"}) + if !a.So(err, should.BeNil) { + t.FailNow() + } + a.So(devs, should.HaveLength, 0) + } +} diff --git a/pkg/joinserver/grpc_deviceregistry.go b/pkg/joinserver/grpc_deviceregistry.go index 0b4135b2a2..054c8f154d 100644 --- a/pkg/joinserver/grpc_deviceregistry.go +++ b/pkg/joinserver/grpc_deviceregistry.go @@ -53,6 +53,14 @@ var ( events.WithClientInfoFromContext(), events.WithPropagateToParent(), ) + evtBatchDeleteEndDevices = events.Define( + "js.end_device.batch.delete", "batch delete end devices", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ), + events.WithDataType(&ttnpb.EndDeviceIdentifiersList{}), + events.WithAuthFromContext(), + events.WithClientInfoFromContext(), + events.WithPropagateToParent(), + ) ) type jsEndDeviceRegistryServer struct { @@ -353,3 +361,52 @@ func (srv jsEndDeviceRegistryServer) Delete(ctx context.Context, ids *ttnpb.EndD } return ttnpb.Empty, err } + +type jsEndDeviceBatchRegistryServer struct { + ttnpb.UnimplementedJsEndDeviceBatchRegistryServer + + JS *JoinServer +} + +// Delete implements ttipb.JsEndDeviceBatchRegistryServer. +func (srv jsEndDeviceBatchRegistryServer) Delete( + ctx context.Context, + req *ttnpb.BatchDeleteEndDevicesRequest, +) (*emptypb.Empty, error) { + // Check if the user has rights on the application. + if err := rights.RequireApplication( + ctx, + req.ApplicationIds, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + deleted, err := srv.JS.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) + if err != nil { + return nil, err + } + if len(deleted) != 0 { + events.Publish( + evtBatchDeleteEndDevices.NewWithIdentifiersAndData( + ctx, req.ApplicationIds, &ttnpb.EndDeviceIdentifiersList{ + EndDeviceIds: deleted, + }, + ), + ) + } + + // Try deleting the session keys in a batch. + devices := []*ttnpb.EndDeviceIdentifiers{} + for _, devID := range deleted { + if devID.DevEui == nil && devID.JoinEui == nil || types.MustEUI64(devID.DevEui).IsZero() { + continue + } + devices = append(devices, devID) + } + if err := srv.JS.keys.BatchDelete(ctx, devices); err != nil { + // We don't return an error since this is an internal cleanup. + log.FromContext(ctx).WithError(err).Warn("Failed to delete session keys") + } + + return ttnpb.Empty, nil +} diff --git a/pkg/joinserver/grpc_deviceregistry_test.go b/pkg/joinserver/grpc_deviceregistry_test.go index 0087df30f1..6e38979255 100644 --- a/pkg/joinserver/grpc_deviceregistry_test.go +++ b/pkg/joinserver/grpc_deviceregistry_test.go @@ -16,6 +16,7 @@ package joinserver_test import ( "context" + "fmt" "sync/atomic" "testing" "time" @@ -1052,3 +1053,456 @@ func TestDeviceRegistryDelete(t *testing.T) { //nolint:paralleltest }) } } + +func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest + registeredApplicationID := "test-app" + registeredApplicationIDs := &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + } + dev1 := &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + }, + DeviceId: "test-device-1", + JoinEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + DevEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + }, + RootKeys: &ttnpb.RootKeys{ + RootKeyId: "testKey", + NwkKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0, + }.Bytes(), + KekLabel: "test", + }, + AppKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + }.Bytes(), + KekLabel: "test", + }, + }, + } + dev2 := ttnpb.Clone(dev1) + dev2.Ids.DeviceId = "test-device-2" + dev2.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev2.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + dev3 := ttnpb.Clone(dev1) + dev3.Ids.DeviceId = "test-device-3" + dev3.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev3.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + for _, tc := range []struct { + Name string + ContextFunc func(context.Context) context.Context + BatchDeleteFunc func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) + BatchDeleteKeysFunc func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error + Request *ttnpb.BatchDeleteEndDevicesRequest + ErrorAssertion func(*testing.T, error) bool + BatchDeleteCalls uint64 + BatchDeleteKeysCalls uint64 + }{ + { + Name: "No device write rights", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + err := errors.New("BatchDeleteKeysFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return err + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + BatchDeleteKeysCalls: 0, + }, + { + Name: "Non-existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + // Devices not found are skipped. + return nil, nil + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + // Devices not found are skipped. + return nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + BatchDeleteKeysCalls: 1, + }, + { + Name: "Wrong application ID", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-unknown-app-id"}, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + err := errors.New("BatchDeleteKeysFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + BatchDeleteKeysCalls: 0, + }, + { + Name: "Invalid Device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + "test-dev-&*@(#)", + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + err := errors.New("BatchDeleteKeysFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + BatchDeleteKeysCalls: 0, + }, + { + Name: "Existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 1) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + a.So(deviceIDs[0], should.Equal, dev1.GetIds().DeviceId) + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + }, nil + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(devIDs, should.HaveLength, 1) { + return fmt.Errorf("Invalid number of devices for BatchDeleteKeysFunc: %d", len(devIDs)) + } + a.So(devIDs[0], should.Resemble, dev1.Ids) + return nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + BatchDeleteKeysCalls: 1, + }, + { + Name: "One invalid device in batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 3) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + t.Log("Known device ID") + case "test-dev-unknown-id": + t.Log("Ignore expected unknown device ID") + default: + t.Log("Unexpected device ID") + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + }, nil + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(devIDs, should.HaveLength, 2) + if !a.So(devIDs, should.HaveLength, 2) { + return fmt.Errorf("Invalid number of devices for BatchDeleteKeysFunc: %d", len(devIDs)) + } + a.So(devIDs[0], should.Resemble, dev1.Ids) + a.So(devIDs[1], should.Resemble, dev2.Ids) + return nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + "test-dev-unknown-id", + }, + }, + BatchDeleteCalls: 1, + BatchDeleteKeysCalls: 1, + }, + { + Name: "Valid Batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 3) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + case dev3.GetIds().DeviceId: + // Known device ID + default: + t.Error("Unknown device ID: ", devID) + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + dev3.Ids, + }, nil + }, + BatchDeleteKeysFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + a := assertions.New(test.MustTFromContext(ctx)) + if !a.So(devIDs, should.HaveLength, 3) { + return fmt.Errorf("Invalid number of devices for BatchDeleteKeysFunc: %d", len(devIDs)) + } + a.So(devIDs[0], should.Resemble, dev1.Ids) + a.So(devIDs[1], should.Resemble, dev2.Ids) + a.So(devIDs[2], should.Resemble, dev3.Ids) + return nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + BatchDeleteKeysCalls: 1, + }, + } { + tc := tc + test.RunSubtest(t, test.SubtestConfig{ + Name: tc.Name, + Parallel: true, + Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { + t.Helper() + var ( + batchDeleteCalls uint64 + batchDeleteKeysCalls uint64 + ) + js := test.Must(New( + componenttest.NewComponent(t, &component.Config{ + ServiceBase: config.ServiceBase{ + KeyVault: config.KeyVault{ + Provider: "static", + Static: map[string][]byte{ + "test": {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, + }, + }, + }, + }), + &Config{ + Devices: &MockDeviceRegistry{ + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + atomic.AddUint64(&batchDeleteCalls, 1) + return tc.BatchDeleteFunc(ctx, appIDs, deviceIDs) + }, + }, + Keys: &MockKeyRegistry{ + BatchDeleteFunc: func(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + atomic.AddUint64(&batchDeleteKeysCalls, 1) + return tc.BatchDeleteKeysFunc(ctx, devIDs) + }, + }, + DevNonceLimit: defaultDevNonceLimit, + }, + )) + js.AddContextFiller(tc.ContextFunc) + js.AddContextFiller(func(ctx context.Context) context.Context { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(Timeout)) + _ = cancel + return ctx + }) + js.AddContextFiller(func(ctx context.Context) context.Context { + return test.ContextWithTB(ctx, t) + }) + componenttest.StartComponent(t, js.Component) + defer js.Close() + ctx = js.FillContext(ctx) + req := ttnpb.Clone(tc.Request) + _, err := ttnpb.NewJsEndDeviceBatchRegistryClient(js.LoopbackConn()).Delete(ctx, req) + a.So(batchDeleteCalls, should.Equal, tc.BatchDeleteCalls) + a.So(batchDeleteKeysCalls, should.Equal, tc.BatchDeleteKeysCalls) + if tc.ErrorAssertion != nil { + a.So(tc.ErrorAssertion(t, err), should.BeTrue) + } else { + a.So(err, should.BeNil) + } + }, + }) + } +} diff --git a/pkg/joinserver/joinserver.go b/pkg/joinserver/joinserver.go index 5213813a99..51085dc885 100644 --- a/pkg/joinserver/joinserver.go +++ b/pkg/joinserver/joinserver.go @@ -65,6 +65,7 @@ type JoinServer struct { asJs asJsServer appJs appJsServer jsDevices jsEndDeviceRegistryServer + jsBatchDevices jsEndDeviceBatchRegistryServer js jsServer applicationActivationSettings applicationActivationSettingsRegistryServer } @@ -111,6 +112,9 @@ func New(c *component.Component, conf *Config) (*JoinServer, error) { JS: js, kekLabel: conf.DeviceKEKLabel, } + js.grpc.jsBatchDevices = jsEndDeviceBatchRegistryServer{ + JS: js, + } js.grpc.nsJs = nsJsServer{JS: js} js.grpc.asJs = asJsServer{JS: js} js.grpc.appJs = appJsServer{JS: js} @@ -130,6 +134,7 @@ func New(c *component.Component, conf *Config) (*JoinServer, error) { "/ttn.lorawan.v3.AppJs", "/ttn.lorawan.v3.NsJs", "/ttn.lorawan.v3.JsEndDeviceRegistry", + "/ttn.lorawan.v3.JsEndDeviceBatchRegistry", "/ttn.lorawan.v3.Js", "/ttn.lorawan.v3.ApplicationActivationSettingRegistry", } { @@ -156,6 +161,7 @@ func (js *JoinServer) RegisterServices(s *grpc.Server) { ttnpb.RegisterAppJsServer(s, js.grpc.appJs) ttnpb.RegisterNsJsServer(s, js.grpc.nsJs) ttnpb.RegisterJsEndDeviceRegistryServer(s, js.grpc.jsDevices) + ttnpb.RegisterJsEndDeviceBatchRegistryServer(s, js.grpc.jsBatchDevices) ttnpb.RegisterJsServer(s, js.grpc.js) ttnpb.RegisterApplicationActivationSettingRegistryServer(s, js.grpc.applicationActivationSettings) } @@ -164,6 +170,7 @@ func (js *JoinServer) RegisterServices(s *grpc.Server) { func (js *JoinServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.ClientConn) { ttnpb.RegisterJsHandler(js.Context(), s, conn) ttnpb.RegisterJsEndDeviceRegistryHandler(js.Context(), s, conn) + ttnpb.RegisterJsEndDeviceBatchRegistryHandler(js.Context(), s, conn) // nolint:errcheck ttnpb.RegisterApplicationActivationSettingRegistryHandler(js.Context(), s, conn) } diff --git a/pkg/joinserver/joinserver_internal_test.go b/pkg/joinserver/joinserver_internal_test.go index a53c9cc145..1fe1ec3b24 100644 --- a/pkg/joinserver/joinserver_internal_test.go +++ b/pkg/joinserver/joinserver_internal_test.go @@ -38,11 +38,31 @@ type ( ) type MockDeviceRegistry struct { - GetByEUIFunc func(context.Context, types.EUI64, types.EUI64, []string) (*ttnpb.ContextualEndDevice, error) - GetByIDFunc func(context.Context, *ttnpb.ApplicationIdentifiers, string, []string) (*ttnpb.EndDevice, error) - SetByEUIFunc func(context.Context, types.EUI64, types.EUI64, []string, func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.ContextualEndDevice, error) - SetByIDFunc func(context.Context, *ttnpb.ApplicationIdentifiers, string, []string, func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, error) - RangeByIDFunc func(context.Context, []string, func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error + GetByEUIFunc func(context.Context, types.EUI64, types.EUI64, []string) (*ttnpb.ContextualEndDevice, error) + GetByIDFunc func(context.Context, *ttnpb.ApplicationIdentifiers, string, []string) (*ttnpb.EndDevice, error) + SetByEUIFunc func( + context.Context, + types.EUI64, + types.EUI64, + []string, + func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.ContextualEndDevice, error) + SetByIDFunc func( + context.Context, + *ttnpb.ApplicationIdentifiers, + string, + []string, func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, error) + RangeByIDFunc func( + context.Context, + []string, + func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool, + ) error + BatchDeleteFunc func( + context.Context, + *ttnpb.ApplicationIdentifiers, + []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } // GetByEUI calls GetByEUIFunc if set and panics otherwise. @@ -85,10 +105,30 @@ func (m MockDeviceRegistry) RangeByID(ctx context.Context, paths []string, f fun return m.RangeByIDFunc(ctx, paths, f) } +// SetByID calls SetByIDFunc if set and panics otherwise. +func (m MockDeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + if m.BatchDeleteFunc == nil { + panic("BatchDelete called, but not set") + } + return m.BatchDeleteFunc(ctx, appIDs, deviceIDs) +} + type MockKeyRegistry struct { GetByIDFunc func(context.Context, types.EUI64, types.EUI64, []byte, []string) (*ttnpb.SessionKeys, error) - SetByIDFunc func(context.Context, types.EUI64, types.EUI64, []byte, []string, func(*ttnpb.SessionKeys) (*ttnpb.SessionKeys, []string, error)) (*ttnpb.SessionKeys, error) - DeleteFunc func(context.Context, types.EUI64, types.EUI64) error + SetByIDFunc func( + context.Context, + types.EUI64, + types.EUI64, + []byte, + []string, + func(*ttnpb.SessionKeys) (*ttnpb.SessionKeys, []string, error), + ) (*ttnpb.SessionKeys, error) + DeleteFunc func(context.Context, types.EUI64, types.EUI64) error + BatchDeleteFunc func(context.Context, []*ttnpb.EndDeviceIdentifiers) error } // GetByID calls GetByIDFunc if set and panics otherwise. @@ -113,3 +153,10 @@ func (m MockKeyRegistry) Delete(ctx context.Context, joinEUI, devEUI types.EUI64 } return m.DeleteFunc(ctx, joinEUI, devEUI) } + +func (m MockKeyRegistry) BatchDelete(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + if m.BatchDeleteFunc == nil { + panic("BatchDelete called, but not set") + } + return m.BatchDeleteFunc(ctx, devIDs) +} diff --git a/pkg/joinserver/redis/registry.go b/pkg/joinserver/redis/registry.go index f061965e33..f2dabd3864 100644 --- a/pkg/joinserver/redis/registry.go +++ b/pkg/joinserver/redis/registry.go @@ -24,6 +24,7 @@ import ( "github.com/redis/go-redis/v9" "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/provisioning" ttnredis "go.thethings.network/lorawan-stack/v3/pkg/redis" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" @@ -592,7 +593,11 @@ func (r *KeyRegistry) SetByID(ctx context.Context, joinEUI, devEUI types.EUI64, return pb, nil } +// Delete implements KeyRegistry. func (r *KeyRegistry) Delete(ctx context.Context, joinEUI, devEUI types.EUI64) error { + if r.Limit == 0 { + return nil + } if devEUI.IsZero() { return errInvalidIdentifiers.New() } @@ -606,7 +611,7 @@ func (r *KeyRegistry) Delete(ctx context.Context, joinEUI, devEUI types.EUI64) e defer trace.StartRegion(ctx, "delete session keys").End() err = ttnredis.LockedWatch(ctx, r.Redis, sk, lockerID, r.LockTTL, func(tx *redis.Tx) error { - sids, err := tx.LRange(ctx, sk, 0, 1<<24).Result() + sids, err := tx.LRange(ctx, sk, 0, int64(r.Limit)).Result() if err != nil { return err } @@ -810,3 +815,156 @@ func (r *ApplicationActivationSettingRegistry) Range(ctx context.Context, paths return true, nil }) } + +// BatchDelete implements DeviceRegistry. +// This function deletes all the devices in a single transaction. +func (r *DeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + var ( + uidKeys = make([]string, 0, len(deviceIDs)) + ret = make([]*ttnpb.EndDeviceIdentifiers, 0) + ) + for _, devID := range deviceIDs { + uidKeys = append( + uidKeys, + r.uidKey( + unique.ID( + ctx, + &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appIDs, + DeviceId: devID, + }), + ), + ) + } + + // Delete the devices in a single transaction. + transaction := func(tx *redis.Tx) error { + keys := make([]string, 0) + // Read the devices to delete. + raw, err := tx.MGet(ctx, uidKeys...).Result() + if err != nil { + return err + } + for _, raw := range raw { + switch val := raw.(type) { + case nil: + continue + case string: + dev := &ttnpb.EndDevice{} + if err := ttnredis.UnmarshalProto(val, dev); err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to decode stored end device") + continue + } + ret = append(ret, dev.Ids) + if dev.Ids.JoinEui != nil && dev.Ids.DevEui != nil { + keys = append(keys, r.euiKey( + types.MustEUI64(dev.GetIds().GetJoinEui()).OrZero(), + types.MustEUI64(dev.GetIds().GetDevEui()).OrZero(), + )) + } + pid, err := provisionerUniqueID(dev) + if err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to get provisioner unique ID") + continue + } + if pid != "" { + keys = append(keys, r.provisionerKey(dev.ProvisionerId, pid)) + } + } + } + if err := tx.Watch(ctx, keys...).Err(); err != nil { + return err + } + if _, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.Del(ctx, append(uidKeys, keys...)...) + return nil + }); err != nil { + return err + } + return nil + } + + err := r.Redis.Watch(ctx, transaction, uidKeys...) + if err != nil { + return nil, ttnredis.ConvertError(err) + } + return ret, nil +} + +// BatchDelete implements KeyRegistry. +func (r *KeyRegistry) BatchDelete(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error { + if r.Limit == 0 { + return nil + } + rangeRes := make([]*redis.StringSliceCmd, 0, len(devIDs)) + _, err := r.Redis.Pipelined(ctx, func(p redis.Pipeliner) error { + for _, devID := range devIDs { + rangeRes = append(rangeRes, p.LRange( + ctx, + r.idSetKey( + types.MustEUI64(devID.JoinEui).OrZero(), + types.MustEUI64(devID.DevEui).OrZero(), + ), + 0, + int64(r.Limit), + )) + } + return nil + }) + if err != nil { + return ttnredis.ConvertError(err) + } + + delItems := make(map[string][]string, 0) + for i, res := range rangeRes { + if res == nil { + continue + } + sids, err := res.Result() + if err != nil { + return ttnredis.ConvertError(err) + } + idSetKey := r.idSetKey( + types.MustEUI64(devIDs[i].JoinEui).OrZero(), + types.MustEUI64(devIDs[i].DevEui).OrZero(), + ) + sidKeys := make([]string, 0, len(sids)) + for _, sid := range sids { + sidKeys = append(sidKeys, r.idKey( + types.MustEUI64(devIDs[i].JoinEui).OrZero(), + types.MustEUI64(devIDs[i].DevEui).OrZero(), + sid, + )) + } + delItems[idSetKey] = sidKeys + } + + // Delete all the keys for all the selected EUI pairs in a single transaction. + transaction := func(tx *redis.Tx) error { + if _, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + for key, sidKeys := range delItems { + p.Del(ctx, sidKeys...) + p.Del(ctx, key) + } + return nil + }); err != nil { + return ttnredis.ConvertError(err) + } + return nil + } + + defer trace.StartRegion(ctx, "batch delete session keys").End() + watched := make([]string, 0, len(delItems)) + for k := range delItems { + watched = append(watched, k) + } + err = r.Redis.Watch(ctx, transaction, watched...) + if err != nil { + return ttnredis.ConvertError(err) + } + return nil +} diff --git a/pkg/joinserver/registry.go b/pkg/joinserver/registry.go index f05e6cd3d2..ad9267e778 100644 --- a/pkg/joinserver/registry.go +++ b/pkg/joinserver/registry.go @@ -28,6 +28,12 @@ type DeviceRegistry interface { SetByEUI(ctx context.Context, joinEUI types.EUI64, devEUI types.EUI64, paths []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.ContextualEndDevice, error) SetByID(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, paths []string, f func(*ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, error) RangeByID(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error + // BatchDelete deletes a batch of end devices. + BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } // DeleteDevice deletes device identified by joinEUI, devEUI from r. @@ -43,6 +49,7 @@ type KeyRegistry interface { GetByID(ctx context.Context, joinEUI, devEUI types.EUI64, id []byte, paths []string) (*ttnpb.SessionKeys, error) SetByID(ctx context.Context, joinEUI, devEUI types.EUI64, id []byte, paths []string, f func(*ttnpb.SessionKeys) (*ttnpb.SessionKeys, []string, error)) (*ttnpb.SessionKeys, error) Delete(ctx context.Context, joinEUI, devEUI types.EUI64) error + BatchDelete(ctx context.Context, devIDs []*ttnpb.EndDeviceIdentifiers) error } // DeleteKeys deletes session keys identified by devEUI, id pair from r. diff --git a/pkg/joinserver/registry_test.go b/pkg/joinserver/registry_test.go index 1a2c470809..a00424f068 100644 --- a/pkg/joinserver/registry_test.go +++ b/pkg/joinserver/registry_test.go @@ -204,6 +204,98 @@ func handleDeviceRegistryTest(t *testing.T, reg DeviceRegistry) { t.Fatalf("Error received: %v", err) } a.So(retCtx, should.BeNil) + + // Batch Operations + pb1 := ttnpb.Clone(pb) + pb1.Ids.DeviceId = "test-dev-1" + pb1.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + pb1.ProvisionerId = "mock-provisioner-1" + + pb2 := ttnpb.Clone(pb) + pb2.Ids.DeviceId = "test-dev-2" + pb2.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + pb1.ProvisionerId = "mock-provisioner-2" + + pb3 := ttnpb.Clone(pb) + pb3.Ids.DeviceId = "test-dev-3" + pb3.Ids.DevEui = types.EUI64{0x42, 0x45, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + pb3.ProvisionerId = "mock-provisioner-3" + + // Create the devices + for _, dev := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + devEUI := types.EUI64(dev.GetIds().DevEui) + retCtx, err := reg.GetByEUI(ctx, joinEUI, devEUI, ttnpb.EndDeviceFieldPathsTopLevel) + if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) { + t.Fatalf("Error received: %v", err) + } + a.So(retCtx, should.BeNil) + ret, err := reg.SetByID(ctx, dev.Ids.ApplicationIds, dev.Ids.DeviceId, + []string{ + "provisioner_id", + "provisioning_data", + }, + func(stored *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { + if !a.So(stored, should.BeNil) { + t.Fatal("Registry is not empty") + } + return ttnpb.Clone(dev), []string{ + "ids.application_ids", + "ids.dev_eui", + "ids.device_id", + "ids.join_eui", + "provisioner_id", + "provisioning_data", + }, nil + }, + ) + if !a.So(err, should.BeNil) || !a.So(ret, should.NotBeNil) { + t.Fatalf("Failed to create device: %s", err) + } + a.So(*ttnpb.StdTime(ret.CreatedAt), should.HappenAfter, start) + a.So(*ttnpb.StdTime(ret.UpdatedAt), should.HappenAfter, start) + a.So(ret.UpdatedAt, should.Equal, ret.CreatedAt) + dev.CreatedAt = ret.CreatedAt + dev.UpdatedAt = ret.UpdatedAt + a.So(ret, should.HaveEmptyDiff, dev) + + retCtx, err = reg.GetByEUI(ctx, joinEUI, devEUI, ttnpb.EndDeviceFieldPathsTopLevel) + if !a.So(err, should.BeNil) { + t.Fatalf("Failed to get device: %s", err) + } + a.So(retCtx.EndDevice, should.HaveEmptyDiff, dev) + } + + // Batch Delete + deleted, err := reg.BatchDelete( + ctx, + pb.Ids.ApplicationIds, + []string{ + pb1.Ids.DeviceId, + pb2.Ids.DeviceId, + pb3.Ids.DeviceId, + // This unknown device will be ignored. + "test-dev-4", + }, + ) + if !a.So(err, should.BeNil) { + t.Fatalf("BatchDelete failed with: %s", errors.Stack(err)) + } + if !a.So(deleted, should.HaveLength, 3) { + t.Fatalf("BatchDelete returned wrong number of devices: %d", len(deleted)) + } + if !a.So(deleted, should.Resemble, []*ttnpb.EndDeviceIdentifiers{pb1.Ids, pb2.Ids, pb3.Ids}) { + t.Fatalf("Unexpected response from BatchDelete: %s", deleted) + } + + // Check that the devices are deleted + for _, pb := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + devEUI := types.EUI64(pb.GetIds().DevEui) + retCtx, err := reg.GetByEUI(ctx, joinEUI, devEUI, ttnpb.EndDeviceFieldPathsTopLevel) + if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) { + t.Fatalf("Error received: %v", err) + } + a.So(retCtx, should.BeNil) + } } func TestDeviceRegistries(t *testing.T) { @@ -432,6 +524,81 @@ func handleKeyRegistryTest(t *testing.T, reg KeyRegistry) { t.Fatalf("Error received: %v", err) } } + + // Batch Operations + noOfKeysPerDevice := uint8(10) + devEUI1 := types.EUI64{0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + devEUI2 := types.EUI64{0x45, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + devEUI3 := types.EUI64{0x46, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + + // Create keys for each device + for _, devEUI := range []types.EUI64{devEUI1, devEUI2, devEUI3} { + for i := byte(0); i < noOfKeysPerDevice; i++ { + sid := bytes.Repeat([]byte{i}, 4) + _, err := reg.SetByID(ctx, joinEUI, devEUI, sid, []string{ + "app_s_key", + "f_nwk_s_int_key", + "nwk_s_enc_key", + "s_nwk_s_int_key", + }, + func(stored *ttnpb.SessionKeys) (*ttnpb.SessionKeys, []string, error) { + if !a.So(stored, should.BeNil) { + t.Fatal("Registry is not empty") + } + return &ttnpb.SessionKeys{ + SessionKeyId: sid, + FNwkSIntKey: test.DefaultFNwkSIntKeyEnvelope, + SNwkSIntKey: test.DefaultSNwkSIntKeyEnvelope, + NwkSEncKey: test.DefaultNwkSEncKeyEnvelope, + AppSKey: test.DefaultAppSKeyEnvelope, + }, []string{ + "app_s_key", + "f_nwk_s_int_key", + "nwk_s_enc_key", + "s_nwk_s_int_key", + "session_key_id", + }, nil + }) + if err != nil { + t.Fatalf("Error creating session key with ID %v for devEUI %v: %v", sid, devEUI, err) + } + // Read the keys back + _, err = reg.GetByID(ctx, joinEUI, devEUI, sid, ttnpb.SessionKeysFieldPathsTopLevel) + if err != nil { + t.Fatalf("Error reading session key with ID %v for devEUI %v: %v", sid, devEUI, err) + } + } + } + + // Batch Delete + err = reg.BatchDelete(ctx, []*ttnpb.EndDeviceIdentifiers{ + { + JoinEui: joinEUI.Bytes(), + DevEui: devEUI1.Bytes(), + }, + { + JoinEui: joinEUI.Bytes(), + DevEui: devEUI2.Bytes(), + }, + { + JoinEui: joinEUI.Bytes(), + DevEui: devEUI3.Bytes(), + }, + }) + if !a.So(err, should.BeNil) { + t.Fatalf("Could not BatchDelete keys: %v", err) + } + + // Check if all keys are deleted + for _, devEUI := range []types.EUI64{devEUI1, devEUI2, devEUI3} { + for i := byte(0); i < noOfKeysPerDevice; i++ { + sid := bytes.Repeat([]byte{i}, 4) + _, err := reg.GetByID(ctx, joinEUI, devEUI, sid, ttnpb.SessionKeysFieldPathsTopLevel) + if !a.So(err, should.NotBeNil) || !a.So(errors.IsNotFound(err), should.BeTrue) { + t.Fatalf("Error received: %v", err) + } + } + } } func TestSessionKeyRegistries(t *testing.T) { diff --git a/pkg/joinserver/util_test.go b/pkg/joinserver/util_test.go index 14ab577208..a39f9194ce 100644 --- a/pkg/joinserver/util_test.go +++ b/pkg/joinserver/util_test.go @@ -32,4 +32,7 @@ func (p *byteToSerialNumber) UniqueID(entry *structpb.Struct) (string, error) { func init() { provisioning.Register("mock", &byteToSerialNumber{}) + provisioning.Register("mock-provisioner-1", &byteToSerialNumber{}) + provisioning.Register("mock-provisioner-2", &byteToSerialNumber{}) + provisioning.Register("mock-provisioner-3", &byteToSerialNumber{}) } diff --git a/pkg/networkserver/grpc_deviceregistry.go b/pkg/networkserver/grpc_deviceregistry.go index e6ab10f8b2..c0e84fcfc9 100644 --- a/pkg/networkserver/grpc_deviceregistry.go +++ b/pkg/networkserver/grpc_deviceregistry.go @@ -62,6 +62,14 @@ var ( events.WithClientInfoFromContext(), events.WithPropagateToParent(), ) + evtBatchDeleteEndDevices = events.Define( + "ns.end_device.batch.delete", "batch delete end devices", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ), + events.WithDataType(&ttnpb.EndDeviceIdentifiersList{}), + events.WithAuthFromContext(), + events.WithClientInfoFromContext(), + events.WithPropagateToParent(), + ) ) const maxRequiredDeviceReadRightCount = 3 @@ -2893,6 +2901,44 @@ func (ns *NetworkServer) Delete(ctx context.Context, req *ttnpb.EndDeviceIdentif return ttnpb.Empty, nil } +type nsEndDeviceBatchRegistry struct { + ttnpb.UnimplementedNsEndDeviceBatchRegistryServer + + NS *NetworkServer +} + +// Delete implements ttipb.NsEndDeviceBatchRegistryServer. +func (srv *nsEndDeviceBatchRegistry) Delete( + ctx context.Context, + req *ttnpb.BatchDeleteEndDevicesRequest, +) (*emptypb.Empty, error) { + // Check if the user has rights on the application. + if err := rights.RequireApplication( + ctx, + req.ApplicationIds, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + deleted, err := srv.NS.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) + if err != nil { + logRegistryRPCError(ctx, err, "Failed to delete device from registry") + return nil, err + } + + if len(deleted) != 0 { + events.Publish( + evtBatchDeleteEndDevices.NewWithIdentifiersAndData( + ctx, req.ApplicationIds, &ttnpb.EndDeviceIdentifiersList{ + EndDeviceIds: deleted, + }, + ), + ) + } + + return ttnpb.Empty, nil +} + func init() { // The legacy and modern ADR fields should be mutually exclusive. // As such, specifying one of the fields means that every other field of the opposite diff --git a/pkg/networkserver/grpc_deviceregistry_test.go b/pkg/networkserver/grpc_deviceregistry_test.go index 5f1c635455..66b7293b64 100644 --- a/pkg/networkserver/grpc_deviceregistry_test.go +++ b/pkg/networkserver/grpc_deviceregistry_test.go @@ -1368,3 +1368,385 @@ func TestDeviceRegistryDelete(t *testing.T) { }) } } + +func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest + registeredApplicationID := "test-app" + registeredApplicationIDs := &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + } + dev1 := &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + }, + DeviceId: "test-device-1", + JoinEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + DevEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + }, + RootKeys: &ttnpb.RootKeys{ + RootKeyId: "testKey", + NwkKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x0, + }.Bytes(), + KekLabel: "test", + }, + AppKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{ + 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + }.Bytes(), + KekLabel: "test", + }, + }, + } + dev2 := ttnpb.Clone(dev1) + dev2.Ids.DeviceId = "test-device-2" + dev2.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev2.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + dev3 := ttnpb.Clone(dev1) + dev3.Ids.DeviceId = "test-device-3" + dev3.Ids.JoinEui = types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + dev3.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + for _, tc := range []struct { + Name string + ContextFunc func(context.Context) context.Context + BatchDeleteFunc func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) + Request *ttnpb.BatchDeleteEndDevicesRequest + ErrorAssertion func(*testing.T, error) bool + BatchDeleteCalls uint64 + }{ + { + Name: "No device write rights", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Non-existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func(ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + // Devices not found are skipped. + return nil, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "Wrong application ID", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-unknown-app-id"}, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsPermissionDenied(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Invalid Device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + "test-dev-&*@(#)", + }, + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + err := errors.New("BatchDeleteFunc must not be called") + test.MustTFromContext(ctx).Error(err) + return nil, err + }, + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + if !assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) { + t.Errorf("Received error: %s", err) + return false + } + return true + }, + BatchDeleteCalls: 0, + }, + { + Name: "Existing device", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 1) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + a.So(deviceIDs[0], should.Equal, dev1.GetIds().DeviceId) + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "One invalid device in batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(deviceIDs, should.HaveLength, 3) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + t.Log("Known device ID") + case "test-dev-unknown-id": + t.Log("Ignore expected unknown device ID") + default: + t.Log("Unexpected device ID") + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + "test-dev-unknown-id", + }, + }, + BatchDeleteCalls: 1, + }, + { + Name: "Valid Batch", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), registeredApplicationIDs): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(appIDs, should.Resemble, registeredApplicationIDs) + a.So(deviceIDs, should.HaveLength, 3) + for _, devID := range deviceIDs { + switch devID { + case dev1.GetIds().DeviceId: + case dev2.GetIds().DeviceId: + case dev3.GetIds().DeviceId: + // Known device ID + default: + t.Error("Unknown device ID: ", devID) + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + dev3.Ids, + }, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + }, + } { + tc := tc + test.RunSubtest(t, test.SubtestConfig{ + Name: tc.Name, + Parallel: true, + Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { + t.Helper() + var batchDeleteCalls uint64 + ns, ctx, env, stop := StartTest( + ctx, + TestConfig{ + NetworkServer: Config{ + Devices: &MockDeviceRegistry{ + BatchDeleteFunc: func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) { + atomic.AddUint64(&batchDeleteCalls, 1) + return tc.BatchDeleteFunc(ctx, appIDs, deviceIDs) + }, + }, + }, + TaskStarter: StartTaskExclude( + DownlinkProcessTaskName, + DownlinkDispatchTaskName, + ), + }, + ) + defer stop() + + go LogEvents(t, env.Events) + + ns.AddContextFiller(tc.ContextFunc) + ns.AddContextFiller(func(ctx context.Context) context.Context { + return test.ContextWithTB(ctx, t) + }) + + req := ttnpb.Clone(tc.Request) + a.So(req, should.Resemble, tc.Request) + + _, err := ttnpb.NewNsEndDeviceBatchRegistryClient(ns.LoopbackConn()).Delete(ctx, req) + a.So(batchDeleteCalls, should.Equal, tc.BatchDeleteCalls) + if tc.ErrorAssertion != nil { + a.So(tc.ErrorAssertion(t, err), should.BeTrue) + } else { + a.So(err, should.BeNil) + } + }, + }) + } +} diff --git a/pkg/networkserver/internal/test/shared/device_registry.go b/pkg/networkserver/internal/test/shared/device_registry.go index 1ff2f46560..bc99b37aac 100644 --- a/pkg/networkserver/internal/test/shared/device_registry.go +++ b/pkg/networkserver/internal/test/shared/device_registry.go @@ -134,7 +134,12 @@ func handleDeviceRegistryTest(ctx context.Context, reg DeviceRegistry) { t, a := test.MustNewTFromContext(ctx) t.Helper() - stored, storedCtx, err := reg.GetByID(ctx, pb.Ids.ApplicationIds, pb.Ids.DeviceId, ttnpb.EndDeviceFieldPathsTopLevel) + stored, storedCtx, err := reg.GetByID( + ctx, + pb.Ids.ApplicationIds, + pb.Ids.DeviceId, + ttnpb.EndDeviceFieldPathsTopLevel, + ) if !test.AllTrue( a.So(err, should.NotBeNil), a.So(errors.IsNotFound(err), should.BeTrue), @@ -272,25 +277,39 @@ func handleDeviceRegistryTest(ctx context.Context, reg DeviceRegistry) { }, }, }, - MacState: MakeDefaultEU868MACState(ttnpb.Class_CLASS_A, ttnpb.MACVersion_MAC_V1_0_3, ttnpb.PHYVersion_RP001_V1_0_3_REV_A), + MacState: MakeDefaultEU868MACState( + ttnpb.Class_CLASS_A, + ttnpb.MACVersion_MAC_V1_0_3, + ttnpb.PHYVersion_RP001_V1_0_3_REV_A, + ), PendingSession: &ttnpb.Session{ DevAddr: types.DevAddr{0x43, 0xff, 0xff, 0xff}.Bytes(), Keys: &ttnpb.SessionKeys{ FNwkSIntKey: &ttnpb.KeyEnvelope{ - EncryptedKey: []byte{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}, - KekLabel: "kek-label", + EncryptedKey: []byte{ + 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + }, + KekLabel: "kek-label", }, SNwkSIntKey: &ttnpb.KeyEnvelope{ - EncryptedKey: []byte{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd}, - KekLabel: "kek-label", + EncryptedKey: []byte{ + 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, + }, + KekLabel: "kek-label", }, NwkSEncKey: &ttnpb.KeyEnvelope{ - EncryptedKey: []byte{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc}, - KekLabel: "kek-label", + EncryptedKey: []byte{ + 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + }, + KekLabel: "kek-label", }, }, }, - PendingMacState: MakeDefaultEU868MACState(ttnpb.Class_CLASS_A, ttnpb.MACVersion_MAC_V1_1, ttnpb.PHYVersion_RP001_V1_1_REV_B), + PendingMacState: MakeDefaultEU868MACState( + ttnpb.Class_CLASS_A, + ttnpb.MACVersion_MAC_V1_1, + ttnpb.PHYVersion_RP001_V1_1_REV_B, + ), } pbFields := []string{ "frequency_plan_id", @@ -391,6 +410,54 @@ func handleDeviceRegistryTest(ctx context.Context, reg DeviceRegistry) { if !a.So(assertNoDevice(ctx, pbOther), should.BeTrue) { t.Fatalf("Failed to assert registry emptiness after pbOther deletion") } + + // Batch Operations + pb1 := ttnpb.Clone(pb) + pb1.Ids.DeviceId = "test-dev-1" + pb1.Ids.DevEui = types.EUI64{0x42, 0x43, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + pb2 := ttnpb.Clone(pb) + pb2.Ids.DeviceId = "test-dev-2" + pb2.Ids.DevEui = types.EUI64{0x42, 0x44, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + + pb3 := ttnpb.Clone(pb) + pb3.Ids.DeviceId = "test-dev-3" + pb3.Ids.DevEui = types.EUI64{0x42, 0x45, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes() + pb3.PendingSession = nil + + // Create the devices + for _, pb := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + assertCreateDevice(ctx, pb, pbFields...) + } + + // Batch Delete + deleted, err := reg.BatchDelete( + ctx, + pb.Ids.ApplicationIds, + []string{ + pb1.Ids.DeviceId, + pb2.Ids.DeviceId, + pb3.Ids.DeviceId, + // This unknown device will be ignored. + "test-dev-4", + }, + ) + if !a.So(err, should.BeNil) { + t.Fatalf("BatchDelete failed with: %s", errors.Stack(err)) + } + if !a.So(deleted, should.HaveLength, 3) { + t.Fatalf("BatchDelete returned wrong number of devices: %d", len(deleted)) + } + if !a.So(deleted, should.Resemble, []*ttnpb.EndDeviceIdentifiers{pb1.Ids, pb2.Ids, pb3.Ids}) { + t.Fatalf("Unexpected response from BatchDelete: %s", deleted) + } + + // Check that the devices are deleted + for _, pb := range []*ttnpb.EndDevice{pb1, pb2, pb3} { + if !a.So(assertNoDevice(ctx, pb), should.BeTrue) { + t.Fatalf("Registry not empty after BatchDelete") + } + } } // HandleDeviceRegistryTest runs a DeviceRegistry test suite on reg. diff --git a/pkg/networkserver/networkserver.go b/pkg/networkserver/networkserver.go index 5f3d9aecc8..d9951a239a 100644 --- a/pkg/networkserver/networkserver.go +++ b/pkg/networkserver/networkserver.go @@ -126,7 +126,8 @@ type NetworkServer struct { *component.Component ctx context.Context - devices DeviceRegistry + devices DeviceRegistry + batchDevices *nsEndDeviceBatchRegistry netID types.NetID clusterID string @@ -271,6 +272,9 @@ func New(c *component.Component, conf *Config, opts ...Option) (*NetworkServer, QueueSize: int(conf.ApplicationUplinkQueue.FastBufferSize), MaxWorkers: int(conf.ApplicationUplinkQueue.FastNumConsumers), }) + ns.batchDevices = &nsEndDeviceBatchRegistry{ + NS: ns, + } ctx = ns.Context() if len(opts) == 0 { @@ -291,6 +295,7 @@ func New(c *component.Component, conf *Config, opts ...Option) (*NetworkServer, "/ttn.lorawan.v3.GsNs", "/ttn.lorawan.v3.AsNs", "/ttn.lorawan.v3.NsEndDeviceRegistry", + "/ttn.lorawan.v3.NsEndDeviceBatchRegistry", "/ttn.lorawan.v3.Ns", } { c.GRPC.RegisterUnaryHook(filter, hook.name, hook.middleware) @@ -360,12 +365,14 @@ func (ns *NetworkServer) RegisterServices(s *grpc.Server) { ttnpb.RegisterGsNsServer(s, ns) ttnpb.RegisterAsNsServer(s, ns) ttnpb.RegisterNsEndDeviceRegistryServer(s, ns) + ttnpb.RegisterNsEndDeviceBatchRegistryServer(s, ns.batchDevices) ttnpb.RegisterNsServer(s, ns) } // RegisterHandlers registers gRPC handlers. func (ns *NetworkServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.ClientConn) { ttnpb.RegisterNsEndDeviceRegistryHandler(ns.Context(), s, conn) + ttnpb.RegisterNsEndDeviceBatchRegistryHandler(ns.Context(), s, conn) // nolint:errcheck ttnpb.RegisterNsHandler(ns.Context(), s, conn) } diff --git a/pkg/networkserver/networkserver_util_internal_test.go b/pkg/networkserver/networkserver_util_internal_test.go index e5036dd663..4c615527ed 100644 --- a/pkg/networkserver/networkserver_util_internal_test.go +++ b/pkg/networkserver/networkserver_util_internal_test.go @@ -2511,8 +2511,23 @@ var _ DeviceRegistry = MockDeviceRegistry{} // MockDeviceRegistry is a mock DeviceRegistry used for testing. type MockDeviceRegistry struct { - GetByIDFunc func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, paths []string) (*ttnpb.EndDevice, context.Context, error) - SetByIDFunc func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, paths []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) + GetByIDFunc func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, paths []string, + ) (*ttnpb.EndDevice, context.Context, error) + SetByIDFunc func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, + paths []string, + f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) + BatchDeleteFunc func( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } // GetByEUI panics. @@ -2545,3 +2560,15 @@ func (m MockDeviceRegistry) RangeByUplinkMatches(context.Context, *ttnpb.UplinkM func (m MockDeviceRegistry) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error { panic("Range must not be called") } + +// GetByID calls GetByIDFunc if set and panics otherwise. +func (m MockDeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + if m.BatchDeleteFunc == nil { + panic("BatchDeleteFunc called, but not set") + } + return m.BatchDeleteFunc(ctx, appIDs, deviceIDs) +} diff --git a/pkg/networkserver/redis/registry.go b/pkg/networkserver/redis/registry.go index 9703905cd9..d04be36a8e 100644 --- a/pkg/networkserver/redis/registry.go +++ b/pkg/networkserver/redis/registry.go @@ -920,26 +920,128 @@ func (r *DeviceRegistry) SetByID(ctx context.Context, appID *ttnpb.ApplicationId } // Range ranges over device uid keys in DeviceRegistry. -func (r *DeviceRegistry) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error { +func (r *DeviceRegistry) Range( + ctx context.Context, + paths []string, + f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool, +) error { deviceEntityRegex, err := ttnredis.EntityRegex((r.uidKey(unique.GenericID(ctx, "*")))) if err != nil { return err } - return ttnredis.RangeRedisKeys(ctx, r.Redis, r.uidKey(unique.GenericID(ctx, "*")), ttnredis.DefaultRangeCount, func(key string) (bool, error) { - if !deviceEntityRegex.MatchString(key) { + return ttnredis.RangeRedisKeys( + ctx, + r.Redis, + r.uidKey(unique.GenericID(ctx, "*")), + ttnredis.DefaultRangeCount, + func(key string) (bool, error) { + if !deviceEntityRegex.MatchString(key) { + return true, nil + } + dev := &ttnpb.EndDevice{} + if err := ttnredis.GetProto(ctx, r.Redis, key).ScanProto(dev); err != nil { + return false, err + } + dev, err := ttnpb.FilterGetEndDevice(dev, paths...) + if err != nil { + return false, err + } + if !f(ctx, dev.Ids, dev) { + return false, nil + } return true, nil + }) +} + +// BatchDelete implements DeviceRegistry. +// This function deletes all the devices in a single transaction. +func (r *DeviceRegistry) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + var ( + uidKeys = make([]string, 0, len(deviceIDs)) + ret = make([]*ttnpb.EndDeviceIdentifiers, 0) + ) + for _, devID := range deviceIDs { + uidKeys = append( + uidKeys, + r.uidKey( + unique.ID( + ctx, + &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: appIDs, + DeviceId: devID, + }), + ), + ) + } + + // Delete the devices in a single transaction. + transaction := func(tx *redis.Tx) error { + // Read the devices to delete. + addrMapping := make(map[string][]string) + raw, err := tx.MGet(ctx, uidKeys...).Result() + if err != nil { + return err } - dev := &ttnpb.EndDevice{} - if err := ttnredis.GetProto(ctx, r.Redis, key).ScanProto(dev); err != nil { - return false, err + euiKeys := make([]string, 0, len(raw)) + for _, raw := range raw { + switch val := raw.(type) { + case nil: + continue + case string: + dev := &ttnpb.EndDevice{} + if err := ttnredis.UnmarshalProto(val, dev); err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to decode stored end device") + continue + } + ret = append(ret, dev.Ids) + uid := unique.ID(ctx, dev.GetIds()) + if dev.Ids.JoinEui != nil && dev.Ids.DevEui != nil { + euiKeys = append(euiKeys, r.euiKey( + types.MustEUI64(dev.GetIds().GetJoinEui()).OrZero(), + types.MustEUI64(dev.GetIds().GetDevEui()).OrZero(), + )) + } + if dev.PendingSession != nil { + addrMapping[uid] = []string{ + PendingAddrKey(r.addrKey( + types.MustDevAddr(dev.PendingSession.DevAddr).OrZero(), + )), + } + } + if dev.Session != nil { + addrMapping[uid] = append(addrMapping[uid], []string{ + CurrentAddrKey(r.addrKey( + types.MustDevAddr(dev.Session.DevAddr).OrZero(), + )), + }...) + } + } } - dev, err := ttnpb.FilterGetEndDevice(dev, paths...) - if err != nil { - return false, err + if err := tx.Watch(ctx, euiKeys...).Err(); err != nil { + return err } - if !f(ctx, dev.Ids, dev) { - return false, nil + if _, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error { + p.Del(ctx, append(uidKeys, euiKeys...)...) + for uid, keys := range addrMapping { + for _, key := range keys { + removeAddrMapping(ctx, p, key, uid) + } + } + return nil + }); err != nil { + return err } - return true, nil - }) + return nil + } + + defer trace.StartRegion(ctx, "batch delete end devices").End() + err := r.Redis.Watch(ctx, transaction, uidKeys...) + if err != nil { + return nil, ttnredis.ConvertError(err) + } + return ret, nil } diff --git a/pkg/networkserver/registry.go b/pkg/networkserver/registry.go index f348ae2ec1..2c58d582f9 100644 --- a/pkg/networkserver/registry.go +++ b/pkg/networkserver/registry.go @@ -43,6 +43,11 @@ type DeviceRegistry interface { RangeByUplinkMatches(ctx context.Context, up *ttnpb.UplinkMessage, f func(context.Context, *UplinkMatch) (bool, error)) error SetByID(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, paths []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error + BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, + ) ([]*ttnpb.EndDeviceIdentifiers, error) } var errDeviceExists = errors.DefineAlreadyExists("device_exists", "device already exists") @@ -135,6 +140,14 @@ func (w replacedEndDeviceFieldRegistryWrapper) SetByID(ctx context.Context, appI return dev, ctx, nil } +func (w replacedEndDeviceFieldRegistryWrapper) BatchDelete( + ctx context.Context, + appIDs *ttnpb.ApplicationIdentifiers, + deviceIDs []string, +) ([]*ttnpb.EndDeviceIdentifiers, error) { + return w.DeviceRegistry.BatchDelete(ctx, appIDs, deviceIDs) +} + func (w replacedEndDeviceFieldRegistryWrapper) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error { paths, replaced := registry.MatchReplacedEndDeviceFields(paths, w.fields) return w.DeviceRegistry.Range(ctx, paths, func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, dev *ttnpb.EndDevice) bool { diff --git a/pkg/ttnpb/applicationserver.pb.go b/pkg/ttnpb/applicationserver.pb.go index 0ba16ee867..158c8cd3b8 100644 --- a/pkg/ttnpb/applicationserver.pb.go +++ b/pkg/ttnpb/applicationserver.pb.go @@ -1538,10 +1538,22 @@ var file_lorawan_stack_api_applicationserver_proto_rawDesc = []byte{ 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x7d, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, - 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, - 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x7d, 0x32, 0xb4, 0x01, 0x0a, 0x18, 0x41, 0x73, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, 0x97, + 0x01, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x47, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x41, 0x2a, 0x3f, 0x2f, 0x61, 0x73, 0x2f, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, + 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, + 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1592,10 +1604,11 @@ var file_lorawan_stack_api_applicationserver_proto_goTypes = []interface{}{ (*DownlinkQueueRequest)(nil), // 30: ttn.lorawan.v3.DownlinkQueueRequest (*GetEndDeviceRequest)(nil), // 31: ttn.lorawan.v3.GetEndDeviceRequest (*SetEndDeviceRequest)(nil), // 32: ttn.lorawan.v3.SetEndDeviceRequest - (*emptypb.Empty)(nil), // 33: google.protobuf.Empty - (*ApplicationDownlinks)(nil), // 34: ttn.lorawan.v3.ApplicationDownlinks - (*MQTTConnectionInfo)(nil), // 35: ttn.lorawan.v3.MQTTConnectionInfo - (*EndDevice)(nil), // 36: ttn.lorawan.v3.EndDevice + (*BatchDeleteEndDevicesRequest)(nil), // 33: ttn.lorawan.v3.BatchDeleteEndDevicesRequest + (*emptypb.Empty)(nil), // 34: google.protobuf.Empty + (*ApplicationDownlinks)(nil), // 35: ttn.lorawan.v3.ApplicationDownlinks + (*MQTTConnectionInfo)(nil), // 36: ttn.lorawan.v3.MQTTConnectionInfo + (*EndDevice)(nil), // 37: ttn.lorawan.v3.EndDevice } var file_lorawan_stack_api_applicationserver_proto_depIdxs = []int32{ 18, // 0: ttn.lorawan.v3.ApplicationLink.default_formatters:type_name -> ttn.lorawan.v3.MessagePayloadFormatters @@ -1649,26 +1662,28 @@ var file_lorawan_stack_api_applicationserver_proto_depIdxs = []int32{ 31, // 48: ttn.lorawan.v3.AsEndDeviceRegistry.Get:input_type -> ttn.lorawan.v3.GetEndDeviceRequest 32, // 49: ttn.lorawan.v3.AsEndDeviceRegistry.Set:input_type -> ttn.lorawan.v3.SetEndDeviceRequest 24, // 50: ttn.lorawan.v3.AsEndDeviceRegistry.Delete:input_type -> ttn.lorawan.v3.EndDeviceIdentifiers - 1, // 51: ttn.lorawan.v3.As.GetLink:output_type -> ttn.lorawan.v3.ApplicationLink - 1, // 52: ttn.lorawan.v3.As.SetLink:output_type -> ttn.lorawan.v3.ApplicationLink - 33, // 53: ttn.lorawan.v3.As.DeleteLink:output_type -> google.protobuf.Empty - 4, // 54: ttn.lorawan.v3.As.GetLinkStats:output_type -> ttn.lorawan.v3.ApplicationLinkStats - 7, // 55: ttn.lorawan.v3.As.GetConfiguration:output_type -> ttn.lorawan.v3.GetAsConfigurationResponse - 33, // 56: ttn.lorawan.v3.NsAs.HandleUplink:output_type -> google.protobuf.Empty - 23, // 57: ttn.lorawan.v3.AppAs.Subscribe:output_type -> ttn.lorawan.v3.ApplicationUp - 33, // 58: ttn.lorawan.v3.AppAs.DownlinkQueuePush:output_type -> google.protobuf.Empty - 33, // 59: ttn.lorawan.v3.AppAs.DownlinkQueueReplace:output_type -> google.protobuf.Empty - 34, // 60: ttn.lorawan.v3.AppAs.DownlinkQueueList:output_type -> ttn.lorawan.v3.ApplicationDownlinks - 35, // 61: ttn.lorawan.v3.AppAs.GetMQTTConnectionInfo:output_type -> ttn.lorawan.v3.MQTTConnectionInfo - 33, // 62: ttn.lorawan.v3.AppAs.SimulateUplink:output_type -> google.protobuf.Empty - 10, // 63: ttn.lorawan.v3.AppAs.EncodeDownlink:output_type -> ttn.lorawan.v3.EncodeDownlinkResponse - 12, // 64: ttn.lorawan.v3.AppAs.DecodeUplink:output_type -> ttn.lorawan.v3.DecodeUplinkResponse - 14, // 65: ttn.lorawan.v3.AppAs.DecodeDownlink:output_type -> ttn.lorawan.v3.DecodeDownlinkResponse - 36, // 66: ttn.lorawan.v3.AsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice - 36, // 67: ttn.lorawan.v3.AsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice - 33, // 68: ttn.lorawan.v3.AsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty - 51, // [51:69] is the sub-list for method output_type - 33, // [33:51] is the sub-list for method input_type + 33, // 51: ttn.lorawan.v3.AsEndDeviceBatchRegistry.Delete:input_type -> ttn.lorawan.v3.BatchDeleteEndDevicesRequest + 1, // 52: ttn.lorawan.v3.As.GetLink:output_type -> ttn.lorawan.v3.ApplicationLink + 1, // 53: ttn.lorawan.v3.As.SetLink:output_type -> ttn.lorawan.v3.ApplicationLink + 34, // 54: ttn.lorawan.v3.As.DeleteLink:output_type -> google.protobuf.Empty + 4, // 55: ttn.lorawan.v3.As.GetLinkStats:output_type -> ttn.lorawan.v3.ApplicationLinkStats + 7, // 56: ttn.lorawan.v3.As.GetConfiguration:output_type -> ttn.lorawan.v3.GetAsConfigurationResponse + 34, // 57: ttn.lorawan.v3.NsAs.HandleUplink:output_type -> google.protobuf.Empty + 23, // 58: ttn.lorawan.v3.AppAs.Subscribe:output_type -> ttn.lorawan.v3.ApplicationUp + 34, // 59: ttn.lorawan.v3.AppAs.DownlinkQueuePush:output_type -> google.protobuf.Empty + 34, // 60: ttn.lorawan.v3.AppAs.DownlinkQueueReplace:output_type -> google.protobuf.Empty + 35, // 61: ttn.lorawan.v3.AppAs.DownlinkQueueList:output_type -> ttn.lorawan.v3.ApplicationDownlinks + 36, // 62: ttn.lorawan.v3.AppAs.GetMQTTConnectionInfo:output_type -> ttn.lorawan.v3.MQTTConnectionInfo + 34, // 63: ttn.lorawan.v3.AppAs.SimulateUplink:output_type -> google.protobuf.Empty + 10, // 64: ttn.lorawan.v3.AppAs.EncodeDownlink:output_type -> ttn.lorawan.v3.EncodeDownlinkResponse + 12, // 65: ttn.lorawan.v3.AppAs.DecodeUplink:output_type -> ttn.lorawan.v3.DecodeUplinkResponse + 14, // 66: ttn.lorawan.v3.AppAs.DecodeDownlink:output_type -> ttn.lorawan.v3.DecodeDownlinkResponse + 37, // 67: ttn.lorawan.v3.AsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice + 37, // 68: ttn.lorawan.v3.AsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice + 34, // 69: ttn.lorawan.v3.AsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty + 34, // 70: ttn.lorawan.v3.AsEndDeviceBatchRegistry.Delete:output_type -> google.protobuf.Empty + 52, // [52:71] is the sub-list for method output_type + 33, // [33:52] is the sub-list for method input_type 33, // [33:33] is the sub-list for extension type_name 33, // [33:33] is the sub-list for extension extendee 0, // [0:33] is the sub-list for field type_name @@ -1897,7 +1912,7 @@ func file_lorawan_stack_api_applicationserver_proto_init() { NumEnums: 1, NumMessages: 17, NumExtensions: 0, - NumServices: 4, + NumServices: 5, }, GoTypes: file_lorawan_stack_api_applicationserver_proto_goTypes, DependencyIndexes: file_lorawan_stack_api_applicationserver_proto_depIdxs, diff --git a/pkg/ttnpb/applicationserver.pb.gw.go b/pkg/ttnpb/applicationserver.pb.gw.go index 01e38727f1..edf77f48da 100644 --- a/pkg/ttnpb/applicationserver.pb.gw.go +++ b/pkg/ttnpb/applicationserver.pb.gw.go @@ -1297,6 +1297,76 @@ func local_request_AsEndDeviceRegistry_Delete_0(ctx context.Context, marshaler r } +var ( + filter_AsEndDeviceBatchRegistry_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{"application_ids": 0, "application_id": 1, "applicationId": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 1, 3, 4}} +) + +func request_AsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client AsEndDeviceBatchRegistryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_AsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server AsEndDeviceBatchRegistryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Delete(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterAsHandlerServer registers the http handlers for service As to "mux". // UnaryRPC :call AsServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1749,6 +1819,40 @@ func RegisterAsEndDeviceRegistryHandlerServer(ctx context.Context, mux *runtime. return nil } +// RegisterAsEndDeviceBatchRegistryHandlerServer registers the http handlers for service AsEndDeviceBatchRegistry to "mux". +// UnaryRPC :call AsEndDeviceBatchRegistryServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAsEndDeviceBatchRegistryHandlerFromEndpoint instead. +func RegisterAsEndDeviceBatchRegistryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AsEndDeviceBatchRegistryServer) error { + + mux.Handle("DELETE", pattern_AsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.AsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/as/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_AsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterAsHandlerFromEndpoint is same as RegisterAsHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterAsHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -2325,3 +2429,74 @@ var ( forward_AsEndDeviceRegistry_Delete_0 = runtime.ForwardResponseMessage ) + +// RegisterAsEndDeviceBatchRegistryHandlerFromEndpoint is same as RegisterAsEndDeviceBatchRegistryHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterAsEndDeviceBatchRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterAsEndDeviceBatchRegistryHandler(ctx, mux, conn) +} + +// RegisterAsEndDeviceBatchRegistryHandler registers the http handlers for service AsEndDeviceBatchRegistry to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterAsEndDeviceBatchRegistryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterAsEndDeviceBatchRegistryHandlerClient(ctx, mux, NewAsEndDeviceBatchRegistryClient(conn)) +} + +// RegisterAsEndDeviceBatchRegistryHandlerClient registers the http handlers for service AsEndDeviceBatchRegistry +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AsEndDeviceBatchRegistryClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AsEndDeviceBatchRegistryClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "AsEndDeviceBatchRegistryClient" to call the correct interceptors. +func RegisterAsEndDeviceBatchRegistryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AsEndDeviceBatchRegistryClient) error { + + mux.Handle("DELETE", pattern_AsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.AsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/as/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_AsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_AsEndDeviceBatchRegistry_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"as", "applications", "application_ids.application_id", "devices", "batch"}, "")) +) + +var ( + forward_AsEndDeviceBatchRegistry_Delete_0 = runtime.ForwardResponseMessage +) diff --git a/pkg/ttnpb/applicationserver_grpc.pb.go b/pkg/ttnpb/applicationserver_grpc.pb.go index 06ccb0a19f..ce7132f738 100644 --- a/pkg/ttnpb/applicationserver_grpc.pb.go +++ b/pkg/ttnpb/applicationserver_grpc.pb.go @@ -984,3 +984,100 @@ var AsEndDeviceRegistry_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "lorawan-stack/api/applicationserver.proto", } + +const ( + AsEndDeviceBatchRegistry_Delete_FullMethodName = "/ttn.lorawan.v3.AsEndDeviceBatchRegistry/Delete" +) + +// AsEndDeviceBatchRegistryClient is the client API for AsEndDeviceBatchRegistry service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AsEndDeviceBatchRegistryClient interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type asEndDeviceBatchRegistryClient struct { + cc grpc.ClientConnInterface +} + +func NewAsEndDeviceBatchRegistryClient(cc grpc.ClientConnInterface) AsEndDeviceBatchRegistryClient { + return &asEndDeviceBatchRegistryClient{cc} +} + +func (c *asEndDeviceBatchRegistryClient) Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, AsEndDeviceBatchRegistry_Delete_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AsEndDeviceBatchRegistryServer is the server API for AsEndDeviceBatchRegistry service. +// All implementations must embed UnimplementedAsEndDeviceBatchRegistryServer +// for forward compatibility +type AsEndDeviceBatchRegistryServer interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedAsEndDeviceBatchRegistryServer() +} + +// UnimplementedAsEndDeviceBatchRegistryServer must be embedded to have forward compatible implementations. +type UnimplementedAsEndDeviceBatchRegistryServer struct { +} + +func (UnimplementedAsEndDeviceBatchRegistryServer) Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedAsEndDeviceBatchRegistryServer) mustEmbedUnimplementedAsEndDeviceBatchRegistryServer() { +} + +// UnsafeAsEndDeviceBatchRegistryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AsEndDeviceBatchRegistryServer will +// result in compilation errors. +type UnsafeAsEndDeviceBatchRegistryServer interface { + mustEmbedUnimplementedAsEndDeviceBatchRegistryServer() +} + +func RegisterAsEndDeviceBatchRegistryServer(s grpc.ServiceRegistrar, srv AsEndDeviceBatchRegistryServer) { + s.RegisterService(&AsEndDeviceBatchRegistry_ServiceDesc, srv) +} + +func _AsEndDeviceBatchRegistry_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchDeleteEndDevicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AsEndDeviceBatchRegistryServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AsEndDeviceBatchRegistry_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AsEndDeviceBatchRegistryServer).Delete(ctx, req.(*BatchDeleteEndDevicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AsEndDeviceBatchRegistry_ServiceDesc is the grpc.ServiceDesc for AsEndDeviceBatchRegistry service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AsEndDeviceBatchRegistry_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.AsEndDeviceBatchRegistry", + HandlerType: (*AsEndDeviceBatchRegistryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Delete", + Handler: _AsEndDeviceBatchRegistry_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lorawan-stack/api/applicationserver.proto", +} diff --git a/pkg/ttnpb/end_device.pb.go b/pkg/ttnpb/end_device.pb.go index c944602a64..c308af4a10 100644 --- a/pkg/ttnpb/end_device.pb.go +++ b/pkg/ttnpb/end_device.pb.go @@ -2845,6 +2845,61 @@ func (x *ConvertEndDeviceTemplateRequest) GetEndDeviceVersionIds() *EndDeviceVer return nil } +type BatchDeleteEndDevicesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ApplicationIds *ApplicationIdentifiers `protobuf:"bytes,1,opt,name=application_ids,json=applicationIds,proto3" json:"application_ids,omitempty"` + DeviceIds []string `protobuf:"bytes,2,rep,name=device_ids,json=deviceIds,proto3" json:"device_ids,omitempty"` +} + +func (x *BatchDeleteEndDevicesRequest) Reset() { + *x = BatchDeleteEndDevicesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BatchDeleteEndDevicesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchDeleteEndDevicesRequest) ProtoMessage() {} + +func (x *BatchDeleteEndDevicesRequest) ProtoReflect() protoreflect.Message { + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchDeleteEndDevicesRequest.ProtoReflect.Descriptor instead. +func (*BatchDeleteEndDevicesRequest) Descriptor() ([]byte, []int) { + return file_lorawan_stack_api_end_device_proto_rawDescGZIP(), []int{23} +} + +func (x *BatchDeleteEndDevicesRequest) GetApplicationIds() *ApplicationIdentifiers { + if x != nil { + return x.ApplicationIds + } + return nil +} + +func (x *BatchDeleteEndDevicesRequest) GetDeviceIds() []string { + if x != nil { + return x.DeviceIds + } + return nil +} + type MACParameters_Channel struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2865,7 +2920,7 @@ type MACParameters_Channel struct { func (x *MACParameters_Channel) Reset() { *x = MACParameters_Channel{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[23] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2878,7 +2933,7 @@ func (x *MACParameters_Channel) String() string { func (*MACParameters_Channel) ProtoMessage() {} func (x *MACParameters_Channel) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[23] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2946,7 +3001,7 @@ type ADRSettings_StaticMode struct { func (x *ADRSettings_StaticMode) Reset() { *x = ADRSettings_StaticMode{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[24] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2959,7 +3014,7 @@ func (x *ADRSettings_StaticMode) String() string { func (*ADRSettings_StaticMode) ProtoMessage() {} func (x *ADRSettings_StaticMode) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[24] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3030,7 +3085,7 @@ type ADRSettings_DynamicMode struct { func (x *ADRSettings_DynamicMode) Reset() { *x = ADRSettings_DynamicMode{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[25] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3043,7 +3098,7 @@ func (x *ADRSettings_DynamicMode) String() string { func (*ADRSettings_DynamicMode) ProtoMessage() {} func (x *ADRSettings_DynamicMode) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[25] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3126,7 +3181,7 @@ type ADRSettings_DisabledMode struct { func (x *ADRSettings_DisabledMode) Reset() { *x = ADRSettings_DisabledMode{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[26] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3139,7 +3194,7 @@ func (x *ADRSettings_DisabledMode) String() string { func (*ADRSettings_DisabledMode) ProtoMessage() {} func (x *ADRSettings_DisabledMode) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[26] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3170,7 +3225,7 @@ type ADRSettings_DynamicMode_ChannelSteeringSettings struct { func (x *ADRSettings_DynamicMode_ChannelSteeringSettings) Reset() { *x = ADRSettings_DynamicMode_ChannelSteeringSettings{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[27] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3183,7 +3238,7 @@ func (x *ADRSettings_DynamicMode_ChannelSteeringSettings) String() string { func (*ADRSettings_DynamicMode_ChannelSteeringSettings) ProtoMessage() {} func (x *ADRSettings_DynamicMode_ChannelSteeringSettings) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[27] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3250,7 +3305,7 @@ type ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode struct { func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode) Reset() { *x = ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[28] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3263,7 +3318,7 @@ func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode) String( func (*ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode) ProtoMessage() {} func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[28] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3290,7 +3345,7 @@ type ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode struct { func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode) Reset() { *x = ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[29] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3303,7 +3358,7 @@ func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode) String() func (*ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode) ProtoMessage() {} func (x *ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[29] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3332,7 +3387,7 @@ type MACState_JoinRequest struct { func (x *MACState_JoinRequest) Reset() { *x = MACState_JoinRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[30] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3345,7 +3400,7 @@ func (x *MACState_JoinRequest) String() string { func (*MACState_JoinRequest) ProtoMessage() {} func (x *MACState_JoinRequest) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[30] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3400,7 +3455,7 @@ type MACState_JoinAccept struct { func (x *MACState_JoinAccept) Reset() { *x = MACState_JoinAccept{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[31] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3413,7 +3468,7 @@ func (x *MACState_JoinAccept) String() string { func (*MACState_JoinAccept) ProtoMessage() {} func (x *MACState_JoinAccept) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[31] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3489,7 +3544,7 @@ type MACState_UplinkMessage struct { func (x *MACState_UplinkMessage) Reset() { *x = MACState_UplinkMessage{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[32] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3502,7 +3557,7 @@ func (x *MACState_UplinkMessage) String() string { func (*MACState_UplinkMessage) ProtoMessage() {} func (x *MACState_UplinkMessage) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[32] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3574,7 +3629,7 @@ type MACState_DownlinkMessage struct { func (x *MACState_DownlinkMessage) Reset() { *x = MACState_DownlinkMessage{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[33] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3587,7 +3642,7 @@ func (x *MACState_DownlinkMessage) String() string { func (*MACState_DownlinkMessage) ProtoMessage() {} func (x *MACState_DownlinkMessage) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[33] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3629,7 +3684,7 @@ type MACState_DataRateRange struct { func (x *MACState_DataRateRange) Reset() { *x = MACState_DataRateRange{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[34] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3642,7 +3697,7 @@ func (x *MACState_DataRateRange) String() string { func (*MACState_DataRateRange) ProtoMessage() {} func (x *MACState_DataRateRange) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[34] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3683,7 +3738,7 @@ type MACState_DataRateRanges struct { func (x *MACState_DataRateRanges) Reset() { *x = MACState_DataRateRanges{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[35] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3696,7 +3751,7 @@ func (x *MACState_DataRateRanges) String() string { func (*MACState_DataRateRanges) ProtoMessage() {} func (x *MACState_DataRateRanges) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[35] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3730,7 +3785,7 @@ type MACState_UplinkMessage_TxSettings struct { func (x *MACState_UplinkMessage_TxSettings) Reset() { *x = MACState_UplinkMessage_TxSettings{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[37] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3743,7 +3798,7 @@ func (x *MACState_UplinkMessage_TxSettings) String() string { func (*MACState_UplinkMessage_TxSettings) ProtoMessage() {} func (x *MACState_UplinkMessage_TxSettings) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[37] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3782,7 +3837,7 @@ type MACState_UplinkMessage_RxMetadata struct { func (x *MACState_UplinkMessage_RxMetadata) Reset() { *x = MACState_UplinkMessage_RxMetadata{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[38] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3795,7 +3850,7 @@ func (x *MACState_UplinkMessage_RxMetadata) String() string { func (*MACState_UplinkMessage_RxMetadata) ProtoMessage() {} func (x *MACState_UplinkMessage_RxMetadata) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[38] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3862,7 +3917,7 @@ type MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata struct { func (x *MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata) Reset() { *x = MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[39] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3875,7 +3930,7 @@ func (x *MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata) String() string func (*MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata) ProtoMessage() {} func (x *MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[39] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3903,7 +3958,7 @@ type MACState_DownlinkMessage_Message struct { func (x *MACState_DownlinkMessage_Message) Reset() { *x = MACState_DownlinkMessage_Message{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[40] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3916,7 +3971,7 @@ func (x *MACState_DownlinkMessage_Message) String() string { func (*MACState_DownlinkMessage_Message) ProtoMessage() {} func (x *MACState_DownlinkMessage_Message) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[40] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3957,7 +4012,7 @@ type MACState_DownlinkMessage_Message_MHDR struct { func (x *MACState_DownlinkMessage_Message_MHDR) Reset() { *x = MACState_DownlinkMessage_Message_MHDR{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[41] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3970,7 +4025,7 @@ func (x *MACState_DownlinkMessage_Message_MHDR) String() string { func (*MACState_DownlinkMessage_Message_MHDR) ProtoMessage() {} func (x *MACState_DownlinkMessage_Message_MHDR) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[41] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4005,7 +4060,7 @@ type MACState_DownlinkMessage_Message_MACPayload struct { func (x *MACState_DownlinkMessage_Message_MACPayload) Reset() { *x = MACState_DownlinkMessage_Message_MACPayload{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[42] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4018,7 +4073,7 @@ func (x *MACState_DownlinkMessage_Message_MACPayload) String() string { func (*MACState_DownlinkMessage_Message_MACPayload) ProtoMessage() {} func (x *MACState_DownlinkMessage_Message_MACPayload) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[42] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4060,7 +4115,7 @@ type BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate struct { func (x *BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate) Reset() { *x = BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[45] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4073,7 +4128,7 @@ func (x *BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate) String() s func (*BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate) ProtoMessage() {} func (x *BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate) ProtoReflect() protoreflect.Message { - mi := &file_lorawan_stack_api_end_device_proto_msgTypes[45] + mi := &file_lorawan_stack_api_end_device_proto_msgTypes[46] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5438,16 +5493,28 @@ var file_lorawan_stack_api_end_device_proto_rawDesc = []byte{ 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x13, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x73, 0x2a, 0x55, 0x0a, 0x0a, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x11, 0x0a, 0x0d, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, - 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x42, 0x41, 0x54, 0x54, - 0x45, 0x52, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x45, - 0x58, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x1a, 0x0d, 0xea, 0xaa, 0x19, 0x09, 0x18, - 0x01, 0x2a, 0x05, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, - 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, - 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x73, 0x22, 0xca, 0x01, 0x0a, 0x1c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x59, 0x0a, 0x0f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x73, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0e, 0x61, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x4f, 0x0a, + 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x42, 0x30, 0xfa, 0x42, 0x2d, 0x92, 0x01, 0x2a, 0x08, 0x01, 0x10, 0x14, 0x22, 0x24, 0x72, + 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, + 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, + 0x2c, 0x7d, 0x24, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x2a, 0x55, + 0x0a, 0x0a, 0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x11, 0x0a, 0x0d, + 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x11, 0x0a, 0x0d, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x42, 0x41, 0x54, 0x54, 0x45, 0x52, 0x59, + 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x50, 0x4f, 0x57, 0x45, 0x52, 0x5f, 0x45, 0x58, 0x54, 0x45, + 0x52, 0x4e, 0x41, 0x4c, 0x10, 0x02, 0x1a, 0x0d, 0xea, 0xaa, 0x19, 0x09, 0x18, 0x01, 0x2a, 0x05, + 0x50, 0x4f, 0x57, 0x45, 0x52, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, + 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, + 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5463,7 +5530,7 @@ func file_lorawan_stack_api_end_device_proto_rawDescGZIP() []byte { } var file_lorawan_stack_api_end_device_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_lorawan_stack_api_end_device_proto_msgTypes = make([]protoimpl.MessageInfo, 47) +var file_lorawan_stack_api_end_device_proto_msgTypes = make([]protoimpl.MessageInfo, 48) var file_lorawan_stack_api_end_device_proto_goTypes = []interface{}{ (PowerState)(0), // 0: ttn.lorawan.v3.PowerState (*Session)(nil), // 1: ttn.lorawan.v3.Session @@ -5489,241 +5556,243 @@ var file_lorawan_stack_api_end_device_proto_goTypes = []interface{}{ (*EndDeviceTemplateFormat)(nil), // 21: ttn.lorawan.v3.EndDeviceTemplateFormat (*EndDeviceTemplateFormats)(nil), // 22: ttn.lorawan.v3.EndDeviceTemplateFormats (*ConvertEndDeviceTemplateRequest)(nil), // 23: ttn.lorawan.v3.ConvertEndDeviceTemplateRequest - (*MACParameters_Channel)(nil), // 24: ttn.lorawan.v3.MACParameters.Channel - (*ADRSettings_StaticMode)(nil), // 25: ttn.lorawan.v3.ADRSettings.StaticMode - (*ADRSettings_DynamicMode)(nil), // 26: ttn.lorawan.v3.ADRSettings.DynamicMode - (*ADRSettings_DisabledMode)(nil), // 27: ttn.lorawan.v3.ADRSettings.DisabledMode - (*ADRSettings_DynamicMode_ChannelSteeringSettings)(nil), // 28: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings - (*ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode)(nil), // 29: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode - (*ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode)(nil), // 30: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode - (*MACState_JoinRequest)(nil), // 31: ttn.lorawan.v3.MACState.JoinRequest - (*MACState_JoinAccept)(nil), // 32: ttn.lorawan.v3.MACState.JoinAccept - (*MACState_UplinkMessage)(nil), // 33: ttn.lorawan.v3.MACState.UplinkMessage - (*MACState_DownlinkMessage)(nil), // 34: ttn.lorawan.v3.MACState.DownlinkMessage - (*MACState_DataRateRange)(nil), // 35: ttn.lorawan.v3.MACState.DataRateRange - (*MACState_DataRateRanges)(nil), // 36: ttn.lorawan.v3.MACState.DataRateRanges - nil, // 37: ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry - (*MACState_UplinkMessage_TxSettings)(nil), // 38: ttn.lorawan.v3.MACState.UplinkMessage.TxSettings - (*MACState_UplinkMessage_RxMetadata)(nil), // 39: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata - (*MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata)(nil), // 40: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.PacketBrokerMetadata - (*MACState_DownlinkMessage_Message)(nil), // 41: ttn.lorawan.v3.MACState.DownlinkMessage.Message - (*MACState_DownlinkMessage_Message_MHDR)(nil), // 42: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR - (*MACState_DownlinkMessage_Message_MACPayload)(nil), // 43: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MACPayload - nil, // 44: ttn.lorawan.v3.EndDevice.AttributesEntry - nil, // 45: ttn.lorawan.v3.EndDevice.LocationsEntry - (*BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate)(nil), // 46: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate - nil, // 47: ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry - (*SessionKeys)(nil), // 48: ttn.lorawan.v3.SessionKeys - (*timestamppb.Timestamp)(nil), // 49: google.protobuf.Timestamp - (*ApplicationDownlink)(nil), // 50: ttn.lorawan.v3.ApplicationDownlink - (DataRateIndex)(0), // 51: ttn.lorawan.v3.DataRateIndex - (RxDelay)(0), // 52: ttn.lorawan.v3.RxDelay - (DataRateOffset)(0), // 53: ttn.lorawan.v3.DataRateOffset - (AggregatedDutyCycle)(0), // 54: ttn.lorawan.v3.AggregatedDutyCycle - (RejoinTimeExponent)(0), // 55: ttn.lorawan.v3.RejoinTimeExponent - (RejoinCountExponent)(0), // 56: ttn.lorawan.v3.RejoinCountExponent - (*ADRAckLimitExponentValue)(nil), // 57: ttn.lorawan.v3.ADRAckLimitExponentValue - (*ADRAckDelayExponentValue)(nil), // 58: ttn.lorawan.v3.ADRAckDelayExponentValue - (*DataRateIndexValue)(nil), // 59: ttn.lorawan.v3.DataRateIndexValue - (*EndDeviceVersionIdentifiers)(nil), // 60: ttn.lorawan.v3.EndDeviceVersionIdentifiers - (MACVersion)(0), // 61: ttn.lorawan.v3.MACVersion - (PHYVersion)(0), // 62: ttn.lorawan.v3.PHYVersion - (*MessagePayloadFormatters)(nil), // 63: ttn.lorawan.v3.MessagePayloadFormatters - (*durationpb.Duration)(nil), // 64: google.protobuf.Duration - (*PingSlotPeriodValue)(nil), // 65: ttn.lorawan.v3.PingSlotPeriodValue - (*ZeroableFrequencyValue)(nil), // 66: ttn.lorawan.v3.ZeroableFrequencyValue - (*RxDelayValue)(nil), // 67: ttn.lorawan.v3.RxDelayValue - (*DataRateOffsetValue)(nil), // 68: ttn.lorawan.v3.DataRateOffsetValue - (*FrequencyValue)(nil), // 69: ttn.lorawan.v3.FrequencyValue - (*AggregatedDutyCycleValue)(nil), // 70: ttn.lorawan.v3.AggregatedDutyCycleValue - (*wrapperspb.FloatValue)(nil), // 71: google.protobuf.FloatValue - (*wrapperspb.UInt32Value)(nil), // 72: google.protobuf.UInt32Value - (*DeviceEIRPValue)(nil), // 73: ttn.lorawan.v3.DeviceEIRPValue - (Class)(0), // 74: ttn.lorawan.v3.Class - (*MACCommand)(nil), // 75: ttn.lorawan.v3.MACCommand - (MACCommandIdentifier)(0), // 76: ttn.lorawan.v3.MACCommandIdentifier - (*EndDeviceIdentifiers)(nil), // 77: ttn.lorawan.v3.EndDeviceIdentifiers - (*Picture)(nil), // 78: ttn.lorawan.v3.Picture - (*RootKeys)(nil), // 79: ttn.lorawan.v3.RootKeys - (*structpb.Struct)(nil), // 80: google.protobuf.Struct - (*wrapperspb.BoolValue)(nil), // 81: google.protobuf.BoolValue - (*LoRaAllianceProfileIdentifiers)(nil), // 82: ttn.lorawan.v3.LoRaAllianceProfileIdentifiers - (*fieldmaskpb.FieldMask)(nil), // 83: google.protobuf.FieldMask - (*ApplicationIdentifiers)(nil), // 84: ttn.lorawan.v3.ApplicationIdentifiers - (*DLSettings)(nil), // 85: ttn.lorawan.v3.DLSettings - (*CFList)(nil), // 86: ttn.lorawan.v3.CFList - (*Message)(nil), // 87: ttn.lorawan.v3.Message - (*DataRate)(nil), // 88: ttn.lorawan.v3.DataRate - (*GatewayIdentifiers)(nil), // 89: ttn.lorawan.v3.GatewayIdentifiers - (DownlinkPathConstraint)(0), // 90: ttn.lorawan.v3.DownlinkPathConstraint - (MType)(0), // 91: ttn.lorawan.v3.MType - (*Location)(nil), // 92: ttn.lorawan.v3.Location + (*BatchDeleteEndDevicesRequest)(nil), // 24: ttn.lorawan.v3.BatchDeleteEndDevicesRequest + (*MACParameters_Channel)(nil), // 25: ttn.lorawan.v3.MACParameters.Channel + (*ADRSettings_StaticMode)(nil), // 26: ttn.lorawan.v3.ADRSettings.StaticMode + (*ADRSettings_DynamicMode)(nil), // 27: ttn.lorawan.v3.ADRSettings.DynamicMode + (*ADRSettings_DisabledMode)(nil), // 28: ttn.lorawan.v3.ADRSettings.DisabledMode + (*ADRSettings_DynamicMode_ChannelSteeringSettings)(nil), // 29: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings + (*ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode)(nil), // 30: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode + (*ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode)(nil), // 31: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode + (*MACState_JoinRequest)(nil), // 32: ttn.lorawan.v3.MACState.JoinRequest + (*MACState_JoinAccept)(nil), // 33: ttn.lorawan.v3.MACState.JoinAccept + (*MACState_UplinkMessage)(nil), // 34: ttn.lorawan.v3.MACState.UplinkMessage + (*MACState_DownlinkMessage)(nil), // 35: ttn.lorawan.v3.MACState.DownlinkMessage + (*MACState_DataRateRange)(nil), // 36: ttn.lorawan.v3.MACState.DataRateRange + (*MACState_DataRateRanges)(nil), // 37: ttn.lorawan.v3.MACState.DataRateRanges + nil, // 38: ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry + (*MACState_UplinkMessage_TxSettings)(nil), // 39: ttn.lorawan.v3.MACState.UplinkMessage.TxSettings + (*MACState_UplinkMessage_RxMetadata)(nil), // 40: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata + (*MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata)(nil), // 41: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.PacketBrokerMetadata + (*MACState_DownlinkMessage_Message)(nil), // 42: ttn.lorawan.v3.MACState.DownlinkMessage.Message + (*MACState_DownlinkMessage_Message_MHDR)(nil), // 43: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR + (*MACState_DownlinkMessage_Message_MACPayload)(nil), // 44: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MACPayload + nil, // 45: ttn.lorawan.v3.EndDevice.AttributesEntry + nil, // 46: ttn.lorawan.v3.EndDevice.LocationsEntry + (*BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate)(nil), // 47: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate + nil, // 48: ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry + (*SessionKeys)(nil), // 49: ttn.lorawan.v3.SessionKeys + (*timestamppb.Timestamp)(nil), // 50: google.protobuf.Timestamp + (*ApplicationDownlink)(nil), // 51: ttn.lorawan.v3.ApplicationDownlink + (DataRateIndex)(0), // 52: ttn.lorawan.v3.DataRateIndex + (RxDelay)(0), // 53: ttn.lorawan.v3.RxDelay + (DataRateOffset)(0), // 54: ttn.lorawan.v3.DataRateOffset + (AggregatedDutyCycle)(0), // 55: ttn.lorawan.v3.AggregatedDutyCycle + (RejoinTimeExponent)(0), // 56: ttn.lorawan.v3.RejoinTimeExponent + (RejoinCountExponent)(0), // 57: ttn.lorawan.v3.RejoinCountExponent + (*ADRAckLimitExponentValue)(nil), // 58: ttn.lorawan.v3.ADRAckLimitExponentValue + (*ADRAckDelayExponentValue)(nil), // 59: ttn.lorawan.v3.ADRAckDelayExponentValue + (*DataRateIndexValue)(nil), // 60: ttn.lorawan.v3.DataRateIndexValue + (*EndDeviceVersionIdentifiers)(nil), // 61: ttn.lorawan.v3.EndDeviceVersionIdentifiers + (MACVersion)(0), // 62: ttn.lorawan.v3.MACVersion + (PHYVersion)(0), // 63: ttn.lorawan.v3.PHYVersion + (*MessagePayloadFormatters)(nil), // 64: ttn.lorawan.v3.MessagePayloadFormatters + (*durationpb.Duration)(nil), // 65: google.protobuf.Duration + (*PingSlotPeriodValue)(nil), // 66: ttn.lorawan.v3.PingSlotPeriodValue + (*ZeroableFrequencyValue)(nil), // 67: ttn.lorawan.v3.ZeroableFrequencyValue + (*RxDelayValue)(nil), // 68: ttn.lorawan.v3.RxDelayValue + (*DataRateOffsetValue)(nil), // 69: ttn.lorawan.v3.DataRateOffsetValue + (*FrequencyValue)(nil), // 70: ttn.lorawan.v3.FrequencyValue + (*AggregatedDutyCycleValue)(nil), // 71: ttn.lorawan.v3.AggregatedDutyCycleValue + (*wrapperspb.FloatValue)(nil), // 72: google.protobuf.FloatValue + (*wrapperspb.UInt32Value)(nil), // 73: google.protobuf.UInt32Value + (*DeviceEIRPValue)(nil), // 74: ttn.lorawan.v3.DeviceEIRPValue + (Class)(0), // 75: ttn.lorawan.v3.Class + (*MACCommand)(nil), // 76: ttn.lorawan.v3.MACCommand + (MACCommandIdentifier)(0), // 77: ttn.lorawan.v3.MACCommandIdentifier + (*EndDeviceIdentifiers)(nil), // 78: ttn.lorawan.v3.EndDeviceIdentifiers + (*Picture)(nil), // 79: ttn.lorawan.v3.Picture + (*RootKeys)(nil), // 80: ttn.lorawan.v3.RootKeys + (*structpb.Struct)(nil), // 81: google.protobuf.Struct + (*wrapperspb.BoolValue)(nil), // 82: google.protobuf.BoolValue + (*LoRaAllianceProfileIdentifiers)(nil), // 83: ttn.lorawan.v3.LoRaAllianceProfileIdentifiers + (*fieldmaskpb.FieldMask)(nil), // 84: google.protobuf.FieldMask + (*ApplicationIdentifiers)(nil), // 85: ttn.lorawan.v3.ApplicationIdentifiers + (*DLSettings)(nil), // 86: ttn.lorawan.v3.DLSettings + (*CFList)(nil), // 87: ttn.lorawan.v3.CFList + (*Message)(nil), // 88: ttn.lorawan.v3.Message + (*DataRate)(nil), // 89: ttn.lorawan.v3.DataRate + (*GatewayIdentifiers)(nil), // 90: ttn.lorawan.v3.GatewayIdentifiers + (DownlinkPathConstraint)(0), // 91: ttn.lorawan.v3.DownlinkPathConstraint + (MType)(0), // 92: ttn.lorawan.v3.MType + (*Location)(nil), // 93: ttn.lorawan.v3.Location } var file_lorawan_stack_api_end_device_proto_depIdxs = []int32{ - 48, // 0: ttn.lorawan.v3.Session.keys:type_name -> ttn.lorawan.v3.SessionKeys - 49, // 1: ttn.lorawan.v3.Session.started_at:type_name -> google.protobuf.Timestamp - 50, // 2: ttn.lorawan.v3.Session.queued_application_downlinks:type_name -> ttn.lorawan.v3.ApplicationDownlink - 51, // 3: ttn.lorawan.v3.MACParameters.adr_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 52, // 4: ttn.lorawan.v3.MACParameters.rx1_delay:type_name -> ttn.lorawan.v3.RxDelay - 53, // 5: ttn.lorawan.v3.MACParameters.rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffset - 51, // 6: ttn.lorawan.v3.MACParameters.rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 54, // 7: ttn.lorawan.v3.MACParameters.max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycle - 55, // 8: ttn.lorawan.v3.MACParameters.rejoin_time_periodicity:type_name -> ttn.lorawan.v3.RejoinTimeExponent - 56, // 9: ttn.lorawan.v3.MACParameters.rejoin_count_periodicity:type_name -> ttn.lorawan.v3.RejoinCountExponent - 51, // 10: ttn.lorawan.v3.MACParameters.ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 24, // 11: ttn.lorawan.v3.MACParameters.channels:type_name -> ttn.lorawan.v3.MACParameters.Channel + 49, // 0: ttn.lorawan.v3.Session.keys:type_name -> ttn.lorawan.v3.SessionKeys + 50, // 1: ttn.lorawan.v3.Session.started_at:type_name -> google.protobuf.Timestamp + 51, // 2: ttn.lorawan.v3.Session.queued_application_downlinks:type_name -> ttn.lorawan.v3.ApplicationDownlink + 52, // 3: ttn.lorawan.v3.MACParameters.adr_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 53, // 4: ttn.lorawan.v3.MACParameters.rx1_delay:type_name -> ttn.lorawan.v3.RxDelay + 54, // 5: ttn.lorawan.v3.MACParameters.rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffset + 52, // 6: ttn.lorawan.v3.MACParameters.rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 55, // 7: ttn.lorawan.v3.MACParameters.max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycle + 56, // 8: ttn.lorawan.v3.MACParameters.rejoin_time_periodicity:type_name -> ttn.lorawan.v3.RejoinTimeExponent + 57, // 9: ttn.lorawan.v3.MACParameters.rejoin_count_periodicity:type_name -> ttn.lorawan.v3.RejoinCountExponent + 52, // 10: ttn.lorawan.v3.MACParameters.ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 25, // 11: ttn.lorawan.v3.MACParameters.channels:type_name -> ttn.lorawan.v3.MACParameters.Channel 2, // 12: ttn.lorawan.v3.MACParameters.uplink_dwell_time:type_name -> ttn.lorawan.v3.BoolValue 2, // 13: ttn.lorawan.v3.MACParameters.downlink_dwell_time:type_name -> ttn.lorawan.v3.BoolValue - 57, // 14: ttn.lorawan.v3.MACParameters.adr_ack_limit_exponent:type_name -> ttn.lorawan.v3.ADRAckLimitExponentValue - 58, // 15: ttn.lorawan.v3.MACParameters.adr_ack_delay_exponent:type_name -> ttn.lorawan.v3.ADRAckDelayExponentValue - 59, // 16: ttn.lorawan.v3.MACParameters.ping_slot_data_rate_index_value:type_name -> ttn.lorawan.v3.DataRateIndexValue - 60, // 17: ttn.lorawan.v3.EndDeviceVersion.ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers - 61, // 18: ttn.lorawan.v3.EndDeviceVersion.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion - 62, // 19: ttn.lorawan.v3.EndDeviceVersion.lorawan_phy_version:type_name -> ttn.lorawan.v3.PHYVersion + 58, // 14: ttn.lorawan.v3.MACParameters.adr_ack_limit_exponent:type_name -> ttn.lorawan.v3.ADRAckLimitExponentValue + 59, // 15: ttn.lorawan.v3.MACParameters.adr_ack_delay_exponent:type_name -> ttn.lorawan.v3.ADRAckDelayExponentValue + 60, // 16: ttn.lorawan.v3.MACParameters.ping_slot_data_rate_index_value:type_name -> ttn.lorawan.v3.DataRateIndexValue + 61, // 17: ttn.lorawan.v3.EndDeviceVersion.ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers + 62, // 18: ttn.lorawan.v3.EndDeviceVersion.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion + 63, // 19: ttn.lorawan.v3.EndDeviceVersion.lorawan_phy_version:type_name -> ttn.lorawan.v3.PHYVersion 6, // 20: ttn.lorawan.v3.EndDeviceVersion.default_mac_settings:type_name -> ttn.lorawan.v3.MACSettings - 63, // 21: ttn.lorawan.v3.EndDeviceVersion.default_formatters:type_name -> ttn.lorawan.v3.MessagePayloadFormatters - 25, // 22: ttn.lorawan.v3.ADRSettings.static:type_name -> ttn.lorawan.v3.ADRSettings.StaticMode - 26, // 23: ttn.lorawan.v3.ADRSettings.dynamic:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode - 27, // 24: ttn.lorawan.v3.ADRSettings.disabled:type_name -> ttn.lorawan.v3.ADRSettings.DisabledMode - 64, // 25: ttn.lorawan.v3.MACSettings.class_b_timeout:type_name -> google.protobuf.Duration - 65, // 26: ttn.lorawan.v3.MACSettings.ping_slot_periodicity:type_name -> ttn.lorawan.v3.PingSlotPeriodValue - 59, // 27: ttn.lorawan.v3.MACSettings.ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 66, // 28: ttn.lorawan.v3.MACSettings.ping_slot_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue - 66, // 29: ttn.lorawan.v3.MACSettings.beacon_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue - 64, // 30: ttn.lorawan.v3.MACSettings.class_c_timeout:type_name -> google.protobuf.Duration - 67, // 31: ttn.lorawan.v3.MACSettings.rx1_delay:type_name -> ttn.lorawan.v3.RxDelayValue - 68, // 32: ttn.lorawan.v3.MACSettings.rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffsetValue - 59, // 33: ttn.lorawan.v3.MACSettings.rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 69, // 34: ttn.lorawan.v3.MACSettings.rx2_frequency:type_name -> ttn.lorawan.v3.FrequencyValue - 70, // 35: ttn.lorawan.v3.MACSettings.max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycleValue + 64, // 21: ttn.lorawan.v3.EndDeviceVersion.default_formatters:type_name -> ttn.lorawan.v3.MessagePayloadFormatters + 26, // 22: ttn.lorawan.v3.ADRSettings.static:type_name -> ttn.lorawan.v3.ADRSettings.StaticMode + 27, // 23: ttn.lorawan.v3.ADRSettings.dynamic:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode + 28, // 24: ttn.lorawan.v3.ADRSettings.disabled:type_name -> ttn.lorawan.v3.ADRSettings.DisabledMode + 65, // 25: ttn.lorawan.v3.MACSettings.class_b_timeout:type_name -> google.protobuf.Duration + 66, // 26: ttn.lorawan.v3.MACSettings.ping_slot_periodicity:type_name -> ttn.lorawan.v3.PingSlotPeriodValue + 60, // 27: ttn.lorawan.v3.MACSettings.ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 67, // 28: ttn.lorawan.v3.MACSettings.ping_slot_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue + 67, // 29: ttn.lorawan.v3.MACSettings.beacon_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue + 65, // 30: ttn.lorawan.v3.MACSettings.class_c_timeout:type_name -> google.protobuf.Duration + 68, // 31: ttn.lorawan.v3.MACSettings.rx1_delay:type_name -> ttn.lorawan.v3.RxDelayValue + 69, // 32: ttn.lorawan.v3.MACSettings.rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffsetValue + 60, // 33: ttn.lorawan.v3.MACSettings.rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 70, // 34: ttn.lorawan.v3.MACSettings.rx2_frequency:type_name -> ttn.lorawan.v3.FrequencyValue + 71, // 35: ttn.lorawan.v3.MACSettings.max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycleValue 2, // 36: ttn.lorawan.v3.MACSettings.supports_32_bit_f_cnt:type_name -> ttn.lorawan.v3.BoolValue 2, // 37: ttn.lorawan.v3.MACSettings.use_adr:type_name -> ttn.lorawan.v3.BoolValue - 71, // 38: ttn.lorawan.v3.MACSettings.adr_margin:type_name -> google.protobuf.FloatValue + 72, // 38: ttn.lorawan.v3.MACSettings.adr_margin:type_name -> google.protobuf.FloatValue 2, // 39: ttn.lorawan.v3.MACSettings.resets_f_cnt:type_name -> ttn.lorawan.v3.BoolValue - 64, // 40: ttn.lorawan.v3.MACSettings.status_time_periodicity:type_name -> google.protobuf.Duration - 72, // 41: ttn.lorawan.v3.MACSettings.status_count_periodicity:type_name -> google.protobuf.UInt32Value - 67, // 42: ttn.lorawan.v3.MACSettings.desired_rx1_delay:type_name -> ttn.lorawan.v3.RxDelayValue - 68, // 43: ttn.lorawan.v3.MACSettings.desired_rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffsetValue - 59, // 44: ttn.lorawan.v3.MACSettings.desired_rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 69, // 45: ttn.lorawan.v3.MACSettings.desired_rx2_frequency:type_name -> ttn.lorawan.v3.FrequencyValue - 70, // 46: ttn.lorawan.v3.MACSettings.desired_max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycleValue - 57, // 47: ttn.lorawan.v3.MACSettings.desired_adr_ack_limit_exponent:type_name -> ttn.lorawan.v3.ADRAckLimitExponentValue - 58, // 48: ttn.lorawan.v3.MACSettings.desired_adr_ack_delay_exponent:type_name -> ttn.lorawan.v3.ADRAckDelayExponentValue - 59, // 49: ttn.lorawan.v3.MACSettings.desired_ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 66, // 50: ttn.lorawan.v3.MACSettings.desired_ping_slot_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue - 66, // 51: ttn.lorawan.v3.MACSettings.desired_beacon_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue - 73, // 52: ttn.lorawan.v3.MACSettings.desired_max_eirp:type_name -> ttn.lorawan.v3.DeviceEIRPValue - 64, // 53: ttn.lorawan.v3.MACSettings.class_b_c_downlink_interval:type_name -> google.protobuf.Duration + 65, // 40: ttn.lorawan.v3.MACSettings.status_time_periodicity:type_name -> google.protobuf.Duration + 73, // 41: ttn.lorawan.v3.MACSettings.status_count_periodicity:type_name -> google.protobuf.UInt32Value + 68, // 42: ttn.lorawan.v3.MACSettings.desired_rx1_delay:type_name -> ttn.lorawan.v3.RxDelayValue + 69, // 43: ttn.lorawan.v3.MACSettings.desired_rx1_data_rate_offset:type_name -> ttn.lorawan.v3.DataRateOffsetValue + 60, // 44: ttn.lorawan.v3.MACSettings.desired_rx2_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 70, // 45: ttn.lorawan.v3.MACSettings.desired_rx2_frequency:type_name -> ttn.lorawan.v3.FrequencyValue + 71, // 46: ttn.lorawan.v3.MACSettings.desired_max_duty_cycle:type_name -> ttn.lorawan.v3.AggregatedDutyCycleValue + 58, // 47: ttn.lorawan.v3.MACSettings.desired_adr_ack_limit_exponent:type_name -> ttn.lorawan.v3.ADRAckLimitExponentValue + 59, // 48: ttn.lorawan.v3.MACSettings.desired_adr_ack_delay_exponent:type_name -> ttn.lorawan.v3.ADRAckDelayExponentValue + 60, // 49: ttn.lorawan.v3.MACSettings.desired_ping_slot_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 67, // 50: ttn.lorawan.v3.MACSettings.desired_ping_slot_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue + 67, // 51: ttn.lorawan.v3.MACSettings.desired_beacon_frequency:type_name -> ttn.lorawan.v3.ZeroableFrequencyValue + 74, // 52: ttn.lorawan.v3.MACSettings.desired_max_eirp:type_name -> ttn.lorawan.v3.DeviceEIRPValue + 65, // 53: ttn.lorawan.v3.MACSettings.class_b_c_downlink_interval:type_name -> google.protobuf.Duration 2, // 54: ttn.lorawan.v3.MACSettings.uplink_dwell_time:type_name -> ttn.lorawan.v3.BoolValue 2, // 55: ttn.lorawan.v3.MACSettings.downlink_dwell_time:type_name -> ttn.lorawan.v3.BoolValue 5, // 56: ttn.lorawan.v3.MACSettings.adr:type_name -> ttn.lorawan.v3.ADRSettings 2, // 57: ttn.lorawan.v3.MACSettings.schedule_downlinks:type_name -> ttn.lorawan.v3.BoolValue 3, // 58: ttn.lorawan.v3.MACState.current_parameters:type_name -> ttn.lorawan.v3.MACParameters 3, // 59: ttn.lorawan.v3.MACState.desired_parameters:type_name -> ttn.lorawan.v3.MACParameters - 74, // 60: ttn.lorawan.v3.MACState.device_class:type_name -> ttn.lorawan.v3.Class - 61, // 61: ttn.lorawan.v3.MACState.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion - 49, // 62: ttn.lorawan.v3.MACState.last_confirmed_downlink_at:type_name -> google.protobuf.Timestamp - 65, // 63: ttn.lorawan.v3.MACState.ping_slot_periodicity:type_name -> ttn.lorawan.v3.PingSlotPeriodValue - 50, // 64: ttn.lorawan.v3.MACState.pending_application_downlink:type_name -> ttn.lorawan.v3.ApplicationDownlink - 75, // 65: ttn.lorawan.v3.MACState.queued_responses:type_name -> ttn.lorawan.v3.MACCommand - 75, // 66: ttn.lorawan.v3.MACState.pending_requests:type_name -> ttn.lorawan.v3.MACCommand - 32, // 67: ttn.lorawan.v3.MACState.queued_join_accept:type_name -> ttn.lorawan.v3.MACState.JoinAccept - 31, // 68: ttn.lorawan.v3.MACState.pending_join_request:type_name -> ttn.lorawan.v3.MACState.JoinRequest - 33, // 69: ttn.lorawan.v3.MACState.recent_uplinks:type_name -> ttn.lorawan.v3.MACState.UplinkMessage - 34, // 70: ttn.lorawan.v3.MACState.recent_downlinks:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage - 49, // 71: ttn.lorawan.v3.MACState.last_network_initiated_downlink_at:type_name -> google.protobuf.Timestamp - 51, // 72: ttn.lorawan.v3.MACState.rejected_adr_data_rate_indexes:type_name -> ttn.lorawan.v3.DataRateIndex - 49, // 73: ttn.lorawan.v3.MACState.last_downlink_at:type_name -> google.protobuf.Timestamp - 37, // 74: ttn.lorawan.v3.MACState.rejected_data_rate_ranges:type_name -> ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry - 76, // 75: ttn.lorawan.v3.MACState.recent_mac_command_identifiers:type_name -> ttn.lorawan.v3.MACCommandIdentifier - 49, // 76: ttn.lorawan.v3.EndDeviceAuthenticationCode.valid_from:type_name -> google.protobuf.Timestamp - 49, // 77: ttn.lorawan.v3.EndDeviceAuthenticationCode.valid_to:type_name -> google.protobuf.Timestamp - 77, // 78: ttn.lorawan.v3.EndDevice.ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers - 49, // 79: ttn.lorawan.v3.EndDevice.created_at:type_name -> google.protobuf.Timestamp - 49, // 80: ttn.lorawan.v3.EndDevice.updated_at:type_name -> google.protobuf.Timestamp - 44, // 81: ttn.lorawan.v3.EndDevice.attributes:type_name -> ttn.lorawan.v3.EndDevice.AttributesEntry - 60, // 82: ttn.lorawan.v3.EndDevice.version_ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers - 45, // 83: ttn.lorawan.v3.EndDevice.locations:type_name -> ttn.lorawan.v3.EndDevice.LocationsEntry - 78, // 84: ttn.lorawan.v3.EndDevice.picture:type_name -> ttn.lorawan.v3.Picture - 61, // 85: ttn.lorawan.v3.EndDevice.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion - 62, // 86: ttn.lorawan.v3.EndDevice.lorawan_phy_version:type_name -> ttn.lorawan.v3.PHYVersion - 79, // 87: ttn.lorawan.v3.EndDevice.root_keys:type_name -> ttn.lorawan.v3.RootKeys + 75, // 60: ttn.lorawan.v3.MACState.device_class:type_name -> ttn.lorawan.v3.Class + 62, // 61: ttn.lorawan.v3.MACState.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion + 50, // 62: ttn.lorawan.v3.MACState.last_confirmed_downlink_at:type_name -> google.protobuf.Timestamp + 66, // 63: ttn.lorawan.v3.MACState.ping_slot_periodicity:type_name -> ttn.lorawan.v3.PingSlotPeriodValue + 51, // 64: ttn.lorawan.v3.MACState.pending_application_downlink:type_name -> ttn.lorawan.v3.ApplicationDownlink + 76, // 65: ttn.lorawan.v3.MACState.queued_responses:type_name -> ttn.lorawan.v3.MACCommand + 76, // 66: ttn.lorawan.v3.MACState.pending_requests:type_name -> ttn.lorawan.v3.MACCommand + 33, // 67: ttn.lorawan.v3.MACState.queued_join_accept:type_name -> ttn.lorawan.v3.MACState.JoinAccept + 32, // 68: ttn.lorawan.v3.MACState.pending_join_request:type_name -> ttn.lorawan.v3.MACState.JoinRequest + 34, // 69: ttn.lorawan.v3.MACState.recent_uplinks:type_name -> ttn.lorawan.v3.MACState.UplinkMessage + 35, // 70: ttn.lorawan.v3.MACState.recent_downlinks:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage + 50, // 71: ttn.lorawan.v3.MACState.last_network_initiated_downlink_at:type_name -> google.protobuf.Timestamp + 52, // 72: ttn.lorawan.v3.MACState.rejected_adr_data_rate_indexes:type_name -> ttn.lorawan.v3.DataRateIndex + 50, // 73: ttn.lorawan.v3.MACState.last_downlink_at:type_name -> google.protobuf.Timestamp + 38, // 74: ttn.lorawan.v3.MACState.rejected_data_rate_ranges:type_name -> ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry + 77, // 75: ttn.lorawan.v3.MACState.recent_mac_command_identifiers:type_name -> ttn.lorawan.v3.MACCommandIdentifier + 50, // 76: ttn.lorawan.v3.EndDeviceAuthenticationCode.valid_from:type_name -> google.protobuf.Timestamp + 50, // 77: ttn.lorawan.v3.EndDeviceAuthenticationCode.valid_to:type_name -> google.protobuf.Timestamp + 78, // 78: ttn.lorawan.v3.EndDevice.ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 50, // 79: ttn.lorawan.v3.EndDevice.created_at:type_name -> google.protobuf.Timestamp + 50, // 80: ttn.lorawan.v3.EndDevice.updated_at:type_name -> google.protobuf.Timestamp + 45, // 81: ttn.lorawan.v3.EndDevice.attributes:type_name -> ttn.lorawan.v3.EndDevice.AttributesEntry + 61, // 82: ttn.lorawan.v3.EndDevice.version_ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers + 46, // 83: ttn.lorawan.v3.EndDevice.locations:type_name -> ttn.lorawan.v3.EndDevice.LocationsEntry + 79, // 84: ttn.lorawan.v3.EndDevice.picture:type_name -> ttn.lorawan.v3.Picture + 62, // 85: ttn.lorawan.v3.EndDevice.lorawan_version:type_name -> ttn.lorawan.v3.MACVersion + 63, // 86: ttn.lorawan.v3.EndDevice.lorawan_phy_version:type_name -> ttn.lorawan.v3.PHYVersion + 80, // 87: ttn.lorawan.v3.EndDevice.root_keys:type_name -> ttn.lorawan.v3.RootKeys 6, // 88: ttn.lorawan.v3.EndDevice.mac_settings:type_name -> ttn.lorawan.v3.MACSettings 7, // 89: ttn.lorawan.v3.EndDevice.mac_state:type_name -> ttn.lorawan.v3.MACState 7, // 90: ttn.lorawan.v3.EndDevice.pending_mac_state:type_name -> ttn.lorawan.v3.MACState 1, // 91: ttn.lorawan.v3.EndDevice.session:type_name -> ttn.lorawan.v3.Session 1, // 92: ttn.lorawan.v3.EndDevice.pending_session:type_name -> ttn.lorawan.v3.Session - 49, // 93: ttn.lorawan.v3.EndDevice.last_dev_status_received_at:type_name -> google.protobuf.Timestamp + 50, // 93: ttn.lorawan.v3.EndDevice.last_dev_status_received_at:type_name -> google.protobuf.Timestamp 0, // 94: ttn.lorawan.v3.EndDevice.power_state:type_name -> ttn.lorawan.v3.PowerState - 71, // 95: ttn.lorawan.v3.EndDevice.battery_percentage:type_name -> google.protobuf.FloatValue - 50, // 96: ttn.lorawan.v3.EndDevice.queued_application_downlinks:type_name -> ttn.lorawan.v3.ApplicationDownlink - 63, // 97: ttn.lorawan.v3.EndDevice.formatters:type_name -> ttn.lorawan.v3.MessagePayloadFormatters - 80, // 98: ttn.lorawan.v3.EndDevice.provisioning_data:type_name -> google.protobuf.Struct + 72, // 95: ttn.lorawan.v3.EndDevice.battery_percentage:type_name -> google.protobuf.FloatValue + 51, // 96: ttn.lorawan.v3.EndDevice.queued_application_downlinks:type_name -> ttn.lorawan.v3.ApplicationDownlink + 64, // 97: ttn.lorawan.v3.EndDevice.formatters:type_name -> ttn.lorawan.v3.MessagePayloadFormatters + 81, // 98: ttn.lorawan.v3.EndDevice.provisioning_data:type_name -> google.protobuf.Struct 8, // 99: ttn.lorawan.v3.EndDevice.claim_authentication_code:type_name -> ttn.lorawan.v3.EndDeviceAuthenticationCode - 81, // 100: ttn.lorawan.v3.EndDevice.skip_payload_crypto_override:type_name -> google.protobuf.BoolValue - 49, // 101: ttn.lorawan.v3.EndDevice.activated_at:type_name -> google.protobuf.Timestamp - 49, // 102: ttn.lorawan.v3.EndDevice.last_seen_at:type_name -> google.protobuf.Timestamp - 82, // 103: ttn.lorawan.v3.EndDevice.lora_alliance_profile_ids:type_name -> ttn.lorawan.v3.LoRaAllianceProfileIdentifiers + 82, // 100: ttn.lorawan.v3.EndDevice.skip_payload_crypto_override:type_name -> google.protobuf.BoolValue + 50, // 101: ttn.lorawan.v3.EndDevice.activated_at:type_name -> google.protobuf.Timestamp + 50, // 102: ttn.lorawan.v3.EndDevice.last_seen_at:type_name -> google.protobuf.Timestamp + 83, // 103: ttn.lorawan.v3.EndDevice.lora_alliance_profile_ids:type_name -> ttn.lorawan.v3.LoRaAllianceProfileIdentifiers 9, // 104: ttn.lorawan.v3.EndDevices.end_devices:type_name -> ttn.lorawan.v3.EndDevice 9, // 105: ttn.lorawan.v3.CreateEndDeviceRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice 9, // 106: ttn.lorawan.v3.UpdateEndDeviceRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice - 83, // 107: ttn.lorawan.v3.UpdateEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask - 46, // 108: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.updates:type_name -> ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate - 77, // 109: ttn.lorawan.v3.GetEndDeviceRequest.end_device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers - 83, // 110: ttn.lorawan.v3.GetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask - 84, // 111: ttn.lorawan.v3.ListEndDevicesRequest.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers - 83, // 112: ttn.lorawan.v3.ListEndDevicesRequest.field_mask:type_name -> google.protobuf.FieldMask + 84, // 107: ttn.lorawan.v3.UpdateEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask + 47, // 108: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.updates:type_name -> ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate + 78, // 109: ttn.lorawan.v3.GetEndDeviceRequest.end_device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 84, // 110: ttn.lorawan.v3.GetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask + 85, // 111: ttn.lorawan.v3.ListEndDevicesRequest.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers + 84, // 112: ttn.lorawan.v3.ListEndDevicesRequest.field_mask:type_name -> google.protobuf.FieldMask 9, // 113: ttn.lorawan.v3.SetEndDeviceRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice - 83, // 114: ttn.lorawan.v3.SetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask - 77, // 115: ttn.lorawan.v3.ResetAndGetEndDeviceRequest.end_device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers - 83, // 116: ttn.lorawan.v3.ResetAndGetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask + 84, // 114: ttn.lorawan.v3.SetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask + 78, // 115: ttn.lorawan.v3.ResetAndGetEndDeviceRequest.end_device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 84, // 116: ttn.lorawan.v3.ResetAndGetEndDeviceRequest.field_mask:type_name -> google.protobuf.FieldMask 9, // 117: ttn.lorawan.v3.EndDeviceTemplate.end_device:type_name -> ttn.lorawan.v3.EndDevice - 83, // 118: ttn.lorawan.v3.EndDeviceTemplate.field_mask:type_name -> google.protobuf.FieldMask - 47, // 119: ttn.lorawan.v3.EndDeviceTemplateFormats.formats:type_name -> ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry - 60, // 120: ttn.lorawan.v3.ConvertEndDeviceTemplateRequest.end_device_version_ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers - 51, // 121: ttn.lorawan.v3.MACParameters.Channel.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 51, // 122: ttn.lorawan.v3.MACParameters.Channel.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 51, // 123: ttn.lorawan.v3.ADRSettings.StaticMode.data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 71, // 124: ttn.lorawan.v3.ADRSettings.DynamicMode.margin:type_name -> google.protobuf.FloatValue - 59, // 125: ttn.lorawan.v3.ADRSettings.DynamicMode.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 59, // 126: ttn.lorawan.v3.ADRSettings.DynamicMode.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue - 72, // 127: ttn.lorawan.v3.ADRSettings.DynamicMode.min_tx_power_index:type_name -> google.protobuf.UInt32Value - 72, // 128: ttn.lorawan.v3.ADRSettings.DynamicMode.max_tx_power_index:type_name -> google.protobuf.UInt32Value - 72, // 129: ttn.lorawan.v3.ADRSettings.DynamicMode.min_nb_trans:type_name -> google.protobuf.UInt32Value - 72, // 130: ttn.lorawan.v3.ADRSettings.DynamicMode.max_nb_trans:type_name -> google.protobuf.UInt32Value - 28, // 131: ttn.lorawan.v3.ADRSettings.DynamicMode.channel_steering:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings - 29, // 132: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.lora_narrow:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode - 30, // 133: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.disabled:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode - 85, // 134: ttn.lorawan.v3.MACState.JoinRequest.downlink_settings:type_name -> ttn.lorawan.v3.DLSettings - 52, // 135: ttn.lorawan.v3.MACState.JoinRequest.rx_delay:type_name -> ttn.lorawan.v3.RxDelay - 86, // 136: ttn.lorawan.v3.MACState.JoinRequest.cf_list:type_name -> ttn.lorawan.v3.CFList - 31, // 137: ttn.lorawan.v3.MACState.JoinAccept.request:type_name -> ttn.lorawan.v3.MACState.JoinRequest - 48, // 138: ttn.lorawan.v3.MACState.JoinAccept.keys:type_name -> ttn.lorawan.v3.SessionKeys - 87, // 139: ttn.lorawan.v3.MACState.UplinkMessage.payload:type_name -> ttn.lorawan.v3.Message - 38, // 140: ttn.lorawan.v3.MACState.UplinkMessage.settings:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.TxSettings - 39, // 141: ttn.lorawan.v3.MACState.UplinkMessage.rx_metadata:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata - 49, // 142: ttn.lorawan.v3.MACState.UplinkMessage.received_at:type_name -> google.protobuf.Timestamp - 41, // 143: ttn.lorawan.v3.MACState.DownlinkMessage.payload:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message - 51, // 144: ttn.lorawan.v3.MACState.DataRateRange.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 51, // 145: ttn.lorawan.v3.MACState.DataRateRange.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex - 35, // 146: ttn.lorawan.v3.MACState.DataRateRanges.ranges:type_name -> ttn.lorawan.v3.MACState.DataRateRange - 36, // 147: ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry.value:type_name -> ttn.lorawan.v3.MACState.DataRateRanges - 88, // 148: ttn.lorawan.v3.MACState.UplinkMessage.TxSettings.data_rate:type_name -> ttn.lorawan.v3.DataRate - 89, // 149: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers - 90, // 150: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.downlink_path_constraint:type_name -> ttn.lorawan.v3.DownlinkPathConstraint - 40, // 151: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.packet_broker:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.PacketBrokerMetadata - 42, // 152: ttn.lorawan.v3.MACState.DownlinkMessage.Message.m_hdr:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR - 43, // 153: ttn.lorawan.v3.MACState.DownlinkMessage.Message.mac_payload:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message.MACPayload - 91, // 154: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR.m_type:type_name -> ttn.lorawan.v3.MType - 92, // 155: ttn.lorawan.v3.EndDevice.LocationsEntry.value:type_name -> ttn.lorawan.v3.Location - 77, // 156: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate.ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers - 49, // 157: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate.last_seen_at:type_name -> google.protobuf.Timestamp - 21, // 158: ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.EndDeviceTemplateFormat - 159, // [159:159] is the sub-list for method output_type - 159, // [159:159] is the sub-list for method input_type - 159, // [159:159] is the sub-list for extension type_name - 159, // [159:159] is the sub-list for extension extendee - 0, // [0:159] is the sub-list for field type_name + 84, // 118: ttn.lorawan.v3.EndDeviceTemplate.field_mask:type_name -> google.protobuf.FieldMask + 48, // 119: ttn.lorawan.v3.EndDeviceTemplateFormats.formats:type_name -> ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry + 61, // 120: ttn.lorawan.v3.ConvertEndDeviceTemplateRequest.end_device_version_ids:type_name -> ttn.lorawan.v3.EndDeviceVersionIdentifiers + 85, // 121: ttn.lorawan.v3.BatchDeleteEndDevicesRequest.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers + 52, // 122: ttn.lorawan.v3.MACParameters.Channel.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 52, // 123: ttn.lorawan.v3.MACParameters.Channel.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 52, // 124: ttn.lorawan.v3.ADRSettings.StaticMode.data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 72, // 125: ttn.lorawan.v3.ADRSettings.DynamicMode.margin:type_name -> google.protobuf.FloatValue + 60, // 126: ttn.lorawan.v3.ADRSettings.DynamicMode.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 60, // 127: ttn.lorawan.v3.ADRSettings.DynamicMode.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndexValue + 73, // 128: ttn.lorawan.v3.ADRSettings.DynamicMode.min_tx_power_index:type_name -> google.protobuf.UInt32Value + 73, // 129: ttn.lorawan.v3.ADRSettings.DynamicMode.max_tx_power_index:type_name -> google.protobuf.UInt32Value + 73, // 130: ttn.lorawan.v3.ADRSettings.DynamicMode.min_nb_trans:type_name -> google.protobuf.UInt32Value + 73, // 131: ttn.lorawan.v3.ADRSettings.DynamicMode.max_nb_trans:type_name -> google.protobuf.UInt32Value + 29, // 132: ttn.lorawan.v3.ADRSettings.DynamicMode.channel_steering:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings + 30, // 133: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.lora_narrow:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.LoRaNarrowMode + 31, // 134: ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.disabled:type_name -> ttn.lorawan.v3.ADRSettings.DynamicMode.ChannelSteeringSettings.DisabledMode + 86, // 135: ttn.lorawan.v3.MACState.JoinRequest.downlink_settings:type_name -> ttn.lorawan.v3.DLSettings + 53, // 136: ttn.lorawan.v3.MACState.JoinRequest.rx_delay:type_name -> ttn.lorawan.v3.RxDelay + 87, // 137: ttn.lorawan.v3.MACState.JoinRequest.cf_list:type_name -> ttn.lorawan.v3.CFList + 32, // 138: ttn.lorawan.v3.MACState.JoinAccept.request:type_name -> ttn.lorawan.v3.MACState.JoinRequest + 49, // 139: ttn.lorawan.v3.MACState.JoinAccept.keys:type_name -> ttn.lorawan.v3.SessionKeys + 88, // 140: ttn.lorawan.v3.MACState.UplinkMessage.payload:type_name -> ttn.lorawan.v3.Message + 39, // 141: ttn.lorawan.v3.MACState.UplinkMessage.settings:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.TxSettings + 40, // 142: ttn.lorawan.v3.MACState.UplinkMessage.rx_metadata:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata + 50, // 143: ttn.lorawan.v3.MACState.UplinkMessage.received_at:type_name -> google.protobuf.Timestamp + 42, // 144: ttn.lorawan.v3.MACState.DownlinkMessage.payload:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message + 52, // 145: ttn.lorawan.v3.MACState.DataRateRange.min_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 52, // 146: ttn.lorawan.v3.MACState.DataRateRange.max_data_rate_index:type_name -> ttn.lorawan.v3.DataRateIndex + 36, // 147: ttn.lorawan.v3.MACState.DataRateRanges.ranges:type_name -> ttn.lorawan.v3.MACState.DataRateRange + 37, // 148: ttn.lorawan.v3.MACState.RejectedDataRateRangesEntry.value:type_name -> ttn.lorawan.v3.MACState.DataRateRanges + 89, // 149: ttn.lorawan.v3.MACState.UplinkMessage.TxSettings.data_rate:type_name -> ttn.lorawan.v3.DataRate + 90, // 150: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers + 91, // 151: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.downlink_path_constraint:type_name -> ttn.lorawan.v3.DownlinkPathConstraint + 41, // 152: ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.packet_broker:type_name -> ttn.lorawan.v3.MACState.UplinkMessage.RxMetadata.PacketBrokerMetadata + 43, // 153: ttn.lorawan.v3.MACState.DownlinkMessage.Message.m_hdr:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR + 44, // 154: ttn.lorawan.v3.MACState.DownlinkMessage.Message.mac_payload:type_name -> ttn.lorawan.v3.MACState.DownlinkMessage.Message.MACPayload + 92, // 155: ttn.lorawan.v3.MACState.DownlinkMessage.Message.MHDR.m_type:type_name -> ttn.lorawan.v3.MType + 93, // 156: ttn.lorawan.v3.EndDevice.LocationsEntry.value:type_name -> ttn.lorawan.v3.Location + 78, // 157: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate.ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 50, // 158: ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate.last_seen_at:type_name -> google.protobuf.Timestamp + 21, // 159: ttn.lorawan.v3.EndDeviceTemplateFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.EndDeviceTemplateFormat + 160, // [160:160] is the sub-list for method output_type + 160, // [160:160] is the sub-list for method input_type + 160, // [160:160] is the sub-list for extension type_name + 160, // [160:160] is the sub-list for extension extendee + 0, // [0:160] is the sub-list for field type_name } func init() { file_lorawan_stack_api_end_device_proto_init() } @@ -6016,7 +6085,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACParameters_Channel); i { + switch v := v.(*BatchDeleteEndDevicesRequest); i { case 0: return &v.state case 1: @@ -6028,7 +6097,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_StaticMode); i { + switch v := v.(*MACParameters_Channel); i { case 0: return &v.state case 1: @@ -6040,7 +6109,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_DynamicMode); i { + switch v := v.(*ADRSettings_StaticMode); i { case 0: return &v.state case 1: @@ -6052,7 +6121,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_DisabledMode); i { + switch v := v.(*ADRSettings_DynamicMode); i { case 0: return &v.state case 1: @@ -6064,7 +6133,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings); i { + switch v := v.(*ADRSettings_DisabledMode); i { case 0: return &v.state case 1: @@ -6076,7 +6145,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode); i { + switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings); i { case 0: return &v.state case 1: @@ -6088,7 +6157,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode); i { + switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings_LoRaNarrowMode); i { case 0: return &v.state case 1: @@ -6100,7 +6169,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACState_JoinRequest); i { + switch v := v.(*ADRSettings_DynamicMode_ChannelSteeringSettings_DisabledMode); i { case 0: return &v.state case 1: @@ -6112,7 +6181,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACState_JoinAccept); i { + switch v := v.(*MACState_JoinRequest); i { case 0: return &v.state case 1: @@ -6124,7 +6193,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACState_UplinkMessage); i { + switch v := v.(*MACState_JoinAccept); i { case 0: return &v.state case 1: @@ -6136,7 +6205,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACState_DownlinkMessage); i { + switch v := v.(*MACState_UplinkMessage); i { case 0: return &v.state case 1: @@ -6148,7 +6217,7 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MACState_DataRateRange); i { + switch v := v.(*MACState_DownlinkMessage); i { case 0: return &v.state case 1: @@ -6160,6 +6229,18 @@ func file_lorawan_stack_api_end_device_proto_init() { } } file_lorawan_stack_api_end_device_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MACState_DataRateRange); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lorawan_stack_api_end_device_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_DataRateRanges); i { case 0: return &v.state @@ -6171,7 +6252,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_UplinkMessage_TxSettings); i { case 0: return &v.state @@ -6183,7 +6264,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_UplinkMessage_RxMetadata); i { case 0: return &v.state @@ -6195,7 +6276,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_UplinkMessage_RxMetadata_PacketBrokerMetadata); i { case 0: return &v.state @@ -6207,7 +6288,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_DownlinkMessage_Message); i { case 0: return &v.state @@ -6219,7 +6300,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_DownlinkMessage_Message_MHDR); i { case 0: return &v.state @@ -6231,7 +6312,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MACState_DownlinkMessage_Message_MACPayload); i { case 0: return &v.state @@ -6243,7 +6324,7 @@ func file_lorawan_stack_api_end_device_proto_init() { return nil } } - file_lorawan_stack_api_end_device_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { + file_lorawan_stack_api_end_device_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateEndDeviceLastSeenRequest_EndDeviceLastSeenUpdate); i { case 0: return &v.state @@ -6261,7 +6342,7 @@ func file_lorawan_stack_api_end_device_proto_init() { (*ADRSettings_Dynamic)(nil), (*ADRSettings_Disabled)(nil), } - file_lorawan_stack_api_end_device_proto_msgTypes[27].OneofWrappers = []interface{}{ + file_lorawan_stack_api_end_device_proto_msgTypes[28].OneofWrappers = []interface{}{ (*ADRSettings_DynamicMode_ChannelSteeringSettings_LoraNarrow)(nil), (*ADRSettings_DynamicMode_ChannelSteeringSettings_Disabled)(nil), } @@ -6271,7 +6352,7 @@ func file_lorawan_stack_api_end_device_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lorawan_stack_api_end_device_proto_rawDesc, NumEnums: 1, - NumMessages: 47, + NumMessages: 48, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/ttnpb/end_device.pb.paths.fm.go b/pkg/ttnpb/end_device.pb.paths.fm.go index 94704c341d..b73f7c05de 100644 --- a/pkg/ttnpb/end_device.pb.paths.fm.go +++ b/pkg/ttnpb/end_device.pb.paths.fm.go @@ -3165,6 +3165,16 @@ var ConvertEndDeviceTemplateRequestFieldPathsTopLevel = []string{ "end_device_version_ids", "format_id", } +var BatchDeleteEndDevicesRequestFieldPathsNested = []string{ + "application_ids", + "application_ids.application_id", + "device_ids", +} + +var BatchDeleteEndDevicesRequestFieldPathsTopLevel = []string{ + "application_ids", + "device_ids", +} var MACParameters_ChannelFieldPathsNested = []string{ "downlink_frequency", "enable_uplink", diff --git a/pkg/ttnpb/end_device.pb.setters.fm.go b/pkg/ttnpb/end_device.pb.setters.fm.go index c8bc88b9a7..5f23a0c94e 100644 --- a/pkg/ttnpb/end_device.pb.setters.fm.go +++ b/pkg/ttnpb/end_device.pb.setters.fm.go @@ -3118,6 +3118,51 @@ func (dst *ConvertEndDeviceTemplateRequest) SetFields(src *ConvertEndDeviceTempl return nil } +func (dst *BatchDeleteEndDevicesRequest) SetFields(src *BatchDeleteEndDevicesRequest, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "application_ids": + if len(subs) > 0 { + var newDst, newSrc *ApplicationIdentifiers + if (src == nil || src.ApplicationIds == nil) && dst.ApplicationIds == nil { + continue + } + if src != nil { + newSrc = src.ApplicationIds + } + if dst.ApplicationIds != nil { + newDst = dst.ApplicationIds + } else { + newDst = &ApplicationIdentifiers{} + dst.ApplicationIds = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.ApplicationIds = src.ApplicationIds + } else { + dst.ApplicationIds = nil + } + } + case "device_ids": + if len(subs) > 0 { + return fmt.Errorf("'device_ids' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.DeviceIds = src.DeviceIds + } else { + dst.DeviceIds = nil + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + func (dst *MACParameters_Channel) SetFields(src *MACParameters_Channel, paths ...string) error { for name, subs := range _processPaths(paths) { switch name { diff --git a/pkg/ttnpb/end_device.pb.validate.go b/pkg/ttnpb/end_device.pb.validate.go index 40b3ea456b..79541bc09f 100644 --- a/pkg/ttnpb/end_device.pb.validate.go +++ b/pkg/ttnpb/end_device.pb.validate.go @@ -4016,6 +4016,137 @@ var _ interface { var _ConvertEndDeviceTemplateRequest_FormatId_Pattern = regexp.MustCompile("^[a-z0-9](?:[-]?[a-z0-9]){2,}$") +// ValidateFields checks the field values on BatchDeleteEndDevicesRequest with +// the rules defined in the proto definition for this message. If any rules +// are violated, an error is returned. +func (m *BatchDeleteEndDevicesRequest) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = BatchDeleteEndDevicesRequestFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "application_ids": + + if m.GetApplicationIds() == nil { + return BatchDeleteEndDevicesRequestValidationError{ + field: "application_ids", + reason: "value is required", + } + } + + if v, ok := interface{}(m.GetApplicationIds()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return BatchDeleteEndDevicesRequestValidationError{ + field: "application_ids", + reason: "embedded message failed validation", + cause: err, + } + } + } + + case "device_ids": + + if l := len(m.GetDeviceIds()); l < 1 || l > 20 { + return BatchDeleteEndDevicesRequestValidationError{ + field: "device_ids", + reason: "value must contain between 1 and 20 items, inclusive", + } + } + + for idx, item := range m.GetDeviceIds() { + _, _ = idx, item + + if utf8.RuneCountInString(item) > 36 { + return BatchDeleteEndDevicesRequestValidationError{ + field: fmt.Sprintf("device_ids[%v]", idx), + reason: "value length must be at most 36 runes", + } + } + + if !_BatchDeleteEndDevicesRequest_DeviceIds_Pattern.MatchString(item) { + return BatchDeleteEndDevicesRequestValidationError{ + field: fmt.Sprintf("device_ids[%v]", idx), + reason: "value does not match regex pattern \"^[a-z0-9](?:[-]?[a-z0-9]){2,}$\"", + } + } + + } + + default: + return BatchDeleteEndDevicesRequestValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// BatchDeleteEndDevicesRequestValidationError is the validation error returned +// by BatchDeleteEndDevicesRequest.ValidateFields if the designated +// constraints aren't met. +type BatchDeleteEndDevicesRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e BatchDeleteEndDevicesRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e BatchDeleteEndDevicesRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e BatchDeleteEndDevicesRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e BatchDeleteEndDevicesRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e BatchDeleteEndDevicesRequestValidationError) ErrorName() string { + return "BatchDeleteEndDevicesRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e BatchDeleteEndDevicesRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sBatchDeleteEndDevicesRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = BatchDeleteEndDevicesRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = BatchDeleteEndDevicesRequestValidationError{} + +var _BatchDeleteEndDevicesRequest_DeviceIds_Pattern = regexp.MustCompile("^[a-z0-9](?:[-]?[a-z0-9]){2,}$") + // ValidateFields checks the field values on MACParameters_Channel with the // rules defined in the proto definition for this message. If any rules are // violated, an error is returned. diff --git a/pkg/ttnpb/end_device_services.pb.go b/pkg/ttnpb/end_device_services.pb.go index 4909136773..649f5f1a53 100644 --- a/pkg/ttnpb/end_device_services.pb.go +++ b/pkg/ttnpb/end_device_services.pb.go @@ -132,10 +132,21 @@ var file_lorawan_stack_api_end_device_services_proto_rawDesc = []byte{ 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x65, 0x64, 0x74, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x30, - 0x01, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, - 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, - 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x01, 0x32, 0xaf, 0x01, 0x0a, 0x16, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, 0x94, 0x01, 0x0a, + 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x44, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x3e, 0x2a, 0x3c, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x62, 0x61, + 0x74, 0x63, 0x68, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, + 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var file_lorawan_stack_api_end_device_services_proto_goTypes = []interface{}{ @@ -148,10 +159,11 @@ var file_lorawan_stack_api_end_device_services_proto_goTypes = []interface{}{ (*EndDeviceIdentifiers)(nil), // 6: ttn.lorawan.v3.EndDeviceIdentifiers (*emptypb.Empty)(nil), // 7: google.protobuf.Empty (*ConvertEndDeviceTemplateRequest)(nil), // 8: ttn.lorawan.v3.ConvertEndDeviceTemplateRequest - (*EndDevice)(nil), // 9: ttn.lorawan.v3.EndDevice - (*EndDevices)(nil), // 10: ttn.lorawan.v3.EndDevices - (*EndDeviceTemplateFormats)(nil), // 11: ttn.lorawan.v3.EndDeviceTemplateFormats - (*EndDeviceTemplate)(nil), // 12: ttn.lorawan.v3.EndDeviceTemplate + (*BatchDeleteEndDevicesRequest)(nil), // 9: ttn.lorawan.v3.BatchDeleteEndDevicesRequest + (*EndDevice)(nil), // 10: ttn.lorawan.v3.EndDevice + (*EndDevices)(nil), // 11: ttn.lorawan.v3.EndDevices + (*EndDeviceTemplateFormats)(nil), // 12: ttn.lorawan.v3.EndDeviceTemplateFormats + (*EndDeviceTemplate)(nil), // 13: ttn.lorawan.v3.EndDeviceTemplate } var file_lorawan_stack_api_end_device_services_proto_depIdxs = []int32{ 0, // 0: ttn.lorawan.v3.EndDeviceRegistry.Create:input_type -> ttn.lorawan.v3.CreateEndDeviceRequest @@ -163,17 +175,19 @@ var file_lorawan_stack_api_end_device_services_proto_depIdxs = []int32{ 6, // 6: ttn.lorawan.v3.EndDeviceRegistry.Delete:input_type -> ttn.lorawan.v3.EndDeviceIdentifiers 7, // 7: ttn.lorawan.v3.EndDeviceTemplateConverter.ListFormats:input_type -> google.protobuf.Empty 8, // 8: ttn.lorawan.v3.EndDeviceTemplateConverter.Convert:input_type -> ttn.lorawan.v3.ConvertEndDeviceTemplateRequest - 9, // 9: ttn.lorawan.v3.EndDeviceRegistry.Create:output_type -> ttn.lorawan.v3.EndDevice - 9, // 10: ttn.lorawan.v3.EndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice - 6, // 11: ttn.lorawan.v3.EndDeviceRegistry.GetIdentifiersForEUIs:output_type -> ttn.lorawan.v3.EndDeviceIdentifiers - 10, // 12: ttn.lorawan.v3.EndDeviceRegistry.List:output_type -> ttn.lorawan.v3.EndDevices - 9, // 13: ttn.lorawan.v3.EndDeviceRegistry.Update:output_type -> ttn.lorawan.v3.EndDevice - 7, // 14: ttn.lorawan.v3.EndDeviceRegistry.BatchUpdateLastSeen:output_type -> google.protobuf.Empty - 7, // 15: ttn.lorawan.v3.EndDeviceRegistry.Delete:output_type -> google.protobuf.Empty - 11, // 16: ttn.lorawan.v3.EndDeviceTemplateConverter.ListFormats:output_type -> ttn.lorawan.v3.EndDeviceTemplateFormats - 12, // 17: ttn.lorawan.v3.EndDeviceTemplateConverter.Convert:output_type -> ttn.lorawan.v3.EndDeviceTemplate - 9, // [9:18] is the sub-list for method output_type - 0, // [0:9] is the sub-list for method input_type + 9, // 9: ttn.lorawan.v3.EndDeviceBatchRegistry.Delete:input_type -> ttn.lorawan.v3.BatchDeleteEndDevicesRequest + 10, // 10: ttn.lorawan.v3.EndDeviceRegistry.Create:output_type -> ttn.lorawan.v3.EndDevice + 10, // 11: ttn.lorawan.v3.EndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice + 6, // 12: ttn.lorawan.v3.EndDeviceRegistry.GetIdentifiersForEUIs:output_type -> ttn.lorawan.v3.EndDeviceIdentifiers + 11, // 13: ttn.lorawan.v3.EndDeviceRegistry.List:output_type -> ttn.lorawan.v3.EndDevices + 10, // 14: ttn.lorawan.v3.EndDeviceRegistry.Update:output_type -> ttn.lorawan.v3.EndDevice + 7, // 15: ttn.lorawan.v3.EndDeviceRegistry.BatchUpdateLastSeen:output_type -> google.protobuf.Empty + 7, // 16: ttn.lorawan.v3.EndDeviceRegistry.Delete:output_type -> google.protobuf.Empty + 12, // 17: ttn.lorawan.v3.EndDeviceTemplateConverter.ListFormats:output_type -> ttn.lorawan.v3.EndDeviceTemplateFormats + 13, // 18: ttn.lorawan.v3.EndDeviceTemplateConverter.Convert:output_type -> ttn.lorawan.v3.EndDeviceTemplate + 7, // 19: ttn.lorawan.v3.EndDeviceBatchRegistry.Delete:output_type -> google.protobuf.Empty + 10, // [10:20] is the sub-list for method output_type + 0, // [0:10] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -194,7 +208,7 @@ func file_lorawan_stack_api_end_device_services_proto_init() { NumEnums: 0, NumMessages: 0, NumExtensions: 0, - NumServices: 2, + NumServices: 3, }, GoTypes: file_lorawan_stack_api_end_device_services_proto_goTypes, DependencyIndexes: file_lorawan_stack_api_end_device_services_proto_depIdxs, diff --git a/pkg/ttnpb/end_device_services.pb.gw.go b/pkg/ttnpb/end_device_services.pb.gw.go index e226cb38a1..ca6df9b37f 100644 --- a/pkg/ttnpb/end_device_services.pb.gw.go +++ b/pkg/ttnpb/end_device_services.pb.gw.go @@ -481,6 +481,76 @@ func request_EndDeviceTemplateConverter_Convert_0(ctx context.Context, marshaler } +var ( + filter_EndDeviceBatchRegistry_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{"application_ids": 0, "application_id": 1, "applicationId": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 1, 3, 4}} +) + +func request_EndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client EndDeviceBatchRegistryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_EndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server EndDeviceBatchRegistryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Delete(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterEndDeviceRegistryHandlerServer registers the http handlers for service EndDeviceRegistry to "mux". // UnaryRPC :call EndDeviceRegistryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -656,6 +726,40 @@ func RegisterEndDeviceTemplateConverterHandlerServer(ctx context.Context, mux *r return nil } +// RegisterEndDeviceBatchRegistryHandlerServer registers the http handlers for service EndDeviceBatchRegistry to "mux". +// UnaryRPC :call EndDeviceBatchRegistryServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterEndDeviceBatchRegistryHandlerFromEndpoint instead. +func RegisterEndDeviceBatchRegistryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server EndDeviceBatchRegistryServer) error { + + mux.Handle("DELETE", pattern_EndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.EndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_EndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_EndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterEndDeviceRegistryHandlerFromEndpoint is same as RegisterEndDeviceRegistryHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterEndDeviceRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -927,3 +1031,74 @@ var ( forward_EndDeviceTemplateConverter_Convert_0 = runtime.ForwardResponseStream ) + +// RegisterEndDeviceBatchRegistryHandlerFromEndpoint is same as RegisterEndDeviceBatchRegistryHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterEndDeviceBatchRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterEndDeviceBatchRegistryHandler(ctx, mux, conn) +} + +// RegisterEndDeviceBatchRegistryHandler registers the http handlers for service EndDeviceBatchRegistry to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterEndDeviceBatchRegistryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterEndDeviceBatchRegistryHandlerClient(ctx, mux, NewEndDeviceBatchRegistryClient(conn)) +} + +// RegisterEndDeviceBatchRegistryHandlerClient registers the http handlers for service EndDeviceBatchRegistry +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "EndDeviceBatchRegistryClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "EndDeviceBatchRegistryClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "EndDeviceBatchRegistryClient" to call the correct interceptors. +func RegisterEndDeviceBatchRegistryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client EndDeviceBatchRegistryClient) error { + + mux.Handle("DELETE", pattern_EndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.EndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_EndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_EndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_EndDeviceBatchRegistry_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3}, []string{"applications", "application_ids.application_id", "devices", "batch"}, "")) +) + +var ( + forward_EndDeviceBatchRegistry_Delete_0 = runtime.ForwardResponseMessage +) diff --git a/pkg/ttnpb/end_device_services_grpc.pb.go b/pkg/ttnpb/end_device_services_grpc.pb.go index 9556b76d23..5835318559 100644 --- a/pkg/ttnpb/end_device_services_grpc.pb.go +++ b/pkg/ttnpb/end_device_services_grpc.pb.go @@ -548,3 +548,114 @@ var EndDeviceTemplateConverter_ServiceDesc = grpc.ServiceDesc{ }, Metadata: "lorawan-stack/api/end_device_services.proto", } + +const ( + EndDeviceBatchRegistry_Delete_FullMethodName = "/ttn.lorawan.v3.EndDeviceBatchRegistry/Delete" +) + +// EndDeviceBatchRegistryClient is the client API for EndDeviceBatchRegistry service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type EndDeviceBatchRegistryClient interface { + // Delete a batch of end devices with the given IDs. + // + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + // Before calling this RPC, use the corresponding BatchDelete RPCs + // of NsEndDeviceRegistry, AsEndDeviceRegistry and + // optionally the JsEndDeviceRegistry to delete the end devices. + // If the devices were claimed on a Join Server, use the BatchUnclaim RPC + // of the DeviceClaimingServer. + // This is NOT done automatically. + Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type endDeviceBatchRegistryClient struct { + cc grpc.ClientConnInterface +} + +func NewEndDeviceBatchRegistryClient(cc grpc.ClientConnInterface) EndDeviceBatchRegistryClient { + return &endDeviceBatchRegistryClient{cc} +} + +func (c *endDeviceBatchRegistryClient) Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, EndDeviceBatchRegistry_Delete_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EndDeviceBatchRegistryServer is the server API for EndDeviceBatchRegistry service. +// All implementations must embed UnimplementedEndDeviceBatchRegistryServer +// for forward compatibility +type EndDeviceBatchRegistryServer interface { + // Delete a batch of end devices with the given IDs. + // + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + // Before calling this RPC, use the corresponding BatchDelete RPCs + // of NsEndDeviceRegistry, AsEndDeviceRegistry and + // optionally the JsEndDeviceRegistry to delete the end devices. + // If the devices were claimed on a Join Server, use the BatchUnclaim RPC + // of the DeviceClaimingServer. + // This is NOT done automatically. + Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedEndDeviceBatchRegistryServer() +} + +// UnimplementedEndDeviceBatchRegistryServer must be embedded to have forward compatible implementations. +type UnimplementedEndDeviceBatchRegistryServer struct { +} + +func (UnimplementedEndDeviceBatchRegistryServer) Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedEndDeviceBatchRegistryServer) mustEmbedUnimplementedEndDeviceBatchRegistryServer() { +} + +// UnsafeEndDeviceBatchRegistryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EndDeviceBatchRegistryServer will +// result in compilation errors. +type UnsafeEndDeviceBatchRegistryServer interface { + mustEmbedUnimplementedEndDeviceBatchRegistryServer() +} + +func RegisterEndDeviceBatchRegistryServer(s grpc.ServiceRegistrar, srv EndDeviceBatchRegistryServer) { + s.RegisterService(&EndDeviceBatchRegistry_ServiceDesc, srv) +} + +func _EndDeviceBatchRegistry_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchDeleteEndDevicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EndDeviceBatchRegistryServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EndDeviceBatchRegistry_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EndDeviceBatchRegistryServer).Delete(ctx, req.(*BatchDeleteEndDevicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// EndDeviceBatchRegistry_ServiceDesc is the grpc.ServiceDesc for EndDeviceBatchRegistry service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EndDeviceBatchRegistry_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.EndDeviceBatchRegistry", + HandlerType: (*EndDeviceBatchRegistryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Delete", + Handler: _EndDeviceBatchRegistry_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lorawan-stack/api/end_device_services.proto", +} diff --git a/pkg/ttnpb/identifiers.pb.go b/pkg/ttnpb/identifiers.pb.go index 15d7481fc4..f41237900d 100644 --- a/pkg/ttnpb/identifiers.pb.go +++ b/pkg/ttnpb/identifiers.pb.go @@ -815,6 +815,53 @@ func (x *LoRaAllianceProfileIdentifiers) GetVendorProfileId() uint32 { return 0 } +type EndDeviceIdentifiersList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + EndDeviceIds []*EndDeviceIdentifiers `protobuf:"bytes,1,rep,name=end_device_ids,json=endDeviceIds,proto3" json:"end_device_ids,omitempty"` +} + +func (x *EndDeviceIdentifiersList) Reset() { + *x = EndDeviceIdentifiersList{} + if protoimpl.UnsafeEnabled { + mi := &file_lorawan_stack_api_identifiers_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EndDeviceIdentifiersList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EndDeviceIdentifiersList) ProtoMessage() {} + +func (x *EndDeviceIdentifiersList) ProtoReflect() protoreflect.Message { + mi := &file_lorawan_stack_api_identifiers_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EndDeviceIdentifiersList.ProtoReflect.Descriptor instead. +func (*EndDeviceIdentifiersList) Descriptor() ([]byte, []int) { + return file_lorawan_stack_api_identifiers_proto_rawDescGZIP(), []int{11} +} + +func (x *EndDeviceIdentifiersList) GetEndDeviceIds() []*EndDeviceIdentifiers { + if x != nil { + return x.EndDeviceIds + } + return nil +} + var File_lorawan_stack_api_identifiers_proto protoreflect.FileDescriptor var file_lorawan_stack_api_identifiers_proto_rawDesc = []byte{ @@ -1086,10 +1133,17 @@ var file_lorawan_stack_api_identifiers_proto_rawDesc = []byte{ 0x6f, 0x72, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x49, 0x64, - 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, - 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, - 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x66, 0x0a, 0x18, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0e, + 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0c, 0x65, 0x6e, 0x64, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, + 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, + 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1104,7 +1158,7 @@ func file_lorawan_stack_api_identifiers_proto_rawDescGZIP() []byte { return file_lorawan_stack_api_identifiers_proto_rawDescData } -var file_lorawan_stack_api_identifiers_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_lorawan_stack_api_identifiers_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_lorawan_stack_api_identifiers_proto_goTypes = []interface{}{ (*ApplicationIdentifiers)(nil), // 0: ttn.lorawan.v3.ApplicationIdentifiers (*ClientIdentifiers)(nil), // 1: ttn.lorawan.v3.ClientIdentifiers @@ -1117,22 +1171,24 @@ var file_lorawan_stack_api_identifiers_proto_goTypes = []interface{}{ (*EndDeviceVersionIdentifiers)(nil), // 8: ttn.lorawan.v3.EndDeviceVersionIdentifiers (*NetworkIdentifiers)(nil), // 9: ttn.lorawan.v3.NetworkIdentifiers (*LoRaAllianceProfileIdentifiers)(nil), // 10: ttn.lorawan.v3.LoRaAllianceProfileIdentifiers + (*EndDeviceIdentifiersList)(nil), // 11: ttn.lorawan.v3.EndDeviceIdentifiersList } var file_lorawan_stack_api_identifiers_proto_depIdxs = []int32{ - 0, // 0: ttn.lorawan.v3.EndDeviceIdentifiers.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers - 4, // 1: ttn.lorawan.v3.OrganizationOrUserIdentifiers.organization_ids:type_name -> ttn.lorawan.v3.OrganizationIdentifiers - 5, // 2: ttn.lorawan.v3.OrganizationOrUserIdentifiers.user_ids:type_name -> ttn.lorawan.v3.UserIdentifiers - 0, // 3: ttn.lorawan.v3.EntityIdentifiers.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers - 1, // 4: ttn.lorawan.v3.EntityIdentifiers.client_ids:type_name -> ttn.lorawan.v3.ClientIdentifiers - 2, // 5: ttn.lorawan.v3.EntityIdentifiers.device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers - 3, // 6: ttn.lorawan.v3.EntityIdentifiers.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers - 4, // 7: ttn.lorawan.v3.EntityIdentifiers.organization_ids:type_name -> ttn.lorawan.v3.OrganizationIdentifiers - 5, // 8: ttn.lorawan.v3.EntityIdentifiers.user_ids:type_name -> ttn.lorawan.v3.UserIdentifiers - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 0, // 0: ttn.lorawan.v3.EndDeviceIdentifiers.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers + 4, // 1: ttn.lorawan.v3.OrganizationOrUserIdentifiers.organization_ids:type_name -> ttn.lorawan.v3.OrganizationIdentifiers + 5, // 2: ttn.lorawan.v3.OrganizationOrUserIdentifiers.user_ids:type_name -> ttn.lorawan.v3.UserIdentifiers + 0, // 3: ttn.lorawan.v3.EntityIdentifiers.application_ids:type_name -> ttn.lorawan.v3.ApplicationIdentifiers + 1, // 4: ttn.lorawan.v3.EntityIdentifiers.client_ids:type_name -> ttn.lorawan.v3.ClientIdentifiers + 2, // 5: ttn.lorawan.v3.EntityIdentifiers.device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 3, // 6: ttn.lorawan.v3.EntityIdentifiers.gateway_ids:type_name -> ttn.lorawan.v3.GatewayIdentifiers + 4, // 7: ttn.lorawan.v3.EntityIdentifiers.organization_ids:type_name -> ttn.lorawan.v3.OrganizationIdentifiers + 5, // 8: ttn.lorawan.v3.EntityIdentifiers.user_ids:type_name -> ttn.lorawan.v3.UserIdentifiers + 2, // 9: ttn.lorawan.v3.EndDeviceIdentifiersList.end_device_ids:type_name -> ttn.lorawan.v3.EndDeviceIdentifiers + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_lorawan_stack_api_identifiers_proto_init() } @@ -1273,6 +1329,18 @@ func file_lorawan_stack_api_identifiers_proto_init() { return nil } } + file_lorawan_stack_api_identifiers_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EndDeviceIdentifiersList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_lorawan_stack_api_identifiers_proto_msgTypes[6].OneofWrappers = []interface{}{ (*OrganizationOrUserIdentifiers_OrganizationIds)(nil), @@ -1292,7 +1360,7 @@ func file_lorawan_stack_api_identifiers_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lorawan_stack_api_identifiers_proto_rawDesc, NumEnums: 0, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/ttnpb/identifiers.pb.paths.fm.go b/pkg/ttnpb/identifiers.pb.paths.fm.go index c3161d75a0..196d80b998 100644 --- a/pkg/ttnpb/identifiers.pb.paths.fm.go +++ b/pkg/ttnpb/identifiers.pb.paths.fm.go @@ -134,3 +134,10 @@ var LoRaAllianceProfileIdentifiersFieldPathsTopLevel = []string{ "vendor_id", "vendor_profile_id", } +var EndDeviceIdentifiersListFieldPathsNested = []string{ + "end_device_ids", +} + +var EndDeviceIdentifiersListFieldPathsTopLevel = []string{ + "end_device_ids", +} diff --git a/pkg/ttnpb/identifiers.pb.setters.fm.go b/pkg/ttnpb/identifiers.pb.setters.fm.go index b24f101d48..1037f5f479 100644 --- a/pkg/ttnpb/identifiers.pb.setters.fm.go +++ b/pkg/ttnpb/identifiers.pb.setters.fm.go @@ -704,3 +704,23 @@ func (dst *LoRaAllianceProfileIdentifiers) SetFields(src *LoRaAllianceProfileIde } return nil } + +func (dst *EndDeviceIdentifiersList) SetFields(src *EndDeviceIdentifiersList, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "end_device_ids": + if len(subs) > 0 { + return fmt.Errorf("'end_device_ids' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.EndDeviceIds = src.EndDeviceIds + } else { + dst.EndDeviceIds = nil + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} diff --git a/pkg/ttnpb/identifiers.pb.validate.go b/pkg/ttnpb/identifiers.pb.validate.go index df23f3ecf6..012b9be3ce 100644 --- a/pkg/ttnpb/identifiers.pb.validate.go +++ b/pkg/ttnpb/identifiers.pb.validate.go @@ -1399,3 +1399,102 @@ var _ interface { Cause() error ErrorName() string } = LoRaAllianceProfileIdentifiersValidationError{} + +// ValidateFields checks the field values on EndDeviceIdentifiersList with the +// rules defined in the proto definition for this message. If any rules are +// violated, an error is returned. +func (m *EndDeviceIdentifiersList) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = EndDeviceIdentifiersListFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "end_device_ids": + + for idx, item := range m.GetEndDeviceIds() { + _, _ = idx, item + + if v, ok := interface{}(item).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return EndDeviceIdentifiersListValidationError{ + field: fmt.Sprintf("end_device_ids[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + default: + return EndDeviceIdentifiersListValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// EndDeviceIdentifiersListValidationError is the validation error returned by +// EndDeviceIdentifiersList.ValidateFields if the designated constraints +// aren't met. +type EndDeviceIdentifiersListValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e EndDeviceIdentifiersListValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e EndDeviceIdentifiersListValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e EndDeviceIdentifiersListValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e EndDeviceIdentifiersListValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e EndDeviceIdentifiersListValidationError) ErrorName() string { + return "EndDeviceIdentifiersListValidationError" +} + +// Error satisfies the builtin error interface +func (e EndDeviceIdentifiersListValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sEndDeviceIdentifiersList.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = EndDeviceIdentifiersListValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = EndDeviceIdentifiersListValidationError{} diff --git a/pkg/ttnpb/identifiers_json.pb.go b/pkg/ttnpb/identifiers_json.pb.go index 73026cab22..f08ccee4ea 100644 --- a/pkg/ttnpb/identifiers_json.pb.go +++ b/pkg/ttnpb/identifiers_json.pb.go @@ -349,3 +349,66 @@ func (x *NetworkIdentifiers) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { func (x *NetworkIdentifiers) UnmarshalJSON(b []byte) error { return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) } + +// MarshalProtoJSON marshals the EndDeviceIdentifiersList message to JSON. +func (x *EndDeviceIdentifiersList) MarshalProtoJSON(s *jsonplugin.MarshalState) { + if x == nil { + s.WriteNil() + return + } + s.WriteObjectStart() + var wroteField bool + if len(x.EndDeviceIds) > 0 || s.HasField("end_device_ids") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("end_device_ids") + s.WriteArrayStart() + var wroteElement bool + for _, element := range x.EndDeviceIds { + s.WriteMoreIf(&wroteElement) + element.MarshalProtoJSON(s.WithField("end_device_ids")) + } + s.WriteArrayEnd() + } + s.WriteObjectEnd() +} + +// MarshalJSON marshals the EndDeviceIdentifiersList to JSON. +func (x *EndDeviceIdentifiersList) MarshalJSON() ([]byte, error) { + return jsonplugin.DefaultMarshalerConfig.Marshal(x) +} + +// UnmarshalProtoJSON unmarshals the EndDeviceIdentifiersList message from JSON. +func (x *EndDeviceIdentifiersList) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { + if s.ReadNil() { + return + } + s.ReadObject(func(key string) { + switch key { + default: + s.ReadAny() // ignore unknown field + case "end_device_ids", "endDeviceIds": + s.AddField("end_device_ids") + if s.ReadNil() { + x.EndDeviceIds = nil + return + } + s.ReadArray(func() { + if s.ReadNil() { + x.EndDeviceIds = append(x.EndDeviceIds, nil) + return + } + v := &EndDeviceIdentifiers{} + v.UnmarshalProtoJSON(s.WithField("end_device_ids", false)) + if s.Err() != nil { + return + } + x.EndDeviceIds = append(x.EndDeviceIds, v) + }) + } + }) +} + +// UnmarshalJSON unmarshals the EndDeviceIdentifiersList from JSON. +func (x *EndDeviceIdentifiersList) UnmarshalJSON(b []byte) error { + return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) +} diff --git a/pkg/ttnpb/joinserver.pb.go b/pkg/ttnpb/joinserver.pb.go index 3fe01690d2..b9daf44271 100644 --- a/pkg/ttnpb/joinserver.pb.go +++ b/pkg/ttnpb/joinserver.pb.go @@ -1811,60 +1811,72 @@ var file_lorawan_stack_api_joinserver_proto_rawDesc = []byte{ 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x7d, 0x32, 0xb4, 0x04, 0x0a, 0x24, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, 0xb1, 0x01, 0x0a, 0x03, 0x47, - 0x65, 0x74, 0x12, 0x37, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, - 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x74, - 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x70, 0x70, + 0x7d, 0x32, 0xb4, 0x01, 0x0a, 0x18, 0x4a, 0x73, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, 0x97, + 0x01, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x47, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x41, 0x2a, 0x3f, 0x2f, 0x6a, 0x73, 0x2f, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x32, 0xb4, 0x04, 0x0a, 0x24, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x42, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x3c, 0x12, 0x3a, 0x2f, 0x6a, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0xb4, - 0x01, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, - 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, - 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, - 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x45, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3f, 0x3a, 0x01, 0x2a, 0x22, 0x3a, 0x2f, 0x6a, 0x73, 0x2f, 0x61, + 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x12, 0xb1, 0x01, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x37, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, + 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, + 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, + 0x73, 0x22, 0x42, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3c, 0x12, 0x3a, 0x2f, 0x6a, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0xa0, 0x01, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x12, 0x3a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, - 0x33, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0xb4, 0x01, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x12, 0x37, 0x2e, + 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x53, + 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, + 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, - 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x45, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3f, 0x3a, 0x01, 0x2a, + 0x22, 0x3a, 0x2f, 0x6a, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0xa0, 0x01, 0x0a, + 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x3a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, + 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x42, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x3c, 0x2a, 0x3a, 0x2f, 0x6a, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x32, + 0xe8, 0x01, 0x0a, 0x02, 0x4a, 0x73, 0x12, 0x6c, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x69, + 0x6e, 0x45, 0x55, 0x49, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x42, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3c, 0x2a, 0x3a, 0x2f, 0x6a, - 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, - 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, - 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, - 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x32, 0xe8, 0x01, 0x0a, 0x02, 0x4a, 0x73, 0x12, - 0x6c, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x50, 0x72, 0x65, - 0x66, 0x69, 0x78, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, - 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x4a, - 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x22, 0x1d, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x6a, 0x73, 0x2f, 0x6a, 0x6f, 0x69, 0x6e, - 0x5f, 0x65, 0x75, 0x69, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x73, 0x12, 0x74, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4a, 0x6f, 0x69, 0x6e, 0x45, - 0x55, 0x49, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x29, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, - 0x6a, 0x73, 0x2f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x6a, 0x6f, 0x69, 0x6e, 0x5f, - 0x65, 0x75, 0x69, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, - 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, - 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x50, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x65, 0x73, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, + 0x6a, 0x73, 0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x5f, 0x65, 0x75, 0x69, 0x5f, 0x70, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x65, 0x73, 0x12, 0x74, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x4a, 0x6f, 0x69, 0x6e, 0x45, 0x55, 0x49, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, + 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4a, 0x6f, 0x69, + 0x6e, 0x45, 0x55, 0x49, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x6a, 0x73, 0x2f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x5f, 0x6a, 0x6f, 0x69, 0x6e, 0x5f, 0x65, 0x75, 0x69, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, + 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1910,9 +1922,10 @@ var file_lorawan_stack_api_joinserver_proto_goTypes = []interface{}{ (*JoinRequest)(nil), // 26: ttn.lorawan.v3.JoinRequest (*GetEndDeviceRequest)(nil), // 27: ttn.lorawan.v3.GetEndDeviceRequest (*SetEndDeviceRequest)(nil), // 28: ttn.lorawan.v3.SetEndDeviceRequest - (*emptypb.Empty)(nil), // 29: google.protobuf.Empty - (*JoinResponse)(nil), // 30: ttn.lorawan.v3.JoinResponse - (*EndDevice)(nil), // 31: ttn.lorawan.v3.EndDevice + (*BatchDeleteEndDevicesRequest)(nil), // 29: ttn.lorawan.v3.BatchDeleteEndDevicesRequest + (*emptypb.Empty)(nil), // 30: google.protobuf.Empty + (*JoinResponse)(nil), // 31: ttn.lorawan.v3.JoinResponse + (*EndDevice)(nil), // 32: ttn.lorawan.v3.EndDevice } var file_lorawan_stack_api_joinserver_proto_depIdxs = []int32{ 19, // 0: ttn.lorawan.v3.NwkSKeysResponse.f_nwk_s_int_key:type_name -> ttn.lorawan.v3.KeyEnvelope @@ -1958,34 +1971,36 @@ var file_lorawan_stack_api_joinserver_proto_depIdxs = []int32{ 28, // 40: ttn.lorawan.v3.JsEndDeviceRegistry.Set:input_type -> ttn.lorawan.v3.SetEndDeviceRequest 8, // 41: ttn.lorawan.v3.JsEndDeviceRegistry.Provision:input_type -> ttn.lorawan.v3.ProvisionEndDevicesRequest 20, // 42: ttn.lorawan.v3.JsEndDeviceRegistry.Delete:input_type -> ttn.lorawan.v3.EndDeviceIdentifiers - 10, // 43: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Get:input_type -> ttn.lorawan.v3.GetApplicationActivationSettingsRequest - 11, // 44: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Set:input_type -> ttn.lorawan.v3.SetApplicationActivationSettingsRequest - 12, // 45: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Delete:input_type -> ttn.lorawan.v3.DeleteApplicationActivationSettingsRequest - 29, // 46: ttn.lorawan.v3.Js.GetJoinEUIPrefixes:input_type -> google.protobuf.Empty - 29, // 47: ttn.lorawan.v3.Js.GetDefaultJoinEUI:input_type -> google.protobuf.Empty - 30, // 48: ttn.lorawan.v3.NsJs.HandleJoin:output_type -> ttn.lorawan.v3.JoinResponse - 1, // 49: ttn.lorawan.v3.NsJs.GetNwkSKeys:output_type -> ttn.lorawan.v3.NwkSKeysResponse - 2, // 50: ttn.lorawan.v3.AsJs.GetAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse - 2, // 51: ttn.lorawan.v3.AppJs.GetAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse - 4, // 52: ttn.lorawan.v3.NetworkCryptoService.JoinRequestMIC:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse - 4, // 53: ttn.lorawan.v3.NetworkCryptoService.JoinAcceptMIC:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse - 4, // 54: ttn.lorawan.v3.NetworkCryptoService.EncryptJoinAccept:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse - 4, // 55: ttn.lorawan.v3.NetworkCryptoService.EncryptRejoinAccept:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse - 1, // 56: ttn.lorawan.v3.NetworkCryptoService.DeriveNwkSKeys:output_type -> ttn.lorawan.v3.NwkSKeysResponse - 19, // 57: ttn.lorawan.v3.NetworkCryptoService.GetNwkKey:output_type -> ttn.lorawan.v3.KeyEnvelope - 2, // 58: ttn.lorawan.v3.ApplicationCryptoService.DeriveAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse - 19, // 59: ttn.lorawan.v3.ApplicationCryptoService.GetAppKey:output_type -> ttn.lorawan.v3.KeyEnvelope - 31, // 60: ttn.lorawan.v3.JsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice - 31, // 61: ttn.lorawan.v3.JsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice - 31, // 62: ttn.lorawan.v3.JsEndDeviceRegistry.Provision:output_type -> ttn.lorawan.v3.EndDevice - 29, // 63: ttn.lorawan.v3.JsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty - 9, // 64: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Get:output_type -> ttn.lorawan.v3.ApplicationActivationSettings - 9, // 65: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Set:output_type -> ttn.lorawan.v3.ApplicationActivationSettings - 29, // 66: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Delete:output_type -> google.protobuf.Empty - 14, // 67: ttn.lorawan.v3.Js.GetJoinEUIPrefixes:output_type -> ttn.lorawan.v3.JoinEUIPrefixes - 15, // 68: ttn.lorawan.v3.Js.GetDefaultJoinEUI:output_type -> ttn.lorawan.v3.GetDefaultJoinEUIResponse - 48, // [48:69] is the sub-list for method output_type - 27, // [27:48] is the sub-list for method input_type + 29, // 43: ttn.lorawan.v3.JsEndDeviceBatchRegistry.Delete:input_type -> ttn.lorawan.v3.BatchDeleteEndDevicesRequest + 10, // 44: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Get:input_type -> ttn.lorawan.v3.GetApplicationActivationSettingsRequest + 11, // 45: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Set:input_type -> ttn.lorawan.v3.SetApplicationActivationSettingsRequest + 12, // 46: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Delete:input_type -> ttn.lorawan.v3.DeleteApplicationActivationSettingsRequest + 30, // 47: ttn.lorawan.v3.Js.GetJoinEUIPrefixes:input_type -> google.protobuf.Empty + 30, // 48: ttn.lorawan.v3.Js.GetDefaultJoinEUI:input_type -> google.protobuf.Empty + 31, // 49: ttn.lorawan.v3.NsJs.HandleJoin:output_type -> ttn.lorawan.v3.JoinResponse + 1, // 50: ttn.lorawan.v3.NsJs.GetNwkSKeys:output_type -> ttn.lorawan.v3.NwkSKeysResponse + 2, // 51: ttn.lorawan.v3.AsJs.GetAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse + 2, // 52: ttn.lorawan.v3.AppJs.GetAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse + 4, // 53: ttn.lorawan.v3.NetworkCryptoService.JoinRequestMIC:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse + 4, // 54: ttn.lorawan.v3.NetworkCryptoService.JoinAcceptMIC:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse + 4, // 55: ttn.lorawan.v3.NetworkCryptoService.EncryptJoinAccept:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse + 4, // 56: ttn.lorawan.v3.NetworkCryptoService.EncryptRejoinAccept:output_type -> ttn.lorawan.v3.CryptoServicePayloadResponse + 1, // 57: ttn.lorawan.v3.NetworkCryptoService.DeriveNwkSKeys:output_type -> ttn.lorawan.v3.NwkSKeysResponse + 19, // 58: ttn.lorawan.v3.NetworkCryptoService.GetNwkKey:output_type -> ttn.lorawan.v3.KeyEnvelope + 2, // 59: ttn.lorawan.v3.ApplicationCryptoService.DeriveAppSKey:output_type -> ttn.lorawan.v3.AppSKeyResponse + 19, // 60: ttn.lorawan.v3.ApplicationCryptoService.GetAppKey:output_type -> ttn.lorawan.v3.KeyEnvelope + 32, // 61: ttn.lorawan.v3.JsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice + 32, // 62: ttn.lorawan.v3.JsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice + 32, // 63: ttn.lorawan.v3.JsEndDeviceRegistry.Provision:output_type -> ttn.lorawan.v3.EndDevice + 30, // 64: ttn.lorawan.v3.JsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty + 30, // 65: ttn.lorawan.v3.JsEndDeviceBatchRegistry.Delete:output_type -> google.protobuf.Empty + 9, // 66: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Get:output_type -> ttn.lorawan.v3.ApplicationActivationSettings + 9, // 67: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Set:output_type -> ttn.lorawan.v3.ApplicationActivationSettings + 30, // 68: ttn.lorawan.v3.ApplicationActivationSettingRegistry.Delete:output_type -> google.protobuf.Empty + 14, // 69: ttn.lorawan.v3.Js.GetJoinEUIPrefixes:output_type -> ttn.lorawan.v3.JoinEUIPrefixes + 15, // 70: ttn.lorawan.v3.Js.GetDefaultJoinEUI:output_type -> ttn.lorawan.v3.GetDefaultJoinEUIResponse + 49, // [49:71] is the sub-list for method output_type + 27, // [27:49] is the sub-list for method input_type 27, // [27:27] is the sub-list for extension type_name 27, // [27:27] is the sub-list for extension extendee 0, // [0:27] is the sub-list for field type_name @@ -2244,7 +2259,7 @@ func file_lorawan_stack_api_joinserver_proto_init() { NumEnums: 0, NumMessages: 19, NumExtensions: 0, - NumServices: 8, + NumServices: 9, }, GoTypes: file_lorawan_stack_api_joinserver_proto_goTypes, DependencyIndexes: file_lorawan_stack_api_joinserver_proto_depIdxs, diff --git a/pkg/ttnpb/joinserver.pb.gw.go b/pkg/ttnpb/joinserver.pb.gw.go index deab6dfabd..b6a7414982 100644 --- a/pkg/ttnpb/joinserver.pb.gw.go +++ b/pkg/ttnpb/joinserver.pb.gw.go @@ -410,6 +410,76 @@ func local_request_JsEndDeviceRegistry_Delete_0(ctx context.Context, marshaler r } +var ( + filter_JsEndDeviceBatchRegistry_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{"application_ids": 0, "application_id": 1, "applicationId": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 1, 3, 4}} +) + +func request_JsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client JsEndDeviceBatchRegistryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_JsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_JsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server JsEndDeviceBatchRegistryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_JsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Delete(ctx, &protoReq) + return msg, metadata, err + +} + var ( filter_ApplicationActivationSettingRegistry_Get_0 = &utilities.DoubleArray{Encoding: map[string]int{"application_ids": 0, "application_id": 1, "applicationId": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 1, 3, 4}} ) @@ -770,6 +840,40 @@ func RegisterJsEndDeviceRegistryHandlerServer(ctx context.Context, mux *runtime. return nil } +// RegisterJsEndDeviceBatchRegistryHandlerServer registers the http handlers for service JsEndDeviceBatchRegistry to "mux". +// UnaryRPC :call JsEndDeviceBatchRegistryServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterJsEndDeviceBatchRegistryHandlerFromEndpoint instead. +func RegisterJsEndDeviceBatchRegistryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server JsEndDeviceBatchRegistryServer) error { + + mux.Handle("DELETE", pattern_JsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.JsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/js/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_JsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_JsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterApplicationActivationSettingRegistryHandlerServer registers the http handlers for service ApplicationActivationSettingRegistry to "mux". // UnaryRPC :call ApplicationActivationSettingRegistryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1088,6 +1192,77 @@ var ( forward_JsEndDeviceRegistry_Delete_0 = runtime.ForwardResponseMessage ) +// RegisterJsEndDeviceBatchRegistryHandlerFromEndpoint is same as RegisterJsEndDeviceBatchRegistryHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterJsEndDeviceBatchRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterJsEndDeviceBatchRegistryHandler(ctx, mux, conn) +} + +// RegisterJsEndDeviceBatchRegistryHandler registers the http handlers for service JsEndDeviceBatchRegistry to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterJsEndDeviceBatchRegistryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterJsEndDeviceBatchRegistryHandlerClient(ctx, mux, NewJsEndDeviceBatchRegistryClient(conn)) +} + +// RegisterJsEndDeviceBatchRegistryHandlerClient registers the http handlers for service JsEndDeviceBatchRegistry +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "JsEndDeviceBatchRegistryClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "JsEndDeviceBatchRegistryClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "JsEndDeviceBatchRegistryClient" to call the correct interceptors. +func RegisterJsEndDeviceBatchRegistryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client JsEndDeviceBatchRegistryClient) error { + + mux.Handle("DELETE", pattern_JsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.JsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/js/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_JsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_JsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_JsEndDeviceBatchRegistry_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"js", "applications", "application_ids.application_id", "devices", "batch"}, "")) +) + +var ( + forward_JsEndDeviceBatchRegistry_Delete_0 = runtime.ForwardResponseMessage +) + // RegisterApplicationActivationSettingRegistryHandlerFromEndpoint is same as RegisterApplicationActivationSettingRegistryHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterApplicationActivationSettingRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { diff --git a/pkg/ttnpb/joinserver_grpc.pb.go b/pkg/ttnpb/joinserver_grpc.pb.go index ccf18fb4de..397ccc6661 100644 --- a/pkg/ttnpb/joinserver_grpc.pb.go +++ b/pkg/ttnpb/joinserver_grpc.pb.go @@ -1013,6 +1013,103 @@ var JsEndDeviceRegistry_ServiceDesc = grpc.ServiceDesc{ Metadata: "lorawan-stack/api/joinserver.proto", } +const ( + JsEndDeviceBatchRegistry_Delete_FullMethodName = "/ttn.lorawan.v3.JsEndDeviceBatchRegistry/Delete" +) + +// JsEndDeviceBatchRegistryClient is the client API for JsEndDeviceBatchRegistry service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type JsEndDeviceBatchRegistryClient interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type jsEndDeviceBatchRegistryClient struct { + cc grpc.ClientConnInterface +} + +func NewJsEndDeviceBatchRegistryClient(cc grpc.ClientConnInterface) JsEndDeviceBatchRegistryClient { + return &jsEndDeviceBatchRegistryClient{cc} +} + +func (c *jsEndDeviceBatchRegistryClient) Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, JsEndDeviceBatchRegistry_Delete_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// JsEndDeviceBatchRegistryServer is the server API for JsEndDeviceBatchRegistry service. +// All implementations must embed UnimplementedJsEndDeviceBatchRegistryServer +// for forward compatibility +type JsEndDeviceBatchRegistryServer interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedJsEndDeviceBatchRegistryServer() +} + +// UnimplementedJsEndDeviceBatchRegistryServer must be embedded to have forward compatible implementations. +type UnimplementedJsEndDeviceBatchRegistryServer struct { +} + +func (UnimplementedJsEndDeviceBatchRegistryServer) Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedJsEndDeviceBatchRegistryServer) mustEmbedUnimplementedJsEndDeviceBatchRegistryServer() { +} + +// UnsafeJsEndDeviceBatchRegistryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to JsEndDeviceBatchRegistryServer will +// result in compilation errors. +type UnsafeJsEndDeviceBatchRegistryServer interface { + mustEmbedUnimplementedJsEndDeviceBatchRegistryServer() +} + +func RegisterJsEndDeviceBatchRegistryServer(s grpc.ServiceRegistrar, srv JsEndDeviceBatchRegistryServer) { + s.RegisterService(&JsEndDeviceBatchRegistry_ServiceDesc, srv) +} + +func _JsEndDeviceBatchRegistry_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchDeleteEndDevicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(JsEndDeviceBatchRegistryServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: JsEndDeviceBatchRegistry_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(JsEndDeviceBatchRegistryServer).Delete(ctx, req.(*BatchDeleteEndDevicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// JsEndDeviceBatchRegistry_ServiceDesc is the grpc.ServiceDesc for JsEndDeviceBatchRegistry service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var JsEndDeviceBatchRegistry_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.JsEndDeviceBatchRegistry", + HandlerType: (*JsEndDeviceBatchRegistryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Delete", + Handler: _JsEndDeviceBatchRegistry_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lorawan-stack/api/joinserver.proto", +} + const ( ApplicationActivationSettingRegistry_Get_FullMethodName = "/ttn.lorawan.v3.ApplicationActivationSettingRegistry/Get" ApplicationActivationSettingRegistry_Set_FullMethodName = "/ttn.lorawan.v3.ApplicationActivationSettingRegistry/Set" diff --git a/pkg/ttnpb/networkserver.pb.go b/pkg/ttnpb/networkserver.pb.go index 6b452a13f1..0eef9f712d 100644 --- a/pkg/ttnpb/networkserver.pb.go +++ b/pkg/ttnpb/networkserver.pb.go @@ -441,10 +441,22 @@ var file_lorawan_stack_api_networkserver_proto_rawDesc = []byte{ 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x7d, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, - 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x7d, 0x32, 0xb4, 0x01, 0x0a, 0x18, 0x4e, 0x73, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x12, + 0x97, 0x01, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x74, 0x74, 0x6e, + 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x47, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x41, 0x2a, 0x3f, 0x2f, 0x6e, 0x73, 0x2f, 0x61, 0x70, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x2e, 0x61, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, + 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, + 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -474,9 +486,10 @@ var file_lorawan_stack_api_networkserver_proto_goTypes = []interface{}{ (*GetEndDeviceRequest)(nil), // 10: ttn.lorawan.v3.GetEndDeviceRequest (*SetEndDeviceRequest)(nil), // 11: ttn.lorawan.v3.SetEndDeviceRequest (*ResetAndGetEndDeviceRequest)(nil), // 12: ttn.lorawan.v3.ResetAndGetEndDeviceRequest - (*MACSettings)(nil), // 13: ttn.lorawan.v3.MACSettings - (*ApplicationDownlinks)(nil), // 14: ttn.lorawan.v3.ApplicationDownlinks - (*EndDevice)(nil), // 15: ttn.lorawan.v3.EndDevice + (*BatchDeleteEndDevicesRequest)(nil), // 13: ttn.lorawan.v3.BatchDeleteEndDevicesRequest + (*MACSettings)(nil), // 14: ttn.lorawan.v3.MACSettings + (*ApplicationDownlinks)(nil), // 15: ttn.lorawan.v3.ApplicationDownlinks + (*EndDevice)(nil), // 16: ttn.lorawan.v3.EndDevice } var file_lorawan_stack_api_networkserver_proto_depIdxs = []int32{ 4, // 0: ttn.lorawan.v3.GetDefaultMACSettingsRequest.lorawan_phy_version:type_name -> ttn.lorawan.v3.PHYVersion @@ -493,21 +506,23 @@ var file_lorawan_stack_api_networkserver_proto_depIdxs = []int32{ 11, // 11: ttn.lorawan.v3.NsEndDeviceRegistry.Set:input_type -> ttn.lorawan.v3.SetEndDeviceRequest 12, // 12: ttn.lorawan.v3.NsEndDeviceRegistry.ResetFactoryDefaults:input_type -> ttn.lorawan.v3.ResetAndGetEndDeviceRequest 7, // 13: ttn.lorawan.v3.NsEndDeviceRegistry.Delete:input_type -> ttn.lorawan.v3.EndDeviceIdentifiers - 0, // 14: ttn.lorawan.v3.Ns.GenerateDevAddr:output_type -> ttn.lorawan.v3.GenerateDevAddrResponse - 13, // 15: ttn.lorawan.v3.Ns.GetDefaultMACSettings:output_type -> ttn.lorawan.v3.MACSettings - 2, // 16: ttn.lorawan.v3.Ns.GetNetID:output_type -> ttn.lorawan.v3.GetNetIDResponse - 3, // 17: ttn.lorawan.v3.Ns.GetDeviceAddressPrefixes:output_type -> ttn.lorawan.v3.GetDeviceAdressPrefixesResponse - 5, // 18: ttn.lorawan.v3.AsNs.DownlinkQueueReplace:output_type -> google.protobuf.Empty - 5, // 19: ttn.lorawan.v3.AsNs.DownlinkQueuePush:output_type -> google.protobuf.Empty - 14, // 20: ttn.lorawan.v3.AsNs.DownlinkQueueList:output_type -> ttn.lorawan.v3.ApplicationDownlinks - 5, // 21: ttn.lorawan.v3.GsNs.HandleUplink:output_type -> google.protobuf.Empty - 5, // 22: ttn.lorawan.v3.GsNs.ReportTxAcknowledgment:output_type -> google.protobuf.Empty - 15, // 23: ttn.lorawan.v3.NsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice - 15, // 24: ttn.lorawan.v3.NsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice - 15, // 25: ttn.lorawan.v3.NsEndDeviceRegistry.ResetFactoryDefaults:output_type -> ttn.lorawan.v3.EndDevice - 5, // 26: ttn.lorawan.v3.NsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty - 14, // [14:27] is the sub-list for method output_type - 1, // [1:14] is the sub-list for method input_type + 13, // 14: ttn.lorawan.v3.NsEndDeviceBatchRegistry.Delete:input_type -> ttn.lorawan.v3.BatchDeleteEndDevicesRequest + 0, // 15: ttn.lorawan.v3.Ns.GenerateDevAddr:output_type -> ttn.lorawan.v3.GenerateDevAddrResponse + 14, // 16: ttn.lorawan.v3.Ns.GetDefaultMACSettings:output_type -> ttn.lorawan.v3.MACSettings + 2, // 17: ttn.lorawan.v3.Ns.GetNetID:output_type -> ttn.lorawan.v3.GetNetIDResponse + 3, // 18: ttn.lorawan.v3.Ns.GetDeviceAddressPrefixes:output_type -> ttn.lorawan.v3.GetDeviceAdressPrefixesResponse + 5, // 19: ttn.lorawan.v3.AsNs.DownlinkQueueReplace:output_type -> google.protobuf.Empty + 5, // 20: ttn.lorawan.v3.AsNs.DownlinkQueuePush:output_type -> google.protobuf.Empty + 15, // 21: ttn.lorawan.v3.AsNs.DownlinkQueueList:output_type -> ttn.lorawan.v3.ApplicationDownlinks + 5, // 22: ttn.lorawan.v3.GsNs.HandleUplink:output_type -> google.protobuf.Empty + 5, // 23: ttn.lorawan.v3.GsNs.ReportTxAcknowledgment:output_type -> google.protobuf.Empty + 16, // 24: ttn.lorawan.v3.NsEndDeviceRegistry.Get:output_type -> ttn.lorawan.v3.EndDevice + 16, // 25: ttn.lorawan.v3.NsEndDeviceRegistry.Set:output_type -> ttn.lorawan.v3.EndDevice + 16, // 26: ttn.lorawan.v3.NsEndDeviceRegistry.ResetFactoryDefaults:output_type -> ttn.lorawan.v3.EndDevice + 5, // 27: ttn.lorawan.v3.NsEndDeviceRegistry.Delete:output_type -> google.protobuf.Empty + 5, // 28: ttn.lorawan.v3.NsEndDeviceBatchRegistry.Delete:output_type -> google.protobuf.Empty + 15, // [15:29] is the sub-list for method output_type + 1, // [1:15] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name 1, // [1:1] is the sub-list for extension extendee 0, // [0:1] is the sub-list for field type_name @@ -580,7 +595,7 @@ func file_lorawan_stack_api_networkserver_proto_init() { NumEnums: 0, NumMessages: 4, NumExtensions: 0, - NumServices: 4, + NumServices: 5, }, GoTypes: file_lorawan_stack_api_networkserver_proto_goTypes, DependencyIndexes: file_lorawan_stack_api_networkserver_proto_depIdxs, diff --git a/pkg/ttnpb/networkserver.pb.gw.go b/pkg/ttnpb/networkserver.pb.gw.go index 6cef0d2ced..81a7a7be99 100644 --- a/pkg/ttnpb/networkserver.pb.gw.go +++ b/pkg/ttnpb/networkserver.pb.gw.go @@ -588,6 +588,76 @@ func local_request_NsEndDeviceRegistry_Delete_0(ctx context.Context, marshaler r } +var ( + filter_NsEndDeviceBatchRegistry_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{"application_ids": 0, "application_id": 1, "applicationId": 2}, Base: []int{1, 1, 1, 2, 0, 0}, Check: []int{0, 1, 2, 1, 3, 4}} +) + +func request_NsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client NsEndDeviceBatchRegistryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_NsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_NsEndDeviceBatchRegistry_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server NsEndDeviceBatchRegistryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BatchDeleteEndDevicesRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["application_ids.application_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "application_ids.application_id") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "application_ids.application_id", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "application_ids.application_id", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_NsEndDeviceBatchRegistry_Delete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Delete(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterNsHandlerServer registers the http handlers for service Ns to "mux". // UnaryRPC :call NsServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -831,6 +901,40 @@ func RegisterNsEndDeviceRegistryHandlerServer(ctx context.Context, mux *runtime. return nil } +// RegisterNsEndDeviceBatchRegistryHandlerServer registers the http handlers for service NsEndDeviceBatchRegistry to "mux". +// UnaryRPC :call NsEndDeviceBatchRegistryServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterNsEndDeviceBatchRegistryHandlerFromEndpoint instead. +func RegisterNsEndDeviceBatchRegistryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server NsEndDeviceBatchRegistryServer) error { + + mux.Handle("DELETE", pattern_NsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.NsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/ns/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_NsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_NsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterNsHandlerFromEndpoint is same as RegisterNsHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterNsHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -1154,3 +1258,74 @@ var ( forward_NsEndDeviceRegistry_Delete_0 = runtime.ForwardResponseMessage ) + +// RegisterNsEndDeviceBatchRegistryHandlerFromEndpoint is same as RegisterNsEndDeviceBatchRegistryHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterNsEndDeviceBatchRegistryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterNsEndDeviceBatchRegistryHandler(ctx, mux, conn) +} + +// RegisterNsEndDeviceBatchRegistryHandler registers the http handlers for service NsEndDeviceBatchRegistry to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterNsEndDeviceBatchRegistryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterNsEndDeviceBatchRegistryHandlerClient(ctx, mux, NewNsEndDeviceBatchRegistryClient(conn)) +} + +// RegisterNsEndDeviceBatchRegistryHandlerClient registers the http handlers for service NsEndDeviceBatchRegistry +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "NsEndDeviceBatchRegistryClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "NsEndDeviceBatchRegistryClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "NsEndDeviceBatchRegistryClient" to call the correct interceptors. +func RegisterNsEndDeviceBatchRegistryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client NsEndDeviceBatchRegistryClient) error { + + mux.Handle("DELETE", pattern_NsEndDeviceBatchRegistry_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.NsEndDeviceBatchRegistry/Delete", runtime.WithHTTPPathPattern("/ns/applications/{application_ids.application_id}/devices/batch")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_NsEndDeviceBatchRegistry_Delete_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_NsEndDeviceBatchRegistry_Delete_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_NsEndDeviceBatchRegistry_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"ns", "applications", "application_ids.application_id", "devices", "batch"}, "")) +) + +var ( + forward_NsEndDeviceBatchRegistry_Delete_0 = runtime.ForwardResponseMessage +) diff --git a/pkg/ttnpb/networkserver_grpc.pb.go b/pkg/ttnpb/networkserver_grpc.pb.go index 4d5576621e..5b2bf55e85 100644 --- a/pkg/ttnpb/networkserver_grpc.pb.go +++ b/pkg/ttnpb/networkserver_grpc.pb.go @@ -757,3 +757,100 @@ var NsEndDeviceRegistry_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "lorawan-stack/api/networkserver.proto", } + +const ( + NsEndDeviceBatchRegistry_Delete_FullMethodName = "/ttn.lorawan.v3.NsEndDeviceBatchRegistry/Delete" +) + +// NsEndDeviceBatchRegistryClient is the client API for NsEndDeviceBatchRegistry service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type NsEndDeviceBatchRegistryClient interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type nsEndDeviceBatchRegistryClient struct { + cc grpc.ClientConnInterface +} + +func NewNsEndDeviceBatchRegistryClient(cc grpc.ClientConnInterface) NsEndDeviceBatchRegistryClient { + return &nsEndDeviceBatchRegistryClient{cc} +} + +func (c *nsEndDeviceBatchRegistryClient) Delete(ctx context.Context, in *BatchDeleteEndDevicesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, NsEndDeviceBatchRegistry_Delete_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NsEndDeviceBatchRegistryServer is the server API for NsEndDeviceBatchRegistry service. +// All implementations must embed UnimplementedNsEndDeviceBatchRegistryServer +// for forward compatibility +type NsEndDeviceBatchRegistryServer interface { + // Delete a list of devices within the same application. + // This operation is atomic; either all devices are deleted or none. + // Devices not found are skipped and no error is returned. + Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedNsEndDeviceBatchRegistryServer() +} + +// UnimplementedNsEndDeviceBatchRegistryServer must be embedded to have forward compatible implementations. +type UnimplementedNsEndDeviceBatchRegistryServer struct { +} + +func (UnimplementedNsEndDeviceBatchRegistryServer) Delete(context.Context, *BatchDeleteEndDevicesRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedNsEndDeviceBatchRegistryServer) mustEmbedUnimplementedNsEndDeviceBatchRegistryServer() { +} + +// UnsafeNsEndDeviceBatchRegistryServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NsEndDeviceBatchRegistryServer will +// result in compilation errors. +type UnsafeNsEndDeviceBatchRegistryServer interface { + mustEmbedUnimplementedNsEndDeviceBatchRegistryServer() +} + +func RegisterNsEndDeviceBatchRegistryServer(s grpc.ServiceRegistrar, srv NsEndDeviceBatchRegistryServer) { + s.RegisterService(&NsEndDeviceBatchRegistry_ServiceDesc, srv) +} + +func _NsEndDeviceBatchRegistry_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchDeleteEndDevicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NsEndDeviceBatchRegistryServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NsEndDeviceBatchRegistry_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NsEndDeviceBatchRegistryServer).Delete(ctx, req.(*BatchDeleteEndDevicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NsEndDeviceBatchRegistry_ServiceDesc is the grpc.ServiceDesc for NsEndDeviceBatchRegistry service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NsEndDeviceBatchRegistry_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.NsEndDeviceBatchRegistry", + HandlerType: (*NsEndDeviceBatchRegistryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Delete", + Handler: _NsEndDeviceBatchRegistry_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "lorawan-stack/api/networkserver.proto", +} diff --git a/sdk/js/generated/api-definition.json b/sdk/js/generated/api-definition.json index cc8abca699..2b52be0c0f 100644 --- a/sdk/js/generated/api-definition.json +++ b/sdk/js/generated/api-definition.json @@ -529,6 +529,20 @@ ] } }, + "AsEndDeviceBatchRegistry": { + "Delete": { + "file": "lorawan-stack/api/applicationserver.proto", + "http": [ + { + "method": "delete", + "pattern": "/as/applications/{application_ids.application_id}/devices/batch", + "parameters": [ + "application_ids.application_id" + ] + } + ] + } + }, "AsEndDeviceRegistry": { "Get": { "file": "lorawan-stack/api/applicationserver.proto", @@ -2697,6 +2711,20 @@ ] } }, + "EndDeviceBatchRegistry": { + "Delete": { + "file": "lorawan-stack/api/end_device_services.proto", + "http": [ + { + "method": "delete", + "pattern": "/applications/{application_ids.application_id}/devices/batch", + "parameters": [ + "application_ids.application_id" + ] + } + ] + } + }, "EndDeviceRegistry": { "Create": { "file": "lorawan-stack/api/end_device_services.proto", @@ -3602,6 +3630,20 @@ ] } }, + "JsEndDeviceBatchRegistry": { + "Delete": { + "file": "lorawan-stack/api/joinserver.proto", + "http": [ + { + "method": "delete", + "pattern": "/js/applications/{application_ids.application_id}/devices/batch", + "parameters": [ + "application_ids.application_id" + ] + } + ] + } + }, "JsEndDeviceRegistry": { "Get": { "file": "lorawan-stack/api/joinserver.proto", @@ -3828,6 +3870,20 @@ ] } }, + "NsEndDeviceBatchRegistry": { + "Delete": { + "file": "lorawan-stack/api/networkserver.proto", + "http": [ + { + "method": "delete", + "pattern": "/ns/applications/{application_ids.application_id}/devices/batch", + "parameters": [ + "application_ids.application_id" + ] + } + ] + } + }, "NsEndDeviceRegistry": { "Get": { "file": "lorawan-stack/api/networkserver.proto", diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 70aba08fe8..cbf4d9c838 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -2681,6 +2681,36 @@ } ] }, + { + "name": "AsEndDeviceBatchRegistry", + "longName": "AsEndDeviceBatchRegistry", + "fullName": "ttn.lorawan.v3.AsEndDeviceBatchRegistry", + "description": "The AsEndDeviceBatchRegistry service allows clients to manage batches end devices on the Application Server.", + "methods": [ + { + "name": "Delete", + "description": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "requestType": "BatchDeleteEndDevicesRequest", + "requestLongType": "BatchDeleteEndDevicesRequest", + "requestFullType": "ttn.lorawan.v3.BatchDeleteEndDevicesRequest", + "requestStreaming": false, + "responseType": "Empty", + "responseLongType": ".google.protobuf.Empty", + "responseFullType": "google.protobuf.Empty", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "DELETE", + "pattern": "/as/applications/{application_ids.application_id}/devices/batch" + } + ] + } + } + } + ] + }, { "name": "AsEndDeviceRegistry", "longName": "AsEndDeviceRegistry", @@ -14236,6 +14266,70 @@ } ] }, + { + "name": "BatchDeleteEndDevicesRequest", + "longName": "BatchDeleteEndDevicesRequest", + "fullName": "ttn.lorawan.v3.BatchDeleteEndDevicesRequest", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "application_ids", + "description": "", + "label": "", + "type": "ApplicationIdentifiers", + "longType": "ApplicationIdentifiers", + "fullType": "ttn.lorawan.v3.ApplicationIdentifiers", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "message.required", + "value": true + } + ] + } + }, + { + "name": "device_ids", + "description": "", + "label": "repeated", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "repeated.min_items", + "value": 1 + }, + { + "name": "repeated.max_items", + "value": 20 + }, + { + "name": "repeated.items.string.max_len", + "value": 36 + }, + { + "name": "repeated.items.string.pattern", + "value": "^[a-z0-9](?:[-]?[a-z0-9]){2,}$" + } + ] + } + } + ] + }, { "name": "BatchUpdateEndDeviceLastSeenRequest", "longName": "BatchUpdateEndDeviceLastSeenRequest", @@ -18389,6 +18483,36 @@ "extensions": [], "messages": [], "services": [ + { + "name": "EndDeviceBatchRegistry", + "longName": "EndDeviceBatchRegistry", + "fullName": "ttn.lorawan.v3.EndDeviceBatchRegistry", + "description": "The EndDeviceBatchRegistry service, exposed by the Identity Server, is used to manage\nend device registrations in batches.", + "methods": [ + { + "name": "Delete", + "description": "Delete a batch of end devices with the given IDs.\n\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.\nBefore calling this RPC, use the corresponding BatchDelete RPCs\nof NsEndDeviceRegistry, AsEndDeviceRegistry and\noptionally the JsEndDeviceRegistry to delete the end devices.\nIf the devices were claimed on a Join Server, use the BatchUnclaim RPC\nof the DeviceClaimingServer.\nThis is NOT done automatically.", + "requestType": "BatchDeleteEndDevicesRequest", + "requestLongType": "BatchDeleteEndDevicesRequest", + "requestFullType": "ttn.lorawan.v3.BatchDeleteEndDevicesRequest", + "requestStreaming": false, + "responseType": "Empty", + "responseLongType": ".google.protobuf.Empty", + "responseFullType": "google.protobuf.Empty", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "DELETE", + "pattern": "/applications/{application_ids.application_id}/devices/batch" + } + ] + } + } + } + ] + }, { "name": "EndDeviceRegistry", "longName": "EndDeviceRegistry", @@ -23059,6 +23183,30 @@ } ] }, + { + "name": "EndDeviceIdentifiersList", + "longName": "EndDeviceIdentifiersList", + "fullName": "ttn.lorawan.v3.EndDeviceIdentifiersList", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "end_device_ids", + "description": "", + "label": "repeated", + "type": "EndDeviceIdentifiers", + "longType": "EndDeviceIdentifiers", + "fullType": "ttn.lorawan.v3.EndDeviceIdentifiers", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, { "name": "EndDeviceVersionIdentifiers", "longName": "EndDeviceVersionIdentifiers", @@ -26013,6 +26161,36 @@ } ] }, + { + "name": "JsEndDeviceBatchRegistry", + "longName": "JsEndDeviceBatchRegistry", + "fullName": "ttn.lorawan.v3.JsEndDeviceBatchRegistry", + "description": "JsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Join Server.", + "methods": [ + { + "name": "Delete", + "description": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "requestType": "BatchDeleteEndDevicesRequest", + "requestLongType": "BatchDeleteEndDevicesRequest", + "requestFullType": "ttn.lorawan.v3.BatchDeleteEndDevicesRequest", + "requestStreaming": false, + "responseType": "Empty", + "responseLongType": ".google.protobuf.Empty", + "responseFullType": "google.protobuf.Empty", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "DELETE", + "pattern": "/js/applications/{application_ids.application_id}/devices/batch" + } + ] + } + } + } + ] + }, { "name": "JsEndDeviceRegistry", "longName": "JsEndDeviceRegistry", @@ -34875,6 +35053,36 @@ } ] }, + { + "name": "NsEndDeviceBatchRegistry", + "longName": "NsEndDeviceBatchRegistry", + "fullName": "ttn.lorawan.v3.NsEndDeviceBatchRegistry", + "description": "The NsEndDeviceBatchRegistry service allows clients to manage batches of end devices on the Network Server.", + "methods": [ + { + "name": "Delete", + "description": "Delete a list of devices within the same application.\nThis operation is atomic; either all devices are deleted or none.\nDevices not found are skipped and no error is returned.", + "requestType": "BatchDeleteEndDevicesRequest", + "requestLongType": "BatchDeleteEndDevicesRequest", + "requestFullType": "ttn.lorawan.v3.BatchDeleteEndDevicesRequest", + "requestStreaming": false, + "responseType": "Empty", + "responseLongType": ".google.protobuf.Empty", + "responseFullType": "google.protobuf.Empty", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "DELETE", + "pattern": "/ns/applications/{application_ids.application_id}/devices/batch" + } + ] + } + } + } + ] + }, { "name": "NsEndDeviceRegistry", "longName": "NsEndDeviceRegistry",