A YANG-centric Go toolkit - Go/Protobuf Code Generation; Validation; Marshaling/Unmarshaling
ygot (YANG Go Tools) is a collection of Go utilities that can be used to:
Whilst ygot is designed to work with any YANG module, for OpenConfig modules, it can provide transformations of the schema to optimise the data structures that are produced for use in systems that generate data instances of the models for configuration purposes. These helper methods require that the OpenConfig style guide patterns are implemented, a model can be verified to conform with these requirements using the OpenConfig linter.
Note: This is not an official Google product.
Current support for
ygotis for the latest 3 Go releases.
ygotconsists of a number of parts,
generatorwhich is a binary using the
ygenlibrary to generate Go code from a set of YANG modules.
ygotwhich provides helper methods for the
ygen-produced structs - for example, rendering to JSON, or gNMI notifications - and
ytypeswhich provides validation of the contents of
ygenstructs against the YANG schema.
The basic workflow for working with
ygotis as follows:
The
demo/getting_starteddirectory walks through this process for a simple implementation of
openconfig-interfaces.
The generator binary takes a set of YANG modules as input and outputs generated code. For example:
generator -output_file= -package_name= [yangfiles]
Will output generated Go code for
yangfiles(a space separated list of YANG files) to a file at with the Go package named .
Most YANG modules include other modules. If these included modules are not within the current working directory, the
pathargument is used. The argument to
pathis a comma-separated list of directories which will be recursively searched for included files.
By default,
ygotdoes not output an entity for the root of the schema tree - such that there is not a root entity to consider in code. If one is desired then it can be produced by using the
generate_fakerootargument. If specified an element with the name specified by
fakeroot_namewill be created in the output code. By default the fake root element is called
device, since the root is often considered to be a device within the OpenConfig use case.
If schema transformations for OpenConfig are desired, these are enabled using the
compress_pathsargument.
Putting this all together, a command line to generate OpenConfig interfaces from the contents of the
demo/getting_started/yangdirectory is:
go run $GOPATH/src/github.com/openconfig/ygot/generator/generator.go -path=yang -output_file=pkg/ocdemo/oc.go -package_name=ocdemo -generate_fakeroot -fakeroot_name=device -compress_paths=true -shorten_enum_leaf_names -typedef_enum_with_defmod -exclude_modules=ietf-interfaces yang/openconfig-interfaces.yang
To allow this file to be auto-created, you can place a command which allows this code generation to be done automatically, either by creating a file within the YANG directory, or directly embedding this command within the source file that populates the structures. For an example, see the
demo/getting_started/main.gofile which includes:
//go:generate go run ../../generator/generator.go -path=yang -output_file=pkg/ocdemo/oc.go -package_name=ocdemo -generate_fakeroot -fakeroot_name=device -compress_paths=true -shorten_enum_leaf_names -typedef_enum_with_defmod -exclude_modules=ietf-interfaces yang/openconfig-interfaces.yang
This means that we can simply type
go generatewithin
demo/getting_started- and the
demo/getting_started/pkg/ocdemo/oc.gois created with the code bindings for the OpenConfig interfaces module.
Once we have generated the Go bindings for the YANG module, we're ready to use them in an application.
First, let's take a look at what the
demo/getting_started/pkg/ocdemo/oc.gofile contains. Particularly, looking at the fake root entity that we created (named device):
// Device represents the /device YANG schema element. type Device struct { Interface map[string]*Interface `path:"interfaces/interface" rootname:"interface" module:"openconfig-interfaces"` }
Since we enabled
compress_paths, then the
/interfaces/interfaceelement in OpenConfig was represented as
Interfaceat the root (called
Device). We can see that since
interfaceis a list, keyed by the
nameelement, then the
Interfacemap is keyed by a string.
Looking further down the tree at
Interface:
// Interface represents the /openconfig-interfaces/interfaces/interface YANG schema element. type Interface struct { AdminStatus E_OpenconfigInterfaces_Interface_AdminStatus `path:"state/admin-status" module:"openconfig-interfaces"` Counters *Interface_Counters `path:"state/counters" module:"openconfig-interfaces"` Description *string `path:"config/description" module:"openconfig-interfaces"` Enabled *bool `path:"config/enabled" module:"openconfig-interfaces"` HoldTime *Interface_HoldTime `path:"hold-time" module:"openconfig-interfaces"` Ifindex *uint32 `path:"state/ifindex" module:"openconfig-interfaces"` LastChange *uint32 `path:"state/last-change" module:"openconfig-interfaces"` Mtu *uint16 `path:"config/mtu" module:"openconfig-interfaces"` Name *string `path:"config/name|name" module:"openconfig-interfaces"` OperStatus E_OpenconfigInterfaces_Interface_AdminStatus `path:"state/oper-status" module:"openconfig-interfaces"` Subinterface map[uint32]*Interface_Subinterface `path:"subinterfaces/subinterface" module:"openconfig-interfaces"` Type E_IETFInterfaces_InterfaceType `path:"config/type" module:"openconfig-interfaces"` }
Since OpenConfig path compression was enabled, then this
Interfacestruct contains both direct descendants of
/interfaces/interface- such as
hold-time(in the
Hold-Timefield), along with those that were within the
configand
statefields. The path information is retained in the
pathstruct tag -- but this isn't of interest to most developers working directly with the structs!
We can populate an interface by using a mixture of the helper methods, and directly setting fields of the struct. To create a new interface within the device, we can use the
NewInterfacemethod. A
New...method is created for all lists within the YANG schema, and takes an argument of the key that is used for the list. It creates a new entry in the map with the specified key, returning an error if the key is already defined.
An example is shown below:
// Create a new interface called "eth0" i, err := d.NewInterface("eth0")// Set the fields that are within the struct. i.AdminStatus = oc.OpenconfigInterfaces_Interface_AdminStatus_UP i.Mtu = ygot.Uint16(1500) i.Description = ygot.String("An Interface")
The
ygotpackage provides helpers that allow an input type to returned as a pointer to be populated within the structs. For example,
ygot.Stringreturns a string pointer to the argument supplied.
Equally, we can define a new interface directly and add it to the map, without using the
NewInterfacemethod:
d.Interface["eth1"] = &oc.Interface{ Name: ygot.String("eth1"), Description: ygot.String("Another Interface"), Enabled: ygot.Bool(false), Type: oc.IETFInterfaces_InterfaceType_ethernetCsmacd, }
For some fields of the structures, enumerated values for example, values of fields are restricted such that they cannot have invalid values specified. In other cases, such as an IPv4 addresses, a string may not match a regular expression, but the Go structure does not restrict the contents of the struct being populated with this data.
By default each struct has a
Validatemethod, this can be used to validate the struct's contents against the schema.
Validatecan be called against each structure, for example:
if err := d.Interface["eth0"].Validate(); err != nil { panic(fmt.Sprintf("Interface validation failed: %v", err)) }
In the case that the struct does not contain valid contents,
Validatereturns an error, containing a list of errors encountered during validation of the struct contents. Whilst the error can be directly handled as a comma-separated list of strings containing validation errors, casting it to the
ytypes.Errorstype allows handling of individual errors more cleanly. For example:
_, err = subif.Ipv4.NewAddress("Not a valid address") if err := invalidIf.Validate(); err == nil { panic(fmt.Sprintf("Did not find invalid address, got nil err: %v", err)) } else { errs := err.(ytypes.Errors) for _, err := range errs { fmt.Printf("Got expected error: %v\n", err) } }
To serialise the structures to JSON, the
ygotpackage provides an
EmitJSONmethod which can be called with an arbitrary structure. In the example below, the fake root (
Device) struct is called:
json, err := ygot.EmitJSON(d, &ygot.EmitJSONConfig{ Format: ygot.RFC7951, Indent: " ", RFC7951Config: &ygot.RFC7951JSONConfig{ AppendModuleName: true, }, })if err != nil { panic(fmt.Sprintf("JSON demo error: %v", err)) } fmt.Println(json)
EmitJSONperforms both
Validateand outputs the structure to JSON. The format can be an internal JSON format, or that described by RFC7951. Validation or JSON marshalling errors are directly returned.
ygot includes a function to unmarshal data from RFC7951-encoded JSON to a GoStruct. Since this function relies on the schema of the generated code, it us output within the generated code package - and named
Unmarshal. The function takes an argument of a
[]byte(byte slice) containing the JSON document to be unmarshalled, and a pointer to the struct into which it should be unmarshalled. Any struct can be unmarshalled into. If data cannot be unmarshalled, an error is returned.
To unmarshal the example created in this guide, we call
Unmarshalwith the
oc.Devicestruct pointer, and the JSON document:
// Device struct to unmarshal into. loadd := &oc.Device{} if err := oc.Unmarshal([]byte(json), loadd); err != nil { panic(fmt.Sprintf("Cannot unmarshal JSON: %v", err)) }
Currently, only the
RFC7951format of JSON is supported for unmarshalling, the
Internalformat supported by ygot is not yet supported.
Copyright 2017 Google Inc.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.