gnmi_transport.go 8.42 KB
Newer Older
1
2
3
4
5
6
package nucleus

import (
	"code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi"
	"context"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
7
8
9
	"github.com/openconfig/gnmi/proto/gnmi_ext"
	"github.com/openconfig/goyang/pkg/yang"
	"github.com/openconfig/ygot/ytypes"
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
10
	log "github.com/sirupsen/logrus"
Manuel Kieweg's avatar
Manuel Kieweg committed
11
	"reflect"
12
	"strings"
13
14
)

Malte Bauch's avatar
Malte Bauch committed
15
16
17
// CtxKeyType is a custom type to be used as key in a context.WithValue() or
// context.Value() call. For more information see:
// https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/
18
type CtxKeyType string
Malte Bauch's avatar
Malte Bauch committed
19

Manuel Kieweg's avatar
Manuel Kieweg committed
20
const (
Malte Bauch's avatar
Malte Bauch committed
21
	// CtxKeyOpts context key for gnmi.SubscribeOptions
22
	CtxKeyOpts CtxKeyType = "opts"
Malte Bauch's avatar
Malte Bauch committed
23
	// CtxKeyConfig is a context key for gnmi.Config
24
	CtxKeyConfig = "config"
Manuel Kieweg's avatar
Manuel Kieweg committed
25
26
)

27
28
// Gnmi implements the Transport interface and provides an SBI with the
// possibility to access a gNMI endpoint.
29
type Gnmi struct {
30
31
	SetNode   func(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error
	RespChan  chan *gpb.SubscribeResponse
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
32
	Unmarshal func([]byte, []string, interface{}, ...ytypes.UnmarshalOpt) error
33
34
	Options   *GnmiTransportOptions
	client    gpb.GNMIClient
35
36
}

37
38
// NewGnmiTransport takes a struct of GnmiTransportOptions and returns a Gnmi
// transport based on the values of it.
39
func NewGnmiTransport(opts *GnmiTransportOptions) (*Gnmi, error) {
40
	c, err := gnmi.Dial(&opts.Config)
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
41
42
43
	if err != nil {
		return nil, err
	}
44
	log.WithFields(log.Fields{
Manuel Kieweg's avatar
Manuel Kieweg committed
45
46
		"target":   opts.Addr,
		"tls":      opts.TLS,
47
48
		"encoding": opts.Encoding,
	}).Info("building new gNMI transport")
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
49
	return &Gnmi{
50
51
		SetNode:  opts.SetNode,
		RespChan: opts.RespChan,
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
52
		Options:  opts,
53
		client:   c,
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
54
	}, nil
Malte Bauch's avatar
Malte Bauch committed
55
56
}

57
//SetOptions sets Gnmi Options
Malte Bauch's avatar
Malte Bauch committed
58
func (g *Gnmi) SetOptions(to TransportOptions) {
59
	g.Options = to.(*GnmiTransportOptions)
Malte Bauch's avatar
Malte Bauch committed
60
61
}

62
//GetOptions returns the Gnmi options
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
63
func (g *Gnmi) GetOptions() interface{} {
64
	return g.Options
Malte Bauch's avatar
Malte Bauch committed
65
66
}

Malte Bauch's avatar
Malte Bauch committed
67
// Get takes a slice of gnmi paths, splits them and calls get for each one of them.
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
68
func (g *Gnmi) Get(ctx context.Context, params ...string) (interface{}, error) {
69
70
71
	if g.client == nil {
		return nil, &ErrNilClient{}
	}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
72
73
74
	paths := gnmi.SplitPaths(params)
	return g.get(ctx, paths, "")
}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
75
76
77

// Set takes a slice of params. This slice must contain at least one operation.
// It can contain an additional arbitrary amount of operations and extensions.
Manuel Kieweg's avatar
Manuel Kieweg committed
78
func (g *Gnmi) Set(ctx context.Context, args ...interface{}) (interface{}, error) {
79
80
81
	if g.client == nil {
		return nil, &ErrNilClient{}
	}
Manuel Kieweg's avatar
Manuel Kieweg committed
82
	if len(args) == 0 {
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
83
84
85
86
87
88
		return nil, &ErrInvalidParameters{
			f: "gnmi.Set()",
			r: "no parameters provided",
		}
	}

Manuel Kieweg's avatar
Manuel Kieweg committed
89
90
	// Loop over args and create ops and exts
	// Invalid args cause unhealable error
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
91
92
	ops := make([]*gnmi.Operation, 0)
	exts := make([]*gnmi_ext.Extension, 0)
Manuel Kieweg's avatar
Manuel Kieweg committed
93
	for _, p := range args {
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
94
95
		switch p.(type) {
		case *gnmi.Operation:
Manuel Kieweg's avatar
Manuel Kieweg committed
96
97
98
99
100
			op := p.(*gnmi.Operation)
			if op.Target == "" {
				op.Target = g.Options.Addr
			}
			ops = append(ops, op)
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
101
102
103
104
105
		case *gnmi_ext.Extension:
			exts = append(exts, p.(*gnmi_ext.Extension))
		default:
			return nil, &ErrInvalidParameters{
				f: "gnmi.Set()",
Manuel Kieweg's avatar
Manuel Kieweg committed
106
				r: "args contain invalid type",
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
107
108
109
110
111
112
113
114
115
116
			}
		}
	}
	if len(ops) == 0 {
		return nil, &ErrInvalidParameters{
			f: "gnmi.Set()",
			r: "no operations provided",
		}
	}
	return g.set(ctx, ops, exts...)
117
118
}

Malte Bauch's avatar
Malte Bauch committed
119
//Subscribe subscribes to a gNMI target
120
func (g *Gnmi) Subscribe(ctx context.Context, params ...string) error {
121
122
123
	if g.client == nil {
		return &ErrNilClient{}
	}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
124
125
	return g.subscribe(ctx)
}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
126

127
// Type returns the gNMI transport type
128
func (g *Gnmi) Type() string {
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
129
130
131
	return "gnmi"
}

132
133
// 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 {
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
134
135
136
137
	models := s.SchemaTree
	r := resp.(*gpb.GetResponse)
	rn := r.Notification
	for _, msg := range rn {
138
139
140
		for _, update := range msg.Update {
			path := update.Path
			fullPath := path
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
141
142
143
144
			val, ok := update.Val.Value.(*gpb.TypedValue_JsonIetfVal)
			if ok {
				opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}}
				if err := g.Unmarshal(val.JsonIetfVal, extraxtPathElements(fullPath), root, opts...); err != nil {
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
145
146
					return err
				}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
147
				return nil
148
			}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
149
			// TODO(mk): Evaluate hardcoded model key
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
150
			schema := models["Device"]
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
151
			opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}}
