From 4aa288b25f1faa2b4d1863e4e0db7f9504bb5ae2 Mon Sep 17 00:00:00 2001 From: Sven Rebhan <36194019+srebhan@users.noreply.github.com> Date: Thu, 23 May 2024 15:09:11 -0400 Subject: [PATCH] feat(inputs.gnmi): Add yang-model decoding of JSON IETF payloads (#15201) --- docs/LICENSE_OF_DEPENDENCIES.md | 1 + go.mod | 1 + go.sum | 1 + plugins/common/yangmodel/decoder.go | 263 ++++++++++++++++++ plugins/inputs/gnmi/README.md | 5 + plugins/inputs/gnmi/gnmi.go | 13 + plugins/inputs/gnmi/gnmi_test.go | 2 +- plugins/inputs/gnmi/handler.go | 8 +- plugins/inputs/gnmi/path.go | 8 + plugins/inputs/gnmi/sample.conf | 5 + plugins/inputs/gnmi/sample.conf.in | 5 + .../gnmi/testcases/issue_15046/expected.out | 5 + .../gnmi/testcases/issue_15046/models/LICENSE | 202 ++++++++++++++ .../testcases/issue_15046/models/README.md | 2 + .../models/openconfig-alarm-types.yang | 109 ++++++++ .../models/openconfig-extensions.yang | 110 ++++++++ .../models/openconfig-platform-psu.yang | 92 ++++++ .../models/openconfig-platform-types.yang | 80 ++++++ .../models/openconfig-platform.yang | 162 +++++++++++ .../issue_15046/models/openconfig-types.yang | 66 +++++ .../gnmi/testcases/issue_15046/responses.json | 141 ++++++++++ .../gnmi/testcases/issue_15046/telegraf.conf | 18 ++ plugins/inputs/gnmi/update_fields.go | 112 ++++++-- 23 files changed, 1386 insertions(+), 25 deletions(-) create mode 100644 plugins/common/yangmodel/decoder.go create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/expected.out create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/LICENSE create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/README.md create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-alarm-types.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-extensions.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-psu.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-types.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-types.yang create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/responses.json create mode 100644 plugins/inputs/gnmi/testcases/issue_15046/telegraf.conf diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index dc0a80569..1e1b91732 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -291,6 +291,7 @@ following works: - github.com/olivere/elastic [MIT License](https://github.com/olivere/elastic/blob/release-branch.v7/LICENSE) - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil [Apache License 2.0](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/LICENSE) - github.com/openconfig/gnmi [Apache License 2.0](https://github.com/openconfig/gnmi/blob/master/LICENSE) +- github.com/openconfig/goyang [Apache License 2.0](https://github.com/openconfig/goyang/blob/master/LICENSE) - github.com/opencontainers/go-digest [Apache License 2.0](https://github.com/opencontainers/go-digest/blob/master/LICENSE) - github.com/opencontainers/image-spec [Apache License 2.0](https://github.com/opencontainers/image-spec/blob/master/LICENSE) - github.com/opensearch-project/opensearch-go [Apache License 2.0](https://github.com/opensearch-project/opensearch-go/blob/main/LICENSE.txt) diff --git a/go.mod b/go.mod index e562e5cc2..bd1318e9e 100644 --- a/go.mod +++ b/go.mod @@ -148,6 +148,7 @@ require ( github.com/nwaples/tacplus v0.0.3 github.com/olivere/elastic v6.2.37+incompatible github.com/openconfig/gnmi v0.10.0 + github.com/openconfig/goyang v1.0.0 github.com/opensearch-project/opensearch-go/v2 v2.3.0 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b github.com/openzipkin-contrib/zipkin-go-opentracing v0.5.0 diff --git a/go.sum b/go.sum index a014c8d21..6f6387cbc 100644 --- a/go.sum +++ b/go.sum @@ -1945,6 +1945,7 @@ github.com/openconfig/gnmi v0.10.0 h1:kQEZ/9ek3Vp2Y5IVuV2L/ba8/77TgjdXg505QXvYmg github.com/openconfig/gnmi v0.10.0/go.mod h1:Y9os75GmSkhHw2wX8sMsxfI7qRGAEcDh8NTa5a8vj6E= github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU= github.com/openconfig/goyang v0.2.2/go.mod h1:vX61x01Q46AzbZUzG617vWqh/cB+aisc+RrNkXRd3W8= +github.com/openconfig/goyang v1.0.0 h1:nYaFu7BOAk/eQn4CgAUjgYPfp3J6CdXrBryp32E5CjI= github.com/openconfig/goyang v1.0.0/go.mod h1:vX61x01Q46AzbZUzG617vWqh/cB+aisc+RrNkXRd3W8= github.com/openconfig/gribi v0.1.1-0.20210423184541-ce37eb4ba92f/go.mod h1:OoH46A2kV42cIXGyviYmAlGmn6cHjGduyC2+I9d/iVs= github.com/openconfig/grpctunnel v0.0.0-20210610163803-fde4a9dc048d/go.mod h1:x9tAZ4EwqCQ0jI8D6S8Yhw9Z0ee7/BxWQX0k0Uib5Q8= diff --git a/plugins/common/yangmodel/decoder.go b/plugins/common/yangmodel/decoder.go new file mode 100644 index 000000000..08a9e5637 --- /dev/null +++ b/plugins/common/yangmodel/decoder.go @@ -0,0 +1,263 @@ +package yangmodel + +import ( + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "strconv" + + "github.com/openconfig/goyang/pkg/yang" +) + +var ( + ErrInsufficientData = errors.New("insufficient data") + ErrNotFound = errors.New("no such node") +) + +type Decoder struct { + modules map[string]*yang.Module + rootNodes map[string][]yang.Node +} + +func NewDecoder(paths ...string) (*Decoder, error) { + modules := yang.NewModules() + modules.ParseOptions.IgnoreSubmoduleCircularDependencies = true + + var moduleFiles []string + modulePaths := paths + unresolved := paths + for { + var newlyfound []string + for _, path := range unresolved { + entries, err := os.ReadDir(path) + if err != nil { + return nil, fmt.Errorf("reading directory %q failed: %w", path, err) + } + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + fmt.Printf("Couldn't get info for %q: %v", entry.Name(), err) + continue + } + + if info.Mode()&os.ModeSymlink != 0 { + target, err := filepath.EvalSymlinks(entry.Name()) + if err != nil { + fmt.Printf("Couldn't evaluate symbolic links for %q: %v", entry.Name(), err) + continue + } + info, err = os.Lstat(target) + if err != nil { + fmt.Printf("Couldn't stat target %v: %v", target, err) + continue + } + } + + newPath := filepath.Join(path, info.Name()) + if info.IsDir() { + newlyfound = append(newlyfound, newPath) + continue + } + if info.Mode().IsRegular() && filepath.Ext(info.Name()) == ".yang" { + moduleFiles = append(moduleFiles, info.Name()) + } + } + } + if len(newlyfound) == 0 { + break + } + + modulePaths = append(modulePaths, newlyfound...) + unresolved = newlyfound + } + + // Add the module paths + modules.AddPath(modulePaths...) + for _, fn := range moduleFiles { + if err := modules.Read(fn); err != nil { + fmt.Printf("reading file %q failed: %v\n", fn, err) + } + } + if errs := modules.Process(); len(errs) > 0 { + return nil, errors.Join(errs...) + } + + // Get all root nodes defined in models with their origin. We require + // those nodes to later resolve paths to YANG model leaf nodes... + moduleLUT := make(map[string]*yang.Module) + moduleRootNodes := make(map[string][]yang.Node) + for _, m := range modules.Modules { + // Check if we processed the module already + if _, found := moduleLUT[m.Name]; found { + continue + } + // Create a module mapping for easily finding modules by name + moduleLUT[m.Name] = m + + // Determine the origin defined in the module + var prefix string + for _, imp := range m.Import { + if imp.Name == "openconfig-extensions" { + prefix = imp.Name + if imp.Prefix != nil { + prefix = imp.Prefix.Name + } + break + } + } + + var moduleOrigin string + if prefix != "" { + for _, e := range m.Extensions { + if e.Keyword == prefix+":origin" || e.Keyword == "origin" { + moduleOrigin = e.Argument + break + } + } + } + for _, u := range m.Uses { + root, err := yang.FindNode(m, u.Name) + if err != nil { + return nil, err + } + moduleRootNodes[moduleOrigin] = append(moduleRootNodes[moduleOrigin], root) + } + } + + return &Decoder{modules: moduleLUT, rootNodes: moduleRootNodes}, nil +} + +func (d *Decoder) FindLeaf(name, identifier string) (*yang.Leaf, error) { + // Get module name from the element + module, found := d.modules[name] + if !found { + return nil, fmt.Errorf("cannot find module %q", name) + } + + for _, grp := range module.Grouping { + for _, leaf := range grp.Leaf { + if leaf.Name == identifier { + return leaf, nil + } + } + } + return nil, ErrNotFound +} + +func DecodeLeafValue(leaf *yang.Leaf, value interface{}) (interface{}, error) { + schema := leaf.Type.YangType + + // Ignore all non-string values as the types seem already converted... + s, ok := value.(string) + if !ok { + return value, nil + } + + switch schema.Kind { + case yang.Ybinary: + // Binary values are encodes as base64 string, so decode the string + raw, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return value, err + } + + switch schema.Name { + case "ieeefloat32": + if len(raw) != 4 { + return raw, fmt.Errorf("%w, expected 4 but got %d bytes", ErrInsufficientData, len(raw)) + } + return math.Float32frombits(binary.BigEndian.Uint32(raw)), nil + default: + return raw, nil + } + case yang.Yint8: + v, err := strconv.ParseInt(s, 10, 8) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return int8(v), nil + case yang.Yint16: + v, err := strconv.ParseInt(s, 10, 16) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return int16(v), nil + case yang.Yint32: + v, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return int32(v), nil + case yang.Yint64: + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return v, nil + case yang.Yuint8: + v, err := strconv.ParseUint(s, 10, 8) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return uint8(v), nil + case yang.Yuint16: + v, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return uint16(v), nil + case yang.Yuint32: + v, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return uint32(v), nil + case yang.Yuint64: + v, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return v, nil + case yang.Ydecimal64: + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return value, fmt.Errorf("parsing %s %q failed: %w", yang.TypeKindToName[schema.Kind], s, err) + } + return v, nil + } + return value, nil +} + +func (d *Decoder) DecodeLeafElement(namespace, identifier string, value interface{}) (interface{}, error) { + leaf, err := d.FindLeaf(namespace, identifier) + if err != nil { + return nil, fmt.Errorf("finding %s failed: %w", identifier, err) + } + + return DecodeLeafValue(leaf, value) +} + +func (d *Decoder) DecodePathElement(origin, path string, value interface{}) (interface{}, error) { + rootNodes, found := d.rootNodes[origin] + if !found || len(rootNodes) == 0 { + return value, nil + } + + for _, root := range rootNodes { + node, _ := yang.FindNode(root, path) + if node == nil { + // The path does not exist in this root node + continue + } + // We do expect a leaf node... + if leaf, ok := node.(*yang.Leaf); ok { + return DecodeLeafValue(leaf, value) + } + } + + return value, nil +} diff --git a/plugins/inputs/gnmi/README.md b/plugins/inputs/gnmi/README.md index 1a4bcac34..237da33c1 100644 --- a/plugins/inputs/gnmi/README.md +++ b/plugins/inputs/gnmi/README.md @@ -129,6 +129,11 @@ details on how to use them. ## adds component, component_id & sub_component_id as additional tags # vendor_specific = [] + ## YANG model paths for decoding IETF JSON payloads + ## Model files are loaded recursively from the given directories. Disabled if + ## no models are specified. + # yang_model_paths = [] + ## Define additional aliases to map encoding paths to measurement names # [inputs.gnmi.aliases] # ifcounters = "openconfig:/interfaces/interface/state/counters" diff --git a/plugins/inputs/gnmi/gnmi.go b/plugins/inputs/gnmi/gnmi.go index a3170b2ba..8ca463cd6 100644 --- a/plugins/inputs/gnmi/gnmi.go +++ b/plugins/inputs/gnmi/gnmi.go @@ -20,6 +20,7 @@ import ( "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal/choice" internaltls "github.com/influxdata/telegraf/plugins/common/tls" + "github.com/influxdata/telegraf/plugins/common/yangmodel" "github.com/influxdata/telegraf/plugins/inputs" ) @@ -62,11 +63,13 @@ type GNMI struct { EnableTLS bool `toml:"enable_tls" deprecated:"1.27.0;use 'tls_enable' instead"` KeepaliveTime config.Duration `toml:"keepalive_time"` KeepaliveTimeout config.Duration `toml:"keepalive_timeout"` + YangModelPaths []string `toml:"yang_model_paths"` Log telegraf.Logger `toml:"-"` internaltls.ClientConfig // Internal state internalAliases map[*pathInfo]string + decoder *yangmodel.Decoder cancel context.CancelFunc wg sync.WaitGroup } @@ -219,6 +222,15 @@ func (c *GNMI) Init() error { return err } + // Load the YANG models if specified by the user + if len(c.YangModelPaths) > 0 { + decoder, err := yangmodel.NewDecoder(c.YangModelPaths...) + if err != nil { + return fmt.Errorf("creating YANG model decoder failed: %w", err) + } + c.decoder = decoder + } + return nil } @@ -275,6 +287,7 @@ func (c *GNMI) Start(acc telegraf.Accumulator) error { trimSlash: c.TrimFieldNames, tagPathPrefix: c.PrefixTagKeyWithPath, guessPathStrategy: c.GuessPathStrategy, + decoder: c.decoder, log: c.Log, ClientParameters: keepalive.ClientParameters{ Time: time.Duration(c.KeepaliveTime), diff --git a/plugins/inputs/gnmi/gnmi_test.go b/plugins/inputs/gnmi/gnmi_test.go index fc1aaa24c..041f06c62 100644 --- a/plugins/inputs/gnmi/gnmi_test.go +++ b/plugins/inputs/gnmi/gnmi_test.go @@ -1210,7 +1210,7 @@ func TestCases(t *testing.T) { require.Eventually(t, func() bool { return acc.NMetrics() >= uint64(len(expected)) - }, 1*time.Second, 100*time.Millisecond) + }, 15*time.Second, 100*time.Millisecond) plugin.Stop() grpcServer.Stop() wg.Wait() diff --git a/plugins/inputs/gnmi/handler.go b/plugins/inputs/gnmi/handler.go index e7da9c413..e65af846a 100644 --- a/plugins/inputs/gnmi/handler.go +++ b/plugins/inputs/gnmi/handler.go @@ -25,6 +25,7 @@ import ( "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/internal/choice" "github.com/influxdata/telegraf/metric" + "github.com/influxdata/telegraf/plugins/common/yangmodel" jnprHeader "github.com/influxdata/telegraf/plugins/inputs/gnmi/extensions/jnpr_gnmi_extention" "github.com/influxdata/telegraf/selfstat" ) @@ -44,6 +45,7 @@ type handler struct { trimSlash bool tagPathPrefix bool guessPathStrategy string + decoder *yangmodel.Decoder log telegraf.Logger keepalive.ClientParameters } @@ -172,7 +174,11 @@ func (h *handler) handleSubscribeResponseUpdate(acc telegraf.Accumulator, respon var valueFields []updateField for _, update := range response.Update.Update { fullPath := prefix.append(update.Path) - fields, err := newFieldsFromUpdate(fullPath, update) + if update.Path.Origin != "" { + fullPath.origin = update.Path.Origin + } + + fields, err := h.newFieldsFromUpdate(fullPath, update) if err != nil { h.log.Errorf("Processing update %v failed: %v", update, err) } diff --git a/plugins/inputs/gnmi/path.go b/plugins/inputs/gnmi/path.go index 027a328a9..32636f250 100644 --- a/plugins/inputs/gnmi/path.go +++ b/plugins/inputs/gnmi/path.go @@ -279,6 +279,14 @@ func (pi *pathInfo) String() string { return out } +func (pi *pathInfo) Path() (origin, path string) { + if len(pi.segments) == 0 { + return pi.origin, "/" + } + + return pi.origin, "/" + strings.Join(pi.segments, "/") +} + func (pi *pathInfo) Tags(pathPrefix bool) map[string]string { tags := make(map[string]string, len(pi.keyValues)) for _, s := range pi.keyValues { diff --git a/plugins/inputs/gnmi/sample.conf b/plugins/inputs/gnmi/sample.conf index a72854fbe..a1dcf5eec 100644 --- a/plugins/inputs/gnmi/sample.conf +++ b/plugins/inputs/gnmi/sample.conf @@ -82,6 +82,11 @@ ## adds component, component_id & sub_component_id as additional tags # vendor_specific = [] + ## YANG model paths for decoding IETF JSON payloads + ## Model files are loaded recursively from the given directories. Disabled if + ## no models are specified. + # yang_model_paths = [] + ## Define additional aliases to map encoding paths to measurement names # [inputs.gnmi.aliases] # ifcounters = "openconfig:/interfaces/interface/state/counters" diff --git a/plugins/inputs/gnmi/sample.conf.in b/plugins/inputs/gnmi/sample.conf.in index 11af0fac6..f4a9af6b9 100644 --- a/plugins/inputs/gnmi/sample.conf.in +++ b/plugins/inputs/gnmi/sample.conf.in @@ -61,6 +61,11 @@ ## adds component, component_id & sub_component_id as additional tags # vendor_specific = [] + ## YANG model paths for decoding IETF JSON payloads + ## Model files are loaded recursively from the given directories. Disabled if + ## no models are specified. + # yang_model_paths = [] + ## Define additional aliases to map encoding paths to measurement names # [inputs.gnmi.aliases] # ifcounters = "openconfig:/interfaces/interface/state/counters" diff --git a/plugins/inputs/gnmi/testcases/issue_15046/expected.out b/plugins/inputs/gnmi/testcases/issue_15046/expected.out new file mode 100644 index 000000000..f7d721bb0 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/expected.out @@ -0,0 +1,5 @@ +psu,name=PowerSupply1/A,path=openconfig:/components/component/power-supply/state,source=127.0.0.1 openconfig_platform_psu:capacity=715,openconfig_platform_psu:enabled=true,openconfig_platform_psu:input_current=0.47099998593330383,openconfig_platform_psu:input_voltage=208.5,openconfig_platform_psu:output_current=1.2029999494552612,openconfig_platform_psu:output_power=68.625,openconfig_platform_psu:output_voltage=56.367000579833984 1711178737105194000 +psu,name=PowerSupply1/B,path=openconfig:/components/component/power-supply/state,source=127.0.0.1 openconfig_platform_psu:capacity=715,openconfig_platform_psu:enabled=true,openconfig_platform_psu:input_current=0.3930000066757202,openconfig_platform_psu:input_voltage=209.75,openconfig_platform_psu:output_current=0.9380000233650208,openconfig_platform_psu:output_power=51.875,openconfig_platform_psu:output_voltage=56.367000579833984 1711178737105194000 +temp,name=InletTempSensor1,path=openconfig:/components/component/state/temperature,source=127.0.0.1 alarm_severity="openconfig-alarm-types:MINOR",alarm_status=false,alarm_threshold=0,avg=24.000000,instant=35.000000,interval=180000000000u,max=36.000000,min=0.000000 1715838159171548000 +temp,name=OutletTempSensor1,path=openconfig:/components/component/state/temperature,source=127.0.0.1 alarm_severity="openconfig-alarm-types:MINOR",alarm_status=false,alarm_threshold=0,avg=29.000000,instant=44.000000,interval=180000000000u,max=44.000000,min=0.000000 1715838159171548000 +temp,name=HotSpotTempSensor1,path=openconfig:/components/component/state/temperature,source=127.0.0.1 alarm_severity="openconfig-alarm-types:MINOR",alarm_status=false,alarm_threshold=0,avg=39.000000,instant=58.000000,interval=180000000000u,max=59.000000,min=0.000000 1715838159171548000 diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/LICENSE b/plugins/inputs/gnmi/testcases/issue_15046/models/LICENSE new file mode 100644 index 000000000..8f71f43fe --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/README.md b/plugins/inputs/gnmi/testcases/issue_15046/models/README.md new file mode 100644 index 000000000..68c039db8 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/README.md @@ -0,0 +1,2 @@ + +Files extracted from https://github.com/openconfig/public diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-alarm-types.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-alarm-types.yang new file mode 100644 index 000000000..fb7934b1e --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-alarm-types.yang @@ -0,0 +1,109 @@ +module openconfig-alarm-types { + + yang-version "1"; + + // namespace + namespace "http://openconfig.net/yang/alarms/types"; + + prefix "oc-alarm-types"; + + // import some basic types + import openconfig-extensions { prefix oc-ext; } + + // meta + organization "OpenConfig working group"; + + contact + "OpenConfig working group + www.openconfig.net"; + + description + "This module defines operational state data related to alarms + that the device is reporting. + + This model reuses some data items defined in the draft IETF + YANG Alarm Module: + https://tools.ietf.org/html/draft-vallin-netmod-alarm-module-02 + + Portions of this code were derived from the draft IETF YANG Alarm + Module. Please reproduce this note if possible. + + IETF code is subject to the following copyright and license: + Copyright (c) IETF Trust and the persons identified as authors of + the code. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, is permitted pursuant to, and subject to the license + terms contained in, the Simplified BSD License set forth in + Section 4.c of the IETF Trust's Legal Provisions Relating + to IETF Documents (http://trustee.ietf.org/license-info)."; + + oc-ext:openconfig-version "0.2.1"; + + // OpenConfig specific extensions for module metadata. + oc-ext:regexp-posix; + oc-ext:catalog-organization "openconfig"; + oc-ext:origin "openconfig"; + + // identity statements + + identity OPENCONFIG_ALARM_SEVERITY { + description + "Base identity for alarm severity profiles. Derived + identities are based on contents of the draft + IETF YANG Alarm Module"; + reference + "IETF YANG Alarm Module: Draft - typedef severity + https://tools.ietf.org/html/draft-vallin-netmod-alarm-module-02"; + + } + + identity UNKNOWN { + base OPENCONFIG_ALARM_SEVERITY; + description + "Indicates that the severity level could not be determined. + This level SHOULD be avoided."; + } + + identity MINOR { + base OPENCONFIG_ALARM_SEVERITY; + description + "Indicates the existence of a non-service affecting fault + condition and that corrective action should be taken in + order to prevent a more serious (for example, service + affecting) fault. Such a severity can be reported, for + example, when the detected alarm condition is not currently + degrading the capacity of the resource"; + } + + identity WARNING { + base OPENCONFIG_ALARM_SEVERITY; + description + "Indicates the detection of a potential or impending service + affecting fault, before any significant effects have been felt. + Action should be taken to further diagnose (if necessary) and + correct the problem in order to prevent it from becoming a more + serious service affecting fault."; + } + + identity MAJOR { + base OPENCONFIG_ALARM_SEVERITY; + description + "Indicates that a service affecting condition has developed + and an urgent corrective action is required. Such a severity + can be reported, for example, when there is a severe + degradation in the capability of the resource and its full + capability must be restored."; + } + + identity CRITICAL { + base OPENCONFIG_ALARM_SEVERITY; + description + "Indicates that a service affecting condition has occurred + and an immediate corrective action is required. Such a + severity can be reported, for example, when a resource becomes + totally out of service and its capability must be restored."; + } + +} \ No newline at end of file diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-extensions.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-extensions.yang new file mode 100644 index 000000000..c0e472c40 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-extensions.yang @@ -0,0 +1,110 @@ +module openconfig-extensions { + + yang-version "1"; + + // namespace + namespace "http://openconfig.net/yang/openconfig-ext"; + + prefix "oc-ext"; + + // meta + organization "OpenConfig working group"; + + contact + "OpenConfig working group + www.openconfig.net"; + + description + "This module provides extensions to the YANG language to allow + OpenConfig specific functionality and meta-data to be defined."; + + oc-ext:openconfig-version "0.5.1"; + + // extension statements + extension openconfig-version { + argument "semver" { + yin-element false; + } + description + "The OpenConfig version number for the module. This is + expressed as a semantic version number of the form: + x.y.z + where: + * x corresponds to the major version, + * y corresponds to a minor version, + * z corresponds to a patch version. + This version corresponds to the model file within which it is + defined, and does not cover the whole set of OpenConfig models. + + Individual YANG modules are versioned independently -- the + semantic version is generally incremented only when there is a + change in the corresponding file. Submodules should always + have the same semantic version as their parent modules. + + A major version number of 0 indicates that this model is still + in development (whether within OpenConfig or with industry + partners), and is potentially subject to change. + + Following a release of major version 1, all modules will + increment major revision number where backwards incompatible + changes to the model are made. + + The minor version is changed when features are added to the + model that do not impact current clients use of the model. + + The patch-level version is incremented when non-feature changes + (such as bugfixes or clarifications to human-readable + descriptions that do not impact model functionality) are made + that maintain backwards compatibility. + + The version number is stored in the module meta-data."; + } + + extension regexp-posix { + description + "This extension indicates that the regular expressions included + within the YANG module specified are conformant with the POSIX + regular expression format rather than the W3C standard that is + specified by RFC6020 and RFC7950."; + } + + extension operational { + description + "The operational annotation is specified in the context of a + grouping, leaf, or leaf-list within a YANG module. It indicates + that the nodes within the context are derived state on the device. + + OpenConfig data models divide nodes into the following three categories: + + - intended configuration - these are leaves within a container named + 'config', and are the writable configuration of a target. + - applied configuration - these are leaves within a container named + 'state' and are the currently running value of the intended configuration. + - derived state - these are the values within the 'state' container which + are not part of the applied configuration of the device. Typically, they + represent state values reflecting underlying operational counters, or + protocol statuses."; + } + + extension catalog-organization { + argument "org" { + yin-element false; + } + description + "This extension specifies the organization name that should be used within + the module catalogue on the device for the specified YANG module. It stores + a pithy string where the YANG organization statement may contain more + details."; + } + + extension origin { + argument "origin" { + yin-element false; + } + description + "This extension specifies the name of the origin that the YANG module + falls within. This allows multiple overlapping schema trees to be used + on a single network element without requiring module based prefixing + of paths."; + } +} diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-psu.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-psu.yang new file mode 100644 index 000000000..a0fbff41f --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-psu.yang @@ -0,0 +1,92 @@ +module openconfig-platform-psu { + + yang-version "1"; + + // namespace + namespace "http://openconfig.net/yang/platform/psu"; + + prefix "oc-platform-psu"; + + // import some basic types + import openconfig-extensions { prefix oc-ext; } + import openconfig-types { prefix oc-types; } + + + // meta + organization "OpenConfig working group"; + + contact + "OpenConfig working group + www.openconfig.net"; + + description + "This module defines a schema for power supply components in + the OpenConfig platform model."; + + oc-ext:openconfig-version "0.2.1"; + + // OpenConfig specific extensions for module metadata. + oc-ext:regexp-posix; + oc-ext:catalog-organization "openconfig"; + oc-ext:origin "openconfig"; + + grouping psu-config { + description + "Configuration data for power supply components"; + + leaf enabled { + type boolean; + default true; + description + "Adminsitrative control on the on/off state of the power + supply unit."; + } + } + + grouping psu-state { + description + "Operational state data for power supply components"; + + leaf capacity { + type oc-types:ieeefloat32; + units watts; + description + "Maximum power capacity of the power supply."; + } + + leaf input-current { + type oc-types:ieeefloat32; + units amps; + description + "The input current draw of the power supply."; + } + + leaf input-voltage { + type oc-types:ieeefloat32; + units volts; + description + "Input voltage to the power supply."; + } + + leaf output-current { + type oc-types:ieeefloat32; + units amps; + description + "The output current supplied by the power supply."; + } + + leaf output-voltage { + type oc-types:ieeefloat32; + units volts; + description + "Output voltage supplied by the power supply."; + } + + leaf output-power { + type oc-types:ieeefloat32; + units watts; + description + "Output power supplied by the power supply."; + } + } +} \ No newline at end of file diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-types.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-types.yang new file mode 100644 index 000000000..2d917e3f7 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform-types.yang @@ -0,0 +1,80 @@ +module openconfig-platform-types { + + yang-version "1"; + + // namespace + namespace "http://openconfig.net/yang/platform-types"; + + prefix "oc-platform-types"; + + import openconfig-types { prefix oc-types; } + import openconfig-extensions { prefix oc-ext; } + + // meta + organization + "OpenConfig working group"; + + contact + "OpenConfig working group + www.openconfig.net"; + + description + "This module defines data types (e.g., YANG identities) + to support the OpenConfig component inventory model."; + + oc-ext:openconfig-version "1.6.0"; + + // OpenConfig specific extensions for module metadata. + oc-ext:regexp-posix; + oc-ext:catalog-organization "openconfig"; + oc-ext:origin "openconfig"; + + // grouping statements + grouping avg-min-max-instant-stats-precision1-celsius { + description + "Common grouping for recording temperature values in + Celsius with 1 decimal precision. Values include the + instantaneous, average, minimum, and maximum statistics"; + + leaf instant { + type decimal64 { + fraction-digits 1; + } + units celsius; + description + "The instantaneous value of the statistic."; + } + + leaf avg { + type decimal64 { + fraction-digits 1; + } + units celsius; + description + "The arithmetic mean value of the statistic over the + sampling period."; + } + + leaf min { + type decimal64 { + fraction-digits 1; + } + units celsius; + description + "The minimum value of the statistic over the sampling + period"; + } + + leaf max { + type decimal64 { + fraction-digits 1; + } + units celsius; + description + "The maximum value of the statistic over the sampling + period"; + } + + uses oc-types:stat-interval-state; + } +} diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform.yang new file mode 100644 index 000000000..2307bf8fa --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-platform.yang @@ -0,0 +1,162 @@ +module openconfig-platform { + + yang-version "1"; + + // namespace + namespace "http://openconfig.net/yang/platform"; + + prefix "oc-platform"; + + import openconfig-platform-types { prefix oc-platform-types; } + import openconfig-extensions { prefix oc-ext; } + import openconfig-alarm-types { prefix oc-alarm-types; } + + // meta + organization "OpenConfig working group"; + + contact + "OpenConfig working group + www.openconfig.net"; + + description + "This module defines a data model for representing a system + component inventory, which can include hardware or software + elements arranged in an arbitrary structure. The primary + relationship supported by the model is containment, e.g., + components containing subcomponents. + + It is expected that this model reflects every field replacable + unit on the device at a minimum (i.e., additional information + may be supplied about non-replacable components). + + Every element in the inventory is termed a 'component' with each + component expected to have a unique name and type, and optionally + a unique system-assigned identifier and FRU number. The + uniqueness is guaranteed by the system within the device. + + Components may have properties defined by the system that are + modeled as a list of key-value pairs. These may or may not be + user-configurable. The model provides a flag for the system + to optionally indicate which properties are user configurable. + + Each component also has a list of 'subcomponents' which are + references to other components. Appearance in a list of + subcomponents indicates a containment relationship as described + above. For example, a linecard component may have a list of + references to port components that reside on the linecard. + + This schema is generic to allow devices to express their own + platform-specific structure. It may be augmented by additional + component type-specific schemas that provide a common structure + for well-known component types. In these cases, the system is + expected to populate the common component schema, and may + optionally also represent the component and its properties in the + generic structure. + + The properties for each component may include dynamic values, + e.g., in the 'state' part of the schema. For example, a CPU + component may report its utilization, temperature, or other + physical properties. The intent is to capture all platform- + specific physical data in one location, including inventory + (presence or absence of a component) and state (physical + attributes or status)."; + + oc-ext:openconfig-version "0.24.0"; + + // OpenConfig specific extensions for module metadata. + oc-ext:regexp-posix; + oc-ext:catalog-organization "openconfig"; + oc-ext:origin "openconfig"; + + // grouping statements + + grouping platform-component-temp-alarm-state { + description + "Temperature alarm data for platform components"; + + // TODO(aashaikh): consider if these leaves could be in a + // reusable grouping (not temperature-specific); threshold + // may always need to be units specific. + + leaf alarm-status { + type boolean; + description + "A value of true indicates the alarm has been raised or + asserted. The value should be false when the alarm is + cleared."; + } + + leaf alarm-threshold { + type uint32; + description + "The threshold value that was crossed for this alarm."; + } + + leaf alarm-severity { + type identityref { + base oc-alarm-types:OPENCONFIG_ALARM_SEVERITY; + } + description + "The severity of the current alarm."; + } + } + + grouping platform-component-temp-state { + description + "Temperature state data for device components"; + + container temperature { + description + "Temperature in degrees Celsius of the component. Values include + the instantaneous, average, minimum, and maximum statistics. If + avg/min/max statistics are not supported, the target is expected + to just supply the instant value"; + + uses oc-platform-types:avg-min-max-instant-stats-precision1-celsius; + uses platform-component-temp-alarm-state; + } + } + + grouping platform-component-top { + description + "Top-level grouping for components in the device inventory"; + + container components { + description + "Enclosing container for the components in the system."; + + list component { + key "name"; + description + "List of components, keyed by component name."; + + leaf name { + type leafref { + path "../config/name"; + } + description + "References the component name"; + } + + container state { + config false; + + description + "Operational state data for each component"; + + uses platform-component-temp-state; + } + } + } + } + + + // data definition statements + + uses platform-component-top; + + + // augments + + +} diff --git a/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-types.yang b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-types.yang new file mode 100644 index 000000000..974688011 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/models/openconfig-types.yang @@ -0,0 +1,66 @@ +module openconfig-types { + yang-version "1"; + + namespace "http://openconfig.net/yang/openconfig-types"; + + prefix "oc-types"; + + // import statements + import openconfig-extensions { prefix oc-ext; } + + // meta + organization + "OpenConfig working group"; + + contact + "OpenConfig working group + netopenconfig@googlegroups.com"; + + description + "This module contains a set of general type definitions that + are used across OpenConfig models. It can be imported by modules + that make use of these types."; + + oc-ext:openconfig-version "1.0.0"; + + // OpenConfig specific extensions for module metadata. + oc-ext:regexp-posix; + oc-ext:catalog-organization "openconfig"; + oc-ext:origin "openconfig"; + + typedef stat-interval { + type uint64; + units nanoseconds; + description + "A time interval over which a set of statistics is computed. + A common usage is to report the interval over which + avg/min/max stats are computed and reported."; + } + + grouping stat-interval-state { + description + "Reusable leaf definition for stats computation interval"; + + leaf interval { + type oc-types:stat-interval; + description + "If supported by the system, this reports the time interval + over which the min/max/average statistics are computed by + the system."; + } + } + + typedef ieeefloat32 { + type binary { + length "4"; + } + description + "An IEEE 32-bit floating point number. The format of this number + is of the form: + 1-bit sign + 8-bit exponent + 23-bit fraction + The floating point value is calculated using: + (-1)**S * 2**(Exponent-127) * (1+Fraction)"; + } +} diff --git a/plugins/inputs/gnmi/testcases/issue_15046/responses.json b/plugins/inputs/gnmi/testcases/issue_15046/responses.json new file mode 100644 index 000000000..5da12da49 --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/responses.json @@ -0,0 +1,141 @@ +[ + { + "update": { + "timestamp": "1711178737105194000", + "update": [ + { + "path": { + "origin": "openconfig", + "elem": [ + { + "name": "components" + }, + { + "name": "component", + "key": { + "name": "PowerSupply1/A" + } + }, + { + "name": "power-supply" + }, + { + "name": "state" + } + ] + }, + "val": { + "jsonIetfVal": "eyJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTplbmFibGVkIjp0cnVlLCJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTpjYXBhY2l0eSI6IlJETEFBQT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6aW5wdXQtY3VycmVudCI6IlB2RW02UT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6aW5wdXQtdm9sdGFnZSI6IlExQ0FBQT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6b3V0cHV0LWN1cnJlbnQiOiJQNW43NXc9PSIsIm9wZW5jb25maWctcGxhdGZvcm0tcHN1Om91dHB1dC12b2x0YWdlIjoiUW1GM3p3PT0iLCJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTpvdXRwdXQtcG93ZXIiOiJRb2xBQUE9PSJ9" + } + }, + { + "path": { + "origin": "openconfig", + "elem": [ + { + "name": "components" + }, + { + "name": "component", + "key": { + "name": "PowerSupply1/B" + } + }, + { + "name": "power-supply" + }, + { + "name": "state" + } + ] + }, + "val": { + "jsonIetfVal": "eyJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTplbmFibGVkIjp0cnVlLCJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTpjYXBhY2l0eSI6IlJETEFBQT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6aW5wdXQtY3VycmVudCI6IlBzazNUQT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6aW5wdXQtdm9sdGFnZSI6IlExSEFBQT09Iiwib3BlbmNvbmZpZy1wbGF0Zm9ybS1wc3U6b3V0cHV0LWN1cnJlbnQiOiJQM0FneFE9PSIsIm9wZW5jb25maWctcGxhdGZvcm0tcHN1Om91dHB1dC12b2x0YWdlIjoiUW1GM3p3PT0iLCJvcGVuY29uZmlnLXBsYXRmb3JtLXBzdTpvdXRwdXQtcG93ZXIiOiJRaytBQUE9PSJ9" + } + } + ] + } + }, + { + "update": { + "timestamp": "1715838159171548000", + "update": [ + { + "path": { + "origin": "openconfig", + "elem": [ + { + "name": "components" + }, + { + "name": "component", + "key": { + "name": "InletTempSensor1" + } + }, + { + "name": "state" + }, + { + "name": "temperature" + } + ] + }, + "val": { + "jsonIetfVal": "eyJpbnN0YW50IjoiMzUuMDAwMDAwIiwiYXZnIjoiMjQuMDAwMDAwIiwibWluIjoiMC4wMDAwMDAiLCJtYXgiOiIzNi4wMDAwMDAiLCJpbnRlcnZhbCI6IjE4MDAwMDAwMDAwMCIsImFsYXJtLXN0YXR1cyI6ZmFsc2UsImFsYXJtLXRocmVzaG9sZCI6MCwiYWxhcm0tc2V2ZXJpdHkiOiJvcGVuY29uZmlnLWFsYXJtLXR5cGVzOk1JTk9SIn0=" + } + }, + { + "path": { + "origin": "openconfig", + "elem": [ + { + "name": "components" + }, + { + "name": "component", + "key": { + "name": "OutletTempSensor1" + } + }, + { + "name": "state" + }, + { + "name": "temperature" + } + ] + }, + "val": { + "jsonIetfVal": "eyJpbnN0YW50IjoiNDQuMDAwMDAwIiwiYXZnIjoiMjkuMDAwMDAwIiwibWluIjoiMC4wMDAwMDAiLCJtYXgiOiI0NC4wMDAwMDAiLCJpbnRlcnZhbCI6IjE4MDAwMDAwMDAwMCIsImFsYXJtLXN0YXR1cyI6ZmFsc2UsImFsYXJtLXRocmVzaG9sZCI6MCwiYWxhcm0tc2V2ZXJpdHkiOiJvcGVuY29uZmlnLWFsYXJtLXR5cGVzOk1JTk9SIn0=" + } + }, + { + "path": { + "origin": "openconfig", + "elem": [ + { + "name": "components" + }, + { + "name": "component", + "key": { + "name": "HotSpotTempSensor1" + } + }, + { + "name": "state" + }, + { + "name": "temperature" + } + ] + }, + "val": { + "jsonIetfVal": "eyJpbnN0YW50IjoiNTguMDAwMDAwIiwiYXZnIjoiMzkuMDAwMDAwIiwibWluIjoiMC4wMDAwMDAiLCJtYXgiOiI1OS4wMDAwMDAiLCJpbnRlcnZhbCI6IjE4MDAwMDAwMDAwMCIsImFsYXJtLXN0YXR1cyI6ZmFsc2UsImFsYXJtLXRocmVzaG9sZCI6MCwiYWxhcm0tc2V2ZXJpdHkiOiJvcGVuY29uZmlnLWFsYXJtLXR5cGVzOk1JTk9SIn0=" + } + } + ] + } + } +] \ No newline at end of file diff --git a/plugins/inputs/gnmi/testcases/issue_15046/telegraf.conf b/plugins/inputs/gnmi/testcases/issue_15046/telegraf.conf new file mode 100644 index 000000000..64ca8736a --- /dev/null +++ b/plugins/inputs/gnmi/testcases/issue_15046/telegraf.conf @@ -0,0 +1,18 @@ +[[inputs.gnmi]] + addresses = ["dummy"] + path_guessing_strategy = "subscription" + yang_model_paths = ["testcases/issue_15046/models"] + + [[inputs.gnmi.subscription]] + name = "psu" + origin = "openconfig" + path = "/components/component/power-supply/state" + subscription_mode = "sample" + sample_interval = "60s" + + [[inputs.gnmi.subscription]] + name = "temp" + origin = "openconfig" + path = "/components/component/state/temperature" + subscription_mode = "sample" + sample_interval = "60s" diff --git a/plugins/inputs/gnmi/update_fields.go b/plugins/inputs/gnmi/update_fields.go index 36e35ac33..a4298b6d8 100644 --- a/plugins/inputs/gnmi/update_fields.go +++ b/plugins/inputs/gnmi/update_fields.go @@ -4,17 +4,23 @@ import ( "encoding/json" "fmt" "strconv" + "strings" gnmiLib "github.com/openconfig/gnmi/proto/gnmi" gnmiValue "github.com/openconfig/gnmi/value" ) +type keyValuePair struct { + key []string + value interface{} +} + type updateField struct { path *pathInfo value interface{} } -func newFieldsFromUpdate(path *pathInfo, update *gnmiLib.Update) ([]updateField, error) { +func (h *handler) newFieldsFromUpdate(path *pathInfo, update *gnmiLib.Update) ([]updateField, error) { if update.Val == nil || update.Val.Value == nil { return []updateField{{path: path}}, nil } @@ -26,7 +32,7 @@ func newFieldsFromUpdate(path *pathInfo, update *gnmiLib.Update) ([]updateField, case *gnmiLib.TypedValue_JsonVal: // requires special path handling return processJSON(path, v.JsonVal) case *gnmiLib.TypedValue_JsonIetfVal: // requires special path handling - return processJSON(path, v.JsonIetfVal) + return h.processJSONIETF(path, v.JsonIetfVal) } // Convert the protobuf "oneof" data to a Golang type. @@ -48,45 +54,105 @@ func processJSON(path *pathInfo, data []byte) ([]updateField, error) { // Create an update-field with the complete path for all entries fields := make([]updateField, 0, len(entries)) - for key, v := range entries { + for _, entry := range entries { fields = append(fields, updateField{ - path: path.appendSegments(key), - value: v, + path: path.appendSegments(entry.key...), + value: entry.value, }) } return fields, nil } -func flatten(nested interface{}) map[string]interface{} { - fields := make(map[string]interface{}) +func (h *handler) processJSONIETF(path *pathInfo, data []byte) ([]updateField, error) { + var nested interface{} + if err := json.Unmarshal(data, &nested); err != nil { + return nil, fmt.Errorf("failed to parse JSON value: %w", err) + } + + // Flatten the JSON data to get a key-value map + entries := flatten(nested) + + // Lookup the data in the YANG model if any + if h.decoder != nil { + for i, e := range entries { + var namespace, identifier string + for _, k := range e.key { + if n, _, found := strings.Cut(k, ":"); found { + namespace = n + } + } + + // IETF nodes referencing YANG entries require a namespace + if namespace == "" { + continue + } + + if a, b, found := strings.Cut(e.key[len(e.key)-1], ":"); !found { + identifier = a + } else { + identifier = b + } + + if decoded, err := h.decoder.DecodeLeafElement(namespace, identifier, e.value); err != nil { + h.log.Debugf("Decoding %s:%s failed: %v", namespace, identifier, err) + } else { + entries[i].value = decoded + } + } + } + + fields := make([]updateField, 0, len(entries)) + for _, entry := range entries { + p := path.appendSegments(entry.key...) + + // Try to lookup the full path to decode the field according to the + // YANG model if any + if h.decoder != nil { + origin, fieldPath := p.Path() + if decoded, err := h.decoder.DecodePathElement(origin, fieldPath, entry.value); err != nil { + h.log.Debugf("Decoding %s failed: %v", p, err) + } else { + entry.value = decoded + } + } + + // Create an update-field with the complete path for all entries + fields = append(fields, updateField{ + path: p, + value: entry.value, + }) + } + + return fields, nil +} + +func flatten(nested interface{}) []keyValuePair { + var values []keyValuePair switch n := nested.(type) { case map[string]interface{}: for k, child := range n { - for ck, cv := range flatten(child) { - key := k - if ck != "" { - key += "/" + ck - } - fields[key] = cv + for _, c := range flatten(child) { + values = append(values, keyValuePair{ + key: append([]string{k}, c.key...), + value: c.value, + }) } } case []interface{}: for i, child := range n { k := strconv.Itoa(i) - for ck, cv := range flatten(child) { - key := k - if ck != "" { - key += "/" + ck - } - fields[key] = cv + for _, c := range flatten(child) { + values = append(values, keyValuePair{ + key: append([]string{k}, c.key...), + value: c.value, + }) } } - case nil: - return nil default: - return map[string]interface{}{"": nested} + values = append(values, keyValuePair{value: n}) } - return fields + + return values }