Implement CLI and REST API for Xconnect

Deprecate the old way of configuring Xconnect via network config

Change-Id: I5b9ac7852517c25805bcbfc0e7b3bec3a52eed9f
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index 2417231..292c941 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -125,6 +125,7 @@
 import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.EventuallyConsistentMap;
 import org.onosproject.store.service.EventuallyConsistentMapBuilder;
@@ -230,6 +231,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     public LeadershipService leadershipService;
 
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
+    public XconnectService xconnectService;
+
     @Property(name = "activeProbing", boolValue = true,
             label = "Enable active probing to discover dual-homed hosts.")
     boolean activeProbing = true;
@@ -809,11 +813,7 @@
                 ImmutableMap.copyOf(defaultRoutingHandler.shouldProgramCache);
     }
 
-    /**
-     * Extracts the application ID from the manager.
-     *
-     * @return application ID
-     */
+    @Override
     public ApplicationId appId() {
         return appId;
     }
@@ -890,38 +890,21 @@
         return tunnelHandler.getTunnel(tunnelId);
     }
 
-    /**
-     * Returns internal VLAN for untagged hosts on given connect point.
-     * <p>
-     * The internal VLAN is either vlan-untagged for an access port,
-     * or vlan-native for a trunk port.
-     *
-     * @param connectPoint connect point
-     * @return internal VLAN or null if both vlan-untagged and vlan-native are undefined
-     */
+    @Override
     public VlanId getInternalVlanId(ConnectPoint connectPoint) {
         VlanId untaggedVlanId = interfaceService.getUntaggedVlanId(connectPoint);
         VlanId nativeVlanId = interfaceService.getNativeVlanId(connectPoint);
         return untaggedVlanId != null ? untaggedVlanId : nativeVlanId;
     }
 
