czechlight-cfg-fs: migration scripts and tests

Sometimes we need to make alterations to the startup datastore (for
instance when new feature to the YANG model is added). This commit
introduces a migration script that is to be run after the machine boots.

The persistent /cfg now stores not just the data, but also the system
SW version which produced the stored configuration. Upon boot, the
current system looks at the previous config (the startup.json file
located at /cfg/sysrepo) and its associated system version
(/cfg/sysrepo/version), and if any changes need to happen, they are
automatically applied.

This commit introduces first migration.
This initial content makes sure that there's something (like a default
channel plan, or some mandatory YANG data nodes) when booting from
factory defaults (or from a pre-versioning SW release). Since this is a
one-shot operation, the operator is free to overwrite the data (e.g.,
rewrite the channel plan), and the system will happily accept the new data
as long as they pass the YANG validation. This is different from the
previous behavior of the SW stack which would apply the defaults during
each boot, even when the configuration was restored.

We also introduce tests for these migrations scripts. They are located
under tests/czechlight-cfg-fs directory and there is a pytest runner.
Each test case is a single directory containing current startup DS
content (as JSON file), expected content after running migrations,
and three control files: One for current startup DS version (so we know
which migrations to run), one for cmdline (so both scripts
czechlight-install-yang and czechlight-migrate can distinguish between
czechlight box types in code), and xpath file used only if we want to
check for a part of the sysrepo DS content.

The tests simulate the workflow that is done when the system boots.
I.e., they install YANG models, restore startup DS backup into sysrepo
and then they run the migrations if necessary.

Change-Id: I96191630fd6b3a1ffbc5ba9f62c4a6c14ab5ed92
diff --git a/ci/build.sh b/ci/build.sh
index d7a6ada..14e73bb 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -87,6 +87,8 @@
 make -j${CI_PARALLEL_JOBS} --output-sync=target rootfs-czechlight-rauc
 mv images/update.raucb ~/zuul-output/artifacts/
 
+PATH="$PATH:$(pwd)/host/bin/" pytest -vv ${ZUUL_PROJECT_SRC_DIR}/tests/czechlight-cfg-fs/migrations.py
+
 if [[ "${ZUUL_JOB_NAME}" =~ clearfog ]]; then
     if [[ ${TRIGGERED_VIA_DEP} != 1 ]]; then
         # store a cached tarball as an artifact
diff --git a/doc/architecture.md b/doc/architecture.md
index 3abd868..c38cdfa 100644
--- a/doc/architecture.md
+++ b/doc/architecture.md
@@ -30,8 +30,9 @@
 The required modules (and configuration) is added in several steps:
 
 - the YANG modules for `netopeer2-server` are added via `netopeer2-install-yang.service` (via our Buildroot patches),
-- CzechLight-specific YANG modules and their initial data are added via [`czechlight-install-yang.service`](../package/czechlight-cfg-fs/czechlight-install-yang.service),
+- CzechLight-specific YANG modules are added via [`czechlight-install-yang.service`](../package/czechlight-cfg-fs/czechlight-install-yang.service),
 - system configuration is restored from the persistent location in `/cfg` via [`cfg-restore-sysrepo.service`](../package/czechlight-cfg-fs/cfg-restore-sysrepo.service),
+- migrations to system configuration are applied via [`czechlight-migrate.service`](../package/czechlight-cfg-fs/czechlight-migrate.service),
 - configuration of the Netopeer server gets re-checked via `netopeer2-setup.service` (once again in our Buildroot patches); this is needed especially during the first boot with no previous configuration to restore,
 - configuration of NACM is applied via [`nacm-restore.service`](../package/czechlight-cfg-fs/nacm-restore.service),
 - finally, any daemons that use sysrepo are started.
diff --git a/package/cla-sysrepo/cla-appliance.service.in b/package/cla-sysrepo/cla-appliance.service.in
index b3ef5b0..b01a7a1 100644
--- a/package/cla-sysrepo/cla-appliance.service.in
+++ b/package/cla-sysrepo/cla-appliance.service.in
@@ -1,9 +1,9 @@
 [Unit]
 Description=CzechLight __MODEL__ driver
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 Before=rauc-mark-good.service velia-hardware-g1.service velia-hardware-g2.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 StartLimitIntervalSec=0
 ConditionKernelCommandLine=|czechlight=__MODEL__
 ConditionKernelCommandLine=|czechlight=__MODEL__-g2
diff --git a/package/czechlight-cfg-fs/czechlight-cfg-fs.mk b/package/czechlight-cfg-fs/czechlight-cfg-fs.mk
index 775b088..5b21b97 100644
--- a/package/czechlight-cfg-fs/czechlight-cfg-fs.mk
+++ b/package/czechlight-cfg-fs/czechlight-cfg-fs.mk
@@ -62,6 +62,17 @@
 			$(TARGET_DIR)/usr/lib/systemd/system/
 		ln -sf ../cfg-restore-systemd-network.service $(TARGET_DIR)/usr/lib/systemd/system/network-pre.target.wants/
 	$(endif)
+
+	$(INSTALL) -D -m 0755 -t $(TARGET_DIR)/usr/libexec/czechlight-cfg-fs \
+		$(BR2_EXTERNAL_CZECHLIGHT_PATH)/package/czechlight-cfg-fs/czechlight-migrate.sh \
+		$(BR2_EXTERNAL_CZECHLIGHT_PATH)/package/czechlight-cfg-fs/czechlight-migration-list.sh
+
+	$(INSTALL) -D -m 0644 -t $(TARGET_DIR)/usr/libexec/czechlight-cfg-fs/migrations \
+		$(BR2_EXTERNAL_CZECHLIGHT_PATH)/package/czechlight-cfg-fs/migrations/*
+
+	$(INSTALL) -D -m 0644 -t $(TARGET_DIR)/usr/lib/systemd/system/ \
+		$(BR2_EXTERNAL_CZECHLIGHT_PATH)/package/czechlight-cfg-fs/czechlight-migrate.service
+	ln -sf ../czechlight-migrate.service $(TARGET_DIR)/usr/lib/systemd/system/multi-user.target.wants/
 endef
 
 # Configure OpenSSH to look for *user* keys in the /cfg
diff --git a/package/czechlight-cfg-fs/czechlight-install-yang.service b/package/czechlight-cfg-fs/czechlight-install-yang.service
index e34aee2..80e89fc 100644
--- a/package/czechlight-cfg-fs/czechlight-install-yang.service
+++ b/package/czechlight-cfg-fs/czechlight-install-yang.service
@@ -2,7 +2,7 @@
 Description=Install CzechLight YANG models
 After=netopeer2-install-yang.service cfg.mount
 Requires=netopeer2-install-yang.service cfg.mount
-Before=netopeer2.service cfg-restore-sysrepo.service
+Before=netopeer2.service cfg-restore-sysrepo.service czechlight-migrate.service
 
 [Service]
 Type=oneshot
diff --git a/package/czechlight-cfg-fs/czechlight-install-yang.sh b/package/czechlight-cfg-fs/czechlight-install-yang.sh
index 6c9f829..77c805d 100755
--- a/package/czechlight-cfg-fs/czechlight-install-yang.sh
+++ b/package/czechlight-cfg-fs/czechlight-install-yang.sh
@@ -58,23 +58,19 @@
         done
     fi
     sysrepoctl --search-dirs ${CLA_YANG} --install ${CLA_YANG}/czechlight-roadm-device@2021-03-05.yang ${FEATURE_ARGS} --permissions 0660
-    sysrepocfg --datastore=startup --format=json --module=czechlight-roadm-device --import="${CLA_YANG}/${INITIAL_DATA}.json"
 fi
 
 if [[ ${YANG_COHERENT} == 1 ]]; then
     sysrepoctl --search-dirs ${CLA_YANG} --install ${CLA_YANG}/czechlight-coherent-add-drop@2021-03-05.yang --permissions 0660
-    sysrepocfg --datastore=startup --format=json --module=czechlight-coherent-add-drop --new-data="${CLA_YANG}/${INITIAL_DATA}.json"
     sysrepoctl --change czechlight-coherent-add-drop --permissions 0660
 fi
 
 if [[ ${YANG_INLINE} == 1 ]]; then
     sysrepoctl --search-dirs ${CLA_YANG} --install ${CLA_YANG}/czechlight-inline-amp@2021-03-05.yang --permissions 0660
-    sysrepocfg --datastore=startup --format=json --module=czechlight-inline-amp --import="${CLA_YANG}/${INITIAL_DATA}.json"
 fi
 
 if [[ ${YANG_CALIBRATION} == 1 ]]; then
     sysrepoctl --search-dirs ${CLA_YANG} --install ${CLA_YANG}/czechlight-calibration-device@2019-06-25.yang --permissions 0660
-    sysrepocfg --datastore=startup --format=json --module=czechlight-calibration-device --import="${CLA_YANG}/${INITIAL_DATA}.json"
 fi
 
 sysrepoctl --search-dirs ${VELIA_YANG} --install ${VELIA_YANG}/ietf-system@2014-08-06.yang --permissions 0660
@@ -93,6 +89,3 @@
 
 sysrepoctl --search-dirs ${VELIA_YANG} --install ${VELIA_YANG}/czechlight-firewall@2021-01-25.yang --permissions 0600
 sysrepoctl --change ietf-access-control-list --enable-feature match-on-eth --enable-feature eth --enable-feature match-on-ipv4 --enable-feature ipv4 --enable-feature match-on-ipv6 --enable-feature ipv6 --enable-feature mixed-eth-ipv4-ipv6
-
-# If not do not copy here from startup -> running, running might be stale.
-sysrepocfg -C startup
diff --git a/package/czechlight-cfg-fs/czechlight-migrate.service b/package/czechlight-cfg-fs/czechlight-migrate.service
new file mode 100644
index 0000000..7e58d4d
--- /dev/null
+++ b/package/czechlight-cfg-fs/czechlight-migrate.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Migrate CzechLight YANG models startup data
+After=netopeer2-install-yang.service czechlight-install-yang.service cfg-restore-sysrepo.service cfg.mount
+Requires=netopeer2-install-yang.service czechlight-install-yang.service cfg-restore-sysrepo.service cfg.mount
+Before=netopeer2-setup.service netopeer2.service sysrepo-persistent-cfg.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/bash /usr/libexec/czechlight-cfg-fs/czechlight-migrate.sh
+Group=sysrepo
+
+[Install]
+WantedBy=multi-user.target
diff --git a/package/czechlight-cfg-fs/czechlight-migrate.sh b/package/czechlight-cfg-fs/czechlight-migrate.sh
new file mode 100755
index 0000000..fd49f60
--- /dev/null
+++ b/package/czechlight-cfg-fs/czechlight-migrate.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+set -ex
+
+SCRIPT_ROOT=$(dirname $(realpath -s $0))
+MIGRATIONS=$SCRIPT_ROOT/czechlight-migration-list.sh
+export MIGRATIONS_DIRECTORY=${SCRIPT_ROOT}/migrations
+CFG_VERSION_FILE="${CFG_VERSION_FILE:-/cfg/sysrepo/version}"
+CFG_STARTUP_FILE="${CFG_STARTUP_FILE:-/cfg/sysrepo/startup.json}"
+PROC_CMDLINE="${PROC_CMDLINE:-/proc/cmdline}"
+
+export CLA_YANG="${CLA_YANG:-/usr/share/cla-sysrepo/yang}"
+export VELIA_YANG="${VELIA_YANG:-/usr/share/velia/yang}"
+
+# load migrations and perform a sanity check (filename's numerical prefix corresponds to the order in the MIGRATIONS array)
+source $MIGRATIONS
+for i in $(seq ${#MIGRATION_FILES[@]}); do
+    FILENAME=${MIGRATION_FILES[$(($i - 1))]}
+
+    if ! [[ "$FILENAME" =~ ^[0-9]{4}_.*.sh$ ]]; then
+        echo "Migration file '$FILENAME' has unexpected name"
+        exit 1
+    fi
+
+    FILE_REV=$(echo "$FILENAME" | grep -Eo "^[0-9]{4}")
+    if [[ $((FILE_REV + 0)) != $i ]]; then
+        echo "Migration filename '$FILENAME' hints revision $(($FILE_REV + 0)) but it is at position $i of the migration array"
+        exit 1
+    fi
+done
+
+for ARG in $(cat "$PROC_CMDLINE"); do
+    case "${ARG}" in
+        czechlight=*)
+            export CZECHLIGHT="${ARG:11}"
+            ;;
+    esac
+done
+
+case "${CZECHLIGHT}" in
+    sdn-roadm-line*)
+        export YANG_ROADM=1
+        ;;
+    sdn-roadm-add-drop*)
+        export YANG_ROADM=1
+        ;;
+    sdn-roadm-hires-add-drop*)
+        export YANG_ROADM=1
+        ;;
+    sdn-roadm-coherent-a-d*)
+        export YANG_COHERENT=1
+        ;;
+    sdn-inline*)
+        export YANG_INLINE=1
+        ;;
+    calibration-box)
+        export YANG_CALIBRATION=1
+        ;;
+esac
+
+
+# we might end up on the system
+# * that was created before the migrations were introduced; such system does not have ${CFG_VERSION_FILE}
+# * that was just created and freshly initialized with firmware; it has ${CFG_VERSION_FILE} but startup.json does not exist
+if [[ -r "$CFG_VERSION_FILE" && -f "$CFG_STARTUP_FILE" ]]; then
+    CURRENT_VERSION="$(cat ${CFG_VERSION_FILE})"
+else
+    CURRENT_VERSION=0
+fi
+
+if [[ ! "$CURRENT_VERSION" =~ ^[0-9]+$ ]]; then
+    echo "Invalid version '$CURRENT_VERSION'"
+    exit 1
+fi
+
+while [[ $CURRENT_VERSION -lt ${#MIGRATION_FILES[@]} ]]; do
+    /bin/bash ${SCRIPT_ROOT}/migrations/${MIGRATION_FILES[$CURRENT_VERSION]}
+    CURRENT_VERSION=$(($CURRENT_VERSION + 1))
+done
+
+# store current version and save startup.json
+sysrepocfg -d startup -f json -X > "$CFG_STARTUP_FILE"
+echo "$CURRENT_VERSION" > "$CFG_VERSION_FILE"
+
+# If not do not copy here from startup -> running, running might be stale.
+sysrepocfg -C startup
diff --git a/package/czechlight-cfg-fs/czechlight-migration-list.sh b/package/czechlight-cfg-fs/czechlight-migration-list.sh
new file mode 100644
index 0000000..9b5a49d
--- /dev/null
+++ b/package/czechlight-cfg-fs/czechlight-migration-list.sh
@@ -0,0 +1,5 @@
+MIGRATION_FILES=(
+    '0001_initial-data.sh'
+)
+
+
diff --git a/package/czechlight-cfg-fs/migrations/0001_initial-data.sh b/package/czechlight-cfg-fs/migrations/0001_initial-data.sh
new file mode 100644
index 0000000..8b81ccb
--- /dev/null
+++ b/package/czechlight-cfg-fs/migrations/0001_initial-data.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Load initial data
+# -----------------
+# These data are imported into the sysrepo's startup datastore only once. This happens either when the box is restored to
+# its factory settings (the box is new and boots for the first time or someone deletes the startup.json backup in /cfg)
+# or when the box is upgraded from the state before the migrations were introduced (versions released before July 2022).
+#
+# It's OK for user to remove these settings from sysrepo startup DS.
+# However, the data will NEVER get restored by us (unless somebody deletes /cfg/startup.json, see above).
+
+case "${CZECHLIGHT}" in
+    sdn-roadm-line*)
+        sysrepocfg --datastore=startup --format=json --module=czechlight-roadm-device --import="${CLA_YANG}/sdn-roadm-line.json"
+        ;;
+    sdn-roadm-add-drop*)
+        ;& # fallthrough
+    sdn-roadm-hires-add-drop*)
+        sysrepocfg --datastore=startup --format=json --module=czechlight-roadm-device --import="${CLA_YANG}/sdn-roadm-add-drop.json"
+        ;;
+    sdn-roadm-coherent-a-d*)
+        sysrepocfg --datastore=startup --format=json --module=czechlight-coherent-add-drop --import="${CLA_YANG}/sdn-roadm-coherent-a-d.json"
+        ;;
+    sdn-inline*)
+        sysrepocfg --datastore=startup --format=json --module=czechlight-inline-amp --import="${CLA_YANG}/sdn-inline.json"
+        ;;
+    calibration-box)
+        sysrepocfg --datastore=startup --format=json --module=czechlight-calibration-device --import="${CLA_YANG}/calibration-box.json"
+        ;;
+esac
diff --git a/package/czechlight-cfg-fs/nacm-restore.service b/package/czechlight-cfg-fs/nacm-restore.service
index 2cb3dec..4f5e5b9 100644
--- a/package/czechlight-cfg-fs/nacm-restore.service
+++ b/package/czechlight-cfg-fs/nacm-restore.service
@@ -1,7 +1,7 @@
 [Unit]
 Description=Restore NACM rules
-After=netopeer2-install-yang.service czechlight-install-yang.service cfg.mount
-Requires=netopeer2-install-yang.service czechlight-install-yang.service cfg.mount
+After=netopeer2-install-yang.service czechlight-migrate.service cfg.mount
+Requires=netopeer2-install-yang.service czechlight-migrate.service cfg.mount
 Before=netopeer2-setup.service netopeer2.service sysrepo-persistent-cfg.service
 
 [Service]
diff --git a/package/czechlight-cfg-fs/sysrepo-persistent-cfg.service b/package/czechlight-cfg-fs/sysrepo-persistent-cfg.service
index 007bd7b..25991f4 100644
--- a/package/czechlight-cfg-fs/sysrepo-persistent-cfg.service
+++ b/package/czechlight-cfg-fs/sysrepo-persistent-cfg.service
@@ -1,10 +1,11 @@
 [Unit]
 Description=Persisting persistent sysrepo datastores to /cfg
-After=netopeer2.service cfg.mount
-Requires=netopeer2.service cfg.mount
+After=netopeer2.service cfg.mount czechlight-migrate.service
+Requires=netopeer2.service cfg.mount czechlight-migrate.service
 
 [Service]
 Type=simple
 UMask=0077
-ExecStart=/bin/sh -c 'while true; do inotifywait -e CLOSE_WRITE /etc/sysrepo/data/*.startup && mkdir -p /cfg/sysrepo/ && sysrepocfg -d startup -f json -X > /cfg/sysrepo/startup.json; done'
+ExecStartPre=/bin/sh -c 'mkdir -p /cfg/sysrepo/ && sysrepocfg -d startup -f json -X > /cfg/sysrepo/startup.json'
+ExecStart=/bin/sh -c 'while true; do inotifywait -e CLOSE_WRITE /etc/sysrepo/data/*.startup && sysrepocfg -d startup -f json -X > /cfg/sysrepo/startup.json; done'
 Group=sysrepo
diff --git a/package/rousette/rousette.service b/package/rousette/rousette.service
index cb79d27..ba9efbb 100644
--- a/package/rousette/rousette.service
+++ b/package/rousette/rousette.service
@@ -1,8 +1,8 @@
 [Unit]
 Description=RESTCONFish server
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 
 [Service]
 Type=simple
diff --git a/package/velia/velia-firewall.service b/package/velia/velia-firewall.service
index 1fdb318..e5dd1d6 100644
--- a/package/velia/velia-firewall.service
+++ b/package/velia/velia-firewall.service
@@ -1,9 +1,9 @@
 [Unit]
 Description=Firewall management via sysrepo
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 Before=rauc-mark-good.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 ConditionKernelCommandLine=czechlight
 
 [Service]
diff --git a/package/velia/velia-hardware-g1.service b/package/velia/velia-hardware-g1.service
index 6e09df3..cb0cd30 100644
--- a/package/velia/velia-hardware-g1.service
+++ b/package/velia/velia-hardware-g1.service
@@ -1,9 +1,9 @@
 [Unit]
 Description=Tracking hardware metrics
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 Before=rauc-mark-good.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 ConditionKernelCommandLine=|czechlight=sdn-inline
 ConditionKernelCommandLine=|czechlight=sdn-roadm-add-drop
 ConditionKernelCommandLine=|czechlight=sdn-roadm-coherent-a-d
diff --git a/package/velia/velia-hardware-g2.service b/package/velia/velia-hardware-g2.service
index 84342b7..8a58eab 100644
--- a/package/velia/velia-hardware-g2.service
+++ b/package/velia/velia-hardware-g2.service
@@ -1,9 +1,9 @@
 [Unit]
 Description=Tracking hardware metrics
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 Before=rauc-mark-good.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 ConditionKernelCommandLine=|czechlight=sdn-inline-g2
 ConditionKernelCommandLine=|czechlight=sdn-roadm-add-drop-g2
 ConditionKernelCommandLine=|czechlight=sdn-roadm-coherent-a-d-g2
diff --git a/package/velia/velia-system.service b/package/velia/velia-system.service
index 6963e91..113be5c 100644
--- a/package/velia/velia-system.service
+++ b/package/velia/velia-system.service
@@ -1,9 +1,9 @@
 [Unit]
 Description=System management via sysrepo
-After=syslog.target network.target czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+After=syslog.target network.target cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 Before=rauc-mark-good.service
 PartOf=netopeer2.service
-Requires=czechlight-install-yang.service cfg-restore-sysrepo.service nacm-restore.service
+Requires=cfg-restore-sysrepo.service czechlight-migrate.service nacm-restore.service
 ConditionKernelCommandLine=czechlight
 
 [Service]
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/cmdline b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/cmdline
new file mode 100644
index 0000000..7231fc3
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/cmdline
@@ -0,0 +1 @@
+czechlight=sdn-inline-g2
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/expected.json b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/expected.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/expected.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/startup.json b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/startup.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/startup.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/version b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/version
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_empty/version
@@ -0,0 +1 @@
+0
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/cmdline b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/cmdline
new file mode 100644
index 0000000..17d977e
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/cmdline
@@ -0,0 +1 @@
+czechlight=sdn-inline
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/expected.json b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/expected.json
new file mode 100644
index 0000000..52a1f53
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/expected.json
@@ -0,0 +1,113 @@
+{
+  "ietf-interfaces:interfaces": {
+    "interface": [
+      {
+        "name": "br0",
+        "type": "iana-if-type:bridge",
+        "ietf-ip:ipv4": {
+          "czechlight-network:dhcp-client": true
+        },
+        "ietf-ip:ipv6": {
+          "enabled": true,
+          "autoconf": {
+            "create-global-addresses": true
+          }
+        }
+      },
+      {
+        "name": "eth1",
+        "type": "iana-if-type:ethernetCsmacd",
+        "czechlight-network:bridge": "br0"
+      },
+      {
+        "name": "eth0",
+        "type": "iana-if-type:ethernetCsmacd",
+        "czechlight-network:bridge": "br0"
+      }
+    ]
+  },
+  "ietf-keystore:keystore": {
+    "asymmetric-keys": {
+      "asymmetric-key": [
+        {
+          "name": "genkey",
+          "algorithm": "rsa2048",
+          "public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/tnburJOGHcvSOKw7SdOfEvZD28WatHUfuWRCWZoElXtX1sNkBFXvRbKahOR8sZ221iRdlZ89bBVtTeX8N2wgPApy+Xi/9X1Icee2cU7QC7UfnNnWMW1C6WEZQS8zt+6IAQsOLk1AWyFeAN1EZQfjM/2UfxOW/kxXBu+MeVvdjouZYs73mW4OyV6VptMkAk4JDk5ieEp65p/rQOF0pIvpnu56nl9cybQ1s3DAQ76ZJP5qf8VdKS2WcBlS7UwXAMvXPM5TiIebnXtrF8jTd3S3ApOyZv00jxn/s5l6L1VA0p5HyB9W4DfNztINRyE56BEBvPeJePNWwNg+sVClLdmQIDAQAB",
+          "private-key": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz+2du6sk4Ydy9I4rDtJ058S9kPbxZq0dR+5ZEJZmgSVe1fWw2QEVe9FspqE5HyxnbbWJF2Vnz1sFW1N5fw3bCA8CnL5eL/1fUhx57ZxTtALtR+c2dYxbULpYRlBLzO37ogBCw4uTUBbIV4A3URlB+Mz/ZR/E5b+TFcG74x5W92Oi5lizveZbg7JXpWm0yQCTgkOTmJ4Snrmn+tA4XSki+me7nqeX1zJtDWzcMBDvpkk/mp/xV0pLZZwGVLtTBcAy9c8zlOIh5ude2sXyNN3dLcCk7Jm/TSPGf+zmXovVUDSnkfIH1bgN83O0g1HITnoEQG894l481bA2D6xUKUt2ZAgMBAAECggEBAJlzS2ih/RV5On54AyOAplx0afeJO0EoaxJW8nL/q8+hOIqgeNZ4Taz7oY4O1U8Ytt8Cj7sF6U+gVg72RvJW8LXSBTuFtCvEdNnaqf6EkK7Q5OSrmscJaLlXTtOF7/I5U6ZfKdvmVzr98Cv+b0wA9zfh8hoK4HwcDmzXw4WQhPbqvp47HPrmq/cCfAVR0cD/T6ZlS1zWU/6xXz9vIA9GmXiO+xrVD5TS5i4DVgZL+uY4a/IJkP/FmQL8lu9mMcR3EZ2SRQD3FZysF7OoWfT7/L03qCj/32RegwA8FPQsyYOAos0m1J5Cp+fUGF9HnqNohBoRcDuh0DYY0pc9PQq/MAECgYEA3oPpuLQLM+AA1LBwlLeK0UaxQ3hQtaLmSwOtMVPrCqe9NavzJPzhaYeUT6+QPeDhl+/zwuPV2NqjG6kdnX66BCDTp6zy8BFs3yzTazOp6hhi2RlzWGeCAhcjmbR4+YMOQx5RW4MocRycu5dTMnOJU3aDZJQd5AoxtXhkW7VsCIECgYEAzxD1SU2ApRlNT2FZ5ZaKg2zEqh/Q+iLB3ORra8n1EyxIl/smS33+L1Qd0z/tnlLu6Hr+nYAtnhTgssFTPXq2X0+CURQA85mlQ+W8pstsckXHl+5ZYeqRaE+A5irimFJVNS8iU751HlvxrsgpmnkkqoS7pgIZeYFIj93boN25iRkCgYBzxKvoBfxCHAwynC2moiueICp/0Owk5EDuzFXicSe7XnQJpZGWL99TGU/neY8RMpwMgbIQNpt+/JmZ/Y3D8Df1h+K2vTuT/WzoXBkPEE0Z12AuVZh2aTvxvHowMP4zyqSz9OUdOIrk9p7w1pVZjVToUKOhw5Idn9Qm9yE3uZcggQKBgFNVKNIKXdaT2zgyHqK2NiJ0OKWazliMemayisTDb1z6+nAnxB6now9bE/G7jZ6lE77+GNA8EJ/JGTD3es+IPjMXZYPJwCRJVmh/4a9iuM84Y/4yD0jHxSMss9xtr+nLYL1Zrb/0K2A/coF7zrp7PJVfe8FwoN30eMZlbAlOkmepAoGBAIHPHBIgfSaUJziqqrlqygbZkMhN69k8C5chQJCEl8JfTlr+aX7Gz2+pWpuQoE/JVAZO8w5p608TIavZuy5bANJ0RnDg6EVWuvzpMw92G9OPV30dCy+WmEHaoEkvtl7kIpHdxN0MCFVM7eY14qcxB4p1eRWgJZFBQLa3GuGK5GVX"
+        }
+      ]
+    }
+  },
+  "ietf-netconf-acm:nacm": {
+    "rule-list": [
+      {
+        "name": "Allow DWDM control to the optics group",
+        "group": [
+          "optics"
+        ],
+        "rule": [
+          {
+            "name": "czechlight-roadm-device",
+            "module-name": "czechlight-roadm-device",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-inline-amp",
+            "module-name": "czechlight-inline-amp",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-coherent-add-drop",
+            "module-name": "czechlight-coherent-add-drop",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-calibration-device",
+            "module-name": "czechlight-calibration-device",
+            "action": "permit"
+          }
+        ]
+      }
+    ]
+  },
+  "ietf-netconf-server:netconf-server": {
+    "listen": {
+      "endpoint": [
+        {
+          "name": "default-ssh",
+          "ssh": {
+            "tcp-server-parameters": {
+              "local-address": "::",
+              "keepalives": {
+                "idle-time": 1,
+                "max-probes": 10,
+                "probe-interval": 5
+              }
+            },
+            "ssh-server-parameters": {
+              "server-identity": {
+                "host-key": [
+                  {
+                    "name": "default-key",
+                    "public-key": {
+                      "keystore-reference": "genkey"
+                    }
+                  }
+                ]
+              },
+              "client-authentication": {
+                "supported-authentication-methods": {
+                  "publickey": [null],
+                  "passsword": [null],
+                  "other": [
+                    "interactive"
+                  ]
+                }
+              }
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/startup.json b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/startup.json
new file mode 100644
index 0000000..52a1f53
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/startup.json
@@ -0,0 +1,113 @@
+{
+  "ietf-interfaces:interfaces": {
+    "interface": [
+      {
+        "name": "br0",
+        "type": "iana-if-type:bridge",
+        "ietf-ip:ipv4": {
+          "czechlight-network:dhcp-client": true
+        },
+        "ietf-ip:ipv6": {
+          "enabled": true,
+          "autoconf": {
+            "create-global-addresses": true
+          }
+        }
+      },
+      {
+        "name": "eth1",
+        "type": "iana-if-type:ethernetCsmacd",
+        "czechlight-network:bridge": "br0"
+      },
+      {
+        "name": "eth0",
+        "type": "iana-if-type:ethernetCsmacd",
+        "czechlight-network:bridge": "br0"
+      }
+    ]
+  },
+  "ietf-keystore:keystore": {
+    "asymmetric-keys": {
+      "asymmetric-key": [
+        {
+          "name": "genkey",
+          "algorithm": "rsa2048",
+          "public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/tnburJOGHcvSOKw7SdOfEvZD28WatHUfuWRCWZoElXtX1sNkBFXvRbKahOR8sZ221iRdlZ89bBVtTeX8N2wgPApy+Xi/9X1Icee2cU7QC7UfnNnWMW1C6WEZQS8zt+6IAQsOLk1AWyFeAN1EZQfjM/2UfxOW/kxXBu+MeVvdjouZYs73mW4OyV6VptMkAk4JDk5ieEp65p/rQOF0pIvpnu56nl9cybQ1s3DAQ76ZJP5qf8VdKS2WcBlS7UwXAMvXPM5TiIebnXtrF8jTd3S3ApOyZv00jxn/s5l6L1VA0p5HyB9W4DfNztINRyE56BEBvPeJePNWwNg+sVClLdmQIDAQAB",
+          "private-key": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz+2du6sk4Ydy9I4rDtJ058S9kPbxZq0dR+5ZEJZmgSVe1fWw2QEVe9FspqE5HyxnbbWJF2Vnz1sFW1N5fw3bCA8CnL5eL/1fUhx57ZxTtALtR+c2dYxbULpYRlBLzO37ogBCw4uTUBbIV4A3URlB+Mz/ZR/E5b+TFcG74x5W92Oi5lizveZbg7JXpWm0yQCTgkOTmJ4Snrmn+tA4XSki+me7nqeX1zJtDWzcMBDvpkk/mp/xV0pLZZwGVLtTBcAy9c8zlOIh5ude2sXyNN3dLcCk7Jm/TSPGf+zmXovVUDSnkfIH1bgN83O0g1HITnoEQG894l481bA2D6xUKUt2ZAgMBAAECggEBAJlzS2ih/RV5On54AyOAplx0afeJO0EoaxJW8nL/q8+hOIqgeNZ4Taz7oY4O1U8Ytt8Cj7sF6U+gVg72RvJW8LXSBTuFtCvEdNnaqf6EkK7Q5OSrmscJaLlXTtOF7/I5U6ZfKdvmVzr98Cv+b0wA9zfh8hoK4HwcDmzXw4WQhPbqvp47HPrmq/cCfAVR0cD/T6ZlS1zWU/6xXz9vIA9GmXiO+xrVD5TS5i4DVgZL+uY4a/IJkP/FmQL8lu9mMcR3EZ2SRQD3FZysF7OoWfT7/L03qCj/32RegwA8FPQsyYOAos0m1J5Cp+fUGF9HnqNohBoRcDuh0DYY0pc9PQq/MAECgYEA3oPpuLQLM+AA1LBwlLeK0UaxQ3hQtaLmSwOtMVPrCqe9NavzJPzhaYeUT6+QPeDhl+/zwuPV2NqjG6kdnX66BCDTp6zy8BFs3yzTazOp6hhi2RlzWGeCAhcjmbR4+YMOQx5RW4MocRycu5dTMnOJU3aDZJQd5AoxtXhkW7VsCIECgYEAzxD1SU2ApRlNT2FZ5ZaKg2zEqh/Q+iLB3ORra8n1EyxIl/smS33+L1Qd0z/tnlLu6Hr+nYAtnhTgssFTPXq2X0+CURQA85mlQ+W8pstsckXHl+5ZYeqRaE+A5irimFJVNS8iU751HlvxrsgpmnkkqoS7pgIZeYFIj93boN25iRkCgYBzxKvoBfxCHAwynC2moiueICp/0Owk5EDuzFXicSe7XnQJpZGWL99TGU/neY8RMpwMgbIQNpt+/JmZ/Y3D8Df1h+K2vTuT/WzoXBkPEE0Z12AuVZh2aTvxvHowMP4zyqSz9OUdOIrk9p7w1pVZjVToUKOhw5Idn9Qm9yE3uZcggQKBgFNVKNIKXdaT2zgyHqK2NiJ0OKWazliMemayisTDb1z6+nAnxB6now9bE/G7jZ6lE77+GNA8EJ/JGTD3es+IPjMXZYPJwCRJVmh/4a9iuM84Y/4yD0jHxSMss9xtr+nLYL1Zrb/0K2A/coF7zrp7PJVfe8FwoN30eMZlbAlOkmepAoGBAIHPHBIgfSaUJziqqrlqygbZkMhN69k8C5chQJCEl8JfTlr+aX7Gz2+pWpuQoE/JVAZO8w5p608TIavZuy5bANJ0RnDg6EVWuvzpMw92G9OPV30dCy+WmEHaoEkvtl7kIpHdxN0MCFVM7eY14qcxB4p1eRWgJZFBQLa3GuGK5GVX"
+        }
+      ]
+    }
+  },
+  "ietf-netconf-acm:nacm": {
+    "rule-list": [
+      {
+        "name": "Allow DWDM control to the optics group",
+        "group": [
+          "optics"
+        ],
+        "rule": [
+          {
+            "name": "czechlight-roadm-device",
+            "module-name": "czechlight-roadm-device",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-inline-amp",
+            "module-name": "czechlight-inline-amp",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-coherent-add-drop",
+            "module-name": "czechlight-coherent-add-drop",
+            "action": "permit"
+          },
+          {
+            "name": "czechlight-calibration-device",
+            "module-name": "czechlight-calibration-device",
+            "action": "permit"
+          }
+        ]
+      }
+    ]
+  },
+  "ietf-netconf-server:netconf-server": {
+    "listen": {
+      "endpoint": [
+        {
+          "name": "default-ssh",
+          "ssh": {
+            "tcp-server-parameters": {
+              "local-address": "::",
+              "keepalives": {
+                "idle-time": 1,
+                "max-probes": 10,
+                "probe-interval": 5
+              }
+            },
+            "ssh-server-parameters": {
+              "server-identity": {
+                "host-key": [
+                  {
+                    "name": "default-key",
+                    "public-key": {
+                      "keystore-reference": "genkey"
+                    }
+                  }
+                ]
+              },
+              "client-authentication": {
+                "supported-authentication-methods": {
+                  "publickey": [null],
+                  "passsword": [null],
+                  "other": [
+                    "interactive"
+                  ]
+                }
+              }
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/version b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/version
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-inline_somedata/version
@@ -0,0 +1 @@
+0
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/cmdline b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/cmdline
new file mode 100644
index 0000000..6b1d906
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/cmdline
@@ -0,0 +1 @@
+czechlight=sdn-roadm-line
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/expected.json b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/expected.json
new file mode 100644
index 0000000..4ab1a31
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/expected.json
@@ -0,0 +1,732 @@
+{
+  "czechlight-roadm-device:channel-plan": {
+    "channel": [
+      {
+        "name": "C-band",
+        "lower-frequency": "191325000",
+        "upper-frequency": "196125000"
+      },
+      {
+        "name": "13.5",
+        "lower-frequency": "191325000",
+        "upper-frequency": "191375000"
+      },
+      {
+        "name": "14.0",
+        "lower-frequency": "191375000",
+        "upper-frequency": "191425000"
+      },
+      {
+        "name": "14.5",
+        "lower-frequency": "191425000",
+        "upper-frequency": "191475000"
+      },
+      {
+        "name": "15.0",
+        "lower-frequency": "191475000",
+        "upper-frequency": "191525000"
+      },
+      {
+        "name": "15.5",
+        "lower-frequency": "191525000",
+        "upper-frequency": "191575000"
+      },
+      {
+        "name": "16.0",
+        "lower-frequency": "191575000",
+        "upper-frequency": "191625000"
+      },
+      {
+        "name": "16.5",
+        "lower-frequency": "191625000",
+        "upper-frequency": "191675000"
+      },
+      {
+        "name": "17.0",
+        "lower-frequency": "191675000",
+        "upper-frequency": "191725000"
+      },
+      {
+        "name": "17.5",
+        "lower-frequency": "191725000",
+        "upper-frequency": "191775000"
+      },
+      {
+        "name": "18.0",
+        "lower-frequency": "191775000",
+        "upper-frequency": "191825000"
+      },
+      {
+        "name": "18.5",
+        "lower-frequency": "191825000",
+        "upper-frequency": "191875000"
+      },
+      {
+        "name": "19.0",
+        "lower-frequency": "191875000",
+        "upper-frequency": "191925000"
+      },
+      {
+        "name": "19.5",
+        "lower-frequency": "191925000",
+        "upper-frequency": "191975000"
+      },
+      {
+        "name": "20.0",
+        "lower-frequency": "191975000",
+        "upper-frequency": "192025000"
+      },
+      {
+        "name": "20.5",
+        "lower-frequency": "192025000",
+        "upper-frequency": "192075000"
+      },
+      {
+        "name": "21.0",
+        "lower-frequency": "192075000",
+        "upper-frequency": "192125000"
+      },
+      {
+        "name": "21.5",
+        "lower-frequency": "192125000",
+        "upper-frequency": "192175000"
+      },
+      {
+        "name": "22.0",
+        "lower-frequency": "192175000",
+        "upper-frequency": "192225000"
+      },
+      {
+        "name": "22.5",
+        "lower-frequency": "192225000",
+        "upper-frequency": "192275000"
+      },
+      {
+        "name": "23.0",
+        "lower-frequency": "192275000",
+        "upper-frequency": "192325000"
+      },
+      {
+        "name": "23.5",
+        "lower-frequency": "192325000",
+        "upper-frequency": "192375000"
+      },
+      {
+        "name": "24.0",
+        "lower-frequency": "192375000",
+        "upper-frequency": "192425000"
+      },
+      {
+        "name": "24.5",
+        "lower-frequency": "192425000",
+        "upper-frequency": "192475000"
+      },
+      {
+        "name": "25.0",
+        "lower-frequency": "192475000",
+        "upper-frequency": "192525000"
+      },
+      {
+        "name": "25.5",
+        "lower-frequency": "192525000",
+        "upper-frequency": "192575000"
+      },
+      {
+        "name": "26.0",
+        "lower-frequency": "192575000",
+        "upper-frequency": "192625000"
+      },
+      {
+        "name": "26.5",
+        "lower-frequency": "192625000",
+        "upper-frequency": "192675000"
+      },
+      {
+        "name": "27.0",
+        "lower-frequency": "192675000",
+        "upper-frequency": "192725000"
+      },
+      {
+        "name": "27.5",
+        "lower-frequency": "192725000",
+        "upper-frequency": "192775000"
+      },
+      {
+        "name": "28.0",
+        "lower-frequency": "192775000",
+        "upper-frequency": "192825000"
+      },
+      {
+        "name": "28.5",
+        "lower-frequency": "192825000",
+        "upper-frequency": "192875000"
+      },
+      {
+        "name": "29.0",
+        "lower-frequency": "192875000",
+        "upper-frequency": "192925000"
+      },
+      {
+        "name": "29.5",
+        "lower-frequency": "192925000",
+        "upper-frequency": "192975000"
+      },
+      {
+        "name": "30.0",
+        "lower-frequency": "192975000",
+        "upper-frequency": "193025000"
+      },
+      {
+        "name": "30.5",
+        "lower-frequency": "193025000",
+        "upper-frequency": "193075000"
+      },
+      {
+        "name": "31.0",
+        "lower-frequency": "193075000",
+        "upper-frequency": "193125000"
+      },
+      {
+        "name": "31.5",
+        "lower-frequency": "193125000",
+        "upper-frequency": "193175000"
+      },
+      {
+        "name": "32.0",
+        "lower-frequency": "193175000",
+        "upper-frequency": "193225000"
+      },
+      {
+        "name": "32.5",
+        "lower-frequency": "193225000",
+        "upper-frequency": "193275000"
+      },
+      {
+        "name": "33.0",
+        "lower-frequency": "193275000",
+        "upper-frequency": "193325000"
+      },
+      {
+        "name": "33.5",
+        "lower-frequency": "193325000",
+        "upper-frequency": "193375000"
+      },
+      {
+        "name": "34.0",
+        "lower-frequency": "193375000",
+        "upper-frequency": "193425000"
+      },
+      {
+        "name": "34.5",
+        "lower-frequency": "193425000",
+        "upper-frequency": "193475000"
+      },
+      {
+        "name": "35.0",
+        "lower-frequency": "193475000",
+        "upper-frequency": "193525000"
+      },
+      {
+        "name": "35.5",
+        "lower-frequency": "193525000",
+        "upper-frequency": "193575000"
+      },
+      {
+        "name": "36.0",
+        "lower-frequency": "193575000",
+        "upper-frequency": "193625000"
+      },
+      {
+        "name": "36.5",
+        "lower-frequency": "193625000",
+        "upper-frequency": "193675000"
+      },
+      {
+        "name": "37.0",
+        "lower-frequency": "193675000",
+        "upper-frequency": "193725000"
+      },
+      {
+        "name": "37.5",
+        "lower-frequency": "193725000",
+        "upper-frequency": "193775000"
+      },
+      {
+        "name": "38.0",
+        "lower-frequency": "193775000",
+        "upper-frequency": "193825000"
+      },
+      {
+        "name": "38.5",
+        "lower-frequency": "193825000",
+        "upper-frequency": "193875000"
+      },
+      {
+        "name": "39.0",
+        "lower-frequency": "193875000",
+        "upper-frequency": "193925000"
+      },
+      {
+        "name": "39.5",
+        "lower-frequency": "193925000",
+        "upper-frequency": "193975000"
+      },
+      {
+        "name": "40.0",
+        "lower-frequency": "193975000",
+        "upper-frequency": "194025000"
+      },
+      {
+        "name": "40.5",
+        "lower-frequency": "194025000",
+        "upper-frequency": "194075000"
+      },
+      {
+        "name": "41.0",
+        "lower-frequency": "194075000",
+        "upper-frequency": "194125000"
+      },
+      {
+        "name": "41.5",
+        "lower-frequency": "194125000",
+        "upper-frequency": "194175000"
+      },
+      {
+        "name": "42.0",
+        "lower-frequency": "194175000",
+        "upper-frequency": "194225000"
+      },
+      {
+        "name": "42.5",
+        "lower-frequency": "194225000",
+        "upper-frequency": "194275000"
+      },
+      {
+        "name": "43.0",
+        "lower-frequency": "194275000",
+        "upper-frequency": "194325000"
+      },
+      {
+        "name": "43.5",
+        "lower-frequency": "194325000",
+        "upper-frequency": "194375000"
+      },
+      {
+        "name": "44.0",
+        "lower-frequency": "194375000",
+        "upper-frequency": "194425000"
+      },
+      {
+        "name": "44.5",
+        "lower-frequency": "194425000",
+        "upper-frequency": "194475000"
+      },
+      {
+        "name": "45.0",
+        "lower-frequency": "194475000",
+        "upper-frequency": "194525000"
+      },
+      {
+        "name": "45.5",
+        "lower-frequency": "194525000",
+        "upper-frequency": "194575000"
+      },
+      {
+        "name": "46.0",
+        "lower-frequency": "194575000",
+        "upper-frequency": "194625000"
+      },
+      {
+        "name": "46.5",
+        "lower-frequency": "194625000",
+        "upper-frequency": "194675000"
+      },
+      {
+        "name": "47.0",
+        "lower-frequency": "194675000",
+        "upper-frequency": "194725000"
+      },
+      {
+        "name": "47.5",
+        "lower-frequency": "194725000",
+        "upper-frequency": "194775000"
+      },
+      {
+        "name": "48.0",
+        "lower-frequency": "194775000",
+        "upper-frequency": "194825000"
+      },
+      {
+        "name": "48.5",
+        "lower-frequency": "194825000",
+        "upper-frequency": "194875000"
+      },
+      {
+        "name": "49.0",
+        "lower-frequency": "194875000",
+        "upper-frequency": "194925000"
+      },
+      {
+        "name": "49.5",
+        "lower-frequency": "194925000",
+        "upper-frequency": "194975000"
+      },
+      {
+        "name": "50.0",
+        "lower-frequency": "194975000",
+        "upper-frequency": "195025000"
+      },
+      {
+        "name": "50.5",
+        "lower-frequency": "195025000",
+        "upper-frequency": "195075000"
+      },
+      {
+        "name": "51.0",
+        "lower-frequency": "195075000",
+        "upper-frequency": "195125000"
+      },
+      {
+        "name": "51.5",
+        "lower-frequency": "195125000",
+        "upper-frequency": "195175000"
+      },
+      {
+        "name": "52.0",
+        "lower-frequency": "195175000",
+        "upper-frequency": "195225000"
+      },
+      {
+        "name": "52.5",
+        "lower-frequency": "195225000",
+        "upper-frequency": "195275000"
+      },
+      {
+        "name": "53.0",
+        "lower-frequency": "195275000",
+        "upper-frequency": "195325000"
+      },
+      {
+        "name": "53.5",
+        "lower-frequency": "195325000",
+        "upper-frequency": "195375000"
+      },
+      {
+        "name": "54.0",
+        "lower-frequency": "195375000",
+        "upper-frequency": "195425000"
+      },
+      {
+        "name": "54.5",
+        "lower-frequency": "195425000",
+        "upper-frequency": "195475000"
+      },
+      {
+        "name": "55.0",
+        "lower-frequency": "195475000",
+        "upper-frequency": "195525000"
+      },
+      {
+        "name": "55.5",
+        "lower-frequency": "195525000",
+        "upper-frequency": "195575000"
+      },
+      {
+        "name": "56.0",
+        "lower-frequency": "195575000",
+        "upper-frequency": "195625000"
+      },
+      {
+        "name": "56.5",
+        "lower-frequency": "195625000",
+        "upper-frequency": "195675000"
+      },
+      {
+        "name": "57.0",
+        "lower-frequency": "195675000",
+        "upper-frequency": "195725000"
+      },
+      {
+        "name": "57.5",
+        "lower-frequency": "195725000",
+        "upper-frequency": "195775000"
+      },
+      {
+        "name": "58.0",
+        "lower-frequency": "195775000",
+        "upper-frequency": "195825000"
+      },
+      {
+        "name": "58.5",
+        "lower-frequency": "195825000",
+        "upper-frequency": "195875000"
+      },
+      {
+        "name": "59.0",
+        "lower-frequency": "195875000",
+        "upper-frequency": "195925000"
+      },
+      {
+        "name": "59.5",
+        "lower-frequency": "195925000",
+        "upper-frequency": "195975000"
+      },
+      {
+        "name": "60.0",
+        "lower-frequency": "195975000",
+        "upper-frequency": "196025000"
+      },
+      {
+        "name": "60.5",
+        "lower-frequency": "196025000",
+        "upper-frequency": "196075000"
+      },
+      {
+        "name": "61.0",
+        "lower-frequency": "196075000",
+        "upper-frequency": "196125000"
+      },
+      {
+        "name": "14 (100GHz)",
+        "lower-frequency": "191350000",
+        "upper-frequency": "191450000"
+      },
+      {
+        "name": "15 (100GHz)",
+        "lower-frequency": "191450000",
+        "upper-frequency": "191550000"
+      },
+      {
+        "name": "16 (100GHz)",
+        "lower-frequency": "191550000",
+        "upper-frequency": "191650000"
+      },
+      {
+        "name": "17 (100GHz)",
+        "lower-frequency": "191650000",
+        "upper-frequency": "191750000"
+      },
+      {
+        "name": "18 (100GHz)",
+        "lower-frequency": "191750000",
+        "upper-frequency": "191850000"
+      },
+      {
+        "name": "19 (100GHz)",
+        "lower-frequency": "191850000",
+        "upper-frequency": "191950000"
+      },
+      {
+        "name": "20 (100GHz)",
+        "lower-frequency": "191950000",
+        "upper-frequency": "192050000"
+      },
+      {
+        "name": "21 (100GHz)",
+        "lower-frequency": "192050000",
+        "upper-frequency": "192150000"
+      },
+      {
+        "name": "22 (100GHz)",
+        "lower-frequency": "192150000",
+        "upper-frequency": "192250000"
+      },
+      {
+        "name": "23 (100GHz)",
+        "lower-frequency": "192250000",
+        "upper-frequency": "192350000"
+      },
+      {
+        "name": "24 (100GHz)",
+        "lower-frequency": "192350000",
+        "upper-frequency": "192450000"
+      },
+      {
+        "name": "25 (100GHz)",
+        "lower-frequency": "192450000",
+        "upper-frequency": "192550000"
+      },
+      {
+        "name": "26 (100GHz)",
+        "lower-frequency": "192550000",
+        "upper-frequency": "192650000"
+      },
+      {
+        "name": "27 (100GHz)",
+        "lower-frequency": "192650000",
+        "upper-frequency": "192750000"
+      },
+      {
+        "name": "28 (100GHz)",
+        "lower-frequency": "192750000",
+        "upper-frequency": "192850000"
+      },
+      {
+        "name": "29 (100GHz)",
+        "lower-frequency": "192850000",
+        "upper-frequency": "192950000"
+      },
+      {
+        "name": "30 (100GHz)",
+        "lower-frequency": "192950000",
+        "upper-frequency": "193050000"
+      },
+      {
+        "name": "31 (100GHz)",
+        "lower-frequency": "193050000",
+        "upper-frequency": "193150000"
+      },
+      {
+        "name": "32 (100GHz)",
+        "lower-frequency": "193150000",
+        "upper-frequency": "193250000"
+      },
+      {
+        "name": "33 (100GHz)",
+        "lower-frequency": "193250000",
+        "upper-frequency": "193350000"
+      },
+      {
+        "name": "34 (100GHz)",
+        "lower-frequency": "193350000",
+        "upper-frequency": "193450000"
+      },
+      {
+        "name": "35 (100GHz)",
+        "lower-frequency": "193450000",
+        "upper-frequency": "193550000"
+      },
+      {
+        "name": "36 (100GHz)",
+        "lower-frequency": "193550000",
+        "upper-frequency": "193650000"
+      },
+      {
+        "name": "37 (100GHz)",
+        "lower-frequency": "193650000",
+        "upper-frequency": "193750000"
+      },
+      {
+        "name": "38 (100GHz)",
+        "lower-frequency": "193750000",
+        "upper-frequency": "193850000"
+      },
+      {
+        "name": "39 (100GHz)",
+        "lower-frequency": "193850000",
+        "upper-frequency": "193950000"
+      },
+      {
+        "name": "40 (100GHz)",
+        "lower-frequency": "193950000",
+        "upper-frequency": "194050000"
+      },
+      {
+        "name": "41 (100GHz)",
+        "lower-frequency": "194050000",
+        "upper-frequency": "194150000"
+      },
+      {
+        "name": "42 (100GHz)",
+        "lower-frequency": "194150000",
+        "upper-frequency": "194250000"
+      },
+      {
+        "name": "43 (100GHz)",
+        "lower-frequency": "194250000",
+        "upper-frequency": "194350000"
+      },
+      {
+        "name": "44 (100GHz)",
+        "lower-frequency": "194350000",
+        "upper-frequency": "194450000"
+      },
+      {
+        "name": "45 (100GHz)",
+        "lower-frequency": "194450000",
+        "upper-frequency": "194550000"
+      },
+      {
+        "name": "46 (100GHz)",
+        "lower-frequency": "194550000",
+        "upper-frequency": "194650000"
+      },
+      {
+        "name": "47 (100GHz)",
+        "lower-frequency": "194650000",
+        "upper-frequency": "194750000"
+      },
+      {
+        "name": "48 (100GHz)",
+        "lower-frequency": "194750000",
+        "upper-frequency": "194850000"
+      },
+      {
+        "name": "49 (100GHz)",
+        "lower-frequency": "194850000",
+        "upper-frequency": "194950000"
+      },
+      {
+        "name": "50 (100GHz)",
+        "lower-frequency": "194950000",
+        "upper-frequency": "195050000"
+      },
+      {
+        "name": "51 (100GHz)",
+        "lower-frequency": "195050000",
+        "upper-frequency": "195150000"
+      },
+      {
+        "name": "52 (100GHz)",
+        "lower-frequency": "195150000",
+        "upper-frequency": "195250000"
+      },
+      {
+        "name": "53 (100GHz)",
+        "lower-frequency": "195250000",
+        "upper-frequency": "195350000"
+      },
+      {
+        "name": "54 (100GHz)",
+        "lower-frequency": "195350000",
+        "upper-frequency": "195450000"
+      },
+      {
+        "name": "55 (100GHz)",
+        "lower-frequency": "195450000",
+        "upper-frequency": "195550000"
+      },
+      {
+        "name": "56 (100GHz)",
+        "lower-frequency": "195550000",
+        "upper-frequency": "195650000"
+      },
+      {
+        "name": "57 (100GHz)",
+        "lower-frequency": "195650000",
+        "upper-frequency": "195750000"
+      },
+      {
+        "name": "58 (100GHz)",
+        "lower-frequency": "195750000",
+        "upper-frequency": "195850000"
+      },
+      {
+        "name": "59 (100GHz)",
+        "lower-frequency": "195850000",
+        "upper-frequency": "195950000"
+      },
+      {
+        "name": "60 (100GHz)",
+        "lower-frequency": "195950000",
+        "upper-frequency": "196050000"
+      }
+    ]
+  },
+  "czechlight-roadm-device:media-channels": [
+    {
+      "channel": "C-band",
+      "description": "Whole-band overview"
+    }
+  ]
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/startup.json b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/startup.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/startup.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/version b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/version
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_empty/version
@@ -0,0 +1 @@
+0
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/cmdline b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/cmdline
new file mode 100644
index 0000000..6b1d906
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/cmdline
@@ -0,0 +1 @@
+czechlight=sdn-roadm-line
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/expected.json b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/expected.json
new file mode 100644
index 0000000..4ab1a31
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/expected.json
@@ -0,0 +1,732 @@
+{
+  "czechlight-roadm-device:channel-plan": {
+    "channel": [
+      {
+        "name": "C-band",
+        "lower-frequency": "191325000",
+        "upper-frequency": "196125000"
+      },
+      {
+        "name": "13.5",
+        "lower-frequency": "191325000",
+        "upper-frequency": "191375000"
+      },
+      {
+        "name": "14.0",
+        "lower-frequency": "191375000",
+        "upper-frequency": "191425000"
+      },
+      {
+        "name": "14.5",
+        "lower-frequency": "191425000",
+        "upper-frequency": "191475000"
+      },
+      {
+        "name": "15.0",
+        "lower-frequency": "191475000",
+        "upper-frequency": "191525000"
+      },
+      {
+        "name": "15.5",
+        "lower-frequency": "191525000",
+        "upper-frequency": "191575000"
+      },
+      {
+        "name": "16.0",
+        "lower-frequency": "191575000",
+        "upper-frequency": "191625000"
+      },
+      {
+        "name": "16.5",
+        "lower-frequency": "191625000",
+        "upper-frequency": "191675000"
+      },
+      {
+        "name": "17.0",
+        "lower-frequency": "191675000",
+        "upper-frequency": "191725000"
+      },
+      {
+        "name": "17.5",
+        "lower-frequency": "191725000",
+        "upper-frequency": "191775000"
+      },
+      {
+        "name": "18.0",
+        "lower-frequency": "191775000",
+        "upper-frequency": "191825000"
+      },
+      {
+        "name": "18.5",
+        "lower-frequency": "191825000",
+        "upper-frequency": "191875000"
+      },
+      {
+        "name": "19.0",
+        "lower-frequency": "191875000",
+        "upper-frequency": "191925000"
+      },
+      {
+        "name": "19.5",
+        "lower-frequency": "191925000",
+        "upper-frequency": "191975000"
+      },
+      {
+        "name": "20.0",
+        "lower-frequency": "191975000",
+        "upper-frequency": "192025000"
+      },
+      {
+        "name": "20.5",
+        "lower-frequency": "192025000",
+        "upper-frequency": "192075000"
+      },
+      {
+        "name": "21.0",
+        "lower-frequency": "192075000",
+        "upper-frequency": "192125000"
+      },
+      {
+        "name": "21.5",
+        "lower-frequency": "192125000",
+        "upper-frequency": "192175000"
+      },
+      {
+        "name": "22.0",
+        "lower-frequency": "192175000",
+        "upper-frequency": "192225000"
+      },
+      {
+        "name": "22.5",
+        "lower-frequency": "192225000",
+        "upper-frequency": "192275000"
+      },
+      {
+        "name": "23.0",
+        "lower-frequency": "192275000",
+        "upper-frequency": "192325000"
+      },
+      {
+        "name": "23.5",
+        "lower-frequency": "192325000",
+        "upper-frequency": "192375000"
+      },
+      {
+        "name": "24.0",
+        "lower-frequency": "192375000",
+        "upper-frequency": "192425000"
+      },
+      {
+        "name": "24.5",
+        "lower-frequency": "192425000",
+        "upper-frequency": "192475000"
+      },
+      {
+        "name": "25.0",
+        "lower-frequency": "192475000",
+        "upper-frequency": "192525000"
+      },
+      {
+        "name": "25.5",
+        "lower-frequency": "192525000",
+        "upper-frequency": "192575000"
+      },
+      {
+        "name": "26.0",
+        "lower-frequency": "192575000",
+        "upper-frequency": "192625000"
+      },
+      {
+        "name": "26.5",
+        "lower-frequency": "192625000",
+        "upper-frequency": "192675000"
+      },
+      {
+        "name": "27.0",
+        "lower-frequency": "192675000",
+        "upper-frequency": "192725000"
+      },
+      {
+        "name": "27.5",
+        "lower-frequency": "192725000",
+        "upper-frequency": "192775000"
+      },
+      {
+        "name": "28.0",
+        "lower-frequency": "192775000",
+        "upper-frequency": "192825000"
+      },
+      {
+        "name": "28.5",
+        "lower-frequency": "192825000",
+        "upper-frequency": "192875000"
+      },
+      {
+        "name": "29.0",
+        "lower-frequency": "192875000",
+        "upper-frequency": "192925000"
+      },
+      {
+        "name": "29.5",
+        "lower-frequency": "192925000",
+        "upper-frequency": "192975000"
+      },
+      {
+        "name": "30.0",
+        "lower-frequency": "192975000",
+        "upper-frequency": "193025000"
+      },
+      {
+        "name": "30.5",
+        "lower-frequency": "193025000",
+        "upper-frequency": "193075000"
+      },
+      {
+        "name": "31.0",
+        "lower-frequency": "193075000",
+        "upper-frequency": "193125000"
+      },
+      {
+        "name": "31.5",
+        "lower-frequency": "193125000",
+        "upper-frequency": "193175000"
+      },
+      {
+        "name": "32.0",
+        "lower-frequency": "193175000",
+        "upper-frequency": "193225000"
+      },
+      {
+        "name": "32.5",
+        "lower-frequency": "193225000",
+        "upper-frequency": "193275000"
+      },
+      {
+        "name": "33.0",
+        "lower-frequency": "193275000",
+        "upper-frequency": "193325000"
+      },
+      {
+        "name": "33.5",
+        "lower-frequency": "193325000",
+        "upper-frequency": "193375000"
+      },
+      {
+        "name": "34.0",
+        "lower-frequency": "193375000",
+        "upper-frequency": "193425000"
+      },
+      {
+        "name": "34.5",
+        "lower-frequency": "193425000",
+        "upper-frequency": "193475000"
+      },
+      {
+        "name": "35.0",
+        "lower-frequency": "193475000",
+        "upper-frequency": "193525000"
+      },
+      {
+        "name": "35.5",
+        "lower-frequency": "193525000",
+        "upper-frequency": "193575000"
+      },
+      {
+        "name": "36.0",
+        "lower-frequency": "193575000",
+        "upper-frequency": "193625000"
+      },
+      {
+        "name": "36.5",
+        "lower-frequency": "193625000",
+        "upper-frequency": "193675000"
+      },
+      {
+        "name": "37.0",
+        "lower-frequency": "193675000",
+        "upper-frequency": "193725000"
+      },
+      {
+        "name": "37.5",
+        "lower-frequency": "193725000",
+        "upper-frequency": "193775000"
+      },
+      {
+        "name": "38.0",
+        "lower-frequency": "193775000",
+        "upper-frequency": "193825000"
+      },
+      {
+        "name": "38.5",
+        "lower-frequency": "193825000",
+        "upper-frequency": "193875000"
+      },
+      {
+        "name": "39.0",
+        "lower-frequency": "193875000",
+        "upper-frequency": "193925000"
+      },
+      {
+        "name": "39.5",
+        "lower-frequency": "193925000",
+        "upper-frequency": "193975000"
+      },
+      {
+        "name": "40.0",
+        "lower-frequency": "193975000",
+        "upper-frequency": "194025000"
+      },
+      {
+        "name": "40.5",
+        "lower-frequency": "194025000",
+        "upper-frequency": "194075000"
+      },
+      {
+        "name": "41.0",
+        "lower-frequency": "194075000",
+        "upper-frequency": "194125000"
+      },
+      {
+        "name": "41.5",
+        "lower-frequency": "194125000",
+        "upper-frequency": "194175000"
+      },
+      {
+        "name": "42.0",
+        "lower-frequency": "194175000",
+        "upper-frequency": "194225000"
+      },
+      {
+        "name": "42.5",
+        "lower-frequency": "194225000",
+        "upper-frequency": "194275000"
+      },
+      {
+        "name": "43.0",
+        "lower-frequency": "194275000",
+        "upper-frequency": "194325000"
+      },
+      {
+        "name": "43.5",
+        "lower-frequency": "194325000",
+        "upper-frequency": "194375000"
+      },
+      {
+        "name": "44.0",
+        "lower-frequency": "194375000",
+        "upper-frequency": "194425000"
+      },
+      {
+        "name": "44.5",
+        "lower-frequency": "194425000",
+        "upper-frequency": "194475000"
+      },
+      {
+        "name": "45.0",
+        "lower-frequency": "194475000",
+        "upper-frequency": "194525000"
+      },
+      {
+        "name": "45.5",
+        "lower-frequency": "194525000",
+        "upper-frequency": "194575000"
+      },
+      {
+        "name": "46.0",
+        "lower-frequency": "194575000",
+        "upper-frequency": "194625000"
+      },
+      {
+        "name": "46.5",
+        "lower-frequency": "194625000",
+        "upper-frequency": "194675000"
+      },
+      {
+        "name": "47.0",
+        "lower-frequency": "194675000",
+        "upper-frequency": "194725000"
+      },
+      {
+        "name": "47.5",
+        "lower-frequency": "194725000",
+        "upper-frequency": "194775000"
+      },
+      {
+        "name": "48.0",
+        "lower-frequency": "194775000",
+        "upper-frequency": "194825000"
+      },
+      {
+        "name": "48.5",
+        "lower-frequency": "194825000",
+        "upper-frequency": "194875000"
+      },
+      {
+        "name": "49.0",
+        "lower-frequency": "194875000",
+        "upper-frequency": "194925000"
+      },
+      {
+        "name": "49.5",
+        "lower-frequency": "194925000",
+        "upper-frequency": "194975000"
+      },
+      {
+        "name": "50.0",
+        "lower-frequency": "194975000",
+        "upper-frequency": "195025000"
+      },
+      {
+        "name": "50.5",
+        "lower-frequency": "195025000",
+        "upper-frequency": "195075000"
+      },
+      {
+        "name": "51.0",
+        "lower-frequency": "195075000",
+        "upper-frequency": "195125000"
+      },
+      {
+        "name": "51.5",
+        "lower-frequency": "195125000",
+        "upper-frequency": "195175000"
+      },
+      {
+        "name": "52.0",
+        "lower-frequency": "195175000",
+        "upper-frequency": "195225000"
+      },
+      {
+        "name": "52.5",
+        "lower-frequency": "195225000",
+        "upper-frequency": "195275000"
+      },
+      {
+        "name": "53.0",
+        "lower-frequency": "195275000",
+        "upper-frequency": "195325000"
+      },
+      {
+        "name": "53.5",
+        "lower-frequency": "195325000",
+        "upper-frequency": "195375000"
+      },
+      {
+        "name": "54.0",
+        "lower-frequency": "195375000",
+        "upper-frequency": "195425000"
+      },
+      {
+        "name": "54.5",
+        "lower-frequency": "195425000",
+        "upper-frequency": "195475000"
+      },
+      {
+        "name": "55.0",
+        "lower-frequency": "195475000",
+        "upper-frequency": "195525000"
+      },
+      {
+        "name": "55.5",
+        "lower-frequency": "195525000",
+        "upper-frequency": "195575000"
+      },
+      {
+        "name": "56.0",
+        "lower-frequency": "195575000",
+        "upper-frequency": "195625000"
+      },
+      {
+        "name": "56.5",
+        "lower-frequency": "195625000",
+        "upper-frequency": "195675000"
+      },
+      {
+        "name": "57.0",
+        "lower-frequency": "195675000",
+        "upper-frequency": "195725000"
+      },
+      {
+        "name": "57.5",
+        "lower-frequency": "195725000",
+        "upper-frequency": "195775000"
+      },
+      {
+        "name": "58.0",
+        "lower-frequency": "195775000",
+        "upper-frequency": "195825000"
+      },
+      {
+        "name": "58.5",
+        "lower-frequency": "195825000",
+        "upper-frequency": "195875000"
+      },
+      {
+        "name": "59.0",
+        "lower-frequency": "195875000",
+        "upper-frequency": "195925000"
+      },
+      {
+        "name": "59.5",
+        "lower-frequency": "195925000",
+        "upper-frequency": "195975000"
+      },
+      {
+        "name": "60.0",
+        "lower-frequency": "195975000",
+        "upper-frequency": "196025000"
+      },
+      {
+        "name": "60.5",
+        "lower-frequency": "196025000",
+        "upper-frequency": "196075000"
+      },
+      {
+        "name": "61.0",
+        "lower-frequency": "196075000",
+        "upper-frequency": "196125000"
+      },
+      {
+        "name": "14 (100GHz)",
+        "lower-frequency": "191350000",
+        "upper-frequency": "191450000"
+      },
+      {
+        "name": "15 (100GHz)",
+        "lower-frequency": "191450000",
+        "upper-frequency": "191550000"
+      },
+      {
+        "name": "16 (100GHz)",
+        "lower-frequency": "191550000",
+        "upper-frequency": "191650000"
+      },
+      {
+        "name": "17 (100GHz)",
+        "lower-frequency": "191650000",
+        "upper-frequency": "191750000"
+      },
+      {
+        "name": "18 (100GHz)",
+        "lower-frequency": "191750000",
+        "upper-frequency": "191850000"
+      },
+      {
+        "name": "19 (100GHz)",
+        "lower-frequency": "191850000",
+        "upper-frequency": "191950000"
+      },
+      {
+        "name": "20 (100GHz)",
+        "lower-frequency": "191950000",
+        "upper-frequency": "192050000"
+      },
+      {
+        "name": "21 (100GHz)",
+        "lower-frequency": "192050000",
+        "upper-frequency": "192150000"
+      },
+      {
+        "name": "22 (100GHz)",
+        "lower-frequency": "192150000",
+        "upper-frequency": "192250000"
+      },
+      {
+        "name": "23 (100GHz)",
+        "lower-frequency": "192250000",
+        "upper-frequency": "192350000"
+      },
+      {
+        "name": "24 (100GHz)",
+        "lower-frequency": "192350000",
+        "upper-frequency": "192450000"
+      },
+      {
+        "name": "25 (100GHz)",
+        "lower-frequency": "192450000",
+        "upper-frequency": "192550000"
+      },
+      {
+        "name": "26 (100GHz)",
+        "lower-frequency": "192550000",
+        "upper-frequency": "192650000"
+      },
+      {
+        "name": "27 (100GHz)",
+        "lower-frequency": "192650000",
+        "upper-frequency": "192750000"
+      },
+      {
+        "name": "28 (100GHz)",
+        "lower-frequency": "192750000",
+        "upper-frequency": "192850000"
+      },
+      {
+        "name": "29 (100GHz)",
+        "lower-frequency": "192850000",
+        "upper-frequency": "192950000"
+      },
+      {
+        "name": "30 (100GHz)",
+        "lower-frequency": "192950000",
+        "upper-frequency": "193050000"
+      },
+      {
+        "name": "31 (100GHz)",
+        "lower-frequency": "193050000",
+        "upper-frequency": "193150000"
+      },
+      {
+        "name": "32 (100GHz)",
+        "lower-frequency": "193150000",
+        "upper-frequency": "193250000"
+      },
+      {
+        "name": "33 (100GHz)",
+        "lower-frequency": "193250000",
+        "upper-frequency": "193350000"
+      },
+      {
+        "name": "34 (100GHz)",
+        "lower-frequency": "193350000",
+        "upper-frequency": "193450000"
+      },
+      {
+        "name": "35 (100GHz)",
+        "lower-frequency": "193450000",
+        "upper-frequency": "193550000"
+      },
+      {
+        "name": "36 (100GHz)",
+        "lower-frequency": "193550000",
+        "upper-frequency": "193650000"
+      },
+      {
+        "name": "37 (100GHz)",
+        "lower-frequency": "193650000",
+        "upper-frequency": "193750000"
+      },
+      {
+        "name": "38 (100GHz)",
+        "lower-frequency": "193750000",
+        "upper-frequency": "193850000"
+      },
+      {
+        "name": "39 (100GHz)",
+        "lower-frequency": "193850000",
+        "upper-frequency": "193950000"
+      },
+      {
+        "name": "40 (100GHz)",
+        "lower-frequency": "193950000",
+        "upper-frequency": "194050000"
+      },
+      {
+        "name": "41 (100GHz)",
+        "lower-frequency": "194050000",
+        "upper-frequency": "194150000"
+      },
+      {
+        "name": "42 (100GHz)",
+        "lower-frequency": "194150000",
+        "upper-frequency": "194250000"
+      },
+      {
+        "name": "43 (100GHz)",
+        "lower-frequency": "194250000",
+        "upper-frequency": "194350000"
+      },
+      {
+        "name": "44 (100GHz)",
+        "lower-frequency": "194350000",
+        "upper-frequency": "194450000"
+      },
+      {
+        "name": "45 (100GHz)",
+        "lower-frequency": "194450000",
+        "upper-frequency": "194550000"
+      },
+      {
+        "name": "46 (100GHz)",
+        "lower-frequency": "194550000",
+        "upper-frequency": "194650000"
+      },
+      {
+        "name": "47 (100GHz)",
+        "lower-frequency": "194650000",
+        "upper-frequency": "194750000"
+      },
+      {
+        "name": "48 (100GHz)",
+        "lower-frequency": "194750000",
+        "upper-frequency": "194850000"
+      },
+      {
+        "name": "49 (100GHz)",
+        "lower-frequency": "194850000",
+        "upper-frequency": "194950000"
+      },
+      {
+        "name": "50 (100GHz)",
+        "lower-frequency": "194950000",
+        "upper-frequency": "195050000"
+      },
+      {
+        "name": "51 (100GHz)",
+        "lower-frequency": "195050000",
+        "upper-frequency": "195150000"
+      },
+      {
+        "name": "52 (100GHz)",
+        "lower-frequency": "195150000",
+        "upper-frequency": "195250000"
+      },
+      {
+        "name": "53 (100GHz)",
+        "lower-frequency": "195250000",
+        "upper-frequency": "195350000"
+      },
+      {
+        "name": "54 (100GHz)",
+        "lower-frequency": "195350000",
+        "upper-frequency": "195450000"
+      },
+      {
+        "name": "55 (100GHz)",
+        "lower-frequency": "195450000",
+        "upper-frequency": "195550000"
+      },
+      {
+        "name": "56 (100GHz)",
+        "lower-frequency": "195550000",
+        "upper-frequency": "195650000"
+      },
+      {
+        "name": "57 (100GHz)",
+        "lower-frequency": "195650000",
+        "upper-frequency": "195750000"
+      },
+      {
+        "name": "58 (100GHz)",
+        "lower-frequency": "195750000",
+        "upper-frequency": "195850000"
+      },
+      {
+        "name": "59 (100GHz)",
+        "lower-frequency": "195850000",
+        "upper-frequency": "195950000"
+      },
+      {
+        "name": "60 (100GHz)",
+        "lower-frequency": "195950000",
+        "upper-frequency": "196050000"
+      }
+    ]
+  },
+  "czechlight-roadm-device:media-channels": [
+    {
+      "channel": "C-band",
+      "description": "Whole-band overview"
+    }
+  ]
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/startup.json b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/startup.json
new file mode 100644
index 0000000..4435818
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/startup.json
@@ -0,0 +1,11 @@
+{
+  "czechlight-roadm-device:channel-plan": {
+    "channel": [
+      {
+        "name": "haha",
+        "lower-frequency": "195325000",
+        "upper-frequency": "195425000"
+      }
+    ]
+  }
+}
diff --git a/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/version b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/version
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/tests/czechlight-cfg-fs/data/v0_sdn-roadm-line_with_altered_initial_data/version
@@ -0,0 +1 @@
+0
diff --git a/tests/czechlight-cfg-fs/migrations.py b/tests/czechlight-cfg-fs/migrations.py
new file mode 100644
index 0000000..87f4b4a
--- /dev/null
+++ b/tests/czechlight-cfg-fs/migrations.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3
+
+import json
+import re
+import os
+import pathlib
+import shutil
+import subprocess
+import sys
+
+import pytest
+
+
+SCRIPT_ROOT = pathlib.Path(__file__).resolve().parent
+BR2_ROOT = (SCRIPT_ROOT / '../../').resolve()
+
+INSTALL_SCRIPT_PATH = BR2_ROOT / 'package/czechlight-cfg-fs/czechlight-install-yang.sh'
+MIGRATE_SCRIPT_PATH = BR2_ROOT / 'package/czechlight-cfg-fs/czechlight-migrate.sh'
+MIGRATE_DEFINITIONS_PATH = BR2_ROOT / 'package/czechlight-cfg-fs/czechlight-migration-list.sh'
+NETOPEER_SCRIPT_PATH = BR2_ROOT / 'submodules/buildroot/package/netopeer2/setup.sh'
+
+CLA_SYSREPO_PATH = BR2_ROOT / 'submodules/cla-sysrepo'
+VELIA_PATH = BR2_ROOT / 'submodules/velia'
+NETOPEER2_PATH = BR2_ROOT / 'submodules/dependencies/Netopeer2'
+
+
+def run_and_wait(ctx, desc, command_args):
+    print(f'executing {desc}')
+    with subprocess.Popen(command_args, stdout=sys.stdout, stderr=sys.stderr, env=ctx.get_env()) as proc:
+        proc.wait()
+        assert proc.returncode == 0
+
+
+class SysrepoFixture:
+    def __init__(self, test_directory, tmp_path):
+        test_directory = SCRIPT_ROOT / test_directory
+        self.test_name = test_directory.name
+
+        self.expected_file = test_directory / 'expected.json'
+        assert self.expected_file.is_file()
+
+        startup = test_directory / 'startup.json'
+        assert startup.is_file()
+
+        self.proc_cmdline = test_directory / 'cmdline'
+        assert self.proc_cmdline.is_file()
+
+        version_file = test_directory / 'version'
+        assert version_file.is_file()
+
+        tested_xpath_file = test_directory / 'xpath'
+        self.tested_xpath = tested_xpath_file.read_text() if tested_xpath_file.is_file() else None
+
+        self._running_directory = tmp_path / self.test_name
+        self._running_directory.mkdir()
+
+        self.startup_file = self._running_directory / 'startup.json'
+        shutil.copyfile(startup, self.startup_file)
+
+        self.export_file = self._running_directory / 'export.json'
+
+        self.version_file = self._running_directory / 'version'
+        shutil.copy(version_file, self.version_file)
+
+    def get_env(self):
+        res = os.environ.copy()
+        res['SYSREPO_SHM_PREFIX'] = self.test_name
+        res['SYSREPO_REPOSITORY_PATH'] = self._running_directory / 'sysrepo_repository'
+        res['CLA_YANG'] = CLA_SYSREPO_PATH / 'yang'
+        res['VELIA_YANG'] = VELIA_PATH / 'yang'
+        res['PROC_CMDLINE'] = self.proc_cmdline
+        res['CFG_VERSION_FILE'] = self.version_file
+        res['CFG_STARTUP_FILE'] = self.startup_file
+        res['NP2_MODULE_DIR'] = NETOPEER2_PATH / 'modules'
+        res['NP2_MODULE_PERMS'] = '0600'
+        res['USER'] = os.getlogin()
+        return res
+
+
+@pytest.fixture(scope='session')
+def max_version():
+    """
+    Fetches last version from czechlight-migrate script by sourcing the
+    migration definitions file and verifying the length of the migration
+    files array.
+    """
+    args = ["/bin/bash", "-c", "source " + str(MIGRATE_DEFINITIONS_PATH) + " && echo ${#MIGRATION_FILES[@]}"]
+    with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
+        proc.wait()
+        stdout, stderr = proc.communicate()
+        assert stderr.decode().strip() == ''
+        assert proc.returncode == 0
+
+    return int(stdout.decode().strip())
+
+
+@pytest.fixture
+def sysrepo_fixture(request, tmp_path):
+    return SysrepoFixture(pathlib.Path(SCRIPT_ROOT / 'data' / request.param), tmp_path)
+
+
+def find_test_directories():
+    return [pytest.param(dirname) for dirname in os.listdir(path=SCRIPT_ROOT / 'data')]
+
+
+@pytest.mark.parametrize("sysrepo_fixture", find_test_directories(), indirect=True)
+def test(sysrepo_fixture, max_version):
+    # prepare sysrepo
+    run_and_wait(sysrepo_fixture, 'netopeer2 setup.sh', [NETOPEER_SCRIPT_PATH])
+    run_and_wait(sysrepo_fixture, 'czechlight-install-yang.sh', [INSTALL_SCRIPT_PATH])
+    run_and_wait(sysrepo_fixture, 'restoring startup.json to sysrepo', ['sysrepocfg', '--datastore', 'startup', '--format', 'json', f'--import={sysrepo_fixture.startup_file}'])
+
+    current_version = int(sysrepo_fixture.version_file.read_text())
+
+    # perform the actual migration
+    print(f'migration: current version is {current_version}')
+    print('migration: applying migration script')
+    run_and_wait(sysrepo_fixture, 'migration', [MIGRATE_SCRIPT_PATH])
+
+    after_migration_version = int(sysrepo_fixture.version_file.read_text())
+    assert after_migration_version == max_version
+
+    print('migration: checking datastore contents')
+    export_args = ['sysrepocfg', '--datastore', 'startup', '-f', 'json', f'--export={sysrepo_fixture.export_file}']
+    if sysrepo_fixture.tested_xpath:
+        export_args += ['-x', sysrepo_fixture.tested_xpath]
+    run_and_wait(sysrepo_fixture, 'export', export_args)
+
+    with open(sysrepo_fixture.export_file, 'r') as fp_actual:
+        with open(sysrepo_fixture.expected_file, 'r') as fp_expected:
+            print(f'migration: comparing files {sysrepo_fixture.startup_file.name} and {sysrepo_fixture.expected_file.name}')
+
+            actual = json.load(fp_actual)
+            expected = json.load(fp_expected)
+            assert actual == expected