CzechLight ROADM driver

Czech Light is a family of open L0 device designs for building Open Line
Systems for DWDM networks. This driver talks NETCONF, the primary
communication protocol, to the devices and configures Media Channel (MC)
forwarding and provides real-time power readout. Not all features of
these devices are supported -- there's, e.g., detailed live spectrum
scanning, an OTDR functionality, neighbor discovery, etc. These are not
part of this driver.

What works:

- Device discovery for all different ROADM models:
  - Line/Degree w/ 9 Express ports
  - Flex Add/Drop w/ 20 Client ports
  - Coherent Add/Drop w/ 8 Client ports
  - even an Inline Amplifier (which is not a ROADM, but hey)
- Aggregate power readout (everywhere)
  - that's the reason for supporting the Inline Amp
- MC provisioning on all ROADMs:
  - tested on the Line/Degree model
  - including support for flexgrid
- Target power control on WSS-based devices

I noticed that various drivers use a different approach for power
control:

- Lumentum NETCONF reads the target power and pushes it as if it was
attenuation. This does not make sense to me because these values have a
directly opposite effect. Either way, "flows" do not have any info about
attenuation or desired Tx power.

- Oplink ROADM has a concept of target Tx power and it gets stored into
a *flow*.

I think that it would make a lot of sense to push these into the flows
(and also deprecate direct "power control" for ports, then), but I have
no idea how pervasive this is within current ONOS. Perhaps there is code
which makes assumptions about being able to modify port properties?

Limitations:

- The code won't create channels in the channel plan (which might be
important for flexgrid). If a flexgrid channel is already defined (not
necessarily routed, just listed in the list of channels within a channel
plan), then the code can use it just fine. If the channel definition is
missing, the MC won't be provisioned to the device.

- Some of the NETCONF parts could be probably refactored to a reusable
shape. I'm sure that the XPath parsing could be improved by using a
"real" helper of that HierarchicalConfiguration, etc.

- There's no caching, meaning that all queries hit the actual device
(which has an excellent NETCONF throughput if you ask me, though).

- The target Tx power levels when the channels are initially established
are a best effort thing, hoped to work in the majority of situations.
YMMV. They folow manufacturer's suggestions about power levels for
express, and a best guess for line out and client out ports.

- The long-haul connections have a VOA in the Tx path. This is intended
to be used to compensate for shorter spans than the maximal length.
Right now, there's no support for driving this directly from ONOS
because there is no concept for " optical properties applying to the
whole link" as far as I could tell.

- There's no inventory discovery which could be a useful thing.

This is a cleaned up version of the code which powered the demo at the
TIP Summit in Amsterdam, 2019. When testing manually, I was also able to
set up and tead down a MC connection, independently in both directions.
My professional background is not in Java (this is my second Java
project apart from upstream Gerrit at Google) that I contributed to, so
chances are that some of the idioms look weird. I tried to follow some
of the existing drivers, but improvements are certainly welcome!