-    /**
-     * Returns optional pair device ID of given device.
-     *
-     * @param deviceId device ID
-     * @return optional pair device ID. Might be empty if pair device is not configured
-     */
-    Optional<DeviceId> getPairDeviceId(DeviceId deviceId) {
+    @Override
+    public Optional<DeviceId> getPairDeviceId(DeviceId deviceId) {
         SegmentRoutingDeviceConfig deviceConfig =
                 cfgService.getConfig(deviceId, SegmentRoutingDeviceConfig.class);
         return Optional.ofNullable(deviceConfig).map(SegmentRoutingDeviceConfig::pairDeviceId);
     }
-    /**
-     * Returns optional pair device local port of given device.
-     *
-     * @param deviceId device ID
-     * @return optional pair device ID. Might be empty if pair device is not configured
-     */
+
+    @Override
     public Optional<PortNumber> getPairLocalPort(DeviceId deviceId) {
         SegmentRoutingDeviceConfig deviceConfig =
                 cfgService.getConfig(deviceId, SegmentRoutingDeviceConfig.class);
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
index 8d64517..c8c9044 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
@@ -15,10 +15,14 @@
  */
 package org.onosproject.segmentrouting;
 
+import com.google.common.annotations.Beta;
 import com.google.common.collect.Multimap;
+import org.apache.commons.lang3.NotImplementedException;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
+import org.onlab.packet.VlanId;
 import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.Link;
@@ -38,6 +42,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -46,12 +51,18 @@
 public interface SegmentRoutingService {
     /**
      * VLAN cross-connect ACL priority.
+     *
+     * @deprecated in ONOS 1.12. Replaced by {@link org.onosproject.segmentrouting.xconnect.api.XconnectService}
      */
+    @Deprecated
     int XCONNECT_ACL_PRIORITY = 60000;
 
     /**
      * VLAN cross-connect Bridging priority.
+     *
+     * @deprecated in ONOS 1.12. Replaced by {@link org.onosproject.segmentrouting.xconnect.api.XconnectService}
      */
+    @Deprecated
     int XCONNECT_PRIORITY = 1000;
 
     /**
@@ -302,4 +313,51 @@
      * @return shouldProgram local cache
      */
     Map<DeviceId, Boolean> getShouldProgramCache();
+
+    /**
+     * Gets application id.
+     *
+     * @return application id
+     */
+    default ApplicationId appId() {
+        throw new NotImplementedException("appId not implemented");
+    }
+
+    /**
+     * Returns internal VLAN for untagged hosts on given connect point.
+     * <p>
+     * The internal VLAN is either vlan-untagged for an access port,
+     * or vlan-native for a trunk port.
+     *
+     * @param connectPoint connect point
+     * @return internal VLAN or null if both vlan-untagged and vlan-native are undefined
+     */
+    @Beta
+    default VlanId getInternalVlanId(ConnectPoint connectPoint) {
+        throw new NotImplementedException("getInternalVlanId not implemented");
+    }
+
+
+    /**
+     * Returns optional pair device ID of given device.
+     *
+     * @param deviceId device ID
+     * @return optional pair device ID. Might be empty if pair device is not configured
+     */
+    @Beta
+    default Optional<DeviceId> getPairDeviceId(DeviceId deviceId) {
+        throw new NotImplementedException("getPairDeviceId not implemented");
+    }
+
+
+    /**
+     * Returns optional pair device local port of given device.
+     *
+     * @param deviceId device ID
+     * @return optional pair device ID. Might be empty if pair device is not configured
+     */
+    @Beta
+    default Optional<PortNumber> getPairLocalPort(DeviceId deviceId) {
+        throw new NotImplementedException("getPairLocalPort not implemented");
+    }
 }
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
index 96c1a38..5dafb3f 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
@@ -54,7 +54,10 @@
 
 /**
  * Handles cross connect related events.
+ *
+ * @deprecated in ONOS 1.12. Replaced by {@link org.onosproject.segmentrouting.xconnect.impl.XconnectManager}
  */
+@Deprecated
 public class XConnectHandler {
     private static final Logger log = LoggerFactory.getLogger(XConnectHandler.class);
     private static final String CONFIG_NOT_FOUND = "XConnect config not found";
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
new file mode 100644
index 0000000..a1ad929
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import com.google.common.collect.Sets;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
+
+import java.util.Set;
+
+/**
+ * Creates Xconnect.
+ */
+@Command(scope = "onos", name = "sr-xconnect-add", description = "Create Xconnect")
+public class XconnectAddCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "deviceId",
+            description = "Device ID",
+            required = true, multiValued = false)
+    private String deviceIdStr;
+
+    @Argument(index = 1, name = "vlanId",
+            description = "VLAN ID",
+            required = true, multiValued = false)
+    private String vlanIdStr;
+
+    @Argument(index = 2, name = "port1",
+            description = "Port 1",
+            required = true, multiValued = false)
+    private String port1Str;
+
+    @Argument(index = 3, name = "port2",
+            description = "Port 2",
+            required = true, multiValued = false)
+    private String port2Str;
+
+
+    @Override
+    protected void execute() {
+        DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
+        VlanId vlanId = VlanId.vlanId(vlanIdStr);
+        PortNumber port1 = PortNumber.portNumber(port1Str);
+        PortNumber port2 = PortNumber.portNumber(port2Str);
+        Set<PortNumber> ports = Sets.newHashSet(port1, port2);
+
+        XconnectService xconnectService = get(XconnectService.class);
+        xconnectService.addOrUpdateXconnect(deviceId, vlanId, ports);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectListCommand.java
new file mode 100644
index 0000000..43919c4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectListCommand.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
+
+/**
+ * Lists Xconnects.
+ */
+@Command(scope = "onos", name = "sr-xconnect", description = "Lists all Xconnects")
+public class XconnectListCommand extends AbstractShellCommand {
+    @Override
+    protected void execute() {
+        XconnectService xconnectService = get(XconnectService.class);
+        xconnectService.getXconnects().forEach(desc -> print("%s", desc));
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectRemoveCommand.java
new file mode 100644
index 0000000..224a600
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectRemoveCommand.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
+
+/**
+ * Deletes Xconnect.
+ */
+@Command(scope = "onos", name = "sr-xconnect-remove", description = "Remove Xconnect")
+public class XconnectRemoveCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "deviceId",
+            description = "Device ID",
+            required = true, multiValued = false)
+    private String deviceIdStr;
+
+    @Argument(index = 1, name = "vlanId",
+            description = "VLAN ID",
+            required = true, multiValued = false)
+    private String vlanIdStr;
+
+    @Override
+    protected void execute() {
+        DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
+        VlanId vlanId = VlanId.vlanId(vlanIdStr);
+
+        XconnectService xconnectService = get(XconnectService.class);
+        xconnectService.removeXonnect(deviceId, vlanId);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
index 76a3917..6689b93 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
@@ -32,7 +32,10 @@
 
 /**
  * Configuration object for cross-connect.
+ *
+ * @deprecated in ONOS 1.12. Replaced by {@link org.onosproject.segmentrouting.xconnect.impl.XconnectManager}
  */
+@Deprecated
 public class XConnectConfig extends Config<ApplicationId> {
     private static final String VLAN = "vlan";
     private static final String PORTS = "ports";
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
index 44a600c..88601aa 100644
--- a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastHandler.java
@@ -1613,7 +1613,8 @@
                 // Spine-facing port should have no subnet and no xconnect
                 if (srManager.deviceConfiguration() != null &&
                         srManager.deviceConfiguration().getPortSubnets(ingressDevice, port).isEmpty() &&
-                        !srManager.xConnectHandler.hasXConnect(new ConnectPoint(ingressDevice, port))) {
+                        (srManager.xconnectService == null ||
+                        !srManager.xconnectService.hasXconnect(new ConnectPoint(ingressDevice, port)))) {
                     portBuilder.add(port);
                 }
             }
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
new file mode 100644
index 0000000..77824e8
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018-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.segmentrouting.xconnect.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Sets;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+public class XconnectCodec extends JsonCodec<XconnectDesc> {
+    private static final String DEVICE_ID = "deviceId";
+    private static final String VLAN_ID = "vlanId";
+    private static final String PORTS = "ports";
+
+    private static Logger log = LoggerFactory.getLogger(XconnectCodec.class);
+
+    @Override
+    public ObjectNode encode(XconnectDesc desc, CodecContext context) {
+        final ObjectNode result = context.mapper().createObjectNode();
+        result.put(DEVICE_ID, desc.key().deviceId().toString());
+        result.put(VLAN_ID, desc.key().vlanId().toString());
+        final ArrayNode portNode = result.putArray(PORTS);
+        desc.ports().forEach(port -> portNode.add(port.toString()));
+
+        return result;
+    }
+
+    @Override
+    public XconnectDesc decode(ObjectNode json, CodecContext context) {
+        DeviceId deviceId = DeviceId.deviceId(json.path(DEVICE_ID).asText());
+        VlanId vlanId = VlanId.vlanId(json.path(VLAN_ID).asText());
+
+        Set<PortNumber> ports = Sets.newHashSet();
+        JsonNode portNodes = json.get(PORTS);
+        if (portNodes != null) {
+            portNodes.forEach(portNode -> ports.add(PortNumber.portNumber(portNode.asInt())));
+        }
+
+        XconnectKey key = new XconnectKey(deviceId, vlanId);
+        return new XconnectDesc(key, ports);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
new file mode 100644
index 0000000..0eead31
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018-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.segmentrouting.xconnect.api;
+
+import com.google.common.base.MoreObjects;
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Xconnect description.
+ */
+public class XconnectDesc {
+    private XconnectKey key;
+    private Set<PortNumber> ports;
+
+    /**
+     * Constructs new Xconnect description with given device ID and VLAN ID.
+     *
+     * @param key Xconnect key
+     * @param ports set of ports
+     */
+    public XconnectDesc(XconnectKey key, Set<PortNumber> ports) {
+        this.key = key;
+        this.ports = ports;
+    }
+
+    /**
+     * Gets Xconnect key.
+     *
+     * @return Xconnect key
+     */
+    public XconnectKey key() {
+        return key;
+    }
+
+    /**
+     * Gets ports.
+     *
+     * @return set of ports
+     */
+    public Set<PortNumber> ports() {
+        return ports;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof XconnectDesc)) {
+            return false;
+        }
+        final XconnectDesc other = (XconnectDesc) obj;
+        return Objects.equals(this.key, other.key) &&
+                Objects.equals(this.ports, other.ports);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key, ports);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("key", key)
+                .add("ports", ports)
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectKey.java
new file mode 100644
index 0000000..1f2d446
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectKey.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018-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.segmentrouting.xconnect.api;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Xconnect key.
+ */
+public class XconnectKey {
+    private DeviceId deviceId;
+    private VlanId vlanId;
+
+    /**
+     * Constructs new XconnectKey with given device ID and VLAN ID.
+     *
+     * @param deviceId device ID
+     * @param vlanId vlan ID
+     */
+    public XconnectKey(DeviceId deviceId, VlanId vlanId) {
+        this.deviceId = deviceId;
+        this.vlanId = vlanId;
+    }
+
+    /**
+     * Gets device ID.
+     *
+     * @return device ID of the Xconnect key
+     */
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    /**
+     * Gets VLAN ID.
+     *
+     * @return VLAN ID of the Xconnect key
+     */
+    public VlanId vlanId() {
+        return vlanId;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof XconnectKey)) {
+            return false;
+        }
+        final XconnectKey other = (XconnectKey) obj;
+        return Objects.equals(this.deviceId, other.deviceId) &&
+                Objects.equals(this.vlanId, other.vlanId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, vlanId);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("deviceId", deviceId)
+                .add("vlanId", vlanId)
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
new file mode 100644
index 0000000..1fede71
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-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.segmentrouting.xconnect.api;
+
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Set;
+
+/**
+ * VLAN cross connect between exactly two ports.
+ */
+@Service
+public interface XconnectService {
+
+    /**
+     * VLAN cross-connect ACL priority.
+     */
+    int XCONNECT_ACL_PRIORITY = 60000;
+
+    /**
+     * VLAN cross-connect Bridging priority.
+     */
+    int XCONNECT_PRIORITY = 1000;
+
+    /**
+     * Creates or updates Xconnect.
+     *
+     * @param deviceId device ID
+     * @param vlanId VLAN ID
+     * @param ports set of ports
+     */
+    void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<PortNumber> ports);
+
+    /**
+     * Deletes Xconnect.
+     *
+     * @param deviceId device ID
+     * @param vlanId VLAN ID
+     */
+    void removeXonnect(DeviceId deviceId, VlanId vlanId);
+
+    /**
+     * Gets Xconnects.
+     *
+     * @return set of Xconnect descriptions
+     */
+    Set<XconnectDesc> getXconnects();
+
+    /**
+     * Check if there is Xconnect configured on given connect point.
+     *
+     * @param cp connect point
+     * @return true if there is Xconnect configured on the connect point
+     */
+    boolean hasXconnect(ConnectPoint cp);
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/package-info.java
new file mode 100644
index 0000000..56df706
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-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.
+ */
+
+/**
+ * VLAN cross connect API.
+ */
+package org.onosproject.segmentrouting.xconnect.api;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
new file mode 100644
index 0000000..f997d53
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright 2018-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.segmentrouting.xconnect.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+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.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.codec.CodecService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.xconnect.api.XconnectCodec;
+import org.onosproject.segmentrouting.xconnect.api.XconnectDesc;
+import org.onosproject.segmentrouting.xconnect.api.XconnectKey;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+@Service
+@Component(immediate = true)
+public class XconnectManager implements XconnectService {
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private CodecService codecService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public NetworkConfigService netCfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public FlowObjectiveService flowObjectiveService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
+    public SegmentRoutingService srService;
+
+    private static final String APP_NAME = "org.onosproject.xconnect";
+    private static final String ERROR_NOT_MASTER = "Not master controller";
+
+    private static Logger log = LoggerFactory.getLogger(XconnectManager.class);
+
+    private ApplicationId appId;
+    private ConsistentMap<XconnectKey, Set<PortNumber>> xconnectStore;
+    private ConsistentMap<XconnectKey, NextObjective> xconnectNextObjStore;
+
+    private final MapEventListener<XconnectKey, Set<PortNumber>> xconnectListener = new XconnectMapListener();
+    private final DeviceListener deviceListener = new InternalDeviceListener();
+
+    @Activate
+    void activate() {
+        appId = coreService.registerApplication(APP_NAME);
+        codecService.registerCodec(XconnectDesc.class, new XconnectCodec());
+
+        KryoNamespace.Builder serializer = KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(XconnectKey.class);
+
+        xconnectStore = storageService.<XconnectKey, Set<PortNumber>>consistentMapBuilder()
+                .withName("onos-sr-xconnect")
+                .withRelaxedReadConsistency()
+                .withSerializer(Serializer.using(serializer.build()))
+                .build();
+        xconnectStore.addListener(xconnectListener);
+
+        xconnectNextObjStore = storageService.<XconnectKey, NextObjective>consistentMapBuilder()
+                .withName("onos-sr-xconnect-next")
+                .withRelaxedReadConsistency()
+                .withSerializer(Serializer.using(serializer.build()))
+                .build();
+
+        deviceService.addListener(deviceListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    void deactivate() {
+        xconnectStore.removeListener(xconnectListener);
+        deviceService.removeListener(deviceListener);
+        codecService.unregisterCodec(XconnectDesc.class);
+
+        log.info("Stopped");
+    }
+
+    @Override
+    public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<PortNumber> ports) {
+        log.info("Adding or updating xconnect. deviceId={}, vlanId={}, ports={}",
+                deviceId, vlanId, ports);
+        final XconnectKey key = new XconnectKey(deviceId, vlanId);
+        xconnectStore.put(key, ports);
+    }
+
+    @Override
+    public void removeXonnect(DeviceId deviceId, VlanId vlanId) {
+        log.info("Removing xconnect. deviceId={}, vlanId={}",
+                deviceId, vlanId);
+        final XconnectKey key = new XconnectKey(deviceId, vlanId);
+        xconnectStore.remove(key);
+    }
+
+    @Override
+    public Set<XconnectDesc> getXconnects() {
+        return xconnectStore.asJavaMap().entrySet().stream()
+                .map(e -> new XconnectDesc(e.getKey(), e.getValue()))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public boolean hasXconnect(ConnectPoint cp) {
+        return getXconnects().stream().anyMatch(desc ->
+                desc.key().deviceId().equals(cp.deviceId()) && desc.ports().contains(cp.port())
+        );
+    }
+
+    private class XconnectMapListener implements MapEventListener<XconnectKey, Set<PortNumber>> {
+        @Override
+        public void event(MapEvent<XconnectKey, Set<PortNumber>> event) {
+            XconnectKey key = event.key();
+            Versioned<Set<PortNumber>> ports = event.newValue();
+            Versioned<Set<PortNumber>> oldPorts = event.oldValue();
+
+            switch (event.type()) {
+                case INSERT:
+                    populateXConnect(key, ports.value());
+                    break;
+                case UPDATE:
+                    updateXConnect(key, oldPorts.value(), ports.value());
+                    break;
+                case REMOVE:
+                    revokeXConnect(key, oldPorts.value());
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceId deviceId = event.subject().id();
+            if (!mastershipService.isLocalMaster(deviceId)) {
+                return;
+            }
+
+            switch (event.type()) {
+                case DEVICE_ADDED:
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_UPDATED:
+                    if (deviceService.isAvailable(deviceId)) {
+                        init(deviceId);
+                    } else {
+                        cleanup(deviceId);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    void init(DeviceId deviceId) {
+        getXconnects().stream()
+                .filter(desc -> desc.key().deviceId().equals(deviceId))
+                .forEach(desc -> populateXConnect(desc.key(), desc.ports()));
+    }
+
+    void cleanup(DeviceId deviceId) {
+        xconnectNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                .forEach(entry -> xconnectNextObjStore.remove(entry.getKey()));
+        log.debug("{} is removed from xConnectNextObjStore", deviceId);
+    }
+
+    /**
+     * Populates XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param ports a set of ports to be cross-connected
+     */
+    private void populateXConnect(XconnectKey key, Set<PortNumber> ports) {
+        if (!mastershipService.isLocalMaster(key.deviceId())) {
+            log.info("Abort populating XConnect {}: {}", key, ERROR_NOT_MASTER);
+            return;
+        }
+
+        ports = addPairPort(key.deviceId(), ports);
+        populateFilter(key, ports);
+        populateFwd(key, populateNext(key, ports));
+        populateAcl(key);
+    }
+
+    /**
+     * Populates filtering objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private void populateFilter(XconnectKey key, Set<PortNumber> ports) {
+        ports.forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
+                            key, port),
+                    (objective, error) ->
+                            log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
+                                    key, port, error));
+            flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
+        });
+    }
+
+    /**
+     * Populates next objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private NextObjective populateNext(XconnectKey key, Set<PortNumber> ports) {
+        NextObjective nextObj;
+        if (xconnectNextObjStore.containsKey(key)) {
+            nextObj = xconnectNextObjStore.get(key).value();
+            log.debug("NextObj for {} found, id={}", key, nextObj.id());
+        } else {
+            NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports);
+            ObjectiveContext nextContext = new DefaultObjectiveContext(
+                    // To serialize this with kryo
+                    (Serializable & Consumer<Objective>) (objective) ->
+                            log.debug("XConnect NextObj for {} added", key),
+                    (Serializable & BiConsumer<Objective, ObjectiveError>) (objective, error) ->
+                            log.warn("Failed to add XConnect NextObj for {}: {}", key, error)
+            );
+            nextObj = nextObjBuilder.add(nextContext);
+            flowObjectiveService.next(key.deviceId(), nextObj);
+            xconnectNextObjStore.put(key, nextObj);
+            log.debug("NextObj for {} not found. Creating new NextObj with id={}", key, nextObj.id());
+        }
+        return nextObj;
+    }
+
+    /**
+     * Populates bridging forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     */
+    private void populateFwd(XconnectKey key, NextObjective nextObj) {
+        ForwardingObjective.Builder fwdObjBuilder = fwdObjBuilder(key, nextObj.id());
+        ObjectiveContext fwdContext = new DefaultObjectiveContext(
+                (objective) -> log.debug("XConnect FwdObj for {} populated", key),
+                (objective, error) ->
+                        log.warn("Failed to populate XConnect FwdObj for {}: {}", key, error));
+        flowObjectiveService.forward(key.deviceId(), fwdObjBuilder.add(fwdContext));
+    }
+
+    /**
+     * Populates ACL forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     */
+    private void populateAcl(XconnectKey key) {
+        ForwardingObjective.Builder aclObjBuilder = aclObjBuilder(key.vlanId());
+        ObjectiveContext aclContext = new DefaultObjectiveContext(
+                (objective) -> log.debug("XConnect AclObj for {} populated", key),
+                (objective, error) ->
+                        log.warn("Failed to populate XConnect AclObj for {}: {}", key, error));
+        flowObjectiveService.forward(key.deviceId(), aclObjBuilder.add(aclContext));
+    }
+
+    /**
+     * Revokes XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param ports XConnect ports
+     */
+    private void revokeXConnect(XconnectKey key, Set<PortNumber> ports) {
+        if (!mastershipService.isLocalMaster(key.deviceId())) {
+            log.info("Abort populating XConnect {}: {}", key, ERROR_NOT_MASTER);
+            return;
+        }
+
+        ports = addPairPort(key.deviceId(), ports);
+        revokeFilter(key, ports);
+        if (xconnectNextObjStore.containsKey(key)) {
+            NextObjective nextObj = xconnectNextObjStore.get(key).value();
+            revokeFwd(key, nextObj, null);
+            revokeNext(key, nextObj, null);
+        } else {
+            log.warn("NextObj for {} does not exist in the store.", key);
+        }
+        revokeAcl(key);
+    }
+
+    /**
+     * Revokes filtering objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private void revokeFilter(XconnectKey key, Set<PortNumber> ports) {
+        ports.forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
+                            key, port),
+                    (objective, error) ->
+                            log.warn("Failed to revoke XConnect FilterObj for {} on port {}: {}",
+                                    key, port, error));
+            flowObjectiveService.filter(key.deviceId(), filtObjBuilder.remove(context));
+        });
+    }
+
+    /**
+     * Revokes next objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     * @param nextFuture completable future for this next objective operation
+     */
+    private void revokeNext(XconnectKey key, NextObjective nextObj,
+                            CompletableFuture<ObjectiveError> nextFuture) {
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous NextObj for {} removed", key);
+                if (nextFuture != null) {
+                    nextFuture.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous NextObj for {}: {}", key, error);
+                if (nextFuture != null) {
+                    nextFuture.complete(error);
+                }
+            }
+        };
+        flowObjectiveService.next(key.deviceId(),
+                (NextObjective) nextObj.copy().remove(context));
+        xconnectNextObjStore.remove(key);
+    }
+
+    /**
+     * Revokes bridging forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     * @param fwdFuture completable future for this forwarding objective operation
+     */
+    private void revokeFwd(XconnectKey key, NextObjective nextObj,
+                           CompletableFuture<ObjectiveError> fwdFuture) {
+        ForwardingObjective.Builder fwdObjBuilder = fwdObjBuilder(key, nextObj.id());
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous FwdObj for {} removed", key);
+                if (fwdFuture != null) {
+                    fwdFuture.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous FwdObj for {}: {}", key, error);
+                if (fwdFuture != null) {
+                    fwdFuture.complete(error);
+                }
+            }
+        };
+        flowObjectiveService.forward(key.deviceId(), fwdObjBuilder.remove(context));
+    }
+
+    /**
+     * Revokes ACL forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     */
+    private void revokeAcl(XconnectKey key) {
+        ForwardingObjective.Builder aclObjBuilder = aclObjBuilder(key.vlanId());
+        ObjectiveContext aclContext = new DefaultObjectiveContext(
+                (objective) -> log.debug("XConnect AclObj for {} populated", key),
+                (objective, error) ->
+                        log.warn("Failed to populate XConnect AclObj for {}: {}", key, error));
+        flowObjectiveService.forward(key.deviceId(), aclObjBuilder.remove(aclContext));
+    }
+
+    /**
+     * Updates XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param prevPorts previous XConnect ports
+     * @param ports new XConnect ports
+     */
+    private void updateXConnect(XconnectKey key, Set<PortNumber> prevPorts,
+                                Set<PortNumber> ports) {
+        // NOTE: ACL flow doesn't include port information. No need to update it.
+        //       Pair port is built-in and thus not going to change. No need to update it.
+
+        // remove old filter
+        prevPorts.stream().filter(port -> !ports.contains(port)).forEach(port ->
+                revokeFilter(key, ImmutableSet.of(port)));
+        // install new filter
+        ports.stream().filter(port -> !prevPorts.contains(port)).forEach(port ->
+                populateFilter(key, ImmutableSet.of(port)));
+
+        CompletableFuture<ObjectiveError> fwdFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> nextFuture = new CompletableFuture<>();
+
+        if (xconnectNextObjStore.containsKey(key)) {
+            NextObjective nextObj = xconnectNextObjStore.get(key).value();
+            revokeFwd(key, nextObj, fwdFuture);
+
+            fwdFuture.thenAcceptAsync(fwdStatus -> {
+                if (fwdStatus == null) {
+                    log.debug("Fwd removed. Now remove group {}", key);
+                    revokeNext(key, nextObj, nextFuture);
+                }
+            });
+
+            nextFuture.thenAcceptAsync(nextStatus -> {
+                if (nextStatus == null) {
+                    log.debug("Installing new group and flow for {}", key);
+                    populateFwd(key, populateNext(key, ports));
+                }
+            });
+        } else {
+            log.warn("NextObj for {} does not exist in the store.", key);
+        }
+    }
+
+    /**
+     * Creates a next objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param ports set of XConnect ports
+     * @return next objective builder
+     */
+    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<PortNumber> ports) {
+        int nextId = flowObjectiveService.allocateNextId();
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(key.vlanId()).build();
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+                .withMeta(metadata);
+        ports.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+        return nextObjBuilder;
+    }
+
+    /**
+     * Creates a bridging forwarding objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param nextId next ID of the broadcast group for this XConnect key
+     * @return forwarding objective builder
+     */
+    private ForwardingObjective.Builder fwdObjBuilder(XconnectKey key, int nextId) {
+        /*
+         * Driver should treat objectives with MacAddress.NONE and !VlanId.NONE
+         * as the VLAN cross-connect broadcast rules
+         */
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchVlanId(key.vlanId());
+        sbuilder.matchEthDst(MacAddress.NONE);
+
+        ForwardingObjective.Builder fob = DefaultForwardingObjective.builder();
+        fob.withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .withSelector(sbuilder.build())
+                .nextStep(nextId)
+                .withPriority(XCONNECT_PRIORITY)
+                .fromApp(appId)
+                .makePermanent();
+        return fob;
+    }
+
+    /**
+     * Creates an ACL forwarding objective builder for XConnect.
+     *
+     * @param vlanId cross connect VLAN id
+     * @return forwarding objective builder
+     */
+    private ForwardingObjective.Builder aclObjBuilder(VlanId vlanId) {
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchVlanId(vlanId);
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+
+        ForwardingObjective.Builder fob = DefaultForwardingObjective.builder();
+        fob.withFlag(ForwardingObjective.Flag.VERSATILE)
+                .withSelector(sbuilder.build())
+                .withTreatment(tbuilder.build())
+                .withPriority(XCONNECT_ACL_PRIORITY)
+                .fromApp(appId)
+                .makePermanent();
+        return fob;
+    }
+
+    /**
+     * Creates a filtering objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param port XConnect ports
+     * @return next objective builder
+     */
+    private FilteringObjective.Builder filterObjBuilder(XconnectKey key, PortNumber port) {
+        FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+        fob.withKey(Criteria.matchInPort(port))
+                .addCondition(Criteria.matchVlanId(key.vlanId()))
+                .addCondition(Criteria.matchEthDst(MacAddress.NONE))
+                .withPriority(XCONNECT_PRIORITY);
+        return fob.permit().fromApp(appId);
+    }
+
+    /**
+     * Add pair port to the given set of port.
+     *
+     * @param deviceId device Id
+     * @param ports ports specified in the xconnect config
+     * @return port specified in the xconnect config plus the pair port (if configured)
+     */
+    private Set<PortNumber> addPairPort(DeviceId deviceId, Set<PortNumber> ports) {
+        if (srService == null) {
+            return ports;
+        }
+        Set<PortNumber> newPorts = Sets.newHashSet(ports);
+        srService.getPairLocalPort(deviceId).ifPresent(newPorts::add);
+        return newPorts;
+    }
+
+    // TODO DEVICE listener
+    // up : init
+    // down: removeDevice
+
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/package-info.java
new file mode 100644
index 0000000..547e3bc
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-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.
+ */
+
+/**
+ * VLAN cross connect implementation.
+ */
+package org.onosproject.segmentrouting.xconnect.impl;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index d575413..04682e9 100644
--- a/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -98,10 +98,33 @@
                 <entry key="-gAddr" value-ref="mcastGroupCompleter"/>
             </optional-completers>
         </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.XconnectListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.XconnectAddCommand"/>
+            <completers>
+                <ref component-id="deviceIdCompleter"/>
+                <ref component-id="placeholderCompleter"/>
+                <ref component-id="portNumberCompleter"/>
+                <ref component-id="portNumberCompleter"/>
+                <null/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.XconnectRemoveCommand"/>
+            <completers>
+                <ref component-id="deviceIdCompleter"/>
+                <ref component-id="placeholderCompleter"/>
+                <null/>
+            </completers>
+        </command>
     </command-bundle>
 
     <bean id="nullCompleter" class="org.apache.karaf.shell.console.completer.NullCompleter"/>
+    <bean id="placeholderCompleter" class="org.onosproject.cli.PlaceholderCompleter"/>
     <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
+    <bean id="portNumberCompleter" class="org.onosproject.cli.net.PortNumberCompleter"/>
     <bean id="pseudowireIdCompleter" class="org.onosproject.segmentrouting.cli.PseudowireIdCompleter"/>
     <bean id="mcastGroupCompleter" class="org.onosproject.mcast.cli.McastGroupCompleter"/>
     <bean id="connectpointCompleter" class="org.onosproject.cli.net.ConnectPointCompleter"/>
diff --git a/apps/segmentrouting/web/BUCK b/apps/segmentrouting/web/BUCK
index bf6864c..9706438 100644
--- a/apps/segmentrouting/web/BUCK
+++ b/apps/segmentrouting/web/BUCK
@@ -9,7 +9,7 @@
 osgi_jar_with_tests (
     deps = COMPILE_DEPS,
     web_context = '/onos/segmentrouting',
-    api_title = 'Segment Routing Rest Server',
+    api_title = 'Segment Routing REST API',
     api_version = '1.0',
     api_description = 'REST API for Segment Routing Application',
     api_package = 'org.onosproject.segmentrouting.web',
diff --git a/apps/segmentrouting/web/pom.xml b/apps/segmentrouting/web/pom.xml
index 8c3901b..e7345e9 100644
--- a/apps/segmentrouting/web/pom.xml
+++ b/apps/segmentrouting/web/pom.xml
@@ -29,12 +29,12 @@
 
     <url>http://onosproject.org</url>
 
-    <description>Segment Routing REST Server</description>
+    <description>Segment Routing REST API</description>
 
     <properties>
         <web.context>/onos/segmentrouting</web.context>
         <api.version>1.0</api.version>
-        <api.title>Segment Routing Rest Server</api.title>
+        <api.title>Segment Routing REST APIr</api.title>
         <api.description>
             REST API for Segment Routing Application
         </api.description>
diff --git a/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/SegmentRoutingWebApplication.java b/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/SegmentRoutingWebApplication.java
index 75646d1..6704030 100644
--- a/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/SegmentRoutingWebApplication.java
+++ b/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/SegmentRoutingWebApplication.java
@@ -21,11 +21,15 @@
 import java.util.Set;
 
 /**
- * Segment Routing Web application.
+ * Segment Routing REST API.
  */
 public class SegmentRoutingWebApplication extends AbstractWebApplication {
     @Override
     public Set<Class<?>> getClasses() {
-        return getClasses(PseudowireWebResource.class, McastWebResource.class);
+        return getClasses(
+                PseudowireWebResource.class,
+                McastWebResource.class,
+                XconnectWebResource.class
+        );
     }
 }
diff --git a/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java b/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java
new file mode 100644
index 0000000..edaea9e
--- /dev/null
+++ b/apps/segmentrouting/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-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.segmentrouting.web;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.rest.AbstractWebResource;
+import org.onosproject.segmentrouting.xconnect.api.XconnectDesc;
+import org.onosproject.segmentrouting.xconnect.api.XconnectService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.onlab.util.Tools.readTreeFromStream;
+
+/**
+ * Query, create and remove Xconnects.
+ */
+@Path("xconnect")
+public class XconnectWebResource extends AbstractWebResource {
+    private static final String XCONNECTS = "xconnects";
+    private static Logger log = LoggerFactory.getLogger(XconnectWebResource.class);
+
+    /**
+     * Gets all Xconnects.
+     *
+     * @return an array of xconnects
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getXconnects() {
+        XconnectService xconnectService = get(XconnectService.class);
+        Set<XconnectDesc> xconnects = xconnectService.getXconnects();
+
+        ObjectNode result = encodeArray(XconnectDesc.class, XCONNECTS, xconnects);
+        return ok(result).build();
+    }
+
+    /**
+     * Create a new Xconnect.
+     *
+     * @param input JSON stream for xconnect to create
+     * @return 200 OK
+     * @throws IOException Throws IO exception
+     * @onos.rsModel XconnectCreate
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response addOrUpdateXconnect(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode json = readTreeFromStream(mapper, input);
+        XconnectDesc desc = codec(XconnectDesc.class).decode(json, this);
+
+        if (desc.ports().size() != 2) {
+            throw new IllegalArgumentException("Ports should have only two items.");
+        }
+
+        XconnectService xconnectService = get(XconnectService.class);
+        xconnectService.addOrUpdateXconnect(desc.key().deviceId(), desc.key().vlanId(), desc.ports());
+
+        return Response.ok().build();
+    }
+
+
+    /**
+     * Delete an existing Xconnect.
+     *
+     * @param input JSON stream for xconnect to remove
+     * @return 204 NO CONTENT
+     * @throws IOException Throws IO exception
+     * @onos.rsModel XconnectDelete
+     */
+    @DELETE
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response removeXconnect(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode json = readTreeFromStream(mapper, input);
+        XconnectDesc desc = codec(XconnectDesc.class).decode(json, this);
+
+        XconnectService xconnectService = get(XconnectService.class);
+        xconnectService.removeXonnect(desc.key().deviceId(), desc.key().vlanId());
+
+        return Response.noContent().build();
+    }
+}
diff --git a/apps/segmentrouting/web/src/main/resources/definitions/XconnectCreate.json b/apps/segmentrouting/web/src/main/resources/definitions/XconnectCreate.json
new file mode 100644
index 0000000..8d2099f
--- /dev/null
+++ b/apps/segmentrouting/web/src/main/resources/definitions/XconnectCreate.json
@@ -0,0 +1,29 @@
+{
+  "type": "object",
+  "title": "xconnect-creation",
+  "required": [
+    "deviceId",
+    "vlanId",
+    "ports"
+  ],
+  "properties": {
+    "deviceId": {
+      "type": "string",
+      "example": "of:0000000000000201",
+      "description": "Device ID"
+    },
+    "vlanId": {
+      "type": "string",
+      "example": "94",
+      "description": "VLAN ID"
+    },
+    "ports": {
+      "type": "array",
+      "items": {
+        "type": "int8",
+        "description": "Port number"
+      },
+      "example": [1, 2]
+    }
+  }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/web/src/main/resources/definitions/XconnectDelete.json b/apps/segmentrouting/web/src/main/resources/definitions/XconnectDelete.json
new file mode 100644
index 0000000..fcd5f66
--- /dev/null
+++ b/apps/segmentrouting/web/src/main/resources/definitions/XconnectDelete.json
@@ -0,0 +1,20 @@
+{
+  "type": "object",
+  "title": "xconnect-deletion",
+  "required": [
+    "deviceId",
+    "vlanId"
+  ],
+  "properties": {
+    "deviceId": {
+      "type": "string",
+      "example": "of:0000000000000201",
+      "description": "Device ID"
+    },
+    "vlanId": {
+      "type": "string",
+      "example": "94",
+      "description": "VLAN ID"
+    }
+  }
+}
\ No newline at end of file