diff --git a/conf/upf.json b/conf/upf.json index b0f609fba..3a0c65fc4 100644 --- a/conf/upf.json +++ b/conf/upf.json @@ -1,8 +1,9 @@ { - "": "Vdev or sim support. Enable `\"mode\": \"af_xdp\"` to enable AF_XDP mode, or `\"mode\": \"af_packet\"` to enable AF_PACKET mode, or `\"mode\": \"sim\"` to generate synthetic traffic from BESS's Source module", + "": "Vdev or sim support. Enable `\"mode\": \"af_xdp\"` to enable AF_XDP mode, or `\"mode\": \"af_packet\"` to enable AF_PACKET mode, `\"mode\": \"sim\"` to generate synthetic traffic from BESS's Source module or \"mode\": \"\" when running with UP4", "": "mode: af_xdp", "": "mode: af_packet", "": "mode: sim", + "mode": "dpdk", "table_sizes": { "": "Example sizes based on sim mode and 50K sessions. Customize as per your control plane", @@ -145,9 +146,8 @@ "": "p4rtc interface settings", "p4rtciface": { - "access_ip": "172.17.0.1/32", - "p4rtc_server": "onos", - "p4rtc_port": "51001", - "ue_ip_pool": "10.250.0.0/24" + "access_ip": "172.17.0.1/32", + "p4rtc_server": "onos", + "p4rtc_port": "51001" } } diff --git a/pfcpiface/config.go b/pfcpiface/config.go new file mode 100644 index 000000000..b12b2bcd7 --- /dev/null +++ b/pfcpiface/config.go @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022-present Open Networking Foundation + +package main + +import ( + log "github.com/sirupsen/logrus" + + "net" + "time" + + "encoding/json" + "io/ioutil" + "os" +) + +// Conf : Json conf struct. +type Conf struct { + Mode string `json:"mode"` + AccessIface IfaceType `json:"access"` + CoreIface IfaceType `json:"core"` + CPIface CPIfaceInfo `json:"cpiface"` + P4rtcIface P4rtcInfo `json:"p4rtciface"` + EnableP4rt bool `json:"enable_p4rt"` + EnableFlowMeasure bool `json:"measure_flow"` + SimInfo SimModeInfo `json:"sim"` + ConnTimeout uint32 `json:"conn_timeout"` // TODO(max): unused, remove + ReadTimeout uint32 `json:"read_timeout"` // TODO(max): convert to duration string + EnableNotifyBess bool `json:"enable_notify_bess"` + EnableEndMarker bool `json:"enable_end_marker"` + NotifySockAddr string `json:"notify_sockaddr"` + EndMarkerSockAddr string `json:"endmarker_sockaddr"` + LogLevel log.Level `json:"log_level"` + QciQosConfig []QciQosConfig `json:"qci_qos_config"` + SliceMeterConfig SliceMeterConfig `json:"slice_rate_limit_config"` + MaxReqRetries uint8 `json:"max_req_retries"` + RespTimeout string `json:"resp_timeout"` + EnableHBTimer bool `json:"enable_hbTimer"` + HeartBeatInterval string `json:"heart_beat_interval"` +} + +// QciQosConfig : Qos configured attributes. +type QciQosConfig struct { + QCI uint8 `json:"qci"` + CBS uint32 `json:"cbs"` + PBS uint32 `json:"pbs"` + EBS uint32 `json:"ebs"` + BurstDurationMs uint32 `json:"burst_duration_ms"` + SchedulingPriority uint32 `json:"priority"` +} + +type SliceMeterConfig struct { + N6RateBps uint64 `json:"n6_bps"` + N6BurstBytes uint64 `json:"n6_burst_bytes"` + N3RateBps uint64 `json:"n3_bps"` + N3BurstBytes uint64 `json:"n3_burst_bytes"` +} + +// SimModeInfo : Sim mode attributes. +type SimModeInfo struct { + MaxSessions uint32 `json:"max_sessions"` + StartUEIP net.IP `json:"start_ue_ip"` + StartENBIP net.IP `json:"start_enb_ip"` + StartAUPFIP net.IP `json:"start_aupf_ip"` + N6AppIP net.IP `json:"n6_app_ip"` + N9AppIP net.IP `json:"n9_app_ip"` + StartN3TEID string `json:"start_n3_teid"` + StartN9TEID string `json:"start_n9_teid"` +} + +// CPIfaceInfo : CPIface interface settings. +type CPIfaceInfo struct { + Peers []string `json:"peers"` + UseFQDN bool `json:"use_fqdn"` + NodeID string `json:"hostname"` + HTTPPort string `json:"http_port"` + Dnn string `json:"dnn"` + EnableUeIPAlloc bool `json:"enable_ue_ip_alloc"` + UEIPPool string `json:"ue_ip_pool"` +} + +// IfaceType : Gateway interface struct. +type IfaceType struct { + IfName string `json:"ifname"` +} + +// P4rtcInfo : P4 runtime interface settings. +type P4rtcInfo struct { + AccessIP string `json:"access_ip"` + P4rtcServer string `json:"p4rtc_server"` + P4rtcPort string `json:"p4rtc_port"` +} + +// validateConf checks that the given config reaches a baseline of correctness. +func validateConf(conf Conf) error { + if conf.EnableP4rt { + _, _, err := net.ParseCIDR(conf.P4rtcIface.AccessIP) + if err != nil { + return ErrInvalidArgumentWithReason("conf.P4rtcIface.AccessIP", conf.P4rtcIface.AccessIP, err.Error()) + } + + if !conf.CPIface.EnableUeIPAlloc { + return ErrInvalidArgumentWithReason("conf.EnableUeIPAlloc", + conf.CPIface.EnableUeIPAlloc, "UE IP pool allocation must be enabled in P4RT mode") + } + + if conf.Mode != "" { + return ErrInvalidArgumentWithReason("conf.Mode", conf.Mode, "mode must not be set for UP4") + } + } else { + // Mode is only relevant in a BESS deployment. + validModes := map[string]struct{}{ + "af_xdp": {}, + "af_packet": {}, + "dpdk": {}, + "sim": {}, + } + if _, ok := validModes[conf.Mode]; !ok { + return ErrInvalidArgumentWithReason("conf.Mode", conf.Mode, "invalid mode") + } + } + + if conf.CPIface.EnableUeIPAlloc { + _, _, err := net.ParseCIDR(conf.CPIface.UEIPPool) + if err != nil { + return ErrInvalidArgumentWithReason("conf.UEIPPool", conf.CPIface.UEIPPool, err.Error()) + } + } + + for _, peer := range conf.CPIface.Peers { + ip := net.ParseIP(peer) + if ip == nil { + return ErrInvalidArgumentWithReason("conf.CPIface.Peers", peer, "invalid IP") + } + } + + if _, err := time.ParseDuration(conf.RespTimeout); err != nil { + return ErrInvalidArgumentWithReason("conf.RespTimeout", conf.RespTimeout, "invalid duration") + } + + if conf.ReadTimeout == 0 { + return ErrInvalidArgumentWithReason("conf.ReadTimeout", conf.ReadTimeout, "invalid duration") + } + + if conf.MaxReqRetries == 0 { + return ErrInvalidArgumentWithReason("conf.MaxReqRetries", conf.MaxReqRetries, "invalid number of retries") + } + + if conf.EnableHBTimer { + if _, err := time.ParseDuration(conf.HeartBeatInterval); err != nil { + return err + } + } + + return nil +} + +// LoadConfigFile : parse json file and populate corresponding struct. +func LoadConfigFile(filepath string) (Conf, error) { + // Open up file. + jsonFile, err := os.Open(filepath) + if err != nil { + return Conf{}, err + } + defer jsonFile.Close() + + // Read our file into memory. + byteValue, err := ioutil.ReadAll(jsonFile) + if err != nil { + return Conf{}, err + } + + var conf Conf + + err = json.Unmarshal(byteValue, &conf) + if err != nil { + return Conf{}, err + } + + // Set defaults, when missing. + if conf.RespTimeout == "" { + conf.RespTimeout = respTimeoutDefault.String() + } + + if conf.ReadTimeout == 0 { + conf.ReadTimeout = uint32(readTimeoutDefault.Seconds()) + } + + if conf.MaxReqRetries == 0 { + conf.MaxReqRetries = maxReqRetriesDefault + } + + // Perform basic validation. + err = validateConf(conf) + if err != nil { + return Conf{}, err + } + + return conf, nil +} diff --git a/pfcpiface/config_test.go b/pfcpiface/config_test.go new file mode 100644 index 000000000..d72034988 --- /dev/null +++ b/pfcpiface/config_test.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022-present Open Networking Foundation + +package main + +import ( + "io/fs" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mustWriteStringToDisk(s string, path string) { + err := ioutil.WriteFile(path, []byte(s), fs.ModePerm) + if err != nil { + panic(err) + } +} + +func TestLoadConfigFile(t *testing.T) { + t.Run("sample config is valid", func(t *testing.T) { + s := `{ + "mode": "dpdk", + "log_level": "info", + "workers": 1, + "max_sessions": 50000, + "table_sizes": { + "pdrLookup": 50000, + "appQERLookup": 200000, + "sessionQERLookup": 100000, + "farLookup": 150000 + }, + "access": { + "ifname": "access" + }, + "core": { + "ifname": "core" + }, + "measure_upf": true, + "measure_flow": true, + "enable_notify_bess": true, + "notify_sockaddr": "/pod-share/notifycp", + "cpiface": { + "dnn": "internet", + "hostname": "upf", + "http_port": "8080" + }, + "n6_bps": 1000000000, + "n6_burst_bytes": 12500000, + "n3_bps": 1000000000, + "n3_burst_bytes": 12500000, + "qci_qos_config": [{ + "qci": 0, + "cbs": 50000, + "ebs": 50000, + "pbs": 50000, + "burst_duration_ms": 10, + "priority": 7 + }] + }` + confPath := t.TempDir() + "/conf.json" + mustWriteStringToDisk(s, confPath) + + _, err := LoadConfigFile(confPath) + require.NoError(t, err) + }) + + t.Run("all sample configs must be valid", func(t *testing.T) { + paths := []string{ + "../conf/upf.json", + "../ptf/config/upf.json", + "../test/integration/config/default.json", + "../test/integration/config/ue_ip_alloc.json", + } + + for _, path := range paths { + _, err := LoadConfigFile(path) + assert.NoError(t, err, "config %v is not valid", path) + } + }) +} diff --git a/pfcpiface/main.go b/pfcpiface/main.go index b51b6c40a..fa9d8d7e3 100644 --- a/pfcpiface/main.go +++ b/pfcpiface/main.go @@ -1,15 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 Intel Corporation +// Copyright 2022-present Open Networking Foundation package main import ( "context" - "encoding/json" "errors" "flag" - "io/ioutil" - "net" "net/http" "os" "os/signal" @@ -24,102 +22,6 @@ var ( pfcpsim = flag.Bool("pfcpsim", false, "simulate PFCP") ) -// Conf : Json conf struct. -type Conf struct { - Mode string `json:"mode"` - AccessIface IfaceType `json:"access"` - CoreIface IfaceType `json:"core"` - CPIface CPIfaceInfo `json:"cpiface"` - P4rtcIface P4rtcInfo `json:"p4rtciface"` - EnableP4rt bool `json:"enable_p4rt"` - EnableFlowMeasure bool `json:"measure_flow"` - SimInfo SimModeInfo `json:"sim"` - ConnTimeout uint32 `json:"conn_timeout"` - ReadTimeout uint32 `json:"read_timeout"` - EnableNotifyBess bool `json:"enable_notify_bess"` - EnableEndMarker bool `json:"enable_end_marker"` - NotifySockAddr string `json:"notify_sockaddr"` - EndMarkerSockAddr string `json:"endmarker_sockaddr"` - LogLevel string `json:"log_level"` - QciQosConfig []QciQosConfig `json:"qci_qos_config"` - SliceMeterConfig SliceMeterConfig `json:"slice_rate_limit_config"` - MaxReqRetries uint8 `json:"max_req_retries"` - RespTimeout string `json:"resp_timeout"` - EnableHBTimer bool `json:"enable_hbTimer"` - HeartBeatInterval string `json:"heart_beat_interval"` -} - -// QciQosConfig : Qos configured attributes. -type QciQosConfig struct { - QCI uint8 `json:"qci"` - CBS uint32 `json:"cbs"` - PBS uint32 `json:"pbs"` - EBS uint32 `json:"ebs"` - BurstDurationMs uint32 `json:"burst_duration_ms"` - SchedulingPriority uint32 `json:"priority"` -} - -type SliceMeterConfig struct { - N6RateBps uint64 `json:"n6_bps"` - N6BurstBytes uint64 `json:"n6_burst_bytes"` - N3RateBps uint64 `json:"n3_bps"` - N3BurstBytes uint64 `json:"n3_burst_bytes"` -} - -// SimModeInfo : Sim mode attributes. -type SimModeInfo struct { - MaxSessions uint32 `json:"max_sessions"` - StartUEIP net.IP `json:"start_ue_ip"` - StartENBIP net.IP `json:"start_enb_ip"` - StartAUPFIP net.IP `json:"start_aupf_ip"` - N6AppIP net.IP `json:"n6_app_ip"` - N9AppIP net.IP `json:"n9_app_ip"` - StartN3TEID string `json:"start_n3_teid"` - StartN9TEID string `json:"start_n9_teid"` -} - -// CPIfaceInfo : CPIface interface settings. -type CPIfaceInfo struct { - Peers []string `json:"peers"` - UseFQDN bool `json:"use_fqdn"` - NodeID string `json:"hostname"` - EnableUeIPAlloc bool `json:"enable_ue_ip_alloc"` - UEIPPool string `json:"ue_ip_pool"` - HTTPPort string `json:"http_port"` - Dnn string `json:"dnn"` -} - -// IfaceType : Gateway interface struct. -type IfaceType struct { - IfName string `json:"ifname"` -} - -// ParseJSON : parse json file and populate corresponding struct. -func ParseJSON(filepath *string, conf *Conf) { - /* Open up file */ - jsonFile, err := os.Open(*filepath) - if err != nil { - log.Fatalln("Error opening file: ", err) - } - defer jsonFile.Close() - - /* read our opened file */ - byteValue, err := ioutil.ReadAll(jsonFile) - if err != nil { - log.Fatalln("Error reading file: ", err) - } - - err = json.Unmarshal(byteValue, conf) - if err != nil { - log.Fatalln("Unable to unmarshal conf attributes:", err) - } - - // Set default log level - if conf.LogLevel == "" { - conf.LogLevel = "info" - } -} - func init() { flag.Var(&simulate, "simulate", "create|delete|create_continue simulated sessions") // Set up logger @@ -133,22 +35,17 @@ func main() { // cmdline args flag.Parse() - var ( - conf Conf - fp fastPath - ) - - // read and parse json startup file - ParseJSON(configPath, &conf) - - if level, err := log.ParseLevel(conf.LogLevel); err != nil { - log.Fatalln(err) - } else { - log.SetLevel(level) + // Read and parse json startup file. + conf, err := LoadConfigFile(*configPath) + if err != nil { + log.Fatalln("Error reading conf file:", err) } + log.SetLevel(conf.LogLevel) + log.Infof("%+v", conf) + var fp fastPath if conf.EnableP4rt { fp = &UP4{} } else { diff --git a/pfcpiface/up4.go b/pfcpiface/up4.go index 9ad10b5a5..8a1d6f9a3 100644 --- a/pfcpiface/up4.go +++ b/pfcpiface/up4.go @@ -31,15 +31,6 @@ var ( p4RtcServerPort = flag.String("p4RtcServerPort", "", "P4 Server port") ) -// FIXME: it should not be here IMO -// P4rtcInfo : P4 runtime interface settings. -type P4rtcInfo struct { - AccessIP string `json:"access_ip"` - P4rtcServer string `json:"p4rtc_server"` - P4rtcPort string `json:"p4rtc_port"` - UEIP string `json:"ue_ip_pool"` -} - const ( preQosCounterID = iota postQosCounterID diff --git a/pfcpiface/upf.go b/pfcpiface/upf.go index e3112730c..3235c4146 100644 --- a/pfcpiface/upf.go +++ b/pfcpiface/upf.go @@ -133,10 +133,11 @@ func NewUPF(conf *Conf, fp fastPath) *upf { dnn: conf.CPIface.Dnn, peers: conf.CPIface.Peers, reportNotifyChan: make(chan uint64, 1024), - maxReqRetries: maxReqRetriesDefault, + maxReqRetries: conf.MaxReqRetries, respTimeout: respTimeoutDefault, enableHBTimer: conf.EnableHBTimer, hbInterval: hbIntervalDefault, + readTimeout: time.Second * time.Duration(conf.ReadTimeout), } if len(conf.CPIface.Peers) > 0 { @@ -162,20 +163,9 @@ func NewUPF(conf *Conf, fp fastPath) *upf { } } - if conf.MaxReqRetries != 0 { - u.maxReqRetries = conf.MaxReqRetries - } - - if conf.RespTimeout != "" { - u.respTimeout, err = time.ParseDuration(conf.RespTimeout) - if err != nil { - log.Fatalln("Unable to parse resp_timeout") - } - } - - u.readTimeout = readTimeoutDefault - if conf.ReadTimeout != 0 { - u.readTimeout = time.Second * time.Duration(conf.ReadTimeout) + u.respTimeout, err = time.ParseDuration(conf.RespTimeout) + if err != nil { + log.Fatalln("Unable to parse resp_timeout") } if u.enableHBTimer { diff --git a/pfcpiface/utils.go b/pfcpiface/utils.go index 97fc82394..cb57bead6 100644 --- a/pfcpiface/utils.go +++ b/pfcpiface/utils.go @@ -107,7 +107,7 @@ func calcBurstSizeFromRate(kbps uint64, ms uint64) uint64 { func MustParseStrIP(address string) *net.IPNet { ip, ipNet, err := net.ParseCIDR(address) if err != nil { - log.Fatal("unable to parse IP that we should parse") + log.Fatalf("unable to parse IP %v that we should parse", address) } log.Info("Parsed IP: ", ip) diff --git a/ptf/config/upf.json b/ptf/config/upf.json index aabfacb7a..9317385b3 100644 --- a/ptf/config/upf.json +++ b/ptf/config/upf.json @@ -125,9 +125,8 @@ "": "p4rtc interface settings", "p4rtciface": { - "access_ip": "172.17.0.1/32", - "p4rtc_server": "onos", - "p4rtc_port": "51001", - "ue_ip_pool": "10.250.0.0/24" + "access_ip": "172.17.0.1/32", + "p4rtc_server": "onos", + "p4rtc_port": "51001" } } diff --git a/test/integration/config/default.json b/test/integration/config/default.json index a38f4d620..fca8dbcd0 100644 --- a/test/integration/config/default.json +++ b/test/integration/config/default.json @@ -2,6 +2,7 @@ "enable_p4rt": true, "log_level": "trace", "cpiface": { + "enable_ue_ip_alloc": true, "ue_ip_pool": "10.250.0.0/16" }, "p4rtciface": {