-
Notifications
You must be signed in to change notification settings - Fork 7
/
ops.go
514 lines (425 loc) · 16.1 KB
/
ops.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
package netconf
import (
"context"
"encoding/xml"
"fmt"
"strings"
"time"
)
type ExtantBool bool
func (b ExtantBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if !b {
return nil
}
// This produces a empty start/end tag (i.e <tag></tag>) vs a self-closing
// tag (<tag/>() which should be the same in XML, however I know certain
// vendors may have issues with this format. We may have to process this
// after xml encoding.
//
// See https://github.com/golang/go/issues/21399
// or https://github.com/golang/go/issues/26756 for a different hack.
return e.EncodeElement(struct{}{}, start)
}
func (b *ExtantBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
v := &struct{}{}
if err := d.DecodeElement(v, &start); err != nil {
return err
}
*b = v != nil
return nil
}
type OKResp struct {
OK ExtantBool `xml:"ok"`
}
type Datastore string
func (s Datastore) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if s == "" {
return fmt.Errorf("datastores cannot be empty")
}
// XXX: it would be nice to actually just block names with crap in them
// instead of escaping them, but we need to find a list of what is allowed
// in an xml tag.
escaped, err := escapeXML(string(s))
if err != nil {
return fmt.Errorf("invalid string element: %w", err)
}
v := struct {
Elem string `xml:",innerxml"`
}{Elem: "<" + escaped + "/>"}
return e.EncodeElement(&v, start)
}
func escapeXML(input string) (string, error) {
buf := &strings.Builder{}
if err := xml.EscapeText(buf, []byte(input)); err != nil {
return "", err
}
return buf.String(), nil
}
type URL string
func (u URL) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
v := struct {
URL string `xml:"url"`
}{string(u)}
return e.EncodeElement(&v, start)
}
const (
// Running configuration datastore. Required by RFC6241
Running Datastore = "running"
// Candidate configuration configuration datastore. Supported with the
// `:candidate` capability defined in RFC6241 section 8.3
Candidate Datastore = "candidate"
// Startup configuration configuration datastore. Supported with the
// `:startup` capability defined in RFC6241 section 8.7
Startup Datastore = "startup" //
)
type GetConfigReq struct {
XMLName xml.Name `xml:"get-config"`
Source Datastore `xml:"source"`
// Filter
}
type GetConfigReply struct {
XMLName xml.Name `xml:"data"`
Config []byte `xml:",innerxml"`
}
// GetConfig implements the <get-config> rpc operation defined in [RFC6241 7.1].
// `source` is the datastore to query.
//
// [RFC6241 7.1]: https://www.rfc-editor.org/rfc/rfc6241.html#section-7.1
func (s *Session) GetConfig(ctx context.Context, source Datastore) ([]byte, error) {
req := GetConfigReq{
Source: source,
}
var resp GetConfigReply
if err := s.Call(ctx, &req, &resp); err != nil {
return nil, err
}
return resp.Config, nil
}
// MergeStrategy defines the strategies for merging configuration in a
// `<edit-config> operation`.
//
// *Note*: in RFC6241 7.2 this is called the `operation` attribute and
// `default-operation` parameter. Since the `operation` term is already
// overloaded this was changed to `MergeStrategy` for a cleaner API.
type MergeStrategy string
const (
// MergeConfig configuration elements are merged together at the level at
// which this specified. Can be used for config elements as well as default
// defined with [WithDefaultMergeStrategy] option.
MergeConfig MergeStrategy = "merge"
// ReplaceConfig defines that the incoming config change should replace the
// existing config at the level which it is specified. This can be
// specified on individual config elements or set as the default strategy set
// with [WithDefaultMergeStrategy] option.
ReplaceConfig MergeStrategy = "replace"
// NoMergeStrategy is only used as a default strategy defined in
// [WithDefaultMergeStrategy]. Elements must specific one of the other
// strategies with the `operation` Attribute on elements in the `<config>`
// subtree. Elements without the `operation` attribute are ignored.
NoMergeStrategy MergeStrategy = "none"
// CreateConfig allows a subtree element to be created only if it doesn't
// already exist.
// This strategy is only used as the `operation` attribute of
// a `<config>` element and cannot be used as the default strategy.
CreateConfig MergeStrategy = "create"
// DeleteConfig will completely delete subtree from the config only if it
// already exists. This strategy is only used as the `operation` attribute
// of a `<config>` element and cannot be used as the default strategy.
DeleteConfig MergeStrategy = "delete"
// RemoveConfig will remove subtree from the config. If the subtree doesn't
// exist in the datastore then it is silently skipped. This strategy is
// only used as the `operation` attribute of a `<config>` element and cannot
// be used as the default strategy.
RemoveConfig MergeStrategy = "remove"
)
// TestStrategy defines the beahvior for testing configuration before applying it in a `<edit-config>` operation.
//
// *Note*: in RFC6241 7.2 this is called the `test-option` parameter. Since the `option` term is already
// overloaded this was changed to `TestStrategy` for a cleaner API.
type TestStrategy string
const (
// TestThenSet will validate the configuration and only if is is valid then
// apply the configuration to the datastore.
TestThenSet TestStrategy = "test-then-set"
// SetOnly will not do any testing before applying it.
SetOnly TestStrategy = "set"
// Test only will validation the incoming configuration and return the
// results without modifying the underlying store.
TestOnly TestStrategy = "test-only"
)
// ErrorStrategy defines the behavior when an error is encountered during a `<edit-config>` operation.
//
// *Note*: in RFC6241 7.2 this is called the `error-option` parameter. Since the `option` term is already
// overloaded this was changed to `ErrorStrategy` for a cleaner API.
type ErrorStrategy string
const (
// StopOnError will about the `<edit-config>` operation on the first error.
StopOnError ErrorStrategy = "stop-on-error"
// ContinueOnError will continue to parse the configuration data even if an
// error is encountered. Errors are still recorded and reported in the
// reply.
ContinueOnError ErrorStrategy = "continue-on-error"
// RollbackOnError will restore the configuration back to before the
// `<edit-config>` operation took place. This requires the device to
// support the `:rollback-on-error` capabilitiy.
RollbackOnError ErrorStrategy = "rollback-on-error"
)
type (
defaultMergeStrategy MergeStrategy
testStrategy TestStrategy
errorStrategy ErrorStrategy
)
func (o defaultMergeStrategy) apply(req *EditConfigReq) { req.DefaultMergeStrategy = MergeStrategy(o) }
func (o testStrategy) apply(req *EditConfigReq) { req.TestStrategy = TestStrategy(o) }
func (o errorStrategy) apply(req *EditConfigReq) { req.ErrorStrategy = ErrorStrategy(o) }
// WithDefaultMergeStrategy sets the default config merging strategy for the
// <edit-config> operation. Only [Merge], [Replace], and [None] are supported
// (the rest of the strategies are for defining as attributed in individual
// elements inside the `<config>` subtree).
func WithDefaultMergeStrategy(op MergeStrategy) EditConfigOption { return defaultMergeStrategy(op) }
// WithTestStrategy sets the `test-option` in the `<edit-config>“ operation.
// This defines what testing should be done the supplied configuration. See the
// documentation on [TestStrategy] for details on each strategy.
func WithTestStrategy(op TestStrategy) EditConfigOption { return testStrategy(op) }
// WithErrorStrategy sets the `error-option` in the `<edit-config>` operation.
// This defines the behavior when errors are encountered applying the supplied
// config. See [ErrorStrategy] for the available options.
func WithErrorStrategy(opt ErrorStrategy) EditConfigOption { return errorStrategy(opt) }
type EditConfigReq struct {
XMLName xml.Name `xml:"edit-config"`
Target Datastore `xml:"target"`
DefaultMergeStrategy MergeStrategy `xml:"default-operation,omitempty"`
TestStrategy TestStrategy `xml:"test-option,omitempty"`
ErrorStrategy ErrorStrategy `xml:"error-option,omitempty"`
// either of these two values
Config any `xml:"config,omitempty"`
URL string `xml:"url,omitempty"`
}
// EditOption is a optional arguments to [Session.EditConfig] method
type EditConfigOption interface {
apply(*EditConfigReq)
}
// EditConfig issues the `<edit-config>` operation defined in [RFC6241 7.2] for
// updating an existing target config datastore.
//
// [RFC6241 7.2]: https://www.rfc-editor.org/rfc/rfc6241.html#section-7.2
func (s *Session) EditConfig(ctx context.Context, target Datastore, config any, opts ...EditConfigOption) error {
req := EditConfigReq{
Target: target,
}
// XXX: Should we use reflect here?
switch v := config.(type) {
case string:
req.Config = struct {
Inner []byte `xml:",innerxml"`
}{Inner: []byte(v)}
case []byte:
req.Config = struct {
Inner []byte `xml:",innerxml"`
}{Inner: v}
case URL:
req.URL = string(v)
default:
req.Config = config
}
for _, opt := range opts {
opt.apply(&req)
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
type CopyConfigReq struct {
XMLName xml.Name `xml:"copy-config"`
Source any `xml:"source"`
Target any `xml:"target"`
}
// CopyConfig issues the `<copy-config>` operation as defined in [RFC6241 7.3]
// for copying an entire config to/from a source and target datastore.
//
// A `<config>` element defining a full config can be used as the source.
//
// If a device supports the `:url` capability than a [URL] object can be used
// for the source or target datastore.
//
// [RFC6241 7.3] https://www.rfc-editor.org/rfc/rfc6241.html#section-7.3
func (s *Session) CopyConfig(ctx context.Context, source, target any) error {
req := CopyConfigReq{
Source: source,
Target: target,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
type DeleteConfigReq struct {
XMLName xml.Name `xml:"delete-config"`
Target Datastore `xml:"target"`
}
func (s *Session) DeleteConfig(ctx context.Context, target Datastore) error {
req := DeleteConfigReq{
Target: target,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
type LockReq struct {
XMLName xml.Name
Target Datastore `xml:"target"`
}
func (s *Session) Lock(ctx context.Context, target Datastore) error {
req := LockReq{
XMLName: xml.Name{Local: "lock"},
Target: target,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
func (s *Session) Unlock(ctx context.Context, target Datastore) error {
req := LockReq{
XMLName: xml.Name{Local: "unlock"},
Target: target,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
/*
func (s *Session) Get(ctx context.Context, filter Filter) error {
panic("unimplemented")
}
*/
type KillSessionReq struct {
XMLName xml.Name `xml:"kill-session"`
SessionID uint32 `xml:"session-id"`
}
func (s *Session) KillSession(ctx context.Context, sessionID uint32) error {
req := KillSessionReq{
SessionID: sessionID,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
type ValidateReq struct {
XMLName xml.Name `xml:"validate"`
Source any `xml:"source"`
}
func (s *Session) Validate(ctx context.Context, source any) error {
req := ValidateReq{
Source: source,
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
type CommitReq struct {
XMLName xml.Name `xml:"commit"`
Confirmed ExtantBool `xml:"confirmed,omitempty"`
ConfirmTimeout int64 `xml:"confirm-timeout,omitempty"`
Persist string `xml:"persist,omitempty"`
PersistID string `xml:"persist-id,omitempty"`
}
// CommitOption is a optional arguments to [Session.Commit] method
type CommitOption interface {
apply(*CommitReq)
}
type confirmed bool
type confirmedTimeout struct {
time.Duration
}
type persist string
type persistID string
func (o confirmed) apply(req *CommitReq) { req.Confirmed = true }
func (o confirmedTimeout) apply(req *CommitReq) {
req.Confirmed = true
req.ConfirmTimeout = int64(o.Seconds())
}
func (o persist) apply(req *CommitReq) {
req.Confirmed = true
req.Persist = string(o)
}
func (o persistID) apply(req *CommitReq) { req.PersistID = string(o) }
// RollbackOnError will restore the configuration back to before the
// `<edit-config>` operation took place. This requires the device to
// support the `:rollback-on-error` capability.
// WithConfirmed will mark the commits as requiring confirmation or will rollback
// after the default timeout on the device (default should be 600s). The commit
// can be confirmed with another `<commit>` call without the confirmed option,
// extended by calling with `Commit` With `WithConfirmed` or
// `WithConfirmedTimeout` or canceling the commit with a `CommitCancel` call.
// This requires the device to support the `:confirmed-commit:1.1` capability.
func WithConfirmed() CommitOption { return confirmed(true) }
// WithConfirmedTimeout is like `WithConfirmed` but using the given timeout
// duration instead of the device's default.
func WithConfirmedTimeout(timeout time.Duration) CommitOption { return confirmedTimeout{timeout} }
// WithPersist allows you to set a identifier to confirm a commit in another
// sessions. Confirming the commit requires setting the `WithPersistID` in the
// following `Commit` call matching the id set on the confirmed commit. Will
// mark the commit as confirmed if not already set.
func WithPersist(id string) CommitOption { return persist(id) }
// WithPersistID is used to confirm a previous commit set with a given
// identifier. This allows you to confirm a commit from (potentially) another
// sesssion.
func WithPersistID(id string) persistID { return persistID(id) }
// Commit will commit a canidate config to the running comming. This requires
// the device to support the `:canidate` capability.
func (s *Session) Commit(ctx context.Context, opts ...CommitOption) error {
var req CommitReq
for _, opt := range opts {
opt.apply(&req)
}
if req.PersistID != "" && req.Confirmed {
return fmt.Errorf("PersistID cannot be used with Confirmed/ConfirmedTimeout or Persist options")
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
// CancelCommitOption is a optional arguments to [Session.CancelCommit] method
type CancelCommitOption interface {
applyCancelCommit(*CancelCommitReq)
}
func (o persistID) applyCancelCommit(req *CancelCommitReq) { req.PersistID = string(o) }
type CancelCommitReq struct {
XMLName xml.Name `xml:"cancel-commit"`
PersistID string `xml:"persist-id,omitempty"`
}
func (s *Session) CancelCommit(ctx context.Context, opts ...CancelCommitOption) error {
var req CancelCommitReq
for _, opt := range opts {
opt.applyCancelCommit(&req)
}
var resp OKResp
return s.Call(ctx, &req, &resp)
}
// CreateSubscriptionOption is a optional arguments to [Session.CreateSubscription] method
type CreateSubscriptionOption interface {
apply(req *CreateSubscriptionReq)
}
type CreateSubscriptionReq struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:notification:1.0 create-subscription"`
Stream string `xml:"stream,omitempty"`
// TODO: Implement filter
//Filter int64 `xml:"filter,omitempty"`
StartTime string `xml:"startTime,omitempty"`
EndTime string `xml:"endTime,omitempty"`
}
type stream string
type startTime time.Time
type endTime time.Time
func (o stream) apply(req *CreateSubscriptionReq) {
req.Stream = string(o)
}
func (o startTime) apply(req *CreateSubscriptionReq) {
req.StartTime = time.Time(o).Format(time.RFC3339)
}
func (o endTime) apply(req *CreateSubscriptionReq) {
req.EndTime = time.Time(o).Format(time.RFC3339)
}
func WithStreamOption(s string) CreateSubscriptionOption { return stream(s) }
func WithStartTimeOption(st time.Time) CreateSubscriptionOption { return startTime(st) }
func WithEndTimeOption(et time.Time) CreateSubscriptionOption { return endTime(et) }
func (s *Session) CreateSubscription(ctx context.Context, opts ...CreateSubscriptionOption) error {
var req CreateSubscriptionReq
for _, opt := range opts {
opt.apply(&req)
}
// TODO: eventual custom notifications rpc logic, e.g. create subscription only if notification capability is present
var resp OKResp
return s.Call(ctx, &req, &resp)
}