Add VLAN cross connect config for single switch fabric

Change-Id: Ib01f6cd0325225c9573343e1cd3a2c0f52df5b7c
diff --git a/local/ce-fabric/features.xml b/local/ce-fabric/features.xml
new file mode 100644
index 0000000..83ba135
--- /dev/null
+++ b/local/ce-fabric/features.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<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}">
+        <bundle>mvn:com.google.code.gson/gson/2.6.2</bundle>
+    </feature>
+</features>
diff --git a/local/ce-fabric/pom.xml b/local/ce-fabric/pom.xml
new file mode 100644
index 0000000..0e3bb75
--- /dev/null
+++ b/local/ce-fabric/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<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">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.opencord.ce</groupId>
+        <artifactId>local</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>fabric</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>CORD application for Carrier Ethernet Service</description>
+
+    <properties>
+        <onos.app.name>org.opencord.ce.local.fabric</onos.app.name>
+        <onos.version>1.10.6</onos.version>
+        <onos.app.title>E-CORD Central Office fabric config</onos.app.title>
+        <onos.app.url>http://opencord.org</onos.app.url>
+        <onos.app.requires>org.opencord.ce.local.bigswitch</onos.app.requires>
+        <onos.app.requires>org.opencord.ce.local.channel.http</onos.app.requires>
+    </properties>
+
+    <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>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.opencord.ce</groupId>
+            <artifactId>bigswitch</artifactId>
+            <version>${parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.6.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-osgi</artifactId>
+            <version>${onos.version}</version>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <version>2.25.1</version>
+        </dependency>
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <releases><enabled>false</enabled></releases>
+        </repository>
+    </repositories>
+
+    <pluginRepositories>
+        <pluginRepository>
+            <id>snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <releases><enabled>false</enabled></releases>
+        </pluginRepository>
+    </pluginRepositories>
+
+</project>
diff --git a/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricConfig.java b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricConfig.java
new file mode 100644
index 0000000..2d8a9c1
--- /dev/null
+++ b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017-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.opencord.ce.local.fabric;
+
+import org.onlab.packet.IpAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+
+/**
+ * Configuration for single switch CORD fabric manager for Carrier Ethernet services.
+ */
+public class CarrierEthernetFabricConfig extends Config<ApplicationId> {
+    private static final int DEFAULT_PORT = 8181;
+    private static final String DEFAULT_USERNAME = "onos";
+    private static final String DEFAULT_PASSWORD = "rocks";
+
+    private static final String PUBLIC_IP = "publicIp";
+    private static final String PORT = "port";
+    private static final String USERNAME = "username";
+    private static final String PASSWORD = "password";
+    private static final String DEVICE_ID = "deviceId";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(PUBLIC_IP, PORT, USERNAME, PASSWORD, DEVICE_ID) &&
+                publicIp() != null &&
+                deviceId() != null;
+    }
+
+    public IpAddress publicIp() {
+        String ip = get(PUBLIC_IP, null);
+        try {
+            return IpAddress.valueOf(ip);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    public Integer port() {
+        String port = get(PORT, String.valueOf(DEFAULT_PORT));
+        try {
+            return Integer.valueOf(port);
+        } catch (NumberFormatException e) {
+            return DEFAULT_PORT;
+        }
+    }
+
+    public String username() {
+        return get(USERNAME, String.valueOf(DEFAULT_USERNAME));
+    }
+
+    public String password() {
+        return get(PASSWORD, String.valueOf(DEFAULT_PASSWORD));
+    }
+
+    public DeviceId deviceId() {
+        String did = get(DEVICE_ID, null);
+        try {
+            return DeviceId.deviceId(did);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+}
diff --git a/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricManager.java b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricManager.java
new file mode 100644
index 0000000..249031b
--- /dev/null
+++ b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/CarrierEthernetFabricManager.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2017-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.opencord.ce.local.fabric;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+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.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.onlab.packet.IpAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+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.opencord.ce.api.models.CarrierEthernetForwardingConstruct;
+import org.opencord.ce.api.models.CarrierEthernetNetworkInterface;
+import org.opencord.ce.api.models.CarrierEthernetUni;
+import org.opencord.ce.api.models.EvcConnId;
+import org.opencord.ce.api.services.MetroNetworkVirtualNodeService;
+import org.opencord.ce.local.bigswitch.BigSwitchService;
+import org.slf4j.Logger;
+
+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 java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Manages a single switch CORD fabric for Carrier Ethernet services.
+ *
+ * Receives forwarding constructs from global orchestrator,
+ * and generates VLAN cross connect configuration for the
+ * ONOS fabric controller.
+ *
+ * No resources are allocated so only the node forwarding API is implemented.
+ */
+@Component(immediate = true)
+@Service
+public class CarrierEthernetFabricManager implements MetroNetworkVirtualNodeService {
+    private static final Logger log = getLogger(CarrierEthernetFabricManager.class);
+    private static final String APP_NAME = "org.opencord.ce.local.fabric";
+    private static final String APPS = "apps";
+    private static final String SEGMENT_ROUTING = "org.onosproject.segmentrouting";
+    private static final String XCONNECT = "xconnect";
+    private static final String VLAN = "vlan";
+    private static final String PORTS = "ports";
+    private static final String NAME = "name";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected BigSwitchService bigSwitchService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry cfgService;
+
+    private final InternalConfigListener cfgListener = new InternalConfigListener();
+
+    private final ConfigFactory<ApplicationId, CarrierEthernetFabricConfig> configFactory =
+            new ConfigFactory<ApplicationId, CarrierEthernetFabricConfig>(APP_SUBJECT_FACTORY,
+                    CarrierEthernetFabricConfig.class, "segmentrouting_ctl", false) {
+                @Override
+                public CarrierEthernetFabricConfig createConfig() {
+                    return new CarrierEthernetFabricConfig();
+                }
+            };
+
+    private ApplicationId appId;
+    // TODO: use distributed maps via storage service
+    private Set<CarrierEthernetForwardingConstruct> forwardingConstructs = new LinkedHashSet<>();
+    private Map<EvcConnId, ConnectPoint> eePorts = new HashMap<>();
+    private Map<EvcConnId, ConnectPoint> upstreamPorts = new HashMap<>();
+    private IpAddress publicIp;
+    private Integer port;
+    private String username;
+    private String password;
+    private DeviceId deviceId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(APP_NAME);
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(configFactory);
+        cfgListener.doUpdate(cfgService.getConfig(appId, CarrierEthernetFabricConfig.class));
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        cfgService.removeListener(cfgListener);
+        cfgService.unregisterConfigFactory(configFactory);
+        log.info("Stopped");
+    }
+
+    @Override
+    public void setNodeForwarding(CarrierEthernetForwardingConstruct fc,
+                                  CarrierEthernetNetworkInterface srcNi,
+                                  Set<CarrierEthernetNetworkInterface> dstNiSet) {
+        addCrossconnect(fc, srcNi, dstNiSet);
+        postToSegmentRouting(buildConfig());
+    }
+
+    @Override
+    public void createBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
+        // No resources are allocated on the fabric
+        return;
+    }
+
+    @Override
+    public void applyBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
+        // No resources are allocated on the fabric
+        return;
+    }
+
+    @Override
+    public void removeBandwidthProfileResources(CarrierEthernetForwardingConstruct fc, CarrierEthernetUni uni) {
+        // No resources are allocated on the fabric
+        return;
+    }
+
+    @Override
+    public void removeAllForwardingResources(EvcConnId fcId) {
+        removeCrossconnect(fcId);
+        postToSegmentRouting(buildConfig());
+    }
+
+    /**
+     * Adds a fabric cross connect based on given Carrier Ethernet service.
+     *
+     * @param fc forwarding construct
+     * @param srcNi source network interface
+     * @param dstNiSet set of destination network interfaces
+     */
+    public void addCrossconnect(CarrierEthernetForwardingConstruct fc,
+                                CarrierEthernetNetworkInterface srcNi,
+                                Set<CarrierEthernetNetworkInterface> dstNiSet) {
+        // Store fc and extract physical fabric ports
+        Optional<ConnectPoint> eePort = bigSwitchService.connectPointFromVirtPort(srcNi.cp().port());
+        // Assume only a single upstream port is used, so we select randomly from set
+        CarrierEthernetNetworkInterface dstNi = dstNiSet.iterator().next();
+        Optional<ConnectPoint> upstreamPort = (dstNi == null) ? Optional.empty() :
+                bigSwitchService.connectPointFromVirtPort(dstNi.cp().port());
+        if (!eePort.isPresent() || !upstreamPort.isPresent()) {
+            log.error("Failed to install node forwarding, missing fabric ports: EE {} - upstream {}",
+                    eePort, upstreamPort);
+            return;
+        } else {
+            forwardingConstructs.add(fc);
+            eePorts.put(fc.id(), eePort.get());
+            upstreamPorts.put(fc.id(), upstreamPort.get());
+        }
+    }
+
+    /**
+     * Removes a fabric cross connect based on given forwarding construct.
+     *
+     * @param fcId forwarding construct id
+     */
+    public void removeCrossconnect(EvcConnId fcId) {
+        for (Iterator<CarrierEthernetForwardingConstruct> i = forwardingConstructs.iterator(); i.hasNext();) {
+            CarrierEthernetForwardingConstruct fc = i.next();
+            if (fcId.equals(fc.id())) {
+                forwardingConstructs.remove(fc);
+                break;
+            }
+        }
+        eePorts.remove(fcId);
+        upstreamPorts.remove(fcId);
+    }
+
+
+    /**
+     * All VLAN cross connects are rebuilt and pushed out since ONOS network config does not support updates.
+     *
+     * @return JSON with cross connect configuration for segment routing app
+     */
+    public JsonObject buildConfig() {
+        JsonArray xconnects = new JsonArray();
+        forwardingConstructs.stream()
+                .map(fc -> json(fc))
+                .forEach(fc -> xconnects.add(fc));
+
+        JsonObject dpid = new JsonObject();
+        dpid.add(deviceId.toString(), xconnects);
+
+        JsonObject xconnect = new JsonObject();
+        xconnect.add(XCONNECT, dpid);
+
+        JsonObject appName = new JsonObject();
+        appName.add(SEGMENT_ROUTING, xconnect);
+
+        JsonObject config = new JsonObject();
+        config.add(APPS, appName);
+
+        return config;
+    }
+
+    /**
+     * Execute REST POST to segment routing app with given VLAN cross connect config.
+     *
+     * @param json fabric VLAN cross connect configuration in json form
+     */
+    public void postToSegmentRouting(JsonObject json) {
+        // Setup credentials
+        HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder()
+                .nonPreemptive()
+                .credentials(username, password)
+                .build();
+        ClientConfig cfg = new ClientConfig();
+        cfg.register(feature);
+        Client client = ClientBuilder.newClient(cfg);
+
+        // Build URL and perform post
+        WebTarget target = client.target("http://" + publicIp + ":" + port + "/onos/v1/network/configuration/");
+        Response response = target.request().post(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE));
+        response.close();
+    }
+
+    /**
+     * Build fabric config json from forwarding construct.
+     *
+     * Example VLAN cross connect configuration for fabric
+     * "apps": {
+     *    "org.onosproject.segmentrouting": {
+     *       "xconnect": {
+     *          "of:0000000000000001": [{
+     *             "vlan": 10,
+     *             "ports": [5, 73],
+     *             "name": "OLT1"
+     *          }]
+     *       }
+     *    }
+     * }
+     */
+    private JsonObject json(CarrierEthernetForwardingConstruct fc) {
+        JsonObject jo = new JsonObject();
+        jo.addProperty(VLAN, fc.vlanId().toShort());
+
+        // First port is EE -> fabric, second is fabric -> upstream / CO egress
+        JsonArray ports = new JsonArray();
+        // FIXME: need to be more careful of nulls here
+        ports.add(eePorts.get(fc.id()).port().toLong());
+        ports.add(upstreamPorts.get(fc.id()).port().toLong());
+        jo.add(PORTS, ports);
+
+        jo.addProperty(NAME, fc.id().id());
+
+        return jo;
+    }
+
+    private class InternalConfigListener implements NetworkConfigListener {
+        private void doUpdate(CarrierEthernetFabricConfig cfg) {
+            if (cfg == null) {
+                log.error("Fabric config for VLAN xconnect missing");
+                return;
+            }
+
+            publicIp = cfg.publicIp();
+            port = cfg.port();
+            username = cfg.username();
+            password = cfg.password();
+            deviceId = cfg.deviceId();
+            log.info("Reconfigured");
+        }
+
+        @Override
+        public void event(NetworkConfigEvent event) {
+            if ((event.type() == NetworkConfigEvent.Type.CONFIG_ADDED ||
+                    event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED) &&
+                    event.configClass().equals(CarrierEthernetFabricConfig.class)) {
+
+                if (event.config().isPresent()) {
+                    doUpdate((CarrierEthernetFabricConfig) event.config().get());
+                }
+            }
+        }
+    }
+}
diff --git a/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/package-info.java b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/package-info.java
new file mode 100644
index 0000000..0b030a4
--- /dev/null
+++ b/local/ce-fabric/src/main/java/org/opencord/ce/local/fabric/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-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.
+ */
+
+/**
+ * Single switch CORD fabric support for Carrier Ethernet services.
+ */
+package org.opencord.ce.local.fabric;
\ No newline at end of file
diff --git a/local/ce-fabric/src/test/java/org.opencord.ce.local.fabric/CarrierEthernetFabricManagerTest.java b/local/ce-fabric/src/test/java/org.opencord.ce.local.fabric/CarrierEthernetFabricManagerTest.java
new file mode 100644
index 0000000..cabcb77
--- /dev/null
+++ b/local/ce-fabric/src/test/java/org.opencord.ce.local.fabric/CarrierEthernetFabricManagerTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2017-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.opencord.ce.local.fabric;
+
+import com.google.common.collect.Sets;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.osgi.TestServiceDirectory;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.event.AbstractListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.DeviceServiceAdapter;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.domain.DomainId;
+import org.opencord.ce.api.models.CarrierEthernetConnection;
+import org.opencord.ce.api.models.CarrierEthernetEnni;
+import org.opencord.ce.api.models.CarrierEthernetForwardingConstruct;
+import org.opencord.ce.api.models.CarrierEthernetInni;
+import org.opencord.ce.api.models.CarrierEthernetLogicalTerminationPoint;
+import org.opencord.ce.api.models.CarrierEthernetNetworkInterface;
+import org.opencord.ce.api.models.CarrierEthernetUni;
+import org.opencord.ce.api.models.EvcConnId;
+import org.opencord.ce.local.bigswitch.BigSwitchEvent;
+import org.opencord.ce.local.bigswitch.BigSwitchListener;
+import org.opencord.ce.local.bigswitch.BigSwitchService;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link CarrierEthernetFabricManager}.
+ */
+public class CarrierEthernetFabricManagerTest {
+
+    private CarrierEthernetFabricManager target;
+    private static ServiceDirectory original;
+    private static TestServiceDirectory directory;
+
+    private CarrierEthernetLogicalTerminationPoint ltpSrc;
+    private CarrierEthernetLogicalTerminationPoint ltpDst;
+    // Physical connect points
+    private static ConnectPoint srcUniCp = ConnectPoint.deviceConnectPoint("netconf:10.0.0.10/1");
+    private ConnectPoint dstUniCp = ConnectPoint.deviceConnectPoint("netconf:10.0.0.20/1");
+    private ConnectPoint inniPhyCp = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+    private ConnectPoint enni1PhyCp = ConnectPoint.deviceConnectPoint("of:0000000000000001/2");
+    private ConnectPoint enni2PhyCp = ConnectPoint.deviceConnectPoint("of:0000000000000001/3");
+    // Bigswitch (logical) connect points
+    private ConnectPoint inniBsCp = ConnectPoint.deviceConnectPoint("domain:0000000000000001/1");
+    private ConnectPoint enni1BsCp = ConnectPoint.deviceConnectPoint("domain:0000000000000001/2");
+    private ConnectPoint enni2BsCp = ConnectPoint.deviceConnectPoint("domain:0000000000000001/3");
+    // Carrier Ethernet models
+    private CarrierEthernetForwardingConstruct fc1;
+    private CarrierEthernetForwardingConstruct fc2;
+    private CarrierEthernetNetworkInterface srcUni;
+    private CarrierEthernetNetworkInterface dstUni;
+    private CarrierEthernetNetworkInterface inni;
+    private CarrierEthernetNetworkInterface enni1;
+    private CarrierEthernetNetworkInterface enni2;
+
+    private DeviceId deviceId = DeviceId.deviceId("of:0000000000000001");
+    private static VlanId vlan1 = VlanId.vlanId((short) 100);
+    private static VlanId vlan2 = VlanId.vlanId((short) 200);
+
+    @BeforeClass
+    public static void setUpBaseConfigClass() throws TestUtils.TestUtilsException {
+        directory = new TestServiceDirectory();
+        directory.add(DeviceService.class, new DeviceServiceAdapter(Collections.emptyList()));
+        original = TestUtils.getField(CarrierEthernetNetworkInterface.class, "serviceDirectory");
+        TestUtils.setField(CarrierEthernetNetworkInterface.class, "serviceDirectory", directory);
+    }
+
+    @AfterClass
+    public static void tearDownBaseConfigClass() throws TestUtils.TestUtilsException {
+        TestUtils.setField(CarrierEthernetNetworkInterface.class, "serviceDirectory", original);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        // Initialize the fabric manager
+        target = new CarrierEthernetFabricManager();
+        target.bigSwitchService = new TestBigSwitchService();
+        target.cfgService = new NetworkConfigRegistryAdapter();
+        target.coreService = new CoreServiceAdapter();
+
+        // Build UNIs
+        srcUni = CarrierEthernetUni.builder()
+                .cp(srcUniCp)
+                .build();
+        dstUni = CarrierEthernetUni.builder()
+                .cp(dstUniCp)
+                .build();
+        ltpSrc = new CarrierEthernetLogicalTerminationPoint("srcUni", srcUni);
+        ltpDst = new CarrierEthernetLogicalTerminationPoint("dstUni", dstUni);
+
+        // Build FC connecting UNIs
+        fc1 = CarrierEthernetForwardingConstruct.builder()
+                .cfgId("test1")
+                .id(EvcConnId.of("test1"))
+                .type(CarrierEthernetConnection.Type.POINT_TO_POINT)
+                .ltpSet(Sets.newHashSet(ltpSrc, ltpDst))
+                .build();
+        fc1.setVlanId(vlan1);
+        fc2 = CarrierEthernetForwardingConstruct.builder()
+                .cfgId("test2")
+                .id(EvcConnId.of("test2"))
+                .type(CarrierEthernetConnection.Type.POINT_TO_POINT)
+                .ltpSet(Sets.newHashSet(ltpSrc, ltpDst))
+                .build();
+        fc2.setVlanId(vlan2);
+        inni = CarrierEthernetInni.builder()
+                .cp(inniBsCp)
+                .build();
+        enni1 = CarrierEthernetEnni.builder()
+                .cp(enni1BsCp)
+                .build();
+        enni2 = CarrierEthernetEnni.builder()
+                .cp(enni2BsCp)
+                .build();
+
+        target.activate();
+    }
+
+    @After
+    public void tearDown() {
+        target.deactivate();
+    }
+
+    @Test
+    public void testBuildConfig() {
+        TestUtils.setField(target, "deviceId", deviceId);
+        JsonParser parser = new JsonParser();
+        JsonObject json;
+        String expected;
+
+        // Add first fc
+        target.addCrossconnect(fc1, inni, Sets.newHashSet(enni1));
+        json = target.buildConfig();
+        expected =
+                "{\"apps\":{" +
+                        "\"org.onosproject.segmentrouting\":{" +
+                        "\"xconnect\":{" +
+                        "\"of:0000000000000001\":[{" +
+                        "\"vlan\":" + vlan1.toString() + "," +
+                        "\"ports\":[1, 2]," +
+                        "\"name\": \"" + fc1.id().id() + "\"}" +
+                        "]}}}}";
+        assertEquals(json, parser.parse(expected).getAsJsonObject());
+
+        // Add second fc
+        target.addCrossconnect(fc2, inni, Sets.newHashSet(enni2));
+        json = target.buildConfig();
+        expected =
+                "{\"apps\":{" +
+                        "\"org.onosproject.segmentrouting\":{" +
+                            "\"xconnect\":{" +
+                                "\"of:0000000000000001\":[{" +
+                                    "\"vlan\":" + vlan1.toString() + "," +
+                                    "\"ports\":[1, 2]," +
+                                    "\"name\": \"" + fc1.id().id() + "\"}," +
+                                    "{\"vlan\":" + vlan2.toString() + "," +
+                                    "\"ports\":[1, 3]," +
+                                    "\"name\": \"" + fc2.id().id() + "\"}" +
+                                    "]}}}}";
+        assertEquals(json, parser.parse(expected).getAsJsonObject());
+
+        // Remove first fc
+        target.removeCrossconnect(fc1.id());
+        json = target.buildConfig();
+        expected =
+                "{\"apps\":{" +
+                        "\"org.onosproject.segmentrouting\":{" +
+                        "\"xconnect\":{" +
+                        "\"of:0000000000000001\":[{" +
+                        "\"vlan\":" + vlan2.toString() + "," +
+                        "\"ports\":[1, 3]," +
+                        "\"name\": \"" + fc2.id().id() + "\"}" +
+                        "]}}}}";
+        assertEquals(json, parser.parse(expected).getAsJsonObject());
+    }
+
+    private class TestBigSwitchService
+            extends AbstractListenerManager<BigSwitchEvent, BigSwitchListener>
+            implements BigSwitchService {
+
+        @Override
+        public List<PortDescription> getPorts() {
+            return null;
+        }
+
+        @Override
+        public PortNumber getPort(ConnectPoint port) {
+            return null;
+        }
+
+        @Override
+        public Optional<ConnectPoint> connectPointFromVirtPort(PortNumber portNumber) {
+            if (portNumber.toLong() == 1) {
+                return Optional.of(inniPhyCp);
+            }
+            if (portNumber.toLong() == 2) {
+                return Optional.of(enni1PhyCp);
+            }
+            if (portNumber.toLong() == 3) {
+                return Optional.of(enni2PhyCp);
+            }
+
+            return Optional.empty();
+        }
+
+        @Override
+        public DomainId domainId() {
+            return null;
+        }
+    }
+}