Commit d9194f7d authored by Muhammad Umair Khan's avatar Muhammad Umair Khan
Browse files

implement EDGE-4 service provisioning and EDGE-1 EEC registration

parent 3edc8c20
Loading
Loading
Loading
Loading
+349 −0
Changes for go-apps/meep-app-enablement/server/edge-1/ecs_provisioning.go: 349 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// EDGE-4 service provisioning (3GPP TS 24.558 Eecs_ServiceProvisioning), ECS co-hosted on every MEP.
//
// The EESs of the sandbox are the MEPs of the active scenario (physical locations running
// meep-app-enablement). The service area of an EES is the coverage (location + radius) of the points
// of access of its zone, plus those of the zones without MEP that are nearest to it, so that every
// covered location belongs to an EES. The ECS returns the EESs whose service area contains the UE
// location, nearest first (all EESs when no location is given; 204 when none serves the location).
package server

import (
	"encoding/json"
	"math"
	"net/http"
	"sort"
	"strings"
	"time"

	edge3 "github.com/InterDigitalInc/AdvantEDGE/go-apps/meep-app-enablement/server/edge-3"
	dataModel "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-model"
	log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger"
)

const appEnablementImage = "meep-app-enablement"

// DNN of the edge data network of the sandbox (single EDN, reached over the UE's default PDU session)
const ednDnn = "internet"

// Validity of an EDN configuration returned by the ECS
const ednConfigLifetime = 24 * time.Hour

const earthRadiusMeters = 6371000.0

// coverage - point of access coverage (WGS84 point + radius in meters)
type coverage struct {
	name     string
	lon, lat float64
	radius   float64
}

// eesSite - an EES of the sandbox and its service area
type eesSite struct {
	eesId string
	area  []coverage
}

// ---------------------------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------------------------

func requestServProv(w http.ResponseWriter, r *http.Request) {
	var req EcsServProvReq
	if r.Body == nil || json.NewDecoder(r.Body).Decode(&req) != nil {
		problem(w, http.StatusBadRequest, "Invalid JSON body", "", nil)
		return
	}
	if errs := validateServProvReq(&req); errs != nil {
		problem(w, http.StatusBadRequest, "Invalid service provisioning request", "", errs)
		return
	}

	// Informational record of the last request of the EEC (not a precondition of EEC registration)
	if b, err := json.Marshal(req); err == nil {
		if err := setEcsProvisioning(req.EecId, string(b)); err != nil {
			log.Error("Failed to store the service provisioning request: ", err.Error())
		}
	}

	sites, err := currentSites()
	if err != nil {
		log.Error("Service provisioning: ", err.Error())
		problem(w, http.StatusInternalServerError, "Failed to read the sandbox topology", "", nil)
		return
	}
	selected := selectSites(sites, ueLocation(req.LocInf))
	if len(selected) == 0 {
		// No EES serves the UE location (RequestServProv 204: no EDN configuration)
		w.WriteHeader(http.StatusNoContent)
		return
	}

	lifeTime := now().Add(ednConfigLifetime)
	cfg := EdnConfigInfo{EdnConInfo: &EdnConInfo{Dnn: ednDnn}, LifeTime: &lifeTime}
	for _, site := range selected {
		cfg.Eess = append(cfg.Eess, eesInfo(site))
	}
	writeJson(w, http.StatusOK, EcsServProvResp{EdnCnfgInfo: []EdnConfigInfo{cfg}})
}

func validateServProvReq(req *EcsServProvReq) []InvalidParam {
	var errs invalidParams
	if !validIdentifier(req.EecId) {
		errs.add("/eecId", "mandatory, non-empty identifier")
	}
	if req.UeId == swaggerPlaceholder {
		errs.add("/ueId", "invalid GPSI")
	}
	validateAcProfiles(req.AcProfs, "/acProfs", &errs)
	if req.LocInf != nil && req.LocInf.GeographicArea != nil {
		validateGeographicArea(*req.LocInf.GeographicArea, "/locInf/geographicArea", &errs)
	}
	if len(errs) == 0 {
		return nil
	}
	return errs
}

// eesInfo - EES description returned to the EEC
func eesInfo(site eesSite) EesInfo {
	info := EesInfo{
		EesId:      site.eesId,
		EndPt:      &EndPoint{Uri: eesUri(site.eesId)},
		SvcArea:    &LocationArea5G{},
		EecRegConf: true,
	}
	for _, c := range site.area {
		radius := float32(c.radius)
		info.SvcArea.GeographicAreas = append(info.SvcArea.GeographicAreas, GeographicArea{
			Shape:       ShapePointUncertaintyCircle,
			Point:       &GeographicalCoordinates{Lon: c.lon, Lat: c.lat},
			Uncertainty: &radius,
		})
	}
	if easIds, err := edge3.ListEasIdsOnMep(site.eesId); err == nil {
		info.EasIds = easIds
	}
	return info
}

// eesUri - API root of an EES (MEP) of the sandbox
func eesUri(eesId string) string {
	host := ""
	if hostUrl != nil {
		host = strings.TrimSuffix(hostUrl.String(), "/")
	}
	return host + "/" + sandboxName + "/" + eesId
}

// ---------------------------------------------------------------------------------------------
// Topology and selection
// ---------------------------------------------------------------------------------------------

// currentSites - EESs of the active scenario
func currentSites() ([]eesSite, error) {
	if activeModel == nil {
		return nil, errNotInitialized
	}
	activeModel.UpdateScenario()
	return eesSites(activeModel.GetDeployment(nil)), nil
}

// eesSites - EESs (MEPs running meep-app-enablement) and their service areas, sorted by EES id
func eesSites(deployment *dataModel.Deployment) []eesSite {
	if deployment == nil {
		return nil
	}
	type zoneInfo struct {
		meps []string
		ref  map[string][2]float64 // reference point of each MEP of the zone
		poas []coverage
	}
	var zones []zoneInfo
	for _, domain := range deployment.Domains {
		for _, zone := range domain.Zones {
			zi := zoneInfo{ref: map[string][2]float64{}}
			for _, nl := range zone.NetworkLocations {
				if strings.HasPrefix(nl.Type_, "POA") {
					if c, ok := coverageOf(nl.Name, nl.GeoData); ok {
						zi.poas = append(zi.poas, c)
					}
				}
				for _, pl := range nl.PhysicalLocations {
					if pl.Type_ == "EDGE" && runsAppEnablement(pl.Processes) {
						zi.meps = append(zi.meps, pl.Name)
						if c, ok := coverageOf(pl.Name, pl.GeoData); ok {
							zi.ref[pl.Name] = [2]float64{c.lon, c.lat}
						}
					}
				}
			}
			zones = append(zones, zi)
		}
	}

	areas := map[string][]coverage{}
	refs := map[string][2]float64{}
	for _, z := range zones {
		for _, mep := range z.meps {
			areas[mep] = nil
			if ref, ok := z.ref[mep]; ok {
				refs[mep] = ref
			} else if len(z.poas) > 0 {
				refs[mep] = centroid(z.poas)
			}
		}
	}
	for _, z := range zones {
		switch {
		case len(z.meps) == 1:
			areas[z.meps[0]] = append(areas[z.meps[0]], z.poas...)
		case len(z.meps) > 1:
			// Several MEPs in a zone: each point of access goes to the nearest of them
			for _, poa := range z.poas {
				mep := nearest(refs, z.meps, poa.lon, poa.lat)
				areas[mep] = append(areas[mep], poa)
			}
		case len(z.poas) > 0 && len(refs) > 0:
			// Zone without MEP: the whole zone goes to the nearest MEP
			c := centroid(z.poas)
			mep := nearest(refs, nil, c[0], c[1])
			areas[mep] = append(areas[mep], z.poas...)
		}
	}

	var sites []eesSite
	for mep, area := range areas {
		sites = append(sites, eesSite{eesId: mep, area: area})
	}
	sort.Slice(sites, func(i, j int) bool { return sites[i].eesId < sites[j].eesId })
	return sites
}

