├── VERSION ├── pkg ├── certs │ ├── onfca.srl │ ├── v3.ext │ ├── localhost.crt │ ├── onfca.crt │ ├── client1.crt │ ├── onos-gui.crt │ ├── generate_certs.sh │ ├── client1.key │ ├── localhost.key │ ├── onfca.key │ ├── onos-gui.key │ └── README.md ├── events │ ├── README.md │ ├── randomevent.go │ ├── configevent.go │ └── events.go ├── dispatcher │ ├── README.md │ └── dispatcher.go ├── gnmi │ ├── modeldata │ │ ├── gostruct │ │ │ ├── gen.go │ │ │ └── command.sh │ │ └── modeldata.go │ ├── README.md │ ├── server.go │ ├── capabilities.go │ ├── datetime.go │ ├── defs.go │ ├── model.go │ ├── subscribe.go │ ├── get.go │ ├── set.go │ ├── util.go │ └── server_test.go └── utils │ ├── utils.go │ └── gnmiPathUtils.go ├── .reuse └── dep5 ├── .dockerignore ├── cmd └── gnmi_target │ ├── README.md │ ├── subscribe.go │ ├── get.go │ ├── set.go │ ├── defs.go │ ├── gnmi_target.go │ └── gnmi_utils.go ├── .gitignore ├── CODE_OF_CONDUCT.md ├── go.mod ├── .golangci.yml ├── tools ├── docker_compose │ ├── docker-compose-gnoi.yml │ ├── docker-compose-gnmi.yml │ ├── docker-compose.yml │ └── docker-compose-linux.yml └── scripts │ └── run_targets.sh ├── README.md ├── Dockerfile ├── Makefile ├── docs ├── deployment.md ├── README.md ├── gnoi │ └── gnoi_user_manual.md └── gnmi │ └── gnmi_user_manual.md ├── configs └── target_configs │ └── typical_ofsw_config.json └── LICENSES └── Apache-2.0.txt /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.7-dev 2 | -------------------------------------------------------------------------------- /pkg/certs/onfca.srl: -------------------------------------------------------------------------------- 1 | 4AC11B3B171E8D65F1B9994660E417E8766EC754 2 | -------------------------------------------------------------------------------- /pkg/events/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/dispatcher/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/certs/v3.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = onos-gui -------------------------------------------------------------------------------- /pkg/gnmi/modeldata/gostruct/gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package gostruct 6 | 7 | //go:generate ./command.sh 8 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | 3 | Files: VERSION go.mod go.sum *.yaml *.yml *.json \\ 4 | pkg/certs/* 5 | Copyright: 2021 Open Networking Foundation 6 | License: Apache-2.0 7 | -------------------------------------------------------------------------------- /pkg/gnmi/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # gNMI Server 8 | Package gnmi implements a gnmi server to mock a device with YANG models. 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | **/*.md 6 | **/LICENSE 7 | .git 8 | docs 9 | .travis.yml 10 | pkg/store/testout 11 | build/_output 12 | deployments 13 | vendor 14 | -------------------------------------------------------------------------------- /cmd/gnmi_target/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # The Device Simulator Implementation 8 | 9 | The device simulator implements a gNMI target with in-memory configuration and telemetry. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | cmd/gnmi_target/gnmi_target 6 | cmd/dispatcher_main/dispatcher_main 7 | vendor 8 | build/_output 9 | *.DS_Store 10 | coverage.txt 11 | *.coverprofile 12 | .idea 13 | 14 | # Jenkins report files 15 | *report*.xml 16 | *coverage*.xml 17 | *-output.out 18 | 19 | build/build-tools 20 | 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | We expect all ONF employees, member companies, and participants to abide by our [Code of Conduct](https://www.opennetworking.org/wp-content/themes/onf/img/onf-code-of-conduct.pdf). 8 | 9 | If you are being harassed, notice that someone else is being harassed, or have any other concerns involving someone’s welfare, please notify a member of the ONF team or email [conduct@opennetworking.org](conduct@opennetworking.org). 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/onosproject/gnxi-simulators 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/eapache/channels v1.1.0 7 | github.com/golang/protobuf v1.5.0 8 | github.com/google/gnxi v0.0.0-20190228205329-8521faedac37 9 | github.com/onosproject/onos-lib-go v0.8.0 10 | github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 11 | github.com/openconfig/goyang v0.0.0-20200803193518-78bac27bdff1 12 | github.com/openconfig/ygot v0.8.3 13 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e 14 | google.golang.org/genproto v0.0.0-20200731012542-8145dea6a485 15 | google.golang.org/grpc v1.33.2 16 | ) 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | linters: 6 | enable: 7 | - gofmt 8 | - revive 9 | - misspell 10 | - typecheck 11 | - errcheck 12 | - dogsled 13 | - unconvert 14 | - nakedret 15 | - exportloopref 16 | issues: 17 | exclude-use-default: false 18 | exclude: 19 | - Error return value of `.*Close` is not checked 20 | - Error return value of `.*Flush` is not checked 21 | - Error return value of `.*Write` is not checked 22 | run: 23 | skip-dirs: 24 | - pkg/gnmi 25 | - pkg/utils 26 | - cmd/gnmi_target 27 | -------------------------------------------------------------------------------- /cmd/gnmi_target/subscribe.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/google/gnxi/utils/credentials" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/status" 11 | 12 | pb "github.com/openconfig/gnmi/proto/gnmi" 13 | ) 14 | 15 | // Subscribe overrides the Subscribe function of gnmi.Target to provide user auth. 16 | func (s *server) Subscribe(stream pb.GNMI_SubscribeServer) error { 17 | msg, ok := credentials.AuthorizeUser(stream.Context()) 18 | if !ok { 19 | log.Infof("denied a Subscribe request: %v", msg) 20 | return status.Error(codes.PermissionDenied, msg) 21 | } 22 | 23 | return s.Server.Subscribe(stream) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/gnmi_target/get.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/google/gnxi/utils/credentials" 9 | pb "github.com/openconfig/gnmi/proto/gnmi" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | // Get overrides the Get func of gnmi.Target to provide user auth. 16 | func (s *server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { 17 | msg, ok := credentials.AuthorizeUser(ctx) 18 | if !ok { 19 | log.Infof("denied a Get request: %v", msg) 20 | return nil, status.Error(codes.PermissionDenied, msg) 21 | } 22 | 23 | log.Infof("allowed a Get request %+v", req) 24 | return s.Server.Get(ctx, req) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/gnmi_target/set.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/google/gnxi/utils/credentials" 9 | pb "github.com/openconfig/gnmi/proto/gnmi" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | // Set overrides the Set func of gnmi.Target to provide user auth. 16 | func (s *server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, error) { 17 | msg, ok := credentials.AuthorizeUser(ctx) 18 | if !ok { 19 | log.Infof("denied a Set request: %v", msg) 20 | return nil, status.Error(codes.PermissionDenied, msg) 21 | } 22 | log.Infof("allowed a Set request: %v", req) 23 | setResponse, err := s.Server.Set(ctx, req) 24 | return setResponse, err 25 | } 26 | -------------------------------------------------------------------------------- /tools/docker_compose/docker-compose-gnoi.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | devicesim1: 4 | image: onosproject/device-simulator:latest 5 | environment: 6 | - HOSTNAME=localhost - 7 | - GNOI_PORT=50001 8 | - SIM_MODE=2 9 | ports: 10 | - "50001:50001" 11 | labels: 12 | description: "gNOI Simulator Device 1" 13 | devicesim2: 14 | image: onosproject/device-simulator:latest 15 | environment: 16 | - HOSTNAME=localhost - 17 | - GNOI_PORT=50002 18 | - SIM_MODE=2 19 | ports: 20 | - "50002:50002" 21 | labels: 22 | description: "gNOI Simulator Device 2" 23 | devicesim3: 24 | image: onosproject/device-simulator:latest 25 | environment: 26 | - HOSTNAME=localhost - 27 | - GNOI_PORT=50003 28 | - SIM_MODE=2 29 | ports: 30 | - "50003:50003" 31 | labels: 32 | description: "gNOI Simulator Device 3" 33 | -------------------------------------------------------------------------------- /pkg/events/randomevent.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package events 6 | 7 | import "time" 8 | 9 | // RandomEvent a random event 10 | type RandomEvent EventHappend 11 | 12 | // Clone clones the Event 13 | func (ce *RandomEvent) Clone() Event { 14 | clone := &RandomEvent{} 15 | clone.Etype = ce.Etype 16 | clone.Subject = ce.Subject 17 | clone.Time = ce.Time 18 | clone.Values = ce.Values 19 | return clone 20 | } 21 | 22 | // GetType returns type of an Event 23 | func (ce *RandomEvent) GetType() EventType { 24 | return ce.Etype 25 | } 26 | 27 | // GetTime returns the time when the event occurs 28 | func (ce *RandomEvent) GetTime() time.Time { 29 | return ce.Time 30 | } 31 | 32 | // GetValues returns the values of the event 33 | func (ce *RandomEvent) GetValues() interface{} { 34 | return ce.Values 35 | } 36 | 37 | // GetSubject returns the subject of the event 38 | func (ce *RandomEvent) GetSubject() string { 39 | return ce.Subject 40 | } 41 | -------------------------------------------------------------------------------- /tools/docker_compose/docker-compose-gnmi.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | devicesim1: 4 | image: onosproject/device-simulator:latest 5 | environment: 6 | - HOSTNAME=localhost 7 | - GNMI_PORT=10161 8 | - GNMI_INSECURE_PORT=11161 9 | - SIM_MODE=1 10 | ports: 11 | - "10161:10161" 12 | - "11161:11161" 13 | labels: 14 | description: "gNMI Simulator Device 1" 15 | devicesim2: 16 | image: onosproject/device-simulator:latest 17 | environment: 18 | - HOSTNAME=localhost 19 | - GNMI_PORT=10162 20 | - GNMI_INSECURE_PORT=11162 21 | - SIM_MODE=1 22 | ports: 23 | - "10162:10162" 24 | - "11162:11162" 25 | labels: 26 | description: "gNMI Simulator Device 2" 27 | devicesim3: 28 | image: onosproject/device-simulator:latest 29 | environment: 30 | - HOSTNAME=localhost 31 | - GNMI_PORT=10163 32 | - GNMI_INSECURE_PORT=11163 33 | - SIM_MODE=1 34 | ports: 35 | - "10163:10163" 36 | - "11163:11163" 37 | labels: 38 | description: "gNMI Simulator Device 3" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Simulators 8 | 9 | [![Build Status](https://api.travis-ci.org/onosproject/gnxi-simulators.svg?branch=master)](https://travis-ci.org/onosproject/gnxi-simulators) 10 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/gojp/goreportcard/blob/master/LICENSE) 11 | [![GoDoc](https://godoc.org/github.com/onosproject/simulators?status.svg)](https://godoc.org/github.com/onosproject/simulators) 12 | 13 | Simple simulators, used for integration testing of ONOS interactions with devices and various orchestration entities, e.g: 14 | 15 | - Configuring devices via gNMI and OpenConfig 16 | - Controlling operation of devices via gNOI 17 | - Shaping pipelines and controlling traffic flow via P4 programs and P4Runtime 18 | 19 | The simulator facilities are available as Go package libraries, executable commands and as published Docker containers. 20 | 21 | # Additional Documentation 22 | 23 | [How to run](docs/README.md) device simulator and related commands. 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | FROM onosproject/golang-build:v1.2.0 as build 6 | 7 | RUN cd $GOPATH && GO111MODULE=on go install github.com/google/gnxi/gnoi_target@c86e52276d175750acd8e5abc62dd35f5393713f 8 | RUN cd $GOPATH && GO111MODULE=on go install github.com/google/gnxi/gnoi_cert@c86e52276d175750acd8e5abc62dd35f5393713f 9 | 10 | ENV ONOS_SIMULATORS_ROOT=/go/src/github.com/onosproject/gnxi-simulators 11 | ENV CGO_ENABLED=0 12 | 13 | RUN mkdir -p $ONOS_SIMULATORS_ROOT/ 14 | 15 | COPY . $ONOS_SIMULATORS_ROOT 16 | 17 | RUN cd $ONOS_SIMULATORS_ROOT && GO111MODULE=on go build -o /go/bin/gnmi_target ./cmd/gnmi_target 18 | 19 | 20 | FROM alpine:3.11 21 | RUN apk add bash openssl curl libc6-compat 22 | ENV GNMI_PORT=10161 23 | ENV GNMI_INSECURE_PORT=11161 24 | ENV GNOI_PORT=50001 25 | ENV SIM_MODE=1 26 | ENV HOME=/home/devicesim 27 | RUN mkdir $HOME 28 | WORKDIR $HOME 29 | 30 | COPY --from=build /go/bin/gn* /usr/local/bin/ 31 | 32 | COPY configs/target_configs target_configs 33 | COPY tools/scripts scripts 34 | COPY pkg/certs certs 35 | 36 | RUN chmod +x ./scripts/run_targets.sh 37 | CMD ["./scripts/run_targets.sh"] 38 | -------------------------------------------------------------------------------- /pkg/events/configevent.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package events 6 | 7 | import "time" 8 | 9 | // ConfigEvent a configuration event 10 | type ConfigEvent EventHappend 11 | 12 | // Clone clones the Event 13 | func (ce *ConfigEvent) Clone() Event { 14 | clone := &ConfigEvent{} 15 | clone.Etype = ce.Etype 16 | clone.Subject = ce.Subject 17 | clone.Time = ce.Time 18 | clone.Values = ce.Values 19 | clone.Client = ce.Client 20 | return clone 21 | } 22 | 23 | // GetType returns type of an Event 24 | func (ce *ConfigEvent) GetType() EventType { 25 | return ce.Etype 26 | } 27 | 28 | // GetTime returns the time when the event occurs 29 | func (ce *ConfigEvent) GetTime() time.Time { 30 | return ce.Time 31 | } 32 | 33 | // GetValues returns the values of the event 34 | func (ce *ConfigEvent) GetValues() interface{} { 35 | return ce.Values 36 | } 37 | 38 | // GetSubject returns the subject of the event 39 | func (ce *ConfigEvent) GetSubject() string { 40 | return ce.Subject 41 | } 42 | 43 | // GetClient returns the stream client corresponding to the given ConfigEvent 44 | func (ce *ConfigEvent) GetClient() interface{} { 45 | return ce.Client 46 | } 47 | -------------------------------------------------------------------------------- /tools/docker_compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | devicesim1: 4 | image: onosproject/device-simulator:latest 5 | environment: 6 | - HOSTNAME=localhost 7 | - GNOI_PORT=50001 8 | - GNMI_PORT=10161 9 | - GNMI_INSECURE_PORT=11161 10 | - SIM_MODE=3 11 | ports: 12 | - "50001:50001" 13 | - "10161:10161" 14 | - "11161:11161" 15 | labels: 16 | description: "gNOI/gNMI Simulator Device 1" 17 | devicesim2: 18 | image: onosproject/device-simulator:latest 19 | environment: 20 | - HOSTNAME=localhost 21 | - GNOI_PORT=50002 22 | - GNMI_PORT=10162 23 | - GNMI_INSECURE_PORT=11162 24 | - SIM_MODE=3 25 | ports: 26 | - "50002:50002" 27 | - "10162:10162" 28 | - "11162:11162" 29 | labels: 30 | description: "gNOI/gNMI Simulator Device 2" 31 | devicesim3: 32 | image: onosproject/device-simulator:latest 33 | environment: 34 | - HOSTNAME=localhost 35 | - GNOI_PORT=50003 36 | - GNMI_PORT=10163 37 | - GNMI_INSECURE_PORT=11163 38 | - SIM_MODE=3 39 | ports: 40 | - "50003:50003" 41 | - "10163:10163" 42 | - "11163:11163" 43 | labels: 44 | description: "gNOI/gNMI Simulator Device 3" 45 | -------------------------------------------------------------------------------- /pkg/certs/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXzCCAkcCFErBGzsXHo1l8bmZRmDkF+h2bsdUMA0GCSqGSIb3DQEBCwUAMHIx 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJTWVubG9QYXJrMQww 4 | CgYDVQQKDANPTkYxFDASBgNVBAsMC0VuZ2luZWVyaW5nMR4wHAYDVQQDDBVjYS5v 5 | cGVubmV0d29ya2luZy5vcmcwHhcNMjAwNTA1MDc0MzU1WhcNMzAwNTAzMDc0MzU1 6 | WjBmMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCU1lbmxvUGFy 7 | azEMMAoGA1UECgwDT05GMRQwEgYDVQQLDAtFbmdpbmVlcmluZzESMBAGA1UEAwwJ 8 | bG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAthd8lZJo 9 | lyHXgjTGDc5bWGkXVD6bSEc/ZyghWih1rvrVQdT89J4dcUoUNzmyJIUPIOL/m5rP 10 | LRG3GV4lXg+YTiSRGLHtOB2dRKKqV0AfsZfIBTeVDLtJhBguu7RJF4vJ2q6khHm6 11 | lCbTyjBgTOBfUBK3tXToDoiOfizmZhoHefAysxdE3pdUlb+5NMrGeHcxJ7IrWrpV 12 | jwSs+1M0ODhcmP26De9rSfUNwYqg51EB/2nPpKoPBtZF1J+RLAB93ejb/5Lt5VtB 13 | q/bsSMv44sL2VUxu5e/dozzc0SNy5p33wM4P5xnKk0F2YAL8T8+3gHO3Nr+MV63K 14 | ff7VlDRGvWpTeQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBkaee8haWLCP9cUIsm 15 | TWAZxLFV4jqbEdu0/lJDpe8vPlT8rh+Kh47VS0snTR7aL2SwPz+vrU13cJdNtkCQ 16 | NvgpTUZN6GSPrPCOPgJWDvjQ4eeNd+KmdD/GAxk/cZUbC1gJmXTTPu/ZUN03IRKF 17 | 98Tg/oVxq74fKPlJHHAa/eBZBvrjIsFyTXaYMnFMY1vRoWX2bxfFC2vHhJt6xAyV 18 | 6s4ymYhfr2zrWwDpLxsAjyBQ7dCi/UEYVYeTANpk0U4CJqBRb9DQuWEm4oc3eAfL 19 | ac3qA/O+DFJMLDAa/ChFI1c08mlB/+gbfYPRPB3xj0zvI02KGkDigqHrVcaPXQzU 20 | 0C6a 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /pkg/certs/onfca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYDCCAkgCCQDe99fSN9qxSTANBgkqhkiG9w0BAQsFADByMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCU1lbmxvUGFyazEMMAoGA1UECgwDT05G 4 | MRQwEgYDVQQLDAtFbmdpbmVlcmluZzEeMBwGA1UEAwwVY2Eub3Blbm5ldHdvcmtp 5 | bmcub3JnMB4XDTE5MDQxMTA5MDYxM1oXDTI5MDQwODA5MDYxM1owcjELMAkGA1UE 6 | BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlNZW5sb1BhcmsxDDAKBgNVBAoM 7 | A09ORjEUMBIGA1UECwwLRW5naW5lZXJpbmcxHjAcBgNVBAMMFWNhLm9wZW5uZXR3 8 | b3JraW5nLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEg7CZR 9 | X8Y+syKHaQCh6mNIL1D065trwX8RnuKM2kBwSu034zefQAPloWugSoJgJnf5fe0j 10 | nUD8gN3Sm8XRhCkvf67pzfabgw4n8eJmHScyL/ugyExB6Kahwzn37bt3oT3gSqhr 11 | 6PUznWJ8fvfVuCHZZkv/HPRp4eyAcGzbJ4TuB0go4s6VE0WU5OCxCSlAiK3lvpVr 12 | 3DOLdYLVoCa5q8Ctl3wXDrfTLw5/Bpfrg9fF9ED2/YKIdV8KZ2ki/gwEOQqWcKp8 13 | 0LkTlfOWsdGjp4opPuPT7njMBGXMJzJ8/J1e1aJvIsoB7n8XrfvkNiWL5U3fM4N7 14 | UZN9jfcl7ULmm7cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIh6FjkQuTfXddmZY 15 | FYpoTen/VD5iu2Xxc1TexwmKeH+YtaKp1Zk8PTgbCtMEwEiyslfeHTMtODfnpUIk 16 | DwvtB4W0PAnreRsqh9MBzdU6YZmzGyZ92vSUB3yukkHaYzyjeKM0AwgVl9yRNEZw 17 | Y/OM070hJXXzJh3eJpLl9dlUbMKzaoAh2bZx6y3ZJIZFs/zrpGfg4lvBAvfO/59i 18 | mxJ9bQBSN3U2Hwp6ioOQzP0LpllfXtx9N5LanWpB0cu/HN9vAgtp3kRTBZD0M1XI 19 | Ctit8bXV7Mz+1iGqoyUhfCYcCSjuWTgAxzir+hrdn7uO67Hv4ndCoSj4SQaGka3W 20 | eEfVeA== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /cmd/gnmi_target/defs.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "time" 10 | 11 | "github.com/onosproject/gnxi-simulators/pkg/gnmi" 12 | pb "github.com/openconfig/gnmi/proto/gnmi" 13 | "github.com/openconfig/ygot/ygot" 14 | ) 15 | 16 | var ( 17 | bindAddr = flag.String("bind_address", ":10161", "Bind to address:port or just :port") 18 | configFile = flag.String("config", "", "IETF JSON file for target startup config") 19 | readOnlyPath = `elem: elem: elem: elem: > elem: elem: > elem: elem: ` 20 | randomEventInterval = time.Duration(5) * time.Second 21 | ) 22 | 23 | type server struct { 24 | *gnmi.Server 25 | Model *gnmi.Model 26 | configStruct ygot.ValidatedGoStruct 27 | UpdateChann chan *pb.Update 28 | readOnlyUpdateValue *pb.Update 29 | } 30 | 31 | type streamClient struct { 32 | target string 33 | sr *pb.SubscribeRequest 34 | stream pb.GNMI_SubscribeServer 35 | errChan chan<- error 36 | } 37 | -------------------------------------------------------------------------------- /pkg/certs/client1.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDcDCCAlgCFErBGzsXHo1l8bmZRmDkF+h2bsdTMA0GCSqGSIb3DQEBCwUAMHIx 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJTWVubG9QYXJrMQww 4 | CgYDVQQKDANPTkYxFDASBgNVBAsMC0VuZ2luZWVyaW5nMR4wHAYDVQQDDBVjYS5v 5 | cGVubmV0d29ya2luZy5vcmcwHhcNMjAwNTA1MDc0MjEzWhcNMzAwNTAzMDc0MjEz 6 | WjB3MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCU1lbmxvUGFy 7 | azEMMAoGA1UECgwDT05GMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEjMCEGA1UEAwwa 8 | Y2xpZW50MS5vcGVubmV0d29ya2luZy5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB 9 | DwAwggEKAoIBAQDK2amlhSGecWBI9jlj3OvCoGMAlHXffwHAsHskE/bFkyRKeYs0 10 | MHlcNBXSjgADtHt7FpGJ7nbZsMdu0XkNOlN/X0i8FKU7X9DnOHbHhOitg469J89D 11 | 8tHnN2wrTyGxY2Km95PxeCQ4NH7z4MSSbdFfFXnw9k+qr+e1owswloJsYo+eyP7h 12 | jy7QvR2sAX8CC6D0ZaVuqLGK5kHJUyMxifqMa4D4wD8zfzahcTe2QAeOxQwhsaK2 13 | pZ/09bI/KCj8vxbxG2nkcwUgjl0Lgh0g1BFNSu39v1DjsiFWrIxnj3CL2Ncu0ps0 14 | +dPSdpx90ImvjL4BRPz8V4iLPljmBmQfU2HpAgMBAAEwDQYJKoZIhvcNAQELBQAD 15 | ggEBADERPV5ykcjUaskgPHk081A0LmsKKL9AdqLfREreRIJgPg1T+ppE7PAf+i9m 16 | W6lojz+qCOkjd9d7Cd7wWRvrndRs6xt3HoIu3Z+knds/SC6emz/Rf+ZYsODNC3b3 17 | zv7agEeuD2M/Lq8pLfh9MsCxFmbz4JuiJ3kkT8zhiRAbUW6LkYFzdSeq6uUrhZPv 18 | C6VCZtHRQiEHE84Wwkxcz7shyknPwwDZxLoNeGjQPruESsY9Dyt96dz8yBQ6ukD1 19 | hPpnMDb+sksoX0ZOoEYpNdqDvZWa25n64LBBLuxbCuelSVUpsEdjRvh10znFY36x 20 | op/V1+9Wot9zMohw6bu5Aj9K6s8= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /pkg/gnmi/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "github.com/eapache/channels" 10 | "github.com/onosproject/onos-lib-go/pkg/logging" 11 | pb "github.com/openconfig/gnmi/proto/gnmi" 12 | ) 13 | 14 | var log = logging.GetLogger("gnmi") 15 | 16 | // NewServer creates an instance of Server with given json config. 17 | func NewServer(model *Model, config []byte, callback ConfigCallback) (*Server, error) { 18 | rootStruct, err := model.NewConfigStruct(config) 19 | if err != nil { 20 | return nil, err 21 | } 22 | s := &Server{ 23 | model: model, 24 | config: rootStruct, 25 | callback: callback, 26 | } 27 | if config != nil && s.callback != nil { 28 | if err := s.callback(rootStruct); err != nil { 29 | return nil, err 30 | } 31 | } 32 | // Initialize readOnlyUpdateValue variable 33 | 34 | val := &pb.TypedValue{ 35 | Value: &pb.TypedValue_StringVal{ 36 | StringVal: "INIT_STATE", 37 | }, 38 | } 39 | s.readOnlyUpdateValue = &pb.Update{Path: nil, Val: val} 40 | s.subscribers = make(map[string]*streamClient) 41 | s.ConfigUpdate = channels.NewRingChannel(100) 42 | 43 | return s, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/gnmi/modeldata/gostruct/command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | go get -u github.com/openconfig/ygot; (cd $GOPATH/src/github.com/openconfig/ygot && go get -t -d ./...); go get -u github.com/openconfig/public; go get -u github.com/google/go-cmp/cmp; go get -u github.com/openconfig/gnmi/ctree; go get -u github.com/openconfig/gnmi/proto/gnmi; go get -u github.com/openconfig/gnmi/value; go get -u github.com/YangModels/yang; go get -u github.com/golang/glog; go get -u github.com/golang/protobuf/proto; go get -u github.com/kylelemons/godebug/pretty; go get -u github.com/openconfig/goyang/pkg/yang; go get -u google.golang.org/grpc; cd $GOPATH/src && go run github.com/openconfig/ygot/generator/generator.go -generate_fakeroot -output_file github.com/onosproject/gnxi-simulators/pkg/gnmi/modeldata/gostruct/generated.go -package_name gostruct -exclude_modules ietf-interfaces -path github.com/openconfig/public,github.com/YangModels/yang github.com/openconfig/public/release/models/interfaces/openconfig-interfaces.yang github.com/openconfig/public/release/models/openflow/openconfig-openflow.yang github.com/openconfig/public/release/models/platform/openconfig-platform.yang github.com/openconfig/public/release/models/system/openconfig-system.yang -------------------------------------------------------------------------------- /tools/docker_compose/docker-compose-linux.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | devicesim1: 4 | image: onosproject/device-simulator:latest 5 | environment: 6 | - HOSTNAME=device1.opennetworking.org 7 | - SIM_MODE=1 8 | networks: 9 | simnet: 10 | ipv4_address: 172.25.0.11 11 | aliases: 12 | - device1 13 | - device1.opennetworking.org 14 | labels: 15 | description: "gNMI Simulator Device 1" 16 | devicesim2: 17 | image: onosproject/device-simulator:latest 18 | environment: 19 | - HOSTNAME=device2.opennetworking.org 20 | - SIM_MODE=1 21 | networks: 22 | simnet: 23 | ipv4_address: 172.25.0.12 24 | aliases: 25 | - device2 26 | - device2.opennetworking.org 27 | labels: 28 | description: "gNMI Simulator Device 2" 29 | devicesim3: 30 | image: onosproject/device-simulator:latest 31 | environment: 32 | - HOSTNAME=device3.opennetworking.org 33 | - SIM_MODE=1 34 | networks: 35 | simnet: 36 | ipv4_address: 172.25.0.13 37 | aliases: 38 | - device3 39 | - device3.opennetworking.org 40 | labels: 41 | description: "gNMI Simulator Device 3" 42 | networks: 43 | simnet: 44 | driver: bridge 45 | ipam: 46 | config: 47 | - subnet: 172.25.0.0/24 48 | 49 | -------------------------------------------------------------------------------- /pkg/certs/onos-gui.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJzCCAw+gAwIBAgIUSsEbOxcejWXxuZlGYOQX6HZux1IwDQYJKoZIhvcNAQEL 3 | BQAwcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlNZW5sb1Bh 4 | cmsxDDAKBgNVBAoMA09ORjEUMBIGA1UECwwLRW5naW5lZXJpbmcxHjAcBgNVBAMM 5 | FWNhLm9wZW5uZXR3b3JraW5nLm9yZzAeFw0xOTA3MjExMzU3MzJaFw0yOTA3MTgx 6 | MzU3MzJaMGUxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJTWVu 7 | bG9QYXJrMQwwCgYDVQQKDANPTkYxFDASBgNVBAsMC0VuZ2luZWVyaW5nMREwDwYD 8 | VQQDDAhvbm9zLWd1aTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8D 9 | gVeotXoZ0oFvBLAkA+OXLGRpy/NK1Jn+4Z58l4t99IF1FsRLYX7649zZxZ4s/+gR 10 | qS2Il5qc9mmbUZIVWU6LNsabG29KISLPJdR+7/bgv4wkmnRa1flel5OJoA/wEqQW 11 | LfTfIJatu5tK3J4Bzvj6fW4vRydy/87o+3fCua3x8m/ETqALbu1vJ2Vv2/Yul27a 12 | 4kzusJb0iu78tfn+NPUe/Z2ZmRj5kOa5oX4YvYLFYq7BemrDu18mcxPwum7qNjJ9 13 | E4NjGTUKhs5HMl7E+CeapaRhJRWhyoNyzdpFA/TFCXDvQdSTX0wN7DZQgGZBpxEX 14 | aFTG0c0BZ7yMurYqDO0CAwEAAaOBwTCBvjCBjgYDVR0jBIGGMIGDoXakdDByMQsw 15 | CQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCU1lbmxvUGFyazEMMAoG 16 | A1UECgwDT05GMRQwEgYDVQQLDAtFbmdpbmVlcmluZzEeMBwGA1UEAwwVY2Eub3Bl 17 | bm5ldHdvcmtpbmcub3JnggkA3vfX0jfasUkwCQYDVR0TBAIwADALBgNVHQ8EBAMC 18 | BPAwEwYDVR0RBAwwCoIIb25vcy1ndWkwDQYJKoZIhvcNAQELBQADggEBAFa/ug+x 19 | Mrq3g0p3sHTNR82FjjQKPHW4E0h5kGALvQqtwNOyClls9Ik4wYIZNKyHDXOcWP4I 20 | NOgBho8WHh39bixjz0dka96H88u3WptqBKAySk1UGaSmqHVNxGSTtCpwV17QMh0f 21 | +KsnbxVulcQZeOTbgpFoAuCB9S+u4N5F8j40FkjUbIj3rLN76eidinUMYHfeaZ+K 22 | uP8kU+EwPvh70RuX3fOSPUPNdMV1VjtAnPrvQPuqi3K/9FPEtr+YCuXSTk0+Qpcn 23 | 25wdQKAz7ppglbfWYjowtGWU8POx+wuXHyFTvrRrCy+JYnM8DUlPKP1/oDKneoy0 24 | 3h4nV209NiJ4c/A= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package utils 6 | 7 | import ( 8 | pb "github.com/openconfig/gnmi/proto/gnmi" 9 | ) 10 | 11 | var leafValue string 12 | 13 | // FindLeaf finds a leaf in the given JSON tree that is stored in a map. 14 | func FindLeaf(aMap map[string]interface{}, leaf string) (string, error) { 15 | for key, val := range aMap { 16 | switch concreteVal := val.(type) { 17 | case map[string]interface{}: 18 | FindLeaf(val.(map[string]interface{}), leaf) 19 | case []interface{}: 20 | ParseArray(val.([]interface{}), leaf) 21 | default: 22 | if leaf == key { 23 | leafValue = concreteVal.(string) 24 | break 25 | } 26 | 27 | } 28 | } 29 | return leafValue, nil 30 | 31 | } 32 | 33 | // ParseArray Parses a given array 34 | func ParseArray(array []interface{}, leaf string) { 35 | for _, val := range array { 36 | switch val.(type) { 37 | case map[string]interface{}: 38 | FindLeaf(val.(map[string]interface{}), leaf) 39 | case []interface{}: 40 | ParseArray(val.([]interface{}), leaf) 41 | 42 | } 43 | } 44 | } 45 | 46 | // GnmiFullPath builds the full path from the prefix and path. 47 | func GnmiFullPath(prefix, path *pb.Path) *pb.Path { 48 | fullPath := &pb.Path{Origin: path.Origin} 49 | if path.GetElement() != nil { 50 | fullPath.Element = append(prefix.GetElement(), path.GetElement()...) 51 | } 52 | if path.GetElem() != nil { 53 | fullPath.Elem = append(prefix.GetElem(), path.GetElem()...) 54 | } 55 | return fullPath 56 | } 57 | -------------------------------------------------------------------------------- /pkg/gnmi/modeldata/modeldata.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package modeldata contains the following model data in gnmi proto struct: 6 | // openconfig-interfaces 2.0.0, 7 | // openconfig-openflow 0.1.0, 8 | // openconfig-platform 0.5.0, 9 | // openconfig-system 0.2.0. 10 | package modeldata 11 | 12 | import ( 13 | pb "github.com/openconfig/gnmi/proto/gnmi" 14 | ) 15 | 16 | const ( 17 | // OpenconfigInterfacesModel is the openconfig YANG model for interfaces. 18 | OpenconfigInterfacesModel = "openconfig-interfaces" 19 | // OpenconfigOpenflowModel is the openconfig YANG model for openflow. 20 | OpenconfigOpenflowModel = "openconfig-openflow" 21 | // OpenconfigPlatformModel is the openconfig YANG model for platform. 22 | OpenconfigPlatformModel = "openconfig-platform" 23 | // OpenconfigSystemModel is the openconfig YANG model for system. 24 | OpenconfigSystemModel = "openconfig-system" 25 | ) 26 | 27 | var ( 28 | // ModelData is a list of supported models. 29 | ModelData = []*pb.ModelData{{ 30 | Name: OpenconfigInterfacesModel, 31 | Organization: "OpenConfig working group", 32 | Version: "2017-07-14", 33 | }, { 34 | Name: OpenconfigOpenflowModel, 35 | Organization: "OpenConfig working group", 36 | Version: "2017-06-01", 37 | }, { 38 | Name: OpenconfigPlatformModel, 39 | Organization: "OpenConfig working group", 40 | Version: "2016-12-22", 41 | }, { 42 | Name: OpenconfigSystemModel, 43 | Organization: "OpenConfig working group", 44 | Version: "2017-07-06", 45 | }} 46 | ) 47 | -------------------------------------------------------------------------------- /pkg/gnmi/capabilities.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "github.com/golang/protobuf/proto" 10 | protobuf "github.com/golang/protobuf/protoc-gen-go/descriptor" 11 | pb "github.com/openconfig/gnmi/proto/gnmi" 12 | "golang.org/x/net/context" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | // Capabilities returns supported encodings and supported models. 18 | func (s *Server) Capabilities(ctx context.Context, req *pb.CapabilityRequest) (*pb.CapabilityResponse, error) { 19 | ver, err := getGNMIServiceVersion() 20 | if err != nil { 21 | return nil, status.Errorf(codes.Internal, "error in getting gnmi service version: %v", err) 22 | } 23 | return &pb.CapabilityResponse{ 24 | SupportedModels: s.model.modelData, 25 | SupportedEncodings: supportedEncodings, 26 | GNMIVersion: ver, 27 | }, nil 28 | } 29 | 30 | // getGNMIServiceVersion returns a pointer to the gNMI service version string. 31 | // The method is non-trivial because of the way it is defined in the proto file. 32 | func getGNMIServiceVersion() (string, error) { 33 | parentFile := (&pb.Update{}).ProtoReflect().Descriptor().ParentFile() 34 | options := parentFile.Options() 35 | version := "" 36 | if fileOptions, ok := options.(*protobuf.FileOptions); ok { 37 | ver, err := proto.GetExtension(fileOptions, pb.E_GnmiService) 38 | if err != nil { 39 | return "", err 40 | } 41 | version = *ver.(*string) 42 | } 43 | return version, nil 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pkg/certs/generate_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SUBJBASE="/C=US/ST=CA/L=MenloPark/O=ONF/OU=Engineering/" 4 | DEVICE=${1:-device1.opennetworking.org} 5 | SUBJ=${SUBJBASE}"CN="${DEVICE} 6 | 7 | print_usage() { 8 | echo "Generate a certificate." 9 | echo 10 | echo "Usage: " 11 | echo " [-h | --help]" 12 | echo "Options:" 13 | echo " DEVICENAME e.g. device1.opennetworking.org or localhost" 14 | echo " [-h | --help] Print this help" 15 | echo ""; 16 | } 17 | 18 | # Print usage 19 | if [ "${1}" = "-h" -o "${1}" = "--help" ]; then 20 | print_usage 21 | exit 0 22 | fi 23 | 24 | if [ "${PWD##*/}" != "certs" ]; then 25 | cd certs 26 | fi 27 | 28 | rm -f ${DEVICE}.* 29 | 30 | # Generate Server Private Key 31 | openssl req \ 32 | -newkey rsa:2048 \ 33 | -nodes \ 34 | -keyout ${DEVICE}.key \ 35 | -noout \ 36 | -subj $SUBJ \ 37 | > /dev/null 2>&1 38 | 39 | # Generate Req 40 | openssl req \ 41 | -key ${DEVICE}.key \ 42 | -new -out ${DEVICE}.csr \ 43 | -subj $SUBJ \ 44 | > /dev/null 2>&1 45 | 46 | # Generate x509 with signed CA 47 | openssl x509 \ 48 | -req \ 49 | -in ${DEVICE}.csr \ 50 | -CA onfca.crt \ 51 | -CAkey onfca.key \ 52 | -CAcreateserial \ 53 | -days 3650 \ 54 | -sha256 \ 55 | -out ${DEVICE}.crt \ 56 | > /dev/null 2>&1 57 | 58 | rm ${DEVICE}.csr 59 | 60 | echo " == Certificate Generated: "${DEVICE}.crt" ==" 61 | openssl verify -verbose -purpose sslserver -CAfile onfca.crt ${DEVICE}.crt > /dev/null 2>&1 62 | exit $? 63 | #To see full details run 'openssl x509 -in "${TYPE}${INDEX}".crt -text -noout' 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /pkg/gnmi/datetime.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package gnmi 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/openconfig/ygot/ygot" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | 16 | "github.com/golang/protobuf/proto" 17 | pb "github.com/openconfig/gnmi/proto/gnmi" 18 | ) 19 | 20 | // SetDateTime update current-datetime field in runtime 21 | func (s *Server) SetDateTime() error { 22 | s.configMu.Lock() 23 | defer s.configMu.Unlock() 24 | var path pb.Path 25 | textPbPath := `elem: elem: elem: ` 26 | if err := proto.UnmarshalText(textPbPath, &path); err != nil { 27 | return err 28 | } 29 | 30 | val := &pb.TypedValue{ 31 | Value: &pb.TypedValue_StringVal{ 32 | StringVal: time.Now().Format("2006-01-02T15:04:05Z-07:00"), 33 | }, 34 | } 35 | update := &pb.Update{Path: &path, Val: val} 36 | 37 | jsonTree, _ := ygot.ConstructIETFJSON(s.config, &ygot.RFC7951JSONConfig{}) 38 | _, _ = s.doReplaceOrUpdate(jsonTree, pb.UpdateResult_UPDATE, nil, update.GetPath(), update.GetVal()) 39 | jsonDump, err := json.Marshal(jsonTree) 40 | if err != nil { 41 | msg := fmt.Sprintf("error in marshaling IETF JSON tree to bytes: %v", err) 42 | log.Error(msg) 43 | return status.Error(codes.Internal, msg) 44 | } 45 | rootStruct, err := s.model.NewConfigStruct(jsonDump) 46 | if err != nil { 47 | msg := fmt.Sprintf("error in creating config struct from IETF JSON data: %v", err) 48 | log.Error(msg) 49 | return status.Error(codes.Internal, msg) 50 | } 51 | s.config = rootStruct 52 | s.ConfigUpdate.In() <- update 53 | return nil 54 | 55 | } 56 | -------------------------------------------------------------------------------- /pkg/certs/client1.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDK2amlhSGecWBI 3 | 9jlj3OvCoGMAlHXffwHAsHskE/bFkyRKeYs0MHlcNBXSjgADtHt7FpGJ7nbZsMdu 4 | 0XkNOlN/X0i8FKU7X9DnOHbHhOitg469J89D8tHnN2wrTyGxY2Km95PxeCQ4NH7z 5 | 4MSSbdFfFXnw9k+qr+e1owswloJsYo+eyP7hjy7QvR2sAX8CC6D0ZaVuqLGK5kHJ 6 | UyMxifqMa4D4wD8zfzahcTe2QAeOxQwhsaK2pZ/09bI/KCj8vxbxG2nkcwUgjl0L 7 | gh0g1BFNSu39v1DjsiFWrIxnj3CL2Ncu0ps0+dPSdpx90ImvjL4BRPz8V4iLPljm 8 | BmQfU2HpAgMBAAECggEAVEd9Ca03m5nldEsA6zHVrmZu28XS94nQU5u/fezhgZMx 9 | 59N597QQKDPnwTSIYwGwsCJfU5yFOssNAUj873cFTA1trd8yC2oy5G58Q0dAWR8o 10 | xgRtRAD2HwfS5GebSxVM3qxMhm3xNnzxJiiD44bHD6dfo7LixLsTHU9hjc1q4NaR 11 | BfJCl01f1EgEtPB4L7l3E1ivgy2OQ91gSJsIneA4RFLn5NbMac7ZWOTMGThjGFAw 12 | qR38Ua7TzQXL+qNzHU0JUOBg4smk5tzPxD8ik2p8VTc8WrPEBX129fn+JumhKoIC 13 | 4LysZoazLhKem56LBVSW/TNybXPy9z+nBhbFbnoLAQKBgQDpPfvc83f/f5Iu3PaG 14 | 8BsIoOonasqGsFoDB7XN/DZdjk8d9MKF+2tK8tRtwfZ+cWJL7m94ZCudtU72uhwN 15 | 0x1E/p+qAUBvE3WR3MxQQb6R5kyzMUlwTjwrM49kMuQdU7CaL2abqRvvekkE6L13 16 | 9KesvE0TQSTx+beMbzUUdDyCCQKBgQDepInF/pmwbM0E2zKYny0Ro3caW6HoUEoJ 17 | ZGcZ/NLdTgiQ3GJsVddc0M4jgXMndjOJFZcDDeFPgRAgQVnJHbDDWNygmNDBKavj 18 | LNcTIB2AO4ObWIH2C6x38V6l+62Nc9QPlI52tiekWVO1tAhy3FJXRHAUhP43bi36 19 | VnCg0mJY4QKBgFuSTEnpBJm4+imP8vHzXom6s3OaR70ti4lZA5XFiYqdjo5SQ/Ta 20 | Srt4LtKQrjfiSBdLm1QG7+DRCBlx5AXBduJZnVHff+6cEzKbH1P7G9ioNEC9/vkq 21 | nhDQA2HxYQHqk5FVPtGqSR9yQSy+O3TXBuWYYCJJFzoxMlDecFaBdCgRAoGBAIRc 22 | qZPOQyyB4nkKn8/ggfjEh+BhraXhZcKjsC/hALOU2r7UZqcleX2ynXq6UO2a9hR/ 23 | g2HLdLHBdwbWEzzfq+DXCYNolmLgFVJfrBWwuBkuSJWoTssqMYS1OKHROGKqA96n 24 | YPLuZC7u9DdIKuWuWj2LcF6imkf19tunXBogOVvBAoGBAJvIYUGba3WU+/Ugs3yZ 25 | ubNBC52mS2kLrOTZfzQOP6sL3OC5rFgm7A4e/CMCWphns98ftB0c1RaZBCcwskUE 26 | mCxUxvivP7oUNf0CMZd6CW2M0LEWMVrIFJ1o0BtNpUVZ/+nipJblRA1B/JLsgNvn 27 | 11mpIWR4KHd8A5wjkkfb2d/e 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/certs/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC2F3yVkmiXIdeC 3 | NMYNzltYaRdUPptIRz9nKCFaKHWu+tVB1Pz0nh1xShQ3ObIkhQ8g4v+bms8tEbcZ 4 | XiVeD5hOJJEYse04HZ1EoqpXQB+xl8gFN5UMu0mEGC67tEkXi8narqSEebqUJtPK 5 | MGBM4F9QEre1dOgOiI5+LOZmGgd58DKzF0Tel1SVv7k0ysZ4dzEnsitaulWPBKz7 6 | UzQ4OFyY/boN72tJ9Q3BiqDnUQH/ac+kqg8G1kXUn5EsAH3d6Nv/ku3lW0Gr9uxI 7 | y/jiwvZVTG7l792jPNzRI3LmnffAzg/nGcqTQXZgAvxPz7eAc7c2v4xXrcp9/tWU 8 | NEa9alN5AgMBAAECggEAcrj1ax7k+mL97jDlnwkmD9uWMSOInc8VqR5ldPIMwwOR 9 | nHpeLJf5oMi1V93n2I5ka6nYtOaiJJkGrNrd3BcjNAhhyhc/h51Q2k9J1tK1pSQl 10 | hvPv2iedN7Ysq2H4svcFY9uoFzbCUFjuEnLMGWM7aa2BRLe1BIMQk3oiZq17jFyy 11 | v/IYDI8jNFLGyQFwP3iO5EwCIza1MOeKz1vXKPqnLYUk6VjDaOYH3hPqeaMdfzAp 12 | 1NzEZtzfPkJFYu11qmoNwZmTEthhIyJ1pKn82CuE8CiL8jdSQq144k1YR19NmEZX 13 | HU5hZvijU4Iz+1NkAfka6MYuYXEvUKkqkqQzfwnjiQKBgQDfKOAxK1/xalbn9wjo 14 | SkywpmRjbS6M2q7D4fTk7O7PGLTSjroFRcNUe7HI8+jH3GK7Jm1ddViUNaWqQb+8 15 | 9Q7SAAro+CakAuPAbf0e4VXzzyHE/lvtruc/SL953wZGIoAZ38TXsi4fm/UTD9Ox 16 | hVqu/N5AvY4P1g6IXfqynPdXjwKBgQDQ43QStDnOjsiwImOOsKjefhYtHhZQvGQf 17 | cXE3d+qarmxtifYkEyPLLYtQkb8biBDVJaWnEVcqg9Kuxbq6yOLROGPRtio2WIns 18 | FiHiu/h6sgdsBfzKIlcn23UU0Fce2nuMLwC8PGoWkdMQPR+6NTKe1qKkkzhDpA3x 19 | TbGJSzVgdwKBgG5ReLMV7DIeDaRSnRaoVE0nlI0KVm7PVIIFW9knv86lOg60/ATL 20 | Pgqvs23SFgtnSW+XSY1gC1AJTUJjinPQ+WibGMmekwuVWh2wwebYInOKu/j0fWF8 21 | i1jfj7ihpipZt9YSpu6yaNa7dGXd9xrU/8VtwDlk+6uceEa1ns9ZhXTFAn8SxFyp 22 | UYfgBvQA3xYSu8xwMOPNKebXWhWkvYxub1ekjgcv0DVNCGsu1eiuVGnXD2Jzw+4e 23 | FHDAYReMnDcqkOHP6kENllA0kb/SdiqVNE4et9/y1JbhkjRCYHUkaZNqMjbnYVGv 24 | l73wSSmtS9CN6jmiC6aRIqjratHV3CUXMKqbAoGAHvRu9AjfHAnURpUk93DBvMoe 25 | soydv29w2p6V5aiEj0+qRcoDZc1iO4QVy463twkKyEduW+pSJPwYI3/KZCf1zPvZ 26 | apAnh710u1lw5DBjSDN9H+ZL2m6myYy2vdsTUh0T8TN3J5i7kUpobCj7fGvng8dt 27 | f/cOAHPSczsq+mGIYxo= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/certs/onfca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBIOwmUV/GPrMi 3 | h2kAoepjSC9Q9Ouba8F/EZ7ijNpAcErtN+M3n0AD5aFroEqCYCZ3+X3tI51A/IDd 4 | 0pvF0YQpL3+u6c32m4MOJ/HiZh0nMi/7oMhMQeimocM59+27d6E94Eqoa+j1M51i 5 | fH731bgh2WZL/xz0aeHsgHBs2yeE7gdIKOLOlRNFlOTgsQkpQIit5b6Va9wzi3WC 6 | 1aAmuavArZd8Fw630y8OfwaX64PXxfRA9v2CiHVfCmdpIv4MBDkKlnCqfNC5E5Xz 7 | lrHRo6eKKT7j0+54zARlzCcyfPydXtWibyLKAe5/F6375DYli+VN3zODe1GTfY33 8 | Je1C5pu3AgMBAAECggEBAJ25mIM2xAQw4rWZZyzoD3dj1ZjFXcIBv/ZZ2qvlIr2r 9 | t9WCZiPgADTujeVK9G8DvYcQEELiaiRP06LVxPhKwyerrhiYw+faW67s+oPOp5KC 10 | T2OujMaSsUGdLMaj79jBu8K/8dkYwBm5PJjZWgxn08h5Ny1rvSbzblprDuYoGV9a 11 | ZrzDTpsad46WowMvQMAb2YSuWDq3nWlou78UqDnPIUvlO+77M6q5KEIzU2aU0UjG 12 | nqJXTEqdK9OpUeRMstn6HQYAv4GJiFU+GlzTDdXWr/PvhbpR4213QgqwfusbHTF2 13 | upJb58H3F12/q4qxL/YpVULLUV66PmV/oy1hciX1l0kCgYEA6AwDnXZvjHgI0zMj 14 | XUQWkJljh6PnfK091/N8k9ilh9UNylpQnIOU/F1CEBwkF5FCQ1TIShnkvIznCPty 15 | Ypxcc6Z25oTsp+jIt+mySfSrBINQJY1kSk5rNifLl1LjgPVHobDR6HhqoQF1rjVc 16 | l4a8uQM7Z8WU3WbLXxcKc5zITK0CgYEA1RB3lGBh7r3n5RKXwlSYXfb/jaYwxUha 17 | azXWR5aCtUydrul24fugprO6LvTr6bw4MIwckfxSXHZMnCUtShkhmATj4j0NXUi8 18 | Ul7yNgeatGVFhXtX7uXoByVLpHnGOMEQIPICRdAXn7wrlJi1w0VO/KIbv2k5V6oX 19 | blYHauaoEnMCgYAL5ZHJ4OiXKxBIw7ZscbE2eKbBrYWQvtEM4U7hxZm2/RVX6ol3 20 | fMeGqMFaUhcHnkrnaNFb+zfe8tple37Bz4Jt63rtFqOLeEPSKgkaAZFDCfhx9G3P 21 | 4XVdsWyetYE0e17Qy1/3qzTMTGbcJ6A3pJDIa4IAMAER2NNUbLn8c21RgQKBgQCP 22 | MigMEtL+76dA78QLGWvmCzEp9D3m+X+7ek1vg5qJWtrHSaVasBECuNwy3u0HBDcH 23 | ecNh4iFAf0lx0BKmMEnBr9ewn7OxtEbNXX6QDYBOKZoV4hXxO5c75jb2bdlpH2hF 24 | nCEm5npaIs9vaUsP2C8D37eiZ4fggTKKN0t471iaPQKBgQDa9fFyMbWWUN0OcBqO 25 | byPK2tsS6yHqA2nwP4nb7HNOGBwl1KsKCnfCUUl0qDL7TL8sU9UyNoYM8Yi4yTxY 26 | CfYNMeoPk/MlKtcXDmz3IKdy+fNh256Y/bdwNQWJoWcXHZARH7bvCNiS6v2Y4oo1 27 | c/nOg/HJW5J+lvKGbOw61vOtZg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/certs/onos-gui.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvA4FXqLV6GdKB 3 | bwSwJAPjlyxkacvzStSZ/uGefJeLffSBdRbES2F++uPc2cWeLP/oEaktiJeanPZp 4 | m1GSFVlOizbGmxtvSiEizyXUfu/24L+MJJp0WtX5XpeTiaAP8BKkFi303yCWrbub 5 | StyeAc74+n1uL0cncv/O6Pt3wrmt8fJvxE6gC27tbydlb9v2Lpdu2uJM7rCW9Iru 6 | /LX5/jT1Hv2dmZkY+ZDmuaF+GL2CxWKuwXpqw7tfJnMT8Lpu6jYyfRODYxk1CobO 7 | RzJexPgnmqWkYSUVocqDcs3aRQP0xQlw70HUk19MDew2UIBmQacRF2hUxtHNAWe8 8 | jLq2KgztAgMBAAECggEBAJQ117cwfF8mtwo9xi6UkWaPg1yV683hNSIko1TgFkZf 9 | KEzpp5ocbDhop8dD9QL7AMy7CBYzco/RFSxiCDY6NiM56e1PNXCNynn8CwFlbjoY 10 | Ip6/8L1Qn5xK6vpatl5I5MBouAqDWsm/3vyn7SUySuC24LoK96sEzHWhADRvh2cx 11 | 8xZVdhYH6zBQp/+KErrK0BLcVpMfV/yJk2y0mrXafpdymndfz7TBOXCyKQ0cXFne 12 | 14zLw23TlKp6JDSHw3QEaSgVQPmu+BdYfX2SMT3MLt6Im7z3aOG8y3whkt96yL56 13 | I7LBQ/KROfJGL8i8zK3KZNi7IwftKv+x0UU5OdbaA4ECgYEA3+lAJO6en+6lTYMg 14 | hYG7dzjaeW/3XfDIwKvi/5Xv3eA4HOzZ8GhxHssXV9f1VTGMK10vPBPEQBmvioDf 15 | qvMgP76qm85TK86CFqGayZ5ShIyNblOeMUnFanW6y7MylIu9Dmw94CS4tkL4zENG 16 | BpKzoXAMFc9996mQBK9/YxatVKkCgYEAyBhT4Efz+JAkT+3Zr+rIph3ZGoGu0RJ3 17 | zl20su3kzl+QuUVpcbkbsEupjJJ2r6LtAllcSxmnoJ+/f6ckEISNsrfy0ycRW7k5 18 | pqF3Xp9b3XrzQ6bMxyQVSgm1zIfftsCaJ0bexpMjy6acokUWXrE5Buy1qHLev58F 19 | MGP17tewHKUCgYBZILimutErakwkcYi1e/GKQHg+lIILw7e1cfY2tJE5aXIMmX8b 20 | AgfdMQxGrDD8y283J62QpXGd7luAr1HY81Qn65Zv1I4oxtfjeEpr7Ph9yJDXlLNI 21 | fUv214wWX2tH0+PaZN2wZg2ch0YP0MuD/EtCfJ5i5CgJOFaadt0nLTSrmQKBgQCa 22 | c0TTG1czpzeQRt1AT+8/YkzBjBZ/pUy7C1O+xahWsCeLCwwgTy0TQOQH8MoSOqXN 23 | qWJ3Sb89WfG8PCy3X0ntCNYzrLVWYrwgZgQ5ErMbW5tIvgjVMoIIW0RsMvk5HKQg 24 | 6zBsgQkhWmMPUlq5Dv0g3jg+ZSSRLtMXjiE4kl6LTQKBgB9DTdNba8HXgavWOAjm 25 | M37pa+1CqsohKEyHNBPBJUUyvK6XxzsooX6x9X2PvwsJJ+MaMTv6vnH+SVm6ZJ8u 26 | K6iBd3bjQx0G7QXhGT75MSFSOeiMHtbFwh++C7O7s+A3XT8yUwnyG9STH6YhCoDf 27 | jFvx0Laxe0FBnNj4lIzaDFhf 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/events/events.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package events define a generic type of event for implementing of 6 | // an event distribution mechanism. 7 | package events 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | // EventType is an enumeration of the kind of events that can occur. 14 | type EventType uint16 15 | 16 | // Values of the EventType enumeration 17 | const ( // For event types 18 | EventTypeConfiguration EventType = iota 19 | EventTypeOperationalState 20 | EventTypeRandom 21 | ) 22 | 23 | func (et EventType) String() string { 24 | return [...]string{"Configuration", "OperationalState", "RandomEvent"}[et] 25 | } 26 | 27 | // Event an interface which defines the Event methods 28 | type Event interface { 29 | GetType() EventType 30 | GetTime() time.Time 31 | GetValues() interface{} 32 | GetSubject() string 33 | Clone() Event 34 | } 35 | 36 | // EventHappend is a general purpose base type of event 37 | type EventHappend struct { 38 | Subject string 39 | Time time.Time 40 | Etype EventType 41 | Values interface{} 42 | Client interface{} 43 | } 44 | 45 | // Clone clones the Event 46 | func (eh *EventHappend) Clone() Event { 47 | clone := &EventHappend{} 48 | clone.Etype = eh.Etype 49 | clone.Subject = eh.Subject 50 | clone.Time = eh.Time 51 | clone.Values = eh.Values 52 | return clone 53 | } 54 | 55 | // GetType returns type of an Event 56 | func (eh *EventHappend) GetType() EventType { 57 | return eh.Etype 58 | } 59 | 60 | // GetTime returns the time when the event occurs 61 | func (eh *EventHappend) GetTime() time.Time { 62 | return eh.Time 63 | } 64 | 65 | // GetValues returns the values of the event 66 | func (eh *EventHappend) GetValues() interface{} { 67 | return eh.Values 68 | } 69 | 70 | // GetSubject returns the subject of the event 71 | func (eh *EventHappend) GetSubject() string { 72 | return eh.Subject 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | export CGO_ENABLED=1 6 | export GO111MODULE=on 7 | 8 | .PHONY: build 9 | 10 | ONOS_SIMULATORS_VERSION := latest 11 | ONOS_BUILD_VERSION := v0.6.0 12 | 13 | all: build images 14 | 15 | build-tools:=$(shell if [ ! -d "./build/build-tools" ]; then cd build && git clone https://github.com/onosproject/build-tools.git; fi) 16 | include ./build/build-tools/make/onf-common.mk 17 | 18 | images: # @HELP build simulators image 19 | images: simulators-docker 20 | 21 | # @HELP build the go binary in the cmd/gnmi_target package 22 | build: deps 23 | go build -o build/_output/gnmi_target ./cmd/gnmi_target 24 | 25 | test: build deps license linters 26 | go test github.com/onosproject/gnxi-simulators/pkg/... 27 | go test github.com/onosproject/gnxi-simulators/cmd/... 28 | 29 | jenkins-test: # @HELP run the unit tests and source code validation producing a junit style report for Jenkins 30 | jenkins-test: deps license linters 31 | TEST_PACKAGES=github.com/onosproject/gnxi-simulators/... ./build/build-tools/build/jenkins/make-unit 32 | 33 | simulators-docker: 34 | docker build . -f Dockerfile \ 35 | --build-arg ONOS_BUILD_VERSION=${ONOS_BUILD_VERSION} \ 36 | -t onosproject/device-simulator:${ONOS_SIMULATORS_VERSION} 37 | 38 | kind: # @HELP build Docker images and add them to the currently configured kind cluster 39 | kind: images 40 | @if [ "`kind get clusters`" = '' ]; then echo "no kind cluster found" && exit 1; fi 41 | kind load docker-image onosproject/device-simulator:${ONOS_SIMULATORS_VERSION} 42 | 43 | publish: # @HELP publish version on github and dockerhub 44 | ./build/build-tools/publish-version ${VERSION} onosproject/device-simulator 45 | 46 | jenkins-publish: # @HELP Jenkins calls this to publish artifacts 47 | ./build/bin/push-images 48 | ./build/build-tools/release-merge-commit 49 | 50 | clean:: # @HELP remove all the build artifacts 51 | rm -rf ./build/_output 52 | rm -rf ./vendor 53 | rm -rf ./cmd/gnmi_target/gnmi_target 54 | 55 | -------------------------------------------------------------------------------- /pkg/gnmi/defs.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "sync" 10 | 11 | "github.com/eapache/channels" 12 | 13 | pb "github.com/openconfig/gnmi/proto/gnmi" 14 | "github.com/openconfig/ygot/ygot" 15 | ) 16 | 17 | // ConfigCallback is the signature of the function to apply a validated config to the physical device. 18 | type ConfigCallback func(ygot.ValidatedGoStruct) error 19 | 20 | var ( 21 | pbRootPath = &pb.Path{} 22 | supportedEncodings = []pb.Encoding{pb.Encoding_JSON, pb.Encoding_JSON_IETF} 23 | dataTypes = []string{"config", "state", "operational", "all"} 24 | ) 25 | 26 | // Server struct maintains the data structure for device config and implements the interface of gnmi server. It supports Capabilities, Get, and Set APIs. 27 | // Typical usage: 28 | // g := grpc.NewServer() 29 | // s, err := Server.NewServer(model, config, callback) 30 | // pb.NewServer(g, s) 31 | // reflection.Register(g) 32 | // listen, err := net.Listen("tcp", ":8080") 33 | // g.Serve(listen) 34 | // 35 | // For a real device, apply the config changes to the hardware in the callback function. 36 | // Arguments: 37 | // newConfig: new root config to be applied on the device. 38 | // func callback(newConfig ygot.ValidatedGoStruct) error { 39 | // // Apply the config to your device and return nil if success. return error if fails. 40 | // // 41 | // // Do something ... 42 | // } 43 | type Server struct { 44 | model *Model 45 | callback ConfigCallback 46 | config ygot.ValidatedGoStruct 47 | ConfigUpdate *channels.RingChannel 48 | configMu sync.RWMutex // mu is the RW lock to protect the access to config 49 | subMu sync.RWMutex 50 | readOnlyUpdateValue *pb.Update 51 | subscribers map[string]*streamClient 52 | } 53 | 54 | var ( 55 | lowestSampleInterval uint64 = 5000000000 // 5000000000 nanoseconds 56 | ) 57 | 58 | type streamClient struct { 59 | target string 60 | sr *pb.SubscribeRequest 61 | stream pb.GNMI_SubscribeServer 62 | errChan chan error 63 | UpdateChan chan *pb.Update 64 | sampleInterval uint64 65 | } 66 | -------------------------------------------------------------------------------- /pkg/certs/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # ONF Certificates 8 | This is a chain that generates certificates for the device simulator 9 | 10 | ``` 11 | Certificate Authority (ca.opennetworking.org) 12 | / \ \---- localhost 13 | / \ 14 | client1.opennetworking.org \---- 15 | ``` 16 | 17 | The certificate authority (CA) and the client1 and localhosts certificates are 18 | created permanently, versioned in git and should not need to be regenerated. 19 | 20 | To see the details of each use: 21 | ``` 22 | openssl x509 -in onfca.crt -text -noout 23 | openssl x509 -in client1.crt -text -noout 24 | openssl x509 -in localhost.crt -text -noout 25 | ``` 26 | 27 | > Device specific certificates can be created when the device simulator starts up 28 | > when running in Linux ([see devicesim](../README.md)) and are keyed to the 29 | > GNMI_TARGET evnironment variable passed in to the simulator. 30 | 31 | ## Generate new cert 32 | Use the generate_certs.sh script 33 | ```bash 34 | Usage: 35 | [-h | --help] 36 | Options: 37 | DEVICENAME e.g. device1.opennetworking.org or localhost 38 | [-h | --help] Print this help" 39 | ``` 40 | 41 | ## Regenerating Certificate Authority 42 | The should be no need to recreate the CA as it is valid until April 2029, but 43 | if you do here are the steps to take from a bash shell. 44 | 45 | ``` 46 | export SUBJ="/C=US/ST=CA/L=MenloPark/O=ONF/OU=Engineering/CN=ca.opennetworking.org" 47 | openssl req -newkey rsa:2048 -nodes -keyout onfca.key -subj $SUBJ 48 | openssl req -key onfca.key -new -out onfca.csr -subj $SUBJ 49 | openssl x509 -signkey onfca.key -in onfca.csr -req -days 3650 -out onfca.crt 50 | rm onfca.csr 51 | ``` 52 | 53 | ## Signed Certificate authority 54 | At some stage in this project it is expected that a Certificate Authority will 55 | be created from some well know chain e.g. GlobalSign or 56 | [Comodo](https://comodosslstore.com/promoads/cheap-comodo-ssl-certificates.aspx) 57 | 58 | This would mean that any certificates derived from this chain would be well 59 | known in the major operating systems and then utilities like cURL would not have 60 | to import the CA every time. 61 | 62 | -------------------------------------------------------------------------------- /pkg/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package dispatcher : 6 | package dispatcher 7 | 8 | import ( 9 | "reflect" 10 | "sync" 11 | 12 | "github.com/onosproject/onos-lib-go/pkg/logging" 13 | 14 | "github.com/onosproject/gnxi-simulators/pkg/events" 15 | ) 16 | 17 | var log = logging.GetLogger("dispatcher") 18 | 19 | // Dispatcher dispatches the events 20 | type Dispatcher struct { 21 | handlers map[reflect.Type][]reflect.Value 22 | lock *sync.RWMutex 23 | } 24 | 25 | // NewDispatcher creates an instance of Dispatcher struct 26 | func NewDispatcher() *Dispatcher { 27 | return &Dispatcher{ 28 | handlers: make(map[reflect.Type][]reflect.Value), 29 | lock: &sync.RWMutex{}, 30 | } 31 | } 32 | 33 | // RegisterEvent registers custom events making it possible to register listeners for them. 34 | func (d *Dispatcher) RegisterEvent(event events.Event) bool { 35 | d.lock.Lock() 36 | defer d.lock.Unlock() 37 | typ := reflect.TypeOf(event).Elem() 38 | log.Info("Registering the ", typ) 39 | if _, ok := d.handlers[typ]; ok { 40 | return false 41 | } 42 | var chanArr []reflect.Value 43 | d.handlers[typ] = chanArr 44 | return true 45 | } 46 | 47 | // RegisterListener registers chanel accepting desired event - a listener. 48 | func (d *Dispatcher) RegisterListener(pipe interface{}) bool { 49 | d.lock.Lock() 50 | defer d.lock.Unlock() 51 | channelValue := reflect.ValueOf(pipe) 52 | channelType := channelValue.Type() 53 | if channelType.Kind() != reflect.Chan { 54 | panic("Trying to register a non-channel listener") 55 | } 56 | channelIn := channelType.Elem() 57 | if arr, ok := d.handlers[channelIn]; ok { 58 | d.handlers[channelIn] = append(arr, channelValue) 59 | return true 60 | } 61 | return false 62 | } 63 | 64 | // Dispatch provides thread safe method to send event to all listeners 65 | // Returns true if succeeded and false if event was not registered 66 | func (d *Dispatcher) Dispatch(event events.Event) bool { 67 | d.lock.RLock() 68 | defer d.lock.RUnlock() 69 | 70 | eventType := reflect.TypeOf(event).Elem() 71 | if listeners, ok := d.handlers[eventType]; ok { 72 | for _, listener := range listeners { 73 | listener.TrySend(reflect.ValueOf(event.Clone()).Elem()) 74 | } 75 | return true 76 | } 77 | return false 78 | } 79 | -------------------------------------------------------------------------------- /cmd/gnmi_target/gnmi_target.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Binary gnmi_target implements a gNMI Target with in-memory configuration and telemetry. 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "io/ioutil" 12 | "net" 13 | "os" 14 | "reflect" 15 | "time" 16 | 17 | "github.com/onosproject/onos-lib-go/pkg/logging" 18 | 19 | "google.golang.org/grpc" 20 | "google.golang.org/grpc/reflection" 21 | 22 | "github.com/onosproject/gnxi-simulators/pkg/gnmi" 23 | "github.com/onosproject/gnxi-simulators/pkg/gnmi/modeldata" 24 | "github.com/onosproject/gnxi-simulators/pkg/gnmi/modeldata/gostruct" 25 | 26 | "github.com/google/gnxi/utils/credentials" 27 | 28 | pb "github.com/openconfig/gnmi/proto/gnmi" 29 | ) 30 | 31 | var log = logging.GetLogger("main") 32 | 33 | func main() { 34 | model := gnmi.NewModel(modeldata.ModelData, 35 | reflect.TypeOf((*gostruct.Device)(nil)), 36 | gostruct.SchemaTree["Device"], 37 | gostruct.Unmarshal, 38 | gostruct.ΛEnum) 39 | 40 | flag.Usage = func() { 41 | fmt.Fprintf(os.Stderr, "Supported models:\n") 42 | for _, m := range model.SupportedModels() { 43 | fmt.Fprintf(os.Stderr, " %s\n", m) 44 | } 45 | fmt.Fprintf(os.Stderr, "\n") 46 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 47 | flag.PrintDefaults() 48 | } 49 | 50 | flag.Parse() 51 | 52 | opts := credentials.ServerCredentials() 53 | g := grpc.NewServer(opts...) 54 | 55 | var configData []byte 56 | if *configFile != "" { 57 | var err error 58 | configData, err = ioutil.ReadFile(*configFile) 59 | if err != nil { 60 | log.Fatalf("Error in reading config file: %v", err) 61 | } 62 | } 63 | 64 | s, err := newServer(model, configData) 65 | 66 | if err != nil { 67 | log.Fatalf("Error in creating gnmi target: %v", err) 68 | } 69 | pb.RegisterGNMIServer(g, s) 70 | reflection.Register(g) 71 | 72 | log.Infof("Starting gNMI agent to listen on %s", *bindAddr) 73 | listen, err := net.Listen("tcp", *bindAddr) 74 | if err != nil { 75 | log.Fatalf("Failed to listen: %v", err) 76 | } 77 | 78 | go func() { 79 | 80 | for { 81 | s.SetDateTime() 82 | time.Sleep(time.Second * 1) 83 | } 84 | 85 | }() 86 | 87 | log.Infof("Starting gNMI agent to serve on %s", *bindAddr) 88 | if err := g.Serve(listen); err != nil { 89 | log.Fatalf("Failed to serve: %v", err) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /pkg/gnmi/model.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "reflect" 12 | "sort" 13 | 14 | "github.com/openconfig/goyang/pkg/yang" 15 | "github.com/openconfig/ygot/experimental/ygotutils" 16 | "github.com/openconfig/ygot/ygot" 17 | "github.com/openconfig/ygot/ytypes" 18 | 19 | pb "github.com/openconfig/gnmi/proto/gnmi" 20 | cpb "google.golang.org/genproto/googleapis/rpc/code" 21 | ) 22 | 23 | // JSONUnmarshaler is the signature of the Unmarshal() function in the GoStruct code generated by openconfig ygot library. 24 | type JSONUnmarshaler func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error 25 | 26 | // GoStructEnumData is the data type to maintain GoStruct enum type. 27 | type GoStructEnumData map[string]map[int64]ygot.EnumDefinition 28 | 29 | // Model contains the model data and GoStruct information for the device to config. 30 | type Model struct { 31 | modelData []*pb.ModelData 32 | structRootType reflect.Type 33 | schemaTreeRoot *yang.Entry 34 | jsonUnmarshaler JSONUnmarshaler 35 | enumData GoStructEnumData 36 | } 37 | 38 | // NewModel returns an instance of Model struct. 39 | func NewModel(m []*pb.ModelData, t reflect.Type, r *yang.Entry, f JSONUnmarshaler, e GoStructEnumData) *Model { 40 | return &Model{ 41 | modelData: m, 42 | structRootType: t, 43 | schemaTreeRoot: r, 44 | jsonUnmarshaler: f, 45 | enumData: e, 46 | } 47 | } 48 | 49 | // NewConfigStruct creates a ValidatedGoStruct of this model from jsonConfig. If jsonConfig is nil, creates an empty GoStruct. 50 | func (m *Model) NewConfigStruct(jsonConfig []byte) (ygot.ValidatedGoStruct, error) { 51 | rootNode, stat := ygotutils.NewNode(m.structRootType, &pb.Path{}) 52 | if stat.GetCode() != int32(cpb.Code_OK) { 53 | return nil, fmt.Errorf("cannot create root node: %v", stat) 54 | } 55 | 56 | rootStruct, ok := rootNode.(ygot.ValidatedGoStruct) 57 | if !ok { 58 | return nil, errors.New("root node is not a ygot.ValidatedGoStruct") 59 | } 60 | if jsonConfig != nil { 61 | if err := m.jsonUnmarshaler(jsonConfig, rootStruct); err != nil { 62 | return nil, err 63 | } 64 | if err := rootStruct.Validate(); err != nil { 65 | return nil, err 66 | } 67 | } 68 | return rootStruct, nil 69 | } 70 | 71 | // SupportedModels returns a list of supported models. 72 | func (m *Model) SupportedModels() []string { 73 | mDesc := make([]string, len(m.modelData)) 74 | for i, m := range m.modelData { 75 | mDesc[i] = fmt.Sprintf("%s %s", m.Name, m.Version) 76 | } 77 | sort.Strings(mDesc) 78 | return mDesc 79 | } 80 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Deploying the device simulator 8 | 9 | This guide deploys a `device-simulator` through it's [Helm] chart assumes you have a [Kubernetes] cluster running 10 | with an atomix controller deployed in a namespace. 11 | `device-simulator` Helm chart is based on Helm 3.0 version, with no need for the Tiller pod to be present. 12 | If you don't have a cluster running and want to try on your local machine please follow first 13 | the [Kubernetes] setup steps outlined to [deploy with Helm](https://docs.onosproject.org/developers/deploy_with_helm/). 14 | The following steps assume you have the setup outlined in that page, including the `micro-onos` namespace configured. 15 | 16 | Device simulators can be deployed on their own using (from `onos-helm-charts`) 17 | the `device-simulator` chart: 18 | 19 | ```bash 20 | > helm install -n micro-onos devicesim-1 device-simulator 21 | ``` 22 | with output along the lines of 23 | ```bash 24 | NAME: devicesim-1 25 | LAST DEPLOYED: Sun May 12 01:16:41 2019 26 | NAMESPACE: default 27 | STATUS: DEPLOYED 28 | ``` 29 | 30 | The device-simulator chart deploys a single `Pod` containing the device simulator with a `Service` 31 | through which it can be accessed. The device simulator's service can be seen by running the 32 | `kubectl get services` command: 33 | 34 | ```bash 35 | > kubectl get svc 36 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 37 | devicesim-1-device-simulator ClusterIP 10.106.28.52 10161/TCP 25m 38 | ``` 39 | 40 | ### Notify the system about the new simulator. 41 | 42 | To notify the system about the newly added simulator or device please go into `onos-cli` and issue the command 43 | ```bash 44 | onos topo add device devicesim-1 --address devicesim-1-device-simulator:11161 --type Devicesim --version 1.0.0 --insecure 45 | ``` 46 | 47 | ### Installing the chart in a different namespace. 48 | 49 | Issue the `helm install` command substituting `micro-onos` with your namespace. 50 | ```bash 51 | helm install -n devicesim-1 device-simulator 52 | ``` 53 | 54 | ### Deploying multiple simulators 55 | 56 | To deploy multiple simulators, simply install the simulator chart _n_ times 57 | to create _n_ devices, each with a unique name: 58 | 59 | ```bash 60 | > helm install -n micro-onos devicesim-1 device-simulator 61 | > helm install -n micro-onos devicesim-2 device-simulator 62 | > helm install -n micro-onos devicesim-3 device-simulator 63 | ``` 64 | 65 | ### Troubleshoot 66 | 67 | If your chart does not install or the pod is not running for some reason and/or you modified values Helm offers two flags to help you 68 | debug your chart: 69 | 70 | * `--dry-run` check the chart without actually installing the pod. 71 | * `--debug` prints out more information about your chart 72 | 73 | ```bash 74 | helm install -n micro-onos devicesim-1 --debug --dry-run device-simulator 75 | ``` 76 | 77 | [Helm]: https://helm.sh/ 78 | [Kubernetes]: https://kubernetes.io/ 79 | -------------------------------------------------------------------------------- /tools/scripts/run_targets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2022 2020-present Open Networking Foundation 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | hostname=${HOSTNAME:-localhost} 8 | 9 | if [ $SIM_MODE == 1 ]; 10 | then 11 | if [ "$hostname" != "localhost" ]; then \ 12 | IPADDR=`ip route get 1.2.3.4 | grep dev | awk '{print $7}'` 13 | $HOME/certs/generate_certs.sh $hostname > /dev/null 2>&1; 14 | echo "Please add '"$IPADDR" "$hostname"' to /etc/hosts and access with gNMI client at "$hostname":"$GNMI_PORT; \ 15 | else \ 16 | echo "gNMI target in secure mode is on $hostname:"${GNMI_PORT}; 17 | echo "gNMI target insecure mode is on $hostname:"${GNMI_INSECURE_PORT}; 18 | fi 19 | sed -i -e "s/replace-device-name/"$hostname"/g" $HOME/target_configs/typical_ofsw_config.json && \ 20 | sed -i -e "s/replace-motd-banner/Welcome to gNMI service on "$hostname":"$GNMI_PORT"/g" $HOME/target_configs/typical_ofsw_config.json 21 | 22 | gnmi_target \ 23 | -bind_address :$GNMI_INSECURE_PORT \ 24 | -alsologtostderr \ 25 | -notls \ 26 | -insecure \ 27 | -config $HOME/target_configs/typical_ofsw_config.json & 28 | 29 | gnmi_target \ 30 | -bind_address :$GNMI_PORT \ 31 | -key $HOME/certs/$hostname.key \ 32 | -cert $HOME/certs/$hostname.crt \ 33 | -ca $HOME/certs/onfca.crt \ 34 | -alsologtostderr \ 35 | -config $HOME/target_configs/typical_ofsw_config.json 36 | 37 | 38 | elif [ $SIM_MODE == 2 ]; 39 | then 40 | if [ "$hostname" != "localhost" ]; then \ 41 | IPADDR=`ip route get 1.2.3.4 | grep dev | awk '{print $7}'` 42 | echo "Please add '"$IPADDR" "$hostname"' to /etc/hosts and access with gNOI client at "$hostname":"$GNOI_PORT; \ 43 | else \ 44 | echo "gNOI running on $hostname:"$GNOI_PORT; 45 | fi 46 | gnoi_target \ 47 | -bind_address :$GNOI_PORT \ 48 | -alsologtostderr; 49 | elif [ $SIM_MODE == 3 ]; 50 | then 51 | if [ "$hostname" != "localhost" ]; then \ 52 | IPADDR=`ip route get 1.2.3.4 | grep dev | awk '{print $7}'` 53 | $HOME/certs/generate_certs.sh $hostname > /dev/null 2>&1; 54 | echo "Please add '"$IPADDR" "$hostname"' to /etc/hosts and access with gNMI/gNOI clients at "$hostname":"$GNMI_PORT":"$GNOI_PORT":"GNMI_INSECURE_PORT; \ 55 | else \ 56 | echo "gNMI target in secure mode is on $hostname:"${GNMI_PORT}; 57 | echo "gNMI target insecure mode is on $hostname:"${GNMI_INSECURE_PORT}; 58 | echo "gNOI running on $hostname:"$GNOI_PORT; 59 | fi 60 | sed -i -e "s/replace-device-name/"$hostname"/g" $HOME/target_configs/typical_ofsw_config.json && \ 61 | sed -i -e "s/replace-motd-banner/Welcome to gNMI service on "$hostname":"$GNMI_PORT"/g" $HOME/target_configs/typical_ofsw_config.json 62 | gnmi_target \ 63 | -bind_address :$GNMI_PORT \ 64 | -key $HOME/certs/$hostname.key \ 65 | -cert $HOME/certs/$hostname.crt \ 66 | -ca $HOME/certs/onfca.crt \ 67 | -alsologtostderr \ 68 | -config $HOME/target_configs/typical_ofsw_config.json & 69 | 70 | gnmi_target \ 71 | -bind_address :$GNMI_INSECURE_PORT \ 72 | -alsologtostderr \ 73 | -notls \ 74 | -insecure \ 75 | -config $HOME/target_configs/typical_ofsw_config.json & 76 | 77 | gnoi_target \ 78 | -bind_address :$GNOI_PORT \ 79 | -alsologtostderr; 80 | fi 81 | -------------------------------------------------------------------------------- /pkg/gnmi/subscribe.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "time" 12 | 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | 16 | "github.com/openconfig/gnmi/proto/gnmi" 17 | pb "github.com/openconfig/gnmi/proto/gnmi" 18 | ) 19 | 20 | // processSubscribeOnce processes subscribe once requests 21 | func (s *Server) processSubscribeOnce(c *streamClient, request *pb.SubscriptionList) { 22 | go s.collector(c, request) 23 | s.listenForUpdates(c) 24 | 25 | } 26 | 27 | // processSubscribePoll processes subcribe poll requests 28 | func (s *Server) processSubscribePoll(c *streamClient, request *pb.SubscriptionList) { 29 | go s.collector(c, request) 30 | s.listenForUpdates(c) 31 | } 32 | 33 | // processSubStreamOnChange processes subscribe stream requests for on_change subscription mode. 34 | func (s *Server) processSubStreamOnChange(c *streamClient, request *pb.SubscriptionList) { 35 | go s.listenToConfigEvents(request) 36 | 37 | } 38 | 39 | // processSubStreamSample processes subscribe stream requests for sample subscription mode. 40 | func (s *Server) processSubStreamSample(c *streamClient, request *pb.SubscriptionList) { 41 | ticker := time.NewTicker(time.Duration(c.sampleInterval) * time.Nanosecond) 42 | go func() { 43 | for range ticker.C { 44 | s.collector(c, request) 45 | } 46 | }() 47 | s.listenForUpdates(c) 48 | 49 | } 50 | 51 | // Subscribe handle subscribe requests including POLL, STREAM, ONCE subscribe requests 52 | func (s *Server) Subscribe(stream pb.GNMI_SubscribeServer) error { 53 | 54 | c := streamClient{stream: stream} 55 | var err error 56 | c.UpdateChan = make(chan *pb.Update, 100) 57 | 58 | var subscribe *pb.SubscriptionList 59 | var mode gnmi.SubscriptionList_Mode 60 | 61 | for { 62 | c.sr, err = stream.Recv() 63 | 64 | switch { 65 | case err == io.EOF: 66 | return nil 67 | case err != nil: 68 | return err 69 | } 70 | 71 | if c.sr.GetPoll() != nil { 72 | mode = gnmi.SubscriptionList_POLL 73 | } else { 74 | subscribe = c.sr.GetSubscribe() 75 | mode = subscribe.Mode 76 | } 77 | 78 | switch mode { 79 | case pb.SubscriptionList_ONCE: 80 | go s.processSubscribeOnce(&c, subscribe) 81 | case pb.SubscriptionList_POLL: 82 | go s.processSubscribePoll(&c, subscribe) 83 | case pb.SubscriptionList_STREAM: 84 | // Adds streamClient to the list of subscribers 85 | for _, sub := range subscribe.Subscription { 86 | s.addSubscriber(sub.GetPath().String(), &c) 87 | } 88 | 89 | for _, sub := range subscribe.Subscription { 90 | switch sub.GetMode() { 91 | case pb.SubscriptionMode_ON_CHANGE: 92 | go s.processSubStreamOnChange(&c, subscribe) 93 | case pb.SubscriptionMode_SAMPLE: 94 | subSampleInterval := sub.GetSampleInterval() 95 | //If the sample_interval is set to 0, 96 | // the target MUST create the subscription and send the data with the 97 | // lowest interval possible for the target. 98 | if subSampleInterval == 0 { 99 | c.sampleInterval = lowestSampleInterval 100 | } else { 101 | // We assume that the target cannot support 102 | // the sample interval less than the lowest 103 | // sample interval which is defined in the target 104 | if subSampleInterval < lowestSampleInterval { 105 | return status.Error(codes.InvalidArgument, fmt.Sprintf("%s%d", "The sample interval must be higher than ", lowestSampleInterval)) 106 | } 107 | c.sampleInterval = subSampleInterval 108 | 109 | } 110 | go s.processSubStreamSample(&c, subscribe) 111 | case pb.SubscriptionMode_TARGET_DEFINED: 112 | // TODO: when a client creates a 113 | // subscription specifying the target defined mode, 114 | // the target MUST determine the best type of subscription to 115 | // be created on a per-leaf basis. 116 | // That is to say, 117 | // if the path specified within the message refers 118 | // to some leaves which are event driven 119 | // (e.g., the changing of state of an entity based on an external trigger) 120 | // then an ON_CHANGE subscription may be created, 121 | // whereas if other data represents counter values, 122 | // a SAMPLE subscription may be created. 123 | go s.processSubStreamOnChange(&c, subscribe) 124 | 125 | } 126 | 127 | } 128 | 129 | default: 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /cmd/gnmi_target/gnmi_utils.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "time" 10 | 11 | "github.com/onosproject/gnxi-simulators/pkg/gnmi" 12 | "github.com/onosproject/gnxi-simulators/pkg/utils" 13 | pb "github.com/openconfig/gnmi/proto/gnmi" 14 | "github.com/openconfig/ygot/ygot" 15 | "google.golang.org/grpc/codes" 16 | "google.golang.org/grpc/status" 17 | ) 18 | 19 | // newServer creates a new gNMI server. 20 | func newServer(model *gnmi.Model, config []byte) (*server, error) { 21 | s, err := gnmi.NewServer(model, config, nil) 22 | 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | newconfig, _ := model.NewConfigStruct(config) 28 | channelUpdate := make(chan *pb.Update) 29 | server := server{Server: s, Model: model, 30 | configStruct: newconfig, 31 | UpdateChann: channelUpdate} 32 | 33 | return &server, nil 34 | } 35 | 36 | // sendResponse sends an SubscribeResponse to a gNMI client. 37 | func (s *server) sendResponse(response *pb.SubscribeResponse, stream pb.GNMI_SubscribeServer) { 38 | log.Info("Sending SubscribeResponse out to gNMI client: ", response) 39 | err := stream.Send(response) 40 | if err != nil { 41 | //TODO remove channel registrations 42 | log.Errorf("Error in sending response to client %v", err) 43 | } 44 | } 45 | 46 | // buildSubResponse builds a subscribeResponse based on the given Update message. 47 | func buildSubResponse(update *pb.Update) (*pb.SubscribeResponse, error) { 48 | updateArray := make([]*pb.Update, 0) 49 | updateArray = append(updateArray, update) 50 | notification := &pb.Notification{ 51 | Timestamp: time.Now().Unix(), 52 | Update: updateArray, 53 | } 54 | responseUpdate := &pb.SubscribeResponse_Update{ 55 | Update: notification, 56 | } 57 | response := &pb.SubscribeResponse{ 58 | Response: responseUpdate, 59 | } 60 | 61 | return response, nil 62 | } 63 | 64 | // getUpdate finds the node in the tree, build the update message and return it back to the collector 65 | func (s *server) getUpdate(subList *pb.SubscriptionList, path *pb.Path) (*pb.Update, error) { 66 | fullPath := path 67 | prefix := subList.GetPrefix() 68 | 69 | if prefix != nil { 70 | fullPath = utils.GnmiFullPath(prefix, path) 71 | } 72 | if fullPath.GetElem() == nil && fullPath.GetElement() != nil { 73 | return nil, status.Error(codes.Unimplemented, "deprecated path element type is unsupported") 74 | } 75 | 76 | jsonConfigString, _ := ygot.EmitJSON(s.configStruct, nil) 77 | configMap := make(map[string]interface{}) 78 | 79 | err := json.Unmarshal([]byte(jsonConfigString), &configMap) 80 | if err != nil { 81 | return nil, err 82 | } 83 | pathElements := path.GetElem() 84 | 85 | leafValue, _ := utils.FindLeaf(configMap, pathElements[len(pathElements)-1].GetName()) 86 | val := &pb.TypedValue{ 87 | Value: &pb.TypedValue_StringVal{ 88 | StringVal: leafValue, 89 | }, 90 | } 91 | update := pb.Update{Path: path, Val: val} 92 | return &update, nil 93 | 94 | } 95 | 96 | // collector collects the latest update from the config. 97 | func (s *server) collector(ch chan *pb.Update, request *pb.SubscriptionList) { 98 | for _, sub := range request.Subscription { 99 | path := sub.GetPath() 100 | update, err := s.getUpdate(request, path) 101 | if err != nil { 102 | log.Info("Error while collecting data for subscribe once or poll", err) 103 | 104 | } 105 | if err == nil { 106 | ch <- update 107 | } 108 | 109 | } 110 | } 111 | 112 | // listenForUpdates reads update messages from the update channel, creates a 113 | // subscribe response and send it to the gnmi client. 114 | func (s *server) listenForUpdates(updateChan chan *pb.Update, stream pb.GNMI_SubscribeServer, 115 | mode pb.SubscriptionList_Mode, done chan struct{}) { 116 | for update := range updateChan { 117 | response, _ := buildSubResponse(update) 118 | s.sendResponse(response, stream) 119 | 120 | if mode != pb.SubscriptionList_ONCE { 121 | responseSync := &pb.SubscribeResponse_SyncResponse{ 122 | SyncResponse: true, 123 | } 124 | response = &pb.SubscribeResponse{ 125 | Response: responseSync, 126 | } 127 | s.sendResponse(response, stream) 128 | 129 | } else { 130 | //If the subscription mode is ONCE we read from the channel, build a response and issue it 131 | done <- struct{}{} 132 | } 133 | } 134 | } 135 | 136 | // sendUpdate 137 | func (s *server) sendUpdate(updateChan chan<- *pb.Update, path *pb.Path, value string) { 138 | typedValue := pb.TypedValue_StringVal{StringVal: value} 139 | valueGnmi := &pb.TypedValue{Value: &typedValue} 140 | 141 | update := &pb.Update{ 142 | Path: path, 143 | Val: valueGnmi, 144 | } 145 | updateChan <- update 146 | } 147 | -------------------------------------------------------------------------------- /configs/target_configs/typical_ofsw_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-interfaces:interfaces": { 3 | "interface": [ 4 | { 5 | "name": "admin", 6 | "config": { 7 | "name": "admin" 8 | } 9 | } 10 | ] 11 | }, 12 | "openconfig-system:system": { 13 | "aaa": { 14 | "authentication": { 15 | "admin-user": { 16 | "config": { 17 | "admin-password": "password" 18 | } 19 | }, 20 | "config": { 21 | "authentication-method": [ 22 | "openconfig-aaa-types:LOCAL" 23 | ] 24 | } 25 | } 26 | }, 27 | "clock": { 28 | "config": { 29 | "timezone-name": "Europe/Dublin" 30 | } 31 | }, 32 | "config": { 33 | "hostname": "replace-device-name", 34 | "domain-name": "opennetworking.org", 35 | "login-banner": "This device is for authorized use only", 36 | "motd-banner": "replace-motd-banner" 37 | }, 38 | "state" : { 39 | "boot-time": "1575415411", 40 | "current-datetime": "2019-12-04T10:00:00Z-05:00", 41 | "hostname": "replace-device-name", 42 | "domain-name": "opennetworking.org", 43 | "login-banner": "This device is for authorized use only", 44 | "motd-banner": "replace-motd-banner" 45 | 46 | }, 47 | "openconfig-openflow:openflow": { 48 | "agent": { 49 | "config": { 50 | "backoff-interval": 5, 51 | "datapath-id": "00:16:3e:00:00:00:00:00", 52 | "failure-mode": "SECURE", 53 | "inactivity-probe": 10, 54 | "max-backoff": 10 55 | } 56 | }, 57 | "controllers": { 58 | "controller": [ 59 | { 60 | "config": { 61 | "name": "main" 62 | }, 63 | "connections": { 64 | "connection": [ 65 | { 66 | "aux-id": 0, 67 | "config": { 68 | "address": "192.0.2.10", 69 | "aux-id": 0, 70 | "port": 6633, 71 | "priority": 1, 72 | "source-interface": "admin", 73 | "transport": "TLS" 74 | }, 75 | "state": { 76 | "address": "192.0.2.10", 77 | "aux-id": 0, 78 | "port": 6633, 79 | "priority": 1, 80 | "source-interface": "admin", 81 | "transport": "TLS" 82 | } 83 | }, 84 | { 85 | "aux-id": 1, 86 | "config": { 87 | "address": "192.0.2.11", 88 | "aux-id": 1, 89 | "port": 6653, 90 | "priority": 2, 91 | "source-interface": "admin", 92 | "transport": "TLS" 93 | }, 94 | "state": { 95 | "address": "192.0.2.11", 96 | "aux-id": 1, 97 | "port": 6653, 98 | "priority": 2, 99 | "source-interface": "admin", 100 | "transport": "TLS" 101 | } 102 | } 103 | 104 | ] 105 | 106 | }, 107 | "name": "main" 108 | }, 109 | { 110 | "config": { 111 | "name": "second" 112 | }, 113 | "connections": { 114 | "connection": [ 115 | { 116 | "aux-id": 0, 117 | "config": { 118 | "address": "192.0.3.10", 119 | "aux-id": 0, 120 | "port": 6633, 121 | "priority": 1, 122 | "source-interface": "admin", 123 | "transport": "TLS" 124 | }, 125 | "state": { 126 | "address": "192.0.3.10", 127 | "aux-id": 0, 128 | "port": 6633, 129 | "priority": 1, 130 | "source-interface": "admin", 131 | "transport": "TLS" 132 | } 133 | }, 134 | { 135 | "aux-id": 1, 136 | "config": { 137 | "address": "192.0.3.11", 138 | "aux-id": 1, 139 | "port": 6653, 140 | "priority": 2, 141 | "source-interface": "admin", 142 | "transport": "TLS" 143 | }, 144 | "state": { 145 | "address": "192.0.3.11", 146 | "aux-id": 1, 147 | "port": 6653, 148 | "priority": 2, 149 | "source-interface": "admin", 150 | "transport": "TLS" 151 | } 152 | } 153 | 154 | ] 155 | 156 | }, 157 | "name": "second" 158 | } 159 | ] 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Table of Contents** 8 | 9 | - [1. Device Simulator](#1-Device-Simulator) 10 | - [1.1. Simulator mode](#11-Simulator-mode) 11 | - [1.2. Run mode - localhost or network](#12-Run-mode---localhost-or-network) 12 | - [1.3. docker-compose](#13-docker-compose) 13 | - [1.3.1. Running on Linux](#131-Running-on-Linux) 14 | - [1.4. Run a single docker container](#14-Run-a-single-docker-container) 15 | - [1.5. Create the docker image](#15-Create-the-docker-image) 16 | - [2. Client tools for testing](#2-Client-tools-for-testing) 17 | - [2.1. gNMI Client User Manual](#21-gNMI-Client-User-Manual) 18 | - [2.2. gNOI Client User Manual](#22-gNOI-Client-User-Manual) 19 | 20 | # 1. Device Simulator 21 | 22 | This is a docker VM that runs a gNMI and/or gNOI implementation 23 | supporting openconfig models. 24 | 25 | Inspired by https://github.com/faucetsdn/gnmi 26 | 27 | All commands below assume you are in the __devicesim__ directory 28 | 29 | ## 1.1. Simulator mode 30 | The device simulator can operate in three modes, controlled 31 | using **SIM_MODE** environment variable in the docker-compose file. 32 | 1) SIM_MODE=1 as gNMI target only. The configuration is loaded by default from [configs/target_configs/typical_ofsw_config.json](../configs/target_configs/typical_ofsw_config.json) 33 | 2) SIM_MODE=2 as gNOI target only. It supports *Certificate management* that can be used for certificate installation and rotation. 34 | 3) SIM_MODE=3 both gNMI and gNOsI targets simultaneously 35 | 36 | ## 1.2. Run mode - localhost or network 37 | Additionally the simulator can be run in 38 | * localhost mode - use on Docker for Mac, Windows or Linux 39 | * dedicated network mode - for use on Linux only 40 | 41 | > Docker for Mac or Windows does not support accessing docker images 42 | > externally in dedicated network mode. Docker on Linux can run either. 43 | 44 | ## 1.3. docker-compose 45 | Docker compose manages the running of several docker images at once. 46 | 47 | For example to run 3 **SIM_MODE=1** (gNMI only devices) and **localhost** mode, use: 48 | ```bash 49 | cd tools/docker_compose 50 | docker-compose -f docker-compose-gnmi.yml up 51 | ``` 52 | 53 | This gives an output like 54 | ```bash 55 | Creating devicesim_devicesim3_1 ... 56 | Creating devicesim_devicesim1_1 ... 57 | Creating devicesim_devicesim2_1 ... 58 | Creating devicesim_devicesim3_1 59 | Creating devicesim_devicesim2_1 60 | Creating devicesim_devicesim1_1 ... done 61 | Attaching to devicesim_devicesim3_1, devicesim_devicesim2_1, devicesim_devicesim1_1 62 | devicesim3_1 | gNMI running on localhost:10163 63 | devicesim2_1 | gNMI running on localhost:10162 64 | devicesim1_1 | gNMI running on localhost:10161 65 | ``` 66 | > Use the -d mode with docker-compose to make it run as a daemon in the background 67 | 68 | 69 | ### 1.3.1. Running on Linux 70 | If you are fortunate enough to be using Docker on Linux, then you can use the 71 | above method __or__ using the command below to start in **SIM_MODE=1** and **network** mode: 72 | 73 | ```bash 74 | cd tools/docker_compose 75 | docker-compose -f docker-compose-linux.yml up 76 | ``` 77 | 78 | This will use the fixed IP addresses 172.25.0.11, 172.25.0.12, 172.25.0.13 for 79 | device1-3. An entry must still be placed in your /etc/hosts file for all 3 like: 80 | ```bash 81 | 172.25.0.11 device1.opennetworking.org 82 | 172.25.0.12 device2.opennetworking.org 83 | 172.25.0.13 device3.opennetworking.org 84 | ``` 85 | 86 | > This uses a custom network 'simnet' in Docker and is only possible on Docker for Linux. 87 | > If you are on Mac or Windows it is __not possible__ to route to User Defined networks, 88 | > so the port mapping technique must be used. 89 | 90 | > It is not possible to use the name mapping of the docker network from outside 91 | > the cluster, so either the entries have to be placed in /etc/hosts or on some 92 | > DNS server 93 | 94 | ## 1.4. Run a single docker container 95 | If you just want to run a single device, it is not necessary to run 96 | docker-compose. It can be done just by docker directly, and can be 97 | handy for troubleshooting. The following command shows how to run 98 | a standalone simulator in SIM_MODE=3, localhost mode: 99 | ```bash 100 | docker run --env "HOSTNAME=localhost" --env "SIM_MODE=3" \ 101 | --env "GNMI_PORT=10164" --env "GNOI_PORT=50004" \ 102 | -p "10164:10164" -p "50004:50004" onosproject/device-simulator:latest 103 | ``` 104 | To stop it use "docker kill" 105 | 106 | ## 1.5. Create the docker image 107 | By default the docker compose command will pull down the latest docker 108 | image from the Docker Hub. If you need to build it locally, run: 109 | ```bash 110 | docker build -t onosproject/device-simulator:stable -f Dockerfile . 111 | ``` 112 | 113 | # 2. Client tools for testing 114 | You can access to the information about client tools for each SIM_MODE 115 | including troubleshooting tips using the following links: 116 | 117 | ## 2.1. gNMI Client User Manual 118 | [gNMI Client_User Manual](gnmi/gnmi_user_manual.md) 119 | 120 | ## 2.2. gNOI Client User Manual 121 | [gNOI Client_User Manual](gnoi/gnoi_user_manual.md) 122 | -------------------------------------------------------------------------------- /docs/gnoi/gnoi_user_manual.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Table of Content** 8 | - [1. How to test the gNOI simulator?](#1-How-to-test-the-gNOI-simulator) 9 | - [1.1. How to install gNOI_cert on your machine?](#11-How-to-install-gNOIcert-on-your-machine) 10 | - [2. Troubleshooting](#2-Troubleshooting) 11 | - [2.1. Connection Refused](#21-Connection-Refused) 12 | - [2.2. TCP diagnosis](#22-TCP-diagnosis) 13 | - [2.3. HTTP Diagnosis](#23-HTTP-Diagnosis) 14 | 15 | # 1. How to test the gNOI simulator? 16 | 1. First you should ssh into any of the targets using the following command: 17 | ```bash 18 | sudo docker exec -it /bin/bash 19 | ``` 20 | 21 | 2. From the Docker or your own machine, you can install a certificate and CA bundle on a target that is in bootstrapping mode, accepting encrypted TLS connections using the following command: 22 | ```bash 23 | gnoi_cert -target_addr localhost:50001 \ 24 | -target_name "gnoi_localhost_50001" \ 25 | -key certs/client1.key \ 26 | -ca certs/client1.crt \ 27 | -op provision \ 28 | -cert_id provision_cert \ 29 | -alsologtostderr 30 | ``` 31 | 32 | 33 | After executing the above command the output will be like the following: 34 | ```bash 35 | devicesim1_1 | I0430 01:50:17.406240 12 server.go:59] Start Install request. 36 | devicesim1_1 | I0430 01:50:17.420670 12 server.go:152] Success Install request. 37 | devicesim1_1 | I0430 01:50:17.420732 12 manager.go:100] Notifying for: 1 Certificates and 1 CA Certificates. 38 | devicesim1_1 | I0430 01:50:17.420754 12 gnoi_target.go:61] Found Credentials, setting Provisioned state. 39 | devicesim1_1 | I0430 01:50:17.421202 12 gnoi_target.go:48] Starting gNOI server. 40 | ``` 41 | 42 | 3. To get all installed certificate on a provisioned Target, you can run a command like the following: 43 | ```bash 44 | gnoi_cert \ 45 | -target_addr localhost:50001 \ 46 | -target_name "gnoi_localhost_50001" \ 47 | -key certs/client1.key \ 48 | -ca certs/client1.crt \ 49 | -op get \ 50 | -alsologtostderr 51 | ``` 52 | 53 | After the executing the above command, the output in the client side should be like: 54 | ```bash 55 | I0429 15:45:00.910217 84 gnoi_cert.go:226] GetCertificates: 56 | {provision_cert: "gnoi_localhost_50001"} 57 | ``` 58 | 59 | and the the server should report a success message: 60 | ``` 61 | devicesim1_1 | I0429 15:45:00.909347 10 server.go:292] Success GetCertificates. 62 | ``` 63 | 64 | ## 1.1. How to install gNOI_cert on your machine? 65 | To install **gnoi_cert** on your own machine, you can use the following command: 66 | ```bash 67 | go get -u github.com/google/gnxi/gnoi_cert 68 | go install -v github.com/google/gnxi/gnoi_cert 69 | ``` 70 | 71 | # 2. Troubleshooting 72 | 73 | ## 2.1. Connection Refused 74 | If you get an error like 75 | ```bash 76 | F0501 15:32:50.313449 30 gnoi_cert.go:220] Failed GetCertificates:rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:50001: connect: connection refused" 77 | ``` 78 | That means the gNOI target is not running or you are provding wrong ip:port information to the gnoi_cert command. 79 | 80 | 81 | ## 2.2. TCP diagnosis 82 | > This is not a concern with port mapping method using localhost and is for 83 | > the Linux specific option only 84 | 85 | Starting with TCP - see if you can ping the device 86 | 1. by IP address e.g. 17.18.0.2 - if not it might not be up or there's some 87 | other network problem 88 | 2. by short name e.g. device1 - if not maybe your /etc/hosts file is wrong or 89 | DNS domain search is not opennetworking.org 90 | 3. by long name e.g. device1.opennetowrking.org - if not maybe your /etc/hosts 91 | file is wrong 92 | 93 | For the last 2 cases make sure that the IP address that is resolved matches what 94 | was given at the startup of the simulator with docker. 95 | 96 | ## 2.3. HTTP Diagnosis 97 | If TCP shows reachability then try with HTTPS - it's very important to remember 98 | that for HTTPS the address at which you access the server **must** match exactly 99 | the server name in the server key's Common Name (CN) like __localhost__ or 100 | __device1.opennetworking.org__ (and not an IP address!) 101 | 102 | Try using cURL to determine if there is a certificate problem 103 | ``` 104 | curl -v https://localhost:50001 --key certs/client1.key --cert certs/client1.crt --cacert certs/onfca.crt 105 | ``` 106 | This might give an error like 107 | ```bash 108 | * Rebuilt URL to: https://localhost:50001/ 109 | * Trying 172.18.0.3... 110 | * TCP_NODELAY set 111 | * Connected to localhost (127.0.0.1) port 50001 (#0) 112 | * ALPN, offering h2 113 | * ALPN, offering http/1.1 114 | * successfully set certificate verify locations: 115 | * CAfile: certs/onfca.crt 116 | CApath: /etc/ssl/certs 117 | * TLSv1.2 (OUT), TLS handshake, Client hello (1): 118 | * TLSv1.2 (IN), TLS handshake, Server hello (2): 119 | * TLSv1.2 (IN), TLS handshake, Certificate (11): 120 | * TLSv1.2 (IN), TLS handshake, Server key exchange (12): 121 | * TLSv1.2 (IN), TLS handshake, Request CERT (13): 122 | * TLSv1.2 (IN), TLS handshake, Server finished (14): 123 | * TLSv1.2 (OUT), TLS handshake, Certificate (11): 124 | * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): 125 | * TLSv1.2 (OUT), TLS handshake, CERT verify (15): 126 | * TLSv1.2 (OUT), TLS change cipher, Client hello (1): 127 | * TLSv1.2 (OUT), TLS handshake, Finished (20): 128 | * TLSv1.2 (IN), TLS handshake, Finished (20): 129 | * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 130 | * ALPN, server accepted to use h2 131 | * Server certificate: 132 | * subject: C=US; ST=CA; L=MenloPark; O=ONF; OU=Engineering; CN=device3.opennetworking.org 133 | * start date: Apr 16 14:40:46 2019 GMT 134 | * expire date: Apr 15 14:40:46 2020 GMT 135 | * SSL: certificate subject name 'device3.opennetworking.org' does not match target host name 'localhost' 136 | * stopped the pause stream! 137 | * Closing connection 0 138 | * TLSv1.2 (OUT), TLS alert, Client hello (1): 139 | curl: (51) SSL: certificate subject name 'device3.opennetworking.org' does not match target host name 'localhost' 140 | ``` 141 | 142 | > In this case the device at __localhost__ has a certificate for 143 | > device3.opennetworking.org. HTTPS does not accept this as a valid certificate 144 | > as it indicates someone might be spoofing the server. This happens today in 145 | > your browser if you access a site through HTTPS whose certificate CN does not 146 | > match the URL - it is just a fact of life with HTTPS, and is not peculiar to gNMI. 147 | 148 | When device names and certificates match, then curl will reply with a message like: 149 | ```bash 150 | curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2) 151 | ``` 152 | 153 | > This means the HTTPS handshake was __successful__, and it has failed at the 154 | > gNOI level - not surprising since we did not send it any gNOI payload. 155 | -------------------------------------------------------------------------------- /pkg/gnmi/get.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "reflect" 12 | "strings" 13 | "time" 14 | 15 | "github.com/openconfig/ygot/ytypes" 16 | 17 | pb "github.com/openconfig/gnmi/proto/gnmi" 18 | "github.com/openconfig/gnmi/value" 19 | "github.com/openconfig/ygot/ygot" 20 | "golang.org/x/net/context" 21 | "google.golang.org/grpc/codes" 22 | "google.golang.org/grpc/status" 23 | ) 24 | 25 | // Get implements the Get RPC in gNMI spec. 26 | func (s *Server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { 27 | 28 | dataType := req.GetType() 29 | 30 | if err := s.checkEncodingAndModel(req.GetEncoding(), req.GetUseModels()); err != nil { 31 | return nil, status.Error(codes.Unimplemented, err.Error()) 32 | } 33 | 34 | prefix := req.GetPrefix() 35 | paths := req.GetPath() 36 | notifications := make([]*pb.Notification, len(paths)) 37 | 38 | s.configMu.RLock() 39 | defer s.configMu.RUnlock() 40 | 41 | if paths == nil && dataType.String() != "" { 42 | 43 | jsonType := "IETF" 44 | if req.GetEncoding() == pb.Encoding_JSON { 45 | jsonType = "Internal" 46 | } 47 | notifications := make([]*pb.Notification, 1) 48 | path := pb.Path{} 49 | // Gets the whole config data tree 50 | node, err := ytypes.GetNode(s.model.schemaTreeRoot, s.config, &path, nil) 51 | if isNil(node) || err != nil { 52 | return nil, status.Errorf(codes.NotFound, "path %v not found", path) 53 | } 54 | 55 | nodeStruct, _ := node[0].Data.(ygot.GoStruct) 56 | jsonTree, _ := ygot.ConstructIETFJSON(nodeStruct, &ygot.RFC7951JSONConfig{AppendModuleName: true}) 57 | 58 | jsonTree = pruneConfigData(jsonTree, strings.ToLower(dataType.String()), &path).(map[string]interface{}) 59 | jsonDump, err := json.Marshal(jsonTree) 60 | 61 | if err != nil { 62 | msg := fmt.Sprintf("error in marshaling %s JSON tree to bytes: %v", jsonType, err) 63 | log.Error(msg) 64 | return nil, status.Error(codes.Internal, msg) 65 | } 66 | ts := time.Now().UnixNano() 67 | 68 | update := buildUpdate(jsonDump, &path, jsonType) 69 | notifications[0] = &pb.Notification{ 70 | Timestamp: ts, 71 | Prefix: prefix, 72 | Update: []*pb.Update{update}, 73 | } 74 | resp := &pb.GetResponse{Notification: notifications} 75 | return resp, nil 76 | } 77 | 78 | for i, path := range paths { 79 | // Get schema node for path from config struct. 80 | fullPath := path 81 | if prefix != nil { 82 | fullPath = gnmiFullPath(prefix, path) 83 | } 84 | 85 | if fullPath.GetElem() == nil && fullPath.GetElement() != nil { 86 | return nil, status.Error(codes.Unimplemented, "deprecated path element type is unsupported") 87 | } 88 | node, err := ytypes.GetNode(s.model.schemaTreeRoot, s.config, fullPath, nil) 89 | if isNil(node) || err != nil { 90 | return nil, status.Errorf(codes.NotFound, "path %v not found", path) 91 | } 92 | 93 | ts := time.Now().UnixNano() 94 | 95 | nodeStruct, ok := node[0].Data.(ygot.GoStruct) 96 | dataTypeFlag := false 97 | // Return leaf node. 98 | if !ok { 99 | elements := fullPath.GetElem() 100 | dataTypeString := strings.ToLower(dataType.String()) 101 | if strings.Compare(dataTypeString, "all") == 0 { 102 | dataTypeFlag = true 103 | } else { 104 | for _, elem := range elements { 105 | if strings.Compare(dataTypeString, elem.GetName()) == 0 { 106 | dataTypeFlag = true 107 | break 108 | } 109 | 110 | } 111 | } 112 | if dataTypeFlag == false { 113 | return nil, status.Error(codes.Internal, "The requested dataType is not valid") 114 | } 115 | var val *pb.TypedValue 116 | switch kind := reflect.ValueOf(node).Kind(); kind { 117 | case reflect.Ptr, reflect.Interface: 118 | var err error 119 | val, err = value.FromScalar(reflect.ValueOf(node).Elem().Interface()) 120 | if err != nil { 121 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", path, err) 122 | log.Error(msg) 123 | return nil, status.Error(codes.Internal, msg) 124 | } 125 | 126 | case reflect.Slice: 127 | var err error 128 | switch kind := reflect.ValueOf(node[0].Data).Kind(); kind { 129 | case reflect.Int64: 130 | //fmt.Println(reflect.TypeOf(node[0].Data).Elem()) 131 | enumMap, ok := s.model.enumData[reflect.TypeOf(node[0].Data).Name()] 132 | if !ok { 133 | return nil, status.Error(codes.Internal, "not a GoStruct enumeration type") 134 | } 135 | val = &pb.TypedValue{ 136 | Value: &pb.TypedValue_StringVal{ 137 | StringVal: enumMap[reflect.ValueOf(node[0].Data).Int()].Name, 138 | }, 139 | } 140 | default: 141 | if !reflect.ValueOf(node[0].Data).Elem().IsValid() { 142 | return nil, status.Errorf(codes.NotFound, "path %v not found", path) 143 | } 144 | val, err = value.FromScalar(reflect.ValueOf(node[0].Data).Elem().Interface()) 145 | if err != nil { 146 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", path, err) 147 | log.Error(msg) 148 | return nil, status.Error(codes.Internal, msg) 149 | } 150 | } 151 | 152 | default: 153 | return nil, status.Errorf(codes.Internal, "unexpected kind of leaf node type: %v %v", node, kind) 154 | } 155 | 156 | update := &pb.Update{Path: path, Val: val} 157 | notifications[i] = &pb.Notification{ 158 | Timestamp: ts, 159 | Prefix: prefix, 160 | Update: []*pb.Update{update}, 161 | } 162 | continue 163 | } 164 | dataTypeString := strings.ToLower(dataType.String()) 165 | 166 | if req.GetUseModels() != nil { 167 | return nil, status.Errorf(codes.Unimplemented, "filtering Get using use_models is unsupported, got: %v", req.GetUseModels()) 168 | } 169 | 170 | jsonType := "IETF" 171 | 172 | if req.GetEncoding() == pb.Encoding_JSON { 173 | jsonType = "Internal" 174 | } 175 | 176 | var jsonTree map[string]interface{} 177 | 178 | if reflect.ValueOf(nodeStruct).Pointer() == 0 { 179 | return nil, status.Error(codes.NotFound, "value is 0") 180 | 181 | } 182 | jsonTree, err = jsonEncoder(jsonType, nodeStruct) 183 | jsonTree = pruneConfigData(jsonTree, strings.ToLower(dataTypeString), fullPath).(map[string]interface{}) 184 | if err != nil { 185 | msg := fmt.Sprintf("error in constructing %s JSON tree from requested node: %v", jsonType, err) 186 | log.Error(msg) 187 | return nil, status.Error(codes.Internal, msg) 188 | } 189 | 190 | jsonDump, err := json.Marshal(jsonTree) 191 | if err != nil { 192 | msg := fmt.Sprintf("error in marshaling %s JSON tree to bytes: %v", jsonType, err) 193 | log.Error(msg) 194 | return nil, status.Error(codes.Internal, msg) 195 | } 196 | 197 | update := buildUpdate(jsonDump, path, jsonType) 198 | notifications[i] = &pb.Notification{ 199 | Timestamp: ts, 200 | Prefix: prefix, 201 | Update: []*pb.Update{update}, 202 | } 203 | } 204 | resp := &pb.GetResponse{Notification: notifications} 205 | 206 | return resp, nil 207 | } 208 | -------------------------------------------------------------------------------- /pkg/gnmi/set.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "reflect" 12 | 13 | pb "github.com/openconfig/gnmi/proto/gnmi" 14 | "github.com/openconfig/gnmi/value" 15 | "github.com/openconfig/ygot/experimental/ygotutils" 16 | "github.com/openconfig/ygot/ygot" 17 | "golang.org/x/net/context" 18 | cpb "google.golang.org/genproto/googleapis/rpc/code" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/status" 21 | ) 22 | 23 | // doDelete deletes the path from the json tree if the path exists. If success, 24 | // it calls the callback function to apply the change to the device hardware. 25 | func (s *Server) doDelete(jsonTree map[string]interface{}, prefix, path *pb.Path) (*pb.UpdateResult, error) { 26 | // Update json tree of the device config 27 | var curNode interface{} = jsonTree 28 | pathDeleted := false 29 | fullPath := gnmiFullPath(prefix, path) 30 | schema := s.model.schemaTreeRoot 31 | for i, elem := range fullPath.Elem { // Delete sub-tree or leaf node. 32 | node, ok := curNode.(map[string]interface{}) 33 | if !ok { 34 | break 35 | } 36 | 37 | // Delete node 38 | if i == len(fullPath.Elem)-1 { 39 | if elem.GetKey() == nil { 40 | delete(node, elem.Name) 41 | pathDeleted = true 42 | break 43 | } 44 | pathDeleted = deleteKeyedListEntry(node, elem) 45 | break 46 | } 47 | 48 | if curNode, schema = getChildNode(node, schema, elem, false); curNode == nil { 49 | break 50 | } 51 | } 52 | if reflect.DeepEqual(fullPath, pbRootPath) { // Delete root 53 | for k := range jsonTree { 54 | delete(jsonTree, k) 55 | } 56 | } 57 | 58 | // Apply the validated operation to the config tree and device. 59 | if pathDeleted { 60 | newConfig, err := s.toGoStruct(jsonTree) 61 | if err != nil { 62 | return nil, status.Error(codes.Internal, err.Error()) 63 | } 64 | if s.callback != nil { 65 | if applyErr := s.callback(newConfig); applyErr != nil { 66 | if rollbackErr := s.callback(s.config); rollbackErr != nil { 67 | return nil, status.Errorf(codes.Internal, "error in rollback the failed operation (%v): %v", applyErr, rollbackErr) 68 | } 69 | return nil, status.Errorf(codes.Aborted, "error in applying operation to device: %v", applyErr) 70 | } 71 | } 72 | } 73 | return &pb.UpdateResult{ 74 | Path: path, 75 | Op: pb.UpdateResult_DELETE, 76 | }, nil 77 | } 78 | 79 | // doReplaceOrUpdate validates the replace or update operation to be applied to 80 | // the device, modifies the json tree of the config struct, then calls the 81 | // callback function to apply the operation to the device hardware. 82 | func (s *Server) doReplaceOrUpdate(jsonTree map[string]interface{}, op pb.UpdateResult_Operation, prefix, path *pb.Path, val *pb.TypedValue) (*pb.UpdateResult, error) { 83 | // Validate the operation. 84 | fullPath := gnmiFullPath(prefix, path) 85 | emptyNode, stat := ygotutils.NewNode(s.model.structRootType, fullPath) 86 | if stat.GetCode() != int32(cpb.Code_OK) { 87 | return nil, status.Errorf(codes.NotFound, "path %v is not found in the config structure: %v", fullPath, stat) 88 | } 89 | var nodeVal interface{} 90 | nodeStruct, ok := emptyNode.(ygot.ValidatedGoStruct) 91 | if ok { 92 | if err := s.model.jsonUnmarshaler(val.GetJsonIetfVal(), nodeStruct); err != nil { 93 | return nil, status.Errorf(codes.InvalidArgument, "unmarshaling json data to config struct fails: %v", err) 94 | } 95 | if err := nodeStruct.Validate(); err != nil { 96 | return nil, status.Errorf(codes.InvalidArgument, "config data validation fails: %v", err) 97 | } 98 | var err error 99 | if nodeVal, err = ygot.ConstructIETFJSON(nodeStruct, &ygot.RFC7951JSONConfig{}); err != nil { 100 | msg := fmt.Sprintf("error in constructing IETF JSON tree from config struct: %v", err) 101 | log.Error(msg) 102 | return nil, status.Error(codes.Internal, msg) 103 | } 104 | } else { 105 | var err error 106 | if nodeVal, err = value.ToScalar(val); err != nil { 107 | return nil, status.Errorf(codes.Internal, "cannot convert leaf node to scalar type: %v", err) 108 | } 109 | } 110 | 111 | // Update json tree of the device config. 112 | var curNode interface{} = jsonTree 113 | schema := s.model.schemaTreeRoot 114 | for i, elem := range fullPath.Elem { 115 | switch node := curNode.(type) { 116 | case map[string]interface{}: 117 | // Set node value. 118 | if i == len(fullPath.Elem)-1 { 119 | if elem.GetKey() == nil { 120 | if grpcStatusError := setPathWithoutAttribute(op, node, elem, nodeVal); grpcStatusError != nil { 121 | return nil, grpcStatusError 122 | } 123 | break 124 | } 125 | if grpcStatusError := setPathWithAttribute(op, node, elem, nodeVal); grpcStatusError != nil { 126 | return nil, grpcStatusError 127 | } 128 | break 129 | } 130 | 131 | if curNode, schema = getChildNode(node, schema, elem, true); curNode == nil { 132 | return nil, status.Errorf(codes.NotFound, "path elem not found: %v", elem) 133 | } 134 | case []interface{}: 135 | return nil, status.Errorf(codes.NotFound, "incompatible path elem: %v", elem) 136 | default: 137 | return nil, status.Errorf(codes.Internal, "wrong node type: %T", curNode) 138 | } 139 | } 140 | if reflect.DeepEqual(fullPath, pbRootPath) { // Replace/Update root. 141 | if op == pb.UpdateResult_UPDATE { 142 | return nil, status.Error(codes.Unimplemented, "update the root of config tree is unsupported") 143 | } 144 | nodeValAsTree, ok := nodeVal.(map[string]interface{}) 145 | if !ok { 146 | return nil, status.Errorf(codes.InvalidArgument, "expect a tree to replace the root, got a scalar value: %T", nodeVal) 147 | } 148 | for k := range jsonTree { 149 | delete(jsonTree, k) 150 | } 151 | for k, v := range nodeValAsTree { 152 | jsonTree[k] = v 153 | } 154 | } 155 | newConfig, err := s.toGoStruct(jsonTree) 156 | if err != nil { 157 | return nil, status.Error(codes.Internal, err.Error()) 158 | } 159 | 160 | // Apply the validated operation to the device. 161 | if s.callback != nil { 162 | if applyErr := s.callback(newConfig); applyErr != nil { 163 | if rollbackErr := s.callback(s.config); rollbackErr != nil { 164 | return nil, status.Errorf(codes.Internal, "error in rollback the failed operation (%v): %v", applyErr, rollbackErr) 165 | } 166 | return nil, status.Errorf(codes.Aborted, "error in applying operation to device: %v", applyErr) 167 | } 168 | } 169 | return &pb.UpdateResult{ 170 | Path: path, 171 | Op: op, 172 | }, nil 173 | } 174 | 175 | // Set implements the Set RPC in gNMI spec. 176 | func (s *Server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, error) { 177 | s.configMu.Lock() 178 | defer s.configMu.Unlock() 179 | 180 | jsonTree, err := ygot.ConstructIETFJSON(s.config, &ygot.RFC7951JSONConfig{}) 181 | if err != nil { 182 | msg := fmt.Sprintf("error in constructing IETF JSON tree from config struct: %v", err) 183 | log.Error(msg) 184 | return nil, status.Error(codes.Internal, msg) 185 | } 186 | 187 | prefix := req.GetPrefix() 188 | var results []*pb.UpdateResult 189 | 190 | for _, path := range req.GetDelete() { 191 | res, grpcStatusError := s.doDelete(jsonTree, prefix, path) 192 | if grpcStatusError != nil { 193 | return nil, grpcStatusError 194 | } 195 | results = append(results, res) 196 | } 197 | for _, upd := range req.GetReplace() { 198 | res, grpcStatusError := s.doReplaceOrUpdate(jsonTree, pb.UpdateResult_REPLACE, prefix, upd.GetPath(), upd.GetVal()) 199 | if grpcStatusError != nil { 200 | return nil, grpcStatusError 201 | } 202 | results = append(results, res) 203 | } 204 | for _, upd := range req.GetUpdate() { 205 | res, grpcStatusError := s.doReplaceOrUpdate(jsonTree, pb.UpdateResult_UPDATE, prefix, upd.GetPath(), upd.GetVal()) 206 | if grpcStatusError != nil { 207 | return nil, grpcStatusError 208 | } 209 | results = append(results, res) 210 | } 211 | 212 | jsonDump, err := json.Marshal(jsonTree) 213 | if err != nil { 214 | msg := fmt.Sprintf("error in marshaling IETF JSON tree to bytes: %v", err) 215 | log.Error(msg) 216 | return nil, status.Error(codes.Internal, msg) 217 | } 218 | rootStruct, err := s.model.NewConfigStruct(jsonDump) 219 | if err != nil { 220 | msg := fmt.Sprintf("error in creating config struct from IETF JSON data: %v", err) 221 | log.Error(msg) 222 | return nil, status.Error(codes.Internal, msg) 223 | } 224 | log.Infof("Json tree: %v", jsonTree) 225 | 226 | s.config = rootStruct 227 | setResponse := &pb.SetResponse{ 228 | Prefix: req.GetPrefix(), 229 | Response: results, 230 | } 231 | 232 | for _, response := range setResponse.GetResponse() { 233 | update := &pb.Update{ 234 | Path: response.GetPath(), 235 | } 236 | s.ConfigUpdate.In() <- update 237 | } 238 | return setResponse, nil 239 | } 240 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution 10 | as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct 18 | or indirect, to cause the direction or management of such entity, whether 19 | by contract or otherwise, or (ii) ownership of fifty percent (50%) or more 20 | of the outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions 23 | granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation 30 | or translation of a Source form, including but not limited to compiled object 31 | code, generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, 34 | made available under the License, as indicated by a copyright notice that 35 | is included in or attached to the work (an example is provided in the Appendix 36 | below). 37 | 38 | "Derivative Works" shall mean any work, whether in Source or Object form, 39 | that is based on (or derived from) the Work and for which the editorial revisions, 40 | annotations, elaborations, or other modifications represent, as a whole, an 41 | original work of authorship. For the purposes of this License, Derivative 42 | Works shall not include works that remain separable from, or merely link (or 43 | bind by name) to the interfaces of, the Work and Derivative Works thereof. 44 | 45 | "Contribution" shall mean any work of authorship, including the original version 46 | of the Work and any modifications or additions to that Work or Derivative 47 | Works thereof, that is intentionally submitted to Licensor for inclusion in 48 | the Work by the copyright owner or by an individual or Legal Entity authorized 49 | to submit on behalf of the copyright owner. For the purposes of this definition, 50 | "submitted" means any form of electronic, verbal, or written communication 51 | sent to the Licensor or its representatives, including but not limited to 52 | communication on electronic mailing lists, source code control systems, and 53 | issue tracking systems that are managed by, or on behalf of, the Licensor 54 | for the purpose of discussing and improving the Work, but excluding communication 55 | that is conspicuously marked or otherwise designated in writing by the copyright 56 | owner as "Not a Contribution." 57 | 58 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 59 | of whom a Contribution has been received by Licensor and subsequently incorporated 60 | within the Work. 61 | 62 | 2. Grant of Copyright License. Subject to the terms and conditions of this 63 | License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, 64 | no-charge, royalty-free, irrevocable copyright license to reproduce, prepare 65 | Derivative Works of, publicly display, publicly perform, sublicense, and distribute 66 | the Work and such Derivative Works in Source or Object form. 67 | 68 | 3. Grant of Patent License. Subject to the terms and conditions of this License, 69 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, 70 | no-charge, royalty-free, irrevocable (except as stated in this section) patent 71 | license to make, have made, use, offer to sell, sell, import, and otherwise 72 | transfer the Work, where such license applies only to those patent claims 73 | licensable by such Contributor that are necessarily infringed by their Contribution(s) 74 | alone or by combination of their Contribution(s) with the Work to which such 75 | Contribution(s) was submitted. If You institute patent litigation against 76 | any entity (including a cross-claim or counterclaim in a lawsuit) alleging 77 | that the Work or a Contribution incorporated within the Work constitutes direct 78 | or contributory patent infringement, then any patent licenses granted to You 79 | under this License for that Work shall terminate as of the date such litigation 80 | is filed. 81 | 82 | 4. Redistribution. You may reproduce and distribute copies of the Work or 83 | Derivative Works thereof in any medium, with or without modifications, and 84 | in Source or Object form, provided that You meet the following conditions: 85 | 86 | (a) You must give any other recipients of the Work or Derivative Works a copy 87 | of this License; and 88 | 89 | (b) You must cause any modified files to carry prominent notices stating that 90 | You changed the files; and 91 | 92 | (c) You must retain, in the Source form of any Derivative Works that You distribute, 93 | all copyright, patent, trademark, and attribution notices from the Source 94 | form of the Work, excluding those notices that do not pertain to any part 95 | of the Derivative Works; and 96 | 97 | (d) If the Work includes a "NOTICE" text file as part of its distribution, 98 | then any Derivative Works that You distribute must include a readable copy 99 | of the attribution notices contained within such NOTICE file, excluding those 100 | notices that do not pertain to any part of the Derivative Works, in at least 101 | one of the following places: within a NOTICE text file distributed as part 102 | of the Derivative Works; within the Source form or documentation, if provided 103 | along with the Derivative Works; or, within a display generated by the Derivative 104 | Works, if and wherever such third-party notices normally appear. The contents 105 | of the NOTICE file are for informational purposes only and do not modify the 106 | License. You may add Your own attribution notices within Derivative Works 107 | that You distribute, alongside or as an addendum to the NOTICE text from the 108 | Work, provided that such additional attribution notices cannot be construed 109 | as modifying the License. 110 | 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, 113 | or distribution of Your modifications, or for any such Derivative Works as 114 | a whole, provided Your use, reproduction, and distribution of the Work otherwise 115 | complies with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 118 | Contribution intentionally submitted for inclusion in the Work by You to the 119 | Licensor shall be under the terms and conditions of this License, without 120 | any additional terms or conditions. Notwithstanding the above, nothing herein 121 | shall supersede or modify the terms of any separate license agreement you 122 | may have executed with Licensor regarding such Contributions. 123 | 124 | 6. Trademarks. This License does not grant permission to use the trade names, 125 | trademarks, service marks, or product names of the Licensor, except as required 126 | for reasonable and customary use in describing the origin of the Work and 127 | reproducing the content of the NOTICE file. 128 | 129 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to 130 | in writing, Licensor provides the Work (and each Contributor provides its 131 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 132 | KIND, either express or implied, including, without limitation, any warranties 133 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR 134 | A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness 135 | of using or redistributing the Work and assume any risks associated with Your 136 | exercise of permissions under this License. 137 | 138 | 8. Limitation of Liability. In no event and under no legal theory, whether 139 | in tort (including negligence), contract, or otherwise, unless required by 140 | applicable law (such as deliberate and grossly negligent acts) or agreed to 141 | in writing, shall any Contributor be liable to You for damages, including 142 | any direct, indirect, special, incidental, or consequential damages of any 143 | character arising as a result of this License or out of the use or inability 144 | to use the Work (including but not limited to damages for loss of goodwill, 145 | work stoppage, computer failure or malfunction, or any and all other commercial 146 | damages or losses), even if such Contributor has been advised of the possibility 147 | of such damages. 148 | 149 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 150 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 151 | acceptance of support, warranty, indemnity, or other liability obligations 152 | and/or rights consistent with this License. However, in accepting such obligations, 153 | You may act only on Your own behalf and on Your sole responsibility, not on 154 | behalf of any other Contributor, and only if You agree to indemnify, defend, 155 | and hold each Contributor harmless for any liability incurred by, or claims 156 | asserted against, such Contributor by reason of your accepting any such warranty 157 | or additional liability. 158 | 159 | END OF TERMS AND CONDITIONS 160 | 161 | APPENDIX: How to apply the Apache License to your work. 162 | 163 | To apply the Apache License to your work, attach the following boilerplate 164 | notice, with the fields enclosed by brackets "[]" replaced with your own identifying 165 | information. (Don't include the brackets!) The text should be enclosed in 166 | the appropriate comment syntax for the file format. We also recommend that 167 | a file or class name and description of purpose be included on the same "printed 168 | page" as the copyright notice for easier identification within third-party 169 | archives. 170 | 171 | Copyright [yyyy] [name of copyright owner] 172 | 173 | Licensed under the Apache License, Version 2.0 (the "License"); 174 | you may not use this file except in compliance with the License. 175 | You may obtain a copy of the License at 176 | 177 | http://www.apache.org/licenses/LICENSE-2.0 178 | 179 | Unless required by applicable law or agreed to in writing, software 180 | distributed under the License is distributed on an "AS IS" BASIS, 181 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 182 | See the License for the specific language governing permissions and 183 | limitations under the License. 184 | -------------------------------------------------------------------------------- /pkg/utils/gnmiPathUtils.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package utils 6 | 7 | import ( 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | 12 | pb "github.com/openconfig/gnmi/proto/gnmi" 13 | ) 14 | 15 | var ( 16 | idPattern = `[a-zA-Z_][a-zA-Z\d\_\-\.]*` 17 | // YANG identifiers must follow RFC 6020: 18 | // https://tools.ietf.org/html/rfc6020#section-6.2. 19 | idRe = regexp.MustCompile(`^` + idPattern + `$`) 20 | // The sting representation of List key value pairs must follow the 21 | // following pattern: [key=value], where key is the List key leaf name, 22 | // and value is the string representation of key leaf value. 23 | kvRe = regexp.MustCompile(`^\[` + 24 | // Key leaf name must be a valid YANG identifier. 25 | idPattern + `=` + 26 | // Key leaf value must be a non-empty string, which may contain 27 | // newlines. Use (?s) to turn on s flag to match newlines. 28 | `((?s).+)` + 29 | `\]$`) 30 | ) 31 | 32 | // parseKeyValueString parses a List key-value pair, and returns a 33 | // map[string]string whose key is the List key leaf name and whose value is the 34 | // string representation of List key leaf value. The input path-valur pairs are 35 | // encoded using the following pattern: [k1=v1][k2=v2]..., where k1 and k2 must be 36 | // valid YANG identifiers, v1 and v2 can be any non-empty strings where any ']' 37 | // must be escapced by an '\'. Any malformed key-value pair generates an error. 38 | // For example, given 39 | // "[k1=v1][k2=v2]", 40 | // this API returns 41 | // map[string]string{"k1": "v1", "k2": "v2"}. 42 | func parseKeyValueString(str string) (map[string]string, error) { 43 | keyValuePairs := make(map[string]string) 44 | // begin marks the beginning of a key-value pair. 45 | begin := 0 46 | // end marks the end of a key-value pair. 47 | end := 0 48 | // insideBrackets is true when at least one '[' has been found and no 49 | // ']' has been found. It is false when a closing ']' has been found. 50 | insideBrackets := false 51 | 52 | for end < len(str) { 53 | switch str[end] { 54 | case '[': 55 | if (end == 0 || str[end-1] != '\\') && !insideBrackets { 56 | insideBrackets = true 57 | } 58 | end++ 59 | case ']': 60 | if (end == 0 || str[end-1] != '\\') && insideBrackets { 61 | insideBrackets = false 62 | keyValue := str[begin : end+1] 63 | // Key-value pair string must have the 64 | // following pattern: [k=v], where k is a valid 65 | // YANG identifier, and v can be any non-empty 66 | // string. 67 | if !kvRe.MatchString(keyValue) { 68 | return nil, fmt.Errorf("malformed List key-value pair string: %s, in: %s", keyValue, str) 69 | } 70 | keyValue = keyValue[1 : len(keyValue)-1] 71 | i := strings.Index(keyValue, "=") 72 | key, val := keyValue[:i], keyValue[i+1:] 73 | // Recover escaped '[' and ']'. 74 | val = strings.Replace(val, `\]`, `]`, -1) 75 | val = strings.Replace(val, `\[`, `[`, -1) 76 | keyValuePairs[key] = val 77 | begin = end + 1 78 | } 79 | end++ 80 | default: 81 | end++ 82 | } 83 | } 84 | 85 | if begin < end { 86 | return nil, fmt.Errorf("malformed List key-value pair string: %s", str) 87 | } 88 | 89 | return keyValuePairs, nil 90 | } 91 | 92 | // splitPath splits a string representation of path into []string. Path 93 | // elements are separated by '/'. String splitting scans from left to right. A 94 | // '[' marks the beginning of a List key value pair substring. A List key value 95 | // pair string ends at the first ']' encountered. Neither an escaped '[', i.e., 96 | // `\[`, nor an escaped ']', i.e., `\]`, serves as the boundary of a List key 97 | // value pair string. 98 | // 99 | // Within a List key value string, '/', '[' and ']' are treated differently: 100 | // 101 | // 1. A '/' does not act as a separator, and is allowed to be part of a 102 | // List key leaf value. 103 | // 104 | // 2. A '[' is allowed within a List key value. '[' and `\[` are 105 | // equivalent within a List key value. 106 | // 107 | // 3. If a ']' needs to be part of a List key value, it must be escaped as 108 | // '\]'. The first unescaped ']' terminates a List key value string. 109 | // 110 | // Outside of any List key value pair string: 111 | // 112 | // 1. A ']' without a matching '[' does not generate any error in this 113 | // API. This error is caught later by another API. 114 | // 115 | // 2. A '[' without an closing ']' is treated as an error, because it 116 | // indicates an incomplete List key leaf value string. 117 | // 118 | // For example, "/a/b/c" is split into []string{"a", "b", "c"}. 119 | // "/a/b[k=eth1/1]/c" is split into []string{"a", "b[k=eth1/1]", "c"}. 120 | // `/a/b/[k=v\]]/c` is split into []string{"a", "b", `[k=v\]]`, "c"}. 121 | // "a/b][k=v]/c" is split into []string{"a", "b][k=v]", "c"}. The invalid List 122 | // name "b]" error will be caught later by another API. "/a/b[k=v/c" generates 123 | // an error because of incomplete List key value pair string. 124 | func splitPath(str string) ([]string, error) { 125 | var path []string 126 | str += "/" 127 | // insideBrackets is true when at least one '[' has been found and no 128 | // ']' has been found. It is false when a closing ']' has been found. 129 | insideBrackets := false 130 | // begin marks the beginning of a path element, which is separated by 131 | // '/' unclosed between '[' and ']'. 132 | begin := 0 133 | // end marks the end of a path element, which is separated by '/' 134 | // unclosed between '[' and ']'. 135 | end := 0 136 | 137 | // Split the given string using unescaped '/'. 138 | for end < len(str) { 139 | switch str[end] { 140 | case '/': 141 | if !insideBrackets { 142 | // Current '/' is a valid path element 143 | // separator. 144 | if end > begin { 145 | path = append(path, str[begin:end]) 146 | } 147 | end++ 148 | begin = end 149 | } else { 150 | // Current '/' must be part of a List key value 151 | // string. 152 | end++ 153 | } 154 | case '[': 155 | if (end == 0 || str[end-1] != '\\') && !insideBrackets { 156 | // Current '[' is unescacped, and is the 157 | // beginning of List key-value pair(s) string. 158 | insideBrackets = true 159 | } 160 | end++ 161 | case ']': 162 | if (end == 0 || str[end-1] != '\\') && insideBrackets { 163 | // Current ']' is unescacped, and is the end of 164 | // List key-value pair(s) string. 165 | insideBrackets = false 166 | } 167 | end++ 168 | default: 169 | end++ 170 | } 171 | } 172 | 173 | if insideBrackets { 174 | return nil, fmt.Errorf("missing ] in path string: %s", str) 175 | } 176 | return path, nil 177 | } 178 | 179 | // parseElement parses a split path element, and returns the parsed elements. 180 | // Two types of path elements are supported: 181 | // 182 | // 1. Non-List schema node names which must be valid YANG identifiers. A valid 183 | // schema node name is returned as it is. For example, given "abc", this API 184 | // returns []interface{"abc"}. 185 | // 186 | // 2. List elements following this pattern: list-name[k1=v1], where list-name 187 | // is the substring from the beginning of the input string to the first '[', k1 188 | // is the substring from the letter after '[' to the first '=', and v1 is the 189 | // substring from the letter after '=' to the first unescaped ']'. list-name 190 | // and k1 must be valid YANG identifier, and v1 can be any non-empty string 191 | // where ']' is escaped by '\'. A List element is parsed into two parts: List 192 | // name and List key value pair(s). List key value pairs are saved in a 193 | // map[string]string whose key is List key leaf name and whose value is the 194 | // string representation of List key leaf value. For example, given 195 | // "list-name[k1=v1]", 196 | // this API returns 197 | // []interface{}{"list-name", map[string]string{"k1": "v1"}}. 198 | // Multi-key List elements follow a similar pattern: 199 | // list-name[k1=v1]...[kN=vN]. 200 | func parseElement(elem string) ([]interface{}, error) { 201 | i := strings.Index(elem, "[") 202 | if i < 0 { 203 | if !idRe.MatchString(elem) { 204 | return nil, fmt.Errorf("invalid node name: %q", elem) 205 | } 206 | return []interface{}{elem}, nil 207 | } 208 | 209 | listName := elem[:i] 210 | if !idRe.MatchString(listName) { 211 | return nil, fmt.Errorf("invalid List name: %q, in: %s", listName, elem) 212 | } 213 | keyValuePairs, err := parseKeyValueString(elem[i:]) 214 | if err != nil { 215 | return nil, fmt.Errorf("invalid path element %s: %v", elem, err) 216 | } 217 | return []interface{}{listName, keyValuePairs}, nil 218 | } 219 | 220 | // ParseStringPath parses a string path and produces a []interface{} of parsed 221 | // path elements. Path elements in a string path are separated by '/'. Each 222 | // path element can either be a schema node name or a List path element. Schema 223 | // node names must be valid YANG identifiers. A List path element is encoded 224 | // using the following pattern: list-name[key1=value1]...[keyN=valueN]. Each 225 | // List path element generates two parsed path elements: List name and a 226 | // map[string]string containing List key-value pairs with value(s) in string 227 | // representation. A '/' within a List key value pair string, i.e., between a 228 | // pair of '[' and ']', does not serve as a path separator, and is allowed to be 229 | // part of a List key leaf value. For example, given a string path: 230 | // "/a/list-name[k=v/v]/c", 231 | // this API returns: 232 | // []interface{}{"a", "list-name", map[string]string{"k": "v/v"}, "c"}. 233 | // 234 | // String path parsing consists of two passes. In the first pass, the input 235 | // string is split into []string using valid separator '/'. An incomplete List 236 | // key value string, i.e, a '[' which starts a List key value string without a 237 | // closing ']', in input string generates an error. In the above example, this 238 | // pass produces: 239 | // []string{"a", "list-name[k=v/v]", "c"}. 240 | // In the second pass, each element in split []string is parsed checking syntax 241 | // and pattern correctness. Errors are generated for invalid YANG identifiers, 242 | // malformed List key-value string, etc.. In the above example, the second pass 243 | // produces: 244 | // []interface{}{"a", "list-name", map[string]string{"k", "v/v"}, "c"}. 245 | func ParseStringPath(stringPath string) ([]interface{}, error) { 246 | elems, err := splitPath(stringPath) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | var path []interface{} 252 | // Check whether each path element is valid. Parse List key value 253 | // pairs. 254 | for _, elem := range elems { 255 | parts, err := parseElement(elem) 256 | if err != nil { 257 | return nil, fmt.Errorf("invalid string path %s: %v", stringPath, err) 258 | } 259 | path = append(path, parts...) 260 | } 261 | 262 | return path, nil 263 | } 264 | 265 | // ToGNMIPath parses an xpath string into a gnmi Path struct defined in gnmi 266 | // proto. Path convention can be found in 267 | // https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-path-conventions.md 268 | // 269 | // For example, xpath /interfaces/interface[name=Ethernet1/2/3]/state/counters 270 | // will be parsed to: 271 | // 272 | // elem: 273 | // elem: < 274 | // name: "interface" 275 | // key: < 276 | // key: "name" 277 | // value: "Ethernet1/2/3" 278 | // > 279 | // > 280 | // elem: 281 | // elem: 282 | func ToGNMIPath(xpath string) (*pb.Path, error) { 283 | xpathElements, err := ParseStringPath(xpath) 284 | if err != nil { 285 | return nil, err 286 | } 287 | var pbPathElements []*pb.PathElem 288 | for _, elem := range xpathElements { 289 | switch v := elem.(type) { 290 | case string: 291 | pbPathElements = append(pbPathElements, &pb.PathElem{Name: v}) 292 | case map[string]string: 293 | n := len(pbPathElements) 294 | if n == 0 { 295 | return nil, fmt.Errorf("missing name before key-value list") 296 | } 297 | if pbPathElements[n-1].Key != nil { 298 | return nil, fmt.Errorf("two subsequent key-value lists") 299 | } 300 | pbPathElements[n-1].Key = v 301 | default: 302 | return nil, fmt.Errorf("wrong data type: %T", v) 303 | } 304 | } 305 | return &pb.Path{Elem: pbPathElements}, nil 306 | } 307 | -------------------------------------------------------------------------------- /docs/gnmi/gnmi_user_manual.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Table of Contents** 8 | - [1. Introduction](#1-Introduction) 9 | - [2. How to Install gNMI Command Line Interface (CLI) ?](#2-How-to-Install-gNMI-Command-Line-Interface-CLI) 10 | - [3. Get the capabilities](#3-Get-the-capabilities) 11 | - [4. Run the Get Command](#4-Run-the-Get-Command) 12 | - [4.1. Retrieve the motd-banner](#41-Retrieve-the-motd-banner) 13 | - [4.2. Retrieve All CONFIG leaves under "/system"](#42-Retrieve-All-CONFIG-leaves-under-%22system%22) 14 | - [4.3. Retrieve All STATE leaves under "/system"](#43-Retrieve-All-STATE-leaves-under-%22system%22) 15 | - [4.4. Retrieve All Config values under the root](#44-Retrieve-All-Config-values-under-the-root) 16 | - [5. Run the Set command](#5-Run-the-Set-command) 17 | - [6. Run the Subscribe command](#6-Run-the-Subscribe-command) 18 | - [6.1. Subscribe ONCE](#61-Subscribe-ONCE) 19 | - [6.2. Subscribe POLL](#62-Subscribe-POLL) 20 | - [6.3. Subscribe Stream](#63-Subscribe-Stream) 21 | - [6.3.1. ON\_CHANGE](#631-ONCHANGE) 22 | - [6.3.2. SAMPLE](#632-SAMPLE) 23 | - [6.3.3. TARGET\_DEFINED](#633-TARGETDEFINED) 24 | - [6.4. Generate and Stream Random Events for State Type Attributes (Just for **Testing** Purposes)](#64-Generate-and-Stream-Random-Events-for-State-Type-Attributes-Just-for-Testing-Purposes) 25 | - [7. Troubleshooting](#7-Troubleshooting) 26 | - [7.1. Deadline exceeded](#71-Deadline-exceeded) 27 | - [7.2. TCP diagnosis](#72-TCP-diagnosis) 28 | - [7.3. HTTP Diagnosis](#73-HTTP-Diagnosis) 29 | # 1. Introduction 30 | 31 | 32 | # 2. How to Install gNMI Command Line Interface (CLI) ? 33 | 34 | gNMI CLI is a general purpose client tool for testing gNMI devices, from 35 | the OpenConfig project. 36 | To run it, two options are available: 37 | 38 | 1. (**Recommended Option**): you can install the gNMI CLI on your own machine using the following command and run it as an external application to the Docker containers. This option allows you to connect to any of the targets and run the gNMI CLI commands. 39 | ```bash 40 | go get -u github.com/openconfig/gnmi/cmd/gnmi_cli 41 | go install -v github.com/openconfig/gnmi/cmd/gnmi_cli 42 | ``` 43 | 2. Or you can ssh into any of the targets using the following command and run 44 | the gNMI CLI from the Docker container. 45 | ```bash 46 | docker exec -it /bin/bash 47 | ``` 48 | 49 | # 3. Get the capabilities 50 | ```bash 51 | gnmi_cli -address localhost:10161 \ 52 | -capabilities \ 53 | -timeout 5s -alsologtostderr \ 54 | -client_crt certs/client1.crt \ 55 | -client_key certs/client1.key \ 56 | -ca_crt certs/onfca.crt 57 | ``` 58 | 59 | If you get 60 | ```bash 61 | E0416 15:23:08.099600 22997 gnmi_cli.go:180] could not create a gNMI client: Dialer(localhost:10161, 5s): context deadline exceeded 62 | ``` 63 | It indicates a transport problem - see the [troubleshooting](#deadline-exceeded) section below. 64 | 65 | # 4. Run the Get Command 66 | ## 4.1. Retrieve the motd-banner 67 | The following command retrieves the motd-banner. 68 | ```bash 69 | gnmi_cli -address localhost:10162 \ 70 | -get \ 71 | -proto "path: elem: elem: >" \ 72 | -timeout 5s -alsologtostderr \ 73 | -client_crt certs/client1.crt \ 74 | -client_key certs/client1.key \ 75 | -ca_crt certs/onfca.crt 76 | ``` 77 | 78 | This gives a response like 79 | ```bash 80 | notification: < 81 | timestamp: 1555495881239352362 82 | update: < 83 | path: < 84 | elem: < 85 | name: "system" 86 | > 87 | elem: < 88 | name: "config" 89 | > 90 | elem: < 91 | name: "motd-banner" 92 | > 93 | > 94 | val: < 95 | string_val: "Welcome to gNMI service on localhost:10162" 96 | > 97 | > 98 | > 99 | ``` 100 | ## 4.2. Retrieve All CONFIG leaves under "/system" 101 | ```bash 102 | gnmi_cli -address localhost:10162 \ 103 | -get \ 104 | -proto "type:1, path: >" \ 105 | -timeout 5s -alsologtostderr \ 106 | -client_crt certs/client1.crt \ 107 | -client_key certs/client1.key \ 108 | -ca_crt certs/onfca.crt 109 | ``` 110 | 111 | This gives a response like this: 112 | ```bash 113 | notification: < 114 | timestamp: 1561659731806153000 115 | update: < 116 | path: < 117 | elem: < 118 | name: "system" 119 | > 120 | > 121 | val: < 122 | json_val: "{\"aaa\":{\"authentication\":{\"admin-user\":{\"config\":{\"admin-password\":\"password\"}},\"config\":{\"authentication-method\":[\"LOCAL\"]}}},\"clock\":{\"config\":{\"timezone-name\":\"Europe/Dublin\"}},\"config\":{\"domain-name\":\"opennetworking.org\",\"hostname\":\"replace-device-name\",\"login-banner\":\"This device is for authorized use only\",\"motd-banner\":\"replace-motd-banner\"},\"openflow\":{\"agent\":{\"config\":{\"backoff-interval\":5,\"datapath-id\":\"00:16:3e:00:00:00:00:00\",\"failure-mode\":\"SECURE\",\"inactivity-probe\":10,\"max-backoff\":10}},\"controllers\":{\"controller\":{\"main\":{\"config\":{\"name\":\"main\"},\"connections\":{\"connection\":{\"0\":{\"aux-id\":0,\"config\":{\"address\":\"192.0.2.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},\"1\":{\"aux-id\":1,\"config\":{\"address\":\"192.0.2.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}}},\"name\":\"main\"},\"second\":{\"config\":{\"name\":\"second\"},\"connections\":{\"connection\":{\"0\":{\"aux-id\":0,\"config\":{\"address\":\"192.0.3.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},\"1\":{\"aux-id\":1,\"config\":{\"address\":\"192.0.3.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}}},\"name\":\"second\"}}}}}" 123 | > 124 | > 125 | > 126 | ``` 127 | 128 | ## 4.3. Retrieve All STATE leaves under "/system" 129 | 130 | ```bash 131 | gnmi_cli -address localhost:10162 \ 132 | -get \ 133 | -proto "type:2, path: >" \ 134 | -timeout 5s -alsologtostderr \ 135 | -client_crt certs/client1.crt \ 136 | -client_key certs/client1.key \ 137 | -ca_crt certs/onfca.crt 138 | ``` 139 | 140 | ```bash 141 | notification: < 142 | timestamp: 1561659901045020000 143 | update: < 144 | path: < 145 | elem: < 146 | name: "system" 147 | > 148 | > 149 | val: < 150 | json_val: "{\"openflow\":{\"controllers\":{\"controller\":{\"main\":{\"connections\":{\"connection\":{\"0\":{\"aux-id\":0,\"state\":{\"address\":\"192.0.2.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},\"1\":{\"aux-id\":1,\"state\":{\"address\":\"192.0.2.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}}},\"name\":\"main\"},\"second\":{\"connections\":{\"connection\":{\"0\":{\"aux-id\":0,\"state\":{\"address\":\"192.0.3.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},\"1\":{\"aux-id\":1,\"state\":{\"address\":\"192.0.3.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}}},\"name\":\"second\"}}}}}" 151 | > 152 | > 153 | > 154 | ``` 155 | 156 | ## 4.4. Retrieve All Config values under the root 157 | For this case, we assume that when that path is empty but the dataType is 158 | specefied in the request, we return whole config data tree. 159 | 160 | ```bash 161 | gnmi_cli -address localhost:10162 \ 162 | -get \ 163 | -proto "type:1" \ 164 | -timeout 5s -alsologtostderr \ 165 | -client_crt certs/client1.crt \ 166 | -client_key certs/client1.key \ 167 | -ca_crt certs/onfca.crt 168 | ``` 169 | 170 | This gives a response like this: 171 | ```bash 172 | notification: < 173 | timestamp: 1561660173314942000 174 | update: < 175 | path: < 176 | > 177 | val: < 178 | json_val: "{\"openconfig-interfaces:interfaces\":{\"interface\":[{\"config\":{\"name\":\"admin\"},\"name\":\"admin\"}]},\"openconfig-system:system\":{\"aaa\":{\"authentication\":{\"admin-user\":{\"config\":{\"admin-password\":\"password\"}},\"config\":{\"authentication-method\":[\"openconfig-aaa-types:LOCAL\"]}}},\"clock\":{\"config\":{\"timezone-name\":\"Europe/Dublin\"}},\"config\":{\"domain-name\":\"opennetworking.org\",\"hostname\":\"replace-device-name\",\"login-banner\":\"This device is for authorized use only\",\"motd-banner\":\"replace-motd-banner\"},\"openconfig-openflow:openflow\":{\"agent\":{\"config\":{\"backoff-interval\":5,\"datapath-id\":\"00:16:3e:00:00:00:00:00\",\"failure-mode\":\"SECURE\",\"inactivity-probe\":10,\"max-backoff\":10}},\"controllers\":{\"controller\":[{\"config\":{\"name\":\"main\"},\"connections\":{\"connection\":[{\"aux-id\":0,\"config\":{\"address\":\"192.0.2.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},{\"aux-id\":1,\"config\":{\"address\":\"192.0.2.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}]},\"name\":\"main\"},{\"config\":{\"name\":\"second\"},\"connections\":{\"connection\":[{\"aux-id\":0,\"config\":{\"address\":\"192.0.3.10\",\"aux-id\":0,\"port\":6633,\"priority\":1,\"source-interface\":\"admin\",\"transport\":\"TLS\"}},{\"aux-id\":1,\"config\":{\"address\":\"192.0.3.11\",\"aux-id\":1,\"port\":6653,\"priority\":2,\"source-interface\":\"admin\",\"transport\":\"TLS\"}}]},\"name\":\"second\"}]}}}}" 179 | > 180 | > 181 | > 182 | ``` 183 | 184 | # 5. Run the Set command 185 | The following command updates the timezone-name. 186 | ```bash 187 | gnmi_cli -address localhost:10161 \ 188 | -set \ 189 | -proto "update: elem: elem: elem: > val: >" \ 190 | -timeout 5s \ 191 | -alsologtostderr \ 192 | -client_crt certs/client1.crt \ 193 | -client_key certs/client1.key \ 194 | -ca_crt certs/onfca.crt 195 | ``` 196 | 197 | This gives a response like this: 198 | ```bash 199 | response: < 200 | path: < 201 | elem: < 202 | name: "system" 203 | > 204 | elem: < 205 | name: "clock" 206 | > 207 | elem: < 208 | name: "config" 209 | > 210 | elem: < 211 | name: "timezone-name" 212 | > 213 | > 214 | op: UPDATE 215 | > 216 | ``` 217 | 218 | # 6. Run the Subscribe command 219 | ## 6.1. Subscribe ONCE 220 | ```bash 221 | gnmi_cli -address localhost:10161 \ 222 | -proto "subscribe:, subscription: elem: elem: elem: >>>" \ 223 | -timeout 5s -alsologtostderr \ 224 | -client_crt certs/client1.crt -client_key certs/client1.key -ca_crt certs/onfca.crt 225 | ``` 226 | 227 | This gives a response like this. 228 | ```bash 229 | { 230 | "system": { 231 | "clock": { 232 | "config": { 233 | "timezone-name": "Europe/Dublin" 234 | } 235 | } 236 | } 237 | } 238 | ``` 239 | ## 6.2. Subscribe POLL 240 | ```bash 241 | gnmi_cli -address localhost:10161 \ 242 | -proto "subscribe:, subscription: elem: elem: elem: >>>" \ 243 | -timeout 5s -alsologtostderr \ 244 | -polling_interval 5s \ 245 | -client_crt certs/client1.crt -client_key certs/client1.key -ca_crt certs/onfca.crt 246 | ``` 247 | After running the above command the following output will be printed on the screen every 5 seconds. 248 | ```bash 249 | { 250 | "system": { 251 | "clock": { 252 | "config": { 253 | "timezone-name": "Europe/Dublin" 254 | } 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | ## 6.3. Subscribe Stream 261 | Stream subscriptions are long-lived subscriptions which continue to transmit updates relating to the set of paths that are covered within the subscription indefinitely. The current implementaiton of the simulator supports the following stream modes: 262 | 263 | ### 6.3.1. ON\_CHANGE 264 | When a subscription is defined to be "on change", data updates are only sent when the value of the data item changes. To test this mode, you should follow the following steps: 265 | 266 | 1. First you need to run the following command to subcribe for the events on a path: 267 | ```bash 268 | gnmi_cli -address localhost:10161 \ 269 | -proto "subscribe:, subscription: elem: elem: elem: >>>" \ 270 | -timeout 5s -alsologtostderr \ 271 | -polling_interval 5s \ 272 | -client_crt certs/client1.crt -client_key certs/client1.key -ca_crt certs/onfca.crt 273 | ``` 274 | 275 | After running the above command, you need to make a change in the timezone-name using set command as follows to get an update from the target about that change. 276 | 277 | ```bash 278 | gnmi_cli -address localhost:10161 \ 279 | -set \ 280 | -proto "update: elem: elem: elem: > val: >" \ 281 | -timeout 5s \ 282 | -alsologtostderr \ 283 | -client_crt certs/client1.crt \ 284 | -client_key certs/client1.key \ 285 | -ca_crt certs/onfca.crt 286 | ``` 287 | 288 | The output in the terminal which runs subscribe stream will be like this: 289 | ```bash 290 | { 291 | "system": { 292 | "clock": { 293 | "config": { 294 | "timezone-name": "Europe/Spain" 295 | } 296 | } 297 | } 298 | } 299 | { 300 | "system": { 301 | "clock": { 302 | "config": { 303 | "timezone-name": "Europe/Spain" 304 | } 305 | } 306 | } 307 | } 308 | ``` 309 | 310 | ### 6.3.2. SAMPLE 311 | A subscription that is defined to be sampled MUST be specified along with a *sample_interval* encoded as an unsigned 64-bit integer representing nanoseconds between samples. The target sends The value of the data item(s) once per sample interval to the client. For example, we would like to subscribe to receive *timezone-name* value from the gnmi target every 5 seconds. To do that, we can use the following command: 312 | ```bash 313 | gnmi_cli -address localhost:10161 \ 314 | "subscribe:, subscription: elem: elem: elem: >>>" \ 315 | -timeout 5s \ 316 | -alsologtostderr \ 317 | -client_crt certs/client1.crt \ 318 | -client_key certs/client1.key \ 319 | -ca_crt certs/onfca.crt 320 | ``` 321 | Every 5 seconds, the following output will be printed on the screen: 322 | ```bash 323 | { 324 | "system": { 325 | "clock": { 326 | "config": { 327 | "timezone-name": "Europe/Dublin" 328 | } 329 | } 330 | } 331 | } 332 | ``` 333 | The following assumptions have been made based on gNMI specefication to implement 334 | the subscribe SAMPLE mode: 335 | 336 | 1. If the client sets the *sample_interval* to 0, the target uses the 337 | lowest sample interval (i.e. *lowestSampleInterval* variable) which is defined in target and has the default value of 5 seconds (i.e. 5000000000 nanoseconds). 338 | 339 | 2. If the client sets the *sample_interval* to a value lower than *lowestSampleInterval* then the target rejects the request and returns an *InvalidArgument (3)* error code. 340 | 341 | ### 6.3.3. TARGET\_DEFINED 342 | In the current version of the gnmi simulator, we define TARGET_DEFINED mode to behave 343 | always like ON_CHANGE mode. Accroding to the gNMI spec, the target MUST determine the best type of subscription to be created on a per-leaf basis. 344 | 345 | 346 | # 7. Troubleshooting 347 | 348 | ## 7.1. Deadline exceeded 349 | If you get an error like 350 | ```bash 351 | E0416 15:23:08.099600 22997 gnmi_cli.go:180] could not create a gNMI client: 352 | Dialer(localhost:10161, 5s): context deadline exceeded 353 | ``` 354 | 355 | or anything about __deadline exceeded__, then it is **always** related to the 356 | transport mechanism above gNMI i.e. TCP or HTTPS 357 | 358 | ## 7.2. TCP diagnosis 359 | > This is not a concern with port mapping method using localhost and is for 360 | > the Linux specific option only 361 | 362 | Starting with TCP - see if you can ping the device 363 | 1. by IP address e.g. 17.18.0.2 - if not it might not be up or there's some 364 | other network problem 365 | 2. by short name e.g. device1 - if not maybe your /etc/hosts file is wrong or 366 | DNS domain search is not opennetworking.org 367 | 3. by long name e.g. device1.opennetworking.org - if not maybe your /etc/hosts 368 | file is wrong 369 | 370 | For the last 2 cases make sure that the IP address that is resolved matches what 371 | was given at the startup of the simulator with docker. 372 | 373 | ## 7.3. HTTP Diagnosis 374 | If TCP shows reachability then try with HTTPS - it's very important to remember 375 | that for HTTPS the address at which you access the server **must** match exactly 376 | the server name in the server key's Common Name (CN) like __localhost__ or 377 | __device1.opennetworking.org__ (and not an IP address!) 378 | 379 | Try using cURL to determine if there is a certificate problem 380 | ``` 381 | curl -v https://localhost:10164 --key certs/client1.key --cert certs/client1.crt --cacert certs/onfca.crt 382 | ``` 383 | This might give an error like 384 | ```bash 385 | * Rebuilt URL to: https://localhost:10163/ 386 | * Trying 172.18.0.3... 387 | * TCP_NODELAY set 388 | * Connected to localhost (127.0.0.1) port 10163 (#0) 389 | * ALPN, offering h2 390 | * ALPN, offering http/1.1 391 | * successfully set certificate verify locations: 392 | * CAfile: certs/onfca.crt 393 | CApath: /etc/ssl/certs 394 | * TLSv1.2 (OUT), TLS handshake, Client hello (1): 395 | * TLSv1.2 (IN), TLS handshake, Server hello (2): 396 | * TLSv1.2 (IN), TLS handshake, Certificate (11): 397 | * TLSv1.2 (IN), TLS handshake, Server key exchange (12): 398 | * TLSv1.2 (IN), TLS handshake, Request CERT (13): 399 | * TLSv1.2 (IN), TLS handshake, Server finished (14): 400 | * TLSv1.2 (OUT), TLS handshake, Certificate (11): 401 | * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): 402 | * TLSv1.2 (OUT), TLS handshake, CERT verify (15): 403 | * TLSv1.2 (OUT), TLS change cipher, Client hello (1): 404 | * TLSv1.2 (OUT), TLS handshake, Finished (20): 405 | * TLSv1.2 (IN), TLS handshake, Finished (20): 406 | * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 407 | * ALPN, server accepted to use h2 408 | * Server certificate: 409 | * subject: C=US; ST=CA; L=MenloPark; O=ONF; OU=Engineering; CN=device3.opennetworking.org 410 | * start date: Apr 16 14:40:46 2019 GMT 411 | * expire date: Apr 15 14:40:46 2020 GMT 412 | * SSL: certificate subject name 'device3.opennetworking.org' does not match target host name 'localhost' 413 | * stopped the pause stream! 414 | * Closing connection 0 415 | * TLSv1.2 (OUT), TLS alert, Client hello (1): 416 | curl: (51) SSL: certificate subject name 'device3.opennetworking.org' does not match target host name 'localhost' 417 | ``` 418 | 419 | > In this case the device at __localhost__ has a certificate for 420 | > device3.opennetworking.org. HTTPS does not accept this as a valid certificate 421 | > as it indicates someone might be spoofing the server. This happens today in 422 | > your browser if you access a site through HTTPS whose certificate CN does not 423 | > match the URL - it is just a fact of life with HTTPS, and is not peculiar to gNMI. 424 | 425 | Alternatively a message like the following can occur: 426 | ```bash 427 | * Rebuilt URL to: https://onos-config:5150/ 428 | * Trying 172.17.0.4... 429 | * TCP_NODELAY set 430 | * Connected to onos-config (172.17.0.4) port 5150 (#0) 431 | * ALPN, offering h2 432 | * ALPN, offering http/1.1 433 | * successfully set certificate verify locations: 434 | * CAfile: ../simulators/pkg/certs/onfca.crt 435 | CApath: /etc/ssl/certs 436 | * (304) (OUT), TLS handshake, Client hello (1): 437 | * (304) (IN), TLS alert, Server hello (2): 438 | * error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure 439 | * stopped the pause stream! 440 | * Closing connection 0 441 | curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure 442 | ``` 443 | > This could mean many things - e.g. that the cert on the server is empty or 444 | > that the Full Qualified Domainname (FQDN) of the device does not match the 445 | > subject of the certificate. 446 | 447 | When device names and certificates match, then curl will reply with a message like: 448 | ```bash 449 | curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2) 450 | ``` 451 | 452 | > This means the HTTPS handshake was __successful__, and it has failed at the 453 | > gNMI level - not surprising since we did not send it any gNMI payload. At this 454 | > stage you should be able to use **gnmi_cli** directly. 455 | -------------------------------------------------------------------------------- /pkg/gnmi/util.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Package gnmi implements a gnmi server to mock a device with YANG models. 6 | package gnmi 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "reflect" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/openconfig/ygot/ytypes" 18 | 19 | "github.com/openconfig/goyang/pkg/yang" 20 | "github.com/openconfig/ygot/ygot" 21 | "google.golang.org/grpc/codes" 22 | "google.golang.org/grpc/status" 23 | 24 | "github.com/openconfig/gnmi/proto/gnmi" 25 | pb "github.com/openconfig/gnmi/proto/gnmi" 26 | "github.com/openconfig/gnmi/value" 27 | ) 28 | 29 | // getChildNode gets a node's child with corresponding schema specified by path 30 | // element. If not found and createIfNotExist is set as true, an empty node is 31 | // created and returned. 32 | func getChildNode(node map[string]interface{}, schema *yang.Entry, elem *pb.PathElem, createIfNotExist bool) (interface{}, *yang.Entry) { 33 | var nextSchema *yang.Entry 34 | var ok bool 35 | 36 | if nextSchema, ok = schema.Dir[elem.Name]; !ok { 37 | return nil, nil 38 | } 39 | 40 | var nextNode interface{} 41 | if elem.GetKey() == nil { 42 | if nextNode, ok = node[elem.Name]; !ok { 43 | if createIfNotExist { 44 | node[elem.Name] = make(map[string]interface{}) 45 | nextNode = node[elem.Name] 46 | } 47 | } 48 | return nextNode, nextSchema 49 | } 50 | 51 | nextNode = getKeyedListEntry(node, elem, createIfNotExist) 52 | return nextNode, nextSchema 53 | } 54 | 55 | // getKeyedListEntry finds the keyed list entry in node by the name and key of 56 | // path elem. If entry is not found and createIfNotExist is true, an empty entry 57 | // will be created (the list will be created if necessary). 58 | func getKeyedListEntry(node map[string]interface{}, elem *pb.PathElem, createIfNotExist bool) map[string]interface{} { 59 | curNode, ok := node[elem.Name] 60 | if !ok { 61 | if !createIfNotExist { 62 | return nil 63 | } 64 | 65 | // Create a keyed list as node child and initialize an entry. 66 | m := make(map[string]interface{}) 67 | for k, v := range elem.Key { 68 | m[k] = v 69 | if vAsNum, err := strconv.ParseFloat(v, 64); err == nil { 70 | m[k] = vAsNum 71 | } 72 | } 73 | node[elem.Name] = []interface{}{m} 74 | return m 75 | } 76 | 77 | // Search entry in keyed list. 78 | keyedList, ok := curNode.([]interface{}) 79 | if !ok { 80 | return nil 81 | } 82 | for _, n := range keyedList { 83 | m, ok := n.(map[string]interface{}) 84 | if !ok { 85 | log.Errorf("wrong keyed list entry type: %T", n) 86 | return nil 87 | } 88 | keyMatching := true 89 | // must be exactly match 90 | for k, v := range elem.Key { 91 | attrVal, ok := m[k] 92 | if !ok { 93 | return nil 94 | } 95 | if v != fmt.Sprintf("%v", attrVal) { 96 | keyMatching = false 97 | break 98 | } 99 | } 100 | if keyMatching { 101 | return m 102 | } 103 | } 104 | if !createIfNotExist { 105 | return nil 106 | } 107 | 108 | // Create an entry in keyed list. 109 | m := make(map[string]interface{}) 110 | for k, v := range elem.Key { 111 | m[k] = v 112 | if vAsNum, err := strconv.ParseFloat(v, 64); err == nil { 113 | m[k] = vAsNum 114 | } 115 | } 116 | node[elem.Name] = append(keyedList, m) 117 | return m 118 | } 119 | 120 | // gnmiFullPath builds the full path from the prefix and path. 121 | func gnmiFullPath(prefix, path *pb.Path) *pb.Path { 122 | fullPath := &pb.Path{Origin: path.Origin} 123 | if path.GetElement() != nil { 124 | fullPath.Element = append(prefix.GetElement(), path.GetElement()...) 125 | } 126 | if path.GetElem() != nil { 127 | fullPath.Elem = append(prefix.GetElem(), path.GetElem()...) 128 | } 129 | return fullPath 130 | } 131 | 132 | // isNIl checks if an interface is nil or its value is nil. 133 | func isNil(i interface{}) bool { 134 | if i == nil { 135 | return true 136 | } 137 | switch kind := reflect.ValueOf(i).Kind(); kind { 138 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 139 | return reflect.ValueOf(i).IsNil() 140 | default: 141 | return false 142 | } 143 | } 144 | 145 | func (s *Server) toGoStruct(jsonTree map[string]interface{}) (ygot.ValidatedGoStruct, error) { 146 | jsonDump, err := json.Marshal(jsonTree) 147 | if err != nil { 148 | return nil, fmt.Errorf("error in marshaling IETF JSON tree to bytes: %v", err) 149 | } 150 | goStruct, err := s.model.NewConfigStruct(jsonDump) 151 | if err != nil { 152 | return nil, fmt.Errorf("error in creating config struct from IETF JSON data: %v", err) 153 | } 154 | return goStruct, nil 155 | } 156 | 157 | // checkEncodingAndModel checks whether encoding and models are supported by the server. Return error if anything is unsupported. 158 | func (s *Server) checkEncodingAndModel(encoding pb.Encoding, models []*pb.ModelData) error { 159 | hasSupportedEncoding := false 160 | for _, supportedEncoding := range supportedEncodings { 161 | if encoding == supportedEncoding { 162 | hasSupportedEncoding = true 163 | break 164 | } 165 | } 166 | if !hasSupportedEncoding { 167 | return fmt.Errorf("unsupported encoding: %s", pb.Encoding_name[int32(encoding)]) 168 | } 169 | for _, m := range models { 170 | isSupported := false 171 | for _, supportedModel := range s.model.modelData { 172 | if reflect.DeepEqual(m, supportedModel) { 173 | isSupported = true 174 | break 175 | } 176 | } 177 | if !isSupported { 178 | return fmt.Errorf("unsupported model: %v", m) 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | // InternalUpdate is an experimental feature to let the server update its 185 | // internal states. Use it with your own risk. 186 | func (s *Server) InternalUpdate(fp func(config ygot.ValidatedGoStruct) error) error { 187 | s.configMu.Lock() 188 | defer s.configMu.Unlock() 189 | return fp(s.config) 190 | } 191 | 192 | // GetConfig returns the config store 193 | func (s *Server) GetConfig() (ygot.ValidatedGoStruct, error) { 194 | return s.config, nil 195 | } 196 | 197 | // deleteKeyedListEntry deletes the keyed list entry from node that matches the 198 | // path elem. If the entry is the only one in keyed list, deletes the entire 199 | // list. If the entry is found and deleted, the function returns true. If it is 200 | // not found, the function returns false. 201 | func deleteKeyedListEntry(node map[string]interface{}, elem *pb.PathElem) bool { 202 | curNode, ok := node[elem.Name] 203 | if !ok { 204 | return false 205 | } 206 | 207 | keyedList, ok := curNode.([]interface{}) 208 | if !ok { 209 | return false 210 | } 211 | for i, n := range keyedList { 212 | m, ok := n.(map[string]interface{}) 213 | if !ok { 214 | log.Errorf("expect map[string]interface{} for a keyed list entry, got %T", n) 215 | return false 216 | } 217 | keyMatching := true 218 | for k, v := range elem.Key { 219 | attrVal, ok := m[k] 220 | if !ok { 221 | return false 222 | } 223 | if v != fmt.Sprintf("%v", attrVal) { 224 | keyMatching = false 225 | break 226 | } 227 | } 228 | if keyMatching { 229 | listLen := len(keyedList) 230 | if listLen == 1 { 231 | delete(node, elem.Name) 232 | return true 233 | } 234 | keyedList[i] = keyedList[listLen-1] 235 | node[elem.Name] = keyedList[0 : listLen-1] 236 | return true 237 | } 238 | } 239 | return false 240 | } 241 | 242 | // setPathWithAttribute replaces or updates a child node of curNode in the IETF 243 | // JSON config tree, where the child node is indexed by pathElem with attribute. 244 | // The function returns grpc status error if unsuccessful. 245 | func setPathWithAttribute(op pb.UpdateResult_Operation, curNode map[string]interface{}, pathElem *pb.PathElem, nodeVal interface{}) error { 246 | nodeValAsTree, ok := nodeVal.(map[string]interface{}) 247 | if !ok { 248 | return status.Errorf(codes.InvalidArgument, "expect nodeVal is a json node of map[string]interface{}, received %T", nodeVal) 249 | } 250 | m := getKeyedListEntry(curNode, pathElem, true) 251 | if m == nil { 252 | return status.Errorf(codes.NotFound, "path elem not found: %v", pathElem) 253 | } 254 | if op == pb.UpdateResult_REPLACE { 255 | for k := range m { 256 | delete(m, k) 257 | } 258 | } 259 | for attrKey, attrVal := range pathElem.GetKey() { 260 | m[attrKey] = attrVal 261 | if asNum, err := strconv.ParseFloat(attrVal, 64); err == nil { 262 | m[attrKey] = asNum 263 | } 264 | for k, v := range nodeValAsTree { 265 | if k == attrKey && fmt.Sprintf("%v", v) != attrVal { 266 | return status.Errorf(codes.InvalidArgument, "invalid config data: %v is a path attribute", k) 267 | } 268 | } 269 | } 270 | for k, v := range nodeValAsTree { 271 | m[k] = v 272 | } 273 | return nil 274 | } 275 | 276 | // setPathWithoutAttribute replaces or updates a child node of curNode in the 277 | // IETF config tree, where the child node is indexed by pathElem without 278 | // attribute. The function returns grpc status error if unsuccessful. 279 | func setPathWithoutAttribute(op pb.UpdateResult_Operation, curNode map[string]interface{}, pathElem *pb.PathElem, nodeVal interface{}) error { 280 | target, hasElem := curNode[pathElem.Name] 281 | nodeValAsTree, nodeValIsTree := nodeVal.(map[string]interface{}) 282 | if op == pb.UpdateResult_REPLACE || !hasElem || !nodeValIsTree { 283 | curNode[pathElem.Name] = nodeVal 284 | return nil 285 | } 286 | targetAsTree, ok := target.(map[string]interface{}) 287 | if !ok { 288 | return status.Errorf(codes.Internal, "error in setting path: expect map[string]interface{} to update, got %T", target) 289 | } 290 | for k, v := range nodeValAsTree { 291 | targetAsTree[k] = v 292 | } 293 | return nil 294 | } 295 | 296 | // sendResponse sends an SubscribeResponse to a gNMI client. 297 | func (s *Server) sendResponse(response *pb.SubscribeResponse, stream pb.GNMI_SubscribeServer) { 298 | log.Info("Sending SubscribeResponse out to gNMI client: ", response) 299 | err := stream.Send(response) 300 | if err != nil { 301 | //TODO remove channel registrations 302 | log.Errorf("Error in sending response to client %v", err) 303 | } 304 | } 305 | 306 | // getUpdateForPath finds a leaf node in the tree based on a given path, build the update message and return it back to the collector 307 | func (s *Server) getUpdateForPath(fullPath *pb.Path) (*pb.Update, error) { 308 | 309 | node, err := ytypes.GetNode(s.model.schemaTreeRoot, s.config, fullPath, nil) 310 | if isNil(node) || err != nil { 311 | return nil, err 312 | } 313 | 314 | _, ok := node[0].Data.(ygot.GoStruct) 315 | // Return leaf node. 316 | if !ok { 317 | var val *pb.TypedValue 318 | switch kind := reflect.ValueOf(node).Kind(); kind { 319 | case reflect.Ptr, reflect.Interface: 320 | var err error 321 | val, err = value.FromScalar(reflect.ValueOf(node).Elem().Interface()) 322 | if err != nil { 323 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", fullPath, err) 324 | log.Error(msg) 325 | return nil, status.Error(codes.Internal, msg) 326 | } 327 | case reflect.Int64: 328 | enumMap, ok := s.model.enumData[reflect.TypeOf(node).Name()] 329 | if !ok { 330 | return nil, status.Error(codes.Internal, "not a GoStruct enumeration type") 331 | 332 | } 333 | val = &pb.TypedValue{ 334 | Value: &pb.TypedValue_StringVal{ 335 | StringVal: enumMap[reflect.ValueOf(node).Int()].Name, 336 | }, 337 | } 338 | 339 | case reflect.Slice: 340 | var err error 341 | switch kind := reflect.ValueOf(node[0].Data).Kind(); kind { 342 | case reflect.Int64: 343 | enumMap, ok := s.model.enumData[reflect.TypeOf(node[0].Data).Name()] 344 | if !ok { 345 | return nil, status.Error(codes.Internal, "not a GoStruct enumeration type") 346 | } 347 | val = &pb.TypedValue{ 348 | Value: &pb.TypedValue_StringVal{ 349 | StringVal: enumMap[reflect.ValueOf(node[0].Data).Int()].Name, 350 | }, 351 | } 352 | default: 353 | val, err = value.FromScalar(reflect.ValueOf(node[0].Data).Elem().Interface()) 354 | if err != nil { 355 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", fullPath, err) 356 | log.Error(msg) 357 | return nil, status.Error(codes.Internal, msg) 358 | } 359 | } 360 | default: 361 | return nil, status.Errorf(codes.Internal, "unexpected kind of leaf node type: %v %v", node, kind) 362 | } 363 | 364 | update := &pb.Update{Path: fullPath, Val: val} 365 | return update, nil 366 | 367 | } 368 | 369 | return nil, nil 370 | 371 | } 372 | 373 | // getUpdate finds the node in the tree, build the update message and return it back to the collector 374 | func (s *Server) getUpdate(c *streamClient, subList *pb.SubscriptionList, path *pb.Path) (*pb.Update, error) { 375 | 376 | fullPath := path 377 | prefix := subList.GetPrefix() 378 | if prefix != nil { 379 | fullPath = gnmiFullPath(prefix, path) 380 | } 381 | if fullPath.GetElem() == nil && fullPath.GetElement() != nil { 382 | return nil, status.Error(codes.Unimplemented, "deprecated path element type is unsupported") 383 | } 384 | node, err := ytypes.GetNode(s.model.schemaTreeRoot, s.config, fullPath, nil) 385 | if isNil(node) || err != nil { 386 | return nil, err 387 | } 388 | 389 | nodeStruct, ok := node[0].Data.(ygot.GoStruct) 390 | 391 | // Return leaf node. 392 | if !ok { 393 | var val *pb.TypedValue 394 | switch kind := reflect.ValueOf(node).Kind(); kind { 395 | case reflect.Ptr, reflect.Interface: 396 | var err error 397 | val, err = value.FromScalar(reflect.ValueOf(node).Elem().Interface()) 398 | if err != nil { 399 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", path, err) 400 | log.Error(msg) 401 | return nil, status.Error(codes.Internal, msg) 402 | } 403 | case reflect.Int64: 404 | enumMap, ok := s.model.enumData[reflect.TypeOf(node).Name()] 405 | if !ok { 406 | return nil, status.Error(codes.Internal, "not a GoStruct enumeration type") 407 | 408 | } 409 | val = &pb.TypedValue{ 410 | Value: &pb.TypedValue_StringVal{ 411 | StringVal: enumMap[reflect.ValueOf(node).Int()].Name, 412 | }, 413 | } 414 | 415 | case reflect.Slice: 416 | var err error 417 | switch kind := reflect.ValueOf(node[0].Data).Kind(); kind { 418 | case reflect.Int64: 419 | //fmt.Println(reflect.TypeOf(node[0].Data).Elem()) 420 | enumMap, ok := s.model.enumData[reflect.TypeOf(node[0].Data).Name()] 421 | if !ok { 422 | return nil, status.Error(codes.Internal, "not a GoStruct enumeration type") 423 | } 424 | val = &pb.TypedValue{ 425 | Value: &pb.TypedValue_StringVal{ 426 | StringVal: enumMap[reflect.ValueOf(node[0].Data).Int()].Name, 427 | }, 428 | } 429 | default: 430 | val, err = value.FromScalar(reflect.ValueOf(node[0].Data).Elem().Interface()) 431 | if err != nil { 432 | msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", path, err) 433 | log.Error(msg) 434 | return nil, status.Error(codes.Internal, msg) 435 | } 436 | } 437 | 438 | default: 439 | return nil, status.Errorf(codes.Internal, "unexpected kind of leaf node type: %v %v", node, kind) 440 | } 441 | 442 | update := &pb.Update{Path: path, Val: val} 443 | return update, nil 444 | 445 | } 446 | 447 | // Return IETF JSON for the sub-tree. 448 | jsonTree, err := ygot.ConstructIETFJSON(nodeStruct, &ygot.RFC7951JSONConfig{AppendModuleName: true}) 449 | if err != nil { 450 | msg := fmt.Sprintf("error in constructing IETF JSON tree from requested node: %v", err) 451 | log.Error(msg) 452 | return nil, status.Error(codes.Internal, msg) 453 | } 454 | jsonDump, err := json.Marshal(jsonTree) 455 | if err != nil { 456 | msg := fmt.Sprintf("error in marshaling IETF JSON tree to bytes: %v", err) 457 | log.Error(msg) 458 | return nil, status.Error(codes.Internal, msg) 459 | } 460 | update := &pb.Update{ 461 | Path: path, 462 | Val: &pb.TypedValue{ 463 | Value: &pb.TypedValue_JsonIetfVal{ 464 | JsonIetfVal: jsonDump, 465 | }, 466 | }, 467 | } 468 | 469 | return update, nil 470 | 471 | } 472 | 473 | // collector collects the latest update from the config. 474 | func (s *Server) collector(c *streamClient, request *pb.SubscriptionList) { 475 | for _, sub := range request.Subscription { 476 | path := sub.GetPath() 477 | update, err := s.getUpdate(c, request, path) 478 | 479 | if err != nil { 480 | log.Info("Error while collecting data for subscribe once or poll", err) 481 | update = &pb.Update{ 482 | Path: path, 483 | } 484 | c.UpdateChan <- update 485 | } 486 | 487 | if err == nil { 488 | c.UpdateChan <- update 489 | } 490 | } 491 | } 492 | 493 | // listenForUpdates reads update messages from the update channel, creates a 494 | // subscribe response and send it to the gnmi client. 495 | func (s *Server) listenForUpdates(c *streamClient) { 496 | for update := range c.UpdateChan { 497 | if update.Val == nil { 498 | deleteResponse := buildDeleteResponse(update.GetPath()) 499 | s.sendResponse(deleteResponse, c.stream) 500 | syncResponse := buildSyncResponse() 501 | s.sendResponse(syncResponse, c.stream) 502 | 503 | } else { 504 | response, _ := buildSubResponse(update) 505 | s.sendResponse(response, c.stream) 506 | syncResponse := buildSyncResponse() 507 | s.sendResponse(syncResponse, c.stream) 508 | } 509 | } 510 | } 511 | 512 | // configEventProducer produces update events for stream subscribers. 513 | func (s *Server) listenToConfigEvents(request *pb.SubscriptionList) { 514 | for v := range s.ConfigUpdate.Out() { 515 | update := v.(*pb.Update) 516 | subscribers := s.getSubscribers() 517 | for key, c := range subscribers { 518 | if key == update.GetPath().String() { 519 | newUpdateValue, err := s.getUpdate(c, request, update.GetPath()) 520 | 521 | if err != nil { 522 | deleteResponse := buildDeleteResponse(update.GetPath()) 523 | s.sendResponse(deleteResponse, c.stream) 524 | syncResponse := buildSyncResponse() 525 | s.sendResponse(syncResponse, c.stream) 526 | 527 | } else { 528 | update.Val = newUpdateValue.Val 529 | 530 | // builds subscription response 531 | response, _ := buildSubResponse(update) 532 | 533 | s.sendResponse(response, c.stream) 534 | // builds Sync response 535 | syncResponse := buildSyncResponse() 536 | s.sendResponse(syncResponse, c.stream) 537 | } 538 | } 539 | } 540 | } 541 | } 542 | 543 | func (s *Server) getSubscribers() map[string]*streamClient { 544 | s.subMu.RLock() 545 | defer s.subMu.RUnlock() 546 | subscribers := make(map[string]*streamClient) 547 | for key, c := range s.subscribers { 548 | subscribers[key] = c 549 | } 550 | return subscribers 551 | } 552 | 553 | func (s *Server) addSubscriber(key string, c *streamClient) { 554 | s.subMu.Lock() 555 | s.subscribers[key] = c 556 | s.subMu.Unlock() 557 | } 558 | 559 | // buildSubResponse builds a subscribeResponse based on the given Update message. 560 | func buildSubResponse(update *pb.Update) (*pb.SubscribeResponse, error) { 561 | updateArray := make([]*pb.Update, 0) 562 | updateArray = append(updateArray, update) 563 | notification := &pb.Notification{ 564 | Timestamp: time.Now().Unix(), 565 | Update: updateArray, 566 | } 567 | responseUpdate := &pb.SubscribeResponse_Update{ 568 | Update: notification, 569 | } 570 | response := &pb.SubscribeResponse{ 571 | Response: responseUpdate, 572 | } 573 | 574 | return response, nil 575 | } 576 | 577 | // buildDeleteResponse builds a subscribe response for the given deleted path. 578 | func buildDeleteResponse(delete *pb.Path) *gnmi.SubscribeResponse { 579 | deleteArray := []*gnmi.Path{delete} 580 | notification := &gnmi.Notification{ 581 | Timestamp: time.Now().Unix(), 582 | Delete: deleteArray, 583 | } 584 | responseUpdate := &gnmi.SubscribeResponse_Update{ 585 | Update: notification, 586 | } 587 | response := &gnmi.SubscribeResponse{ 588 | Response: responseUpdate, 589 | } 590 | return response 591 | } 592 | 593 | // buildSyncResponse builds a sync response. 594 | func buildSyncResponse() *gnmi.SubscribeResponse { 595 | responseSync := &gnmi.SubscribeResponse_SyncResponse{ 596 | SyncResponse: true, 597 | } 598 | return &gnmi.SubscribeResponse{ 599 | Response: responseSync, 600 | } 601 | } 602 | 603 | // Contains checks the existence of a given string in an array of strings. 604 | func Contains(a []string, x string) bool { 605 | for _, n := range a { 606 | if x == n { 607 | return true 608 | } 609 | } 610 | return false 611 | } 612 | 613 | // checkPathContainType checks if the path contains the data type element 614 | func checkPathContainType(fullPath *pb.Path, dataTypeString string) bool { 615 | elements := fullPath.GetElem() 616 | var dataTypeFlag bool 617 | dataTypeFlag = false 618 | if strings.Compare(dataTypeString, "all") == 0 { 619 | dataTypeFlag = true 620 | return dataTypeFlag 621 | } 622 | for _, elem := range elements { 623 | if strings.Compare(dataTypeString, elem.GetName()) == 0 { 624 | dataTypeFlag = true 625 | break 626 | } 627 | } 628 | return dataTypeFlag 629 | } 630 | 631 | // pruneConfigData prunes the given JSON subtree based on the given data type and path info. 632 | func pruneConfigData(data interface{}, dataType string, fullPath *pb.Path) interface{} { 633 | 634 | if reflect.ValueOf(data).Kind() == reflect.Slice { 635 | d := reflect.ValueOf(data) 636 | tmpData := make([]interface{}, d.Len()) 637 | returnSlice := make([]interface{}, d.Len()) 638 | for i := 0; i < d.Len(); i++ { 639 | tmpData[i] = d.Index(i).Interface() 640 | } 641 | for i, v := range tmpData { 642 | returnSlice[i] = pruneConfigData(v, dataType, fullPath) 643 | } 644 | return returnSlice 645 | } else if reflect.ValueOf(data).Kind() == reflect.Map { 646 | d := reflect.ValueOf(data) 647 | tmpData := make(map[string]interface{}) 648 | for _, k := range d.MapKeys() { 649 | match, _ := regexp.MatchString(dataType, k.String()) 650 | matchAll := strings.Compare(dataType, "all") 651 | typeOfValue := reflect.TypeOf(d.MapIndex(k).Interface()).Kind() 652 | 653 | if match || matchAll == 0 { 654 | newKey := k.String() 655 | if typeOfValue == reflect.Map || typeOfValue == reflect.Slice { 656 | tmpData[newKey] = pruneConfigData(d.MapIndex(k).Interface(), dataType, fullPath) 657 | 658 | } else { 659 | tmpData[newKey] = d.MapIndex(k).Interface() 660 | } 661 | } else { 662 | tmpIteration := pruneConfigData(d.MapIndex(k).Interface(), dataType, fullPath) 663 | if typeOfValue == reflect.Map { 664 | tmpMap := tmpIteration.(map[string]interface{}) 665 | if len(tmpMap) != 0 { 666 | tmpData[k.String()] = tmpIteration 667 | if Contains(dataTypes, k.String()) { 668 | delete(tmpData, k.String()) 669 | } 670 | } 671 | } else if typeOfValue == reflect.Slice { 672 | tmpMap := tmpIteration.([]interface{}) 673 | if len(tmpMap) != 0 { 674 | tmpData[k.String()] = tmpIteration 675 | if Contains(dataTypes, k.String()) { 676 | delete(tmpData, k.String()) 677 | 678 | } 679 | } 680 | } else { 681 | tmpData[k.String()] = d.MapIndex(k).Interface() 682 | 683 | } 684 | } 685 | 686 | } 687 | 688 | return tmpData 689 | } 690 | return data 691 | } 692 | 693 | func buildUpdate(b []byte, path *pb.Path, valType string) *pb.Update { 694 | var update *pb.Update 695 | 696 | if strings.Compare(valType, "Internal") == 0 { 697 | update = &pb.Update{Path: path, Val: &pb.TypedValue{Value: &pb.TypedValue_JsonVal{JsonVal: b}}} 698 | return update 699 | } 700 | update = &pb.Update{Path: path, Val: &pb.TypedValue{Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: b}}} 701 | 702 | return update 703 | } 704 | 705 | func jsonEncoder(encoderType string, nodeStruct ygot.GoStruct) (map[string]interface{}, error) { 706 | 707 | if strings.Compare(encoderType, "Internal") == 0 { 708 | return ygot.ConstructInternalJSON(nodeStruct) 709 | } 710 | 711 | return ygot.ConstructIETFJSON(nodeStruct, &ygot.RFC7951JSONConfig{AppendModuleName: true}) 712 | 713 | } 714 | -------------------------------------------------------------------------------- /pkg/gnmi/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-present Open Networking Foundation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | package gnmi 5 | 6 | import ( 7 | "encoding/json" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/golang/protobuf/proto" 12 | "github.com/openconfig/gnmi/value" 13 | "github.com/openconfig/ygot/ygot" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | 17 | pb "github.com/openconfig/gnmi/proto/gnmi" 18 | 19 | "github.com/onosproject/gnxi-simulators/pkg/gnmi/modeldata" 20 | "github.com/onosproject/gnxi-simulators/pkg/gnmi/modeldata/gostruct" 21 | ) 22 | 23 | var ( 24 | // model is the model for test config server. 25 | model = &Model{ 26 | modelData: modeldata.ModelData, 27 | structRootType: reflect.TypeOf((*gostruct.Device)(nil)), 28 | schemaTreeRoot: gostruct.SchemaTree["Device"], 29 | jsonUnmarshaler: gostruct.Unmarshal, 30 | enumData: gostruct.ΛEnum, 31 | } 32 | ) 33 | 34 | func TestCapabilities(t *testing.T) { 35 | s, err := NewServer(model, nil, nil) 36 | if err != nil { 37 | t.Fatalf("error in creating server: %v", err) 38 | } 39 | resp, err := s.Capabilities(nil, &pb.CapabilityRequest{}) 40 | if err != nil { 41 | t.Fatalf("got error %v, want nil", err) 42 | } 43 | if !reflect.DeepEqual(resp.GetSupportedModels(), model.modelData) { 44 | t.Errorf("got supported models %v\nare not the same as\nmodel supported by the server %v", resp.GetSupportedModels(), model.modelData) 45 | } 46 | if !reflect.DeepEqual(resp.GetSupportedEncodings(), supportedEncodings) { 47 | t.Errorf("got supported encodings %v\nare not the same as\nencodings supported by the server %v", resp.GetSupportedEncodings(), supportedEncodings) 48 | } 49 | } 50 | 51 | func TestGet(t *testing.T) { 52 | jsonConfigRoot := `{ 53 | "openconfig-system:system": { 54 | "openconfig-openflow:openflow": { 55 | "agent": { 56 | "config": { 57 | "failure-mode": "SECURE", 58 | "max-backoff": 10 59 | } 60 | } 61 | } 62 | }, 63 | "openconfig-platform:components": { 64 | "component": [ 65 | { 66 | "config": { 67 | "name": "swpri1-1-1" 68 | }, 69 | "name": "swpri1-1-1" 70 | } 71 | ] 72 | } 73 | }` 74 | 75 | s, err := NewServer(model, []byte(jsonConfigRoot), nil) 76 | if err != nil { 77 | t.Fatalf("error in creating server: %v", err) 78 | } 79 | 80 | tds := []struct { 81 | desc string 82 | textPbPath string 83 | modelData []*pb.ModelData 84 | wantRetCode codes.Code 85 | wantRespVal interface{} 86 | }{{ 87 | desc: "get valid but non-existing node", 88 | textPbPath: ` 89 | elem: 90 | elem: 91 | `, 92 | wantRetCode: codes.NotFound, 93 | }, { 94 | desc: "root node", 95 | wantRetCode: codes.OK, 96 | wantRespVal: jsonConfigRoot, 97 | }, { 98 | desc: "get non-enum type", 99 | textPbPath: ` 100 | elem: 101 | elem: 102 | elem: 103 | elem: 104 | elem: 105 | `, 106 | wantRetCode: codes.OK, 107 | wantRespVal: uint64(10), 108 | }, { 109 | desc: "get enum type", 110 | textPbPath: ` 111 | elem: 112 | elem: 113 | elem: 114 | elem: 115 | elem: 116 | `, 117 | wantRetCode: codes.OK, 118 | wantRespVal: "SECURE", 119 | }, { 120 | desc: "root child node", 121 | textPbPath: `elem: `, 122 | wantRetCode: codes.OK, 123 | wantRespVal: `{ 124 | "openconfig-platform:component": [{ 125 | "config": { 126 | "name": "swpri1-1-1" 127 | }, 128 | "name": "swpri1-1-1" 129 | }]}`, 130 | }, { 131 | desc: "node with attribute", 132 | textPbPath: ` 133 | elem: 134 | elem: < 135 | name: "component" 136 | key: 137 | >`, 138 | wantRetCode: codes.OK, 139 | wantRespVal: `{ 140 | "openconfig-platform:config": {"name": "swpri1-1-1"}, 141 | "openconfig-platform:name": "swpri1-1-1" 142 | }`, 143 | }, { 144 | desc: "node with attribute in its parent", 145 | textPbPath: ` 146 | elem: 147 | elem: < 148 | name: "component" 149 | key: 150 | > 151 | elem: `, 152 | wantRetCode: codes.OK, 153 | wantRespVal: `{"openconfig-platform:name": "swpri1-1-1"}`, 154 | }, { 155 | desc: "ref leaf node", 156 | textPbPath: ` 157 | elem: 158 | elem: < 159 | name: "component" 160 | key: 161 | > 162 | elem: `, 163 | wantRetCode: codes.OK, 164 | wantRespVal: "swpri1-1-1", 165 | }, { 166 | desc: "regular leaf node", 167 | textPbPath: ` 168 | elem: 169 | elem: < 170 | name: "component" 171 | key: 172 | > 173 | elem: 174 | elem: `, 175 | wantRetCode: codes.OK, 176 | wantRespVal: "swpri1-1-1", 177 | }, { 178 | desc: "non-existing node: wrong path name", 179 | textPbPath: ` 180 | elem: 181 | elem: < 182 | name: "component" 183 | key: 184 | > 185 | elem: `, 186 | wantRetCode: codes.NotFound, 187 | }, { 188 | desc: "non-existing node: wrong path attribute", 189 | textPbPath: ` 190 | elem: 191 | elem: < 192 | name: "component" 193 | key: 194 | > 195 | elem: `, 196 | wantRetCode: codes.NotFound, 197 | }, { 198 | desc: "use of model data not supported", 199 | modelData: []*pb.ModelData{{}}, 200 | wantRetCode: codes.Unimplemented, 201 | }} 202 | 203 | for _, td := range tds { 204 | t.Run(td.desc, func(t *testing.T) { 205 | runTestGet(t, s, td.textPbPath, td.wantRetCode, td.wantRespVal, td.modelData) 206 | }) 207 | } 208 | } 209 | 210 | // runTestGet requests a path from the server by Get grpc call, and compares if 211 | // the return code and response value are expected. 212 | func runTestGet(t *testing.T, s *Server, textPbPath string, wantRetCode codes.Code, wantRespVal interface{}, useModels []*pb.ModelData) { 213 | // Send request 214 | var pbPath pb.Path 215 | if err := proto.UnmarshalText(textPbPath, &pbPath); err != nil { 216 | t.Fatalf("error in unmarshaling path: %v", err) 217 | } 218 | req := &pb.GetRequest{ 219 | Path: []*pb.Path{&pbPath}, 220 | Encoding: pb.Encoding_JSON_IETF, 221 | UseModels: useModels, 222 | } 223 | resp, err := s.Get(nil, req) 224 | // Check return code 225 | gotRetStatus, ok := status.FromError(err) 226 | if !ok { 227 | t.Fatal("got a non-grpc error from grpc call") 228 | } 229 | if gotRetStatus.Code() != wantRetCode { 230 | t.Fatalf("got return code %v, want %v", gotRetStatus.Code(), wantRetCode) 231 | } 232 | 233 | // Check response value 234 | var gotVal interface{} 235 | if resp != nil { 236 | notifs := resp.GetNotification() 237 | if len(notifs) != 1 { 238 | t.Fatalf("got %d notifications, want 1", len(notifs)) 239 | } 240 | updates := notifs[0].GetUpdate() 241 | if len(updates) != 1 { 242 | t.Fatalf("got %d updates in the notification, want 1", len(updates)) 243 | } 244 | val := updates[0].GetVal() 245 | if val.GetJsonIetfVal() == nil { 246 | gotVal, err = value.ToScalar(val) 247 | if err != nil { 248 | t.Errorf("got: %v, want a scalar value", gotVal) 249 | } 250 | } else { 251 | // Unmarshal json data to gotVal container for comparison 252 | if err := json.Unmarshal(val.GetJsonIetfVal(), &gotVal); err != nil { 253 | t.Fatalf("error in unmarshaling IETF JSON data to json container: %v", err) 254 | } 255 | var wantJSONStruct interface{} 256 | if err := json.Unmarshal([]byte(wantRespVal.(string)), &wantJSONStruct); err != nil { 257 | t.Fatalf("error in unmarshaling IETF JSON data to json container: %v", err) 258 | } 259 | wantRespVal = wantJSONStruct 260 | } 261 | } 262 | 263 | if !reflect.DeepEqual(gotVal, wantRespVal) { 264 | t.Errorf("got: %v (%T),\nwant %v (%T)", gotVal, gotVal, wantRespVal, wantRespVal) 265 | } 266 | } 267 | 268 | type gnmiSetTestCase struct { 269 | desc string // description of test case. 270 | initConfig string // config before the operation. 271 | op pb.UpdateResult_Operation // operation type. 272 | textPbPath string // text format of gnmi Path proto. 273 | val *pb.TypedValue // value for UPDATE/REPLACE operations. always nil for DELETE. 274 | wantRetCode codes.Code // grpc return code. 275 | wantConfig string // config after the operation. 276 | } 277 | 278 | func TestDelete(t *testing.T) { 279 | tests := []gnmiSetTestCase{{ 280 | desc: "delete leaf node", 281 | initConfig: `{ 282 | "system": { 283 | "config": { 284 | "hostname": "switch_a", 285 | "login-banner": "Hello!" 286 | } 287 | } 288 | }`, 289 | op: pb.UpdateResult_DELETE, 290 | textPbPath: ` 291 | elem: 292 | elem: 293 | elem: 294 | `, 295 | wantRetCode: codes.OK, 296 | wantConfig: `{ 297 | "system": { 298 | "config": { 299 | "hostname": "switch_a" 300 | } 301 | } 302 | }`, 303 | }, { 304 | desc: "delete sub-tree", 305 | initConfig: `{ 306 | "system": { 307 | "clock": { 308 | "config": { 309 | "timezone-name": "Europe/Stockholm" 310 | } 311 | }, 312 | "config": { 313 | "hostname": "switch_a" 314 | } 315 | } 316 | }`, 317 | op: pb.UpdateResult_DELETE, 318 | textPbPath: ` 319 | elem: 320 | elem: 321 | `, 322 | wantRetCode: codes.OK, 323 | wantConfig: `{ 324 | "system": { 325 | "config": { 326 | "hostname": "switch_a" 327 | } 328 | } 329 | }`, 330 | }, { 331 | desc: "delete a sub-tree with only one leaf node", 332 | initConfig: `{ 333 | "system": { 334 | "clock": { 335 | "config": { 336 | "timezone-name": "Europe/Stockholm" 337 | } 338 | }, 339 | "config": { 340 | "hostname": "switch_a" 341 | } 342 | } 343 | }`, 344 | op: pb.UpdateResult_DELETE, 345 | textPbPath: ` 346 | elem: 347 | elem: 348 | elem: 349 | `, 350 | wantRetCode: codes.OK, 351 | wantConfig: `{ 352 | "system": { 353 | "config": { 354 | "hostname": "switch_a" 355 | } 356 | } 357 | }`, 358 | }, { 359 | desc: "delete a leaf node whose parent has only this child", 360 | initConfig: `{ 361 | "system": { 362 | "clock": { 363 | "config": { 364 | "timezone-name": "Europe/Stockholm" 365 | } 366 | }, 367 | "config": { 368 | "hostname": "switch_a" 369 | } 370 | } 371 | }`, 372 | op: pb.UpdateResult_DELETE, 373 | textPbPath: ` 374 | elem: 375 | elem: 376 | elem: 377 | elem: 378 | `, 379 | wantRetCode: codes.OK, 380 | wantConfig: `{ 381 | "system": { 382 | "config": { 383 | "hostname": "switch_a" 384 | } 385 | } 386 | }`, 387 | }, { 388 | desc: "delete root", 389 | initConfig: `{ 390 | "system": { 391 | "config": { 392 | "hostname": "switch_a" 393 | } 394 | } 395 | }`, 396 | op: pb.UpdateResult_DELETE, 397 | wantRetCode: codes.OK, 398 | wantConfig: `{}`, 399 | }, { 400 | desc: "delete non-existing node", 401 | initConfig: `{ 402 | "system": { 403 | "clock": { 404 | "config": { 405 | "timezone-name": "Europe/Stockholm" 406 | } 407 | } 408 | } 409 | }`, 410 | op: pb.UpdateResult_DELETE, 411 | textPbPath: ` 412 | elem: 413 | elem: 414 | elem: 415 | elem: 416 | `, 417 | wantRetCode: codes.OK, 418 | wantConfig: `{ 419 | "system": { 420 | "clock": { 421 | "config": { 422 | "timezone-name": "Europe/Stockholm" 423 | } 424 | } 425 | } 426 | }`, 427 | }, { 428 | desc: "delete node with non-existing precedent path", 429 | initConfig: `{ 430 | "system": { 431 | "clock": { 432 | "config": { 433 | "timezone-name": "Europe/Stockholm" 434 | } 435 | } 436 | } 437 | }`, 438 | op: pb.UpdateResult_DELETE, 439 | textPbPath: ` 440 | elem: 441 | elem: 442 | elem: 443 | elem: 444 | `, 445 | wantRetCode: codes.OK, 446 | wantConfig: `{ 447 | "system": { 448 | "clock": { 449 | "config": { 450 | "timezone-name": "Europe/Stockholm" 451 | } 452 | } 453 | } 454 | }`, 455 | }, { 456 | desc: "delete node with non-existing attribute in precedent path", 457 | initConfig: `{ 458 | "system": { 459 | "clock": { 460 | "config": { 461 | "timezone-name": "Europe/Stockholm" 462 | } 463 | } 464 | } 465 | }`, 466 | op: pb.UpdateResult_DELETE, 467 | textPbPath: ` 468 | elem: 469 | elem: 470 | elem: < 471 | name: "config" 472 | key: 473 | > 474 | elem: `, 475 | wantRetCode: codes.OK, 476 | wantConfig: `{ 477 | "system": { 478 | "clock": { 479 | "config": { 480 | "timezone-name": "Europe/Stockholm" 481 | } 482 | } 483 | } 484 | }`, 485 | }, { 486 | desc: "delete node with non-existing attribute", 487 | initConfig: `{ 488 | "system": { 489 | "clock": { 490 | "config": { 491 | "timezone-name": "Europe/Stockholm" 492 | } 493 | } 494 | } 495 | }`, 496 | op: pb.UpdateResult_DELETE, 497 | textPbPath: ` 498 | elem: 499 | elem: 500 | elem: 501 | elem: < 502 | name: "timezone-name" 503 | key: 504 | > 505 | elem: `, 506 | wantRetCode: codes.OK, 507 | wantConfig: `{ 508 | "system": { 509 | "clock": { 510 | "config": { 511 | "timezone-name": "Europe/Stockholm" 512 | } 513 | } 514 | } 515 | }`, 516 | }, { 517 | desc: "delete leaf node with attribute in its precedent path", 518 | initConfig: `{ 519 | "components": { 520 | "component": [ 521 | { 522 | "name": "swpri1-1-1", 523 | "config": { 524 | "name": "swpri1-1-1" 525 | }, 526 | "state": { 527 | "name": "swpri1-1-1", 528 | "mfg-name": "foo bar inc." 529 | } 530 | } 531 | ] 532 | } 533 | }`, 534 | op: pb.UpdateResult_DELETE, 535 | textPbPath: ` 536 | elem: 537 | elem: < 538 | name: "component" 539 | key: 540 | > 541 | elem: 542 | elem: `, 543 | wantRetCode: codes.OK, 544 | wantConfig: `{ 545 | "components": { 546 | "component": [ 547 | { 548 | "name": "swpri1-1-1", 549 | "config": { 550 | "name": "swpri1-1-1" 551 | }, 552 | "state": { 553 | "name": "swpri1-1-1" 554 | } 555 | } 556 | ] 557 | } 558 | }`, 559 | }, { 560 | desc: "delete sub-tree with attribute in its precedent path", 561 | initConfig: `{ 562 | "components": { 563 | "component": [ 564 | { 565 | "name": "swpri1-1-1", 566 | "config": { 567 | "name": "swpri1-1-1" 568 | }, 569 | "state": { 570 | "name": "swpri1-1-1", 571 | "mfg-name": "foo bar inc." 572 | } 573 | } 574 | ] 575 | } 576 | }`, 577 | op: pb.UpdateResult_DELETE, 578 | textPbPath: ` 579 | elem: 580 | elem: < 581 | name: "component" 582 | key: 583 | > 584 | elem: `, 585 | wantRetCode: codes.OK, 586 | wantConfig: `{ 587 | "components": { 588 | "component": [ 589 | { 590 | "name": "swpri1-1-1", 591 | "config": { 592 | "name": "swpri1-1-1" 593 | } 594 | } 595 | ] 596 | } 597 | }`, 598 | }, { 599 | desc: "delete path node with attribute", 600 | initConfig: `{ 601 | "components": { 602 | "component": [ 603 | { 604 | "name": "swpri1-1-1", 605 | "config": { 606 | "name": "swpri1-1-1" 607 | } 608 | }, 609 | { 610 | "name": "swpri1-1-2", 611 | "config": { 612 | "name": "swpri1-1-2" 613 | } 614 | } 615 | ] 616 | } 617 | }`, 618 | op: pb.UpdateResult_DELETE, 619 | textPbPath: ` 620 | elem: 621 | elem: < 622 | name: "component" 623 | key: 624 | >`, 625 | wantRetCode: codes.OK, 626 | wantConfig: `{ 627 | "components": { 628 | "component": [ 629 | { 630 | "name": "swpri1-1-2", 631 | "config": { 632 | "name": "swpri1-1-2" 633 | } 634 | } 635 | ] 636 | } 637 | }`, 638 | }, { 639 | desc: "delete path node with int type attribute", 640 | initConfig: `{ 641 | "system": { 642 | "openflow": { 643 | "controllers": { 644 | "controller": [ 645 | { 646 | "config": { 647 | "name": "main" 648 | }, 649 | "connections": { 650 | "connection": [ 651 | { 652 | "aux-id": 0, 653 | "config": { 654 | "address": "192.0.2.10", 655 | "aux-id": 0 656 | } 657 | } 658 | ] 659 | }, 660 | "name": "main" 661 | } 662 | ] 663 | } 664 | } 665 | } 666 | }`, 667 | op: pb.UpdateResult_DELETE, 668 | textPbPath: ` 669 | elem: 670 | elem: 671 | elem: 672 | elem: < 673 | name: "controller" 674 | key: 675 | > 676 | elem: 677 | elem: < 678 | name: "connection" 679 | key: 680 | > 681 | `, 682 | wantRetCode: codes.OK, 683 | wantConfig: `{ 684 | "system": { 685 | "openflow": { 686 | "controllers": { 687 | "controller": [ 688 | { 689 | "config": { 690 | "name": "main" 691 | }, 692 | "name": "main" 693 | } 694 | ] 695 | } 696 | } 697 | } 698 | }`, 699 | }, { 700 | desc: "delete leaf node with non-existing attribute value", 701 | initConfig: `{ 702 | "components": { 703 | "component": [ 704 | { 705 | "name": "swpri1-1-1", 706 | "config": { 707 | "name": "swpri1-1-1" 708 | } 709 | } 710 | ] 711 | } 712 | }`, 713 | op: pb.UpdateResult_DELETE, 714 | textPbPath: ` 715 | elem: 716 | elem: < 717 | name: "component" 718 | key: 719 | >`, 720 | wantRetCode: codes.OK, 721 | wantConfig: `{ 722 | "components": { 723 | "component": [ 724 | { 725 | "name": "swpri1-1-1", 726 | "config": { 727 | "name": "swpri1-1-1" 728 | } 729 | } 730 | ] 731 | } 732 | }`, 733 | }, { 734 | desc: "delete leaf node with non-existing attribute value in precedent path", 735 | initConfig: `{ 736 | "components": { 737 | "component": [ 738 | { 739 | "name": "swpri1-1-1", 740 | "config": { 741 | "name": "swpri1-1-1" 742 | }, 743 | "state": { 744 | "name": "swpri1-1-1", 745 | "mfg-name": "foo bar inc." 746 | } 747 | } 748 | ] 749 | } 750 | }`, 751 | op: pb.UpdateResult_DELETE, 752 | textPbPath: ` 753 | elem: 754 | elem: < 755 | name: "component" 756 | key: 757 | > 758 | elem: 759 | elem: 760 | `, 761 | wantRetCode: codes.OK, 762 | wantConfig: `{ 763 | "components": { 764 | "component": [ 765 | { 766 | "name": "swpri1-1-1", 767 | "config": { 768 | "name": "swpri1-1-1" 769 | }, 770 | "state": { 771 | "name": "swpri1-1-1", 772 | "mfg-name": "foo bar inc." 773 | } 774 | } 775 | ] 776 | } 777 | }`, 778 | }} 779 | 780 | for _, tc := range tests { 781 | t.Run(tc.desc, func(t *testing.T) { 782 | runTestSet(t, model, tc) 783 | }) 784 | } 785 | } 786 | 787 | func TestReplace(t *testing.T) { 788 | systemConfig := `{ 789 | "system": { 790 | "clock": { 791 | "config": { 792 | "timezone-name": "Europe/Stockholm" 793 | } 794 | }, 795 | "config": { 796 | "hostname": "switch_a", 797 | "login-banner": "Hello!" 798 | } 799 | } 800 | }` 801 | 802 | tests := []gnmiSetTestCase{{ 803 | desc: "replace root", 804 | initConfig: `{}`, 805 | op: pb.UpdateResult_REPLACE, 806 | val: &pb.TypedValue{ 807 | Value: &pb.TypedValue_JsonIetfVal{ 808 | JsonIetfVal: []byte(systemConfig), 809 | }}, 810 | wantRetCode: codes.OK, 811 | wantConfig: systemConfig, 812 | }, { 813 | desc: "replace a subtree", 814 | initConfig: `{}`, 815 | op: pb.UpdateResult_REPLACE, 816 | textPbPath: ` 817 | elem: 818 | elem: 819 | `, 820 | val: &pb.TypedValue{ 821 | Value: &pb.TypedValue_JsonIetfVal{ 822 | JsonIetfVal: []byte(`{"config": {"timezone-name": "US/New York"}}`), 823 | }, 824 | }, 825 | wantRetCode: codes.OK, 826 | wantConfig: `{ 827 | "system": { 828 | "clock": { 829 | "config": { 830 | "timezone-name": "US/New York" 831 | } 832 | } 833 | } 834 | }`, 835 | }, { 836 | desc: "replace a keyed list subtree", 837 | initConfig: `{}`, 838 | op: pb.UpdateResult_REPLACE, 839 | textPbPath: ` 840 | elem: 841 | elem: < 842 | name: "component" 843 | key: 844 | >`, 845 | val: &pb.TypedValue{ 846 | Value: &pb.TypedValue_JsonIetfVal{ 847 | JsonIetfVal: []byte(`{"config": {"name": "swpri1-1-1"}}`), 848 | }, 849 | }, 850 | wantRetCode: codes.OK, 851 | wantConfig: `{ 852 | "components": { 853 | "component": [ 854 | { 855 | "name": "swpri1-1-1", 856 | "config": { 857 | "name": "swpri1-1-1" 858 | } 859 | } 860 | ] 861 | } 862 | }`, 863 | }, { 864 | desc: "replace node with int type attribute in its precedent path", 865 | initConfig: `{ 866 | "system": { 867 | "openflow": { 868 | "controllers": { 869 | "controller": [ 870 | { 871 | "config": { 872 | "name": "main" 873 | }, 874 | "name": "main" 875 | } 876 | ] 877 | } 878 | } 879 | } 880 | }`, 881 | op: pb.UpdateResult_REPLACE, 882 | textPbPath: ` 883 | elem: 884 | elem: 885 | elem: 886 | elem: < 887 | name: "controller" 888 | key: 889 | > 890 | elem: 891 | elem: < 892 | name: "connection" 893 | key: 894 | > 895 | elem: 896 | `, 897 | val: &pb.TypedValue{ 898 | Value: &pb.TypedValue_JsonIetfVal{ 899 | JsonIetfVal: []byte(`{"address": "192.0.2.10", "aux-id": 0}`), 900 | }, 901 | }, 902 | wantRetCode: codes.OK, 903 | wantConfig: `{ 904 | "system": { 905 | "openflow": { 906 | "controllers": { 907 | "controller": [ 908 | { 909 | "config": { 910 | "name": "main" 911 | }, 912 | "connections": { 913 | "connection": [ 914 | { 915 | "aux-id": 0, 916 | "config": { 917 | "address": "192.0.2.10", 918 | "aux-id": 0 919 | } 920 | } 921 | ] 922 | }, 923 | "name": "main" 924 | } 925 | ] 926 | } 927 | } 928 | } 929 | }`, 930 | }, { 931 | desc: "replace a leaf node of int type", 932 | initConfig: `{}`, 933 | op: pb.UpdateResult_REPLACE, 934 | textPbPath: ` 935 | elem: 936 | elem: 937 | elem: 938 | elem: 939 | elem: 940 | `, 941 | val: &pb.TypedValue{ 942 | Value: &pb.TypedValue_IntVal{IntVal: 5}, 943 | }, 944 | wantRetCode: codes.OK, 945 | wantConfig: `{ 946 | "system": { 947 | "openflow": { 948 | "agent": { 949 | "config": { 950 | "backoff-interval": 5 951 | } 952 | } 953 | } 954 | } 955 | }`, 956 | }, { 957 | desc: "replace a leaf node of string type", 958 | initConfig: `{}`, 959 | op: pb.UpdateResult_REPLACE, 960 | textPbPath: ` 961 | elem: 962 | elem: 963 | elem: 964 | elem: 965 | elem: 966 | `, 967 | val: &pb.TypedValue{ 968 | Value: &pb.TypedValue_StringVal{StringVal: "00:16:3e:00:00:00:00:00"}, 969 | }, 970 | wantRetCode: codes.OK, 971 | wantConfig: `{ 972 | "system": { 973 | "openflow": { 974 | "agent": { 975 | "config": { 976 | "datapath-id": "00:16:3e:00:00:00:00:00" 977 | } 978 | } 979 | } 980 | } 981 | }`, 982 | }, { 983 | desc: "replace a leaf node of enum type", 984 | initConfig: `{}`, 985 | op: pb.UpdateResult_REPLACE, 986 | textPbPath: ` 987 | elem: 988 | elem: 989 | elem: 990 | elem: 991 | elem: 992 | `, 993 | val: &pb.TypedValue{ 994 | Value: &pb.TypedValue_StringVal{StringVal: "SECURE"}, 995 | }, 996 | wantRetCode: codes.OK, 997 | wantConfig: `{ 998 | "system": { 999 | "openflow": { 1000 | "agent": { 1001 | "config": { 1002 | "failure-mode": "SECURE" 1003 | } 1004 | } 1005 | } 1006 | } 1007 | }`, 1008 | }, { 1009 | desc: "replace an non-existing leaf node", 1010 | initConfig: `{}`, 1011 | op: pb.UpdateResult_REPLACE, 1012 | textPbPath: ` 1013 | elem: 1014 | elem: 1015 | elem: 1016 | elem: 1017 | elem: 1018 | `, 1019 | val: &pb.TypedValue{ 1020 | Value: &pb.TypedValue_StringVal{StringVal: "SECURE"}, 1021 | }, 1022 | wantRetCode: codes.NotFound, 1023 | wantConfig: `{}`, 1024 | }} 1025 | 1026 | for _, tc := range tests { 1027 | t.Run(tc.desc, func(t *testing.T) { 1028 | runTestSet(t, model, tc) 1029 | }) 1030 | } 1031 | } 1032 | 1033 | func TestUpdate(t *testing.T) { 1034 | tests := []gnmiSetTestCase{{ 1035 | desc: "update leaf node", 1036 | initConfig: `{ 1037 | "system": { 1038 | "config": { 1039 | "hostname": "switch_a" 1040 | } 1041 | } 1042 | }`, 1043 | op: pb.UpdateResult_UPDATE, 1044 | textPbPath: ` 1045 | elem: 1046 | elem: 1047 | elem: 1048 | `, 1049 | val: &pb.TypedValue{ 1050 | Value: &pb.TypedValue_StringVal{StringVal: "foo.bar.com"}, 1051 | }, 1052 | wantRetCode: codes.OK, 1053 | wantConfig: `{ 1054 | "system": { 1055 | "config": { 1056 | "domain-name": "foo.bar.com", 1057 | "hostname": "switch_a" 1058 | } 1059 | } 1060 | }`, 1061 | }, { 1062 | desc: "update subtree", 1063 | initConfig: `{ 1064 | "system": { 1065 | "config": { 1066 | "hostname": "switch_a" 1067 | } 1068 | } 1069 | }`, 1070 | op: pb.UpdateResult_UPDATE, 1071 | textPbPath: ` 1072 | elem: 1073 | elem: 1074 | `, 1075 | val: &pb.TypedValue{ 1076 | Value: &pb.TypedValue_JsonIetfVal{ 1077 | JsonIetfVal: []byte(`{"domain-name": "foo.bar.com", "hostname": "switch_a"}`), 1078 | }, 1079 | }, 1080 | wantRetCode: codes.OK, 1081 | wantConfig: `{ 1082 | "system": { 1083 | "config": { 1084 | "domain-name": "foo.bar.com", 1085 | "hostname": "switch_a" 1086 | } 1087 | } 1088 | }`, 1089 | }} 1090 | 1091 | for _, tc := range tests { 1092 | t.Run(tc.desc, func(t *testing.T) { 1093 | runTestSet(t, model, tc) 1094 | }) 1095 | } 1096 | } 1097 | 1098 | func runTestSet(t *testing.T, m *Model, tc gnmiSetTestCase) { 1099 | // Create a new server with empty config 1100 | s, err := NewServer(m, []byte(tc.initConfig), nil) 1101 | if err != nil { 1102 | t.Fatalf("error in creating config server: %v", err) 1103 | } 1104 | 1105 | // Send request 1106 | var pbPath pb.Path 1107 | if err := proto.UnmarshalText(tc.textPbPath, &pbPath); err != nil { 1108 | t.Fatalf("error in unmarshaling path: %v", err) 1109 | } 1110 | var req *pb.SetRequest 1111 | switch tc.op { 1112 | case pb.UpdateResult_DELETE: 1113 | req = &pb.SetRequest{Delete: []*pb.Path{&pbPath}} 1114 | case pb.UpdateResult_REPLACE: 1115 | req = &pb.SetRequest{Replace: []*pb.Update{{Path: &pbPath, Val: tc.val}}} 1116 | case pb.UpdateResult_UPDATE: 1117 | req = &pb.SetRequest{Update: []*pb.Update{{Path: &pbPath, Val: tc.val}}} 1118 | default: 1119 | t.Fatalf("invalid op type: %v", tc.op) 1120 | } 1121 | _, err = s.Set(nil, req) 1122 | 1123 | // Check return code 1124 | gotRetStatus, ok := status.FromError(err) 1125 | if !ok { 1126 | t.Fatal("got a non-grpc error from grpc call") 1127 | } 1128 | if gotRetStatus.Code() != tc.wantRetCode { 1129 | t.Fatalf("got return code %v, want %v\nerror message: %v", gotRetStatus.Code(), tc.wantRetCode, err) 1130 | } 1131 | 1132 | // Check server config 1133 | wantConfigStruct, err := m.NewConfigStruct([]byte(tc.wantConfig)) 1134 | if err != nil { 1135 | t.Fatalf("wantConfig data cannot be loaded as a config struct: %v", err) 1136 | } 1137 | wantConfigJSON, err := ygot.ConstructIETFJSON(wantConfigStruct, &ygot.RFC7951JSONConfig{}) 1138 | if err != nil { 1139 | t.Fatalf("error in constructing IETF JSON tree from wanted config: %v", err) 1140 | } 1141 | gotConfigJSON, err := ygot.ConstructIETFJSON(s.config, &ygot.RFC7951JSONConfig{}) 1142 | if err != nil { 1143 | t.Fatalf("error in constructing IETF JSON tree from server config: %v", err) 1144 | } 1145 | if !reflect.DeepEqual(gotConfigJSON, wantConfigJSON) { 1146 | t.Fatalf("got server config %v\nwant: %v", gotConfigJSON, wantConfigJSON) 1147 | } 1148 | } 1149 | --------------------------------------------------------------------------------