Table of Contents
YANG is a data modeling language used to model configuration and state data manipulated by a NETCONF agent. The YANG modeling language is defined in RFC 6020. YANG as a language will not be described in its entirety here - rather we refer to the IETF RFC text at http://www.ietf.org/rfc/rfc6020.txt.
Another source of information regarding the YANG language is the wiki based web site http://www.yang-central.org/. For a tutorial on the data modeling capabilities of YANG, see http://www.yang-central.org/twiki/bin/view/Main/DhcpTutorial.
In ConfD, YANG is not only used for NETCONF data. On the contrary, YANG is used to describe the data model as a whole and used by all northbound interfaces.
A YANG module can be directly transformed into a
final schema (.fxs) file that can be loaded into
ConfD.
Currently all features of the YANG language except the
anyxml
statement are supported.
This section is a brief introduction to YANG. The exact details of all language constructs is fully described in RFC 6020.
The ConfD programmer must know YANG well, since all APIs use various paths that are derived from the YANG datamodel.
A module contains three types of statements: module-header statements, revision statements, and definition statements. The module header statements describe the module and give information about the module itself, the revision statements give information about the history of the module, and the definition statements are the body of the module where the data model is defined.
A module may be divided into submodules, based on the needs of the module owner. The external view remains that of a single module, regardless of the presence or size of its submodules.
The include
statement allows a module or
submodule to reference material in submodules, and the
import
statement allows references to
material defined in other modules.
YANG defines four types of nodes for data modeling. In each of the following subsections, the example shows the YANG syntax as well as a corresponding NETCONF XML representation.
A leaf node contains simple data like an integer or a string. It has exactly one value of a particular type, and no child nodes.
leaf host-name { type string; description "Hostname for this system"; }
With XML value representation for example as:
<host-name>my.example.com</host-name>
An interesting variant of leaf nodes are typeless leafs.
leaf enabled { type empty; description "Enable the interface"; }
With XML value representation for example as:
<enabled/>
A leaf-list
is a sequence of leaf nodes
with exactly one value of a particular type per leaf.
leaf-list domain-search { type string; description "List of domain names to search"; }
With XML value representation for example as:
<domain-search>high.example.com</domain-search> <domain-search>low.example.com</domain-search> <domain-search>everywhere.example.com</domain-search>
A container
node is used to group related
nodes in a subtree.
It has only child nodes and no value and may contain
any number of child nodes of any type (including leafs, lists,
containers, and leaf-lists).
container system { container login { leaf message { type string; description "Message given at start of login session"; } } }
With XML value representation for example as:
<system> <login> <message>Good morning, Dave</message> </login> </system>
A list
defines a sequence of list entries. Each
entry is like a structure or a record instance, and is uniquely
identified by the values of its key leafs.
A list can define multiple keys and may
contain any number of child nodes of any type (including leafs,
lists, containers etc.).
list user { key "name"; leaf name { type string; } leaf full-name { type string; } leaf class { type string; } }
With XML value representation for example as:
<user> <name>glocks</name> <full-name>Goldie Locks</full-name> <class>intruder</class> </user> <user> <name>snowey</name> <full-name>Snow White</full-name> <class>free-loader</class> </user> <user> <name>rzull</name> <full-name>Repun Zell</full-name> <class>tower</class> </user>
These statements are combined to define the module:
// Contents of "acme-system.yang" module acme-system { namespace "http://acme.example.com/system"; prefix "acme"; organization "ACME Inc."; contact "joe@acme.example.com"; description "The module for entities implementing the ACME system."; revision 2007-06-09 { description "Initial revision."; } container system { leaf host-name { type string; description "Hostname for this system"; } leaf-list domain-search { type string; description "List of domain names to search"; } container login { leaf message { type string; description "Message given at start of login session"; } list user { key "name"; leaf name { type string; } leaf full-name { type string; } leaf class { type string; } } } } }
YANG can model state data, as well as configuration data, based on
the config
statement. When a node is
tagged with config false
,
its sub hierarchy is flagged as state data, to be reported using
NETCONF's get operation, not the
get-config operation. Parent
containers, lists, and key leafs are reported also, giving the
context for the state data.
In this example, two leafs are defined for each interface, a configured speed and an observed speed. The observed speed is not configuration, so it can be returned with NETCONF get operations, but not with get-config operations. The observed speed is not configuration data, and cannot be manipulated using edit-config.
list interface { key "name"; config true; leaf name { type string; } leaf speed { type enumeration { enum 10m; enum 100m; enum auto; } } leaf observed-speed { type uint32; config false; } }
YANG has a set of built-in types, similar to those of many programming languages, but with some differences due to special requirements from the management domain. The following table summarizes the built-in types.
Table 3.1. YANG built-in types
Name | Type | Description |
binary | Text | Any binary data |
bits | Text/Number | A set of bits or flags |
boolean | Text | "true" or "false" |
decimal64 | Number | 64-bit fixed point real number |
empty | Empty | A leaf that does not have any value |
enumeration | Text/Number | Enumerated strings with associated numeric values |
identityref | Text | A reference to an abstract identity |
instance-identifier | Text | References a data tree node |
int8 | Number | 8-bit signed integer |
int16 | Number | 16-bit signed integer |
int32 | Number | 32-bit signed integer |
int64 | Number | 64-bit signed integer |
leafref | Text/Number | A reference to a leaf instance |
string | Text | Human readable string |
uint8 | Number | 8-bit unsigned integer |
uint16 | Number | 16-bit unsigned integer |
uint32 | Number | 32-bit unsigned integer |
uint64 | Number | 64-bit unsigned integer |
union | Text/Number | Choice of member types |
YANG can define derived types from base types using the
typedef
statement. A base type can be either a
built-in type or a derived type, allowing a hierarchy of derived
types.
A derived type can be used as the argument for the
type
statement.
typedef percent { type uint16 { range "0 .. 100"; } description "Percentage"; } leaf completed { type percent; }
With XML value representation for example as:
<completed>20</completed>
User defined typedefs are useful when we want to name and reuse a type several times. It is also possible to restrict leafs inline in the data model as in:
leaf completed { type uint16 { range "0 .. 100"; } description "Percentage"; }
Groups of nodes can be assembled into the equivalent of complex
types using the grouping
statement.
grouping
defines a set of nodes
that are instantiated with the uses
statement:
grouping target { leaf address { type inet:ip-address; description "Target IP address"; } leaf port { type inet:port-number; description "Target port number"; } } container peer { container destination { uses target; } }
With XML value representation for example as:
<peer> <destination> <address>192.0.2.1</address> <port>830</port> </destination> </peer>
The grouping can be refined as it is used, allowing certain statements to be overridden. In this example, the description is refined:
container connection { container source { uses target { refine "address" { description "Source IP address"; } refine "port" { description "Source port number"; } } } container destination { uses target { refine "address" { description "Destination IP address"; } refine "port" { description "Destination port number"; } } } }
YANG allows the data model to segregate incompatible nodes into
distinct choices using the choice
and
case
statements. The
choice
statement contains a set of case
statements which define sets of schema nodes that cannot appear
together. Each case
may
contain multiple nodes, but each node may appear in only one
case
under a choice
.
When the nodes from one case are created, all nodes from all other cases are implicitly deleted. The device handles the enforcement of the constraint, preventing incompatibilities from existing in the configuration.
The choice and case nodes appear only in the schema tree, not in the data tree or XML encoding. The additional levels of hierarchy are not needed beyond the conceptual schema.
container food { choice snack { mandatory true; case sports-arena { leaf pretzel { type empty; } leaf beer { type empty; } } case late-night { leaf chocolate { type enumeration { enum dark; enum milk; enum first-available; } } } } }
With XML value reprentation for example as:
<food> <chocolate>first-available</chocolate> </food>
YANG allows a module to insert additional nodes into data models, including both the current module (and its submodules) or an external module. This is useful e.g. for vendors to add vendor-specific parameters to standard data models in an interoperable way.
The augment
statement defines the location in the
data model hierarchy where new nodes are inserted, and the
when
statement
defines the conditions when the new nodes are valid.
augment /system/login/user { when "class != 'wheel'"; leaf uid { type uint16 { range "1000 .. 30000"; } } }
This example defines a uid
node that only is
valid when the user's class
is not wheel
.
If a module augments another model, the XML representation of the
data will reflect the prefix of the augmenting model. For example,
if the above augmentation were in a module with prefix
other
, the XML would look like:
<user> <name>alicew</name> <full-name>Alice N. Wonderland</full-name> <class>drop-out</class> <other:uid>1024</other:uid> </user>
YANG allows the definition of NETCONF RPCs. The method names, input parameters and output parameters are modeled using YANG data definition statements.
rpc activate-software-image { input { leaf image-name { type string; } } output { leaf status { type string; } } }
<rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <activate-software-image xmlns="http://acme.example.com/system"> <name>acmefw-2.3</name> </activate-software-image> </rpc> <rpc-reply message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <status xmlns="http://acme.example.com/system"> The image acmefw-2.3 is being installed. </status> </rpc-reply>
YANG allows the definition of notifications suitable for NETCONF. YANG data definition statements are used to model the content of the notification.
notification link-failure { description "A link failure has been detected"; leaf if-name { type leafref { path "/interfaces/interface/name"; } } leaf if-admin-status { type ifAdminStatus; } }
<notification xmlns="urn:ietf:params:netconf:capability:notification:1.0"> <eventTime>2007-09-01T10:00:00Z</eventTime> <link-failure xmlns="http://acme.example.com/system"> <if-name>so-1/2/3.0</if-name> <if-admin-status>up</if-admin-status> </link-failure> </notification>
Assume we have a small trivial YANG file test.yang
:
module test { namespace "http://tail-f.com/test"; prefix "t"; container top { leaf a { type int32; } leaf b { type string; } } }
There is an Emacs mode suitable for
YANG file editing in the system distribution. It is called
yang-mode.el
We can use confdc compiler to compile the YANG module.
$ confdc -c test.yang
The above command creates an output file test.fxs
that is a compiled schema that can be loaded into the system.
The
confdc
compiler with all its flags is fully
described in
confdc
(1).
There exists a number of standards based auxiliary YANG
modules defining various useful data types.
These modules, as well as their accompanying .fxs files can be
found in the
${CONFD_DIR}/src/confd/yang
directory in the distribution.
The modules are:
ietf-yang-types - defining some basic data types such as counters, dates and times.
ietf-inet-types - defining several useful types related to IP addresses.
Whenever we wish to use any of those predefined modules we need to not only import the module into our YANG module, but we must also load the corresponding .fxs file for the imported module into the system.
So if we extend our test module so that it looks like:
module test { namespace "http://tail-f.com/test"; prefix "t"; import ietf-inet-types { prefix inet; } container top { leaf a { type int32; } leaf b { type string; } leaf ip { type inet:ipv4-address; } } }
Normally when importing other YANG modules we must indicate
through the --yangpath
flag to
confdc
where to
search for the imported module. In the special case of the
standard modules, this is not required.
We compile the above as:
$ confdc -c test.yang $ confdc --get-info test.fxs fxs file Confdc version: "3.0_2" (current Confdc version = "3.0_2") uri: http://tail-f.com/test id: http://tail-f.com/test prefix: "t" flags: 6 type: cs mountpoint: undefined exported agents: all dependencies: ['http://www.w3.org/2001/XMLSchema', 'urn:ietf:params:xml:ns:yang:inet-types'] source: ["test.yang"]
We see that the generated .fxs file has a dependency to
the standard urn:ietf:params:xml:ns:yang:inet-types
namespace. Thus if we try to start
confd,
we must also
ensure that the fxs file for that namespace is loaded.
Failing to do so gives:
$ confd -c confd.conf --foreground --verbose The namespace urn:ietf:params:xml:ns:yang:inet-types (referenced by http://tail-f.com/test) could not be found in the loadPath. Daemon died status=21
The remedy is to modify
confd.conf
so that it contains the proper load path or to
provide the directory containing the fxs file, alternatively
we can provide the path on the command line. The directory
${CONFD_DIR}/etc/confd
contains pre-compiled versions of the standard YANG modules.
$ confd -c confd.conf --addloadpath ${CONFD_DIR}/etc/confd --foreground --verbose
confd.conf
is the configuration file for
ConfD itself. It is described in
confd.conf(5)
The YANG language has built-in declarative constructs
for common integrity constraints. These constructs are
conveniently specified as must
statements.
A must
statement is an XPath expression that must
evaluate to true or a non-empty node-set.
An example is:
container interface { leaf ifType { type enumeration { enum ethernet; enum atm; } } leaf ifMTU { type uint32; } must "ifType != 'ethernet' or " + "(ifType = 'ethernet' and ifMTU = 1500)" { error-message "An ethernet MTU must be 1500"; } must "ifType != 'atm' or " + "(ifType = 'atm' and ifMTU <= 17966 and ifMTU >= 64)" { error-message "An atm MTU must be 64 .. 17966"; } }
XPath is a very powerful tool here. It is often possible to
express most realistic validation constraints using XPath
expressions. Note that for performance reasons, it is
recommended to use the tailf:dependency
statement in the must
statement. The compiler
gives a warning if a must
statement lacks a
tailf:dependency
statement, and it cannot
derive the dependency from the expression. The options
--fail-on-warnings
or -E
TAILF_MUST_NEED_DEPENDENCY
can be given to force
this warning to be treated as an error.
See Section 9.9, “Dependencies - Why Does Validation Points Get Called” for
details.
Another useful built-in constraint checker is the
unique
statement.
With the YANG code:
list server { key "name"; unique "ip port"; leaf name { type string; } leaf ip { type inet:ip-address; } leaf port { type inet:port-number; } }
We specify that the combination of IP and port must be unique. Thus the configuration:
<server> <name>smtp</name> <ip>192.0.2.1</ip> <port>25</port> </server> <server> <name>http</name> <ip>192.0.2.1</ip> <port>25</port> </server>
is not valid.
The usage of leafrefs (See the YANG specification) ensures that we do not end up with configurations with dangling pointers. Leafrefs are also especially good, since the CLI and Web UI can render a better interface.
If other constraints are necessary, validation callback functions can be programmed in C. Read more about validation callbacks in Chapter 9, Semantic validation.
The when
statement is used to make its parent
statement conditional. If the XPath expression specified as
the argument to this statement evaluates to false, the parent
node cannot be given configured. Furthermore, if the parent
node exists, and some other node is changed so that the XPath
expression becomes false, the parent node is automatically
deleted. For example:
leaf a { type boolean; } leaf b { type string; when "../a = 'true'"; }
This data model snippet says that 'b' can only exist if 'a' is true. If 'a' is true, and 'b' has a value, and 'a' is set to false, 'b' will automatically be deleted.
Since the XPath expression in theory can refer to any node in
the data tree, it has to be re-evaluated when any node in the
tree is modified. But this would have a disastrous
performance impact, so in order to avoid this, ConfD keeps
track of dependencies for each when expression. In some
simple cases, the confdc can figure out
these dependencies by itself. In the example above, ConfD
will detect that 'b' is dependant on 'a', and evaluate b's
XPath expression only if 'a' is modified. If
confdc cannot detect the dependencies by
itself, it requires a tailf:dependency
statement
in the when
statement.
See Section 9.9, “Dependencies - Why Does Validation Points Get Called” for
details.
Tail-f has an extensive set of extensions to the YANG
language that integrates YANG models in ConfD.
For example when we have config false;
data,
we may wish to invoke user C code to deliver the statistics
data in runtime. To do this we annotate the YANG model
with a Tail-f extension called tailf:callpoint
.
Alternatively we may wish to invoke user code to
validate the configuration, this is also controlled
through an extension called tailf:validate
.
All these extensions are handled
as normal YANG extensions. (YANG is designed to be extended)
We have defined the Tail-f proprietary extensions in a file
${CONFD_DIR}/src/confd/yang/tailf-common.yang
Continuing with our previous example, adding a callpoint and a validation point we get:
module test { namespace "http://tail-f.com/test"; prefix "t"; import ietf-inet-types { prefix inet; } import tailf-common { prefix tailf; } container top { leaf a { type int32; config false; tailf:callpoint mycp; } leaf b { tailf:validate myvalcp { tailf:dependency "../a"; } type string; } leaf ip { type inet:ipv4-address; } } }
The above module contains a callpoint and a validation point.
The exact syntax for all Tail-f extensions are defined in the
tailf-common.yang
file.
Note the import statement where we import
tailf-common
.
When we are using YANG specifications in order to generate Java classes for ConfM, these extensions are ignored. They only make sense on the device side. It is worth mentioning them though, since EMS developers will certainly get the YANG specifications from the device developers, thus the YANG specifications may contain extensions
The man page tailf_yang_extensions(5) describes all the Tail-f YANG extensions.
Sometimes it is convenient to specify all Tail-f extension statements in-line in the original YANG module. But in some cases, e.g. when implementing a standard YANG module, it is better to keep the Tail-f extension statements in a separate annotation file. When the YANG module is compiled to an fxs file, the compiler is given the original YANG module, and any number of annotation files.
A YANG annotation file is a normal YANG module which
imports the module to annotate. Then the
tailf:annotate
statement is used to annotate
nodes in the original module. For example, the module
test above can be annotated like this:
module test { namespace "http://tail-f.com/test"; prefix "t"; import ietf-inet-types { prefix inet; } container top { leaf a { type int32; config false; } leaf b { type string; } leaf ip { type inet:ipv4-address; } } }
module test-ann { namespace "http://tail-f.com/test-ann"; prefix "ta"; import test { prefix t; } import tailf-common { prefix tailf; } tailf:annotate "/t:top/t:a" { tailf:callpoint mycp; } tailf:annotate "/t:top" { tailf:annotate "t:b" { // recursive annotation tailf:validate myvalcp { tailf:dependency "../t:a"; } } } }
In order to compile the module with annotations, use the -a parameter to confdc:
confdc -c -a test-ann.yang test.yang
Certain parts of a YANG model are used by northbound agents, e.g. CLI and Web UI, to provide the end-user with custom help texts and error messages.
A YANG statement can be annotated with a
description
statement which is used to
describe the definition for a reader of the module.
This text is often too long and too detailed to be
useful as help text in a CLI. For this reason, ConfD by
default does not use the text in the
description
for this purpose. Instead, a
tail-f specific statement, tailf:info
is
used. It is recommended that the standard
description
statement contains a detailed
description suitable for a module reader (e.g. NETCONF
client or server implementor), and
tailf:info
contains a CLI help text.
As an alternative, ConfD can be instructed to use the
text in the description
statement also for
CLI help text. See the option
--use-description to confdc
(1).
For example, CLI uses the help text to prompt for a value of this particular type. The CLI shows this information during tab/command completion or if the end-user explicitly asks for help using the ?-character. The behavior depends on the mode the CLI is running in.
The Web UI uses this information likewise to help the end-user.
The mtu
definition below has been annotated to
enrich the end-user experience:
leaf mtu { type uint16 { range "1 .. 1500"; } description "MTU is the largest frame size that can be transmitted over the network. For example, an Ethernet MTU is 1,500 bytes. Messages longer than the MTU must be divided into smaller frames."; tailf:info "largest frame size"; }
Alternatively, we could have provided the help text
in a typedef
statement as in:
typedef mtuType { type uint16 { range "1 .. 1500"; } description "MTU is the largest frame size that can be transmitted over the network. For example, an Ethernet MTU is 1,500 bytes. Messages longer than the MTU must be divided into smaller frames."; tailf:info "largest frame size"; } leaf mtu { type mtuType; }
If there is an explicit help text attached to a leaf, it overrides the help text attached to the type.
A statement can have an optional error-message statement. The north-bound agents, for example, the CLI uses this to inform the end-user about a provided value which is not of the correct type. If no custom error-message statement is available ConfD generates a built-in error message, e.g. "1505 is too large.".
All northbound agents use the extra information provided by
an error-message
statement.
The typedef
statement below has been annotated to
enrich the end-user experience when it comes to error information:
typedef mtuType { type uint32 { range "1..1500" { error-message "The MTU must be a positive number not " + "larger than 1500"; } } }
It is sometimes useful to hide nodes from some of the
northbound interfaces. The tailf:export
statement, or the --export compile
directive can be used to hide an entire module. It is
recommended to use the tailf:export
statement.
More fine grained control can be attained with the optional
tailf:hidden
statement.
The tailf:hidden
statement names a hide
group
. All nodes belonging to the same hide group
are treated the same way as fas as being hidden or
invisible. The hide group name full
is given a
special meaning. The full
hide group is hidden
from all northbound interfaces, not just user interfaces.
A related situation is when some nodes should be displayed to the user only when a certain condition is met. For example, the "ethernet" subtree should be displayed only when the type of an interface is "ethernet". This is covered in the subsection "Conditional display" below.
This is nodes that may be useful for the MOs, but should be hidden from all northbound interfaces. An example is the set of physical network interfaces on a device and their types. This is "static" data, i.e. it can't be changed by configuration, but it can vary between different models of a device that run the same software, and the device-specific data can be provided via init file or through MAAPI.
This type of data could also be realized via a separate
module where tailf:export
is used to limit
the visibility, but being able to have some nodes in the
data model hidden while others are not allows for greater
flexibility - e.g. lists in the config data can have
hidden child nodes, which get instantiated automatically
along with the visible config nodes.
This is data that is fully visible to programmatic northbound interfaces such as NETCONF, but normally hidden from user interfaces such as CLI and Web UI. Examples are data used for experimental or end-customer-specific features, similar to hidden commands in the CLI but for data nodes.
A user interface may give access to this type of data (and
even totally hidden data) if the user executes an
unhide
command identifying hide group that
should be revealed. After this the nodes belonging to the
hide group appear the same as unhidden data, i.e. they're
included in tab completion, listed by show
commands etc.
A hide group can only be unhidden
if the
group is listed in the
confd.conf
. This means that a hide
group will be completely hidden to the user interfaces
unless it has been explicitly allowed to be unhidden in
confd.conf
. A password can optionally
be required to unhide a group.
<hideGroup> <name>debug</name> <password>secret</password> </hideGroup>
Sometimes it is convenient to hide some CLI commands, or Web UI elements, when certain conditions on the configuration are met. A typical example is a "discriminated union". One leaf is the type of something, and depending on the value of this leaf, different containers are visible:
typedef interface-type { type enumeration { enum ethernet; enum atm; enum appletalk; } } leaf if-type { type interface-type; } container ethernet { ... } container atm { ... } container appletalk { ... }
In this example, the "ethernet" container should be visible to the user only when the value of "if-type" is "ethernet".
This can be accomplished by using the tailf:display-when
statement. It contains an XPath expression which specifies
when the node should be displayed:
container ethernet { tailf:display-when "../if-type = 'ethernet'"; ... } container atm { tailf:display-when "../if-type = 'atm'"; ... } container appletalk { tailf:display-when "../if-type = 'appletalk'"; ... }
With this data model, the CLI behaves like this:
% show if-type ethernet; ethernet { ... # data for ethernet } % set <tab> Possible completions: ethernet if-type % set if-type atm % set atm ... # create atm data % delete <tab> Possible completions: ethernet if-type # NOTE: ethernet NOT present
Say for example that we want to model the interface list on a Linux based device. Running the ip link list command reveals the type of information we have to model
$ /sbin/ip link list 1: eth0: <BROADCAST,MULTICAST,UP>; mtu 1500 qdisc pfifo_fast qlen 1000 link/ether 00:12:3f:7d:b0:32 brd ff:ff:ff:ff:ff:ff 2: lo: <LOOPBACK,UP>; mtu 16436 qdisc noqueue link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop link/ether a6:17:b9:86:2c:04 brd ff:ff:ff:ff:ff:ff
and this is how we want to represent the above in XML:
<?xml version="1.0"?> <config xmlns="http://example.com/ns/link"> <links> <link> <name>eth0</name> <flags> <UP/> <BROADCAST/> <MULTICAST/> </flags> <addr>00:12:3f:7d:b0:32</addr> <brd>ff:ff:ff:ff:ff:ff</brd> <mtu>1500</mtu> </link> <link> <name>lo</name> <flags> <UP/> <LOOPBACK/> </flags> <addr>00:00:00:00:00:00</addr> <brd>00:00:00:00:00:00</brd> <mtu>16436</mtu> </link> </links> </config>
An interface or a link
has data associated with it.
It also has a name, an obvious choice to use as the key - the data
item which uniquely identifies an individual interface.
The structure of a YANG model is always a header, followed by type definitions, followed by the actual structure of the data. A YANG model for the interface list starts with a header:
module links { namespace "http://example.com/ns/links"; prefix link; revision 2007-06-09 { description "Initial revision."; } ...
A number of datatype definitions may follow the YANG module
header. Looking at the output from /sbin/ip we see that
each interface has a number of boolean flags associated with
it, e.g. UP
, and NOARP
.
One way to model a sequence of boolean flags is as a sequence of statements:
leaf UP { type boolean; default false; } leaf NOARP { type boolean; default false; }
A better way is to model this as:
leaf UP { type empty; } leaf NOARP { type empty; }
We could choose to group these leafs together into a grouping. This makes sense if we wish to use the same set of boolean flags in more than one place. We could thus create a named grouping such as:
grouping LinkFlags { leaf UP { type empty; } leaf NOARP { type empty; } leaf BROADCAST { type empty; } leaf MULTICAST { type empty; } leaf LOOPBACK { type empty; } leaf NOTRAILERS { type empty; } }
The output from /sbin/ip also contains Ethernet MAC
addresses. These are best represented by the mac-address
type defined in the ietf-yang-types.yang
file.
The mac-address
type is defined as:
typedef mac-address { type string { pattern '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}'; } description "The mac-address type represents an IEEE 802 MAC address. This type is in the value set and its semantics equivalent to the MacAddress textual convention of the SMIv2."; reference "IEEE 802: IEEE Standard for Local and Metropolitan Area Networks: Overview and Architecture RFC 2579: Textual Conventions for SMIv2"; }
This defines a restriction on the string type, restricting
values of the defined type "mac-address" to be strings
adhering to the regular expression
[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}
Thus strings such as a6:17:b9:86:2c:04
will be
accepted.
Queue disciplines are associated with each device. They are typically used for bandwidth management. Another string restriction we could do is to define an enumeration of the different queue disciplines that can be attached to an interface.
We could write this as:
typedef QueueDisciplineType { type enumeration { enum pfifo_fast; enum noqueue; enum noop; enum htp; } }
There are a large number of queue disciplines and we only list a few here. The example serves to show that using enumerations we can restrict the values of the data set in a way that ensures that data entered always is valid from a syntactical point of view.
Now that we have a number of usable datatypes, we continue with the actual data structure describing a list of interface entries:
container links { list link { key name; unique addr; max-elements 1024; leaf name { type string; } container flags { uses LinkFlags; } leaf addr { type yang:mac-address; mandatory true; } leaf brd { type yang:mac-address; mandatory true; } leaf qdisc { type QueueDisciplineType; mandatory true; } leaf qlen { type uint32; mandatory true; } leaf mtu { type uint32; mandatory true; } } }
The key
attribute on the leaf named "name" is
important. It indicates that the leaf is the instance key
for the list entry named "link". All the link
leafs are guaranteed to have unique values for their
name
leafs due to the key declaration.
If one leaf alone does not uniquely identify an object, we can define multiple keys. At least one leaf must be an instance key - we cannot have lists without a key.
List entries are ordered and indexed according to the value of the key(s).
A very common situation when modeling a device
configuration is that we wish to model a relationship
between two objects. This is achieved by means of the
leafref
statements. A leafref
points to a
child of a list entry which either is defined
using a key
or unique
attribute.
The leafref
statement can be used to express three
flavors of relationships: extensions,
specializations and
associations. Below
we exemplify this by extending the "link" example from
above.
Firstly, assume we want to put/store the queue disciplines
from the previous section in a separate container - not
embedded inside the links
container.
We then specify a separate container , containing
all the queue disciplines which each refers to a specific
link
entry. This is written as:
container queueDisciplines { list queueDiscipline { key linkName; max-elements 1024; leaf linkName { type leafref { path "/config/links/link/name"; } } leaf type { type QueueDisciplineType; mandatory true; } leaf length { type uint32; } } }
The linkName
statement is both an instance key of the
queueDiscipline
list, and at the same time refers
to a specific link
entry. This way we can extend the
amount of configuration data associated with a specific
link
entry.
Secondly, assume we want to express a restriction or
specialization on Ethernet link
entries, e.g. it should
be possible to restrict interface characteristics such as
10Mbps and half duplex.
We then specify a separate container, containing
all the specializations which each refers to a specific
link
:
container linkLimitations { list LinkLimitation { key linkName; max-elements 1024; leaf linkName { type leafref { path "/config/links/link/name"; } } container limitations { leaf only10Mbs { type boolean;} leaf onlyHalfDuplex { type boolean;} } } }
The linkName
leaf is both an instance key to the
linkLimitation
list, and at the same time refers to a
specific link
leaf. This way we can restrict or
specialize a specific link
.
Thirdly, assume we want to express that one of the link
entries should be the default link. In that case we
enforce an association between a non-dynamic defaultLink
and a certain link
entry:
leaf defaultLink { type leafref { path "/config/links/link/name"; } }
Key leafs are always unique. Sometimes we may wish to
impose further restrictions on objects. For example, we can
ensure that all link
entries have a unique MAC
address. This is achieved through the use of the
unique
statement:
container servers { list server { key name; unique "ip port"; unique "index"; max-elements 64; leaf name { type string; } leaf index { type uint32; mandatory true; } leaf ip { type inet:ip-address; mandatory true; } leaf port { type inet:port-number; mandatory true; } } }
In this example we have two unique
statements.
These two groups ensure
that each server has a unique index number as well as a
unique ip and port pair.
A leaf can have a static or dynamic default value.
Static default values are defined with the default
statement in the data model. For example:
leaf mtu { type int32; default 1500; }
and:
leaf UP { type boolean; default true; }
A dynamic default value means that the default value for
the leaf is the value of some other leaf in the data model.
This can be used to make the default values configurable by
the user. Dynamic default values are defined using the
tailf:default-ref
statement. For example, suppose we
want to make the MTU default value configurable:
container links { leaf mtu { type uint32; } list link { key name; leaf name { type string; } leaf mtu { type uint32; tailf:default-ref '../../mtu'; } } }
Now suppose we have the following data:
<links> <mtu>1000</mtu> <link> <name>eth0</name> <mtu>1500</mtu> </link> <link> <name>eth1</name> </link> </links>
In the example above, link eth0
has the mtu 1500,
and link eth1
has mtu 1000. Since eth1
does not have a mtu value set, it defaults to the value of
../../mtu
, which is 1000 in this case.
Whenever a leaf has a default value it implies that the leaf can be left out from the XML document, i.e. mandatory = false.
With the default value mechanism an old configuration can be used even after having added new settings.
Another example where default values are used is when a new instance is created. If all leafs within the instance have default values, these need not be specified in, for example, a NETCONF create operation.
Here is the final interface YANG model with all constructs described above:
module links { namespace "http://example.com/ns/link"; prefix link; import ietf-yang-types { prefix yang; } grouping LinkFlagsType { leaf UP { type empty; } leaf NOARP { type empty; } leaf BROADCAST { type empty; } leaf MULTICAST { type empty; } leaf LOOPBACK { type empty; } leaf NOTRAILERS { type empty; } } typedef QueueDisciplineType { type enumeration { enum pfifo_fast; enum noqueue; enum noop; enum htb; } } container config { container links { list link { key name; unique addr; max-elements 1024; leaf name { type string; } container flags { uses LinkFlagsType; } leaf addr { type yang:mac-address; mandatory true; } leaf brd { type yang:mac-address; mandatory true; } leaf mtu { type uint32; default 1500; } } } container queueDisciplines { list queueDiscipline { key linkName; max-elements 1024; leaf linkName { type leafref { path "/config/links/link/name"; } } leaf type { type QueueDisciplineType; mandatory true; } leaf length { type uint32; } } } container linkLimitations { list linkLimitation { key linkName; leaf linkName { type leafref { path "/config/links/link/name"; } } container limitations { leaf only10Mbps { type boolean; default false; } leaf onlyHalfDuplex { type boolean; default false; } } } } container defaultLink { leaf linkName { type leafref { path "/config/links/link/name"; } } } } }
If the above YANG file is saved on disk, as
links.yang
, we can compile and link it using
the confdc compiler:
$ confdc -c links.yang
We now have a ready to use schema file named
links.fxs
on disk.
This is an example of an XML document adhering to the
links.yang
model:
<?xml version="1.0"?> <config xmlns="http://example.com/ns/link"> <links> <link> <name>eth0</name> <flags> <UP/> <BROADCAST/> <MULTICAST/> </flags> <addr>00:12:3f:7d:b0:32</addr> <brd>ff:ff:ff:ff:ff:ff</brd> <mtu>1500</mtu> </link> <link> <name>lo</name> <flags> <UP/> <LOOPBACK/> </flags> <addr>00:00:00:00:00:00</addr> <brd>00:00:00:00:00:00</brd> <mtu>16436</mtu> </link> </links> <queueDisciplines> <queueDiscipline> <linkName>eth0</linkName> <type>pfifo_fast</type> <length>1000</length> </queueDiscipline> <queueDiscipline> <linkName>lo</linkName> <type>noqueue</type> </queueDiscipline> </queueDisciplines> <linkLimitations> <linkLimitation> <linkName>eth0</linkName> <limitations> <only10Mbps>true</only10Mbps> </limitations> </linkLimitation> </linkLimitations> <defaultLink> <linkName>eth0</linkName> </defaultLink> </config>
We can verify that the above XML document actually adheres to the YANG model by using the -x option to confdc (1)
$ confdc -f links.fxs -x links.xml
To actually run this example, we need to copy the
compiled links.fxs
to a directory where
ConfD
can find it.
A leafref is a used to model relationships in the data model, as described in Section 3.10.1, “Modeling Relationships”. In the simplest case, the leafreaf is a single leaf that references a single key in a list:
list host { key "name"; leaf name { type string; } ... } leaf host-ref { type leafref { path "../host/name"; } }
But sometimes a list has more than one key, or we need to refer to a list entry within another list. Consider this example:
list host { key "name"; leaf name { type string; } list server { key "ip port"; leaf ip { type inet:ip-address; } leaf port { type inet:port-number; } ... } }
If we want to refer to a specific server on a host, we must provide three values; the host name, the server ip and the server port. Using leafrefs, we can accomplish this by using three connected leafs:
leaf server-host { type leafref { path "/host/name"; } } leaf server-ip { type leafref { path "/host[name=current()/../server-host]/server/ip"; } } leaf server-port { type leafref { path "/host[name=current()/../server-host]" + "/server[ip=current()/../server-ip]/../port"; } }
The path specification for server-ip
means the ip
address of the server under the host with same name as specified
in server-host
.
The path specification for server-port
means the
port number of the server with the same ip as specified in
server-ip
, under the host with same name as
specified in server-host
.
This syntax quickly gets awkward and error prone. ConfD
supports a shorthand syntax, by introducing an XPath function
deref()
(see the section called “XPATH FUNCTIONS”).
Technically, this function follows a leafreaf value, and returns
all nodes that the leafref refer to (typically just one). The
example above can be written like this:
leaf server-host { type leafref { path "/host/name"; } } leaf server-ip { type leafref { path "deref(../server-host)/../server/ip"; } } leaf server-port { type leafref { path "deref(../server-ip)/../port"; } }
Note that using the deref
function is syntactic
sugar for the basic syntax. The translation between the two
formats is trivial. Also note that deref()
is an
extension to YANG, and third party tools might not understand
this syntax. In order to make sure that only plain YANG
constructs are used in a module, the parameter
--strict-yang
can be given to confdc
-c.
There are several reasons for supporting multiple configuration namespaces. Multiple namespaces can be used to group common datatypes and hierarchies to be used by other YANG models. Separate namespaces can be used to describe the configuration of unrelated sub-systems, i.e. to achieve strict configuration data model boundaries between these sub-systems.
As an example, datatypes.yang
is a YANG module
which defines a reusable data type.
module datatypes { namespace "http://example.com/ns/dt"; prefix dt; grouping countersType { leaf recvBytes { type uint64; mandatory true; } leaf sentBytes { type uint64; mandatory true; } } }
We compile and link datatypes.yang
into a
final schema
file representing the http://example.com/ns/dt
namespace:
$ confdc -c datatypes.yang
To reuse our user defined countersType
, we must
import the datatypes
module.
module test { namespace "http://tail-f.com/test"; prefix "t"; import datatypes { prefix dt; } container stats { uses dt:countersType; } }
When compiling this new module that refers to another module, we must indicate to confdc where to search for the imported module:
$ confdc -c test.yang --yangpath /path/to/dt
confdc also searches for referred
modules in the colon (:) separated path defined by the
environment variable YANG_MODPATH
and . (dot) is
implicitly included. Thus we can run pyang on
test.yang
and it will search for the
datatypes.yang
file and import it.
We have three different entities that define our configuration data.
The module name. A system typically consists of several modules. In the future we also expect to see standard modules in a manner similar to how we have standard SNMP modules.
It is highly recommended to have the vendor name embedded in the module name, similar to how vendors have their names in proprietary MIB s today.
The XML namespace. A module defines a namespace. This is an important part of the module header. For example we have:
module acme-system { namespace "http://acme.example.com/system"; .....
The namespace string must uniquely define the namespace. It is very important that once we have settled on a namespace we never change it. The namespace string should remain the same between revisions of a product. Do not embed revision information in the namespace string since that breaks manager side NETCONF scripts.
The revision
statement as in:
module acme-system { namespace "http://acme.example.com/system"; prefix "acme"; revision 2007-06-09; .....
The revision is exposed to a NETCONF manager in the capabilities sent from the agent to the NETCONF manager in the initial hello message. The fine details of revision management is being worked on in the IETF NETMOD working group and is not finalized at the time of this writing.
What is clear though, is that a manager should base its version decisions on the information in the revision string.
A capabilities reply from a NETCONF agent to the manager may look as:
<?xml version="1.0" encoding="UTF-8"?> <hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <capabilities> <capability>urn:ietf:params:netconf:base:1.0</capability> <capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability> <capability>urn:ietf:params:netconf:capability:candidate:1.0</capability> <capability>urn:ietf:params:netconf:capability:confirmed-commit:1.0</capability> <capability>urn:ietf:params:netconf:capability:xpath:1.0</capability> <capability>urn:ietf:params:netconf:capability:validate:1.0</capability> <capability>urn:ietf:params:netconf:capability:rollback-on-error:1.0</capability> <capability>http://example.com/ns/link?revision=2007-06-09</capability> ....
where the revision information for the
http://example.com/ns/link
namespace is encoded
as ?revision=2007-06-09
using standard URI notation.
When we change the data model for a namespace, it is recommended to change the revision statement, and to never make any changes to the data model that are backwards incompatible. This means that all leafs that are added must be either optional or have a default value. That way it is ensured that old NETCONF client code will continue to function on the new data model. Section 10 of RFC 6020 defines exactly what changes can be made to a data model in order to not break old NETCONF clients.
Internally and in the programmer APIs,
ConfD
uses integer values
to represent YANG node names and the namespace URI. This
conserves space and allows for more
efficient comparisons (including switch
statements) in
the user application code.
By default, confdc automatically computes a hash value
for the namespace URI and for each string that is used as a
node name.
Conflicts can occur in the mapping between strings and integer values - i.e. the initial assignment of integers to strings is unable to provide a unique, bi-directional mapping. Such conflicts are extremely rare (but possible) when the default hashing mechanism is used.
The conflicts are detected either by confdc
or by the
ConfD
daemon when it loads the .fxs
files.
If there are any conflicts reported they will pertain to XML tags (or the namespace URI),
There are two different cases:
Two different strings mapped to the same integer. This is
the classical hash conflict - extremely rare due to the high
quality of the hash function used. The
resolution is to use the tailf:id-value
statement to
assign
a different integer to one of the strings - we should choose
a number above 2^31+1 to avoid collisions with the generated
hash values. To assign a different integer to the namespace
URI, we need to use the --ns-id-value option with
confdc rather than tailf:id-value
.
One string mapped to two different integers. This is even
more rare than the previous case - it can only happen if
a hash conflict was detected and avoided through the use of
tailf:id-value
on one of the strings,
and that string also occurs somewhere else. The resolution is
to use tailf:id-value
(or --ns-id-value) to assign the same integer on
both occurrences of the string. As in the
previous case, we should choose a number above 2^31+1.
There are some constructs in confspecs that are not possible to directly translate into YANG models. Probably the most problematic is how we, using confspecs, can mount pieces of configuration in one confspec file into a mount point in another confspec file. It is also possible to mount entire namespaces, .fxs files, into mount points in other namespaces. Neither is possible with the YANG language.
YANG has the submodule concept as a construct to divide a module. If the overall goal with confspec mounting is to let different developers work on specific files that are subsequently linked together, that same goal can be achieved in three different ways with YANG. The method described in Section 3.15.3, “Partitioning the Module Through Augments Statements” is probably the closest match for the confspec mounting functionality, but one of the others may be more appropriate or convenient to use.
The strategy here is have a top module, that simply
consists of a series of include statements,
a series of uses
statements, and a set of
submodules that all define their respective groupings:
module system { namespace "http://tail-f.com/system"; prefix "s"; include system-sub; uses top; uses sys; }
And then we can have arbitrary many sub modules.
submodule system-sub { belongs-to system { prefix s; } import ietf-inet-types { prefix inet; } grouping top { container top { leaf foo { type int32; } leaf addr{ type inet:ip-address; } } } grouping sys { leaf bar { type int32; } } }
The strategy here is have a top module, that simply consists of a series of include statements, and then each of the submodules define top level structures.
module system { namespace "http://tail-f.com/system"; prefix "s"; include system-sub; include system-sub2; ..... }
And then we can have arbitrary many sub modules.
submodule system-sub { belongs-to system { prefix s; } import ietf-inet-types { prefix inet; } container top { leaf foo { type int32; } leaf addr{ type inet:ip-address; } } leaf bar { type int32; } }
The include
statement in the top module
will ensure that the structures defined in the
submodules(s) get expanded.
First we need a toplevel file that includes all the submodules:
module system { namespace "http://tail-f.com/system"; prefix "s"; include system-common; include system-sub; }
Following that we have one submodule that defines some shared data types, and most important, the structure of the entire data model as a skeleton.
submodule system-common { belongs-to system { prefix s; } import ietf-inet-types { prefix inet; } typedef myType { type int32; } container system { container chassis { } container stats { } } }
And finally, we have one or more submodules that
augment the structure defined in
system-common.yang
submodule system-sub { belongs-to system { prefix s; } import ietf-inet-types { prefix inet; } include system-common; augment "/system/chassis" { leaf foo { type int32; } leaf addr{ type inet:ip-address; } } augment "/s:system/s:stats" { leaf bar { type int32; } } }
The confspec language has a construct called
indexedView
. This is a means to indicate
in the data model that items in a list have an order. Some of the
north bound agents can then insert new list
entries given by the integer key values.
Typical examples are things like firewall rules.
The indexedView
concept is supported by the
tailf:indexed-view
extension to YANG. However in
most cases, it will probably be more appropriate to use the
standard, more powerful YANG mechanism called ordered-by
user
. Thus confspec users that use the
indexedView
may want to migrate to
ordered-by user
instead of
tailf:indexed-view
. Read more about
ordered-by user
in the YANG specification.
An alternative is to continue to use the Tail-f proprietary
indexedView
feature in the YANG model.
Yet another area that differs slightly are confspec
keyref
s. A confspec keyref
uses tagpaths
to indicate
what it points to. The YANG equivalent is
leafref
that behave in a similar way. The major
difference is that a leafref
use an XPath expression
instead of a tagpath.
Leafrefs that are not also keys may be on the tagpath form we have in confspecs, but they may also be instantiated XPath expressions - thus making leafrefs more powerful that confspec keyrefs.
We may also have list entries where one or more of the keys are leafrefs. As long as we have list entries with a single key there is no difference at all compared to confspecs. It is only when we have multiple keys that we see a difference. See Section 3.11, “More on leafrefs” for more info.
confspecs have enumeration on the form of:
<xs:simpleType name="YesNo"> <xs:restriction base="xs:string"> <xs:enumeration value="yes"/> <xs:enumeration value="no"/> </xs:restriction> </xs:simpleType>
The equivalent enumeration in YANG looks as:
typedef YesNo { type enumeration { enum yes; enum no; } }
These two enumerations are however not completely equivalent. YANG enumerations are implicitly numbered, starting at zero. Thus the exactly equivalent confspec enumerations looks like:
<xs:simpleType name="YesNo"> <xs:restriction base="xs:string"> <xs:enumeration value="yes" idValue="0"/> <xs:enumeration value="no" idValue="1"/> </xs:restriction> </xs:simpleType>
Thus, applications that are currently not using the --allow-enum-conflicts flag to confdc may break. The C API functions to map from hash value to string doesn't work properly for enumerations any longer when we use YANG models. When we use YANG models, the flag --allow-enum-conflicts is implicitly used due to the above mentioned fact that YANG enumerations always start at zero.
Prior to YANG we have recommended confspec users to embed version information in the string that identifies a namespace as in:
<confspec xmlns="http://tail-f.com/ns/confspec/1.0" xmlns:confd="http://tail-f.com/ns/confd/1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/ns/interfaces/1.1" mount="/">
using for example the targetNamespace
attribute. This is no longer recommended practice. The
recommendation is now to never embed version information
in the namespace URI, see Section 3.13, “Module Names, Namespaces and Revisions”.
Using the new naming scheme one would translate the example above into a YANG module like this:
module interfaces { namespace "http://example.com/ns/interfaces"; prefix interfaces; ... }
However if we need to support upgrade from a
software release that used confspec to a new release that
uses YANG for the same config we have to be careful. In
confspec the identifier of a module is the part of the
namespace before the version number. That is, in
http://example.com/ns/interfaces/1.1
the
identifier is http://example.com/ns/interfaces
(in confspec it can also be explicitly set using the
id
attribute on the top-level
confspec
node). This matters to CDB upgrade, in
that CDB will use the identifier to
determine if a namespace is the same or not, when
upgrading (so that it can correctly identify
http://example.com/ns/interfaces/2.0
as the new
version of
http://example.com/ns/interfaces/1.1
). When
using YANG ConfD (and CDB) considers the whole namespace
string (since it isn't supposed to change between
revisions) as the identifier. To help there is a YANG
extension called tailf:id
which takes either an
empty string (which means “treat the namespace just
as confspec would, and deduce the id from the namespace
string”) or an explicit string (the equivalent of
using id
in confspec).
In summary: if we need to support upgrade from a previous release and need to keep the exact namespace string we had in confspec we can translate the initial confspec fragment above to:
module interfaces { namespace "http://example.com/ns/interfaces/1.1"; prefix interfaces; tailf:id ""; ... }
Finally we must mention the cs2yang tool. It is an XSLT based tool that can be used to translate confspec files into YANG files. There are some constructs that are not directly translatable, but the output from cs2yang provides an excellent starting point for migration.
Thus the first step in a migration from confspecs to YANG is to use the cs2yang tool on all confspec files we wish to translate.
Following that, we must decide on a multi-file mount strategy as described above. The generated files must then subsequently be hand edited into a suitable form.
In many confspec models, the namespace URI contains a
version number. In YANG, the namespace URI of a data
model must not change if the model is updated. Instead,
YANG has a revision statement to handle data model
versioning. Unless confspec based systems need to be
upgraded in the field to YANG, it is recommended that
the version number in the namespace URI is removed, and
a revision statement is added. In this case, the
tailf:id
statement that cs2yang generated,
must also be removed from the YANG module. See Section 3.15.6, “The Namespace URI” for a more
detailed discussion of this problem.
pyang
is a Python based tool that
is integrated into the confdc
tool chain.
Pyang can be used to check and transform YANG models.
It has an extensible plugin architecture that allows users
to plugin their own output modules. Thus if we wish to
to transform YANG data models - processing e.g proprietary
extensions, writing a Python plugin to pyang is a feasible
way forward.
Pyang is developed by Tail-f, and it is open source. Code and documentation are available at http://www.yang-central.org/.
Pyang is also described in the man page pyang(1)