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

Gnmi proto target mvp.

parent 8057d336
No related branches found
No related tags found
3 merge requests!98Resolve "gNMI proto encoding",!91"Overhaul Architecture",!90Develop
This commit is part of merge request !91. Comments created here will be created in the context of that merge request.
/*
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
}
......@@ -15,15 +15,13 @@ import (
"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")
bindAddr = flag.String("bind_address", ":9339", "Bind to address:port or just :port")
)
type server struct {
......@@ -94,13 +92,6 @@ func main() {
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)
......
......@@ -6,9 +6,14 @@ import (
schema "code.fbi.h-da.de/cocsn/yang-models/generated/arista"
"context"
"github.com/google/uuid"
gpb "github.com/openconfig/gnmi/proto/gnmi"
log "github.com/sirupsen/logrus"
)
/*
Simple gnmi request program. Use with cauton and leaf paths only.
*/
func main() {
log.SetLevel(log.DebugLevel)
sbi := &nucleus.AristaOC{}
......@@ -33,15 +38,15 @@ func main() {
Addr: device.Config.Address,
Password: device.Config.Password,
Username: device.Config.Username,
Encoding: gpb.Encoding_PROTO,
}
ctx := gnmi.NewContext(context.Background(), cfg)
ctx = context.WithValue(ctx, "config", cfg)
p := []string{"/interfaces/interface"}
p := []string{"/interfaces/interface[name=en0]/state/name"}
errors := 0
for _, path := range p {
req, err := gnmi.NewGetRequest(gnmi.SplitPaths([]string{path}), "")
resp, err := nucleus.GetWithRequest(ctx, req)
resp, err := device.Transport.Get(ctx, path)
if err != nil {
log.Debug(err)
errors++
......
......@@ -64,6 +64,7 @@ type Config struct {
Compression string
DialOptions []grpc.DialOption
Token string
Encoding pb.Encoding
}
// SubscribeOptions is the gNMI subscription request options
......@@ -250,10 +251,10 @@ func NewContext(ctx context.Context, cfg *Config) context.Context {
}
// NewGetRequest returns a GetRequest for the given paths
func NewGetRequest(paths [][]string, origin string) (*pb.GetRequest, error) {
func NewGetRequest(ctx context.Context, paths [][]string, origin string) (*pb.GetRequest, error) {
req := &pb.GetRequest{
Path: make([]*pb.Path, len(paths)),
Encoding: pb.Encoding_PROTO,
Encoding: ctx.Value("Config").(*Config).Encoding,
}
for i, p := range paths {
gnmiPath, err := ParseGNMIElements(p)
......
......@@ -285,8 +285,8 @@ func (s *Server) toGoStruct(jsonTree map[string]interface{}) (ygot.ValidatedGoSt
// 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))
gzB := (&pb.Update{}).ProtoReflect().Descriptor()
r, err := gzip.NewReader(bytes.NewReader([]byte(gzB.Name())))
if err != nil {
return nil, fmt.Errorf("error in initializing gzip reader: %v", err)
}
......@@ -354,9 +354,6 @@ func deleteKeyedListEntry(node map[string]interface{}, elem *pb.PathElem) bool {
// 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()...)
}
......
......@@ -3,6 +3,7 @@ package nucleus
import (
"code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi"
"context"
"errors"
gpb "github.com/openconfig/gnmi/proto/gnmi"
"github.com/openconfig/gnmi/proto/gnmi_ext"
"github.com/openconfig/goyang/pkg/yang"
......@@ -26,7 +27,10 @@ func (g *Gnmi) GetConfig() interface{} {
// interface satisfaction for now
// 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) {
paths := gnmi.SplitPaths(params)
return g.get(ctx, paths, "")
}
func (g *Gnmi) Set(ctx context.Context, params ...string) (interface{}, error) { return nil, nil }
func (g *Gnmi) Subscribe(ctx context.Context, params ...string) error {
return g.subscribe(ctx)
......@@ -48,13 +52,17 @@ func (g *Gnmi) ProcessResponse(resp interface{}, root interface{}, s *ytypes.Sch
path := update.Path
fullPath := path
if prefix != nil {
fullPath = gnmiFullPath(prefix, path)
var err error
fullPath, err = gnmiFullPath(prefix, path)
if err != nil {
return err
}
}
modelKey := extractModelKey(fullPath)
log.Debug(modelKey)
schema := models["device"]
schema := models["Device"]
if err := g.SetNode(schema, root, update.Path, update.Val, opts...); err != nil {
log.Error(err)
}
......@@ -70,25 +78,25 @@ 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]
for i := 0; i < len(path.Elem)-1; i++ {
pathElement := path.Elem[i]
b.WriteString(delim)
b.WriteString(strings.Title(pathElement))
b.WriteString(strings.Title(pathElement.Name))
}
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 {
func gnmiFullPath(prefix, path *gpb.Path) (*gpb.Path, error) {
fullPath := &gpb.Path{Origin: path.Origin}
if path.GetElement() != nil {
fullPath.Element = append(prefix.GetElement(), path.GetElement()...)
return nil, errors.New("deprecated path element type is unsupported")
}
if path.GetElem() != nil {
fullPath.Elem = append(prefix.GetElem(), path.GetElem()...)
}
return fullPath
return fullPath, nil
}
// Capabilities calls GNMI capabilities
......@@ -110,16 +118,18 @@ func (g *Gnmi) get(ctx context.Context, paths [][]string, origin string) (interf
if err != nil {
return nil, err
}
return GetWithRequest(ctx, req)
return getWithRequest(ctx, req)
}
// GetWithRequest takes a fully formed GetRequest, performs the Get,
// and displays any response.
func GetWithRequest(ctx context.Context, req *gpb.GetRequest) (interface{}, error) {
client, err := gnmi.Dial(ctx.Value("config").(*gnmi.Config))
// getWithRequest takes a fully formed GetRequest, performs the Get,
// and returns any response.
func getWithRequest(ctx context.Context, req *gpb.GetRequest) (interface{}, error) {
cfg := ctx.Value("config").(*gnmi.Config)
client, err := gnmi.Dial(cfg)
if err != nil {
return nil, err
}
req.Encoding = cfg.Encoding
resp, err := client.Get(ctx, req)
if err != nil {
return nil, err
......
......@@ -24,13 +24,13 @@ func TestGetWithRequest(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetWithRequest(tt.args.ctx, tt.args.req)
got, err := getWithRequest(tt.args.ctx, tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("GetWithRequest() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("getWithRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetWithRequest() got = %v, want %v", got, tt.want)
t.Errorf("getWithRequest() got = %v, want %v", got, tt.want)
}
})
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment