From 2e78e42c5896b61b77ab3a97e45704f6749161b2 Mon Sep 17 00:00:00 2001 From: Mohamed Abukar Date: Sun, 2 Jun 2019 11:45:52 +0300 Subject: [PATCH] Initial version Change-Id: Ia95ac0cc8321d78489de7b728449f94e5553f27a Signed-off-by: Mohamed Abukar --- .gitignore | 6 ++ Makefile | 58 ++++++++++ README.md | 86 +++++++++++++++ assets/xappframe-arch.png | Bin 0 -> 88672 bytes config/config-file.yaml | 38 +++++++ config/uta_rtg.rt | 4 + examples/example-xapp.go | 18 ++++ examples/go.mod | 6 ++ examples/go.sum | 0 go.mod | 15 +++ go.sum | 119 +++++++++++++++++++++ pkg/api/rest_api.json | 43 ++++++++ pkg/xapp/asn.go | 35 ++++++ pkg/xapp/config.go | 81 ++++++++++++++ pkg/xapp/db.go | 174 ++++++++++++++++++++++++++++++ pkg/xapp/logger.go | 84 +++++++++++++++ pkg/xapp/metrics.go | 86 +++++++++++++++ pkg/xapp/mtypes.go | 65 ++++++++++++ pkg/xapp/restapi.go | 102 ++++++++++++++++++ pkg/xapp/restapi_test.go | 41 +++++++ pkg/xapp/rmr.go | 217 ++++++++++++++++++++++++++++++++++++++ pkg/xapp/xapp.go | 71 +++++++++++++ pkg/xapp/xapp_test.go | 188 +++++++++++++++++++++++++++++++++ test/manifest/config-file-rx.yaml | 38 +++++++ test/manifest/config-file-tx.yaml | 38 +++++++ test/manifest/gnb-sim.yaml | 37 +++++++ test/manifest/uta_rtg_ric.rt | 4 + test/xapp/forwarder.go | 38 +++++++ test/xapp/generator.go | 99 +++++++++++++++++ test/xapp/gnbsim.go | 15 +++ 30 files changed, 1806 insertions(+) create mode 100644 .gitignore create mode 100755 Makefile create mode 100755 README.md create mode 100755 assets/xappframe-arch.png create mode 100755 config/config-file.yaml create mode 100755 config/uta_rtg.rt create mode 100755 examples/example-xapp.go create mode 100755 examples/go.mod create mode 100644 examples/go.sum create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/api/rest_api.json create mode 100755 pkg/xapp/asn.go create mode 100755 pkg/xapp/config.go create mode 100755 pkg/xapp/db.go create mode 100755 pkg/xapp/logger.go create mode 100755 pkg/xapp/metrics.go create mode 100755 pkg/xapp/mtypes.go create mode 100755 pkg/xapp/restapi.go create mode 100644 pkg/xapp/restapi_test.go create mode 100755 pkg/xapp/rmr.go create mode 100755 pkg/xapp/xapp.go create mode 100755 pkg/xapp/xapp_test.go create mode 100755 test/manifest/config-file-rx.yaml create mode 100755 test/manifest/config-file-tx.yaml create mode 100755 test/manifest/gnb-sim.yaml create mode 100755 test/manifest/uta_rtg_ric.rt create mode 100755 test/xapp/forwarder.go create mode 100755 test/xapp/generator.go create mode 100755 test/xapp/gnbsim.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b6464e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +cache +build +helm_chart +scripts +Dockerfile + diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..4edffec --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2019 Nokia. +# +# 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. + +ROOT_DIR:=. +BUILD_DIR:=$(ROOT_DIR)/build + +COVEROUT := $(BUILD_DIR)/cover.out +COVERHTML := $(BUILD_DIR)/cover.html + +GOOS=$(shell go env GOOS) +GOCMD=go +GOBUILD=$(GOCMD) build -a -installsuffix cgo +GOTEST=$(GOCMD) test -v -coverprofile $(COVEROUT) + +GOFILES := $(shell find $(ROOT_DIR) -name '*.go' -not -name '*_test.go') go.mod go.sum +GOFILES_NO_VENDOR := $(shell find $(ROOT_DIR) -path ./vendor -prune -o -name "*.go" -not -name '*_test.go' -print) + +APP:=$(BUILD_DIR)/xapp-sim +APPTST:=$(APP)_test + +.PHONY: FORCE + +.DEFAULT: build + +default: build + +$(APP): $(GOFILES) + GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOBUILD) -o $@ ./test/xapp + +$(APPTST): $(GOFILES) + GO111MODULE=on GO_ENABLED=0 GOOS=linux $(GOTEST) -c -o $@ ./pkg/xapp + RMR_SEED_RT=config/uta_rtg.rt $@ -f config/config-file.yaml -test.coverprofile $(COVEROUT) + go tool cover -html=$(COVEROUT) -o $(COVERHTML) + +build: $(APP) + +test: $(APPTST) + +fmt: $(GOFILES_NO_VENDOR) + gofmt -w -s $^ + @(RESULT="$$(gofmt -l $^)"; test -z "$${RESULT}" || (echo -e "gofmt failed:\n$${RESULT}" && false) ) + +clean: + @echo " > Cleaning build cache" + @-rm -rf $(APP) $(APPTST) 2> /dev/null + go clean 2> /dev/null diff --git a/README.md b/README.md new file mode 100755 index 0000000..71e03b4 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# XAPP-FRAME + +## Introduction +**xapp-frame** is a simple framework for rapid development of RIC xapps, and supports various services essential for RIC xapps such as RESTful APIs, RMR (RIC Message Routing), database backend services and watching and populating config-map changes in K8S environment. + +## Architecture + +![Architecture](assets/xappframe-arch.png) + +## Features and Components + +* RESTful support +* Health check/probes (readiness and liveliness) +* Reading and watching config-map +* RMR messaging +* SDL +* Loggind and tracing +* Encoding and decoding of commonly used RIC ASN.1 messages +* And more to come + +## Quick Start + +#### Below is a simple example xapp. For more information, see the sample code in the xapp/examples folder: +```go +package main + +import "gitlabe1.ext.net.nokia.com/ric_dev/nokia-xapps/xapp/pkg/xapp" + +type MessageCounsumer struct { +} + +func (m MessageCounsumer) Consume(mtype, len int, payload []byte) (err error) { + xapp.Logger.Debug("Message received - type=%d len=%d", mtype, len) + + xapp.Sdl.Store("myKey", payload) + xapp.Rmr.Send(10005, len, payload) + return nil +} + +func main() { + xapp.Run(MessageCounsumer{}) +} +``` +#### Installing and running the example xapp + + git clone git@gitlabe1.ext.net.nokia.com:ric_dev/nokia-xapps/xapp.git + +#### Build and run + unset GOPATH + cd xapp/examples + go build example-xapp.go + ./example-xapp + +Congratulations! You've just built your first **xapp** application. + +## API +#### API List + * TBD + +#### API Usage and Examples +* Setting logging level and writing to log + ``` + xapp.Logger.SetLevel(4) + xapp.Logger.Info("Status inquiry ...") + ``` +* Storing key-value data to SDL + ``` + xapp.Sdl.Store("myKey", payload) + ``` +* Sending RMR messages + ``` + mid := Rmr.GetRicMessageId("RIC_SUB_RESP") + xapp.Rmr.Send(mid, 1234, len, payload) + ``` +* Injecting REST API resources (URL) + ``` + xapp.Resource.InjectRoute("/ric/v1/health/stat", statisticsHandler, "GET") + Resource.InjectQueryRoute("/ric/v1/user", handler, "GET", "foo", "bar", "id", "mykey") + ``` + +## Documentation + +## Community + +## License +This project is licensed under the Apache License 2.0 - see the [LICENSE.md](LICENSE.md) file for details \ No newline at end of file diff --git a/assets/xappframe-arch.png b/assets/xappframe-arch.png new file mode 100755 index 0000000000000000000000000000000000000000..2f3e4bd7d9652f29a2a5c96f063864217d8993d7 GIT binary patch literal 88672 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