Change-Id: Id59c5a9e71715d0ca63a7f5babe36b909970eb37
Bug: ONOS-8039
diff --git a/drivers/czechlight/BUILD b/drivers/czechlight/BUILD
new file mode 100644
index 0000000..82dc1a9
--- /dev/null
+++ b/drivers/czechlight/BUILD
@@ -0,0 +1,37 @@
+COMPILE_DEPS = CORE_DEPS + [
+    "@org_apache_servicemix_bundles_snmp4j//jar",
+    "//drivers/utilities:onos-drivers-utilities",
+    "//protocols/netconf/api:onos-protocols-netconf-api",
+    "//apps/optical-model:onos-apps-optical-model",
+    "//drivers/odtn-driver:onos-drivers-odtn-driver",
+    "//drivers/optical:onos-drivers-optical",
+    "//apps/odtn/api:onos-apps-odtn-api-native",
+]
+
+BUNDLES = [
+    ":onos-drivers-czechlight",
+    "//drivers/utilities:onos-drivers-utilities",
+]
+
+osgi_jar_with_tests(
+    resources = glob(["src/main/resources/**"]),
+    resources_root = "src/main/resources",
+    test_deps = TEST_ADAPTERS,
+    deps = COMPILE_DEPS,
+)
+
+onos_app(
+    app_name = "org.onosproject.drivers.czechlight",
+    category = "Drivers",
+    description = "Device drivers for CzechLight SDN ROADMs",
+    included_bundles = BUNDLES,
+    required_apps = [
+        "org.onosproject.optical-model",
+        "org.onosproject.netconf",
+        "org.onosproject.drivers.odtn-driver",
+        "org.onosproject.drivers.netconf",
+        "org.onosproject.drivers.optical",
+    ],
+    title = "CzechLight Drivers",
+    url = "https://czechlight.cesnet.cz/en/open-line-system/sdn-roadm",
+)
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDiscovery.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDiscovery.java
new file mode 100644
index 0000000..1ccbcad
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDiscovery.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.czechlight;
+
+import com.google.common.collect.Lists;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+
+import org.onlab.packet.ChassisId;
+import org.onlab.util.Frequency;
+import org.onosproject.drivers.utilities.XmlConfigParser;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Device;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceDescriptionDiscovery;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.netconf.DatastoreId;
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.netconf.NetconfSession;
+
+import org.onosproject.odtn.behaviour.OdtnDeviceDescriptionDiscovery;
+import org.slf4j.Logger;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.optical.device.OmsPortHelper.omsPortDescription;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Device description behaviour for CzechLight SDN ROADM devices using NETCONF.
+ */
+public class CzechLightDiscovery
+        extends AbstractHandlerBehaviour implements DeviceDescriptionDiscovery {
+
+    public enum DeviceType {
+        LINE_DEGREE,
+        ADD_DROP_FLEX,
+        COHERENT_ADD_DROP,
+        INLINE_AMP,
+    };
+    public static final String DEVICE_TYPE_ANNOTATION = "czechlight.model";
+
+    public static final int PORT_COMMON = 100;
+    public static final int PORT_INLINE_WEST = PORT_COMMON + 1;
+    public static final int PORT_INLINE_EAST = PORT_INLINE_WEST + 1;
+    public static final ChannelSpacing CHANNEL_SPACING_50 = ChannelSpacing.CHL_50GHZ;
+    public static final ChannelSpacing CHANNEL_SPACING_NONE = ChannelSpacing.CHL_0GHZ;
+    public static final Frequency START_CENTER_FREQ_50 = Frequency.ofGHz(191_350);
+    public static final Frequency END_CENTER_FREQ_50 = Frequency.ofGHz(196_100);
+
+    private static final String MOD_ROADM_DEVICE = "czechlight-roadm-device";
+    private static final String MOD_ROADM_DEVICE_DATE = "2019-09-30";
+    private static final String MOD_ROADM_FEATURE_LINE_DEGREE = "hw-line-9";
+    private static final String MOD_ROADM_FEATURE_FLEX_ADD_DROP = "hw-add-drop-20";
+    private static final String MOD_COHERENT_A_D = "czechlight-coherent-add-drop";
+    private static final String MOD_COHERENT_A_D_DATE = "2019-09-30";
+    private static final String MOD_INLINE_AMP = "czechlight-inline-amp";
+    private static final String MOD_INLINE_AMP_DATE = "2019-09-30";
+    private static final String NS_CZECHLIGHT_PREFIX = "http://czechlight.cesnet.cz/yang/";
+    public static final String NS_CZECHLIGHT_ROADM_DEVICE = NS_CZECHLIGHT_PREFIX + MOD_ROADM_DEVICE;
+    public static final String NS_CZECHLIGHT_COHERENT_A_D = NS_CZECHLIGHT_PREFIX + MOD_COHERENT_A_D;
+    public static final String NS_CZECHLIGHT_INLINE_AMP  = NS_CZECHLIGHT_PREFIX + MOD_INLINE_AMP;
+
+    private static final String YANGLIB_KEY_REVISION = "data.modules-state.module.revision";
+    private static final String YANGLIB_KEY_MODULE_NAME = "data.modules-state.module.name";
+    private static final String YANGLIB_XMLNS = "urn:ietf:params:xml:ns:yang:ietf-yang-library";
+    private static final String YANGLIB_XML_PREFIX = "yanglib";
+    private static final String YANGLIB_XPATH_FILTER = "/" + YANGLIB_XML_PREFIX + ":modules-state/module[(name='"
+            + MOD_ROADM_DEVICE + "') or (name='" + MOD_COHERENT_A_D + "') or (name='" + MOD_INLINE_AMP + "')]";
+    private static final String YANGLIB_PATH_QUERY_FEATURES = "data.modules-state.module.feature";
+
+    public static final String CHANNEL_DEFS_FILTER =
+            "<channel-plan xmlns=\"http://czechlight.cesnet.cz/yang/czechlight-roadm-device\">" +
+                    "<channel><lower-frequency/><upper-frequency/></channel>" +
+                    "</channel-plan>";
+    private static final String UNIDIR_CFG_SUBSTR = "<port/><attenuation/><power/>";
+    public static final String XML_MC_OPEN = "<media-channels " +
+            "xmlns=\"http://czechlight.cesnet.cz/yang/czechlight-roadm-device\">";
+    public static final String XML_MC_CLOSE = "</media-channels>";
+    public static final String MC_ROUTING_FILTER =
+            XML_MC_OPEN +
+                    "<add>" + UNIDIR_CFG_SUBSTR + "</add>" +
+                    "<drop>" + UNIDIR_CFG_SUBSTR + "</drop>" +
+                    XML_MC_CLOSE;
+
+    public static final String LINE_EXPRESS_PREFIX = "E";
+
+    private static final String DESC_PORT_LINE_WEST = "Line West";
+    private static final String DESC_PORT_LINE_EAST = "Line East";
+    private static final String DESC_PORT_LINE = "Line";
+    private static final String DESC_PORT_EXPRESS = "Express";
+
+
+    private static final Logger log = getLogger(CzechLightDiscovery.class);
+
+    @Override
+    public DeviceDescription discoverDeviceDetails() {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return null;
+        }
+
+        DefaultAnnotations.Builder annotations = DefaultAnnotations.builder();
+        final var noDevice = new DefaultDeviceDescription(handler().data().deviceId().uri(), Device.Type.OTHER,
+                null, null, null, null, null, annotations.build());
+
+        try {
+            Boolean isLineDegree = false, isAddDrop = false, isCoherentAddDrop = false, isInlineAmp = false;
+            var data = doGetXPath(getNetconfSession(), YANGLIB_XML_PREFIX, YANGLIB_XMLNS, YANGLIB_XPATH_FILTER);
+            if (!data.containsKey(YANGLIB_KEY_REVISION)) {
+                log.error("Not talking to a supported CzechLight device, is that a teapot?");
+                return noDevice;
+            }
+            final var revision = data.getString(YANGLIB_KEY_REVISION);
+            if (data.getString(YANGLIB_KEY_MODULE_NAME).equals(MOD_ROADM_DEVICE)) {
+                if (!revision.equals(MOD_ROADM_DEVICE_DATE)) {
+                    log.error("Revision mismatch for YANG module {}: got {}", MOD_ROADM_DEVICE, revision);
+                    return noDevice;
+                }
+                final var features = data.getStringArray(YANGLIB_PATH_QUERY_FEATURES);
+                isLineDegree = Arrays.stream(features)
+                        .anyMatch(s -> s.equals(MOD_ROADM_FEATURE_LINE_DEGREE));
+                isAddDrop = Arrays.stream(features)
+                        .anyMatch(s -> s.equals(MOD_ROADM_FEATURE_FLEX_ADD_DROP));
+                if (!isLineDegree && !isAddDrop) {
+                    log.error("Device type not recognized, but {} YANG model is present. Reported YANG features: {}",
+                            MOD_ROADM_DEVICE, String.join(", ", features));
+                    return noDevice;
+                }
+            } else if (data.getString(YANGLIB_KEY_MODULE_NAME).equals(MOD_COHERENT_A_D)) {
+                if (!revision.equals(MOD_COHERENT_A_D_DATE)) {
+                    log.error("Revision mismatch for YANG module {}: got {}", MOD_COHERENT_A_D, revision);
+                    return noDevice;
+                }
+                isCoherentAddDrop = true;
+            } else if (data.getString(YANGLIB_KEY_MODULE_NAME).equals(MOD_INLINE_AMP)) {
+                if (!revision.equals(MOD_INLINE_AMP_DATE)) {
+                    log.error("Revision mismatch for YANG module {}: got {}", MOD_INLINE_AMP, revision);
+                    return noDevice;
+                }
+                isInlineAmp = true;
+            }
+
+            if (isLineDegree) {
+                log.info("Talking to a Line/Degree ROADM node");
+                annotations.set(DEVICE_TYPE_ANNOTATION, DeviceType.LINE_DEGREE.toString());
+            } else if (isAddDrop) {
+                log.info("Talking to an Add/Drop ROADM node");
+                annotations.set(DEVICE_TYPE_ANNOTATION, DeviceType.ADD_DROP_FLEX.toString());
+            } else if (isCoherentAddDrop) {
+                log.info("Talking to a Coherent Add/Drop ROADM node");
+                annotations.set(DEVICE_TYPE_ANNOTATION, DeviceType.COHERENT_ADD_DROP.toString());
+            } else if (isInlineAmp) {
+                log.info("Talking to an inline ampifier, not a ROADM, but we will fake it as a ROADM for now");
+                annotations.set(DEVICE_TYPE_ANNOTATION, DeviceType.INLINE_AMP.toString());
+            } else {
+                log.error("Device type not recognized");
+                return noDevice;
+            }
+        } catch (NetconfException e) {
+            log.error("Cannot request ietf-yang-library data", e);
+            return noDevice;
+        }
+
+        // FIXME: initialize these
+        String vendor       = "CzechLight";
+        String hwVersion    = "n/a";
+        String swVersion    = "n/a";
+        String serialNumber = "n/a";
+        ChassisId chassisId = null;
+
+        return new DefaultDeviceDescription(handler().data().deviceId().uri(), Device.Type.ROADM,
+                vendor, hwVersion, swVersion, serialNumber, chassisId, annotations.build());
+    }
+
+    public static String leafPortName(final DeviceType deviceType, final long number) {
+        switch (deviceType) {
+            case LINE_DEGREE:
+                return LINE_EXPRESS_PREFIX + Long.toString(number);
+            default:
+                return Long.toString(number);
+        }
+    }
+
+    @Override
+    public List<PortDescription> discoverPortDetails() {
+        DeviceId deviceId = handler().data().deviceId();
+        DeviceService deviceService = checkNotNull(handler().get(DeviceService.class));
+        Device device = deviceService.getDevice(deviceId);
+        var deviceType = DeviceType.valueOf(device.annotations().value(DEVICE_TYPE_ANNOTATION));
+
+        List<PortDescription> portDescriptions = Lists.newArrayList();
+
+        if (deviceType == DeviceType.INLINE_AMP) {
+            DefaultAnnotations.Builder annotations = DefaultAnnotations.builder();
+            annotations.set(AnnotationKeys.PORT_NAME, DESC_PORT_LINE_WEST);
+            annotations.set(OdtnDeviceDescriptionDiscovery.PORT_TYPE,
+                    OdtnDeviceDescriptionDiscovery.OdtnPortType.LINE.toString());
+            portDescriptions.add(omsPortDescription(PortNumber.portNumber(PORT_INLINE_WEST),
+                    true,
+                    START_CENTER_FREQ_50,
+                    END_CENTER_FREQ_50,
+                    CHANNEL_SPACING_50.frequency(),
+                    annotations.build()));
+
+            annotations = DefaultAnnotations.builder();
+            annotations.set(AnnotationKeys.PORT_NAME, DESC_PORT_LINE_EAST);
+            annotations.set(OdtnDeviceDescriptionDiscovery.PORT_TYPE,
+                    OdtnDeviceDescriptionDiscovery.OdtnPortType.LINE.toString());
+            portDescriptions.add(omsPortDescription(PortNumber.portNumber(PORT_INLINE_EAST),
+                    true,
+                    START_CENTER_FREQ_50,
+                    END_CENTER_FREQ_50,
+                    CHANNEL_SPACING_50.frequency(),
+                    annotations.build()));
+
+            return portDescriptions;
+        }
+
+        DefaultAnnotations.Builder annotationsForCommon = DefaultAnnotations.builder();
+        switch (deviceType) {
+            case LINE_DEGREE:
+                annotationsForCommon.set(AnnotationKeys.PORT_NAME, DESC_PORT_LINE);
+                annotationsForCommon.set(OdtnDeviceDescriptionDiscovery.PORT_TYPE,
+                        OdtnDeviceDescriptionDiscovery.OdtnPortType.LINE.toString());
+                break;
+            case ADD_DROP_FLEX:
+            case COHERENT_ADD_DROP:
+                annotationsForCommon.set(AnnotationKeys.PORT_NAME, DESC_PORT_EXPRESS);
+                break;
+            case INLINE_AMP:
+                assert false : "this cannot happen because it's handled above, but I have to type this here anyway";
+            default:
+                assert false : "unhandled device type";
+        }
+        portDescriptions.add(omsPortDescription(PortNumber.portNumber(PORT_COMMON),
+                true,
+                START_CENTER_FREQ_50,
+                END_CENTER_FREQ_50,
+                CHANNEL_SPACING_50.frequency(),
+                annotationsForCommon.build()));
+
+        final int leafPortCount;
+        switch (deviceType) {
+            case LINE_DEGREE:
+                leafPortCount = 9;
+                break;
+            case ADD_DROP_FLEX:
+                leafPortCount = 20;
+                break;
+            case COHERENT_ADD_DROP:
+                leafPortCount = 8;
+                break;
+            default:
+                log.error("Unsupported CzechLight device type");
+                return null;
+        }
+
+        for (var i = 1; i <= leafPortCount; ++i) {
+            DefaultAnnotations.Builder annotations = DefaultAnnotations.builder();
+            final Frequency channelSpacing;
+            annotations.set(AnnotationKeys.PORT_NAME, leafPortName(deviceType, i));
+            switch (deviceType) {
+                case LINE_DEGREE:
+                    channelSpacing = CHANNEL_SPACING_50.frequency();
+                    break;
+                case ADD_DROP_FLEX:
+                    annotations.set(OdtnDeviceDescriptionDiscovery.PORT_TYPE,
+                            OdtnDeviceDescriptionDiscovery.OdtnPortType.CLIENT.toString());
+                    channelSpacing = CHANNEL_SPACING_50.frequency();
+                    break;
+                case COHERENT_ADD_DROP:
+                    annotations.set(OdtnDeviceDescriptionDiscovery.PORT_TYPE,
+                            OdtnDeviceDescriptionDiscovery.OdtnPortType.CLIENT.toString());
+                    channelSpacing = CHANNEL_SPACING_NONE.frequency();
+                    break;
+                default:
+                    log.error("Unsupported CzechLight device type");
+                    return null;
+            }
+            portDescriptions.add(omsPortDescription(PortNumber.portNumber(i),
+                    true,
+                    START_CENTER_FREQ_50,
+                    END_CENTER_FREQ_50,
+                    channelSpacing,
+                    annotations.build()));
+        }
+        return portDescriptions;
+    }
+
+    private NetconfSession getNetconfSession() {
+        NetconfController controller =
+                checkNotNull(handler().get(NetconfController.class));
+        return controller.getNetconfDevice(data().deviceId()).getSession();
+    }
+
+    public static double dbmToMilliwatts(final double dbm) {
+        return java.lang.Math.pow(10, dbm / 10);
+    }
+
+    public static double milliwattsToDbm(final double mw) {
+        return 10 * java.lang.Math.log10(mw);
+    }
+
+    /** Run a <get> NETCONF command with an XPath filter.
+     *
+     * @param session well, a NETCONF session
+     * @param prefix Name of a XML element prefix to use. Can be meaningless, such as "M"
+     * @param namespace Full URI of the XML namespace to use. This is the real meat.
+     * @param xpathFilter String with a relative XPath filter. This *MUST* start with "/" followed by 'prefix' and ":".
+     * @return Result of the <get/> operation via NETCONF as a XmlHierarchicalConfiguration
+     * @throws NetconfException exactly as session.doWrappedRpc() would do.
+     * */
+    public static HierarchicalConfiguration doGetXPath(final NetconfSession session, final String prefix,
+                                                       final String namespace, final String xpathFilter)
+            throws NetconfException {
+        final var reply = session.doWrappedRpc("<get xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">"
+                + "<filter type=\"xpath\" xmlns:" + prefix + "=\"" + namespace + "\""
+                + " select=\"" + xpathFilter.replace("\"", "&quot;") + "\"/>"
+                + "</get>");
+        log.debug("GET RPC w/XPath {}", reply);
+        var data = XmlConfigParser.loadXmlString(reply);
+        if (!data.containsKey("data[@xmlns]")) {
+            log.error("NETCONF <get> w/XPath returned error: {}", reply);
+            return null;
+        }
+        return data;
+    }
+
+    public static HierarchicalConfiguration doGetSubtree(final NetconfSession session, final String subtreeXml)
+            throws NetconfException {
+        final var data = XmlConfigParser.loadXmlString(session.getConfig(DatastoreId.RUNNING, subtreeXml));
+        if (!data.containsKey("data[@xmlns]")) {
+            log.error("NETCONF <get> w/subtree returned error");
+            return null;
+        }
+        return data;
+    }
+
+    /** Massage an XPath fragment (without the "/module:" prefix) into a key suitable for XmlConfigParser.getString()
+     *
+     * This might or might not work properly for various corner cases. It will fail horribly when XML namespaces are not
+     * being done in exactly the same manner as the author of this code assumed was the case.
+     *
+     * @param xpath XPath subset without the "/module:" prefix, such as "container/another-container/leaf"
+     * @return a string to be passed to XmlConfigParser.getString(...) of the XmlHierarchicalConfiguration
+     * */
+    public static String xpathToXmlKey(final String xpath) {
+        return "data." // prefix added by RPC handling
+                // turn XPath level delimiters into XmlConfigParser's key format
+                + xpath.replace('/', '.')
+                // filter out XPath list keys/selectors, they are not visible in XmlConfigParser
+                .replaceAll("\\[[^]]*\\]", "");
+    }
+
+}
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDriversLoader.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDriversLoader.java
new file mode 100644
index 0000000..e98e8d2
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightDriversLoader.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.czechlight;
+
+import org.osgi.service.component.annotations.Component;
+import org.onosproject.net.driver.AbstractDriverLoader;
+import org.onosproject.net.optical.OpticalDevice;
+
+/**
+ * Loader for CzechLight device drivers from specific XML.
+ */
+@Component(immediate = true)
+public class CzechLightDriversLoader extends AbstractDriverLoader {
+
+    // OSGI: help bundle plugin discover runtime package dependency.
+    @SuppressWarnings("unused")
+    private OpticalDevice optical;
+
+    public CzechLightDriversLoader() {
+        super("/czechlight-drivers.xml");
+    }
+}
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightFlowRuleProgrammable.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightFlowRuleProgrammable.java
new file mode 100644
index 0000000..e3205c2
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightFlowRuleProgrammable.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.czechlight;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.onlab.util.Spectrum;
+import org.onosproject.core.CoreService;
+import org.onosproject.drivers.odtn.impl.DeviceConnectionCache;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.GridType;
+import org.onosproject.net.Lambda;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.OchSignalType;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleProgrammable;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.criteria.OchSignalCriterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.L0ModificationInstruction;
+import org.onosproject.netconf.DatastoreId;
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.netconf.NetconfSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/** Modification of the MC by a ROADM device.
+ * The signal might be either attenuated by a specified amount of dB, or its target power can be set
+ * to a specified power in dBm.
+ */
+class MCManipulation {
+    Double attenuation;
+    Double targetPower;
+
+    public MCManipulation(final Double attenuation, final Double targetPower) {
+        this.attenuation = attenuation;
+        this.targetPower = targetPower;
+    }
+
+    public String toString() {
+        if (attenuation != null) {
+            return "attenuation: " + String.valueOf(attenuation);
+        }
+
+        if (targetPower != null) {
+            return "targetPower: " + String.valueOf(targetPower);
+        }
+
+        return "none";
+    }
+};
+
+/** Representation of a ROADM configuration for a given Media Channel.
+ * This contains frequency (`channel`), routing (`leafPort`) and attenuation or power set point (`manipulation`).
+ * */
+class CzechLightRouting {
+    MediaChannelDefinition channel;
+    int leafPort;
+    MCManipulation manipulation;
+
+    public CzechLightRouting(final MediaChannelDefinition channel, final int leafPort, final MCManipulation manip) {
+        this.channel = channel;
+        this.leafPort = leafPort;
+        this.manipulation = manip;
+    }
+
+    public String toString() {
+        return channel.toString() + " -> " + String.valueOf(leafPort) + " (" + manipulation.toString() + ")";
+    }
+};
+
+/**
+ * Implementation of FlowRuleProgrammable interface for CzechLight SDN ROADMs.
+ */
+public class CzechLightFlowRuleProgrammable extends AbstractHandlerBehaviour implements FlowRuleProgrammable {
+
+    private final Logger log =
+            LoggerFactory.getLogger(getClass());
+
+    private static final String NETCONF_OP_MERGE = "merge";
+    private static final String NETCONF_OP_NONE = "none";
+    private static final String ELEMENT_ADD = "add";
+    private static final String ELEMENT_DROP = "drop";
+
+    private enum Direction {
+        ADD,
+        DROP,
+    };
+    // FIXME: can we get this programmaticaly?
+    private static final String DEFAULT_APP = "org.onosproject.drivers.czechlight";
+
+    @Override
+    public Collection<FlowEntry> getFlowEntries() {
+        if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
+                || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
+            final var data = getConnectionCache().get(data().deviceId());
+            if (data == null) {
+                return new ArrayList<>();
+            }
+            return data.stream()
+                    .map(rule -> new DefaultFlowEntry(rule))
+                    .collect(Collectors.toList());
+        }
+
+        HierarchicalConfiguration xml;
+        try {
+            xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
+        } catch (NetconfException e) {
+            log.error("Cannot read data from NETCONF: {}", e);
+            return new ArrayList<>();
+        }
+        final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
+
+        Collection<FlowEntry> list = new ArrayList<>();
+
+        final var allMCs = xml.configurationsAt("data.media-channels");
+        allMCs.stream()
+                .map(cfg -> confToMCRouting(ELEMENT_ADD, allChannels, cfg))
+                .filter(Objects::nonNull)
+                .forEach(flow -> {
+                    log.debug("{}: found ADD: {}", data().deviceId(), flow.toString());
+                    list.add(new DefaultFlowEntry(asFlowRule(Direction.ADD, flow), FlowEntry.FlowEntryState.ADDED));
+                });
+        allMCs.stream()
+                .map(cfg -> confToMCRouting(ELEMENT_DROP, allChannels, cfg))
+                .filter(Objects::nonNull)
+                .forEach(flow -> {
+                    log.debug("{}: found DROP: {}", data().deviceId(), flow.toString());
+                    list.add(new DefaultFlowEntry(asFlowRule(Direction.DROP, flow), FlowEntry.FlowEntryState.ADDED));
+                });
+        return list;
+    }
+
+    @Override
+    public Collection<FlowRule> applyFlowRules(Collection<FlowRule> rules) {
+        if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
+                || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
+            rules.forEach(
+                    rule -> {
+                        log.debug("{}: asked for {} (whole C-band is always forwarded by the HW)",
+                                data().deviceId(), rule);
+                        getConnectionCache().add(data().deviceId(), rule.toString(), rule);
+                    }
+            );
+            return rules;
+        }
+
+        HierarchicalConfiguration xml;
+        try {
+            xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
+        } catch (NetconfException e) {
+            log.error("Cannot read data from NETCONF: {}", e);
+            return new ArrayList<>();
+        }
+        final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
+        var hopefullyAdded = new ArrayList<FlowRule>();
+
+        // temporary store because both ADD and DROP must go into the same <media-channel> list item
+        var changes = new TreeMap<String, String>();
+        rules.forEach(
+                rule -> {
+                    log.debug("{}: asked to INSERT rule for:", data().deviceId());
+                    rule.selector().criteria().forEach(
+                            criteria -> log.debug("  criteria {}", criteria.toString())
+                    );
+                    rule.treatment().allInstructions().forEach(
+                            instruction -> log.debug("  instruction {}", instruction.toString())
+                    );
+
+                    String element;
+                    long leafPort;
+                    if (inputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
+                        element = ELEMENT_DROP;
+                        leafPort = outputPortFromFlow(rule).toLong();
+                    } else {
+                        element = ELEMENT_ADD;
+                        leafPort = inputPortFromFlow(rule).toLong();
+                    }
+                    final var och = ochSignalFromFlow(rule);
+                    final var channel = allChannels.entrySet().stream()
+                            .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
+                            .findAny()
+                            .orElse(null);
+                    if (channel == null) {
+                        log.error("No matching channel definition available for the following rule at {}:",
+                                data().deviceId());
+                        rule.selector().criteria().forEach(
+                                criteria -> log.error("  criteria {}", criteria.toString())
+                        );
+                        rule.treatment().allInstructions().forEach(
+                                instruction -> log.error("  instruction {}", instruction.toString())
+                        );
+                    } else {
+                        log.info("{}: Creating \"{}\" MC {}: leaf {}", data().deviceId(),
+                                element, channel.getKey(), leafPort);
+                        var sb = new StringBuilder();
+                        sb.append("<");
+                        sb.append(element);
+                        sb.append(">");
+                        sb.append("<port>");
+                        if (deviceType() == CzechLightDiscovery.DeviceType.LINE_DEGREE) {
+                            sb.append(CzechLightDiscovery.LINE_EXPRESS_PREFIX);
+                        }
+                        sb.append(String.valueOf(leafPort));
+                        sb.append("</port>");
+                        // FIXME: propagate attenuation or power target
+                        if (deviceType() == CzechLightDiscovery.DeviceType.LINE_DEGREE) {
+                            if (outputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
+                                sb.append("<power>-5.0</power>");
+                            } else {
+                                sb.append("<power>-12.0</power>");
+                            }
+                        } else {
+                            if (outputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON) {
+                                sb.append("<power>-12.0</power>");
+                            } else {
+                                sb.append("<power>-5.0</power>");
+                            }
+                        }
+                        sb.append("</");
+                        sb.append(element);
+                        sb.append(">");
+                        changes.put(channel.getKey(),
+                                changes.getOrDefault(channel.getKey(), "") + sb.toString());
+                        hopefullyAdded.add(rule);
+                    }
+                });
+
+        if (!hopefullyAdded.isEmpty()) {
+            var sb = new StringBuilder();
+            changes.forEach(
+                    (channel, data) -> {
+                        sb.append(CzechLightDiscovery.XML_MC_OPEN);
+                        sb.append("<channel>");
+                        sb.append(channel);
+                        sb.append("</channel>");
+                        sb.append(data);
+                        sb.append(CzechLightDiscovery.XML_MC_CLOSE);
+                    });
+            doEditConfig(NETCONF_OP_MERGE, sb.toString());
+        }
+        return hopefullyAdded;
+    }
+
+    @Override
+    public Collection<FlowRule> removeFlowRules(Collection<FlowRule> rules) {
+        if (deviceType() == CzechLightDiscovery.DeviceType.INLINE_AMP
+                || deviceType() == CzechLightDiscovery.DeviceType.COHERENT_ADD_DROP) {
+            rules.forEach(
+                    rule -> {
+                        log.debug("{}: asked to remove {} (whole C-band is always forwarded by the HW)",
+                                data().deviceId(), rule);
+                        getConnectionCache().remove(data().deviceId(), rule);
+                    }
+            );
+            return rules;
+        }
+        HierarchicalConfiguration xml;
+        try {
+            xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
+        } catch (NetconfException e) {
+            log.error("Cannot read data from NETCONF: {}", e);
+            return new ArrayList<>();
+        }
+        final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
+
+        var hopefullyRemoved = new ArrayList<FlowRule>();
+
+        // temporary store because both ADD and DROP must go into the same <media-channel> list item
+        var changes = new TreeMap<String, String>();
+
+        rules.forEach(
+                rule -> {
+                    final String element = inputPortFromFlow(rule).toLong() == CzechLightDiscovery.PORT_COMMON ?
+                            ELEMENT_DROP : ELEMENT_ADD;
+                    final var och = ochSignalFromFlow(rule);
+                    final var channel = allChannels.entrySet().stream()
+                            .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
+                            .findAny()
+                            .orElse(null);
+                    if (channel == null) {
+                        log.error("Cannot find what channel to remove for the following flow rule at {}:",
+                                data().deviceId());
+                        rule.selector().criteria().forEach(
+                                criteria -> log.error("  criteria {}", criteria.toString())
+                        );
+                        rule.treatment().allInstructions().forEach(
+                                instruction -> log.error("  instruction {}", instruction.toString())
+                        );
+                    } else {
+                        log.info("{}: Removing {} MC {}", data().deviceId(), element, channel.getKey());
+                        changes.put(channel.getKey(),
+                                changes.getOrDefault(channel.getKey(), "")
+                                        + "<" + element + " nc:operation=\"remove\"/>");
+                        hopefullyRemoved.add(rule);
+                    }
+                });
+
+        if (!hopefullyRemoved.isEmpty()) {
+            var sb = new StringBuilder();
+            changes.forEach(
+                    (channel, data) -> {
+                        sb.append(CzechLightDiscovery.XML_MC_OPEN);
+                        sb.append("<channel>");
+                        sb.append(channel);
+                        sb.append("</channel>");
+                        sb.append(data);
+                        sb.append(CzechLightDiscovery.XML_MC_CLOSE);
+                    });
+            doEditConfig(NETCONF_OP_NONE, sb.toString());
+        }
+        return hopefullyRemoved;
+    }
+
+    private static CzechLightRouting confToMCRouting(final String keyPrefix,
+                                                     final Map<String, MediaChannelDefinition> allChannels,
+                                                     final HierarchicalConfiguration item) {
+        if (!item.containsKey(keyPrefix + ".port")) {
+            return null;
+        }
+        // the leaf port is either just a number, or a number prefixed by "E"
+        final var portStr = item.getString(keyPrefix + ".port");
+        final int leafPort = Integer.parseInt(portStr.startsWith(CzechLightDiscovery.LINE_EXPRESS_PREFIX) ?
+                portStr.substring(1) : portStr);
+        return new CzechLightRouting(
+                allChannels.get(item.getString("channel")),
+                leafPort,
+                new MCManipulation(
+                        item.getDouble(keyPrefix + ".attenuation", null),
+                        item.getDouble(keyPrefix + ".power", null)
+                )
+        );
+    }
+
+    private FlowRule asFlowRule(final Direction direction, final CzechLightRouting routing) {
+        FlowRuleService service = handler().get(FlowRuleService.class);
+        Iterable<FlowEntry> entries = service.getFlowEntries(data().deviceId());
+
+        final var portIn = PortNumber.portNumber(direction == Direction.DROP ?
+                CzechLightDiscovery.PORT_COMMON : routing.leafPort);
+        final var portOut = PortNumber.portNumber(direction == Direction.ADD ?
+                CzechLightDiscovery.PORT_COMMON : routing.leafPort);
+
+        final var channelWidth = routing.channel.highMHz - routing.channel.lowMHz;
+        final var channelCentralFreq = (int) (routing.channel.lowMHz + channelWidth / 2);
+
+        for (FlowEntry entry : entries) {
+            final var och = ochSignalFromFlow(entry);
+            if (och.centralFrequency().asMHz() == channelCentralFreq
+                    && och.slotWidth().asMHz() == channelWidth
+                    && portIn.equals(inputPortFromFlow(entry))
+                    && portOut.equals(outputPortFromFlow(entry))) {
+                return entry;
+            }
+        }
+
+        final var channelSlotWidth = (int) (channelWidth / ChannelSpacing.CHL_12P5GHZ.frequency().asMHz());
+        final var channelMultiplier = (int) ((channelCentralFreq - Spectrum.CENTER_FREQUENCY.asMHz())
+                / ChannelSpacing.CHL_6P25GHZ.frequency().asMHz());
+
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchInPort(portIn)
+                .add(Criteria.matchOchSignalType(OchSignalType.FLEX_GRID))
+                .add(Criteria.matchLambda(Lambda.ochSignal(GridType.FLEX, ChannelSpacing.CHL_6P25GHZ,
+                        channelMultiplier, channelSlotWidth)))
+                .build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setOutput(portOut)
+                .build();
+        return DefaultFlowRule.builder()
+                .forDevice(data().deviceId())
+                .withSelector(selector)
+                .withTreatment(treatment)
+                // the concept of priorities does not make sense for a ROADM MC configuration,
+                // but it's mandatory nonetheless
+                .withPriority(666)
+                .makePermanent()
+                .fromApp(handler().get(CoreService.class).getAppId(DEFAULT_APP))
+                .build();
+    }
+
+    private static PortNumber inputPortFromFlow(final Object flow) {
+        return ((flow instanceof FlowEntry) ?
+                ((FlowEntry) flow).selector() : ((FlowRule) flow).selector()).criteria().stream()
+                .filter(c -> c instanceof PortCriterion)
+                .map(c -> ((PortCriterion) c).port())
+                .findAny()
+                .orElse(null);
+    }
+
+    private static PortNumber outputPortFromFlow(final Object flow) {
+        return ((flow instanceof FlowEntry) ?
+                ((FlowEntry) flow).treatment() : ((FlowRule) flow).treatment()).immediate().stream()
+                .filter(c -> c instanceof Instructions.OutputInstruction)
+                .map(c -> ((Instructions.OutputInstruction) c).port())
+                .findAny()
+                .orElse(null);
+    }
+
+    private static OchSignal ochSignalFromFlow(final Object flow) {
+        final var fromCriteria = ((flow instanceof FlowEntry) ?
+                ((FlowEntry) flow).selector() : ((FlowRule) flow).selector()).criteria().stream()
+                .filter(c -> c instanceof OchSignalCriterion)
+                .map(c -> ((OchSignalCriterion) c).lambda())
+                .findAny()
+                .orElse(null);
+        if (fromCriteria != null) {
+            return fromCriteria;
+        }
+        return ((flow instanceof FlowEntry) ?
+                ((FlowEntry) flow).treatment() : ((FlowRule) flow).treatment()).immediate().stream()
+                .filter(c -> c instanceof L0ModificationInstruction.ModOchSignalInstruction)
+                .map(c -> ((L0ModificationInstruction.ModOchSignalInstruction) c).lambda())
+                .findAny()
+                .orElse(null);
+    }
+
+    private CzechLightDiscovery.DeviceType deviceType() {
+        var annotations = this.handler().get(DeviceService.class).getDevice(handler().data().deviceId()).annotations();
+        return CzechLightDiscovery.DeviceType.valueOf(annotations.value(CzechLightDiscovery.DEVICE_TYPE_ANNOTATION));
+    }
+
+    private DeviceConnectionCache getConnectionCache() {
+        return DeviceConnectionCache.init();
+    }
+
+    private HierarchicalConfiguration doGetSubtree(final String subtreeXml) throws NetconfException {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return null;
+        }
+        return CzechLightDiscovery.doGetSubtree(session, subtreeXml);
+    }
+
+    private HierarchicalConfiguration doGetXPath(final String prefix, final String namespace, final String xpathFilter)
+            throws NetconfException {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return null;
+        }
+        return CzechLightDiscovery.doGetXPath(session, prefix, namespace, xpathFilter);
+    }
+
+    public boolean doEditConfig(String mode, String cfg) {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return false;
+        }
+
+        try {
+            return session.editConfig(DatastoreId.RUNNING, mode, cfg);
+        } catch (NetconfException e) {
+            throw new IllegalStateException(new NetconfException("Failed to edit configuration.", e));
+        }
+    }
+
+    private NetconfSession getNetconfSession() {
+        NetconfController controller =
+                checkNotNull(handler().get(NetconfController.class));
+        return controller.getNetconfDevice(data().deviceId()).getSession();
+    }
+}
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightLambdaQuery.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightLambdaQuery.java
new file mode 100644
index 0000000..f8533dd
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightLambdaQuery.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.drivers.czechlight;
+
+import org.onlab.util.Frequency;
+import org.onlab.util.Spectrum;
+import org.onosproject.net.ChannelSpacing;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.behaviour.LambdaQuery;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.IntStream;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of lambda query interface for CzechLight ROADMs.
+ *
+ * These devices are actually fully flexgrid-capable. Same as the other devices supported by ONOS,
+ * we're just returning a dummy list of 50 GHz channels.
+ */
+public class CzechLightLambdaQuery extends AbstractHandlerBehaviour implements LambdaQuery {
+
+    private static final Frequency START_CENTER_FREQ_50 = Frequency.ofGHz(191_350);
+    private static final Frequency END_CENTER_FREQ_50 = Frequency.ofGHz(196_100);
+
+    @Override
+    public Set<OchSignal> queryLambdas(PortNumber portNumber) {
+        DeviceService deviceService = this.handler().get(DeviceService.class);
+        Port port = deviceService.getPort(data().deviceId(), portNumber);
+
+        if ((port.type() == Port.Type.FIBER) || (port.type() == Port.Type.OMS)) {
+            final int startMultiplier50 = (int) (START_CENTER_FREQ_50.subtract(Spectrum.CENTER_FREQUENCY).asHz()
+                    / Frequency.ofGHz(50).asHz());
+            final int endMultiplier50 = (int) (END_CENTER_FREQ_50.subtract(Spectrum.CENTER_FREQUENCY).asHz()
+                    / Frequency.ofGHz(50).asHz());
+            return IntStream.range(startMultiplier50, endMultiplier50 + 1)
+                    .mapToObj(x -> OchSignal.newDwdmSlot(ChannelSpacing.CHL_50GHZ, x))
+                    .collect(Collectors.toSet());
+        } else {
+            return Collections.emptySet();
+        }
+    }
+}
+
+
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightPowerConfig.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightPowerConfig.java
new file mode 100644
index 0000000..8e63bd7
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/CzechLightPowerConfig.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.czechlight;
+
+import com.google.common.collect.Range;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.OchSignal;
+import org.onosproject.net.behaviour.PowerConfig;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.onosproject.netconf.DatastoreId;
+import org.onosproject.netconf.NetconfController;
+import org.onosproject.netconf.NetconfException;
+import org.onosproject.netconf.NetconfSession;
+import org.slf4j.Logger;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+public class CzechLightPowerConfig<T> extends AbstractHandlerBehaviour
+        implements PowerConfig<T> {
+
+    private final Logger log = getLogger(getClass());
+
+
+    @Override
+    public Optional<Double> getTargetPower(PortNumber port, T component) {
+        return Optional.empty();
+    }
+
+    //Used by the ROADM app to set the "attenuation" parameter
+    @Override
+    public void setTargetPower(PortNumber port, T component, double power) {
+        switch (deviceType()) {
+            case LINE_DEGREE:
+            case ADD_DROP_FLEX:
+                if (!(component instanceof OchSignal)) {
+                    log.error("Cannot set target power or anything but a Media Channel");
+                    return;
+                }
+                HierarchicalConfiguration xml;
+                try {
+                    xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
+                } catch (NetconfException e) {
+                    log.error("Cannot read data from NETCONF: {}", e);
+                    return;
+                }
+                final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
+                final var och = ((OchSignal) component);
+                final var channel = allChannels.entrySet().stream()
+                        .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
+                        .findAny()
+                        .orElse(null);
+                if (channel == null) {
+                    log.error("Cannot map OCh definition {} to a channel from the channel plan", och);
+                    return;
+                }
+                final String element = port.toLong() == CzechLightDiscovery.PORT_COMMON ? "add" : "drop";
+                log.debug("{}: {} power for {} to {}", data().deviceId(), channel.getKey(), power);
+                var sb = new StringBuilder();
+                sb.append(CzechLightDiscovery.XML_MC_OPEN);
+                sb.append("<channel>");
+                sb.append(channel.getKey());
+                sb.append("</channel>");
+                sb.append("<");
+                sb.append(element);
+                sb.append("><power>");
+                sb.append(power);
+                sb.append("</power></");
+                sb.append(element);
+                sb.append(">");
+                sb.append(CzechLightDiscovery.XML_MC_CLOSE);
+                doEditConfig("merge", sb.toString());
+                return;
+            default:
+                log.error("Target power is only supported on WSS-based devices");
+                return;
+        }
+    }
+
+    @Override
+    public Optional<Double> currentPower(PortNumber port, T component) {
+        if (component instanceof OchSignal) {
+            // FIXME: this should be actually very easy for MCs that are routed...
+            log.debug("per-MC power not implemented yet");
+            return Optional.empty();
+        }
+        switch (deviceType()) {
+            case LINE_DEGREE:
+            case ADD_DROP_FLEX:
+                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
+                            "aggregate-power/common-out"));
+                } else {
+                    return Optional.ofNullable(fetchLeafSum(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
+                            "media-channels[drop/port = '" +
+                                    CzechLightDiscovery.leafPortName(deviceType(), port.toLong()) +
+                                    "']/power/leaf-out"));
+                }
+            case COHERENT_ADD_DROP:
+                if (component instanceof OchSignal) {
+                    log.debug("Coherent Add/Drop: cannot query per-MC channel power");
+                    return Optional.empty();
+                }
+                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
+                            "aggregate-power/express-out"));
+                } else {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
+                            "aggregate-power/drop"));
+                }
+            case INLINE_AMP:
+                return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_INLINE_AMP,
+                        inlineAmpStageNameFor(port) + "/optical-power/output"));
+            default:
+                assert false : "unhandled device type";
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Double> currentInputPower(PortNumber port, T component)  {
+        if (component instanceof OchSignal) {
+            log.debug("per-MC power not implemented yet");
+            return Optional.empty();
+        }
+        switch (deviceType()) {
+            case LINE_DEGREE:
+            case ADD_DROP_FLEX:
+                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
+                            "aggregate-power/common-in"));
+                } else {
+                    return Optional.ofNullable(fetchLeafSum(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
+                            "media-channels[add/port = '" +
+                                    CzechLightDiscovery.leafPortName(deviceType(), port.toLong()) +
+                                    "']/power/leaf-in"));
+                }
+            case COHERENT_ADD_DROP:
+                if (component instanceof OchSignal) {
+                    log.debug("Coherent Add/Drop: cannot query per-MC channel power");
+                    return Optional.empty();
+                }
+                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
+                            "aggregate-power/express-in"));
+                } else {
+                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
+                            "client-ports[port='" + Long.toString(port.toLong()) + "']/input-power"));
+                }
+            case INLINE_AMP:
+                return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_INLINE_AMP,
+                        inlineAmpStageNameFor(port) + "/optical-power/input"));
+            default:
+                assert false : "unhandled device type";
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Range<Double>> getTargetPowerRange(PortNumber portNumber, T component) {
+        switch (deviceType()) {
+            case LINE_DEGREE:
+            case ADD_DROP_FLEX:
+                if (component instanceof OchSignal) {
+                    // not all values might be actually set, it's complicated, so at least return some limit
+                    return Optional.ofNullable(Range.closed(-25.0, 5.0));
+                }
+            default:
+                // pass
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<Range<Double>> getInputPowerRange(PortNumber portNumber, T component) {
+        switch (deviceType()) {
+            case LINE_DEGREE:
+            case ADD_DROP_FLEX:
+                if (component instanceof OchSignal) {
+                    // not all values might be actually set, it's complicated, so at least return some limit
+                    return Optional.ofNullable(Range.closed(-30.0, +10.0));
+                }
+            default:
+                // pass
+        }
+        return Optional.empty();
+    }
+
+    private CzechLightDiscovery.DeviceType deviceType() {
+        var annotations = this.handler().get(DeviceService.class).getDevice(handler().data().deviceId()).annotations();
+        return CzechLightDiscovery.DeviceType.valueOf(annotations.value(CzechLightDiscovery.DEVICE_TYPE_ANNOTATION));
+    }
+
+    private Double fetchLeafDouble(final String namespace, final String xpath) {
+        try {
+            final var res = doGetXPath("M", namespace, "/M:" + xpath);
+            final var key = CzechLightDiscovery.xpathToXmlKey(xpath);
+            if (!res.containsKey(key)) {
+                log.error("<get> reply does not contain data for key '{}'", key);
+                return null;
+            }
+            return res.getDouble(key);
+        } catch (NetconfException e) {
+            log.error("Cannot read data from NETCONF: {}", e);
+            return null;
+        }
+    }
+
+    private Double fetchLeafSum(final String namespace, final String xpath) {
+        try {
+            final var data = doGetXPath("M", namespace, "/M:" + xpath);
+            final var key = CzechLightDiscovery.xpathToXmlKey(xpath);
+            final var power = Arrays.stream(data.getStringArray(key))
+                    .map(s -> Double.valueOf(s))
+                    .map(dBm -> CzechLightDiscovery.dbmToMilliwatts(dBm))
+                    .reduce(0.0, Double::sum);
+            log.debug(" -> power lin {}, dBm: {}", power, CzechLightDiscovery.milliwattsToDbm(power));
+            return CzechLightDiscovery.milliwattsToDbm(power);
+        } catch (NetconfException e) {
+            log.error("Cannot read data from NETCONF: {}", e);
+            return null;
+        }
+    }
+
+    private String inlineAmpStageNameFor(final PortNumber port) {
+        return port.toLong() == CzechLightDiscovery.PORT_INLINE_WEST ? "west-to-east" : "east-to-west";
+    }
+
+    private HierarchicalConfiguration doGetXPath(final String prefix, final String namespace, final String xpathFilter)
+            throws NetconfException {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return null;
+        }
+        return CzechLightDiscovery.doGetXPath(session, prefix, namespace, xpathFilter);
+    }
+
+    private HierarchicalConfiguration doGetSubtree(final String subtreeXml) throws NetconfException {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return null;
+        }
+        return CzechLightDiscovery.doGetSubtree(session, subtreeXml);
+    }
+
+    public boolean doEditConfig(String mode, String cfg) {
+        NetconfSession session = getNetconfSession();
+        if (session == null) {
+            log.error("Cannot request NETCONF session for {}", data().deviceId());
+            return false;
+        }
+
+        try {
+            return session.editConfig(DatastoreId.RUNNING, mode, cfg);
+        } catch (NetconfException e) {
+            throw new IllegalStateException(new NetconfException("Failed to edit configuration.", e));
+        }
+    }
+
+    private NetconfSession getNetconfSession() {
+        NetconfController controller =
+                checkNotNull(handler().get(NetconfController.class));
+        return controller.getNetconfDevice(data().deviceId()).getSession();
+    }
+}
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/MediaChannelDefinition.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/MediaChannelDefinition.java
new file mode 100644
index 0000000..124337e
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/MediaChannelDefinition.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.czechlight;
+
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.onosproject.net.OchSignal;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Media Channel definition specifies a frequency range, i.e., a channel in a flexgrid DWDM system as retrieved from
+ * the ROADM device. We cannot use something like an OchSignal because this represents raw data on the device.
+ */
+class MediaChannelDefinition {
+    public int lowMHz;
+    public int highMHz;
+
+    public MediaChannelDefinition(final int lowMHz, final int highMHz) {
+        this.lowMHz = lowMHz;
+        this.highMHz = highMHz;
+    }
+
+    public String toString() {
+        return "Channel{" + String.valueOf(lowMHz / 1_000_000.0) + " - " + String.valueOf(highMHz / 1_000_000.0) + "}";
+    }
+
+    public static Map<String, MediaChannelDefinition> parseChannelDefinitions(final HierarchicalConfiguration xml) {
+        return xml.configurationsAt("data.channel-plan.channel").stream()
+                .collect(Collectors.toMap(x -> x.getString("name"),
+                        x -> new MediaChannelDefinition(x.getInt("lower-frequency"),
+                                x.getInt("upper-frequency"))));
+    }
+
+    public static boolean mcMatches(final Map.Entry<String, MediaChannelDefinition> entry, final OchSignal och) {
+        return entry.getValue().lowMHz == och.centralFrequency().asMHz() - och.slotWidth().asMHz() / 2
+                && entry.getValue().highMHz == och.centralFrequency().asMHz() + och.slotWidth().asMHz() / 2;
+    }
+
+};
+
diff --git a/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/package-info.java b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/package-info.java
new file mode 100644
index 0000000..2c40d55
--- /dev/null
+++ b/drivers/czechlight/src/main/java/org/onosproject/drivers/czechlight/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Package for CzechLight device drivers.
+ */
+package org.onosproject.drivers.czechlight;
diff --git a/drivers/czechlight/src/main/resources/czechlight-drivers.xml b/drivers/czechlight/src/main/resources/czechlight-drivers.xml
new file mode 100644
index 0000000..4b4d929
--- /dev/null
+++ b/drivers/czechlight/src/main/resources/czechlight-drivers.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<drivers>
+    <driver name="czechlight-roadm" manufacturer="CzechLight" hwVersion="sdn-roadm-line" swVersion="0">
+        <behaviour api="org.onosproject.net.device.DeviceDescriptionDiscovery"
+                   impl="org.onosproject.drivers.czechlight.CzechLightDiscovery"/>
+        <behaviour api="org.onosproject.net.behaviour.LambdaQuery"
+                   impl="org.onosproject.drivers.czechlight.CzechLightLambdaQuery"/>
+        <behaviour api="org.onosproject.net.flow.FlowRuleProgrammable"
+                   impl="org.onosproject.drivers.czechlight.CzechLightFlowRuleProgrammable"/>
+        <behaviour api="org.onosproject.net.optical.OpticalDevice"
+                   impl="org.onosproject.net.optical.DefaultOpticalDevice"/>
+        <behaviour api="org.onosproject.net.behaviour.PowerConfig"
+                   impl="org.onosproject.drivers.czechlight.CzechLightPowerConfig"/>
+    </driver>
+</drivers>
diff --git a/tools/build/bazel/modules.bzl b/tools/build/bazel/modules.bzl
index cdf8f46..d23328a 100644
--- a/tools/build/bazel/modules.bzl
+++ b/tools/build/bazel/modules.bzl
@@ -160,6 +160,7 @@
     "//drivers/cisco/netconf:onos-drivers-cisco-netconf-oar": [],
     "//drivers/cisco/rest:onos-drivers-cisco-rest-oar": [],
     "//drivers/corsa:onos-drivers-corsa-oar": [],
+    "//drivers/czechlight:onos-drivers-czechlight-oar": [],
     "//drivers/flowspec:onos-drivers-flowspec-oar": [],
     "//drivers/fujitsu:onos-drivers-fujitsu-oar": [],
     "//drivers/gnmi:onos-drivers-gnmi-oar": ["stratum"],