Skip to content

Commit 311b592

Browse files
authored
feat: implement polling for NF configuration (#229)
* feat: implement polling for NF configuration Signed-off-by: gatici <[email protected]> --------- Signed-off-by: gatici <[email protected]>
1 parent 648d87d commit 311b592

20 files changed

+1178
-513
lines changed

README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# UDM
22
<!--
3+
SPDX-FileCopyrightText: 2025 Canonical Ltd
34
SPDX-FileCopyrightText: 2021 Open Networking Foundation <[email protected]>
45
Copyright 2019 free5GC.org
56
@@ -13,15 +14,36 @@ Implements 3gpp 29.503 specification. Provides service to AUSF, AMF, SMF and
1314
consumes service from UDR. UDM supports SBI interface and any other network
1415
function can use the service.
1516

17+
Compliance of the 5G Network functions can be found at [5G Compliance](https://docs.sd-core.opennetworking.org/main/overview/3gpp-compliance-5g.html)
18+
1619
## UDM block diagram
1720
![UDM Block Diagram](/docs/images/README-UDM.png)
1821

19-
## Upcoming changes
20-
- Subscription management callbacks to network functions.
22+
## Dynamic Network configuration (via webconsole)
2123

22-
Compliance of the 5G Network functions can be found at [5G Compliance](https://docs.sd-core.opennetworking.org/main/overview/3gpp-compliance-5g.html)
24+
UDM polls the webconsole every 5 seconds to fetch the latest PLMN configuration.
25+
26+
### Setting Up Polling
27+
28+
Include the `webuiUri` of the webconsole in the configuration file
29+
```
30+
configuration:
31+
...
32+
webuiUri: https://webui:5001 # or http://webui:5001
33+
...
34+
```
35+
The scheme (http:// or https://) must be explicitly specified. If no parameter is specified,
36+
UDM will use `http://webui:5001` by default.
37+
38+
### HTTPS Support
39+
40+
If the webconsole is served over HTTPS and uses a custom or self-signed certificate,
41+
you must install the root CA certificate into the trust store of the UDM environment.
42+
43+
Check the official guide for installing root CA certificates on Ubuntu:
44+
[Install a Root CA Certificate in the Trust Store](https://documentation.ubuntu.com/server/how-to/security/install-a-root-ca-certificate-in-the-trust-store/index.html)
2345

24-
## Reach out to us thorugh
46+
## Reach out to us through
2547

26-
1. #sdcore-dev channel in [ONF Community Slack](https://onf-community.slack.com/)
27-
2. Raise Github issues
48+
1. #sdcore-dev channel in [Aether Community Slack](https://aether5g-project.slack.com)
49+
2. Raise Github [issues](https://github.com/omec-project/udm/issues/new)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.5-dev
1+
2.0.0

consumer/nf_management.go

Lines changed: 74 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-FileCopyrightText: 2021 Open Networking Foundation <[email protected]>
22
// Copyright 2019 free5GC.org
3-
// SPDX-FileCopyrightText: 2024 Canonical Ltd.
3+
// SPDX-FileCopyrightText: 2025 Canonical Ltd.
44
// SPDX-License-Identifier: Apache-2.0
55
//
66

@@ -11,7 +11,6 @@ import (
1111
"fmt"
1212
"net/http"
1313
"strings"
14-
"time"
1514

1615
"github.com/omec-project/openapi"
1716
"github.com/omec-project/openapi/Nnrf_NFManagement"
@@ -20,76 +19,69 @@ import (
2019
"github.com/omec-project/udm/logger"
2120
)
2221

23-
func BuildNFInstance(udmContext *udmContext.UDMContext) (profile models.NfProfile, err error) {
22+
func getNfProfile(udmContext *udmContext.UDMContext, plmnConfig []models.PlmnId) (profile models.NfProfile, err error) {
23+
if udmContext == nil {
24+
return profile, fmt.Errorf("udm context has not been intialized. NF profile cannot be built")
25+
}
2426
profile.NfInstanceId = udmContext.NfId
25-
profile.NfStatus = models.NfStatus_REGISTERED
2627
profile.NfType = models.NfType_UDM
28+
profile.NfStatus = models.NfStatus_REGISTERED
29+
profile.Ipv4Addresses = append(profile.Ipv4Addresses, udmContext.RegisterIPv4)
2730
services := []models.NfService{}
28-
for _, nfservice := range udmContext.NfService {
29-
services = append(services, nfservice)
31+
for _, nfService := range udmContext.NfService {
32+
services = append(services, nfService)
3033
}
3134
if len(services) > 0 {
3235
profile.NfServices = &services
3336
}
34-
35-
var plmns []models.PlmnId
36-
for _, plmnItem := range udmContext.PlmnList {
37-
plmns = append(plmns, plmnItem.PlmnId)
38-
}
39-
if len(plmns) > 0 {
40-
profile.PlmnList = &plmns
41-
}
42-
4337
var udmInfo models.UdmInfo
38+
udmInfo.GroupId = udmContext.GroupId
4439
profile.UdmInfo = &udmInfo
45-
profile.UdmInfo.GroupId = udmContext.GroupId
46-
if udmContext.RegisterIPv4 == "" {
47-
err = fmt.Errorf("UDM Address is empty")
48-
return
40+
if len(plmnConfig) > 0 {
41+
plmnCopy := make([]models.PlmnId, len(plmnConfig))
42+
copy(plmnCopy, plmnConfig)
43+
profile.PlmnList = &plmnCopy
4944
}
50-
profile.Ipv4Addresses = append(profile.Ipv4Addresses, udmContext.RegisterIPv4)
51-
return
45+
return profile, nil
5246
}
5347

54-
var SendRegisterNFInstance = func(nrfUri, nfInstanceId string, profile models.NfProfile) (prof models.NfProfile, resourceNrfUri string,
55-
retrieveNfInstanceId string, err error,
56-
) {
48+
var SendRegisterNFInstance = func(plmnConfig []models.PlmnId) (prof models.NfProfile, resourceNrfUri string, err error) {
49+
self := udmContext.UDM_Self()
50+
nfProfile, err := getNfProfile(self, plmnConfig)
51+
if err != nil {
52+
return models.NfProfile{}, "", err
53+
}
54+
5755
configuration := Nnrf_NFManagement.NewConfiguration()
58-
configuration.SetBasePath(nrfUri)
56+
configuration.SetBasePath(self.NrfUri)
5957
client := Nnrf_NFManagement.NewAPIClient(configuration)
58+
receivedNfProfile, res, err := client.NFInstanceIDDocumentApi.RegisterNFInstance(context.TODO(), nfProfile.NfInstanceId, nfProfile)
59+
logger.ConsumerLog.Debugf("RegisterNFInstance done using profile: %+v", nfProfile)
6060

61-
var res *http.Response
62-
for {
63-
prof, res, err = client.NFInstanceIDDocumentApi.RegisterNFInstance(context.TODO(), nfInstanceId, profile)
64-
if err != nil || res == nil {
65-
logger.ConsumerLog.Errorf("UDM register to NRF Error[%v]", err.Error())
66-
time.Sleep(2 * time.Second)
67-
continue
68-
}
69-
defer func() {
70-
if rspCloseErr := res.Body.Close(); rspCloseErr != nil {
71-
logger.ConsumerLog.Errorf("GetIdentityData response body cannot close: %+v", rspCloseErr)
72-
}
73-
}()
61+
if err != nil {
62+
return models.NfProfile{}, "", err
63+
}
64+
if res == nil {
65+
return models.NfProfile{}, "", fmt.Errorf("no response from server")
66+
}
7467

75-
status := res.StatusCode
76-
if status == http.StatusOK {
77-
// NFUpdate
78-
break
79-
} else if status == http.StatusCreated {
80-
// NFRegister
81-
resourceUri := res.Header.Get("Location")
82-
resourceNrfUri = resourceUri[:strings.Index(resourceUri, "/nnrf-nfm/")]
83-
retrieveNfInstanceId = resourceUri[strings.LastIndex(resourceUri, "/")+1:]
84-
break
85-
} else {
86-
logger.ConsumerLog.Errorf("NRF returned wrong status code: %+v", status)
87-
}
68+
switch res.StatusCode {
69+
case http.StatusOK: // NFUpdate
70+
logger.ConsumerLog.Debugln("UDM NF profile updated with complete replacement")
71+
return receivedNfProfile, "", nil
72+
case http.StatusCreated: // NFRegister
73+
resourceUri := res.Header.Get("Location")
74+
resourceNrfUri = resourceUri[:strings.Index(resourceUri, "/nnrf-nfm/")]
75+
retrieveNfInstanceId := resourceUri[strings.LastIndex(resourceUri, "/")+1:]
76+
self.NfId = retrieveNfInstanceId
77+
logger.ConsumerLog.Debugln("UDM NF profile registered to the NRF")
78+
return receivedNfProfile, resourceNrfUri, nil
79+
default:
80+
return receivedNfProfile, "", fmt.Errorf("unexpected status code returned by the NRF %d", res.StatusCode)
8881
}
89-
return prof, resourceNrfUri, retrieveNfInstanceId, err
9082
}
9183

92-
func SendDeregisterNFInstance() (problemDetails *models.ProblemDetails, err error) {
84+
var SendDeregisterNFInstance = func() error {
9385
logger.ConsumerLog.Infoln("send Deregister NFInstance")
9486

9587
udmSelf := udmContext.UDM_Self()
@@ -98,30 +90,20 @@ func SendDeregisterNFInstance() (problemDetails *models.ProblemDetails, err erro
9890
configuration.SetBasePath(udmSelf.NrfUri)
9991
client := Nnrf_NFManagement.NewAPIClient(configuration)
10092

101-
var res *http.Response
102-
103-
res, err = client.NFInstanceIDDocumentApi.DeregisterNFInstance(context.Background(), udmSelf.NfId)
104-
if err == nil {
105-
return
106-
} else if res != nil {
107-
defer func() {
108-
if rspCloseErr := res.Body.Close(); rspCloseErr != nil {
109-
logger.ConsumerLog.Errorf("DeregisterNFInstance response body cannot close: %+v", rspCloseErr)
110-
}
111-
}()
112-
113-
if res.Status != err.Error() {
114-
return
115-
}
116-
problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails)
117-
problemDetails = &problem
118-
} else {
119-
err = openapi.ReportError("server no response")
93+
res, err := client.NFInstanceIDDocumentApi.DeregisterNFInstance(context.Background(), udmSelf.NfId)
94+
if err != nil {
95+
return err
12096
}
121-
return
97+
if res == nil {
98+
return fmt.Errorf("no response from server")
99+
}
100+
if res.StatusCode == http.StatusNoContent {
101+
return nil
102+
}
103+
return fmt.Errorf("unexpected response code")
122104
}
123105

124-
var SendUpdateNFInstance = func(patchItem []models.PatchItem) (nfProfile models.NfProfile, problemDetails *models.ProblemDetails, err error) {
106+
var SendUpdateNFInstance = func(patchItem []models.PatchItem) (receivedNfProfile models.NfProfile, problemDetails *models.ProblemDetails, err error) {
125107
logger.ConsumerLog.Debugln("send Update NFInstance")
126108

127109
udmSelf := udmContext.UDM_Self()
@@ -130,25 +112,25 @@ var SendUpdateNFInstance = func(patchItem []models.PatchItem) (nfProfile models.
130112
client := Nnrf_NFManagement.NewAPIClient(configuration)
131113

132114
var res *http.Response
133-
nfProfile, res, err = client.NFInstanceIDDocumentApi.UpdateNFInstance(context.Background(), udmSelf.NfId, patchItem)
134-
if err == nil {
135-
return
136-
} else if res != nil {
137-
defer func() {
138-
if resCloseErr := res.Body.Close(); resCloseErr != nil {
139-
logger.ConsumerLog.Errorf("UpdateNFInstance response cannot close: %+v", resCloseErr)
115+
receivedNfProfile, res, err = client.NFInstanceIDDocumentApi.UpdateNFInstance(context.Background(), udmSelf.NfId, patchItem)
116+
if err != nil {
117+
if openapiErr, ok := err.(openapi.GenericOpenAPIError); ok {
118+
if model := openapiErr.Model(); model != nil {
119+
if problem, ok := model.(models.ProblemDetails); ok {
120+
return models.NfProfile{}, &problem, nil
121+
}
140122
}
141-
}()
142-
if res.Status != err.Error() {
143-
logger.ConsumerLog.Errorf("UpdateNFInstance received error response: %v", res.Status)
144-
return
145123
}
146-
problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails)
147-
problemDetails = &problem
148-
} else {
149-
err = openapi.ReportError("server no response")
124+
return models.NfProfile{}, nil, err
150125
}
151-
return
126+
127+
if res == nil {
128+
return models.NfProfile{}, nil, fmt.Errorf("no response from server")
129+
}
130+
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusNoContent {
131+
return receivedNfProfile, nil, nil
132+
}
133+
return models.NfProfile{}, nil, fmt.Errorf("unexpected response code")
152134
}
153135

154136
func SendCreateSubscription(nrfUri string, nrfSubscriptionData models.NrfSubscriptionData) (nrfSubData models.NrfSubscriptionData, problemDetails *models.ProblemDetails, err error) {
@@ -176,7 +158,7 @@ func SendCreateSubscription(nrfUri string, nrfSubscriptionData models.NrfSubscri
176158
problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails)
177159
problemDetails = &problem
178160
} else {
179-
err = openapi.ReportError("server no response")
161+
err = fmt.Errorf("server no response")
180162
}
181163
return
182164
}
@@ -206,7 +188,7 @@ func SendRemoveSubscription(subscriptionId string) (problemDetails *models.Probl
206188
problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails)
207189
problemDetails = &problem
208190
} else {
209-
err = openapi.ReportError("server no response")
191+
err = fmt.Errorf("server no response")
210192
}
211193
return
212194
}

consumer/nf_management_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2023 Open Networking Foundation <[email protected]>
3+
package consumer
4+
5+
import (
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
12+
"github.com/omec-project/openapi/models"
13+
udmContext "github.com/omec-project/udm/context"
14+
"github.com/omec-project/udm/factory"
15+
)
16+
17+
func Test_nf_id_updated_and_nrf_url_is_not_overwritten_when_registering(t *testing.T) {
18+
svr := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "/nnrf-nfm/v1/nf-instances/") {
20+
w.Header().Set("Location", fmt.Sprintf("%s/nnrf-nfm/v1/nf-instances/mocked-id", r.Host))
21+
w.WriteHeader(http.StatusCreated)
22+
} else {
23+
t.Errorf("Unexpected request: %s %s", r.Method, r.URL.Path)
24+
http.Error(w, "Not Found", http.StatusNotFound)
25+
}
26+
}))
27+
svr.EnableHTTP2 = true
28+
svr.StartTLS()
29+
defer svr.Close()
30+
if err := factory.InitConfigFactory("../factory/udmcfg.yaml"); err != nil {
31+
t.Fatalf("Could not read example configuration file")
32+
}
33+
self := udmContext.UDM_Self()
34+
self.NrfUri = svr.URL
35+
self.RegisterIPv4 = "127.0.0.2"
36+
37+
_, _, err := SendRegisterNFInstance([]models.PlmnId{{Mcc: "123", Mnc: "45"}})
38+
if err != nil {
39+
t.Errorf("Got and error %+v", err)
40+
}
41+
if self.NfId != "mocked-id" {
42+
t.Errorf("Expected NfId to be 'mocked-id', got %v", self.NfId)
43+
}
44+
if self.NrfUri != svr.URL {
45+
t.Errorf("Expected NRF URL to stay %s, but was %s", svr.URL, self.NrfUri)
46+
}
47+
}

context/context.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/omec-project/openapi"
1818
"github.com/omec-project/openapi/Nnrf_NFDiscovery"
1919
"github.com/omec-project/openapi/models"
20-
"github.com/omec-project/udm/factory"
2120
"github.com/omec-project/util/idgenerator"
2221
"github.com/omec-project/util/util_3gpp/suci"
2322
)
@@ -54,7 +53,6 @@ type UDMContext struct {
5453
NfStatusSubscriptions sync.Map // map[NfInstanceID]models.NrfSubscriptionData.SubscriptionId
5554
SuciProfiles []suci.SuciProfile
5655
EeSubscriptionIDGenerator *idgenerator.IDGenerator
57-
PlmnList []factory.PlmnSupportItem
5856
SBIPort int
5957
EnableNrfCaching bool
6058
NrfCacheEvictionInterval time.Duration

factory/config.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
package factory
1212

1313
import (
14-
"github.com/omec-project/openapi/models"
1514
"github.com/omec-project/util/logger"
1615
)
1716

@@ -38,16 +37,14 @@ const (
3837
)
3938

4039
type Configuration struct {
41-
UdmName string `yaml:"udmName,omitempty"`
42-
Sbi *Sbi `yaml:"sbi,omitempty"`
43-
ServiceList []string `yaml:"serviceList,omitempty"`
44-
NrfUri string `yaml:"nrfUri,omitempty"`
45-
WebuiUri string `yaml:"webuiUri"`
46-
Keys *Keys `yaml:"keys,omitempty"`
47-
PlmnSupportList []models.PlmnId `yaml:"plmnSupportList,omitempty"`
48-
PlmnList []PlmnSupportItem `yaml:"plmnList,omitempty"`
49-
EnableNrfCaching bool `yaml:"enableNrfCaching"`
50-
NrfCacheEvictionInterval int `yaml:"nrfCacheEvictionInterval,omitempty"`
40+
UdmName string `yaml:"udmName,omitempty"`
41+
Sbi *Sbi `yaml:"sbi,omitempty"`
42+
ServiceList []string `yaml:"serviceList,omitempty"`
43+
NrfUri string `yaml:"nrfUri,omitempty"`
44+
WebuiUri string `yaml:"webuiUri"`
45+
Keys *Keys `yaml:"keys,omitempty"`
46+
EnableNrfCaching bool `yaml:"enableNrfCaching"`
47+
NrfCacheEvictionInterval int `yaml:"nrfCacheEvictionInterval,omitempty"`
5148
}
5249

5350
type Sbi struct {
@@ -72,10 +69,6 @@ type Keys struct {
7269
UdmProfileBHNPublicKey string `yaml:"udmProfileBHNPublicKey,omitempty"`
7370
}
7471

75-
type PlmnSupportItem struct {
76-
PlmnId models.PlmnId `yaml:"plmnId"`
77-
}
78-
7972
func (c *Config) GetVersion() string {
8073
if c.Info != nil && c.Info.Version != "" {
8174
return c.Info.Version

0 commit comments

Comments
 (0)