pE(lW&Hg`TCO}0Q3YSawf8o9_vO2FxC*mU1 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 literal 0 HcmV?d00001 diff --git a/config/config-file.yaml b/config/config-file.yaml new file mode 100755 index 0000000..b4bbde3 --- /dev/null +++ b/config/config-file.yaml @@ -0,0 +1,38 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2019 Nokia. +# +# 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. + +"local": + "host": ":8080" +"logger": + "level": 4 +"rmr": + "protPort": "tcp:4560" + "maxSize": 2072 + "numWorkers": 1 +"db": + "host": "localhost" + "port": 6379 + "namespaces": ["sdl", "rnib"] +"test": + "mode": "forwarder" + "mtype": 10004 + "subId": 1111 + "size": 100 + "rate": 10 + "amount": 10 + "rounds": 1 + "store": 0 + "waitForAck": 0 + diff --git a/config/uta_rtg.rt b/config/uta_rtg.rt new file mode 100755 index 0000000..2b08655 --- /dev/null +++ b/config/uta_rtg.rt @@ -0,0 +1,4 @@ +newrt|start +rte|10004|localhost:4560 +rte|10005|localhost:4591 +newrt|end diff --git a/examples/example-xapp.go b/examples/example-xapp.go new file mode 100755 index 0000000..03764a1 --- /dev/null +++ b/examples/example-xapp.go @@ -0,0 +1,18 @@ +package main + +import "gitlabe1.ext.net.nokia.com/ric_dev/nokia-xapps/xapp/pkg/xapp" + +type MessageCounter struct { +} + +func (m MessageCounter) Consume(mtype, len int, payload []byte) (err error) { + xapp.Logger.Debug("Message received - type=%d len=%d", mtype, len) + + xapp.Sdl.Store("myKey", payload) + xapp.Rmr.Send(10005, len, payload) + return nil +} + +func main() { + xapp.Run(MessageCounter{}) +} diff --git a/examples/go.mod b/examples/go.mod new file mode 100755 index 0000000..c39c00b --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,6 @@ +go 1.12 + +module gitlabe1.ext.net.nokia.com/example-xapp + +require gitlabe1.ext.net.nokia.com/ric_dev/nokia-xapps/xapp v0.0.0 +replace gitlabe1.ext.net.nokia.com/ric_dev/nokia-xapps/xapp => gitlabe1.ext.net.nokia.com/ric_dev/nokia-xapps/xapp.git v0.0.0-20190520084019-6d175b3c7483 diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ce000a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module gerrit.o-ran-sc.org/r/ric-plt/xapp-frame + +go 1.12 + +require ( + gerrit.o-ran-sc.org/r/ric-plt/sdlgo v0.1.1 + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/fsnotify/fsnotify v1.4.7 + github.com/gorilla/mux v1.7.1 + github.com/prometheus/client_golang v0.9.3 + github.com/spf13/viper v1.3.2 + gitlabe1.ext.net.nokia.com/ric_dev/ue-nib latest +) + +replace gerrit.o-ran-sc.org/r/ric-plt/sdlgo => gerrit.o-ran-sc.org/r/ric-plt/sdlgo.git v0.1.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7ad1b40 --- /dev/null +++ b/go.sum @@ -0,0 +1,119 @@ +gerrit.o-ran-sc.org/r/ric-plt/sdlgo.git v0.1.1 h1:D2fU0/YXdqSNYsmptSBbkDfG76uBFKjnhQiq5cD4WT4= +gerrit.o-ran-sc.org/r/ric-plt/sdlgo.git v0.1.1/go.mod h1:2Y8gw2jqj9urI8VFqFQn7BX0J3A852+YrXVV9V8gOt4= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= +github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= +github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +gitlabe1.ext.net.nokia.com/ric_dev/ue-nib v0.0.0-20190531121637-5379da45e235 h1:oTxU1A7TVgLmAkJGBTKYPxyBsq5KWm/gPjKuznIIpMQ= +gitlabe1.ext.net.nokia.com/ric_dev/ue-nib v0.0.0-20190531121637-5379da45e235/go.mod h1:rhiDbAhxaCJouoZfj0+vSoYUWM2t9i1EdR0MpewgTYo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/api/rest_api.json b/pkg/api/rest_api.json new file mode 100644 index 0000000..a778c1d --- /dev/null +++ b/pkg/api/rest_api.json @@ -0,0 +1,43 @@ +{ + "swagger" : "2.0", + "info" : { + "description" : "REST API specification xAPP Framework", + "version" : "0.0.1", + "title" : "RIC xAPP Framework", + "license" : { + "name" : "", + "url" : "http://www.nokia.com" + } + }, + "host" : "xapp-service", + "basePath" : "/ric/v1", + "schemes" : [ + "https", + "http" + ], + "paths" : { + "/health/alive" : { + "get" : { + "summary" : "Health check of xAPP - Liveness probe", + "operationId" : "getHealthAlive", + "responses" : { + "200" : { + "description" : "Status of xApp is ok" + } + } + } + }, + "/health/ready" : { + "get" : { + "summary" : "Health check of xAPP - Readiness probe", + "operationId" : "getHealthReady", + "responses" : { + "200" : { + "description" : "Status of xApp is ok" + } + } + } + } + } +} + \ No newline at end of file diff --git a/pkg/xapp/asn.go b/pkg/xapp/asn.go new file mode 100755 index 0000000..9c50491 --- /dev/null +++ b/pkg/xapp/asn.go @@ -0,0 +1,35 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +type ASN struct { +} + +func (*ASN) Encode(mtype int, len int, pdu []byte) (b []byte) { + Logger.Info("Encode: mtype=%d len=%d ... not implemented yet!", mtype, len) + + return +} + +func (*ASN) Decode(mtype int, len int, pdu []byte) (b []byte) { + Logger.Info("Decode: mtype=%d len=%d ... not implemented yet", mtype, len) + + return +} diff --git a/pkg/xapp/config.go b/pkg/xapp/config.go new file mode 100755 index 0000000..c63e32c --- /dev/null +++ b/pkg/xapp/config.go @@ -0,0 +1,81 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + "flag" + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + "os" +) + +type Configurator struct { +} + +type ConfigChangeCB func(filename string) + +var ConfigChangeListeners []ConfigChangeCB + +func parseCmd() string { + var fileName *string + fileName = flag.String("f", os.Getenv("CFG_FILE"), "Specify the configuration file.") + flag.Parse() + + return *fileName +} + +func LoadConfig() (l Log) { + l = Log{} + viper.SetConfigFile(parseCmd()) + + if err := viper.ReadInConfig(); err != nil { + l.Error("Reading config file failed: %v", err.Error()) + } + l.Info("Using config file: %s", viper.ConfigFileUsed()) + + viper.WatchConfig() + viper.OnConfigChange(func(e fsnotify.Event) { + l.Info("config file %s changed ", e.Name) + + Logger.SetLevel(viper.GetInt("logger.level")) + if len(ConfigChangeListeners) > 0 { + for _, f := range ConfigChangeListeners { + go f(e.Name) + } + } + }) + + return +} + +func AddConfigChangeListener(f ConfigChangeCB) { + if ConfigChangeListeners == nil { + ConfigChangeListeners = make([]ConfigChangeCB, 0) + } + ConfigChangeListeners = append(ConfigChangeListeners, f) +} + +func (*Configurator) GetString(key string) string { + return viper.GetString(key) +} + +func (*Configurator) GetInt(key string) int { + return viper.GetInt(key) +} diff --git a/pkg/xapp/db.go b/pkg/xapp/db.go new file mode 100755 index 0000000..0ab0b1f --- /dev/null +++ b/pkg/xapp/db.go @@ -0,0 +1,174 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + sdl "gerrit.o-ran-sc.org/r/ric-plt/sdlgo" + "gitlabe1.ext.net.nokia.com/ric_dev/ue-nib/api" + "gitlabe1.ext.net.nokia.com/ric_dev/ue-nib/pkg/uenibreader" + "gitlabe1.ext.net.nokia.com/ric_dev/ue-nib/pkg/uenibwriter" + "sync" + "time" +) + +// To be removed later +type SDLStatistics struct{} + +var SDLCounterOpts = []CounterOpts{ + {Name: "Stored", Help: "The total number of stored SDL transactions"}, + {Name: "StoreError", Help: "The total number of SDL store errors"}, +} + +type SDLClient struct { + db *sdl.SdlInstance + stat map[string]Counter + mux sync.Mutex +} + +type UENIBClient struct { + reader *uenibreader.Reader + writer *uenibwriter.Writer +} + +type RNIBClient struct { + db *sdl.SdlInstance +} + +// NewSDLClient returns a new SDLClient. +func NewSDLClient(ns string) *SDLClient { + return &SDLClient{ + db: sdl.NewSdlInstance(ns, sdl.NewDatabase()), + } +} + +func (s *SDLClient) TestConnection() { + // Test DB connection, and wait until ready! + for { + if _, err := s.db.GetAll(); err == nil { + break + } + Logger.Warn("Database connection not ready, waiting ...") + time.Sleep(time.Duration(5 * time.Second)) + } + Logger.Info("Connection to database established!") + + s.RegisterMetrics() +} + +func (s *SDLClient) Store(key string, value interface{}) (err error) { + err = s.db.Set(key, value) + if err != nil { + s.UpdateStatCounter("StoreError") + } else { + s.UpdateStatCounter("Stored") + } + return +} + +func (s *SDLClient) Read(key string) (value map[string]interface{}, err error) { + value, err = s.db.Get([]string{key}) + return +} + +func (s *SDLClient) Subscribe(cb func(string, ...string), channel string) error { + return s.db.SubscribeChannel(cb, channel) +} + +func (s *SDLClient) MSubscribe(cb func(string, ...string), channels ...string) error { + return s.db.SubscribeChannel(cb, channels...) +} + +func (s *SDLClient) StoreAndPublish(channel string, event string, pairs ...interface{}) error { + return s.db.SetAndPublish([]string{channel, event}, pairs...) +} + +func (s *SDLClient) MStoreAndPublish(channelsAndEvents []string, pairs ...interface{}) error { + return s.db.SetAndPublish(channelsAndEvents, pairs...) +} + +func (s *SDLClient) Clear() { + s.db.RemoveAll() +} + +func (s *SDLClient) RegisterMetrics() { + s.stat = Metric.RegisterCounterGroup(SDLCounterOpts, "SDL") +} + +func (s *SDLClient) UpdateStatCounter(name string) { + s.mux.Lock() + s.stat[name].Inc() + s.mux.Unlock() +} + +func (c *SDLClient) GetStat() (t SDLStatistics) { + return +} + +func NewUENIBClient() *UENIBClient { + return &UENIBClient{ + reader: uenibreader.NewReader(), + writer: uenibwriter.NewWriter(), + } +} + +func (u *UENIBClient) StoreUeMeasurement(gNbId string, gNbUeX2ApId string, data *api.MeasResults) error { + return u.writer.UpdateUeMeasurement(gNbId, gNbUeX2ApId, data) +} + +func (u *UENIBClient) ReadUeMeasurement(gNbId string, gNbUeX2ApId string) (*api.MeasResults, error) { + return u.reader.GetUeMeasurement(gNbId, gNbUeX2ApId) +} + +// To be removed ... +func NewRNIBClient(ns string) *RNIBClient { + return &RNIBClient{ + db: sdl.NewSdlInstance(ns, sdl.NewDatabase()), + } +} + +func (r *RNIBClient) GetgNBList() (values map[string]interface{}, err error) { + keys, err := r.db.GetAll() + if err == nil { + values = make(map[string]interface{}) + for _, key := range keys { + v, err := r.db.Get([]string{key}) + if err == nil { + values[key] = v[key] + } + } + } + return +} + +func (r *RNIBClient) GetNRCellList(key string) (value map[string]interface{}, err error) { + return r.db.Get([]string{key}) +} + +func (r *RNIBClient) GetUE(key1, key2 string) (value map[string]interface{}, err error) { + return r.db.Get([]string{key1 + key2}) +} + +func (r *RNIBClient) Store(key string, value interface{}) (err error) { + return r.db.Set(key, value) +} + +func (r *RNIBClient) Clear() { + r.db.RemoveAll() +} diff --git a/pkg/xapp/logger.go b/pkg/xapp/logger.go new file mode 100755 index 0000000..a73cb36 --- /dev/null +++ b/pkg/xapp/logger.go @@ -0,0 +1,84 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +/* +#cgo CFLAGS: -I/usr/local/include +#cgo LDFLAGS: -lmdclog +# +#include +void xAppMgr_mdclog_write(mdclog_severity_t severity, const char *msg) { + mdclog_write(severity, "%s", msg); +} +*/ +import "C" + +import ( + "fmt" + "log" + "time" +) + +type Log struct { +} + +const ( + LogLvlErr = C.MDCLOG_ERR + LogLvlWarn = C.MDCLOG_WARN + LogLvlInfo = C.MDCLOG_INFO + LogLvlDebug = C.MDCLOG_DEBUG +) + +func WriteLog(lvl C.mdclog_severity_t, msg string) { + t := time.Now().Format("2019-01-02 15:04:05") + text := fmt.Sprintf("%s:: %s ", t, msg) + + C.xAppMgr_mdclog_write(lvl, C.CString(text)) +} + +func (Log) SetLevel(level int) { + l := C.mdclog_severity_t(level) + C.mdclog_level_set(l) +} + +func (Log) SetMdc(key string, value string) { + C.mdclog_mdc_add(C.CString(key), C.CString(value)) +} + +func (Log) Fatal(pattern string, args ...interface{}) { + WriteLog(LogLvlErr, fmt.Sprintf(pattern, args...)) + log.Panic("Fatal error occured, exiting ...") +} + +func (Log) Error(pattern string, args ...interface{}) { + WriteLog(LogLvlErr, fmt.Sprintf(pattern, args...)) +} + +func (Log) Warn(pattern string, args ...interface{}) { + WriteLog(LogLvlWarn, fmt.Sprintf(pattern, args...)) +} + +func (Log) Info(pattern string, args ...interface{}) { + WriteLog(LogLvlInfo, fmt.Sprintf(pattern, args...)) +} + +func (Log) Debug(pattern string, args ...interface{}) { + WriteLog(LogLvlDebug, fmt.Sprintf(pattern, args...)) +} diff --git a/pkg/xapp/metrics.go b/pkg/xapp/metrics.go new file mode 100755 index 0000000..e31fdb7 --- /dev/null +++ b/pkg/xapp/metrics.go @@ -0,0 +1,86 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type Metrics struct { + Namespace string +} + +// Alias +type CounterOpts prometheus.CounterOpts +type Counter prometheus.Counter +type Gauge prometheus.Gauge + +func NewMetrics(url, namespace string, r *mux.Router) *Metrics { + if url == "" { + url = "/ric/v1/metrics" + } + if namespace == "" { + namespace = "ricxapp" + } + + Logger.Info("Serving metrics on: url=%s namespace=%s", url, namespace) + + // Expose 'metrics' endpoint with standard golang metrics used by prometheus + r.Handle(url, promhttp.Handler()) + + return &Metrics{Namespace: namespace} +} + +func (m *Metrics) RegisterCounter(opts CounterOpts) Counter { + Logger.Info("Register new counter with opts: %v", opts) + + return promauto.NewCounter(prometheus.CounterOpts(opts)) +} + +func (m *Metrics) RegisterCounterGroup(opts []CounterOpts, subsytem string) (c map[string]Counter) { + c = make(map[string]Counter) + for _, opt := range opts { + opt.Namespace = m.Namespace + opt.Subsystem = subsytem + c[opt.Name] = m.RegisterCounter(opt) + } + + return +} + +func (m *Metrics) RegisterGauge(opts CounterOpts) Gauge { + Logger.Info("Register new gauge with opts: %v", opts) + + return promauto.NewGauge(prometheus.GaugeOpts(opts)) +} + +func (m *Metrics) RegisterGaugeGroup(opts []CounterOpts, subsytem string) (c map[string]Gauge) { + c = make(map[string]Gauge) + for _, opt := range opts { + opt.Namespace = m.Namespace + opt.Subsystem = subsytem + c[opt.Name] = m.RegisterGauge(opt) + } + + return +} diff --git a/pkg/xapp/mtypes.go b/pkg/xapp/mtypes.go new file mode 100755 index 0000000..9a84447 --- /dev/null +++ b/pkg/xapp/mtypes.go @@ -0,0 +1,65 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +/* +#include +*/ +import "C" + +var RICMessageTypes = map[string]int{ + "RIC_SUB_REQ": C.RIC_SUB_REQ, + "RIC_SUB_RESP": C.RIC_SUB_RESP, + "RIC_SUB_FAILURE": C.RIC_SUB_FAILURE, + "RIC_SUB_DEL_REQ": C.RIC_SUB_DEL_REQ, + "RIC_SUB_DEL_RESP": C.RIC_SUB_DEL_RESP, + "RIC_SUB_DEL_FAILURE": C.RIC_SUB_DEL_FAILURE, + "RIC_SERVICE_UPDATE": C.RIC_SERVICE_UPDATE, + "RIC_SERVICE_UPDATE_ACK": C.RIC_SERVICE_UPDATE_ACK, + "RIC_SERVICE_UPDATE_FAILURE": C.RIC_SERVICE_UPDATE_FAILURE, + "RIC_CONTROL_REQ": C.RIC_CONTROL_REQ, + "RIC_CONTROL_ACK": C.RIC_CONTROL_ACK, + "RIC_CONTROL_FAILURE": C.RIC_CONTROL_FAILURE, + "RIC_INDICATION": C.RIC_INDICATION, + "RIC_SERVICE_QUERY": C.RIC_SERVICE_QUERY, + "RIC_X2_SETUP_REQ": C.RIC_X2_SETUP_REQ, + "RIC_X2_SETUP_RESP": C.RIC_X2_SETUP_RESP, + "RIC_X2_SETUP_FAILURE": C.RIC_X2_SETUP_FAILURE, + "RIC_X2_RESET": C.RIC_X2_RESET, + "RIC_X2_RESET_RESP": C.RIC_X2_RESET_RESP, + "RIC_ENDC_X2_SETUP_REQ": C.RIC_ENDC_X2_SETUP_REQ, + "RIC_ENDC_X2_SETUP_RESP": C.RIC_ENDC_X2_SETUP_RESP, + "RIC_ENDC_X2_SETUP_FAILURE": C.RIC_ENDC_X2_SETUP_FAILURE, + "RIC_ENDC_CONF_UPDATE": C.RIC_ENDC_CONF_UPDATE, + "RIC_ENDC_CONF_UPDATE_ACK": C.RIC_ENDC_CONF_UPDATE_ACK, + "RIC_ENDC_CONF_UPDATE_FAILURE": C.RIC_ENDC_CONF_UPDATE_FAILURE, + "RIC_RES_STATUS_REQ": C.RIC_RES_STATUS_REQ, + "RIC_RES_STATUS_RESP": C.RIC_RES_STATUS_RESP, + "RIC_RES_STATUS_FAILURE": C.RIC_RES_STATUS_FAILURE, + "RIC_ENB_CONF_UPDATE": C.RIC_ENB_CONF_UPDATE, + "RIC_ENB_CONF_UPDATE_ACK": C.RIC_ENB_CONF_UPDATE_ACK, + "RIC_ENB_CONF_UPDATE_FAILURE": C.RIC_ENB_CONF_UPDATE_FAILURE, + "RIC_ENB_LOAD_INFORMATION": C.RIC_ENB_LOAD_INFORMATION, + "RIC_GNB_STATUS_INDICATION": C.RIC_GNB_STATUS_INDICATION, + "RIC_RESOURCE_STATUS_UPDATE": C.RIC_RESOURCE_STATUS_UPDATE, + "RIC_ERROR_INDICATION": C.RIC_ERROR_INDICATION, + "DC_ADM_INT_CONTROL": C.DC_ADM_INT_CONTROL, + "DC_ADM_INT_CONTROL_ACK": C.DC_ADM_INT_CONTROL_ACK, +} diff --git a/pkg/xapp/restapi.go b/pkg/xapp/restapi.go new file mode 100755 index 0000000..291a874 --- /dev/null +++ b/pkg/xapp/restapi.go @@ -0,0 +1,102 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + "encoding/json" + "github.com/gorilla/mux" + "net/http" +) + +const ( + ReadyURL = "/ric/v1/health/ready" + AliveURL = "/ric/v1/health/alive" +) + +type StatusCb func() bool + +type Router struct { + router *mux.Router + cbMap []StatusCb +} + +func NewRouter() *Router { + r := &Router{ + router: mux.NewRouter().StrictSlash(true), + cbMap: make([]StatusCb, 0), + } + + // Inject default routes for health probes + r.InjectRoute(ReadyURL, readyHandler, "GET") + r.InjectRoute(AliveURL, aliveHandler, "GET") + + return r +} + +func (r *Router) serviceChecker(inner http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + Logger.Info("restapi: method=%s url=%s", req.Method, req.URL.RequestURI()) + if req.URL.RequestURI() == AliveURL || (Rmr.IsReady() && r.CheckStatus()) { + inner.ServeHTTP(w, req) + } else { + respondWithJSON(w, http.StatusServiceUnavailable, nil) + } + }) +} + +func (r *Router) InjectRoute(url string, handler http.HandlerFunc, method string) *mux.Route { + return r.router.HandleFunc(url, r.serviceChecker(handler)).Methods(method) +} + +func (r *Router) InjectQueryRoute(url string, h http.HandlerFunc, m string, q ...string) *mux.Route { + return r.router.HandleFunc(url, r.serviceChecker(h)).Methods(m).Queries(q...) +} + +func (r *Router) InjectStatusCb(f StatusCb) { + r.cbMap = append(r.cbMap, f) +} + +func (r *Router) CheckStatus() (status bool) { + if len(r.cbMap) == 0 { + return true + } + + for _, f := range r.cbMap { + status = f() + } + return +} + +func readyHandler(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, nil) +} + +func aliveHandler(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, nil) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + if payload != nil { + response, _ := json.Marshal(payload) + w.Write(response) + } +} diff --git a/pkg/xapp/restapi_test.go b/pkg/xapp/restapi_test.go new file mode 100644 index 0000000..76d5c5f --- /dev/null +++ b/pkg/xapp/restapi_test.go @@ -0,0 +1,41 @@ +package xapp + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetHealthReadyCheck(t *testing.T) { + req, err := http.NewRequest("GET", "/ric/v1/health/ready", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(readyHandler) + handler.ServeHTTP(rr, req) + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + //expected := `{"ready": true}` + //if rr.Body.String() != expected { + // t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + //} +} + +func TestGetHealthAliveCheck(t *testing.T) { + req, err := http.NewRequest("GET", "/ric/v1/health/alive", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aliveHandler) + handler.ServeHTTP(rr, req) + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + //expected := `{"alive": true}` + //if rr.Body.String() != expected { + // t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + //} +} diff --git a/pkg/xapp/rmr.go b/pkg/xapp/rmr.go new file mode 100755 index 0000000..9b1edd7 --- /dev/null +++ b/pkg/xapp/rmr.go @@ -0,0 +1,217 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +/* +#include +#include +#include +#include +#include +#include + +void write_bytes_array(unsigned char *dst, void *data, int len) { + memcpy((void *)dst, (void *)data, len); +} + +#cgo CFLAGS: -I../ +#cgo LDFLAGS: -lrmr_nng -lnng +*/ +import "C" + +import ( + "github.com/spf13/viper" + "strconv" + "sync" + "time" + "unsafe" +) + +var RMRCounterOpts = []CounterOpts{ + {Name: "Transmitted", Help: "The total number of transmited RMR messages"}, + {Name: "Received", Help: "The total number of received RMR messages"}, + {Name: "TransmitError", Help: "The total number of RMR transmission errors"}, + {Name: "ReceiveError", Help: "The total number of RMR receive errors"}, +} + +// To be removed ... +type RMRStatistics struct{} + +type RMRClient struct { + context unsafe.Pointer + ready int + wg sync.WaitGroup + mux sync.Mutex + stat map[string]Counter + consumers []MessageConsumer +} + +type MessageConsumer interface { + Consume(mtype int, sid int, len int, payload []byte) error +} + +func NewRMRClient() *RMRClient { + r := &RMRClient{} + r.consumers = make([]MessageConsumer, 0) + + p := C.CString(viper.GetString("rmr.protPort")) + m := C.int(viper.GetInt("rmr.maxSize")) + defer C.free(unsafe.Pointer(p)) + + r.context = C.rmr_init(p, m, C.int(0)) + if r.context == nil { + Logger.Fatal("rmrClient: Initializing RMR context failed, bailing out!") + } + + return r +} + +func (m *RMRClient) Start(c MessageConsumer) { + m.RegisterMetrics() + + for { + Logger.Info("rmrClient: Waiting for RMR to be ready ...") + + if m.ready = int(C.rmr_ready(m.context)); m.ready == 1 { + break + } + time.Sleep(10 * time.Second) + } + m.wg.Add(viper.GetInt("rmr.numWorkers")) + + if c != nil { + m.consumers = append(m.consumers, c) + } + + for w := 0; w < viper.GetInt("rmr.numWorkers"); w++ { + go m.Worker("worker-"+strconv.Itoa(w), 0) + } + + m.Wait() +} + +func (m *RMRClient) Worker(taskName string, msgSize int) { + p := viper.GetString("rmr.protPort") + Logger.Info("rmrClient: '%s': receiving messages on [%s]", taskName, p) + + defer m.wg.Done() + for { + rxBuffer := C.rmr_rcv_msg(m.context, nil) + if rxBuffer == nil { + m.UpdateStatCounter("ReceiveError") + continue + } + m.UpdateStatCounter("Received") + + go m.parseMessage(rxBuffer) + } +} + +func (m *RMRClient) parseMessage(rxBuffer *C.rmr_mbuf_t) { + if len(m.consumers) == 0 { + Logger.Info("rmrClient: No message handlers defined, message discarded!") + return + } + + for _, c := range m.consumers { + cptr := unsafe.Pointer(rxBuffer.payload) + payload := C.GoBytes(cptr, C.int(rxBuffer.len)) + + err := c.Consume(int(rxBuffer.mtype), int(rxBuffer.sub_id), int(rxBuffer.len), payload) + if err != nil { + Logger.Warn("rmrClient: Consumer returned error: %v", err) + } + } +} + +func (m *RMRClient) Allocate() *C.rmr_mbuf_t { + buf := C.rmr_alloc_msg(m.context, 0) + if buf == nil { + Logger.Fatal("rmrClient: Allocating message buffer failed!") + } + + return buf +} + +func (m *RMRClient) Send(mtype int, sid int, len int, payload []byte) bool { + buf := m.Allocate() + + buf.mtype = C.int(mtype) + buf.sub_id = C.int(sid) + buf.len = C.int(len) + datap := C.CBytes(payload) + defer C.free(datap) + + C.write_bytes_array(buf.payload, datap, C.int(len)) + + return m.SendBuf(buf) +} + +func (m *RMRClient) SendBuf(txBuffer *C.rmr_mbuf_t) bool { + for i := 0; i < 10; i++ { + txBuffer.state = 0 + txBuffer := C.rmr_send_msg(m.context, txBuffer) + if txBuffer == nil { + break + } else if txBuffer.state != C.RMR_OK { + if txBuffer.state != C.RMR_ERR_RETRY { + time.Sleep(100 * time.Microsecond) + m.UpdateStatCounter("TransmitError") + } + for j := 0; j < 100 && txBuffer.state == C.RMR_ERR_RETRY; j++ { + txBuffer = C.rmr_send_msg(m.context, txBuffer) + } + } + + if txBuffer.state == C.RMR_OK { + m.UpdateStatCounter("Transmitted") + return true + } + } + m.UpdateStatCounter("TransmitError") + return false +} + +func (m *RMRClient) UpdateStatCounter(name string) { + m.mux.Lock() + m.stat[name].Inc() + m.mux.Unlock() +} + +func (m *RMRClient) RegisterMetrics() { + m.stat = Metric.RegisterCounterGroup(RMRCounterOpts, "RMR") +} + +// To be removed ... +func (m *RMRClient) GetStat() (r RMRStatistics) { + return +} + +func (m *RMRClient) Wait() { + m.wg.Wait() +} + +func (m *RMRClient) IsReady() bool { + return m.ready != 0 +} + +func (m *RMRClient) GetRicMessageId(mid string) int { + return RICMessageTypes[mid] +} diff --git a/pkg/xapp/xapp.go b/pkg/xapp/xapp.go new file mode 100755 index 0000000..d796afe --- /dev/null +++ b/pkg/xapp/xapp.go @@ -0,0 +1,71 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + "fmt" + "github.com/spf13/viper" + "net/http" +) + +var ( + // XApp is an application instance + Rmr *RMRClient + Sdl *SDLClient + UeNib *UENIBClient + Rnib *RNIBClient + Resource *Router + Metric *Metrics + Logger Log + Config Configurator +) + +func init() { + // Load xapp configuration + Logger = LoadConfig() + + Logger.SetLevel(viper.GetInt("logger.level")) + Rmr = NewRMRClient() + Resource = NewRouter() + Config = Configurator{} + UeNib = NewUENIBClient() + Metric = NewMetrics(viper.GetString("metrics.url"), viper.GetString("metrics.namespace"), Resource.router) + + if viper.IsSet("db.namespaces") { + namespaces := viper.GetStringSlice("db.namespaces") + if namespaces[0] != "" { + Sdl = NewSDLClient(viper.GetStringSlice("db.namespaces")[0]) + } + if namespaces[1] != "" { + Rnib = NewRNIBClient(viper.GetStringSlice("db.namespaces")[1]) + } + } else { + Sdl = NewSDLClient(viper.GetString("db.namespace")) + } +} + +func Run(c MessageConsumer) { + go http.ListenAndServe(viper.GetString("local.host"), Resource.router) + + Logger.Info(fmt.Sprintf("Xapp started, listening on: %s", viper.GetString("local.host"))) + + Sdl.TestConnection() + Rmr.Start(c) +} diff --git a/pkg/xapp/xapp_test.go b/pkg/xapp/xapp_test.go new file mode 100755 index 0000000..51ac171 --- /dev/null +++ b/pkg/xapp/xapp_test.go @@ -0,0 +1,188 @@ +/* +================================================================================== + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2019 Nokia + + 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 xapp + +import ( + "github.com/gorilla/mux" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" +) + +type Consumer struct { +} + +func (m Consumer) Consume(mtype, sid, len int, payload []byte) (err error) { + Sdl.Store("myKey", payload) + return nil +} + +// Test cases +func TestMain(m *testing.M) { + // Just run on the background (for coverage) + go Run(Consumer{}) + + code := m.Run() + os.Exit(code) +} + +func TestGetHealthCheckRetursServiceUnavailableError(t *testing.T) { + req, _ := http.NewRequest("GET", "/ric/v1/health/ready", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusServiceUnavailable, response.Code) +} + +func TestGetHealthCheckReturnsSuccess(t *testing.T) { + // Wait until RMR is up-and-running + for Rmr.IsReady() == false { + time.Sleep(time.Duration(2) * time.Second) + } + + req, _ := http.NewRequest("GET", "/ric/v1/health/ready", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusOK, response.Code) +} + +func TestInjectQuerySinglePath(t *testing.T) { + var handler = func(w http.ResponseWriter, r *http.Request) { + } + + Resource.InjectQueryRoute("/ric/v1/user", handler, "GET", "foo", "bar") + + req, _ := http.NewRequest("GET", "/ric/v1/user?foo=bar", nil) + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) +} + +func TestInjectQueryMultiplePaths(t *testing.T) { + var handler = func(w http.ResponseWriter, r *http.Request) { + } + + Resource.InjectQueryRoute("/ric/v1/user", handler, "GET", "foo", "bar", "id", "mykey") + + req, _ := http.NewRequest("GET", "/ric/v1/user?foo=bar&id=mykey", nil) + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) +} + +func TestInjectQueryFailures(t *testing.T) { + var handler = func(w http.ResponseWriter, r *http.Request) { + } + + Resource.InjectQueryRoute("/ric/v1/user", handler, "GET", "foo", "bar", "id", "mykey") + + req, _ := http.NewRequest("GET", "/ric/v1/user?invalid=bar&no=mykey", nil) + response := executeRequest(req) + checkResponseCode(t, http.StatusNotFound, response.Code) +} + +func TestMessagesReceivedSuccessfully(t *testing.T) { + for i := 0; i < 100; i++ { + Rmr.Send(10004, 1111, 100, []byte{1, 2, 3, 4, 5, 6}) + } + + // Allow time to process the messages + time.Sleep(time.Duration(2) * time.Second) + + stats := getMetrics(t) + if !strings.Contains(stats, "ricxapp_RMR_Transmitted 100") { + t.Errorf("Error: ricxapp_RMR_Transmitted value incorrect") + } + + if !strings.Contains(stats, "ricxapp_RMR_Received 100") { + t.Errorf("Error: ricxapp_RMR_Received value incorrect") + } + + if !strings.Contains(stats, "ricxapp_RMR_TransmitError 0") { + t.Errorf("Error: ricxapp_RMR_TransmitError value incorrect") + } + + if !strings.Contains(stats, "ricxapp_RMR_ReceiveError 0") { + t.Errorf("Error: ricxapp_RMR_ReceiveError value incorrect") + } + + if !strings.Contains(stats, "ricxapp_SDL_Stored 100") { + t.Errorf("Error: ricxapp_SDL_Stored value incorrect") + } + + if !strings.Contains(stats, "ricxapp_SDL_StoreError 0") { + t.Errorf("Error: ricxapp_SDL_StoreError value incorrect") + } +} + +func TestGetgNBList(t *testing.T) { + Rnib.Store("Kiikale", "Hello") + Rnib.Store("mykey", "myval") + + v, _ := Rnib.GetgNBList() + if v["Kiikale"] != "Hello" || v["mykey"] != "myval" { + t.Errorf("Error: GetgNBList failed!") + } +} + +func TestSubscribeChannels(t *testing.T) { + var NotificationCb = func(ch string, events ...string) { + if ch != "channel1" { + t.Errorf("Error: Callback function called with incorrect params") + } + } + + if err := Sdl.Subscribe(NotificationCb, "channel1"); err != nil { + t.Errorf("Error: Subscribe failed: %v", err) + } + time.Sleep(time.Duration(2) * time.Second) + + if err := Sdl.StoreAndPublish("channel1", "event", "key1", "data1"); err != nil { + t.Errorf("Error: Publish failed: %v", err) + } +} + +func TestTeardown(t *testing.T) { + Sdl.Clear() + Rnib.Clear() +} + +// Helper functions +func executeRequest(req *http.Request) *httptest.ResponseRecorder { + rr := httptest.NewRecorder() + vars := map[string]string{"id": "1"} + req = mux.SetURLVars(req, vars) + Resource.router.ServeHTTP(rr, req) + + return rr +} + +func checkResponseCode(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected response code %d. Got %d\n", expected, actual) + } +} + +func getMetrics(t *testing.T) string { + req, _ := http.NewRequest("GET", "/ric/v1/metrics", nil) + response := executeRequest(req) + + return response.Body.String() +} \ No newline at end of file diff --git a/test/manifest/config-file-rx.yaml b/test/manifest/config-file-rx.yaml new file mode 100755 index 0000000..4b1c3d0 --- /dev/null +++ b/test/manifest/config-file-rx.yaml @@ -0,0 +1,38 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2019 Nokia. +# +# 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. + +"local": + "host": ":8080" +"logger": + "level": 4 +"rmr": + "protPort": "tcp:4560" + "maxSize": 2072 + "numWorkers": 1 +"db": + "host": "localhost" + "port": 6379 + "namespaces": ["sdl", "rnib"] +"test": + "mode": "forwarder" + "mtype": 10005 + "subId": 2222 + "size": 100 + "rate": 10 + "amount": 10 + "rounds": 1 + "store": 0 + "waitForAck": 0 + diff --git a/test/manifest/config-file-tx.yaml b/test/manifest/config-file-tx.yaml new file mode 100755 index 0000000..c3ba458 --- /dev/null +++ b/test/manifest/config-file-tx.yaml @@ -0,0 +1,38 @@ +# Copyright (c) 2019 AT&T Intellectual Property. +# Copyright (c) 2019 Nokia. +# +# 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. + +"local": + "host": ":8080" +"logger": + "level": 4 +"rmr": + "protPort": "tcp:4591" + "maxSize": 2072 + "numWorkers": 1 +"db": + "host": "localhost" + "port": 6379 + "namespaces": ["sdl", "rnib"] +"test": + "mode": "generator" + "mtype": 10004 + "subId": 1111 + "size": 100 + "rate": 10 + "amount": 10 + "rounds": 1 + "store": 0 + "waitForAck": 0 + diff --git a/test/manifest/gnb-sim.yaml b/test/manifest/gnb-sim.yaml new file mode 100755 index 0000000..bb05747 --- /dev/null +++ b/test/manifest/gnb-sim.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: gnb-sim + namespace: ricxapp +spec: + hostname: gnb-sim + containers: + - name: gnb-sim + image: 192.168.0.6:5000/gnb-sim:latest + env: + - name: RMR_SEED_RT + value: "/opt/gnbsim/uta_rtg_ric.rt" + ports: + - name: sim + containerPort: 4591 + protocol: TCP + - name: rep + containerPort: 4560 + protocol: TCP + command: ["/bin/bash", "-c", "--"] + args: ["while true; do sleep 30; done;"] +--- +apiVersion: v1 +kind: Service +metadata: + name: gnb-sim-service + namespace: ricxapp +spec: + type: ClusterIP + ports: + - port: 4591 + targetPort: 4591 + protocol: TCP + name: sim + diff --git a/test/manifest/uta_rtg_ric.rt b/test/manifest/uta_rtg_ric.rt new file mode 100755 index 0000000..afe9319 --- /dev/null +++ b/test/manifest/uta_rtg_ric.rt @@ -0,0 +1,4 @@ +newrt|start +rte|10004|uemgr-service:4560,anr-service:4560,dualco-service:4560 +rte|10005|gnb-sim-service:4591 +newrt|end diff --git a/test/xapp/forwarder.go b/test/xapp/forwarder.go new file mode 100755 index 0000000..03a3154 --- /dev/null +++ b/test/xapp/forwarder.go @@ -0,0 +1,38 @@ +package main + +import ( + "gerrit.o-ran-sc.org/r/ric-plt/xapp-frame/pkg/xapp" +) + +type Forwarder struct { +} + +func (m Forwarder) Consume(mtype, subId, len int, payload []byte) (err error) { + xapp.Logger.Debug("Message received - type=%d subId=%d len=%d", mtype, subId, len) + + // Store data and reply with the same message payload + if xapp.Config.GetInt("test.store") != 0 { + xapp.Sdl.Store("myKey", payload) + } + + mid := xapp.Config.GetInt("test.mtype") + if mid != 0 { + mtype = mid + } else { + mtype = mtype + 1 + } + + sid := xapp.Config.GetInt("test.subId") + if sid != 0 { + subId = sid + } + + if ok := xapp.Rmr.Send(mtype, subId, len, payload); !ok { + xapp.Logger.Info("Rmr.Send failed ...") + } + return +} + +func forwarder() { + xapp.Run(Forwarder{}) +} diff --git a/test/xapp/generator.go b/test/xapp/generator.go new file mode 100755 index 0000000..4a86edb --- /dev/null +++ b/test/xapp/generator.go @@ -0,0 +1,99 @@ +package main + +import ( + "gerrit.o-ran-sc.org/r/ric-plt/xapp-frame/pkg/xapp" + "sync" + "time" +) + +var ( + wg sync.WaitGroup + mux sync.Mutex + rx int + tx int + failed int +) + +type Generator struct { +} + +func (m Generator) Consume(mtype, subId, len int, payload []byte) (err error) { + xapp.Logger.Debug("message received - type=%d subId=%d len=%d", mtype, subId, len) + + mux.Lock() + rx++ + mux.Unlock() + + ack := xapp.Config.GetInt("test.waitForAck") + if ack != 0 { + wg.Done() + } + + return nil +} + +func waitForMessages() { + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + // All done! + case <-time.After(5000 * time.Millisecond): + xapp.Logger.Warn("Message waiting timed out!") + } +} + +func runTests(mtype, subId, amount, msize, ack int) { + tx = 0 + rx = 0 + s := make([]byte, msize, msize) + + start := time.Now() + for i := 0; i < amount; i++ { + if ok := xapp.Rmr.Send(mtype, subId, msize, s); ok { + tx++ + if ack != 0 { + wg.Add(1) + } + } else { + failed++ + } + } + + // Wait until all replies are received, or timeout occurs + waitForMessages() + + elapsed := time.Since(start) + xapp.Logger.Info("amount=%d|tx=%d|rx=%d|failed=%d|time=%v\n", amount, tx, rx, failed, elapsed) +} + +func generator() { + // Start RMR and wait until engine is ready + go xapp.Rmr.Start(Generator{}) + for xapp.Rmr.IsReady() == false { + time.Sleep(time.Duration(2) * time.Second) + } + + // Read parameters + interval := 1000000 * 1.0 / xapp.Config.GetInt("test.rate") + mtype := xapp.Config.GetInt("test.mtype") + subId := xapp.Config.GetInt("test.subId") + amount := xapp.Config.GetInt("test.amount") + size := xapp.Config.GetInt("test.size") + ack := xapp.Config.GetInt("test.waitForAck") + rounds := xapp.Config.GetInt("test.rounds") + + // Now generate message load as per request + for i := 0; i < rounds; i++ { + runTests(mtype, subId, amount, size, ack) + if interval != 0 { + time.Sleep(time.Duration(interval) * time.Microsecond) + } + } + + return +} diff --git a/test/xapp/gnbsim.go b/test/xapp/gnbsim.go new file mode 100755 index 0000000..8a46c58 --- /dev/null +++ b/test/xapp/gnbsim.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/spf13/viper" +) + +// A simple XApp sample program that use "xapp" skeleton + +func main() { + if viper.GetString("test.mode") == "generator" { + generator() + } else { + forwarder() + } +} -- 2.16.6