ONOS-3422 inter-domain cross connect

depend on ONOS master repo: (Change-Id: Ie90e69c4134d1f71893bf43ee6c290bdbd273aeb)

Change-Id: I3892e780bc6550f8a8d8be622b9fee5322c1dab5
diff --git a/ecord/co/features.xml b/ecord/co/features.xml
new file mode 100644
index 0000000..c68a07b
--- /dev/null
+++ b/ecord/co/features.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Copyright 2015 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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${project.version}">
+    <feature name="${project.artifactId}" version="${project.version}"
+             description="${project.description}">
+        <feature>onos-api</feature>
+
+        <!-- based on output of mvn karaf:features-generate-descriptor -->
+        <bundle>mvn:org.glassfish.jersey.core/jersey-client/2.22.1</bundle>
+        <bundle>mvn:javax.ws.rs/javax.ws.rs-api/2.0.1</bundle>
+        <bundle>mvn:org.glassfish.jersey.core/jersey-common/2.22.1</bundle>
+        <bundle>mvn:javax.annotation/javax.annotation-api/1.2</bundle>
+        <bundle>mvn:org.glassfish.jersey.bundles.repackaged/jersey-guava/2.22.1</bundle>
+        <bundle>mvn:org.glassfish.hk2/osgi-resource-locator/1.0.1</bundle>
+        <bundle>mvn:org.glassfish.hk2/hk2-api/2.4.0-b31</bundle>
+        <bundle>mvn:org.glassfish.hk2/hk2-utils/2.4.0-b31</bundle>
+        <bundle>mvn:org.glassfish.hk2.external/aopalliance-repackaged/2.4.0-b31</bundle>
+        <bundle>mvn:org.glassfish.hk2.external/javax.inject/2.4.0-b31</bundle>
+        <bundle>mvn:org.glassfish.hk2/hk2-locator/2.4.0-b31</bundle>
+        <bundle>mvn:org.javassist/javassist/3.18.1-GA</bundle>
+
+        <bundle>mvn:${project.groupId}/onos-app-ecord-co/${project.version}</bundle>
+    </feature>
+</features>
+
diff --git a/ecord/co/pom.xml b/ecord/co/pom.xml
index d804cd3..48a7698 100644
--- a/ecord/co/pom.xml
+++ b/ecord/co/pom.xml
@@ -34,6 +34,8 @@
     <properties>
         <onos.version>1.5.0-SNAPSHOT</onos.version>
         <onos.app.name>org.onosproject.ecord.co</onos.app.name>
+        <!-- TODO App dependency not working? -->
+        <onos.app.requires>org.onosproject.incubator.rpc,org.onosproject.incubator.rpc.grpc</onos.app.requires>
         <onos.app.origin>Open Networking Lab</onos.app.origin>
     </properties>
 
@@ -59,7 +61,6 @@
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.11</version>
             <scope>test</scope>
         </dependency>
 
@@ -74,12 +75,42 @@
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.scr.annotations</artifactId>
-            <version>1.9.8</version>
             <scope>provided</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-misc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>2.22.1</version>
+        </dependency>
     </dependencies>
 
     <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.karaf.tooling</groupId>
+                    <artifactId>karaf-maven-plugin</artifactId>
+                    <version>3.0.5</version>
+                    <extensions>true</extensions>
+                </plugin>
+            </plugins>
+        </pluginManagement>
         <plugins>
             <plugin>
                 <groupId>org.apache.felix</groupId>
diff --git a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchDeviceProvider.java b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchDeviceProvider.java
index ce0bf3f..a098193 100644
--- a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchDeviceProvider.java
+++ b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchDeviceProvider.java
@@ -15,27 +15,52 @@
  */
 package org.onosproject.ecord.co;
 
+import org.apache.commons.lang3.tuple.Pair;
 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.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
 import org.onosproject.cluster.ClusterService;
 import org.onosproject.incubator.rpc.RemoteServiceContext;
 import org.onosproject.incubator.rpc.RemoteServiceDirectory;
+import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.MastershipRole;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.config.basics.BasicLinkConfig;
 import org.onosproject.net.device.DefaultDeviceDescription;
 import org.onosproject.net.device.DeviceDescription;
 import org.onosproject.net.device.DeviceProvider;
 import org.onosproject.net.device.DeviceProviderRegistry;
 import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.device.PortDescription;
 import org.onosproject.net.provider.AbstractProvider;
 import org.onosproject.net.provider.ProviderId;
 import org.slf4j.Logger;
 
