VyOS Platform Blog

Building an open source network OS for the people, together.

Adding custom operational mode commands to VyOS

Posted 28 Jan, 2021 by Erkin Batu Altunbas

The operational mode (op mode for short) is half of the VyOS CLI, the other half being configuration mode (conf mode for short). Maintenance and administration take place in the op mode, using tight-knit, high-level commands and subcommands that wrap lower level programs and scripts, and form an overarching web of abstraction over the system.

Although the existing commands tend to cover most use-cases in practice, there will always be cases where the user is forced to reach into the guts of the system to carry out operations not covered by the CLI, or simply needs to integrate their own programs into the system. Keeping track of operations outside the CLI can be tiresome, and such operations can possibly disrupt the system maintained through the CLI in unexpected ways if there’s an overlap in functionality.

The clear solution to this conundrum is integrating your programs and services into the CLI. In addition to working out the problems above, you can benefit from the convenience of tab completion. Let’s see how we can go about accomplishing this.

For the sake of simplicity, we're going to wrap ncal(1) with a command, then see how we can abstract away minute details with high-level subcommands and add tab-completion help text to them.

First things first, let's start with a template: calendar.xml. Op mode definitions are written in XML so that they can be validated against a schema at build time before being converted to internal node.def files, so that packages containing invalid definitions will simply fail to build.

<?xml version="1.0"?>
<interfaceDefinition>
<node name="calendar"> <!-- Name of the command -->
</node>
</interfaceDefinition>

We've got the template. Now let's add the functionality and the help text.

<?xml version="1.0"?>
<interfaceDefinition>
<node name="calendar">
<properties>
<help>Show the monthly calendar</help> <!-- Tab completion help -->
</properties>
<command>/usr/bin/cal</command> <!-- The underlying command to be executed -->
</node>
</interfaceDefinition>

Quite simple. We can also add high-level subcommands and map them to low-level flags to be passed to the program.

<?xml version="1.0"?>
<interfaceDefinition>
<node name="calendar">
<properties>
<help>Show the monthly calendar</help>
</properties>
<command>/usr/bin/cal</command> <!-- $ calendar -->
<children>
<leafNode name="year">
<properties>
<help>Show the yearly calendar</help>
</properties>
<command>/usr/bin/cal -y</command> <!-- $ calendar year -->
</leafNode>
</children>
</node>
</interfaceDefinition>

We could also omit the <command> element under the <node>, in which case only the subcommands could be executed.

Hmm, maybe we should add it to the show command instead. Since command definitions can be broken up into multiple files, we can simply wrap a show node declaration around it and it would be automatically included with the rest of the subcommands. Let's call this show-calendar.xml instead. (Note that the name of the file has no effect on the functionality.)

<?xml version="1.0"?>
<interfaceDefinition>
<node name="show">
<children>
<node name="calendar">
<properties>
<help>Show the monthly calendar</help>
</properties>
<command>/usr/bin/cal</command> <!-- $ show calendar -->
<children>
<leafNode name="year">
<properties>
<help>Show the yearly calendar</help>
</properties>
<command>/usr/bin/cal -y</command> <!-- $ show calendar year -->
</leafNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>

Looks good! Now we can build VyOS with our new op mode command. Copy show-calendar.xml into /op-mode-definitions in the source tree and follow the regular package build process. The XML file will be validated and converted during packaging. show calendar and its subcommands will appear in the CLI once the package is installed.


Now let's try something more useful. For anything more complex than wrapping an existing program, we need to write dedicated Python scripts so that our script can integrate with the rest of VyOS API whenever necessary.

As a simple task, we're going to write a short script that displays the I/O scheduler of a given block device. (Note that setting the scheduler would be in the configuration mode's purview.) Let's call it show_scheduler.py.

#!/usr/bin/env python3

import argparse
import re

parser = argparse.ArgumentParser(description="display the I/O scheduler for given block device")
parser.add_argument("block", help="block device")
parser.add_argument("--all", action="store_true", help="display all possible I/O schedulers")

def get_schedulers(device):
with open('/sys/block/' + device + '/queue/scheduler') as f:
return f.readline()

def display_scheduler(device):
print(re.search("\[(.+)\]", get_schedulers(device)).group(1))

def display_all_schedulers(device):
print(re.sub('\[|\]|\n', '', get_schedulers(device)))

if __name__ == '__main__':
args = parser.parse_args()
if args.all:
display_all_schedulers(args.block)
else:
display_scheduler(args.block)

We can put this into /src/op-mode in the source tree (but don't forget to mark it as executable) and the accompanying op mode definition, show-scheduler.xml into /op-mode-definitions again.

<?xml version="1.0"?>
<interfaceDefinition>
<node name="show">
<children>
<tagNode name="scheduler">
<properties>
<help>Show the I/O scheduler for given block device</help>
<completionHelp>
<list>&lt;block&gt;</list>
</completionHelp>
</properties>
<!-- In `$ show scheduler sda', `sda' corresponds to `$3'. -->
<command>${vyos_op_scripts_dir}/show_scheduler.py "$3"</command>
<children>
<leafNode name="all">
<properties>
<help>Show all possible I/O schedulers for given block device</help>
</properties>
<!-- In `$ show scheduler sda all', `sda' corresponds to `$3'. -->
<command>${vyos_op_scripts_dir}/show_scheduler.py --all "$3"</command>
</leafNode>
</children>
</tagNode>
</children>
</node>
</interfaceDefinition>

The path of the script we wrote above is visible to the op mode definition through the $vyos_op_scripts_dir environment variable.

Now, this script works all fine but there's a fine detail we're missing here: Even though all subcommands have tab completion helpers, we couldn't do the same thing for block device arguments here (other than a simple <block>, which isn't too helpful here), because we don't know them beforehand. Fortunately, we can generate the completions at runtime!

Let's call our completion script list_blocks.py and put it in /src/completion in the source tree (and again mark it as executable).

#!/usr/bin/env python3

import os

for f in sorted(os.listdir("/sys/block")):
print(f)

And we need to adjust the op mode definition to add the completion script:

<?xml version="1.0"?>
<interfaceDefinition>
<node name="show">
<children>
<tagNode name="scheduler">
<properties>
<help>Show the I/O scheduler for given block device</help>
<completionHelp>
<list>&lt;block&gt;</list>
<script>${vyos_completion_dir}/list_blocks.py</script>
</completionHelp>
</properties>
<command>${vyos_op_scripts_dir}/show_scheduler.py "$3"</command>
<children>
<leafNode name="all">
<properties>
<help>Show all possible I/O schedulers for given block device</help>
</properties>
<command>${vyos_op_scripts_dir}/show_scheduler.py --all "$3"</command>
</leafNode>
</children>
</tagNode>
</children>
</node>
</interfaceDefinition>

As you can guess, $vyos_completion_dir contains the path to our completion script. Just like <command> elements, <script> elements are run in vbash at runtime.


Of course, there's more to interface definitions than shown here. For example, you can define constraints for arguments, validate them with regexps or even dedicated validator scripts. You should consult the documentation for further details.

Comments