152
			if err := g.SetNode(schema, root, update.Path, update.Val, opts...); err != nil {
153
				return err
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
154
155
156
157
158
159
			}
		}
	}
	return nil
}

Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
160
161
162
163
func extraxtPathElements(path *gpb.Path) []string {
	elems := make([]string, len(path.Elem))
	for i, e := range path.Elem {
		elems[i] = strings.Title(e.Name)
164
	}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
165
	return elems
166
167
}

168
169
// Capabilities calls GNMI capabilities
func (g *Gnmi) Capabilities(ctx context.Context) (interface{}, error) {
170
171
172
	log.WithFields(log.Fields{
		"target": g.Options.Addr,
	}).Info("sending gNMI capabilities request")
173
	ctx = gnmi.NewContext(ctx, &g.Options.Config)
174
	ctx = context.WithValue(ctx, CtxKeyConfig, &g.Options.Config) //nolint
175
	resp, err := g.client.Capabilities(ctx, &gpb.CapabilityRequest{})
176
177
178
179
180
181
	if err != nil {
		return nil, err
	}
	return resp, nil
}

Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
182
// get calls GNMI get
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
183
func (g *Gnmi) get(ctx context.Context, paths [][]string, origin string) (interface{}, error) {
Manuel Kieweg's avatar
Manuel Kieweg committed
184

185
	ctx = gnmi.NewContext(ctx, &g.Options.Config)
186
	ctx = context.WithValue(ctx, CtxKeyConfig, &g.Options.Config) //nolint
187
	req, err := gnmi.NewGetRequest(ctx, paths, origin)
188
189
190
	if err != nil {
		return nil, err
	}
191
	return g.getWithRequest(ctx, req)
192
193
}

Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
194
195
// getWithRequest takes a fully formed GetRequest, performs the Get,
// and returns any response.
196
func (g *Gnmi) getWithRequest(ctx context.Context, req *gpb.GetRequest) (interface{}, error) {
Manuel Kieweg's avatar
Manuel Kieweg committed
197
198
199
	if req == nil {
		return nil, &ErrNil{}
	}
200
201
202
203
204
	log.WithFields(log.Fields{
		"target": g.Options.Addr,
		"path":   req.Path,
	}).Info("sending gNMI get request")

205
	resp, err := g.client.Get(ctx, req)
206
207
208
209
210
211
212
	if err != nil {
		return nil, err
	}
	return resp, nil
}

