VyOS Platform Blog

Writing migration scripts

Written by Erkin Batu Altunbas | August 24, 2021 10:36:07 PM Z

As VyOS evolves, so does its CLI. Obsolete features get deprecated over time and new features replace older ones. Thus, the configuration syntax evolves in tandem with CLI. Consequently, existing configuration can end up being incompatible with newly introduced syntax as the system gets updated, requiring manual intervention by the administrator. This is why we rely on migration scripts — scripts that convert old configuration syntaxes to newer ones during image updates. In this blogpost, we're going to explore migration scripts the way VyOS does them, with an example script for illustration.

Step 0: The task at hand

For the sake of example, we're going to examine a simple migration script written to replace the behaviour in a high-availability unit. To be specific, previously, all conntrack modules were enabled by default and they had to be disabled manually with system conntrack modules <module name> disable.

Since conntrack modules are usually too much trouble to work with (especially on larger networks), we figured it would be a good idea to disable them by default, letting the user to enable them as needed instead. Thus, the new syntax is simply system conntrack modules <module name> to enable <module name>, with everything else assumed to be disabled. This means we need to explicitly enable all modules that were not manually disabled in the previous syntax with this script.

Step 1: Implementation

Since this isn't the focus of the subject, let's quickly skim the implementation process. It should still be a good refresher on working with CLI declarations.

All configuration mode scripts are kept in /src/conf_mode in the main repository. (As you can guess, operational mode scripts go in /src/op_mode.)

We jump into conntrack.py and quickly replace

if dict_search(f'modules.{module}.disable', conntrack) != None:

with

if dict_search(f'modules.{module}', conntrack) is None:

in the module unloading logic to adjust it to the new syntax.

Next, we update the CLI itself. (See the previous post Adding custom operational mode commands to VyOS.) All configuration mode commands are defined in /interface-definitions. (Operational mode commands go in /op-mode-definitions.)

We then find system-conntrack.xml.in and replace

<node name="ftp">
<properties>
<help>FTP connection tracking settings</help>
</properties>
<children>
#include <include/conntrack-module-disable.xml.i>
</children>
</node>

with

<leafNode name="ftp">
<properties>
<help>FTP connection tracking</help>
<valueless/>
</properties>
</leafNode>

and repeat the process for every other module declared in there, and get rid of include/conntrack-module-disable.xml.in while at it. That's it. The new behavior is implemented.

Step 2: Syntax version

Each configuration component has its own syntax version. You can see them for yourself by checking the last two lines of your configuration file. On the system I'm working on right now, it looks like this:

$ tail -2 /config/config.boot
// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@3:interfaces@23:ipoe-server@1:ipsec@8:isis@1:l2tp@4:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:openconnect@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@4:system@21:vrf@3:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
// Release version: 1.4-rolling-202108150610

You can probably guess that the syntax goes name@version:name@version:.... We see here that the current conntrack syntax version is 2. This means there exists a migration script from version 1 to version 2 already and the new version will be 3.

The current versions of the components are kept in the form of empty files in /cfg-version in the vyatta-cfg-system repository.  You can see the file conntrack@2 here. All we need to do is to rename it to conntrack@3 and modify /Makefile.am accordingly.

In the Makefile, we find the line

curver_DATA += cfg-version/conntrack@2

and update it to be

curver_DATA += cfg-version/conntrack@3

Step 3: Migration script

Now, let's write the migration script itself. You can find each of them in /src/migration-scripts in the main repository. When we descend into the conntrack directory from there, we can see the existing 1-to-2 script. During the migration process, each script is run back-to-back until the desired version is reached. That is to say, if the system version for the module foo is 2 and the version in the new image is 4, the scripts foo/2-to-3 and foo/3-to-4 will run in sequence during migration.

So, let's write a 2-to-3 script. Each migration script is simply a Python script that reads a configuration file (given as an argument) and overwrites it with an updated one.

We recite the following boilerplate, with a little documentation blurb at the top, to read the file and convert it into the internal ConfigTree data structure for easy manipulation:

#!/usr/bin/env python3

# Conntrack syntax version 3
# Enables all conntrack modules (previous default behaviour) and omits manually disabled modules.

import sys
from vyos.configtree import ConfigTree

if len(sys.argv) < 1:
print('Must specify file name!')
sys.exit(1)

filename = sys.argv[1]

with open(filename, 'r') as f:
config = ConfigTree(f.read())

Next, we replace the configuration statements in the ConfigTree.

module_path = ['system', 'conntrack', 'modules']

# Go over all conntrack modules available as of v1.3.0.
for module in ['ftp', 'h323', 'nfs', 'pptp', 'sip', 'sqlnet', 'tftp']:
# 'disable' is being phased out.
if config.exists(module_path + [module, 'disable']):
config.delete(module_path + [module])
# If it wasn't manually 'disable'd, it was enabled by default.
else:
config.set(module_path + [module])

And finally, we overwrite the configuration file.

try:
if config.exists(module_path):
with open(filename, 'w') as f:
f.write(config.to_string())
except OSError as e:
print(f'Failed to save the modified config: {e}')
sys.exit(1)

Et voilà! This script will automatically run during the update if the conntrack syntax version is below 3.

Step 4: Smoke tests

VyOS makes use of "smoke tests" in the build process to detect regressions before the image can be distributed. If the component you're working on isn't covered by a smoke test, consider writing one for it. Smoke tests go in /smoketests in the main repository and our specific component system conntrack is covered by scripts/cli/test_system_conntrack.py under it.

We simply replace the semantics by swapping

self.cli_set(base_path + ['modules', module, 'disable'])

with

self.cli_set(base_path + ['modules', module]) 

and invert the assertions so that the modules are checked to be enabled (rather than disabled) explicitly.

That's it. The unit tests will confirm that modules are not loaded by default and will be loaded with configuration commands through the CLI.