diff --git a/pkg/networkserver/grpc_deviceregistry.go b/pkg/networkserver/grpc_deviceregistry.go index c1b0323eb3..0742884dc8 100644 --- a/pkg/networkserver/grpc_deviceregistry.go +++ b/pkg/networkserver/grpc_deviceregistry.go @@ -1613,10 +1613,25 @@ func (ns *NetworkServer) Delete(ctx context.Context, req *ttnpb.EndDeviceIdentif return nil, err } var evt events.Event - _, _, err := ns.devices.SetByID(ctx, req.ApplicationIds, req.DeviceId, nil, func(ctx context.Context, dev *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { + _, _, err := ns.devices.SetByID(ctx, req.ApplicationIds, req.DeviceId, []string{"mac_settings_profile_ids"}, func(ctx context.Context, dev *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { // nolint: lll if dev == nil { return nil, nil, errDeviceNotFound.New() } + if dev.MacSettingsProfileIds != nil { + _, err := ns.macSettingsProfiles.Set( + ctx, + dev.MacSettingsProfileIds, + []string{"ids", "mac_settings", "end_devices_count"}, + func(_ context.Context, existing *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) { + if existing.EndDevicesCount > 0 { + existing.EndDevicesCount-- + } + return existing, []string{"ids", "mac_settings", "end_devices_count"}, nil + }) + if err != nil { + return nil, nil, err + } + } evt = evtDeleteEndDevice.NewWithIdentifiersAndData(ctx, req, nil) return nil, nil, nil }) @@ -1651,7 +1666,27 @@ func (srv *nsEndDeviceBatchRegistry) Delete( ); err != nil { return nil, err } - deleted, err := srv.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) + deleted, err := srv.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds, func(dev *ttnpb.EndDevice) error { + if dev == nil { + return errDeviceNotFound.New() + } + if dev.MacSettingsProfileIds != nil { + _, err := srv.macSettingsProfiles.Set( + ctx, + dev.MacSettingsProfileIds, + []string{"ids", "mac_settings", "end_devices_count"}, + func(_ context.Context, existing *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) { + if existing.EndDevicesCount > 0 { + existing.EndDevicesCount-- + } + return existing, []string{"ids", "mac_settings", "end_devices_count"}, nil + }) + if err != nil { + return err + } + } + return nil + }) if err != nil { logRegistryRPCError(ctx, err, "Failed to delete device from registry") return nil, err diff --git a/pkg/networkserver/grpc_deviceregistry_test.go b/pkg/networkserver/grpc_deviceregistry_test.go index c744417943..1564b1c8b9 100644 --- a/pkg/networkserver/grpc_deviceregistry_test.go +++ b/pkg/networkserver/grpc_deviceregistry_test.go @@ -1290,10 +1290,12 @@ func TestDeviceRegistryDelete(t *testing.T) { for _, tc := range []struct { Name string ContextFunc func(context.Context) context.Context - SetByIDFunc func(context.Context, *ttnpb.ApplicationIdentifiers, string, []string, func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) + SetByIDFunc func(context.Context, *ttnpb.ApplicationIdentifiers, string, []string, func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) // nolint: lll + SetFunc func(context.Context, *ttnpb.MACSettingsProfileIdentifiers, []string, func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error)) (*ttnpb.MACSettingsProfile, error) // nolint: lll Request *ttnpb.EndDeviceIdentifiers ErrorAssertion func(*testing.T, error) bool SetByIDCalls uint64 + SetCalls uint64 }{ { Name: "No device write rights", @@ -1308,7 +1310,13 @@ func TestDeviceRegistryDelete(t *testing.T) { }), }) }, - SetByIDFunc: func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, gets []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) { + SetByIDFunc: func( + ctx context.Context, + _ *ttnpb.ApplicationIdentifiers, + _ string, + _ []string, + _ func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) { err := errors.New("SetByIDFunc must not be called") test.MustTFromContext(ctx).Error(err) return nil, ctx, err @@ -1339,12 +1347,18 @@ func TestDeviceRegistryDelete(t *testing.T) { }), }) }, - SetByIDFunc: func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, gets []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) { + SetByIDFunc: func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, + gets []string, + f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) { t := test.MustTFromContext(ctx) a := assertions.New(t) a.So(appID, should.Resemble, &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}) a.So(devID, should.Equal, "test-dev-id") - a.So(gets, should.BeNil) + a.So(gets, should.Equal, []string{"mac_settings_profile_ids"}) dev, sets, err := f(ctx, nil) if !a.So(errors.IsNotFound(err), should.BeTrue) { @@ -1374,11 +1388,17 @@ func TestDeviceRegistryDelete(t *testing.T) { }), }) }, - SetByIDFunc: func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, gets []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) { + SetByIDFunc: func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, + gets []string, + f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) { a := assertions.New(test.MustTFromContext(ctx)) a.So(appID, should.Resemble, &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}) a.So(devID, should.Equal, "test-dev-id") - a.So(gets, should.BeNil) + a.So(gets, should.Equal, []string{"mac_settings_profile_ids"}) dev, sets, err := f(ctx, &ttnpb.EndDevice{ Ids: &ttnpb.EndDeviceIdentifiers{ @@ -1399,24 +1419,123 @@ func TestDeviceRegistryDelete(t *testing.T) { }, SetByIDCalls: 1, }, + + { + Name: "Existing device with MAC settings profile", + ContextFunc: func(ctx context.Context) context.Context { + return rights.NewContext(ctx, &rights.Rights{ + ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ + unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}): { + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + }, + }, + }), + }) + }, + SetByIDFunc: func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, + gets []string, + f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(appID, should.Resemble, &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}) + a.So(devID, should.Equal, "test-dev-id") + a.So(gets, should.Equal, []string{"mac_settings_profile_ids"}) + + dev, sets, err := f(ctx, &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + DeviceId: "test-dev-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }, + MacSettingsProfileIds: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }, + }) + if !a.So(err, should.BeNil) { + return nil, ctx, err + } + a.So(sets, should.BeNil) + a.So(dev, should.BeNil) + return nil, ctx, nil + }, + SetFunc: func( + ctx context.Context, + ids *ttnpb.MACSettingsProfileIdentifiers, + paths []string, + f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), + ) (*ttnpb.MACSettingsProfile, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(ids, should.Resemble, &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }) + a.So(paths, should.Equal, []string{"ids", "mac_settings", "end_devices_count"}) + profile, sets, err := f(ctx, &ttnpb.MACSettingsProfile{ + Ids: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }, + EndDevicesCount: 1, + }) + if !a.So(err, should.BeNil) { + return nil, err + } + a.So(sets, should.Equal, []string{"ids", "mac_settings", "end_devices_count"}) + a.So(profile, should.Resemble, &ttnpb.MACSettingsProfile{ + Ids: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }, + EndDevicesCount: 0, + }) + return profile, nil + }, + Request: &ttnpb.EndDeviceIdentifiers{ + DeviceId: "test-dev-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app-id"}, + }, + SetByIDCalls: 1, + SetCalls: 1, + }, } { tc := tc test.RunSubtest(t, test.SubtestConfig{ Name: tc.Name, Parallel: true, Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { - var setByIDCalls uint64 + var setByIDCalls, setCalls uint64 ns, ctx, env, stop := StartTest( ctx, TestConfig{ NetworkServer: Config{ Devices: &MockDeviceRegistry{ - SetByIDFunc: func(ctx context.Context, appID *ttnpb.ApplicationIdentifiers, devID string, gets []string, f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error)) (*ttnpb.EndDevice, context.Context, error) { + SetByIDFunc: func( + ctx context.Context, + appID *ttnpb.ApplicationIdentifiers, + devID string, + gets []string, + f func(context.Context, *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error), + ) (*ttnpb.EndDevice, context.Context, error) { atomic.AddUint64(&setByIDCalls, 1) return tc.SetByIDFunc(ctx, appID, devID, gets, f) }, }, + MACSettingsProfileRegistry: &MockMACSettingsProfileRegistry{ + SetFunc: func( + ctx context.Context, + ids *ttnpb.MACSettingsProfileIdentifiers, + paths []string, + f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), + ) (*ttnpb.MACSettingsProfile, error) { + atomic.AddUint64(&setCalls, 1) + return tc.SetFunc(ctx, ids, paths, f) + }, + }, }, TaskStarter: StartTaskExclude( DownlinkProcessTaskName, @@ -1436,6 +1555,7 @@ func TestDeviceRegistryDelete(t *testing.T) { req := ttnpb.Clone(tc.Request) res, err := ttnpb.NewNsEndDeviceRegistryClient(ns.LoopbackConn()).Delete(ctx, req) a.So(setByIDCalls, should.Equal, tc.SetByIDCalls) + a.So(setCalls, should.Equal, tc.SetCalls) if tc.ErrorAssertion != nil && a.So(tc.ErrorAssertion(t, err), should.BeTrue) { a.So(res, should.BeNil) } else if a.So(err, should.BeNil) { @@ -1476,6 +1596,12 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest KekLabel: "test", }, }, + MacSettingsProfileIds: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: registeredApplicationID, + }, + }, } dev2 := ttnpb.Clone(dev1) dev2.Ids.DeviceId = "test-device-2" @@ -1494,10 +1620,18 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) + SetFunc func( + context.Context, + *ttnpb.MACSettingsProfileIdentifiers, + []string, + func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), + ) (*ttnpb.MACSettingsProfile, error) Request *ttnpb.BatchDeleteEndDevicesRequest ErrorAssertion func(*testing.T, error) bool BatchDeleteCalls uint64 + SetCalls uint64 }{ { Name: "No device write rights", @@ -1516,6 +1650,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { err := errors.New("BatchDeleteFunc must not be called") test.MustTFromContext(ctx).Error(err) @@ -1555,6 +1690,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest BatchDeleteFunc: func(ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { // Devices not found are skipped. return nil, nil @@ -1594,6 +1730,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { err := errors.New("BatchDeleteFunc must not be called") test.MustTFromContext(ctx).Error(err) @@ -1632,6 +1769,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { err := errors.New("BatchDeleteFunc must not be called") test.MustTFromContext(ctx).Error(err) @@ -1664,6 +1802,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { a := assertions.New(test.MustTFromContext(ctx)) a.So(deviceIDs, should.HaveLength, 1) @@ -1698,6 +1837,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { a := assertions.New(test.MustTFromContext(ctx)) a.So(deviceIDs, should.HaveLength, 3) @@ -1746,6 +1886,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + _ func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { a := assertions.New(test.MustTFromContext(ctx)) a.So(appIDs, should.Resemble, registeredApplicationIDs) @@ -1776,6 +1917,92 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest }, BatchDeleteCalls: 1, }, + { + Name: "Valid Batch with MAC settings profile", + 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, + callback func(dev *ttnpb.EndDevice) error, + ) ([]*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: + err := callback(dev1) + a.So(err, should.BeNil) + case dev2.GetIds().DeviceId: + err := callback(dev2) + a.So(err, should.BeNil) + case dev3.GetIds().DeviceId: + err := callback(dev3) + a.So(err, should.BeNil) + default: + t.Error("Unknown device ID: ", devID) + } + } + return []*ttnpb.EndDeviceIdentifiers{ + dev1.Ids, + dev2.Ids, + dev3.Ids, + }, nil + }, + SetFunc: func( + ctx context.Context, + ids *ttnpb.MACSettingsProfileIdentifiers, + paths []string, + f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), + ) (*ttnpb.MACSettingsProfile, error) { + a := assertions.New(test.MustTFromContext(ctx)) + a.So(ids, should.Resemble, &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: registeredApplicationID}, + }) + a.So(paths, should.Equal, []string{"ids", "mac_settings", "end_devices_count"}) + profile, sets, err := f(ctx, &ttnpb.MACSettingsProfile{ + Ids: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: registeredApplicationID}, + }, + EndDevicesCount: 1, + }) + if !a.So(err, should.BeNil) { + return nil, err + } + a.So(sets, should.Equal, []string{"ids", "mac_settings", "end_devices_count"}) + a.So(profile, should.Resemble, &ttnpb.MACSettingsProfile{ + Ids: &ttnpb.MACSettingsProfileIdentifiers{ + ProfileId: "test-mac-settings-profile-id", + ApplicationIds: &ttnpb.ApplicationIdentifiers{ApplicationId: registeredApplicationID}, + }, + EndDevicesCount: 0, + }) + return profile, nil + }, + Request: &ttnpb.BatchDeleteEndDevicesRequest{ + ApplicationIds: registeredApplicationIDs, + DeviceIds: []string{ + dev1.Ids.DeviceId, + dev2.Ids.DeviceId, + dev3.Ids.DeviceId, + }, + }, + BatchDeleteCalls: 1, + SetCalls: 3, + }, } { tc := tc test.RunSubtest(t, test.SubtestConfig{ @@ -1783,7 +2010,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest Parallel: true, Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { t.Helper() - var batchDeleteCalls uint64 + var batchDeleteCalls, setCalls uint64 ns, ctx, env, stop := StartTest( ctx, TestConfig{ @@ -1793,9 +2020,21 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { atomic.AddUint64(&batchDeleteCalls, 1) - return tc.BatchDeleteFunc(ctx, appIDs, deviceIDs) + return tc.BatchDeleteFunc(ctx, appIDs, deviceIDs, callback) + }, + }, + MACSettingsProfileRegistry: &MockMACSettingsProfileRegistry{ + SetFunc: func( + ctx context.Context, + ids *ttnpb.MACSettingsProfileIdentifiers, + paths []string, + f func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error), + ) (*ttnpb.MACSettingsProfile, error) { + atomic.AddUint64(&setCalls, 1) + return tc.SetFunc(ctx, ids, paths, f) }, }, }, @@ -1819,6 +2058,7 @@ func TestDeviceRegistryBatchDelete(t *testing.T) { // nolint:paralleltest _, err := ttnpb.NewNsEndDeviceBatchRegistryClient(ns.LoopbackConn()).Delete(ctx, req) a.So(batchDeleteCalls, should.Equal, tc.BatchDeleteCalls) + a.So(setCalls, should.Equal, tc.SetCalls) if tc.ErrorAssertion != nil { a.So(tc.ErrorAssertion(t, err), should.BeTrue) } else { diff --git a/pkg/networkserver/internal/test/shared/device_registry.go b/pkg/networkserver/internal/test/shared/device_registry.go index 416e6d87a5..fb70285e67 100644 --- a/pkg/networkserver/internal/test/shared/device_registry.go +++ b/pkg/networkserver/internal/test/shared/device_registry.go @@ -500,6 +500,11 @@ func handleDeviceRegistryTest(ctx context.Context, reg DeviceRegistry) { // This unknown device will be ignored. "test-dev-4", }, + func(stored *ttnpb.EndDevice) error { + a.So(stored, should.NotBeNil) + a.So(stored.MacSettingsProfileIds, should.Resemble, macSettingsProfileID) + return nil + }, ) if !a.So(err, should.BeNil) { t.Fatalf("BatchDelete failed with: %s", errors.Stack(err)) diff --git a/pkg/networkserver/networkserver_util_internal_test.go b/pkg/networkserver/networkserver_util_internal_test.go index a586e9bb18..61f33680b4 100644 --- a/pkg/networkserver/networkserver_util_internal_test.go +++ b/pkg/networkserver/networkserver_util_internal_test.go @@ -2547,6 +2547,7 @@ type MockDeviceRegistry struct { ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) BatchSetByIDFunc func( ctx context.Context, @@ -2603,11 +2604,12 @@ func (m MockDeviceRegistry) BatchDelete( ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { if m.BatchDeleteFunc == nil { panic("BatchDeleteFunc called, but not set") } - return m.BatchDeleteFunc(ctx, appIDs, deviceIDs) + return m.BatchDeleteFunc(ctx, appIDs, deviceIDs, callback) } // BatchSetByID calls BatchSetByIDFunc if set and panics otherwise. diff --git a/pkg/networkserver/redis/registry.go b/pkg/networkserver/redis/registry.go index 5a198ba851..e09575bf78 100644 --- a/pkg/networkserver/redis/registry.go +++ b/pkg/networkserver/redis/registry.go @@ -1050,6 +1050,7 @@ func (r *DeviceRegistry) BatchDelete( ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { var ( uidKeys = make([]string, 0, len(deviceIDs)) @@ -1089,6 +1090,11 @@ func (r *DeviceRegistry) BatchDelete( log.FromContext(ctx).WithError(err).Warn("Failed to decode stored end device") continue } + if callback != nil { + if err := callback(dev); err != nil { + return err + } + } ret = append(ret, dev.Ids) uid := unique.ID(ctx, dev.GetIds()) if dev.Ids.JoinEui != nil && dev.Ids.DevEui != nil { diff --git a/pkg/networkserver/registry.go b/pkg/networkserver/registry.go index 14d32b5dad..893c488524 100644 --- a/pkg/networkserver/registry.go +++ b/pkg/networkserver/registry.go @@ -50,6 +50,7 @@ type DeviceRegistry interface { ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) BatchSetByID( ctx context.Context, @@ -154,8 +155,9 @@ func (w replacedEndDeviceFieldRegistryWrapper) BatchDelete( ctx context.Context, appIDs *ttnpb.ApplicationIdentifiers, deviceIDs []string, + callback func(dev *ttnpb.EndDevice) error, ) ([]*ttnpb.EndDeviceIdentifiers, error) { - return w.DeviceRegistry.BatchDelete(ctx, appIDs, deviceIDs) + return w.DeviceRegistry.BatchDelete(ctx, appIDs, deviceIDs, callback) } func (w replacedEndDeviceFieldRegistryWrapper) Range(ctx context.Context, paths []string, f func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDevice) bool) error {