Skip to content
Snippets Groups Projects
Commit 4ec6de07 authored by Manuel Kieweg's avatar Manuel Kieweg
Browse files

First draft of gnmi server fork for proto speaking targets

parent 2692f73d
No related branches found
No related tags found
3 merge requests!98Resolve "gNMI proto encoding",!91"Overhaul Architecture",!90Develop
Pipeline #63208 passed with warnings
Showing
with 12909 additions and 44 deletions
/*
Copyright 2018 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package target has utility functions for working with target configuration
// proto messages in target.proto.
package main
import (
"errors"
"fmt"
"sync"
"github.com/golang/protobuf/proto"
gpb "github.com/openconfig/gnmi/proto/gnmi"
pb "github.com/openconfig/gnmi/proto/target"
)
// Update describes a single target configuration.
type Update struct {
Name string
Request *gpb.SubscribeRequest
Target *pb.Target
}
// Handler defines callbacks to be synchronously invoked in response to
// configuration changes.
type Handler struct {
// Add handles addition of a new target.
Add func(Update)
// Update handles target modification, including subscription request changes.
Update func(Update)
// Delete handles a target being removed.
Delete func(name string)
}
// Config handles configuration file changes and contains configuration state.
type Config struct {
h Handler
mu sync.Mutex
configuration *pb.Configuration
}
// NewConfig creates a new Config that can process configuration changes.
func NewConfig(h Handler) *Config {
return &Config{
h: h,
}
}
// NewConfigWithBase creates a new Config that can process configuration
// changes. An optional configuration is used as the initial state.
func NewConfigWithBase(h Handler, config *pb.Configuration) (*Config, error) {
if config != nil {
if err := Validate(config); err != nil {
return nil, fmt.Errorf("invalid configuration: %v", err)
}
}
return &Config{
configuration: config,
h: h,
}, nil
}
// Current returns a copy of the current configuration.
func (c *Config) Current() *pb.Configuration {
c.mu.Lock()
defer c.mu.Unlock()
return proto.Clone(c.configuration).(*pb.Configuration)
}
// Load updates the current configuration and invokes Handler callbacks for
// detected changes. An error is returned when loading fails, or the new revision
// is not strictly increasing.
func (c *Config) Load(config *pb.Configuration) error {
if config == nil {
return fmt.Errorf("attempted to load nil configuration")
}
if err := Validate(config); err != nil {
return fmt.Errorf("invalid configuration: %v", err)
}
c.mu.Lock()
defer c.mu.Unlock()
if err := c.checkRevision(config); err != nil {
return err
}
// Diff before setting new state.
c.handleDiffs(config)
c.configuration = config
return nil
}
func (c *Config) checkRevision(cf *pb.Configuration) error {
switch {
case c.configuration == nil:
return nil
case cf.Revision <= c.configuration.GetRevision():
return fmt.Errorf("revision %v is not strictly greater than %v", cf.Revision, c.configuration.GetRevision())
}
return nil
}
// handleDiffs should be called while locking c. It performs a read-only diff on
// state in c against the new configuration.
func (c *Config) handleDiffs(config *pb.Configuration) {
requestChanged := map[string]bool{}
for k, new := range config.Request {
if old, ok := c.configuration.GetRequest()[k]; ok {
if !proto.Equal(old, new) {
requestChanged[k] = true
}
}
}
// Make a copy of new targets so we can safely modify the map.
newTargets := make(map[string]*pb.Target)
for k, t := range config.GetTarget() {
newTargets[k] = t
}
for k, t := range c.configuration.GetTarget() {
nt := newTargets[k]
switch {
case nt == nil:
if c.h.Delete != nil {
c.h.Delete(k)
}
case !requestChanged[t.GetRequest()] && proto.Equal(t, nt):
delete(newTargets, k)
default:
if c.h.Update != nil {
r := config.GetRequest()[nt.GetRequest()]
c.h.Update(Update{
Name: k,
Request: r,
Target: nt,
})
}
delete(newTargets, k)
}
}
// Anything left in newTargets must be a new target.
for k, t := range newTargets {
r := config.GetRequest()[t.GetRequest()]
if c.h.Add != nil {
c.h.Add(Update{
Name: k,
Request: r,
Target: t,
})
}
}
}
// Validate confirms that the configuration is valid.
func Validate(config *pb.Configuration) error {
for name, target := range config.Target {
if name == "" {
return errors.New("target with empty name")
}
if target == nil {
return fmt.Errorf("missing target configuration for %q", name)
}
if len(target.Addresses) == 0 {
return fmt.Errorf("target %q missing address", name)
}
if target.Request == "" {
return fmt.Errorf("target %q missing request", name)
}
if _, ok := config.Request[target.Request]; !ok {
return fmt.Errorf("missing request %q for target %q", target.Request, name)
}
}
return nil
}
package main
import (
"code.fbi.h-da.de/cocsn/gosdn/forks/google/gnmi"
"code.fbi.h-da.de/cocsn/gosdn/forks/google/gnmi/modeldata"
oc "code.fbi.h-da.de/cocsn/yang-models/generated/arista"
"context"
"flag"
"fmt"
"github.com/google/gnxi/utils/credentials"
pb "github.com/openconfig/gnmi/proto/gnmi"
"github.com/openconfig/ygot/ygot"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"io/ioutil"
"net"
"os"
"reflect"
)
var (
bindAddr = flag.String("bind_address", ":9339", "Bind to address:port or just :port")
configFile = flag.String("config", "", "IETF JSON file for target startup config")
)
type server struct {
*gnmi.Server
}
func callback(newConfig ygot.ValidatedGoStruct) error {
// Apply the config to your device and return nil if success. return error if fails.
//
// Do something ...
return nil
}
func newServer(model *gnmi.Model, config []byte) (*server, error) {
s, err := gnmi.NewServer(model, config, callback)
if err != nil {
return nil, err
}
return &server{Server: s}, nil
}
// Get overrides the Get func of gnmi.Target to provide user auth.
func (s *server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
msg, ok := credentials.AuthorizeUser(ctx)
if !ok {
log.Infof("denied a Get request: %v", msg)
return nil, status.Error(codes.PermissionDenied, msg)
}
log.Infof("allowed a Get request: %v", msg)
return s.Server.Get(ctx, req)
}
// Set overrides the Set func of gnmi.Target to provide user auth.
/*
func (s *server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, error) {
msg, ok := credentials.AuthorizeUser(ctx)
if !ok {
log.Infof("denied a Set request: %v", msg)
return nil, status.Error(codes.PermissionDenied, msg)
}
log.Infof("allowed a Set request: %v", msg)
return s.Server.Set(ctx, req)
}
*/
func main() {
// Google stuff from here
model := gnmi.NewModel(modeldata.ModelData,
reflect.TypeOf((*oc.Device)(nil)),
oc.SchemaTree["Device"],
oc.Unmarshal,
oc.ΛEnum)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Supported models:\n")
for _, m := range model.SupportedModels() {
fmt.Fprintf(os.Stderr, " %s\n", m)
}
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Set("logtostderr", "true")
flag.Parse()
g := grpc.NewServer()
var configData []byte
if *configFile != "" {
var err error
configData, err = ioutil.ReadFile(*configFile)
if err != nil {
log.Fatalf("error in reading config file: %v", err)
}
}
s, err := newServer(model, configData)
if err != nil {
log.Fatalf("error in creating gnmi target: %v", err)
}
pb.RegisterGNMIServer(g, s)
reflection.Register(g)
log.Infof("starting to listen on %s", *bindAddr)
listen, err := net.Listen("tcp", *bindAddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Info("starting to serve")
if err := g.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
...@@ -19,7 +19,7 @@ func main() { ...@@ -19,7 +19,7 @@ func main() {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
sbi := &nucleus.AristaOC{} sbi := &nucleus.AristaOC{}
transport := &nucleus.Gnmi{ transport := &nucleus.Gnmi{
SetNode: sbi.SetNode(), SetNode: sbi.SetNode(),
RespChan: make(chan *gpb.SubscribeResponse), RespChan: make(chan *gpb.SubscribeResponse),
} }
device := nucleus.Device{ device := nucleus.Device{
...@@ -27,7 +27,7 @@ func main() { ...@@ -27,7 +27,7 @@ func main() {
SBI: sbi, SBI: sbi,
Config: nucleus.DeviceConfig{ Config: nucleus.DeviceConfig{
Uuid: uuid.New(), Uuid: uuid.New(),
Address: "[fdfd::ce05]:6030", Address: "localhost:9339",
Username: "admin", Username: "admin",
Password: "arista", Password: "arista",
}, },
...@@ -71,4 +71,4 @@ func main() { ...@@ -71,4 +71,4 @@ func main() {
fmt.Println("awaiting signal") fmt.Println("awaiting signal")
<-done <-done
fmt.Println("exiting") fmt.Println("exiting")
} }
\ No newline at end of file
File added
...@@ -3,7 +3,6 @@ package main ...@@ -3,7 +3,6 @@ package main
import ( import (
"code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi" "code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi"
"code.fbi.h-da.de/cocsn/gosdn/nucleus" "code.fbi.h-da.de/cocsn/gosdn/nucleus"
"code.fbi.h-da.de/cocsn/gosdn/nucleus/util"
schema "code.fbi.h-da.de/cocsn/yang-models/generated/arista" schema "code.fbi.h-da.de/cocsn/yang-models/generated/arista"
"context" "context"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -19,7 +18,7 @@ func main() { ...@@ -19,7 +18,7 @@ func main() {
SBI: sbi, SBI: sbi,
Config: nucleus.DeviceConfig{ Config: nucleus.DeviceConfig{
Uuid: uuid.New(), Uuid: uuid.New(),
Address: "[fdfd::ce05]:6030", Address: "localhost:9339",
Username: "admin", Username: "admin",
Password: "arista", Password: "arista",
}, },
...@@ -38,11 +37,9 @@ func main() { ...@@ -38,11 +37,9 @@ func main() {
ctx := gnmi.NewContext(context.Background(), cfg) ctx := gnmi.NewContext(context.Background(), cfg)
ctx = context.WithValue(ctx, "config", cfg) ctx = context.WithValue(ctx, "config", cfg)
paths := util.NewPaths() p := []string{"/interfaces/interface[name=en0]/state/name"}
paths.ParseSchema(sbi.Schema(), "device")
p := paths.StringBuilder()
errors := 0 errors := 0
for _,path := range p { for _, path := range p {
req, err := gnmi.NewGetRequest(gnmi.SplitPaths([]string{path}), "") req, err := gnmi.NewGetRequest(gnmi.SplitPaths([]string{path}), "")
resp, err := nucleus.GetWithRequest(ctx, req) resp, err := nucleus.GetWithRequest(ctx, req)
if err != nil { if err != nil {
......
/* Copyright 2018 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Binary implements a gNOI Target with a Certificate Management service.
package main
import (
"flag"
"net"
"strings"
"sync"
"time"
"github.com/google/gnxi/gnoi"
"github.com/google/gnxi/gnoi/cert"
"github.com/google/gnxi/gnoi/os"
"github.com/google/gnxi/gnoi/reset"
"github.com/google/gnxi/utils/credentials"
"google.golang.org/grpc"
log "github.com/golang/glog"
)
var (
gNOIServer *gnoi.Server
grpcServer *grpc.Server
muServe sync.Mutex
bootstrapping bool
certID = flag.String("cert_id", "default", "Certificate ID for preloaded certificates")
//bindAddr = flag.String("bind_address", ":9339", "Bind to address:port or just :port")
resetDelay = flag.Duration("reset_delay", 3*time.Second, "Delay before resetting the service upon factory reset request, 3 seconds by default")
zeroFillUnsupported = flag.Bool("zero_fill_unsupported", false, "Make the target not support zero filling storage")
factoryOSUnsupported = flag.Bool("reset_unsupported", false, "Make the target not support factory resetting OS")
factoryVersion = flag.String("factoryOS_version", "1.0.0a", "Specify factory OS version, 1.0.0a by default")
installedVersions = flag.String("installedOS_versions", "", "Specify installed OS versions, e.g \"1.0.1a 2.01b\"")
receiveChunkSizeAck = flag.Uint64("chunk_size_ack", 12000000, "The chunk size of the image to respond with a TransfreResponse in bytes. Example: -chunk_size 12000000")
)
// serve binds to an address and starts serving a gRPCServer.
func serve() {
muServe.Lock()
defer muServe.Unlock()
listen, err := net.Listen("tcp", *bindAddr)
if err != nil {
log.Fatal("Failed to listen:", err)
}
defer listen.Close()
log.Info("Starting gNOI server.")
if err := grpcServer.Serve(listen); err != nil {
log.Fatal("Failed to serve:", err)
}
}
// notifyCerts can be called with the number of certs and ca certs installed. It will
// (re)start the gRPC server in encrypted mode if no certs are installed. It will
// (re)start in authenticated mode otherwise.
func notifyCerts(certs, caCerts int) {
hasCredentials := certs != 0 && caCerts != 0
if bootstrapping != hasCredentials {
// Nothing to do, either I am bootstrapping and I have no
// certificates or I am provisioned and I have certificates.
return
}
if bootstrapping {
log.Info("Found Credentials, setting Provisioned state.")
if grpcServer != nil {
grpcServer.GracefulStop()
}
grpcServer = gNOIServer.PrepareAuthenticated()
// Register all gNOI services.
gNOIServer.Register(grpcServer)
} else {
log.Info("No credentials, setting Bootstrapping state.")
if grpcServer != nil {
grpcServer.GracefulStop()
}
grpcServer = gNOIServer.PrepareEncrypted()
// Only register the gNOI Cert service for bootstrapping.
gNOIServer.RegCertificateManagement(grpcServer)
}
bootstrapping = !bootstrapping
go serve()
}
// start creates the new gNOI server.
func start() {
resetSettings := &reset.Settings{
ZeroFillUnsupported: *zeroFillUnsupported,
FactoryOSUnsupported: *factoryOSUnsupported,
}
osSettings := &os.Settings{
FactoryVersion: *factoryVersion,
InstalledVersions: strings.Split(*installedVersions, " "),
ReceiveChunkSizeAck: *receiveChunkSizeAck,
}
var (
numCerts,
numCA int
certSettings = &cert.Settings{}
)
certSettings.CertID = *certID
credentials.SetTargetName("target.com")
certSettings.Cert, certSettings.CA = credentials.ParseCertificates()
if certSettings.Cert != nil && certSettings.CA != nil {
numCerts, numCA = 1, 1
}
var err error
if gNOIServer, err = gnoi.NewServer(certSettings, resetSettings, notifyReset, osSettings); err != nil {
log.Fatal("Failed to create gNOI Server:", err)
}
// Registers a caller for whenever the number of installed certificates changes.
gNOIServer.RegisterCertNotifier(notifyCerts)
bootstrapping = numCerts != 0 && numCA != 0
notifyCerts(numCerts, numCA) // Triggers bootstraping mode.
}
// notifyReset is called when the factory reset service requires the server to be restarted.
func notifyReset() {
log.Info("Server factory reset triggered")
<-time.After(*resetDelay)
start()
}
func main() {
flag.Set("logtostderr", "true")
flag.Parse()
start()
select {} // Loop forever.
}
/* Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Binary gnmi_target implements a gNMI Target with in-memory configuration and telemetry.
package main
import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"reflect"
log "github.com/golang/glog"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
"github.com/google/gnxi/gnmi"
"github.com/google/gnxi/gnmi/modeldata"
"github.com/google/gnxi/gnmi/modeldata/gostruct"
"github.com/google/gnxi/utils/credentials"
pb "github.com/openconfig/gnmi/proto/gnmi"
)
var (
bindAddr = flag.String("bind_address", ":9339", "Bind to address:port or just :port")
configFile = flag.String("config", "", "IETF JSON file for target startup config")
)
type server struct {
*gnmi.Server
}
func newServer(model *gnmi.Model, config []byte) (*server, error) {
s, err := gnmi.NewServer(model, config, nil)
if err != nil {
return nil, err
}
return &server{Server: s}, nil
}
// Get overrides the Get func of gnmi.Target to provide user auth.
func (s *server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
msg, ok := credentials.AuthorizeUser(ctx)
if !ok {
log.Infof("denied a Get request: %v", msg)
return nil, status.Error(codes.PermissionDenied, msg)
}
log.Infof("allowed a Get request: %v", msg)
return s.Server.Get(ctx, req)
}
// Set overrides the Set func of gnmi.Target to provide user auth.
func (s *server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, error) {
msg, ok := credentials.AuthorizeUser(ctx)
if !ok {
log.Infof("denied a Set request: %v", msg)
return nil, status.Error(codes.PermissionDenied, msg)
}
log.Infof("allowed a Set request: %v", msg)
return s.Server.Set(ctx, req)
}
func main() {
model := gnmi.NewModel(modeldata.ModelData,
reflect.TypeOf((*gostruct.Device)(nil)),
gostruct.SchemaTree["Device"],
gostruct.Unmarshal,
gostruct.ΛEnum)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Supported models:\n")
for _, m := range model.SupportedModels() {
fmt.Fprintf(os.Stderr, " %s\n", m)
}
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Set("logtostderr", "true")
flag.Parse()
opts := credentials.ServerCredentials()
g := grpc.NewServer(opts...)
var configData []byte
if *configFile != "" {
var err error
configData, err = ioutil.ReadFile(*configFile)
if err != nil {
log.Exitf("error in reading config file: %v", err)
}
}
s, err := newServer(model, configData)
if err != nil {
log.Exitf("error in creating gnmi target: %v", err)
}
pb.RegisterGNMIServer(g, s)
reflection.Register(g)
log.Infof("starting to listen on %s", *bindAddr)
listen, err := net.Listen("tcp", *bindAddr)
if err != nil {
log.Exitf("failed to listen: %v", err)
}
log.Info("starting to serve")
if err := g.Serve(listen); err != nil {
log.Exitf("failed to serve: %v", err)
}
}
...@@ -252,7 +252,8 @@ func NewContext(ctx context.Context, cfg *Config) context.Context { ...@@ -252,7 +252,8 @@ func NewContext(ctx context.Context, cfg *Config) context.Context {
// NewGetRequest returns a GetRequest for the given paths // NewGetRequest returns a GetRequest for the given paths
func NewGetRequest(paths [][]string, origin string) (*pb.GetRequest, error) { func NewGetRequest(paths [][]string, origin string) (*pb.GetRequest, error) {
req := &pb.GetRequest{ req := &pb.GetRequest{
Path: make([]*pb.Path, len(paths)), Path: make([]*pb.Path, len(paths)),
Encoding: pb.Encoding_PROTO,
} }
for i, p := range paths { for i, p := range paths {
gnmiPath, err := ParseGNMIElements(p) gnmiPath, err := ParseGNMIElements(p)
......
/* Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gnmi
import (
oc "code.fbi.h-da.de/cocsn/yang-models/generated/arista"
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"net"
"reflect"
"sort"
"github.com/openconfig/goyang/pkg/yang"
"github.com/openconfig/ygot/ygot"
"github.com/openconfig/ygot/ytypes"
pb "github.com/openconfig/gnmi/proto/gnmi"
)
// JSONUnmarshaler is the signature of the Unmarshal() function in the GoStruct code generated by openconfig ygot library.
type JSONUnmarshaler func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error
// GoStructEnumData is the data type to maintain GoStruct enum type.
type GoStructEnumData map[string]map[int64]ygot.EnumDefinition
// Model contains the model data and GoStruct information for the device to config.
type Model struct {
modelData []*pb.ModelData
structRootType reflect.Type
schemaTreeRoot *yang.Entry
jsonUnmarshaler JSONUnmarshaler
enumData GoStructEnumData
}
// NewModel returns an instance of Model struct.
func NewModel(m []*pb.ModelData, t reflect.Type, r *yang.Entry, f JSONUnmarshaler, e GoStructEnumData) *Model {
return &Model{
modelData: m,
structRootType: t,
schemaTreeRoot: r,
jsonUnmarshaler: f,
enumData: e,
}
}
func (m *Model) newRootValue() interface{} {
return reflect.New(m.structRootType.Elem()).Interface()
}
// NewConfigStruct creates a ValidatedGoStruct of this model from jsonConfig. If jsonConfig is nil, creates an empty GoStruct.
func (m *Model) NewConfigStruct(jsonConfig []byte) (ygot.ValidatedGoStruct, error) {
rootStruct, ok := m.newRootValue().(ygot.ValidatedGoStruct)
if !ok {
return nil, errors.New("root node is not a ygot.ValidatedGoStruct")
}
ifaces, err := getInterfaces()
if err != nil {
return nil, err
}
device, ok := rootStruct.(*oc.Device)
if !ok {
return nil, errors.New("root node is not a oc.Device")
}
device.Interfaces = ifaces
return device, nil
}
// SupportedModels returns a list of supported models.
func (m *Model) SupportedModels() []string {
mDesc := make([]string, len(m.modelData))
for i, m := range m.modelData {
mDesc[i] = fmt.Sprintf("%s %s", m.Name, m.Version)
}
sort.Strings(mDesc)
return mDesc
}
func getInterfaces() (*oc.OpenconfigInterfaces_Interfaces, error) {
ifaces, err := net.Interfaces()
if err != nil {
log.Fatal()
}
interfaces := &oc.OpenconfigInterfaces_Interfaces{
Interface: make(map[string]*oc.OpenconfigInterfaces_Interfaces_Interface),
}
for _, tInterface := range ifaces {
var mtu *uint16
var name *string
var index *uint32
rmtu := uint16(tInterface.MTU)
rname := tInterface.Name
rindex := uint32(tInterface.Index)
mtu = &rmtu
name = &rname
index = &rindex
iface, err := interfaces.NewInterface(tInterface.Name)
if err != nil {
return nil, err
}
iface.State = &oc.OpenconfigInterfaces_Interfaces_Interface_State{
Ifindex: &rindex,
Mtu: &rmtu,
Name: &rname,
}
iface.State.Name = name
iface.State.Mtu = mtu
iface.State.Ifindex = index
}
return interfaces, nil
}
package gostruct
//go:generate sh -c "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/google/gnxi/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"
This diff is collapsed.
/* Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package modeldata contains the following model data in gnmi proto struct:
// openconfig-interfaces 2.0.0,
// openconfig-openflow 0.1.0,
// openconfig-platform 0.5.0,
// openconfig-system 0.2.0.
package modeldata
import (
pb "github.com/openconfig/gnmi/proto/gnmi"
)
const (
// OpenconfigInterfacesModel is the openconfig YANG model for interfaces.
OpenconfigInterfacesModel = "openconfig-interfaces"
// OpenconfigOpenflowModel is the openconfig YANG model for openflow.
OpenconfigOpenflowModel = "openconfig-openflow"
// OpenconfigPlatformModel is the openconfig YANG model for platform.
OpenconfigPlatformModel = "openconfig-platform"
// OpenconfigSystemModel is the openconfig YANG model for system.
OpenconfigSystemModel = "openconfig-system"
)
var (
// ModelData is a list of supported models.
ModelData = []*pb.ModelData{{
Name: OpenconfigInterfacesModel,
Organization: "OpenConfig working group",
Version: "2.0.0",
}, {
Name: OpenconfigOpenflowModel,
Organization: "OpenConfig working group",
Version: "0.1.0",
}, {
Name: OpenconfigPlatformModel,
Organization: "OpenConfig working group",
Version: "0.5.0",
}, {
Name: OpenconfigSystemModel,
Organization: "OpenConfig working group",
Version: "0.2.0",
}}
)
/* Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package gnmi implements a gnmi server to mock a device with YANG models.
package gnmi
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"strconv"
"sync"
"time"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
log "github.com/golang/glog"
"github.com/golang/protobuf/proto"
"github.com/openconfig/gnmi/value"
"github.com/openconfig/ygot/util"
"github.com/openconfig/ygot/ygot"
"github.com/openconfig/ygot/ytypes"
dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
pb "github.com/openconfig/gnmi/proto/gnmi"
)
// ConfigCallback is the signature of the function to apply a validated config to the physical device.
type ConfigCallback func(ygot.ValidatedGoStruct) error
var (
pbRootPath = &pb.Path{}
supportedEncodings = []pb.Encoding{pb.Encoding_PROTO}
)
// Server struct maintains the data structure for device config and implements the interface of gnmi server.
// It supports Capabilities, Get, and Set APIs.
// Typical usage:
// g := grpc.NewServer()
// s, err := Server.NewServer(model, config, callback)
// pb.NewServer(g, s)
// reflection.Register(g)
// listen, err := net.Listen("tcp", ":8080")
// g.Serve(listen)
//
// For a real device, apply the config changes to the hardware in the callback function.
// Arguments:
// newConfig: new root config to be applied on the device.
type Server struct {
model *Model
callback ConfigCallback
config ygot.ValidatedGoStruct
mu sync.RWMutex // mu is the RW lock to protect the access to config
}
// NewServer creates an instance of Server with given json config.
func NewServer(model *Model, config []byte, callback ConfigCallback) (*Server, error) {
rootStruct, err := model.NewConfigStruct(config)
if err != nil {
return nil, err
}
s := &Server{
model: model,
config: rootStruct,
callback: callback,
}
if config != nil && s.callback != nil {
if err := s.callback(rootStruct); err != nil {
return nil, err
}
}
return s, nil
}
// checkEncodingAndModel checks whether encoding and models are supported by the server. Return error if anything is unsupported.
func (s *Server) checkEncodingAndModel(encoding pb.Encoding, models []*pb.ModelData) error {
hasSupportedEncoding := false
for _, supportedEncoding := range supportedEncodings {
if encoding == supportedEncoding {
hasSupportedEncoding = true
break
}
}
if !hasSupportedEncoding {
return fmt.Errorf("unsupported encoding: %s", pb.Encoding_name[int32(encoding)])
}
for _, m := range models {
isSupported := false
for _, supportedModel := range s.model.modelData {
if reflect.DeepEqual(m, supportedModel) {
isSupported = true
break
}
}
if !isSupported {
return fmt.Errorf("unsupported model: %v", m)
}
}
return nil
}
// doDelete deletes the path from the json tree if the path exists. If success,
// it calls the callback function to apply the change to the device hardware.
func (s *Server) doDelete(jsonTree map[string]interface{}, prefix, path *pb.Path) (*pb.UpdateResult, error) {
// Update json tree of the device config
var curNode interface{} = jsonTree
pathDeleted := false
fullPath := gnmiFullPath(prefix, path)
schema := s.model.schemaTreeRoot
for i, elem := range fullPath.Elem { // Delete sub-tree or leaf node.
node, ok := curNode.(map[string]interface{})
if !ok {
break
}
// Delete node
if i == len(fullPath.Elem)-1 {
if elem.GetKey() == nil {
delete(node, elem.Name)
pathDeleted = true
break
}
pathDeleted = deleteKeyedListEntry(node, elem)
break
}
if curNode, schema = getChildNode(node, schema, elem, false); curNode == nil {
break
}
}
if reflect.DeepEqual(fullPath, pbRootPath) { // Delete root
for k := range jsonTree {
delete(jsonTree, k)
}
}
// Apply the validated operation to the config tree and device.
if pathDeleted {
newConfig, err := s.toGoStruct(jsonTree)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
if s.callback != nil {
if applyErr := s.callback(newConfig); applyErr != nil {
if rollbackErr := s.callback(s.config); rollbackErr != nil {
return nil, status.Errorf(codes.Internal, "error in rollback the failed operation (%v): %v", applyErr, rollbackErr)
}
return nil, status.Errorf(codes.Aborted, "error in applying operation to device: %v", applyErr)
}
}
}
return &pb.UpdateResult{
Path: path,
Op: pb.UpdateResult_DELETE,
}, nil
}
// doReplaceOrUpdate validates the replace or update operation to be applied to
// the device, modifies the json tree of the config struct, then calls the
// callback function to apply the operation to the device hardware.
func (s *Server) doReplaceOrUpdate(jsonTree map[string]interface{}, op pb.UpdateResult_Operation, prefix, path *pb.Path, val *pb.TypedValue) (*pb.UpdateResult, error) {
// Validate the operation.
fullPath := gnmiFullPath(prefix, path)
emptyNode, _, err := ytypes.GetOrCreateNode(s.model.schemaTreeRoot, s.model.newRootValue(), fullPath)
if err != nil {
return nil, status.Errorf(codes.NotFound, "path %v is not found in the config structure: %v", fullPath, err)
}
var nodeVal interface{}
nodeStruct, ok := emptyNode.(ygot.ValidatedGoStruct)
if ok {
if err := s.model.jsonUnmarshaler(val.GetJsonIetfVal(), nodeStruct); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "unmarshaling json data to config struct fails: %v", err)
}
if err := nodeStruct.Validate(); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "config data validation fails: %v", err)
}
var err error
if nodeVal, err = ygot.ConstructIETFJSON(nodeStruct, &ygot.RFC7951JSONConfig{}); err != nil {
msg := fmt.Sprintf("error in constructing IETF JSON tree from config struct: %v", err)
log.Error(msg)
return nil, status.Error(codes.Internal, msg)
}
} else {
var err error
if nodeVal, err = value.ToScalar(val); err != nil {
return nil, status.Errorf(codes.Internal, "cannot convert leaf node to scalar type: %v", err)
}
}
// Update json tree of the device config.
var curNode interface{} = jsonTree
schema := s.model.schemaTreeRoot
for i, elem := range fullPath.Elem {
switch node := curNode.(type) {
case map[string]interface{}:
// Set node value.
if i == len(fullPath.Elem)-1 {
if elem.GetKey() == nil {
if grpcStatusError := setPathWithoutAttribute(op, node, elem, nodeVal); grpcStatusError != nil {
return nil, grpcStatusError
}
break
}
if grpcStatusError := setPathWithAttribute(op, node, elem, nodeVal); grpcStatusError != nil {
return nil, grpcStatusError
}
break
}
if curNode, schema = getChildNode(node, schema, elem, true); curNode == nil {
return nil, status.Errorf(codes.NotFound, "path elem not found: %v", elem)
}
case []interface{}:
return nil, status.Errorf(codes.NotFound, "incompatible path elem: %v", elem)
default:
return nil, status.Errorf(codes.Internal, "wrong node type: %T", curNode)
}
}
if reflect.DeepEqual(fullPath, pbRootPath) { // Replace/Update root.
if op == pb.UpdateResult_UPDATE {
return nil, status.Error(codes.Unimplemented, "update the root of config tree is unsupported")
}
nodeValAsTree, ok := nodeVal.(map[string]interface{})
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "expect a tree to replace the root, got a scalar value: %T", nodeVal)
}
for k := range jsonTree {
delete(jsonTree, k)
}
for k, v := range nodeValAsTree {
jsonTree[k] = v
}
}
newConfig, err := s.toGoStruct(jsonTree)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// Apply the validated operation to the device.
if s.callback != nil {
if applyErr := s.callback(newConfig); applyErr != nil {
if rollbackErr := s.callback(s.config); rollbackErr != nil {
return nil, status.Errorf(codes.Internal, "error in rollback the failed operation (%v): %v", applyErr, rollbackErr)
}
return nil, status.Errorf(codes.Aborted, "error in applying operation to device: %v", applyErr)
}
}
return &pb.UpdateResult{
Path: path,
Op: op,
}, nil
}
func (s *Server) toGoStruct(jsonTree map[string]interface{}) (ygot.ValidatedGoStruct, error) {
jsonDump, err := json.Marshal(jsonTree)
if err != nil {
return nil, fmt.Errorf("error in marshaling IETF JSON tree to bytes: %v", err)
}
goStruct, err := s.model.NewConfigStruct(jsonDump)
if err != nil {
return nil, fmt.Errorf("error in creating config struct from IETF JSON data: %v", err)
}
return goStruct, nil
}
// getGNMIServiceVersion returns a pointer to the gNMI service version string.
// The method is non-trivial because of the way it is defined in the proto file.
func getGNMIServiceVersion() (*string, error) {
gzB, _ := (&pb.Update{}).Descriptor()
r, err := gzip.NewReader(bytes.NewReader(gzB))
if err != nil {
return nil, fmt.Errorf("error in initializing gzip reader: %v", err)
}
defer r.Close()
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("error in reading gzip data: %v", err)
}
desc := &dpb.FileDescriptorProto{}
if err := proto.Unmarshal(b, desc); err != nil {
return nil, fmt.Errorf("error in unmarshaling proto: %v", err)
}
ver, err := proto.GetExtension(desc.Options, pb.E_GnmiService)
if err != nil {
return nil, fmt.Errorf("error in getting version from proto extension: %v", err)
}
return ver.(*string), nil
}
// deleteKeyedListEntry deletes the keyed list entry from node that matches the
// path elem. If the entry is the only one in keyed list, deletes the entire
// list. If the entry is found and deleted, the function returns true. If it is
// not found, the function returns false.
func deleteKeyedListEntry(node map[string]interface{}, elem *pb.PathElem) bool {
curNode, ok := node[elem.Name]
if !ok {
return false
}
keyedList, ok := curNode.([]interface{})
if !ok {
return false
}
for i, n := range keyedList {
m, ok := n.(map[string]interface{})
if !ok {
log.Errorf("expect map[string]interface{} for a keyed list entry, got %T", n)
return false
}
keyMatching := true
for k, v := range elem.Key {
attrVal, ok := m[k]
if !ok {
return false
}
if v != fmt.Sprintf("%v", attrVal) {
keyMatching = false
break
}
}
if keyMatching {
listLen := len(keyedList)
if listLen == 1 {
delete(node, elem.Name)
return true
}
keyedList[i] = keyedList[listLen-1]
node[elem.Name] = keyedList[0 : listLen-1]
return true
}
}
return false
}
// gnmiFullPath builds the full path from the prefix and path.
func gnmiFullPath(prefix, path *pb.Path) *pb.Path {
fullPath := &pb.Path{Origin: path.Origin}
if path.GetElement() != nil {
fullPath.Element = append(prefix.GetElement(), path.GetElement()...)
}
if path.GetElem() != nil {
fullPath.Elem = append(prefix.GetElem(), path.GetElem()...)
}
return fullPath
}
// isNIl checks if an interface is nil or its value is nil.
func isNil(i interface{}) bool {
if i == nil {
return true
}
switch kind := reflect.ValueOf(i).Kind(); kind {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return reflect.ValueOf(i).IsNil()
default:
return false
}
}
// setPathWithAttribute replaces or updates a child node of curNode in the IETF
// JSON config tree, where the child node is indexed by pathElem with attribute.
// The function returns grpc status error if unsuccessful.
func setPathWithAttribute(op pb.UpdateResult_Operation, curNode map[string]interface{}, pathElem *pb.PathElem, nodeVal interface{}) error {
nodeValAsTree, ok := nodeVal.(map[string]interface{})
if !ok {
return status.Errorf(codes.InvalidArgument, "expect nodeVal is a json node of map[string]interface{}, received %T", nodeVal)
}
m := getKeyedListEntry(curNode, pathElem, true)
if m == nil {
return status.Errorf(codes.NotFound, "path elem not found: %v", pathElem)
}
if op == pb.UpdateResult_REPLACE {
for k := range m {
delete(m, k)
}
}
for attrKey, attrVal := range pathElem.GetKey() {
m[attrKey] = attrVal
if asNum, err := strconv.ParseFloat(attrVal, 64); err == nil {
m[attrKey] = asNum
}
for k, v := range nodeValAsTree {
if k == attrKey && fmt.Sprintf("%v", v) != attrVal {
return status.Errorf(codes.InvalidArgument, "invalid config data: %v is a path attribute", k)
}
}
}
for k, v := range nodeValAsTree {
m[k] = v
}
return nil
}
// setPathWithoutAttribute replaces or updates a child node of curNode in the
// IETF config tree, where the child node is indexed by pathElem without
// attribute. The function returns grpc status error if unsuccessful.
func setPathWithoutAttribute(op pb.UpdateResult_Operation, curNode map[string]interface{}, pathElem *pb.PathElem, nodeVal interface{}) error {
target, hasElem := curNode[pathElem.Name]
nodeValAsTree, nodeValIsTree := nodeVal.(map[string]interface{})
if op == pb.UpdateResult_REPLACE || !hasElem || !nodeValIsTree {
curNode[pathElem.Name] = nodeVal
return nil
}
targetAsTree, ok := target.(map[string]interface{})
if !ok {
return status.Errorf(codes.Internal, "error in setting path: expect map[string]interface{} to update, got %T", target)
}
for k, v := range nodeValAsTree {
targetAsTree[k] = v
}
return nil
}
// Capabilities returns supported encodings and supported models.
func (s *Server) Capabilities(ctx context.Context, req *pb.CapabilityRequest) (*pb.CapabilityResponse, error) {
ver, err := getGNMIServiceVersion()
if err != nil {
return nil, status.Errorf(codes.Internal, "error in getting gnmi service version: %v", err)
}
return &pb.CapabilityResponse{
SupportedModels: s.model.modelData,
SupportedEncodings: supportedEncodings,
GNMIVersion: *ver,
}, nil
}
// Get implements the Get RPC in gNMI spec.
func (s *Server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
if req.GetType() != pb.GetRequest_ALL {
return nil, status.Errorf(codes.Unimplemented, "unsupported request type: %s", pb.GetRequest_DataType_name[int32(req.GetType())])
}
if err := s.checkEncodingAndModel(req.GetEncoding(), req.GetUseModels()); err != nil {
return nil, status.Error(codes.Unimplemented, err.Error())
}
prefix := req.GetPrefix()
paths := req.GetPath()
notifications := make([]*pb.Notification, 0)
s.mu.RLock()
defer s.mu.RUnlock()
for _, path := range paths {
// Get schema node for path from config struct.
fullPath := path
if prefix != nil {
fullPath = gnmiFullPath(prefix, path)
}
if fullPath.GetElem() == nil && fullPath.GetElement() != nil {
return nil, status.Error(codes.Unimplemented, "deprecated path element type is unsupported")
}
opts := []ytypes.GetNodeOpt{&ytypes.GetHandleWildcards{}, &ytypes.GetPartialKeyMatch{}}
nodes, err := ytypes.GetNode(s.model.schemaTreeRoot, s.config, fullPath, opts...)
if len(nodes) == 0 || err != nil || util.IsValueNil(nodes[0].Data) {
return nil, status.Errorf(codes.NotFound, "path %v not found: %v", fullPath, err)
}
for _, n := range nodes {
node := n.Data
ts := time.Now().UnixNano()
nodeStruct, ok := node.(ygot.GoStruct)
// Return leaf node.
if !ok {
var val *pb.TypedValue
switch kind := reflect.ValueOf(node).Kind(); kind {
case reflect.Ptr, reflect.Interface:
var err error
val, err = value.FromScalar(reflect.ValueOf(node).Elem().Interface())
if err != nil {
msg := fmt.Sprintf("leaf node %v does not contain a scalar type value: %v", path, err)
log.Error(msg)
return nil, status.Error(codes.Internal, msg)
}
case reflect.Int64:
enumMap, ok := s.model.enumData[reflect.TypeOf(node).Name()]
if !ok {
return nil, status.Error(codes.Internal, "not a GoStruct enumeration type")
}
val = &pb.TypedValue{
Value: &pb.TypedValue_StringVal{
StringVal: enumMap[reflect.ValueOf(node).Int()].Name,
},
}
default:
return nil, status.Errorf(codes.Internal, "unexpected kind of leaf node type: %v %v", node, kind)
}
update := &pb.Update{Path: path, Val: val}
notification := &pb.Notification{
Timestamp: ts,
Prefix: prefix,
Update: []*pb.Update{update},
}
notifications = append(notifications, notification)
continue
}
if req.GetUseModels() != nil {
return nil, status.Errorf(codes.Unimplemented, "filtering Get using use_models is unsupported, got: %v", req.GetUseModels())
}
nots, err := ygot.TogNMINotifications(nodeStruct, ts, ygot.GNMINotificationsConfig{
UsePathElem: false,
StringSlicePrefix: []string{"interfaces", "interface"},
})
if err != nil {
return nil, err
}
notifications = append(notifications, nots...)
}
}
return &pb.GetResponse{Notification: notifications}, nil
}
// Set implements the Set RPC in gNMI spec.
func (s *Server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
jsonTree, err := ygot.ConstructIETFJSON(s.config, &ygot.RFC7951JSONConfig{})
if err != nil {
msg := fmt.Sprintf("error in constructing IETF JSON tree from config struct: %v", err)
log.Error(msg)
return nil, status.Error(codes.Internal, msg)
}
prefix := req.GetPrefix()
var results []*pb.UpdateResult
for _, path := range req.GetDelete() {
res, grpcStatusError := s.doDelete(jsonTree, prefix, path)
if grpcStatusError != nil {
return nil, grpcStatusError
}
results = append(results, res)
}
for _, upd := range req.GetReplace() {
res, grpcStatusError := s.doReplaceOrUpdate(jsonTree, pb.UpdateResult_REPLACE, prefix, upd.GetPath(), upd.GetVal())
if grpcStatusError != nil {
return nil, grpcStatusError
}
results = append(results, res)
}
for _, upd := range req.GetUpdate() {
res, grpcStatusError := s.doReplaceOrUpdate(jsonTree, pb.UpdateResult_UPDATE, prefix, upd.GetPath(), upd.GetVal())
if grpcStatusError != nil {
return nil, grpcStatusError
}
results = append(results, res)
}
jsonDump, err := json.Marshal(jsonTree)
if err != nil {
msg := fmt.Sprintf("error in marshaling IETF JSON tree to bytes: %v", err)
log.Error(msg)
return nil, status.Error(codes.Internal, msg)
}
rootStruct, err := s.model.NewConfigStruct(jsonDump)
if err != nil {
msg := fmt.Sprintf("error in creating config struct from IETF JSON data: %v", err)
log.Error(msg)
return nil, status.Error(codes.Internal, msg)
}
s.config = rootStruct
return &pb.SetResponse{
Prefix: req.GetPrefix(),
Response: results,
}, nil
}
// Subscribe method is not implemented.
func (s *Server) Subscribe(stream pb.GNMI_SubscribeServer) error {
return status.Error(codes.Unimplemented, "Subscribe is not implemented.")
}
// InternalUpdate is an experimental feature to let the server update its
// internal states. Use it with your own risk.
func (s *Server) InternalUpdate(fp func(config ygot.ValidatedGoStruct) error) error {
s.mu.Lock()
defer s.mu.Unlock()
return fp(s.config)
}
This diff is collapsed.
package gnmi
import (
"fmt"
"strconv"
log "github.com/golang/glog"
"github.com/openconfig/goyang/pkg/yang"
pb "github.com/openconfig/gnmi/proto/gnmi"
)
// getChildNode gets a node's child with corresponding schema specified by path
// element. If not found and createIfNotExist is set as true, an empty node is
// created and returned.
func getChildNode(node map[string]interface{}, schema *yang.Entry, elem *pb.PathElem, createIfNotExist bool) (interface{}, *yang.Entry) {
var nextSchema *yang.Entry
var ok bool
if nextSchema, ok = schema.Dir[elem.Name]; !ok {
return nil, nil
}
var nextNode interface{}
if elem.GetKey() == nil {
if nextNode, ok = node[elem.Name]; !ok {
if createIfNotExist {
node[elem.Name] = make(map[string]interface{})
nextNode = node[elem.Name]
}
}
return nextNode, nextSchema
}
nextNode = getKeyedListEntry(node, elem, createIfNotExist)
return nextNode, nextSchema
}
// getKeyedListEntry finds the keyed list entry in node by the name and key of
// path elem. If entry is not found and createIfNotExist is true, an empty entry
// will be created (the list will be created if necessary).
func getKeyedListEntry(node map[string]interface{}, elem *pb.PathElem, createIfNotExist bool) map[string]interface{} {
curNode, ok := node[elem.Name]
if !ok {
if !createIfNotExist {
return nil
}
// Create a keyed list as node child and initialize an entry.
m := make(map[string]interface{})
for k, v := range elem.Key {
m[k] = v
if vAsNum, err := strconv.ParseFloat(v, 64); err == nil {
m[k] = vAsNum
}
}
node[elem.Name] = []interface{}{m}
return m
}
// Search entry in keyed list.
keyedList, ok := curNode.([]interface{})
if !ok {
return nil
}
for _, n := range keyedList {
m, ok := n.(map[string]interface{})
if !ok {
log.Errorf("wrong keyed list entry type: %T", n)
return nil
}
keyMatching := true
// must be exactly match
for k, v := range elem.Key {
attrVal, ok := m[k]
if !ok {
return nil
}
if v != fmt.Sprintf("%v", attrVal) {
keyMatching = false
break
}
}
if keyMatching {
return m
}
}
if !createIfNotExist {
return nil
}
// Create an entry in keyed list.
m := make(map[string]interface{})
for k, v := range elem.Key {
m[k] = v
if vAsNum, err := strconv.ParseFloat(v, 64); err == nil {
m[k] = vAsNum
}
}
node[elem.Name] = append(keyedList, m)
return m
}
...@@ -10,18 +10,19 @@ require ( ...@@ -10,18 +10,19 @@ require (
github.com/go-openapi/runtime v0.19.22 github.com/go-openapi/runtime v0.19.22
github.com/go-openapi/strfmt v0.19.5 github.com/go-openapi/strfmt v0.19.5
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/golang/protobuf v1.4.2 github.com/golang/protobuf v1.4.3
github.com/google/gnxi v0.0.0-20201221102247-c26672548161
github.com/google/uuid v1.1.2 github.com/google/uuid v1.1.2
github.com/neo4j/neo4j-go-driver v1.8.3 github.com/neo4j/neo4j-go-driver v1.8.3
github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802
github.com/openconfig/goyang v0.2.2 github.com/openconfig/goyang v0.2.3
github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696 github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696
github.com/openconfig/ygot v0.10.0 github.com/openconfig/ygot v0.10.0
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 github.com/rivo/tview v0.0.0-20201018122409-d551c850a743
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.7.1 github.com/spf13/viper v1.7.1
github.com/tidwall/gjson v1.6.3 github.com/tidwall/gjson v1.6.3
golang.org/x/net v0.0.0-20200904194848-62affa334b73 golang.org/x/net v0.0.0-20201216054612-986b41b23924
google.golang.org/grpc v1.29.1 google.golang.org/grpc v1.34.0
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.25.0
) )
This diff is collapsed.
...@@ -8,10 +8,11 @@ import ( ...@@ -8,10 +8,11 @@ import (
"github.com/openconfig/goyang/pkg/yang" "github.com/openconfig/goyang/pkg/yang"
"github.com/openconfig/ygot/ytypes" "github.com/openconfig/ygot/ytypes"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"strings"
) )
type Gnmi struct { type Gnmi struct {
SetNode func(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error SetNode func(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error
RespChan chan *gpb.SubscribeResponse RespChan chan *gpb.SubscribeResponse
} }
...@@ -25,32 +26,71 @@ func (g *Gnmi) GetConfig() interface{} { ...@@ -25,32 +26,71 @@ func (g *Gnmi) GetConfig() interface{} {
// interface satisfaction for now // interface satisfaction for now
// TODO: Convert to meaningfiul calls // TODO: Convert to meaningfiul calls
func (g *Gnmi)Get(ctx context.Context, params ...string) (interface{}, error){return nil, nil} func (g *Gnmi) Get(ctx context.Context, params ...string) (interface{}, error) { return nil, nil }
func (g *Gnmi)Set(ctx context.Context, params ...string) (interface{}, error){return nil, nil} func (g *Gnmi) Set(ctx context.Context, params ...string) (interface{}, error) { return nil, nil }
func (g *Gnmi)Subscribe(ctx context.Context, params ...string) error{ func (g *Gnmi) Subscribe(ctx context.Context, params ...string) error {
return g.subscribe(ctx) return g.subscribe(ctx)
} }
func (g *Gnmi)Type() string { func (g *Gnmi) Type() string {
return "gnmi" return "gnmi"
} }
func (g *Gnmi)ProcessResponse(resp interface{},root interface{}, s *ytypes.Schema) error { // ProcessResponse takes a gNMI response and serializes the contents to the root struct.
func (g *Gnmi) ProcessResponse(resp interface{}, root interface{}, s *ytypes.Schema) error {
models := s.SchemaTree models := s.SchemaTree
opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}} opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}}
r := resp.(*gpb.GetResponse) r := resp.(*gpb.GetResponse)
rn := r.Notification rn := r.Notification
for _, msg := range rn { for _, msg := range rn {
for _, val := range msg.Update { for _, update := range msg.Update {
schema := models["Device"] prefix := msg.Prefix
if err := g.SetNode(schema, root, val.Path, val.Val, opts...); err != nil { path := update.Path
return err fullPath := path
if prefix != nil {
fullPath = gnmiFullPath(prefix, path)
}
modelKey := extractModelKey(fullPath)
log.Debug(modelKey)
schema := models["device"]
if err := g.SetNode(schema, root, update.Path, update.Val, opts...); err != nil {
log.Error(err)
} }
} }
} }
return nil return nil
} }
// extractModelKey extracts the model's key from the full path. Highly model specific for now
// TODO: Remove hard coded model prefix
// TODO: Figure out why path.Elem() is empty but path.Elememt() is deprecated
func extractModelKey(path *gpb.Path) string {
var b strings.Builder
delim := "_"
b.WriteString("OpenconfigInterfaces")
for i := 0; i < len(path.Element)-1; i++ {
pathElement := path.Element[i]
b.WriteString(delim)
b.WriteString(strings.Title(pathElement))
}
return b.String()
}
// gnmiFullPath builds the full path from the prefix and path.
// Copycat from forks/google/gnmi/server.go
func gnmiFullPath(prefix, path *gpb.Path) *gpb.Path {
fullPath := &gpb.Path{Origin: path.Origin}
if path.GetElement() != nil {
fullPath.Element = append(prefix.GetElement(), path.GetElement()...)
}
if path.GetElem() != nil {
fullPath.Elem = append(prefix.GetElem(), path.GetElem()...)
}
return fullPath
}
// Capabilities calls GNMI capabilities // Capabilities calls GNMI capabilities
func (g *Gnmi) Capabilities(ctx context.Context) (interface{}, error) { func (g *Gnmi) Capabilities(ctx context.Context) (interface{}, error) {
client, err := gnmi.Dial(ctx.Value("config").(*gnmi.Config)) client, err := gnmi.Dial(ctx.Value("config").(*gnmi.Config))
...@@ -105,8 +145,8 @@ func (g *Gnmi) subscribe(ctx context.Context) error { ...@@ -105,8 +145,8 @@ func (g *Gnmi) subscribe(ctx context.Context) error {
} }
opts := ctx.Value("opts").(*gnmi.SubscribeOptions) opts := ctx.Value("opts").(*gnmi.SubscribeOptions)
go func() { go func() {
for { for {
resp := <- g.RespChan resp := <-g.RespChan
if err := gnmi.LogSubscribeResponse(resp); err != nil { if err := gnmi.LogSubscribeResponse(resp); err != nil {
log.Fatal(err) log.Fatal(err)
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment