TL1 device provider with driver for Lumentum WaveReady.

ONOS-5800 & ONOS-5801

Change-Id: Icd820285eb8db2fd92c03ebf11ce022b6a82b48a
diff --git a/protocols/tl1/ctl/BUCK b/protocols/tl1/ctl/BUCK
new file mode 100644
index 0000000..2dc94c2
--- /dev/null
+++ b/protocols/tl1/ctl/BUCK
@@ -0,0 +1,18 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:netty-transport',
+    '//lib:netty-buffer',
+    '//lib:netty-codec',
+    '//protocols/tl1/api:onos-protocols-tl1-api',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+    '//utils/osgi:onlab-osgi-tests',
+    '//core/api:onos-api-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/protocols/tl1/ctl/pom.xml b/protocols/tl1/ctl/pom.xml
new file mode 100644
index 0000000..033a4f9
--- /dev/null
+++ b/protocols/tl1/ctl/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-present Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>onos-tl1</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.9.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>onos-tl1-ctl</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-tl1-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-handler</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java
new file mode 100644
index 0000000..ab55427
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Command.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import org.onosproject.tl1.Tl1Command;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default implementation of a TL1 command.
+ */
+public final class DefaultTl1Command implements Tl1Command {
+    private static final char HYPHEN = '-';
+    private static final char COLON = ':';
+    private static final char SEMICOLON = ';';
+
+    private String verb;
+    private String modifier;
+    private Optional<String> tid;
+    private String aid;
+    private int ctag;
+    private String parameters;
+
+    private DefaultTl1Command(String verb, String modifier, String tid, String aid, int ctag, String parameters) {
+        this.verb = verb;
+        this.modifier = modifier;
+        this.tid = Optional.ofNullable(tid);
+        this.aid = aid;
+        this.ctag = ctag;
+        this.parameters = parameters;
+    }
+
+    @Override
+    public String verb() {
+        return verb;
+    }
+
+    @Override
+    public String modifier() {
+        return modifier;
+    }
+
+    @Override
+    public Optional<String> tid() {
+        return tid;
+    }
+
+    @Override
+    public Optional<String> aid() {
+        return Optional.ofNullable(aid);
+    }
+
+    @Override
+    public int ctag() {
+        return ctag;
+    }
+
+    @Override
+    public Optional<String> parameters() {
+        return Optional.ofNullable(parameters);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder()
+                .append(verb).append(HYPHEN)
+                .append(modifier).append(COLON)
+                .append(tid().orElse("")).append(COLON)
+                .append(aid().orElse("")).append(COLON)
+                .append(ctag);
+
+        if (parameters().isPresent()) {
+            sb.append(COLON).append(COLON).append(parameters);
+        }
+
+        return sb.append(SEMICOLON).toString();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder implements Tl1Command.Builder {
+        private String verb;
+        private String modifier;
+        private String tid;
+        private String aid;
+        private int ctag;
+        private String parameters;
+
+        @Override
+        public Tl1Command.Builder withVerb(String verb) {
+            this.verb = verb;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withModifier(String modifier) {
+            this.modifier = modifier;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder forTid(String tid) {
+            this.tid = tid;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withAid(String aid) {
+            this.aid = aid;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withCtag(int ctag) {
+            this.ctag = ctag;
+            return this;
+        }
+
+        @Override
+        public Tl1Command.Builder withParameters(String parameters) {
+            this.parameters = parameters;
+            return this;
+        }
+
+        @Override
+        public Tl1Command build() {
+            checkNotNull(verb, "Must supply a verb");
+            checkNotNull(modifier, "Must supply a modifier");
+
+            checkArgument(MIN_CTAG < ctag, "ctag cannot be less than " + MIN_CTAG);
+            checkArgument(ctag <= MAX_CTAG, "ctag cannot be larger than " + MAX_CTAG);
+
+            return new DefaultTl1Command(verb, modifier, tid, aid, ctag, parameters);
+        }
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java
new file mode 100644
index 0000000..7a2fa8f
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Controller.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.DelimiterBasedFrameDecoder;
+import io.netty.handler.codec.string.StringDecoder;
+import io.netty.util.CharsetUtil;
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.Tools;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.tl1.Tl1Command;
+import org.onosproject.tl1.Tl1Controller;
+import org.onosproject.tl1.Tl1Device;
+import org.onosproject.tl1.Tl1Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of TL1 controller.
+ *
+ * Handles the connection and input/output for all registered TL1 devices.
+ * Turn on debug logging if you want to see all message I/O.
+ *
+ * Per device, we track commands using a simple ctag-keyed map. This assumes the client is sending out unique ctag's.
+ */
+@Component(immediate = true)
+@Service
+public class DefaultTl1Controller implements Tl1Controller {
+    private final Logger log = LoggerFactory.getLogger(DefaultTl1Controller.class);
+
+    // TL1 message delimiter (semi colon)
+    private static final ByteBuf DELIMITER = Unpooled.copiedBuffer(new char[]{';'}, Charset.defaultCharset());
+    private static final String COMPLD = "COMPLD";
+    private static final String DENY = "DENY";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    private ConcurrentMap<DeviceId, Tl1Device> deviceMap = new ConcurrentHashMap<>();
+    // Key: channel, value: map with key ctag, value: future TL1 msg (ctags are assumed unique per device)
+    private ConcurrentMap<Channel, ConcurrentMap<Integer, CompletableFuture<String>>> msgMap =
+            new ConcurrentHashMap<>();
+    private EventLoopGroup workerGroup = new NioEventLoopGroup();
+    private Set<Tl1Listener> tl1Listeners = new CopyOnWriteArraySet<>();
+    private ExecutorService executor;
+
+    @Activate
+    public void activate() {
+        executor = Executors.newFixedThreadPool(
+                Runtime.getRuntime().availableProcessors(),
+                Tools.groupedThreads("onos/tl1controller", "%d", log));
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        executor.shutdown();
+        deviceMap.clear();
+        msgMap.clear();
+        log.info("Stopped");
+    }
+
+    @Override
+    /**
+     * This implementation returns an empty string on failure.
+     */
+    public CompletableFuture<String> sendMsg(DeviceId deviceId, Tl1Command msg) {
+        log.debug("Sending TL1 message to device {}: {}", deviceId, msg);
+
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null || !device.isConnected() || !mastershipService.isLocalMaster(deviceId)) {
+            return CompletableFuture.completedFuture(StringUtils.EMPTY);
+        }
+
+        // Create and store completable future, complete it in the channel handler when we receive a response
+        CompletableFuture<String> future = new CompletableFuture<>();
+        Channel channel = device.channel();
+        if (!msgMap.containsKey(channel)) {
+            return CompletableFuture.completedFuture(StringUtils.EMPTY);
+        }
+        msgMap.get(channel).put(msg.ctag(), future);
+
+        // Write message to channel
+        channel.writeAndFlush(Unpooled.copiedBuffer(msg.toString(), CharsetUtil.UTF_8));
+
+        return future;
+    }
+
+    @Override
+    public Optional<Tl1Device> getDevice(DeviceId deviceId) {
+        return Optional.ofNullable(deviceMap.get(deviceId));
+    }
+
+    @Override
+    public boolean addDevice(DeviceId deviceId, Tl1Device device) {
+        log.debug("Adding TL1 device {} {}", deviceId);
+
+        // Ignore if device already known
+        if (deviceMap.containsKey(deviceId)) {
+            log.error("Ignoring duplicate device {}", deviceId);
+            return false;
+        }
+
+        deviceMap.put(deviceId, device);
+        return true;
+    }
+
+    @Override
+    public void connectDevice(DeviceId deviceId) {
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null || device.isConnected()) {
+            return;
+        }
+
+        Bootstrap b = new Bootstrap();
+        b.group(workerGroup)
+                .channel(NioSocketChannel.class)
+                .option(ChannelOption.SO_KEEPALIVE, true)
+                .handler(new ChannelInitializer<SocketChannel>() {
+                    @Override
+                    protected void initChannel(SocketChannel socketChannel) throws Exception {
+                        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(8192, DELIMITER));
+                        socketChannel.pipeline().addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));
+                        // TODO
+                        //socketChannel.pipeline().addLast(new Tl1Decoder());
+                        socketChannel.pipeline().addLast(new Tl1InboundHandler());
+                    }
+                })
+                .remoteAddress(device.ip().toInetAddress(), device.port())
+                .connect()
+                .addListener((ChannelFuture channelFuture) -> {
+                    if (channelFuture.isSuccess()) {
+                        msgMap.put(channelFuture.channel(), new ConcurrentHashMap<>());
+                        device.connect(channelFuture.channel());
+                        tl1Listeners.forEach(l -> executor.execute(() -> l.deviceConnected(deviceId)));
+                    }
+                });
+    }
+
+    @Override
+    public void removeDevice(DeviceId deviceId) {
+        disconnectDevice(deviceId);
+        deviceMap.remove(deviceId);
+    }
+
+    @Override
+    public void addListener(Tl1Listener listener) {
+        tl1Listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(Tl1Listener listener) {
+        tl1Listeners.remove(listener);
+    }
+
+    @Override
+    public void disconnectDevice(DeviceId deviceId) {
+        // Ignore if unknown device
+        Tl1Device device = deviceMap.get(deviceId);
+        if (device == null) {
+            return;
+        }
+
+        Channel channel = device.channel();
+        if (channel != null) {
+            channel.close();
+        }
+
+        msgMap.remove(channel);
+        device.disconnect();
+        tl1Listeners.forEach(l -> l.deviceDisconnected(deviceId));
+    }
+
+    @Override
+    public Set<DeviceId> getDeviceIds() {
+        return deviceMap.keySet();
+    }
+
+    @Override
+    public Collection<Tl1Device> getDevices() {
+        return deviceMap.values();
+    }
+
+    /**
+     * Crude filtering handler that will only complete our stored future upon receiving a TL1 response messages.
+     */
+    private class Tl1InboundHandler extends SimpleChannelInboundHandler<String> {
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
+            log.debug("Received TL1 message {}", s);
+
+            // Search for "COMPLD" or "DENY" to identify a TL1 response,
+            // then return the remainder of the string.
+            String[] words = s.split("\\s");
+            for (int i = 0; i < words.length; i++) {
+                String w = words[i];
+                if (w.startsWith(COMPLD) || w.startsWith(DENY)) {
+                    // ctag is just in front of it
+                    int ctag = Integer.parseInt(words[i - 1]);
+                    // We return everything that follows to the caller (this will lose line breaks and such)
+                    String result = Arrays.stream(words).skip(i + 1).collect(Collectors.joining());
+                    // Set future when command is executed, good or bad
+                    Map<Integer, CompletableFuture<String>> msg = msgMap.get(ctx.channel());
+                    if (msg != null) {
+                        CompletableFuture<String> f = msg.remove(ctag);
+                        if (f != null) {
+                            f.complete(result);
+                        }
+                    }
+
+                    return;
+                }
+            }
+        }
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java
new file mode 100644
index 0000000..602d24c
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/DefaultTl1Device.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.tl1.impl;
+
+import io.netty.channel.Channel;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.tl1.Tl1Device;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default implementation of a TL1 device.
+ */
+public class DefaultTl1Device implements Tl1Device {
+    private final Logger log = LoggerFactory.getLogger(DefaultTl1Device.class);
+    private static final String TL1 = "tl1";
+
+    private IpAddress ip;
+    private int port;
+    private String username;
+    private String password;
+    private String tid;
+    private Channel channel;
+
+    @Override
+    public void connect(Channel channel) {
+        this.channel = channel;
+    }
+
+    @Override
+    public void disconnect() {
+        this.channel = null;
+    }
+
+    @Override
+    public boolean isConnected() {
+        return channel != null;
+    }
+
+    @Override
+    public IpAddress ip() {
+        return ip;
+    }
+
+    @Override
+    public int port() {
+        return port;
+    }
+
+    @Override
+    public String username() {
+        return username;
+    }
+
+    @Override
+    public String password() {
+        return password;
+    }
+
+    @Override
+    public Channel channel() {
+        return channel;
+    }
+
+    @Override
+    public String tid() {
+        return tid;
+    }
+
+    public DefaultTl1Device(IpAddress ip, int port, String username, String password) {
+        this.ip = checkNotNull(ip);
+        checkArgument((TpPort.MIN_PORT <= port) && (port <= TpPort.MAX_PORT));
+        this.port = port;
+        this.username = checkNotNull(username);
+        this.password = checkNotNull(password);
+        this.tid = null;
+        channel = null;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ip, port, username, password);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DefaultTl1Device) {
+            DefaultTl1Device that = (DefaultTl1Device) obj;
+            return Objects.equals(ip, that.ip) &&
+                    Objects.equals(port, that.port) &&
+                    Objects.equals(username, that.username) &&
+                    Objects.equals(password, that.password) &&
+                    Objects.equals(tid, that.tid) &&
+                    Objects.equals(channel, that.channel);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("ip", ip)
+                .add("port", port)
+                .add("username", username)
+                .add("password", password)
+                .add("tid", tid == null ? "N/A" : tid)
+                .toString();
+    }
+}
diff --git a/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java
new file mode 100644
index 0000000..4d7f24e
--- /dev/null
+++ b/protocols/tl1/ctl/src/main/java/org/onosproject/tl1/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * TL1 protocol API implementation.
+ */
+package org.onosproject.tl1.impl;
\ No newline at end of file