Skip to content

Commit

Permalink
Merge pull request #6459 from TheThingsNetwork/feature/3632-batch-del…
Browse files Browse the repository at this point in the history
…ete-gateways

Support batch deletion of gateways
  • Loading branch information
KrishnaIyer authored Aug 23, 2023
2 parents f8bf303 + 1142611 commit 7d87721
Show file tree
Hide file tree
Showing 28 changed files with 1,600 additions and 259 deletions.
36 changes: 36 additions & 0 deletions api/ttn/lorawan/v3/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,11 @@
- [Service `GatewayConfigurationService`](#ttn.lorawan.v3.GatewayConfigurationService)
- [File `ttn/lorawan/v3/gateway_services.proto`](#ttn/lorawan/v3/gateway_services.proto)
- [Message `AssertGatewayRightsRequest`](#ttn.lorawan.v3.AssertGatewayRightsRequest)
- [Message `BatchDeleteGatewaysRequest`](#ttn.lorawan.v3.BatchDeleteGatewaysRequest)
- [Message `PullGatewayConfigurationRequest`](#ttn.lorawan.v3.PullGatewayConfigurationRequest)
- [Service `GatewayAccess`](#ttn.lorawan.v3.GatewayAccess)
- [Service `GatewayBatchAccess`](#ttn.lorawan.v3.GatewayBatchAccess)
- [Service `GatewayBatchRegistry`](#ttn.lorawan.v3.GatewayBatchRegistry)
- [Service `GatewayConfigurator`](#ttn.lorawan.v3.GatewayConfigurator)
- [Service `GatewayRegistry`](#ttn.lorawan.v3.GatewayRegistry)
- [File `ttn/lorawan/v3/gatewayserver.proto`](#ttn/lorawan/v3/gatewayserver.proto)
Expand All @@ -355,6 +357,7 @@
- [Message `EndDeviceVersionIdentifiers`](#ttn.lorawan.v3.EndDeviceVersionIdentifiers)
- [Message `EntityIdentifiers`](#ttn.lorawan.v3.EntityIdentifiers)
- [Message `GatewayIdentifiers`](#ttn.lorawan.v3.GatewayIdentifiers)
- [Message `GatewayIdentifiersList`](#ttn.lorawan.v3.GatewayIdentifiersList)
- [Message `LoRaAllianceProfileIdentifiers`](#ttn.lorawan.v3.LoRaAllianceProfileIdentifiers)
- [Message `NetworkIdentifiers`](#ttn.lorawan.v3.NetworkIdentifiers)
- [Message `OrganizationIdentifiers`](#ttn.lorawan.v3.OrganizationIdentifiers)
Expand Down Expand Up @@ -5016,6 +5019,18 @@ Identifies an end device model with version information.
| `gateway_ids` | <p>`repeated.min_items`: `1`</p><p>`repeated.max_items`: `20`</p> |
| `required` | <p>`message.required`: `true`</p> |

### <a name="ttn.lorawan.v3.BatchDeleteGatewaysRequest">Message `BatchDeleteGatewaysRequest`</a>

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `gateway_ids` | [`GatewayIdentifiers`](#ttn.lorawan.v3.GatewayIdentifiers) | repeated | |

#### Field Rules

| Field | Validations |
| ----- | ----------- |
| `gateway_ids` | <p>`repeated.min_items`: `1`</p><p>`repeated.max_items`: `20`</p> |

### <a name="ttn.lorawan.v3.PullGatewayConfigurationRequest">Message `PullGatewayConfigurationRequest`</a>

| Field | Type | Label | Description |
Expand Down Expand Up @@ -5070,6 +5085,21 @@ EXPERIMENTAL: This service is subject to change.
| ----------- | ------ | ------- | ---- |
| `AssertRights` | `GET` | `/api/v3/gateways/rights/batch` | |

### <a name="ttn.lorawan.v3.GatewayBatchRegistry">Service `GatewayBatchRegistry`</a>

The GatewayBatchRegistry service, exposed by the Identity Server, is used to manage
gateway registrations in batches.

| Method Name | Request Type | Response Type | Description |
| ----------- | ------------ | ------------- | ------------|
| `Delete` | [`BatchDeleteGatewaysRequest`](#ttn.lorawan.v3.BatchDeleteGatewaysRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete a batch of gateways. This operation is atomic; either all gateways are deleted or none. The caller must have delete rights on all requested gateways. |

#### HTTP bindings

| Method Name | Method | Pattern | Body |
| ----------- | ------ | ------- | ---- |
| `Delete` | `DELETE` | `/api/v3/gateways/batch` | |

### <a name="ttn.lorawan.v3.GatewayConfigurator">Service `GatewayConfigurator`</a>

| Method Name | Request Type | Response Type | Description |
Expand Down Expand Up @@ -5315,6 +5345,12 @@ EntityIdentifiers contains one of the possible entity identifiers.
| `gateway_id` | <p>`string.max_len`: `36`</p><p>`string.pattern`: `^[a-z0-9](?:[-]?[a-z0-9]){2,}$`</p> |
| `eui` | <p>`bytes.len`: `8`</p> |

### <a name="ttn.lorawan.v3.GatewayIdentifiersList">Message `GatewayIdentifiersList`</a>

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `gateway_ids` | [`GatewayIdentifiers`](#ttn.lorawan.v3.GatewayIdentifiers) | repeated | |

### <a name="ttn.lorawan.v3.LoRaAllianceProfileIdentifiers">Message `LoRaAllianceProfileIdentifiers`</a>

| Field | Type | Label | Description |
Expand Down
27 changes: 27 additions & 0 deletions api/ttn/lorawan/v3/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@
{
"name": "GatewayBatchAccess"
},
{
"name": "GatewayBatchRegistry"
},
{
"name": "GtwGs"
},
Expand Down Expand Up @@ -8446,6 +8449,30 @@
]
}
},
"/gateways/batch": {
"delete": {
"summary": "Delete a batch of gateways.\nThis operation is atomic; either all gateways are deleted or none.\nThe caller must have delete rights on all requested gateways.",
"operationId": "GatewayBatchRegistry_Delete",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "object",
"properties": {}
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/googlerpcStatus"
}
}
},
"tags": [
"GatewayBatchRegistry"
]
}
},
"/gateways/rights/batch": {
"get": {
"summary": "Assert that the caller has the requested rights on all the requested gateways.\nThe check is successful if there are no errors.",
Expand Down
18 changes: 18 additions & 0 deletions api/ttn/lorawan/v3/gateway_services.proto
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,21 @@ service GatewayBatchAccess {
option (google.api.http) = {get: "/gateways/rights/batch"};
}
}

message BatchDeleteGatewaysRequest {
repeated ttn.lorawan.v3.GatewayIdentifiers gateway_ids = 1 [(validate.rules).repeated = {
min_items: 1,
max_items: 20,
}];
}

// The GatewayBatchRegistry service, exposed by the Identity Server, is used to manage
// gateway registrations in batches.
service GatewayBatchRegistry {
// Delete a batch of gateways.
// This operation is atomic; either all gateways are deleted or none.
// The caller must have delete rights on all requested gateways.
rpc Delete(BatchDeleteGatewaysRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/gateways/batch"};
}
}
4 changes: 4 additions & 0 deletions api/ttn/lorawan/v3/identifiers.proto
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,7 @@ message LoRaAllianceProfileIdentifiers {
message EndDeviceIdentifiersList {
repeated EndDeviceIdentifiers end_device_ids = 1;
}

message GatewayIdentifiersList {
repeated GatewayIdentifiers gateway_ids = 1;
}
40 changes: 40 additions & 0 deletions cmd/ttn-lw-cli/commands/gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,45 @@ var (
return nil
},
}
gatewaysBatchDeleteCommand = &cobra.Command{
Use: "batch-delete [gateway-ids]",
Short: "Delete a batch of gateways (EXPERIMENTAL).",
RunE: func(cmd *cobra.Command, args []string) error {
var gatewayIDs []*ttnpb.GatewayIdentifiers
if inputDecoder != nil {
dec := struct {
GatewayIDs []string `json:"gateway_ids"`
}{}
err := inputDecoder.Decode(&dec)
if err != nil {
return err
}
for _, gtwID := range dec.GatewayIDs {
gatewayIDs = append(gatewayIDs, &ttnpb.GatewayIdentifiers{
GatewayId: gtwID,
})
}
} else if len(args) == 0 {
return errNoIDs.New()
} else {
for _, arg := range args {
gatewayIDs = append(gatewayIDs, &ttnpb.GatewayIdentifiers{
GatewayId: arg,
})
}
}
is, err := api.Dial(ctx, config.IdentityServerGRPCAddress)
if err != nil {
return err
}
_, err = ttnpb.NewGatewayBatchRegistryClient(is).Delete(
ctx, &ttnpb.BatchDeleteGatewaysRequest{
GatewayIds: gatewayIDs,
},
)
return err
},
}
gatewaysRestoreCommand = &cobra.Command{
Use: "restore [gateway-id]",
Short: "Restore a gateway",
Expand Down Expand Up @@ -606,6 +645,7 @@ func init() {
gatewaysPurgeCommand.Flags().AddFlagSet(gatewayIDFlags())
gatewaysPurgeCommand.Flags().AddFlagSet(forceFlags())
gatewaysCommand.AddCommand(gatewaysPurgeCommand)
gatewaysCommand.AddCommand(gatewaysBatchDeleteCommand)
Root.AddCommand(gatewaysCommand)
}

Expand Down
9 changes: 9 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9764,6 +9764,15 @@
"file": "gateway_access.go"
}
},
"event:gateway.batch.delete": {
"translations": {
"en": "batch delete gateways"
},
"description": {
"package": "pkg/identityserver",
"file": "gateway_registry.go"
}
},
"event:gateway.collaborator.delete": {
"translations": {
"en": "delete gateway collaborator"
Expand Down
35 changes: 35 additions & 0 deletions pkg/identityserver/bunstore/gateway_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,41 @@ func (s *gatewayStore) DeleteGateway(ctx context.Context, id *ttnpb.GatewayIdent
return nil
}

func (s *gatewayStore) BatchDeleteGateways(
ctx context.Context,
ids []*ttnpb.GatewayIdentifiers,
) ([]*ttnpb.GatewayIdentifiers, error) {
ctx, span := tracer.StartFromContext(ctx, "BatchDeleteGateways", trace.WithAttributes(
attribute.Int("count", len(ids)),
))
defer span.End()

deleted := make([]*ttnpb.GatewayIdentifiers, 0, len(ids))
for _, id := range ids {
model, err := s.getGatewayModelBy(
ctx,
s.selectWithID(ctx, id.GetGatewayId()),
store.FieldMask{"ids"},
)
if err != nil {
if errors.IsNotFound(err) {
continue
}
return nil, storeutil.WrapDriverError(err)
}

_, err = s.DB.NewDelete().
Model(model).
WherePK().
Exec(ctx)
if err != nil {
return nil, storeutil.WrapDriverError(err)
}
deleted = append(deleted, id)
}
return deleted, nil
}

func (s *gatewayStore) RestoreGateway(ctx context.Context, id *ttnpb.GatewayIdentifiers) error {
ctx, span := tracer.StartFromContext(ctx, "RestoreGateway", trace.WithAttributes(
attribute.String("gateway_id", id.GetGatewayId()),
Expand Down
1 change: 1 addition & 0 deletions pkg/identityserver/bunstore/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func TestGatewayStore(t *testing.T) {
st := storetest.New(t, newTestStore)
st.TestGatewayStoreCRUD(t)
st.TestGatewayStorePagination(t)
st.TestGatewayBatchOperations(t)
}

func TestOrganizationStore(t *testing.T) {
Expand Down
62 changes: 62 additions & 0 deletions pkg/identityserver/gateway_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ var (
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
)
evtBatchDeleteGateways = events.Define(
"gateway.batch.delete", "batch delete gateways",
events.WithVisibility(ttnpb.Right_RIGHT_GATEWAY_INFO),
events.WithDataType(&ttnpb.GatewayIdentifiersList{}),
events.WithAuthFromContext(),
events.WithClientInfoFromContext(),
events.WithPropagateToParent(),
)
)

var (
Expand Down Expand Up @@ -728,6 +736,46 @@ func (is *IdentityServer) purgeGateway(ctx context.Context, ids *ttnpb.GatewayId
return ttnpb.Empty, nil
}

func (is *IdentityServer) batchDeleteGateways(
ctx context.Context,
req *ttnpb.BatchDeleteGatewaysRequest,
) (*emptypb.Empty, error) {
if err := is.assertGatewayRights(
ctx,
req.GatewayIds,
&ttnpb.Rights{
Rights: []ttnpb.Right{ttnpb.Right_RIGHT_GATEWAY_DELETE},
},
); err != nil {
return nil, err
}
var (
err error
deleted = make([]*ttnpb.GatewayIdentifiers, 0)
)
err = is.store.Transact(ctx, func(ctx context.Context, st store.Store) error {
deleted, err = st.BatchDeleteGateways(ctx, req.GatewayIds)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
if len(deleted) != 0 {
events.Publish(
evtBatchDeleteGateways.New(
ctx,
events.WithData(
&ttnpb.GatewayIdentifiersList{GatewayIds: deleted},
),
),
)
}
return ttnpb.Empty, nil
}

func validateClaimAuthenticationCode(authCode *ttnpb.GatewayClaimAuthenticationCode) error {
if authCode.Secret == nil {
return errClaimAuthenticationCode.New()
Expand Down Expand Up @@ -781,3 +829,17 @@ func (gr *gatewayRegistry) Restore(ctx context.Context, req *ttnpb.GatewayIdenti
func (gr *gatewayRegistry) Purge(ctx context.Context, req *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) {
return gr.purgeGateway(ctx, req)
}

type gatewayBatchRegistry struct {
ttnpb.UnimplementedGatewayBatchRegistryServer

*IdentityServer
}

// Delete implements ttnpb.GatewayBatchRegistryServer.
func (gr *gatewayBatchRegistry) Delete(
ctx context.Context,
req *ttnpb.BatchDeleteGatewaysRequest,
) (*emptypb.Empty, error) {
return gr.batchDeleteGateways(ctx, req)
}
Loading

0 comments on commit 7d87721

Please sign in to comment.