This document describes how YANG modeled data is mapped into Go code entities, by the ygen library.
YANG (RFC6020) is a data modelling language, that is used to describe a schema. ygen is primarily developed to meet the use case of allowing Gophers to interact with the OpenConfig data models.
In order to make a YANG schema useful, some means to instantiate data trees
which correspond to the schema is required. The ygen
library uses the
goyang
to parse a YANG model, and
extract the AST corresponding to the model; ygen
takes such a parsed tree
and outputs a set of Go structs and enumerations that correspond to nodes in the
schema.
OpenConfig YANG models correspond to a specific hierarchy; which is designed to allow machine-to-machine interaction as well as for human consumption. This leads to additional levels of hierarchy being introduced to the model, particularly:
- Data values (
leaf
nodes) are mirrored in aconfig
andstate
container - such that it is possible for a system to determine the intended configuration for aleaf
, along with the applied configuration. The former is stored as a read-writeleaf
within acontainer
which is namedconfig
, whilst the latter is reflected by aleaf
of the same name in acontainer
namedstate
at the same level of the tree as theconfig
container. - YANG
list
nodes are enclosed in acontainer
, which they are the sole child of. This allows for some systems to provide means to retrieve the entire list, rather than solely the keys when a particular leaf path is queried.
To improve human usability, the ygen
library provides a CompressOCPaths
option (specified in the YANGCodeGenerator
struct's Config
field). When CompressOCPaths
is
set to true
, the following schema transformations are made:
- The
config
andstate
containers are "compressed" out of the schema. - The surrounding
container
entities are removed fromlist
nodes.
This results in a model such as the OpenConfig interfaces model having paths that are shorter, and more human-usable:
/interfaces/interface/subinterfaces/subinterface/ipv4/addresses/address
becomes/interface/subinterface/ipv4/address
./interfaces/interface/subinterfaces/subinterface/config/enabled
becomes/interface/subinterface/enabled
./interfaces/interface/subinterfaces/subinterface/state/oper-status
becomes/interface/subinterface/oper-status
.
With CompressOCPaths
set to true
, the modified forms of the paths are used
whenever the path of an entity is required (e.g., in YANG name generation).
The logic to extract which entities are valid to have code
generation performed for them (skipping config
/state
containers, and
surrounding containers for lists) is found in
go_elements.go
:findAllChildren
.
ygen
creates two types of Go output, a set of structs - corresponding to
containers or list nodes within the schema; and a set of enumerations which
correspond to nodes that have a restricted set of values in the schema. The set
of enumerated values are:
leaf
nodes which have a YANGtype
of enumeration, whether directly or within aunion
.identity
statements in the schema.typedef
statements within the YANG schema which have atype
ofenumeration
, as their sole type, or within anenumeration
.
Each entity is named in the output Go code according to its path in the schema.
The path may be modified using the CompressOCPaths
as described above.
struct
entities are named according to their path, with each path element
being converted to CamelCase and concatenated in the form
PathElementOne_PathElementTwo
- such that /interfaces/interfaces/config
becomes Interfaces_Interface_Config
(if path compression is disabled). In the
case that path compression is enabled, the interface
list becomes Interface
.
Each leaf that is contained under a particular container
is represented by a
member of the struct, with the leaf's name converted to CamelCase.
If an entity has an extension with the name camelcase-name
, this can be used to specify the CamelCase name of the entity explicitly, rather than relying on the the goyang yang.CamelCase
function for naming.
Pointers are used for all scalar field types (non-slice, or map) such that unset (nil
) fields can be distiguished from those that are set to their null value. The ygot
package provides a set of helper methods to return an input value as a pointer - for example, ygot.String("foo")
will return a string pointer suitable for setting a YANG string field.
For example, the following YANG module:
container test {
leaf a { type string; }
leaf b { type uint8; }
leaf-list c { type string; }
}
Will be output as the following Go struct:
type Test struct {
A *string `path:"a"`
B *uint8 `path:"b"`
C []string `path:"c"`
}
All structs that are produced by the ygen
library implement the ygot.GoStruct
interface, such that handling code can determine the provenance of such structures.
For each enumerated entity (described above), an enumerated type in Go is
generated, in a similar fashion to the proto
library. Naming is according to
the type of the enumerated leaf in YANG.
leaf
nodes with a type ofenumeration
are mapped to an enumeration named according to the path of theleaf
. The path specified isModuleName_LeafParentName_LeafName
such that a path of/interfaces/interface/state/enumerated-value
defined within theopenconfig-interfaces
module is represented by an enumerated type namedOpenconfigInterfaces_State_EnumeratedValue
(assuming path compression is disabled), orOpenconfigInterfaces_Interface_EnumeratedValue
when it is enabled.- This mapping is handled by
yang_helpers.go
:resolveEnumName
.
- This mapping is handled by
- Defined
identity
statements are generated only when they are referenced by aleaf
in the schema (i.e., anidentityref
). They are named according to the module that they are defined in, and theidentity
name - i.e.,identity foo
in modulebar-module
is namedBarModule_Foo
. The naming of such identities is not modified whenCompressOCPaths
is enabled.- This mapping is handled by
yang_helpers.go
:resolveIdentityRefBaseType
.
- This mapping is handled by
- Non-builtin types created via a
typedef
statement that contain an enumeration are identified according to the module that they are defined in, and thetypedef
name - i.e.,typedef bar { type enumeration { ... }}
in modulebaz
is represented by an enumerated type namedBar_Baz
.- This mapping is handled by
yang_helpers.go
:resolveTypedefEnumeratedName
.
- This mapping is handled by
Only a single enumeration is generated for a typedef
or identity
-
regardless of the number of times that is referenced throughout the code. This
ensures that the user of the library does not have to be aware of the
enumeration's context when referencing the Go enumerated type. Since typedef
and identity
nodes do not have a path within the YANG schematree, the library
uses the synthesised name module-name/statement-name
as a pseudo-path to
reference each typedef
and identity
such that the name it is mapped to in Go
code can be re-used throughout code generation.
Since in YANG leaf-one
and leaf-One
are considered unique names, during the
process of converting a name to CamelCase, it is possible that two entities are
mapped to the same CamelCase name (LeafOne
in this case). Such cases are
handled by appending underscores to the name of an entity as its name is
converting to CamelCase until such time as the name is unique. i.e., in the case
that leaf-one
and leaf-One
exist within the same container then the first
mapped entity will be named LeafOne
and the second LeafOne_
. A similar
de-duplication technique is utilised for the names of enumerated types
(following the process described above).
It is not expected that with OpenConfig schemas, such name collisions are encountered, although at the time of writing, no OpenConfig linter rule exists to ensure that this is the case.
The following mapping between YANG and Go types are used by the ygen
library:
YANG Type | Go Type | Notes |
---|---|---|
int{8,16,32,64} |
int{8,16,32,64} |
|
uint{8,16,32,64} |
uint{8,16,32,64} |
|
bool |
bool |
|
empty |
bool |
|
string |
string |
|
union |
interface{} |
A union is represented as an empty interface, with validation intending to be done whilst mapping into the //ops/openconfig/lib/go library. |
enumeration |
int64 |
Each enumeration is generated as a new type based on Go's int64, names are assigned to each value of the enumeration akin to the proto library. |
identityref |
int64 |
The identityref's "base" is mapped using the same process as the an enumeration leaf. |
decimal64 |
float64 |
|
binary |
[]byte |
|
bits |
interface{} |
TODO(robjs): Add support for bits , this is low priority as it is not used in any OpenConfig schema. |
YANG Lists are output as map
fields within the Go structures, with a key type that is derived from the YANG schema, for example:
container c {
list foo {
key "fookey";
leaf fookey { type string; }
}
list bar {
key "barkey1 barkey2";
leaf barkey1 { type string; }
leaf barkey2 { type string; }
leaf barmember { type string; }
}
}
Is output as:
type C struct {
Foo map[string]*C_Foo `path:"foo"`
Bar map[C_Bar_Key]*C_Bar `path:"bar"`
}
type C_Foo struct {
FooKey *string `path:"fookey"`
}
type C_Bar_Key struct {
Barkey1 string
Barkey2 string
}
type C_Bar struct {
Barkey1 *string `path:"barkey1"`
Barkey2 *string `path:"barkey2"`
Barmember *string `path:"barmmember"`
}
Such that the Foo
field is a map, keyed on the type of the key leaf (fookey
). For lists with multiple keys, a specific key struct
is generated (C_Bar_Key
in the above example), with fields that correspond to the key fields of the YANG list.
Each YANG list that exists within a container has a helper-method generated for it. For a list named foo
, the parent container (C
) has a NewFoo(fookey string)
method generated, taking a key value as an argument, and returning a new member of the map within the foo
list.
In order to preserve strict type validation at compile time, union
leaves within the YANG schema are mapped to an Go interface
which is subsequently implemented for each type that is defined within the YANG union.
For the following YANG module:
container foo {
container bar {
leaf union-leaf {
type union {
type string;
type int8;
}
}
}
}
the bar
container is mapped to:
type Bar struct {
UnionLeaf Foo_Bar_UnionLeaf_Union `path:"union-leaf"`
}
type Foo_Bar_UnionLeaf_Union interface {
Is_Foo_Bar_UnionLeaf_Union()
}
type Foo_Bar_UnionLeaf_Union_String struct {
String string
}
func (Foo_Bar_UnionLeaf_Union_String) Is_Foo_Bar_UnionLeaf_Union() {}
type Foo_Bar_UnionLeaf_Union_Int8 struct {
Int8 int8
}
func (Foo_Bar_UnionLeaf_Union_Int8) Is_Foo_Bar_UnionLeaf_Union() {}
The UnionLeaf
field can be set to any of the structs that implement the Foo_Bar_UnionLeaf_Union
interface. Since these structs are single-field entities, a struct initialiser that does not specify the field name can be used (e.g., Foo_Bar_UnionLeaf_Union_String{"baz"}
), similarly to the generate Go code for a Protobuf oneof
.