openconfig / ygot

A YANG-centric Go toolkit - Go/Protobuf Code Generation; Validation; Marshaling/Unmarshaling
Apache License 2.0
284 stars 106 forks source link

Device Returns Array for Each Interface #958

Open achurak opened 6 months ago

achurak commented 6 months ago

I apologize in advance if it's something obvious and I'm missing it but I'm trying to do what I think is as basic as can be, simply pull information about interfaces from a cisco device using gnmi client, however I just can't unmarshal it correctly no matter what I try. It looks like the json that the cisco device returns is an array, something like this:

json_val: "[{\"name\":\"eth1/31\",\"config\":{\"enabled\":true,\"mtu\":1500,\"name\":\"eth1/31\",\"type\":\"ethernetCsmacd\",\"tpid\":\"TPID_0X8100\"},\"hold-time\":{\"config\":{\"down\":100}},\"subinterfaces\":{\"subinterface\":[{\"index\":0,\"config\":{\"index\":0}}]},\"ethernet\":{\"config\":{\"auto-negotiate\":true,\"enable-flow-control\":false,\"mac-address\":\"00:00:00:00:00:00\"},\"switched-vlan\":{\"config\":{\"access-vlan\":1,\"interface-mode\":\"ACCESS\",\"native-vlan\":1}}}}]"

but the generated model expects it to be a map keyed by the interface name.

Failed to unmarshal update: unmarshalContainer for schema device: jsonTree [ map[config:map[enabled:true mtu:1500 name:eth1/31 tpid:TPID_0X8100 type:ethernetCsmacd] ethernet:map[config:map[auto-negotiate:true enable-flow-cont...: got type []interface {} inside container, expect map[string]interface{}

I can even remove the [] from the string and the unmarshaling works after that (though it doesn't recognize ethernetCsmacd as a correct value for interface type but it's a different problem), but I feel like it's a super hacky workaround and there's gotta be a better/correct way of handling this.

wenovus commented 5 months ago

Hi achurak, apologies for the delayed response.

I believe you queried /interfaces/interface from the device, which correctly returns a JSON list.

However, ygot doesn't support unmarshalling directly at the list level -- you must either 1) specify the list key interfaces/interface[name=foo], and unmarshal into the list struct, or 2) query at /interfaces level, and unmarshal at that level. Note that this option is only available if compress=false is used. If compress=true is used then you must wrap the json_val yourself with another JSON object, and unmarshal into the struct one-level higher, which in this case is root-level struct. This is highly inconvenient, and I've opened https://github.com/openconfig/ygot/issues/959 to track it.

achurak commented 5 months ago

@wenovus No worries, thank you for getting back to me! Correct, I queried /interfaces/interface.

  1. Do I understand correctly that the first way will only query a single interface instead of a list of all interfaces? I believe I've actually tried that and Cisco still returns an array but with a single interface/object inside it.
  2. Do I need to unmarshal it into a generic, wrap it and then unmarshal into the ygot struct?
wenovus commented 5 months ago
  1. Looking more, I think what Cisco has might be reasonable (as this is ambiguous in the gNMI spec), since in general it's possible to specify wildcards in queries, so it would be confusing if in the case that all list attributes are specified, a JSON object is returned rather than a list. One way to work around this is to loop through each JSON list element, unmarshal separately, and then call device.AppendInterface to add them one at a time.
  2. You can json.Unmarshal, wrap it inside a map[string]any keyed by "interfaces" per the YANG container name, and then json.Marshal again, and then unmarshal into the ygot struct. However I think 1. is much easier and less hacky.