// selectSites - EESs whose service area contains the location, nearest first; all EESs without location
func selectSites(sites []eesSite, loc *[2]float64) []eesSite {
	if loc == nil {
		return sites
	}
	type candidate struct {
		site     eesSite
		distance float64
	}
	var candidates []candidate
	for _, site := range sites {
		best := math.Inf(1)
		for _, c := range site.area {
			if d := distanceMeters(loc[0], loc[1], c.lon, c.lat); d <= c.radius && d < best {
				best = d
			}
		}
		if !math.IsInf(best, 1) {
			candidates = append(candidates, candidate{site, best})
		}
	}
	sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].distance < candidates[j].distance })
	var selected []eesSite
	for _, c := range candidates {
		selected = append(selected, c.site)
	}
	return selected
}

// ueLocation - point of the UE location (point shapes: the point; polygon: centroid), nil if unknown
func ueLocation(locInf *LocationInfo) *[2]float64 {
	if locInf == nil || locInf.GeographicArea == nil {
		return nil
	}
	area := locInf.GeographicArea
	if area.Point != nil {
		return &[2]float64{area.Point.Lon, area.Point.Lat}
	}
	if len(area.PointList) > 0 {
		var poas []coverage
		for _, p := range area.PointList {
			poas = append(poas, coverage{lon: p.Lon, lat: p.Lat})
		}
		c := centroid(poas)
		return &c
	}
	return nil
}

func runsAppEnablement(processes []dataModel.Process) bool {
	for _, p := range processes {
		image := p.Image[strings.LastIndex(p.Image, "/")+1:]
		if i := strings.Index(image, ":"); i >= 0 {
			image = image[:i]
		}
		if image == appEnablementImage {
			return true
		}
	}
	return false
}

func coverageOf(name string, geo *dataModel.GeoData) (coverage, bool) {
	if geo == nil || geo.Location == nil || len(geo.Location.Coordinates) < 2 {
		return coverage{}, false
	}
	return coverage{name: name, lon: float64(geo.Location.Coordinates[0]), lat: float64(geo.Location.Coordinates[1]),
		radius: float64(geo.Radius)}, true
}

func centroid(points []coverage) [2]float64 {
	var lon, lat float64
	for _, p := range points {
		lon += p.lon
		lat += p.lat
	}
	n := float64(len(points))
	return [2]float64{lon / n, lat / n}
}

// nearest - MEP with the nearest reference point (among candidates, or all MEPs if nil), ties by name
func nearest(refs map[string][2]float64, candidates []string, lon, lat float64) string {
	if candidates == nil {
		for mep := range refs {
			candidates = append(candidates, mep)
		}
	}
	sort.Strings(candidates)
	best, bestDistance := "", math.Inf(1)
	for _, mep := range candidates {
		ref, ok := refs[mep]
		if !ok {
			continue
		}
		if d := distanceMeters(lon, lat, ref[0], ref[1]); d < bestDistance {
			best, bestDistance = mep, d
		}
	}
	if best == "" && len(candidates) > 0 {
		best = candidates[0]
	}
	return best
}

// distanceMeters - great-circle (haversine) distance between two WGS84 points
func distanceMeters(lon1, lat1, lon2, lat2 float64) float64 {
	rad := math.Pi / 180
	dLat := (lat2 - lat1) * rad
	dLon := (lon2 - lon1) * rad
	a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1*rad)*math.Cos(lat2*rad)*math.Sin(dLon/2)*math.Sin(dLon/2)
	return 2 * earthRadiusMeters * math.Asin(math.Sqrt(a))
}
+286 −0
Changes for go-apps/meep-app-enablement/server/edge-1/ecs_provisioning_test.go: 286 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package server

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	edge3 "github.com/InterDigitalInc/AdvantEDGE/go-apps/meep-app-enablement/server/edge-3"
	dkm "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-key-mgr"
	dataModel "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-model"
	mod "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-model"
	redis "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-redis"
)

const ecsTestSandbox = "edge1-ecs-test"

