Writing the new-style command definitions

Earlier I said new features in Perl code and old style templates will not be merged anymore starting from May the 1st (if you have any such features already working and testing, you still have a chance to get them in, so hurry up!).

Now it's time to write step by step guides to using the new style and we'll start with command definitions.

History and motivation

Old-style command definitions (aka "templates") have quite a lot of design issues and proved to be one of the worst deterrents for new contributors (right after Perl code).

If you are not familiar with them, I'll remind you how they work. Suppose we need to create a command for new interface type "silly" (that's like dummy... but also silly). Suppose we start with address option, "set interfaces silly silly0 address 192.0.2.1/24". What we'd need to do:

  • Create directory structure interfaces/silly/node.tag/address
  • Put a node.def file under interfaces/, silly/, and address/, but not under node.tag (otherwise directory will not be recognized as a command definition)
  • Write a bunch of "tags" such as "help: Silly interface name" in the node.def's

There is a whole lot of problems with this approach:

  • To get the complete picture of commands of a component, you need to read a lot of files in multiple deeply nested directories
  • Every such file can contain embedded shell scripts, which means the logic rather than just data can be scattered across dozens files
  • You cannot check whether your node.def's are even syntactically correct without loading them into VyOS and trying them by hand

Some of these problems such as fragility of the data syntax could possibly be fixed. The problems with data and logic scattering, however, are fundamental, and cannot be cured without changing the approach.

A lot of design and development work went into the configuration mode commands definitions for vyconf and thus VyOS 2.0. However, vyconf is not and will never be a drop-in replacement for the old configuration backend (because it means it would have to reimplement the old unfortunate design decisions to be compatible, which defeats the purpose). And, since the plan is to rewrite VyOS 1.x.x gradually in the new style to have an operational system at all times and be able to reuse the code in 2.0 with minimal changes, we need a way to use new style command definitions alongside the old ones. As a compromise, we've made a convertor from new style definitions to the old style.

To learn how to use the new style and how much better it is, read on...

The real fun: writing configuration command definitions just got easier

Before we proceed, I need to say that all code that is using the new style definitions should go to the vyos-1x package. Specifically, they go to the interface-definitions/ directory where you can already find some working examples of command definitions to learn from.

The new format is based on XML. This is the only standardized format in existence that can be automatically verified against a schema. We have made a RelaxNG schema for it, which you can read in compact syntax or XML syntax, whichever you prefer. The best part is that both schema verification and conversion to the old style are already integrated in the vyos-1x package build, and syntactically incorrect definitions will fail the build — if it builds, your commands are guaranteed to at least appear  in the CLI properly.

Or they will be when the convertor is stable and well tested — if you spot any issues, please let us know!

In this post we'll walk through the interface definition for cron that is already in place. Before we start, we need to agree on some definitions, however...

Node taxonomy

VyOS config has three typed of nodes: "normal" nodes, tag nodes, and leaf nodes.

Normal nodes have fixed names and cannot have values, they only serve as containers for other nodes with fixed names. Typical examples are "system", "firewall", or "interfaces". If you look in completion, you'll see that all their children have predefined names, such as "config-management", "modify", or "ethernet".

Tag nodes are also containers for other nodes, but there's one big difference from normal nodes: their children may have arbitrary names. Firewall rulesets or interfaces are common examples: you can have nodes like "firewall name Foo" and "firewall name Bar", or "interfaces ethernet eth0" and "interfaces ethernet eth10". In this case, "name" and "ethernet" are tag nodes and "Foo", "Bar", "eth0", and "eth10" are their children. You can spot them in the config file by the fact that the name is prepended to each of its children.

Leaf nodes cannot have children, but they can have values. In "interfaces ethernet eth0 address 192.0.2.1/24", "address" is a leaf node, and "192.0.2.1/24" is its value.

The file format

Every interface (command) definition file starts with <interfaceDefinition> tag (after the XML header of course). That tag can only include normal nodes, defined by <node> tags.

The <node> tag may include other <node> tags as well as <tagNode> and <leafNode> tags.

Tags for nodes may contain a <properties> tag where node properties such as completion help and name/value validation data are specified, and <children> tag that contains tags for its children nodes.

Implementing the interfaces

Now let's walk throug the task scheduler. I'll remind you what its commands are:

set system task-scheduler task $name interval $interval
set system task-scheduler task $name executable path $path
set system task-scheduler task $name executable arguments $args
set system task-scheduler task $name crontab-spec $spec

The crontab is generated by script ${vyos_sbindir}/vyos-update-crontab.py

So, we need the following nodes:
  • "system" is a normal node. Yes, we need it in every interface definition file even though there are many files that may put something under "system".
  • "task-scheduler" is a normal node, its only child is "task"
  • "task" is a tag node, because task name can be arbitrary, its children are "interval", "executable", and "crontab-spec"
  • "interval" and "crontab-spec" are leaf nodes, they have string values
  • "executable" is a normal node with two leaf nodes "arguments" and "path" as its children

Now the structure of the cron.xml file should be clear. In that file we have no validation because we leave it to the script. And the script — since we want it to run when anything under the "task" node is changed, we use <tagNode name="task" owner="${vyos_sbindir}/vyos-update-crontab.py"> there. In vyconf, the meaning of the "owner" is broader than just a script, but we reused the tag for 1.x without changing its name.

The <help> tags define the help string that is displayed in completion. The <valueHelp> tags are specific to leaf nodes and describe the value format that is displayed in their completion help.

Converting old commands to the new style

I've compiled a table of the old syntax and its equivalents in the new syntax on the wiki: https://wiki.vyos.net/wiki/Interface_definitions 

Since this is new and largely untested, feel free to ask me on Phabricator as well as the IRC if you have any questions or need any help with writing commands for new features or converting the old ones. Rewriting and cleaning up the old code to make it more testable and maintainable is a much needed work and contributions are more than welcome.