Adding custom operational mode commands to VyOS
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><block></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><block></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