// Unmodified copy (YAML converted to JSON) of the frontend network dual-mep-4g-5g-wifi-macro:
// zone01 (mep1, 8 points of access), zone02 (no MEP, 11), zone03 (no MEP, 7), zone04 (mep2, 15)
const scenarioFixture = "testdata/dual-mep-4g-5g-wifi-macro.json"

// Points of access of the fixture
var (
	cell1 = [2]float64{7.419344, 43.72764}  // zone01 (mep1), radius 400 m
	cell8 = [2]float64{7.429504, 43.74301}  // zone04 (mep2), radius 400 m
	cell4 = [2]float64{7.423547, 43.731724} // zone02 (no MEP)
	cell6 = [2]float64{7.421007, 43.737087} // zone03 (no MEP)
	paris = [2]float64{2.3522, 48.8566}
)

func loadFixture(t *testing.T) []byte {
	t.Helper()
	b, err := ioutil.ReadFile(scenarioFixture)
	if err != nil {
		t.Fatal(err)
	}
	return b
}

func fixtureDeployment(t *testing.T) *dataModel.Deployment {
	t.Helper()
	var scenario dataModel.Scenario
	if err := json.Unmarshal(loadFixture(t), &scenario); err != nil {
		t.Fatal(err)
	}
	return scenario.Deployment
}

func siteIds(sites []eesSite) string {
	var ids []string
	for _, s := range sites {
		ids = append(ids, s.eesId)
	}
	return strings.Join(ids, ",")
}

func TestEesSitesOfScenario(t *testing.T) {
	sites := eesSites(fixtureDeployment(t))
	if siteIds(sites) != "mep1,mep2" {
		t.Fatalf("EESs: %s", siteIds(sites))
	}
	area := map[string]map[string]bool{}
	total := 0
	for _, s := range sites {
		area[s.eesId] = map[string]bool{}
		for _, c := range s.area {
			area[s.eesId][c.name] = true
			total++
			if c.radius <= 0 {
				t.Errorf("%s: point of access %s without radius", s.eesId, c.name)
			}
		}
	}
	// Every point of access of the scenario belongs to exactly one EES
	if total != 8+11+7+15 {
		t.Errorf("points of access in service areas: %d, want 41", total)
	}
	if !area["mep1"]["4g-macro-cell-1"] || !area["mep2"]["4g-macro-cell-8"] {
		t.Errorf("zone01 must belong to mep1 and zone04 to mep2")
	}
	// Zones without MEP go, as a whole, to the nearest MEP
	zone2, zone3 := ownerOf(area, "4g-macro-cell-4"), ownerOf(area, "4g-macro-cell-6")
	if zone2 != ownerOf(area, "4g-macro-cell-5") || zone3 != ownerOf(area, "4g-macro-cell-7") {
		t.Errorf("a zone without MEP was split between EESs")
	}
	if zone2 != "mep1" {
		t.Errorf("zone02 (nearest to zone01) assigned to %s", zone2)
	}
	t.Logf("zone02 -> %s, zone03 -> %s", zone2, zone3)

	if eesSites(nil) != nil {
		t.Errorf("no deployment must give no EES")
	}
}

func ownerOf(area map[string]map[string]bool, poa string) string {
	for ees, poas := range area {
		if poas[poa] {
			return ees
		}
	}
	return ""
}

func TestSelectSites(t *testing.T) {
	sites := eesSites(fixtureDeployment(t))
	for name, c := range map[string]struct {
		loc  *[2]float64
		want string
	}{
		"zone01 cell":           {&cell1, "mep1"},
		"zone04 cell":           {&cell8, "mep2"},
		"no location: all EESs": {nil, "mep1,mep2"},
		"outside the sandbox":   {&paris, ""},
	} {
		if got := siteIds(selectSites(sites, c.loc)); got != c.want {
			t.Errorf("%s: %q, want %q", name, got, c.want)
		}
	}
	for _, loc := range [][2]float64{cell4, cell6} {
		if got := selectSites(sites, &loc); len(got) == 0 {
			t.Errorf("location %v of a zone without MEP not served", loc)
		}
	}
	// 2 km south of zone01: outside every coverage circle
	if got := selectSites(sites, &[2]float64{7.419344, 43.70964}); len(got) != 0 {
		t.Errorf("uncovered location served by %s", siteIds(got))
	}
}

func TestUeLocation(t *testing.T) {
	if ueLocation(nil) != nil || ueLocation(&LocationInfo{CellId: "c1"}) != nil {
		t.Errorf("location without geographic area must be unknown")
	}
	loc := ueLocation(&LocationInfo{GeographicArea: &GeographicArea{Shape: ShapePoint, Point: &GeographicalCoordinates{Lon: 7.4, Lat: 43.7}}})
	if loc == nil || loc[0] != 7.4 || loc[1] != 43.7 {
		t.Errorf("point location: %v", loc)
	}
	poly := ueLocation(&LocationInfo{GeographicArea: &GeographicArea{Shape: ShapePolygon,
		PointList: []GeographicalCoordinates{{Lon: 0, Lat: 0}, {Lon: 2, Lat: 0}, {Lon: 1, Lat: 3}}}})
	if poly == nil || poly[0] != 1 || poly[1] != 1 {
		t.Errorf("polygon centroid: %v", poly)
	}
}

func TestDistanceMeters(t *testing.T) {
	// One degree of latitude is about 111.2 km
	if d := distanceMeters(7, 43, 7, 44); d < 111000 || d > 111400 {
		t.Errorf("1 degree of latitude: %.0f m", d)
	}
	if distanceMeters(7.4, 43.7, 7.4, 43.7) != 0 {
		t.Errorf("distance to itself")
	}
}

// setupEcs - EDGE-1 of mep1 with the fixture as active scenario (test Redis) and the EDGE-3 store
func setupEcs(t *testing.T) {
	t.Helper()
	flushEcsSandbox(t)
	t.Cleanup(func() { flushEcsSandbox(t) })
	scenarioModel, err := mod.NewModel(mod.ModelCfg{Name: "activeScenario", Namespace: ecsTestSandbox, Module: "test", DbAddr: redisTestAddr})
	if err != nil {
		t.Fatal(err)
	}
	if err := scenarioModel.SetScenario(loadFixture(t)); err != nil {
		t.Fatal(err)
	}
	if err := scenarioModel.Activate(); err != nil {
		t.Fatal(err)
	}
	if err := Init(ecsTestSandbox, "mep1", &url.URL{Scheme: "https", Host: "sandbox.example"}, nil, redisTestAddr, nil); err != nil {
		t.Fatal(err)
	}
	if err := edge3.Init(ecsTestSandbox, "mep1", nil, nil, redisTestAddr, nil); err != nil {
		t.Fatal(err)
	}
}

func flushEcsSandbox(t *testing.T) {
	c, err := redis.NewConnector(redisTestAddr, redisTable)
	if err != nil {
		t.Fatalf("Redis not available: %v", err)
	}
	_ = c.DBFlush(dkm.GetKeyRoot(ecsTestSandbox))
}

func provision(t *testing.T, body string) *httptest.ResponseRecorder {
	t.Helper()
	rr := httptest.NewRecorder()
	requestServProv(rr, httptest.NewRequest("POST", "/eecs-serviceprovisioning/v1/request", strings.NewReader(body)))
	return rr
}

func pointLoc(p [2]float64) string {
	b, _ := json.Marshal(map[string]interface{}{"geographicArea": map[string]interface{}{"shape": "POINT", "point": map[string]float64{"lon": p[0], "lat": p[1]}}})
	return string(b)
}

func TestRequestServProv(t *testing.T) {
	setupEcs(t)

	// An EAS registered on mep2 is advertised in the EESInfo of mep2
	c, _ := redis.NewConnector(redisTestAddr, redisTable)
	_ = c.JSONSetEntry(dkm.GetKeyRoot(ecsTestSandbox)+"app-enablement:mep:mep2:edge:eas-by-easid:eas-v2x", ".", "reg-1")

	rr := provision(t, `{"eecId":"eec-1","ueId":"msisdn-33600000001","acProfs":[{"acId":"ac-1"}],"locInf":`+pointLoc(cell8)+`}`)
	if rr.Code != http.StatusOK {
		t.Fatalf("provisioning: %d %s", rr.Code, rr.Body.String())
	}
	var resp EcsServProvResp
	_ = json.Unmarshal(rr.Body.Bytes(), &resp)
	if len(resp.EdnCnfgInfo) != 1 || len(resp.EdnCnfgInfo[0].Eess) != 1 {
		t.Fatalf("EDN configuration: %s", rr.Body.String())
	}
	cfg := resp.EdnCnfgInfo[0]
	ees := cfg.Eess[0]
	if ees.EesId != "mep2" || ees.EndPt == nil || ees.EndPt.Uri != "https://sandbox.example/"+ecsTestSandbox+"/mep2" || !ees.EecRegConf {
		t.Errorf("EES: %s", rr.Body.String())
	}
	if len(ees.EasIds) != 1 || ees.EasIds[0] != "eas-v2x" {
		t.Errorf("easIds of mep2: %v", ees.EasIds)
	}
	if ees.SvcArea == nil || len(ees.SvcArea.GeographicAreas) == 0 || ees.SvcArea.GeographicAreas[0].Shape != ShapePointUncertaintyCircle ||
		ees.SvcArea.GeographicAreas[0].Uncertainty == nil {
		t.Errorf("service area: %+v", ees.SvcArea)
	}
	if cfg.EdnConInfo == nil || cfg.EdnConInfo.Dnn != ednDnn || cfg.LifeTime == nil || !cfg.LifeTime.After(now()) {
		t.Errorf("EDN connection info / lifetime: %s", rr.Body.String())
	}
	// The request is recorded (informational)
	if stored, err := getEcsProvisioning("eec-1"); err != nil || !strings.Contains(stored, "msisdn-33600000001") {
		t.Errorf("provisioning request not recorded: %q %v", stored, err)
	}

	// zone01 location -> mep1; no location -> both EESs; outside the sandbox -> 204
	_ = json.Unmarshal(provision(t, `{"eecId":"eec-1","locInf":`+pointLoc(cell1)+`}`).Body.Bytes(), &resp)
	if resp.EdnCnfgInfo[0].Eess[0].EesId != "mep1" {
		t.Errorf("zone01: %+v", resp.EdnCnfgInfo[0].Eess)
	}
	_ = json.Unmarshal(provision(t, `{"eecId":"eec-1"}`).Body.Bytes(), &resp)
	if len(resp.EdnCnfgInfo[0].Eess) != 2 {
		t.Errorf("no location: %d EESs", len(resp.EdnCnfgInfo[0].Eess))
	}
	if rr = provision(t, `{"eecId":"eec-1","locInf":`+pointLoc(paris)+`}`); rr.Code != http.StatusNoContent || rr.Body.Len() != 0 {
		t.Errorf("outside the sandbox: %d %s", rr.Code, rr.Body.String())
	}

	// Invalid requests
	for name, body := range map[string]string{
		"missing eecId":         `{}`,
		"placeholder eecId":     `{"eecId":"string"}`,
		"acProfile w/o acId":    `{"eecId":"e","acProfs":[{"acType":"V2X"}]}`,
		"location out of range": `{"eecId":"e","locInf":{"geographicArea":{"shape":"POINT","point":{"lon":200,"lat":0}}}}`,
		"not JSON":              `{`,
	} {
		if rr = provision(t, body); rr.Code != http.StatusBadRequest {
			t.Errorf("%s: %d", name, rr.Code)
		}
	}
}

func TestRequestServProvWithoutScenario(t *testing.T) {
	setupEcs(t)
	flushEcsSandbox(t) // active scenario removed
	if rr := provision(t, `{"eecId":"eec-1"}`); rr.Code != http.StatusNoContent {
		t.Errorf("no active scenario: %d %s", rr.Code, rr.Body.String())
	}
}
+39 −31
Changes for go-apps/meep-app-enablement/server/edge-1/edge1.go: 39 added lines, 31 removed lines.
Original line number Diff line number Diff line
@@ -25,20 +25,27 @@ import (
	"sync"

	log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger"
	mod "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-model"
	mq "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-mq"
)

const serviceName = "EDGE-1 Service"
const moduleName = "meep-app-enablement"

var sandboxName string
var mepName string
var hostUrl *url.URL
var mqLocal *mq.MsgQueue
var handlerId int
var activeModel *mod.Model

// Init - EDGE-1 service initialization
// Init - EDGE-1 / EDGE-4 service initialization
func Init(sandbox string, mep string, host *url.URL, msgQueue *mq.MsgQueue, redisAddr_ string, globalMutex *sync.Mutex) (err error) {
	// Same signature as the other app-enablement services; the host URL, message queue and global
	// mutex are used by the connector from the next phases onwards.
	// The app-enablement global mutex is not used: EDGE-1 / EDGE-4 state is owned by this service
	sandboxName = sandbox
	mepName = mep
	hostUrl = host
	mqLocal = msgQueue

	// Connect the EEC registration store owned by this service
	err = initStore(sandbox, mep, redisAddr_)
@@ -46,26 +53,48 @@ func Init(sandbox string, mep string, host *url.URL, msgQueue *mq.MsgQueue, redi
		log.Error("Failed to connect to Redis DB. Error: ", err)
		return err
	}

	// Active scenario model: EESs (MEPs) and points of access of the sandbox, for the ECS
	activeModel, err = mod.NewModel(mod.ModelCfg{Name: "activeScenario", Namespace: sandbox, Module: moduleName, DbAddr: redisAddr_})
	if err != nil {
		log.Error("Failed to create the active scenario model: ", err.Error())
		return err
	}
	log.Info(serviceName, " initialized: sandbox ", sandboxName, ", MEP ", mepName)
	return nil
}

// Run - Start EDGE-1 service
// Run - Start EDGE-1 / EDGE-4 service
func Run() (err error) {
	if mqLocal != nil {
		handlerId, err = mqLocal.RegisterHandler(mq.MsgHandler{Handler: msgHandler, UserData: nil})
		if err != nil {
			log.Error("Failed to register MQ handler: ", err.Error())
			return err
		}
	}
	// Stored EEC registrations: expiry timers restored, expired ones removed
	if err = reconcileEecRegistrations(); err != nil {
		log.Error("EEC registration reconciliation failed: ", err.Error())
	}
	return nil
}

// Stop - Stop EDGE-1 service
// Stop - Stop EDGE-1 / EDGE-4 service
func Stop() (err error) {
	if mqLocal != nil {
		mqLocal.UnregisterHandler(handlerId)
	}
	eecMutex.Lock()
	defer eecMutex.Unlock()
	for registrationId := range eecTimers {
		stopEecExpiry(registrationId)
	}
	return nil
}

// Eecs_ServiceProvisioning (EDGE-4)

func requestServProv(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "RequestServProv")
}

func createServProvSub(w http.ResponseWriter, r *http.Request) {
	notSupported(w, "CreateServProvSub")
}
@@ -82,28 +111,7 @@ func deleteIndServProvSub(w http.ResponseWriter, r *http.Request) {
	notSupported(w, "DeleteIndServProvSub")
}

// Eees_EECRegistration (EDGE-1)

func createEECReg(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "CreateEECReg")
}

// getIndEECReg - sandbox extension, not defined by 3GPP TS 24.558
func getIndEECReg(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "GetIndEECReg")
}

func updateIndEECReg(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "UpdateIndEECReg")
}

func modifyIndEECReg(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "ModifyIndEECReg")
}

func deleteIndEECReg(w http.ResponseWriter, r *http.Request) {
	notImplemented(w, "DeleteIndEECReg")
}
// Eees_EECRegistration (EDGE-1): see eec_registration.go; RequestServProv (EDGE-4): see ecs_provisioning.go

// Eees_EASDiscovery (EDGE-1 and EDGE-3)

+175 −0

File added.

Preview size limit exceeded, changes collapsed.

+357 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading