From 80fb18821c4dd81aa8928bbf7e3dc08cb8495078 Mon Sep 17 00:00:00 2001 From: Brandon Bennett Date: Thu, 8 Jan 2026 10:39:09 -0700 Subject: [PATCH] feat: add support for RFC6243 with-defaults --- README.md | 2 +- TODO.md | 4 +- rpc/config.go | 32 +++++++--- rpc/rpc.go | 13 +++- rpc/with_defaults.go | 30 +++++++++ rpc/with_defaults_test.go | 131 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 rpc/with_defaults.go create mode 100644 rpc/with_defaults_test.go diff --git a/README.md b/README.md index 8a2f080..e9a3c93 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ func main() { | [RFC5277 NETCONF Event Notifications][RFC5277] | :white_check_mark: supported | | [RFC5717 Partial Lock Remote Procedure Call (RPC) for NETCONF][RFC5717] | :bulb: planned | | [RFC8071 NETCONF Call Home and RESTCONF Call Home][RFC8071] | :bulb: planned | -| [RFC6243 With-defaults Capability for NETCONF][RFC6243] | :bulb: planned | +| [RFC6243 With-defaults Capability for NETCONF][RFC6243] | :white_check_mark: supported | | [RFC4743 Using NETCONF over the Simple Object Access Protocol (SOAP)][RFC4743] | :x: not planned | | [RFC4744 Using the NETCONF Protocol over the BEEP][RFC4744] | :x: not planned | diff --git a/TODO.md b/TODO.md index 0dfb826..e809786 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,6 @@ - [ ] Pool/SessionManager for automatic reconnects, retries, etc. - [ ] Call Home support - [ ] nccurl command to issue rpc requests from the cli -- [ ] More RFC support +- [~] More RFC support - [ ] Partial Lock - [ ] with-defaults + - [X] with-defaults diff --git a/rpc/config.go b/rpc/config.go index e0c8c37..085dd66 100644 --- a/rpc/config.go +++ b/rpc/config.go @@ -62,20 +62,26 @@ func (u URL) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // // [RFC6241 7.1]: https://www.rfc-editor.org/rfc/rfc6241.html#section-7.1 type GetConfig struct { - Source Datastore - Filter Filter + Source Datastore + Filter Filter + WithDefaults WithDefaultsMode } func (op GetConfig) MarshalXML(e *xml.Encoder, start xml.StartElement) error { req := struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 get-config"` - Source Datastore `xml:"source"` - Filter Filter `xml:"filter,omitempty"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 get-config"` + Source Datastore `xml:"source"` + Filter Filter `xml:"filter,omitempty"` + WithDefaults *withDefaultsElement `xml:",omitempty"` }{ Source: op.Source, Filter: op.Filter, } + if op.WithDefaults != "" { + req.WithDefaults = &withDefaultsElement{Mode: op.WithDefaults} + } + return e.Encode(&req) } @@ -232,20 +238,26 @@ func (rpc EditConfig) Exec(ctx context.Context, session *netconf.Session) error // // [RFC6241 7.3] https://www.rfc-editor.org/rfc/rfc6241.html#section-7.3 type CopyConfig struct { - Source any - Target any + Source any + Target any + WithDefaults WithDefaultsMode } func (rpc CopyConfig) MarshalXML(e *xml.Encoder, start xml.StartElement) error { req := struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 copy-config"` - Source any `xml:"source"` - Target any `xml:"target"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 copy-config"` + Source any `xml:"source"` + Target any `xml:"target"` + WithDefaults *withDefaultsElement `xml:",omitempty"` }{ Source: rpc.Source, Target: rpc.Target, } + if rpc.WithDefaults != "" { + req.WithDefaults = &withDefaultsElement{Mode: rpc.WithDefaults} + } + return e.Encode(&req) } diff --git a/rpc/rpc.go b/rpc/rpc.go index 3ad8d82..92236e8 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -34,16 +34,23 @@ type OkReply struct { } type Get struct { - Filter Filter `xml:"filter,omitempty"` + Filter Filter + WithDefaults WithDefaultsMode } func (rpc *Get) MarshalXML(e *xml.Encoder, start xml.StartElement) error { req := struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 get"` - Filter Filter `xml:"filter,omitempty"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 get"` + Filter Filter `xml:"filter,omitempty"` + WithDefaults *withDefaultsElement `xml:",omitempty"` }{ Filter: rpc.Filter, } + + if rpc.WithDefaults != "" { + req.WithDefaults = &withDefaultsElement{Mode: rpc.WithDefaults} + } + return e.Encode(&req) } diff --git a/rpc/with_defaults.go b/rpc/with_defaults.go new file mode 100644 index 0000000..95e9c51 --- /dev/null +++ b/rpc/with_defaults.go @@ -0,0 +1,30 @@ +package rpc + +import "encoding/xml" + +// WithDefaultsMode specifies how default values should be reported +// as defined in RFC 6243. +type WithDefaultsMode string + +const ( + // DefaultsReportAll returns all data nodes including those set to their + // schema default values. + DefaultsReportAll WithDefaultsMode = "report-all" + + // DefaultsReportAllTagged returns all data nodes, with default nodes + // marked with a default="true" attribute. + DefaultsReportAllTagged WithDefaultsMode = "report-all-tagged" + + // DefaultsTrim omits data nodes set to their schema default values. + DefaultsTrim WithDefaultsMode = "trim" + + // DefaultsExplicit reports only nodes that have been explicitly set + // by the client, plus any state data. + DefaultsExplicit WithDefaultsMode = "explicit" +) + +// withDefaultsElement is a helper for marshaling the with-defaults element. +type withDefaultsElement struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults with-defaults"` + Mode WithDefaultsMode `xml:",chardata"` +} diff --git a/rpc/with_defaults_test.go b/rpc/with_defaults_test.go new file mode 100644 index 0000000..fe16928 --- /dev/null +++ b/rpc/with_defaults_test.go @@ -0,0 +1,131 @@ +package rpc + +import ( + "encoding/xml" + "testing" + + "github.com/carlmjohnson/be" +) + +func TestGetConfig_WithDefaults_MarshalXML(t *testing.T) { + tests := []struct { + name string + op GetConfig + expected string + }{ + { + name: "without with-defaults", + op: GetConfig{ + Source: Running, + }, + expected: ``, + }, + { + name: "report-all", + op: GetConfig{ + Source: Running, + WithDefaults: DefaultsReportAll, + }, + expected: `report-all`, + }, + { + name: "trim", + op: GetConfig{ + Source: Running, + WithDefaults: DefaultsTrim, + }, + expected: `trim`, + }, + { + name: "explicit", + op: GetConfig{ + Source: Running, + WithDefaults: DefaultsExplicit, + }, + expected: `explicit`, + }, + { + name: "report-all-tagged", + op: GetConfig{ + Source: Running, + WithDefaults: DefaultsReportAllTagged, + }, + expected: `report-all-tagged`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := xml.Marshal(tt.op) + be.NilErr(t, err) + be.Equal(t, tt.expected, string(got)) + }) + } +} + +func TestGet_WithDefaults_MarshalXML(t *testing.T) { + tests := []struct { + name string + op Get + expected string + }{ + { + name: "without with-defaults", + op: Get{}, + expected: ``, + }, + { + name: "report-all", + op: Get{ + WithDefaults: DefaultsReportAll, + }, + expected: `report-all`, + }, + { + name: "trim", + op: Get{ + WithDefaults: DefaultsTrim, + }, + expected: `trim`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := xml.Marshal(&tt.op) + be.NilErr(t, err) + be.Equal(t, tt.expected, string(got)) + }) + } +} + +func TestCopyConfig_WithDefaults_MarshalXML(t *testing.T) { + tests := []struct { + name string + op CopyConfig + expected string + }{ + { + name: "without with-defaults", + op: CopyConfig{ + Source: Running, + Target: Startup, + }, + expected: ``, + }, + { + name: "with explicit", + op: CopyConfig{ + Source: Running, + Target: Startup, + WithDefaults: DefaultsExplicit, + }, + expected: `explicit`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := xml.Marshal(tt.op) + be.NilErr(t, err) + be.Equal(t, tt.expected, string(got)) + }) + } +}