-import java.net.URI;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 
+import java.net.URI;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.onosproject.ecord.co.BigSwitchManager.REALIZED_BY;
+import static org.onosproject.net.config.basics.SubjectFactories.CONNECT_POINT_SUBJECT_FACTORY;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -57,32 +82,61 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected RemoteServiceDirectory rpcService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry cfgRegistry;
+
+    private String dpidScheme = SCHEME;
+
+    private String rpcScheme = "grpc";
+
+    private int rpcPort = 11984;
+    // Metro ONOS IP
+    private String metroIp = "172.16.218.128";
+
     private BigSwitch bigSwitch;
     private DeviceDescription bigSwitchDescription;
     private DeviceProviderRegistry providerRegistry;
     private DeviceProviderService providerService;
     private BigSwitchListener listener = new InternalListener();
 
+    private final ConfigFactory<ConnectPoint, CrossConnectConfig> xcConfigFactory
+        = new ConfigFactory<ConnectPoint, CrossConnectConfig>(CONNECT_POINT_SUBJECT_FACTORY,
+                                                    CrossConnectConfig.class,
+                                                    "cross-connect") {
+            @Override
+            public CrossConnectConfig createConfig() {
+                return new CrossConnectConfig();
+            }
+        };
+
     @Activate
     public void activate() {
         // Create big switch device and description
-        DeviceId deviceId = DeviceId.deviceId(SCHEME + ':' + clusterService.getLocalNode().ip());
+        DeviceId deviceId = DeviceId.deviceId(dpidScheme + ':' + clusterService.getLocalNode().ip());
         bigSwitch = new BigSwitch(deviceId, this.id());
         buildDeviceDescription();
         // Register this device provider remotely
         // TODO: make remote configurable
-        RemoteServiceContext remoteServiceContext = rpcService.get(URI.create("local://localhost"));
+        RemoteServiceContext remoteServiceContext
+            = rpcService.get(URI.create(rpcScheme + "://" + metroIp + ":" + rpcPort));
         providerRegistry = remoteServiceContext.get(DeviceProviderRegistry.class);
         providerService = providerRegistry.register(this);
+
+        NetworkConfigListener cfglistener = new InternalConfigListener();
+        cfgRegistry.addListener(cfglistener);
+        cfgRegistry.registerConfigFactory(xcConfigFactory);
+
         // Start big switch service and register device
         bigSwitchService.addListener(listener);
         registerDevice();
+
         LOG.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
         unregisterDevice();
+        cfgRegistry.unregisterConfigFactory(xcConfigFactory);
         providerRegistry.unregister(this);
         // Won't hurt but necessary?
         providerService = null;
@@ -92,12 +146,104 @@
     private void registerDevice() {
         providerService.deviceConnected(bigSwitch.id(), bigSwitchDescription);
         providerService.updatePorts(bigSwitch.id(), bigSwitchService.getPorts());
+        bigSwitchService.getPorts().stream()
+            .forEach(this::advertiseCrossConnectLinks);
     }
 
     private void unregisterDevice() {
         providerService.deviceDisconnected(bigSwitch.id());
     }
 
+    private ConnectPoint toConnectPoint(String strCp) {
+        String[] split = strCp.split("/");
+        if (split.length != 2) {
+            LOG.warn("Unexpected annotation %s:%s", REALIZED_BY, strCp);
+            return null;
+        }
+        DeviceId did = DeviceId.deviceId(split[0]);
+        PortNumber num = PortNumber.fromString(split[1]);
+        return new ConnectPoint(did, num);
+    }
+
+    private Optional<Pair<ConnectPoint, ConnectPoint>> crossConnectLink(PortDescription bigPort) {
+        String sPhyCp = bigPort.annotations().value(REALIZED_BY);
+        if (sPhyCp == null) {
+            return Optional.empty();
+        }
+
+        ConnectPoint phyCp = toConnectPoint(sPhyCp);
+        if (phyCp != null) {
+            CrossConnectConfig config = cfgRegistry.getConfig(phyCp, CrossConnectConfig.class);
+            if (config != null) {
+                return config.remote()
+                    .map(remCp -> Pair.of(new ConnectPoint(bigSwitch.id(), bigPort.portNumber()), remCp));
+            }
+        }
+        return Optional.empty();
+    }
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private ObjectNode crossConnectLinksJson(Pair<ConnectPoint, ConnectPoint> link) {
+        ObjectNode linksCfg = MAPPER.createObjectNode();
+
+        ObjectNode basic = MAPPER.createObjectNode();
+        basic.putObject("basic")
+            .put(BasicLinkConfig.IS_DURABLE, true)
+            .put(BasicLinkConfig.TYPE, "DIRECT");
+        linksCfg.set(String.format("%s/%s-%s/%s",
+                                   link.getLeft().deviceId(), link.getLeft().port(),
+                                   link.getRight().deviceId(), link.getRight().port()),
+                     basic);
+        linksCfg.set(String.format("%s/%s-%s/%s",
+                                   link.getRight().deviceId(), link.getRight().port(),
+                                   link.getLeft().deviceId(), link.getLeft().port()),
+                     basic);
+        return linksCfg;
+    }
+
+    private boolean postNetworkConfig(String subject, ObjectNode cfg) {
+        // TODO Slice out REST Client code as library?
+        Client client = ClientBuilder.newClient();
+
+        client.property(ClientProperties.FOLLOW_REDIRECTS, true);
+
+        // Trying to do JSON processing using Jackson triggered OSGi nightmare
+        //client.register(JacksonFeature.class);
+
+        final Map<String, String> env = System.getenv();
+        // TODO Where should we get the user/password from?
+        String user = env.getOrDefault("ONOS_WEB_USER", "onos");
+        String pass = env.getOrDefault("ONOS_WEB_PASS", "rocks");
+        HttpAuthenticationFeature auth = HttpAuthenticationFeature.basic(user, pass);
+        client.register(auth);
+
+        // TODO configurable base path
+        WebTarget target = client.target("http://" + metroIp + ":8181/onos/v1/")
+                                    .path("network/configuration/")
+                                    .path(subject);
+
+        Response response = target.request(MediaType.APPLICATION_JSON)
+                        .post(Entity.entity(cfg.toString(), MediaType.APPLICATION_JSON));
+
+
+        if (response.getStatusInfo() != Response.Status.OK) {
+            LOG.error("POST failed {}\n{}", response, cfg.toString());
+            return false;
+        }
+        return true;
+    }
+
+    private void advertiseCrossConnectLinks(PortDescription port) {
+        crossConnectLink(port).ifPresent(xcLink -> {
+            LOG.debug("CrossConnect {} is {}!",
+                     xcLink,
+                     port.isEnabled() ? "up" : "down");
+            // TODO check port status and add/remove cross connect Link
+            postNetworkConfig("links", crossConnectLinksJson(xcLink));
+        });
+    }
+
     private class InternalListener implements BigSwitchListener {
         @Override
         public void event(BigSwitchEvent event) {
@@ -105,9 +251,16 @@
                 case PORT_ADDED:
                 case PORT_REMOVED:
                     providerService.updatePorts(bigSwitch.id(), bigSwitchService.getPorts());
+                    // if the subject's underlying port was a cross connect port,
+                    // advertise cross-connect link to Metro-ONOS view
+                    advertiseCrossConnectLinks(event.subject());
                     break;
+
                 case PORT_UPDATED:
                     providerService.portStatusChanged(bigSwitch.id(), event.subject());
+                    // if the subject's underlying port was a cross connect port,
+                    // advertise cross-connect link to Metro-ONOS view
+                    advertiseCrossConnectLinks(event.subject());
                     break;
                 default:
                     break;
@@ -140,4 +293,15 @@
     public boolean isReachable(DeviceId deviceId) {
         return true;
     }
+
+    public class InternalConfigListener implements NetworkConfigListener {
+
+        @Override
+        public void event(NetworkConfigEvent event) {
+            if (event.configClass() == CrossConnectConfig.class) {
+                bigSwitchService.getPorts().stream()
+                    .forEach(BigSwitchDeviceProvider.this::advertiseCrossConnectLinks);
+            }
+        }
+    }
 }
diff --git a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchEvent.java b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchEvent.java
index 4a1252e..2c50dcd 100644
--- a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchEvent.java
+++ b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchEvent.java
@@ -18,6 +18,8 @@
 import org.onosproject.event.AbstractEvent;
 import org.onosproject.net.device.PortDescription;
 
+// TODO probably Event subject should contain Device info.
+//      e.g., (DeviceId, PortDescription)
 public class BigSwitchEvent extends AbstractEvent<BigSwitchEvent.Type, PortDescription> {
 
     public enum Type {
diff --git a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchManager.java b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchManager.java
index d90af33..b58f2a7 100644
--- a/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchManager.java
+++ b/ecord/co/src/main/java/org/onosproject/ecord/co/BigSwitchManager.java
@@ -56,8 +56,13 @@
 public class BigSwitchManager
         extends AbstractListenerManager<BigSwitchEvent, BigSwitchListener>
         implements BigSwitchService {
+
+
     private static final Logger log = getLogger(BigSwitchDeviceProvider.class);
 
+    // annotation on a big switch port
+    public static final String REALIZED_BY = "bigswitch:realizedBy";
+
     private static final String PORT_MAP = "ecord-port-map";
     private static final String PORT_COUNTER = "ecord-port-counter";
     private static final Serializer SERIALIZER = Serializer.using(KryoNamespaces.API);
@@ -107,22 +112,28 @@
     @Override
     public List<PortDescription> getPorts() {
         return portMap.keySet().stream()
-                .map(cp -> toPortDescription(cp))
+                .map(cp -> toVirtualPortDescription(cp))
                 .collect(Collectors.toList());
     }
 
     /**
      * Convert connect point to port description.
      *
-     * @param cp connect point
-     * @return port description
+     * @param cp connect point of physical port
+     * @return port description of virtual big switch port
      */
-    private PortDescription toPortDescription(ConnectPoint cp) {
+    private PortDescription toVirtualPortDescription(ConnectPoint cp) {
         Port p = deviceService.getPort(cp.deviceId(), cp.port());
-        // This is annoying
+        if (p == null) {
+            return null;
+        }
+        // copy annotation
         DefaultAnnotations.Builder annot = DefaultAnnotations.builder();
         p.annotations().keys()
                 .forEach(k -> annot.set(k, p.annotations().value(k)));
+        // add annotation about underlying physical connect-point
+        annot.set(REALIZED_BY, String.format("%s/%s", cp.deviceId().toString(),
+                                                      cp.port().toString()));
 
         return new DefaultPortDescription(
                 PortNumber.portNumber(portMap.get(cp).value()),
@@ -147,20 +158,24 @@
             log.debug("Edge event {} {}", event.subject(), event.type());
             BigSwitchEvent.Type bigSwitchEvent;
 
+            PortDescription descr = null;
             switch (event.type()) {
                 case EDGE_PORT_ADDED:
                     portMap.put(event.subject(), portCounter.getAndIncrement());
+                    descr = toVirtualPortDescription(event.subject());
                     bigSwitchEvent = BigSwitchEvent.Type.PORT_ADDED;
                     break;
                 case EDGE_PORT_REMOVED:
+                    descr = toVirtualPortDescription(event.subject());
                     portMap.remove(event.subject());
                     bigSwitchEvent = BigSwitchEvent.Type.PORT_REMOVED;
                     break;
                 default:
                     return;
             }
-
-            post(new BigSwitchEvent(bigSwitchEvent, toPortDescription(event.subject())));
+            if (descr != null) {
+                post(new BigSwitchEvent(bigSwitchEvent, descr));
+            }
         }
     }
 
@@ -190,7 +205,7 @@
                     // Update if state of existing edge changed
                     ConnectPoint cp = new ConnectPoint(event.subject().id(), event.port().number());
                     if (portMap.containsKey(cp)) {
-                        post(new BigSwitchEvent(BigSwitchEvent.Type.PORT_UPDATED, toPortDescription(cp)));
+                        post(new BigSwitchEvent(BigSwitchEvent.Type.PORT_UPDATED, toVirtualPortDescription(cp)));
                     }
                     break;
                 default:
diff --git a/ecord/co/src/main/java/org/onosproject/ecord/co/CrossConnectConfig.java b/ecord/co/src/main/java/org/onosproject/ecord/co/CrossConnectConfig.java
new file mode 100644
index 0000000..fb76cea
--- /dev/null
+++ b/ecord/co/src/main/java/org/onosproject/ecord/co/CrossConnectConfig.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015 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.ecord.co;
+
+import java.util.Optional;
+
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+
+import com.google.common.annotations.Beta;
+
+// This Config is not specific to E-CORD, so it might make sense to move it out
+/**
+ * Configuration information about Cross Connect ports.
+ */
+@Beta
+public class CrossConnectConfig extends Config<ConnectPoint> {
+
+    /**
+     * Indicates remote ConnectPoint connected to this cross connect port.
+     *
+     * @return ConnectPoint information
+     */
+    public Optional<ConnectPoint> remote() {
+        String s = get("remote", null);
+        if (s == null) {
+            return Optional.empty();
+        }
+        String[] split = s.split("/");
+        if (split.length != 2) {
+            // ill-formed String
+            return Optional.empty();
+        }
+        DeviceId did = DeviceId.deviceId(split[0]);
+        PortNumber num = PortNumber.fromString(split[1]);
+        return Optional.of(new ConnectPoint(did, num));
+    }
+
+    /**
+     * Specifies the remote ConnectPoint connected to this cross connect port.
+     *
+     * @param remote device connect point identifier
+     * @return self
+     */
+    public CrossConnectConfig remote(ConnectPoint remote) {
+        if (remote == null) {
+            return (CrossConnectConfig) setOrClear("remote", (String) null);
+        }
+
+        return (CrossConnectConfig) setOrClear("remote", String.format("%s/%s", remote.deviceId(), remote.port()));
+    }
+}