// Set calls GNMI set
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
213
func (g *Gnmi) set(ctx context.Context, setOps []*gnmi.Operation,
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
214
	exts ...*gnmi_ext.Extension) (*gpb.SetResponse, error) {
215
	ctx = gnmi.NewContext(ctx, &g.Options.Config)
216
217
218
	targets := make([]string, len(setOps))
	paths := make([][]string, len(setOps))
	values := make([]string, len(setOps))
Manuel Kieweg's avatar
Manuel Kieweg committed
219
	for i, v := range setOps {
220
221
222
223
224
225
		targets[i] = v.Target
		paths[i] = v.Path
		values[i] = v.Val
	}
	log.WithFields(log.Fields{
		"targets": targets,
Manuel Kieweg's avatar
Manuel Kieweg committed
226
227
		"paths":   paths,
		"values":  values,
228
	}).Info("sending gNMI set request")
229
	return gnmi.Set(ctx, g.client, setOps, exts...)
230
231
232
}

// Subscribe calls GNMI subscribe
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
233
func (g *Gnmi) subscribe(ctx context.Context) error {
234
	ctx = gnmi.NewContext(ctx, &g.Options.Config)
Manuel Kieweg's avatar
Manuel Kieweg committed
235
	opts, ok := ctx.Value(CtxKeyOpts).(*gnmi.SubscribeOptions)
Manuel Kieweg's avatar
go fmt    
Manuel Kieweg committed
236
	if !ok {
Manuel Kieweg's avatar
Manuel Kieweg committed
237
238
239
240
241
		return &ErrInvalidTypeAssertion{
			v: reflect.TypeOf(ctx.Value(CtxKeyOpts)),
			t: reflect.TypeOf(&gnmi.SubscribeOptions{}),
		}
	}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
242
	go func() {
243
		log.WithFields(log.Fields{
Manuel Kieweg's avatar
Manuel Kieweg committed
244
245
246
			"address":  opts.Target,
			"paths":    opts.Paths,
			"mode":     opts.Mode,
247
248
			"interval": opts.SampleInterval,
		}).Info("subscribed to gNMI target")
249
250
		for {
			resp := <-g.RespChan
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
251
252
253
254
			if resp != nil {
				if err := gnmi.LogSubscribeResponse(resp); err != nil {
					log.Fatal(err)
				}
Manuel Max Kieweg's avatar
Manuel Max Kieweg committed
255
256
257
			}
		}
	}()
258
	return gnmi.SubscribeErr(ctx, g.client, opts, g.RespChan)
259
260
261
262
263
264
}

// Close calls GNMI close
func (g *Gnmi) Close() error {
	return nil
}
Malte Bauch's avatar
Malte Bauch committed
265

266
267
// GnmiTransportOptions implements the TransportOptions interface.
// GnmiTransportOptions contains all needed information to setup a Gnmi
Malte Bauch's avatar
Malte Bauch committed
268
// transport and therefore inherits gnmi.Config.
269
type GnmiTransportOptions struct {
Malte Bauch's avatar
Malte Bauch committed
270
	// all needed gnmi transport parameters
271
272
273
	gnmi.Config

	SetNode func(schema *yang.Entry, root interface{}, path *gpb.Path,
Malte Bauch's avatar
Malte Bauch committed
274
		val interface{}, opts ...ytypes.SetNodeOpt) error
275
276
	Unmarshal func([]byte, []string, interface{}, ...ytypes.UnmarshalOpt) error
	RespChan  chan *gpb.SubscribeResponse
Malte Bauch's avatar
Malte Bauch committed
277
278
}

279
280
// GetAddress returns the address used by the transport to connect to a
// gRPC endpoint.
281
func (gto *GnmiTransportOptions) GetAddress() string {
282
	return gto.Config.Addr
Malte Bauch's avatar
Malte Bauch committed
283
}
284
285
286

// GetUsername returns the username used by the transport to connect to a
// gRPC endpoint.
287
func (gto *GnmiTransportOptions) GetUsername() string {
288
	return gto.Config.Username
Malte Bauch's avatar
Malte Bauch committed
289
}
290
291
292

// GetPassword returns the password used by the transport to connect to a
// gRPC endpoint.
293
func (gto *GnmiTransportOptions) GetPassword() string {
294
	return gto.Config.Password
Malte Bauch's avatar
Malte Bauch committed
295
296
}

297
298
// IsTransportOption is needed to fulfill the requirements of the
// TransportOptions interface. It does not need any further implementation.
299
func (gto *GnmiTransportOptions) IsTransportOption() {}