Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: propagate Subscriber deletion to Device Groups #299

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

dariofaccin
Copy link

@dariofaccin dariofaccin commented Feb 4, 2025

Issue

When we DELETE a subscriber, information is removed from subscriptionData.provisionedData.amData and subscriptionData.authenticationData.authenticationSubscription. However, if a device group contains a reference to this subscriber in imsis list, the reference is not removed.

Description

This PR propagates the DELETE subscriber operation to all the device groups that contains the deletedsubscriber. This way, the removal of the subscriber is done in a single operation, no need to manually update the device groups to remove the dangling reference anymore.
This PR improves the response when the list of subscribers is empty: now it returns an empty list ([]) instead of null.
This PR also improves the response in case of subscriber deletion failure: now it returns 500 Internal Server Error.

How the code works

Webconsole behavior on Subscriber POST before this PR

When a subscriber is created (POST), it creates entries in:

subscriptionData.authenticationData.authenticationSubscription
subscriptionData.provisionedData.amData

Webconsole behavior on Subscriber DELETE before this PR

When a subscriber is deleted (DELETE) , it removes entries for each subscriber in:

subscriptionData.authenticationData.authenticationSubscription
subscriptionData.provisionedData.amData

This PR

  • retrieves all the device groups where the subscriber to be deleted is present
  • creates a new (in memory) configuration for all the device groups on previous point, where the subscriber is removed from imsis
  • sends a message in the config channel for the subscriber removal
  • sends a message in the config channel for the device groups update

How to reproduce

  1. Create a subscriber, a device group containing such subscriber and a network slice containing such device group
  2. Check the DB entries for that device group in webconsoleData.snapshots.devGroupData (imsis list)
  3. Delete the subscriber

What you should see

  1. The subscriber reference is removed from the device group(s) imsis list
  2. The subscriber information is removed from subscriptionData.authenticationData.authenticationSubscription
  3. The subscriber information with the PLMN belonging to the modified device groups is removed from:
    subscriptionData.provisionedData.amData
    subscriptionData.provisionedData.smData
    subscriptionData.provisionedData.smfSelectionSubscriptionData
    policyData.ues.amData
    policyData.ues.smData

{
name: "Create a new subscriber success",
route: "/api/subscriber/imsi-208930100007487",
inputData: `{"UeId":"208930100007487", "plmnId":"12345", "opc":"981d464c7c52eb6e5036234984ad0bcf","key":"5122250214c33e723a5dd523fc145fc0", "sequenceNumber":"16f3b3f70fc2"}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
inputData: `{"UeId":"208930100007487", "plmnId":"12345", "opc":"981d464c7c52eb6e5036234984ad0bcf","key":"5122250214c33e723a5dd523fc145fc0", "sequenceNumber":"16f3b3f70fc2"}`,
inputData: `{"plmnID":"12345", "opc":"981d464c7c52eb6e5036234984ad0bcf","key":"5122250214c33e723a5dd523fc145fc0", "sequenceNumber":"16f3b3f70fc2"}`,

Comment on lines 129 to 130
expectedCode int
expectedBody string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectedCode int
expectedBody string

MsgMethod: configmodels.Post_op,
AuthSubData: &models.AuthenticationSubscription{
AuthenticationManagementField: "8000",
AuthenticationMethod: "5G_AKA", // "5G_AKA", "EAP_AKA_PRIME"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AuthenticationMethod: "5G_AKA", // "5G_AKA", "EAP_AKA_PRIME"
AuthenticationMethod: "5G_AKA",

Op: &models.Op{
EncryptionAlgorithm: 0,
EncryptionKey: 0,
OpValue: "", // Required
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace OpValue with real expected value and remove comment

Opc: &models.Opc{
EncryptionAlgorithm: 0,
EncryptionKey: 0,
// OpcValue: "8e27b6af0e692e750f32667a3b14605d", // Required
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace OpcValue with real expected value and remove comment

Comment on lines 268 to 275
if tc.expectedMessage.AuthSubData != nil {
if msg.AuthSubData == nil {
t.Errorf("expected AuthSubData %+v, but got nil", tc.expectedMessage.AuthSubData)
}
if tc.expectedMessage.Imsi != msg.Imsi {
t.Errorf("expected IMSI %+v, but got %+v", tc.expectedMessage.Imsi, msg.Imsi)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if tc.expectedMessage.AuthSubData != nil {
if msg.AuthSubData == nil {
t.Errorf("expected AuthSubData %+v, but got nil", tc.expectedMessage.AuthSubData)
}
if tc.expectedMessage.Imsi != msg.Imsi {
t.Errorf("expected IMSI %+v, but got %+v", tc.expectedMessage.Imsi, msg.Imsi)
}
}
if tc.expectedMessage.Imsi != msg.Imsi {
t.Errorf("expected IMSI %+v, but got %+v", tc.expectedMessage.Imsi, msg.Imsi)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise you are not really checking the imsi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also add a check to verify that the AuthSubData in the received message is nil. Or maybe use reflect.DeepEqual to check the whole message in one step instead of checking each field

}
}

func TestSubscriberDeleteHandlers(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test in which a Subscriber belongs to a DG is missing

rawDeviceGroups, err := dbadapter.CommonDBClient.RestfulAPIGetMany(devGroupDataColl, filterByImsi)
if err != nil {
logger.WebUILog.Errorf("failed to fetch device groups: %w", err)
return

This comment was marked as outdated.

var deviceGroup configmodels.DeviceGroups
if err = json.Unmarshal(configmodels.MapToByte(rawDeviceGroup), &deviceGroup); err != nil {
logger.WebUILog.Errorf("error unmarshaling device group: %v", err)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return
continue

}
deviceGroupUpdateMessages = append(deviceGroupUpdateMessages, &deviceGroupUpdateMessage)
}
for _, msg := range deviceGroupUpdateMessages {

This comment was marked as outdated.

@patriciareinoso
Copy link
Contributor

Please add to the description the fact that you are changing the behavior of the getter to return [ ] instead of null

{
name: "Create a new subscriber success",
route: "/api/subscriber/imsi-208930100007487",
inputData: `{"plmnId":"12345", "opc":"8e27b6af0e692e750f32667a3b14605d","key":"8baf473f2f8fd09487cccbd7097c6862", "sequenceNumber":"16f3b3f70fc2"}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
inputData: `{"plmnId":"12345", "opc":"8e27b6af0e692e750f32667a3b14605d","key":"8baf473f2f8fd09487cccbd7097c6862", "sequenceNumber":"16f3b3f70fc2"}`,
inputData: `{"plmnID":"12345", "opc":"8e27b6af0e692e750f32667a3b14605d","key":"8baf473f2f8fd09487cccbd7097c6862", "sequenceNumber":"16f3b3f70fc2"}`,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the json representation is plmnID. This does not affect the test but if in the future if someone wants to do something with the plmnID, this subtlety can be a struggling point.

Comment on lines 294 to 300
if tc.expectedMessage.AuthSubData != nil {
if msg.AuthSubData == nil {
t.Errorf("expected AuthSubData %+v, but got nil", tc.expectedMessage.AuthSubData)
}
if tc.expectedMessage.Imsi != msg.Imsi {
t.Errorf("expected IMSI %+v, but got %+v", tc.expectedMessage.Imsi, msg.Imsi)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part of code is never executed because tc.expectedMessage.AuthSubData != nil condition is never true.

it's not recommended to have complicated logic in the checks.
We need to test that in the first tc only one message is sent, and in the second tc we need to check that 2 messages are sent, if the logic becomes complicated it's better to separate en 2 tests.
Complicated logic in tests is error prone, and hard to read

}

func deviceGroupWithImsis(name string, imsis []string) configmodels.DeviceGroups {
traffic_class := configmodels.TrafficClassInfo{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
traffic_class := configmodels.TrafficClassInfo{
trafficClass := configmodels.TrafficClassInfo{

DnnMbrUplink: 10000000,
DnnMbrDownlink: 10000000,
BitrateUnit: "kbps",
TrafficClass: &traffic_class,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
TrafficClass: &traffic_class,
TrafficClass: &trafficClass,

BitrateUnit: "kbps",
TrafficClass: &traffic_class,
}
ipdomain := configmodels.DeviceGroupsIpDomainExpanded{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ipdomain := configmodels.DeviceGroupsIpDomainExpanded{
ipDomain := configmodels.DeviceGroupsIpDomainExpanded{

Imsis: imsis,
SiteInfo: "demo",
IpDomainName: "pool1",
IpDomainExpanded: ipdomain,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IpDomainExpanded: ipdomain,
IpDomainExpanded: ipDomain,

@@ -0,0 +1,354 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Canonical Ltd.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2024 Canonical Ltd.
// Copyright 2025 Canonical Ltd.

Signed-off-by: Dario Faccin <[email protected]>
}
default:
t.Error("expected message in configChannel, but none received")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check over the second message is missing

default:
t.Error("expected message in configChannel, but none received")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can check that only one message is sent: read again from the channel and if it does not go to the default clause, it should raise an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to try to read message again from the channel and msg should not exist.

filterByImsi := bson.M{
"imsis": imsi,
}
rawDeviceGroups, err := dbadapter.CommonDBClient.RestfulAPIGetMany(devGroupDataColl, filterByImsi)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In RestfulAPIGetMany var resultArray []map[string]interface{} initial value is nil. According to cur, err := collection.Find(ctx, filter), if filter does not match it can stay as nil and it can return rawDeviceGroups as nil and err as nil.
In line 509, as we only check the err is nil or not, execution continue.
On line 513, iterating over rawDeviceGroups which is nil may cause panic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial value of var resultArray []map[string]interface{} is not nil but []. Actually, there is a test (TestSubscriberDeleteNoDeviceGroup) where the IMSI to be deleted does not belong to any device group and it does not panic. From the debugger: []map[string]interface {}=[]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. Please discard this comment.

for _, rawDeviceGroup := range rawDeviceGroups {
var deviceGroup configmodels.DeviceGroups
if err = json.Unmarshal(configmodels.MapToByte(rawDeviceGroup), &deviceGroup); err != nil {
logger.WebUILog.Errorf("error unmarshaling device group: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can improve logging here by improving device group name.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point we are not sure that the DeviceGroupName field is available in the object we are trying to unmarshal: logging the raw object could simplify the debugging operations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

@@ -485,6 +489,9 @@ func DeleteSubscriberByID(c *gin.Context) {

c.JSON(http.StatusNoContent, gin.H{})

imsi := strings.TrimPrefix(ueId, "imsi-")
updateSubscriberInDeviceGroups(imsi)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to line 490, before doing any operation, DeleteSubscriberByID function send HTTP 204 no content response, then it calls updateSubscriberInDeviceGroups(imsi) method. I saw two problems here:

  1. Order: response is sent without checking the success of device group update
  2. Error propagation: updateSubscriberInDeviceGroups log the errors but does not propagate them to caller method. The caller always report a successful operation regardless from what happened in the updateSubscriberInDeviceGroups.

Comment on lines +505 to +506
filterByImsi := bson.M{
"imsis": imsi,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to validate the existence of imsi here.

@gab-arrobo
Copy link
Contributor

gab-arrobo commented Feb 12, 2025

@dariofaccin, you need to sign every commit you make (-s), otherwise, the DCO will fail

@dariofaccin dariofaccin force-pushed the TELCO-1556-propagate-subscriber-deletion branch from dcd98c9 to fe11864 Compare February 13, 2025 08:13
Copy link
Contributor

@gatici gatici left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

default:
t.Error("expected message in configChannel, but none received")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to try to read message again from the channel and msg should not exist.

Comment on lines +312 to +313
dbAdapter := &MockMongoClientDeviceGroupsWithSubscriber{}
dbadapter.CommonDBClient = dbAdapter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking, we are accessing the dbadapter.CommonDBClient and configChannel global variables and changing their states in the tests. I saw some tests doing it but some are not. All the tests that accessing those global variables need to save their original states and after test, we need to restore them.
For example:

// Save original state
origDBClient := dbadapter.CommonDBClient
origConfigChannel := configChannel

// Restore them after the test finishes
defer func() {
	dbadapter.CommonDBClient = origDBClient
	configChannel = origConfigChannel
}()

// Set up a mock database adapter
dbAdapter := &MockMongoClientDeviceGroupsWithSubscriber{}
dbadapter.CommonDBClient = dbAdapter

// Create a new llocal config channel for the test
configChannel = make(chan *configmodels.ConfigMessage, 2)

...

@dariofaccin dariofaccin requested a review from gatici February 13, 2025 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants