Refactored sr app to enable rest api.
The segment routing application structure has been refactored
to the following structure :
sr
---- app
---- web
web folder contains the rest cli implementations while app folder
contains everything else. Originally I tried to split the application
in api / app / web, where api would contain all the interfaces. However,
that was not possible due to the fact that most of the classes do not implement
any interfaces and app -- api would result in circular dependencies and we would
not be able to build them.
Change-Id: Ifaaeefe2c5061c8457924ccd01678fb18966c44f
diff --git a/apps/segmentrouting/app/BUCK b/apps/segmentrouting/app/BUCK
new file mode 100644
index 0000000..9674f76
--- /dev/null
+++ b/apps/segmentrouting/app/BUCK
@@ -0,0 +1,22 @@
+COMPILE_DEPS = [
+ '//lib:CORE_DEPS',
+ '//lib:JACKSON',
+ '//lib:KRYO',
+ '//lib:org.apache.karaf.shell.console',
+ '//cli:onos-cli',
+ '//core/common:onos-core-common',
+ '//core/store/serializers:onos-core-serializers',
+ '//incubator/api:onos-incubator-api',
+ '//apps/route-service/api:onos-apps-route-service-api',
+]
+
+TEST_DEPS = [
+ '//lib:TEST_ADAPTERS',
+ '//incubator/api:onos-incubator-api-tests',
+ '//apps/route-service/api:onos-apps-route-service-api-tests',
+]
+
+osgi_jar_with_tests (
+ deps = COMPILE_DEPS,
+ test_deps = TEST_DEPS,
+)
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java
new file mode 100644
index 0000000..f54ee9e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Handles Segment Routing app config events.
+ */
+public class AppConfigHandler {
+ private static final Logger log = LoggerFactory.getLogger(AppConfigHandler.class);
+ private final SegmentRoutingManager srManager;
+ private final DeviceService deviceService;
+
+ /**
+ * Constructs Segment Routing App Config Handler.
+ *
+ * @param srManager instance of {@link SegmentRoutingManager}
+ */
+ public AppConfigHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.deviceService = srManager.deviceService;
+ }
+
+ /**
+ * Processes Segment Routing App Config added event.
+ *
+ * @param event network config added event
+ */
+ protected void processAppConfigAdded(NetworkConfigEvent event) {
+ log.info("Processing AppConfig CONFIG_ADDED");
+ SegmentRoutingAppConfig config = (SegmentRoutingAppConfig) event.config().get();
+ deviceService.getAvailableDevices().forEach(device -> {
+ populateVRouter(device.id(), getMacAddresses(config));
+ });
+ }
+
+ /**
+ * Processes Segment Routing App Config updated event.
+ *
+ * @param event network config updated event
+ */
+ protected void processAppConfigUpdated(NetworkConfigEvent event) {
+ log.info("Processing AppConfig CONFIG_UPDATED");
+ SegmentRoutingAppConfig config = (SegmentRoutingAppConfig) event.config().get();
+ SegmentRoutingAppConfig prevConfig = (SegmentRoutingAppConfig) event.prevConfig().get();
+ deviceService.getAvailableDevices().forEach(device -> {
+ Set<MacAddress> macAddresses = new HashSet<>(getMacAddresses(config));
+ Set<MacAddress> prevMacAddresses = new HashSet<>(getMacAddresses(prevConfig));
+ // Avoid removing and re-adding unchanged MAC addresses since
+ // FlowObjective does not guarantee the execution order.
+ Set<MacAddress> sameMacAddresses = new HashSet<>(macAddresses);
+ sameMacAddresses.retainAll(prevMacAddresses);
+ macAddresses.removeAll(sameMacAddresses);
+ prevMacAddresses.removeAll(sameMacAddresses);
+
+ revokeVRouter(device.id(), prevMacAddresses);
+ populateVRouter(device.id(), macAddresses);
+ });
+
+ }
+
+ /**
+ * Processes Segment Routing App Config removed event.
+ *
+ * @param event network config removed event
+ */
+ protected void processAppConfigRemoved(NetworkConfigEvent event) {
+ log.info("Processing AppConfig CONFIG_REMOVED");
+ SegmentRoutingAppConfig prevConfig = (SegmentRoutingAppConfig) event.prevConfig().get();
+ deviceService.getAvailableDevices().forEach(device -> {
+ revokeVRouter(device.id(), getMacAddresses(prevConfig));
+ });
+ }
+
+ /**
+ * Populates initial vRouter rules.
+ *
+ * @param deviceId device ID
+ */
+ public void init(DeviceId deviceId) {
+ SegmentRoutingAppConfig config =
+ srManager.cfgService.getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+ populateVRouter(deviceId, getMacAddresses(config));
+ }
+
+ private void populateVRouter(DeviceId deviceId, Set<MacAddress> pendingAdd) {
+ if (!isEdge(deviceId)) {
+ return;
+ }
+ getVRouterFlowObjBuilders(pendingAdd).forEach(foBuilder -> {
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("vRouterMac filter for {} populated", pendingAdd),
+ (objective, error) ->
+ log.warn("Failed to populate vRouterMac filter for {}: {}", pendingAdd, error));
+ srManager.flowObjectiveService.filter(deviceId, foBuilder.add(context));
+ });
+ }
+
+ private void revokeVRouter(DeviceId deviceId, Set<MacAddress> pendingRemove) {
+ if (!isEdge(deviceId)) {
+ return;
+ }
+ getVRouterFlowObjBuilders(pendingRemove).forEach(foBuilder -> {
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("vRouterMac filter for {} revoked", pendingRemove),
+ (objective, error) ->
+ log.warn("Failed to revoke vRouterMac filter for {}: {}", pendingRemove, error));
+ srManager.flowObjectiveService.filter(deviceId, foBuilder.remove(context));
+ });
+ }
+
+ private Set<FilteringObjective.Builder> getVRouterFlowObjBuilders(Set<MacAddress> macAddresses) {
+ ImmutableSet.Builder<FilteringObjective.Builder> setBuilder = ImmutableSet.builder();
+ macAddresses.forEach(macAddress -> {
+ FilteringObjective.Builder fobuilder = DefaultFilteringObjective.builder();
+ fobuilder.withKey(Criteria.matchInPort(PortNumber.ANY))
+ .addCondition(Criteria.matchEthDst(macAddress))
+ .permit()
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+ .fromApp(srManager.appId);
+ setBuilder.add(fobuilder);
+ });
+ return setBuilder.build();
+ }
+
+ private Set<MacAddress> getMacAddresses(SegmentRoutingAppConfig config) {
+ if (config == null) {
+ return ImmutableSet.of();
+ }
+ return ImmutableSet.copyOf(config.vRouterMacs());
+ }
+
+ private boolean isEdge(DeviceId deviceId) {
+ try {
+ if (srManager.deviceConfiguration.isEdgeDevice(deviceId)) {
+ return true;
+ }
+ } catch (DeviceConfigNotFoundException e) { }
+ return false;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java
new file mode 100644
index 0000000..b3a0aca
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.packet.ARP;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.host.HostService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.neighbour.NeighbourMessageType.REQUEST;
+
+/**
+ * Handler of ARP packets that responses or forwards ARP packets that
+ * are sent to the controller.
+ */
+public class ArpHandler extends SegmentRoutingNeighbourHandler {
+
+ private static Logger log = LoggerFactory.getLogger(ArpHandler.class);
+
+ /**
+ * Creates an ArpHandler object.
+ *
+ * @param srManager SegmentRoutingManager object
+ */
+ public ArpHandler(SegmentRoutingManager srManager) {
+ super(srManager);
+ }
+
+ /**
+ * Processes incoming ARP packets.
+ *
+ * If it is an ARP request to router itself or known hosts,
+ * then it sends ARP response.
+ * If it is an ARP request to unknown hosts in its own subnet,
+ * then it flood the ARP request to the ports.
+ * If it is an ARP response, then set a flow rule for the host
+ * and forward any IP packets to the host in the packet buffer to the host.
+ * <p>
+ * Note: We handles all ARP packet in, even for those ARP packets between
+ * hosts in the same subnet.
+ * For an ARP packet with broadcast destination MAC,
+ * some switches pipelines will send it to the controller due to table miss,
+ * other switches will flood the packets directly in the data plane without
+ * packet in.
+ * We can deal with both cases.
+ *
+ * @param pkt incoming ARP packet and context information
+ * @param hostService the host service
+ */
+ public void processPacketIn(NeighbourMessageContext pkt, HostService hostService) {
+
+ SegmentRoutingAppConfig appConfig = srManager.cfgService
+ .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+ if (appConfig != null && appConfig.suppressSubnet().contains(pkt.inPort())) {
+ // Ignore ARP packets come from suppressed ports
+ pkt.drop();
+ return;
+ }
+
+ if (!validateArpSpa(pkt)) {
+ log.debug("Ignore ARP packet discovered on {} with unexpected src protocol address {}.",
+ pkt.inPort(), pkt.sender().getIp4Address());
+ pkt.drop();
+ return;
+ }
+
+ if (pkt.type() == REQUEST) {
+ handleArpRequest(pkt, hostService);
+ } else {
+ handleArpReply(pkt, hostService);
+ }
+ }
+
+ private void handleArpRequest(NeighbourMessageContext pkt, HostService hostService) {
+ // ARP request for router. Send ARP reply.
+ if (isArpForRouter(pkt)) {
+ MacAddress targetMac = config.getRouterMacForAGatewayIp(pkt.target().getIp4Address());
+ sendResponse(pkt, targetMac, hostService);
+ } else {
+ // NOTE: Ignore ARP packets except those target for the router
+ // We will reconsider enabling this when we have host learning support
+ /*
+ Set<Host> hosts = hostService.getHostsByIp(pkt.target());
+ if (hosts.size() > 1) {
+ log.warn("More than one host with the same ip {}", pkt.target());
+ }
+ Host targetHost = hosts.stream().findFirst().orElse(null);
+ // ARP request for known hosts. Send proxy ARP reply on behalf of the target.
+ if (targetHost != null) {
+ pkt.forward(targetHost.location());
+ // ARP request for unknown host in the subnet. Flood in the subnet.
+ } else {
+ flood(pkt);
+ }
+ */
+ }
+ }
+
+ private void handleArpReply(NeighbourMessageContext pkt, HostService hostService) {
+ // ARP reply for router. Process all pending IP packets.
+ if (isArpForRouter(pkt)) {
+ Ip4Address hostIpAddress = pkt.sender().getIp4Address();
+ srManager.ipHandler.forwardPackets(pkt.inPort().deviceId(), hostIpAddress);
+ } else {
+ // NOTE: Ignore ARP packets except those target for the router
+ // We will reconsider enabling this when we have host learning support
+ /*
+ HostId targetHostId = HostId.hostId(pkt.dstMac(), pkt.vlan());
+ Host targetHost = hostService.getHost(targetHostId);
+ // ARP reply for known hosts. Forward to the host.
+ if (targetHost != null) {
+ pkt.forward(targetHost.location());
+ // ARP reply for unknown host, Flood in the subnet.
+ } else {
+ // Don't flood to non-edge ports
+ if (pkt.vlan().equals(SegmentRoutingManager.INTERNAL_VLAN)) {
+ return;
+ }
+ flood(pkt);
+ }
+ */
+ }
+ }
+
+ /**
+ * Check if the source protocol address of an ARP packet belongs to the same
+ * subnet configured on the port it is seen.
+ *
+ * @param pkt ARP packet and context information
+ * @return true if the source protocol address belongs to the configured subnet
+ */
+ private boolean validateArpSpa(NeighbourMessageContext pkt) {
+ Ip4Address spa = pkt.sender().getIp4Address();
+ Set<IpPrefix> subnet = config.getPortSubnets(pkt.inPort().deviceId(), pkt.inPort().port())
+ .stream()
+ .filter(ipPrefix -> ipPrefix.isIp4() && ipPrefix.contains(spa))
+ .collect(Collectors.toSet());
+ return !subnet.isEmpty();
+ }
+
+
+ private boolean isArpForRouter(NeighbourMessageContext pkt) {
+ Ip4Address targetProtocolAddress = pkt.target().getIp4Address();
+ Set<IpAddress> gatewayIpAddresses = null;
+ try {
+ if (targetProtocolAddress.equals(config.getRouterIpv4(pkt.inPort().deviceId()))) {
+ return true;
+ }
+ gatewayIpAddresses = config.getPortIPs(pkt.inPort().deviceId());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting check for router IP in processing arp");
+ }
+ if (gatewayIpAddresses != null &&
+ gatewayIpAddresses.contains(targetProtocolAddress)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sends an APR request for the target IP address to all ports except in-port.
+ *
+ * @param deviceId Switch device ID
+ * @param targetAddress target IP address for ARP
+ * @param inPort in-port
+ */
+ public void sendArpRequest(DeviceId deviceId, IpAddress targetAddress, ConnectPoint inPort) {
+ byte[] senderMacAddress = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+ byte[] senderIpAddress = new byte[Ip4Address.BYTE_LENGTH];
+ /*
+ * Retrieves device info.
+ */
+ if (!getSenderInfo(senderMacAddress, senderIpAddress, deviceId, targetAddress)) {
+ log.warn("Aborting sendArpRequest, we cannot get all the information needed");
+ return;
+ }
+ /*
+ * Creates the request.
+ */
+ Ethernet arpRequest = ARP.buildArpRequest(
+ senderMacAddress,
+ senderIpAddress,
+ targetAddress.toOctets(),
+ VlanId.NO_VID
+ );
+ flood(arpRequest, inPort, targetAddress);
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
new file mode 100644
index 0000000..5baa6a9
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
@@ -0,0 +1,1612 @@
+/*
+ * Copyright 2015-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;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Default routing handler that is responsible for route computing and
+ * routing rule population.
+ */
+public class DefaultRoutingHandler {
+ private static final int MAX_CONSTANT_RETRY_ATTEMPTS = 5;
+ private static final long RETRY_INTERVAL_MS = 250L;
+ private static final int RETRY_INTERVAL_SCALE = 1;
+ private static final long STABLITY_THRESHOLD = 10; //secs
+ private static Logger log = LoggerFactory.getLogger(DefaultRoutingHandler.class);
+
+ private SegmentRoutingManager srManager;
+ private RoutingRulePopulator rulePopulator;
+ private HashMap<DeviceId, EcmpShortestPathGraph> currentEcmpSpgMap;
+ private HashMap<DeviceId, EcmpShortestPathGraph> updatedEcmpSpgMap;
+ private DeviceConfiguration config;
+ private final Lock statusLock = new ReentrantLock();
+ private volatile Status populationStatus;
+ private ScheduledExecutorService executorService
+ = newScheduledThreadPool(1, groupedThreads("retryftr", "retry-%d", log));
+ private Instant lastRoutingChange;
+
+ /**
+ * Represents the default routing population status.
+ */
+ public enum Status {
+ // population process is not started yet.
+ IDLE,
+
+ // population process started.
+ STARTED,
+
+ // population process was aborted due to errors, mostly for groups not
+ // found.
+ ABORTED,
+
+ // population process was finished successfully.
+ SUCCEEDED
+ }
+
+ /**
+ * Creates a DefaultRoutingHandler object.
+ *
+ * @param srManager SegmentRoutingManager object
+ */
+ public DefaultRoutingHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.rulePopulator = checkNotNull(srManager.routingRulePopulator);
+ this.config = checkNotNull(srManager.deviceConfiguration);
+ this.populationStatus = Status.IDLE;
+ this.currentEcmpSpgMap = Maps.newHashMap();
+ }
+
+ /**
+ * Returns an immutable copy of the current ECMP shortest-path graph as
+ * computed by this controller instance.
+ *
+ * @return immutable copy of the current ECMP graph
+ */
+ public ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEmcpSpgMap() {
+ Builder<DeviceId, EcmpShortestPathGraph> builder = ImmutableMap.builder();
+ currentEcmpSpgMap.entrySet().forEach(entry -> {
+ if (entry.getValue() != null) {
+ builder.put(entry.getKey(), entry.getValue());
+ }
+ });
+ return builder.build();
+ }
+
+ /**
+ * Acquires the lock used when making routing changes.
+ */
+ public void acquireRoutingLock() {
+ statusLock.lock();
+ }
+
+ /**
+ * Releases the lock used when making routing changes.
+ */
+ public void releaseRoutingLock() {
+ statusLock.unlock();
+ }
+
+ /**
+ * Determines if routing in the network has been stable in the last
+ * STABLITY_THRESHOLD seconds, by comparing the current time to the last
+ * routing change timestamp.
+ *
+ * @return true if stable
+ */
+ public boolean isRoutingStable() {
+ long last = (long) (lastRoutingChange.toEpochMilli() / 1000.0);
+ long now = (long) (Instant.now().toEpochMilli() / 1000.0);
+ log.trace("Routing stable since {}s", now - last);
+ return (now - last) > STABLITY_THRESHOLD;
+ }
+
+
+ //////////////////////////////////////
+ // Route path handling
+ //////////////////////////////////////
+
+ /* The following three methods represent the three major ways in which
+ * route-path handling is triggered in the network
+ * a) due to configuration change
+ * b) due to route-added event
+ * c) due to change in the topology
+ */
+
+ /**
+ * Populates all routing rules to all switches. Typically triggered at
+ * startup or after a configuration event.
+ */
+ public void populateAllRoutingRules() {
+ lastRoutingChange = Instant.now();
+ statusLock.lock();
+ try {
+ if (populationStatus == Status.STARTED) {
+ log.warn("Previous rule population is not finished. Cannot"
+ + " proceed with populateAllRoutingRules");
+ return;
+ }
+
+ populationStatus = Status.STARTED;
+ rulePopulator.resetCounter();
+ log.info("Starting to populate all routing rules");
+ log.debug("populateAllRoutingRules: populationStatus is STARTED");
+
+ // take a snapshot of the topology
+ updatedEcmpSpgMap = new HashMap<>();
+ Set<EdgePair> edgePairs = new HashSet<>();
+ Set<ArrayList<DeviceId>> routeChanges = new HashSet<>();
+ for (DeviceId dstSw : srManager.deviceConfiguration.getRouters()) {
+ EcmpShortestPathGraph ecmpSpgUpdated =
+ new EcmpShortestPathGraph(dstSw, srManager);
+ updatedEcmpSpgMap.put(dstSw, ecmpSpgUpdated);
+ DeviceId pairDev = getPairDev(dstSw);
+ if (pairDev != null) {
+ // pairDev may not be available yet, but we still need to add
+ ecmpSpgUpdated = new EcmpShortestPathGraph(pairDev, srManager);
+ updatedEcmpSpgMap.put(pairDev, ecmpSpgUpdated);
+ edgePairs.add(new EdgePair(dstSw, pairDev));
+ }
+ DeviceId ret = shouldHandleRouting(dstSw);
+ if (ret == null) {
+ continue;
+ }
+ Set<DeviceId> devsToProcess = Sets.newHashSet(dstSw, ret);
+ // To do a full reroute, assume all routes have changed
+ for (DeviceId dev : devsToProcess) {
+ for (DeviceId targetSw : srManager.deviceConfiguration.getRouters()) {
+ if (targetSw.equals(dev)) {
+ continue;
+ }
+ routeChanges.add(Lists.newArrayList(targetSw, dev));
+ }
+ }
+ }
+
+ if (!redoRouting(routeChanges, edgePairs, null)) {
+ log.debug("populateAllRoutingRules: populationStatus is ABORTED");
+ populationStatus = Status.ABORTED;
+ log.warn("Failed to repopulate all routing rules.");
+ return;
+ }
+
+ log.debug("populateAllRoutingRules: populationStatus is SUCCEEDED");
+ populationStatus = Status.SUCCEEDED;
+ log.info("Completed all routing rule population. Total # of rules pushed : {}",
+ rulePopulator.getCounter());
+ return;
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ /**
+ * Populate rules from all other edge devices to the connect-point(s)
+ * specified for the given subnets.
+ *
+ * @param cpts connect point(s) of the subnets being added
+ * @param subnets subnets being added
+ */
+ // XXX refactor
+ protected void populateSubnet(Set<ConnectPoint> cpts, Set<IpPrefix> subnets) {
+ if (cpts == null || cpts.size() < 1 || cpts.size() > 2) {
+ log.warn("Skipping populateSubnet due to illegal size of connect points. {}", cpts);
+ return;
+ }
+
+ lastRoutingChange = Instant.now();
+ statusLock.lock();
+ try {
+ if (populationStatus == Status.STARTED) {
+ log.warn("Previous rule population is not finished. Cannot"
+ + " proceed with routing rules for added routes");
+ return;
+ }
+ populationStatus = Status.STARTED;
+ rulePopulator.resetCounter();
+ log.info("Starting to populate routing rules for added routes, subnets={}, cpts={}",
+ subnets, cpts);
+ // In principle an update to a subnet/prefix should not require a
+ // new ECMPspg calculation as it is not a topology event. As a
+ // result, we use the current/existing ECMPspg in the updated map
+ // used by the redoRouting method.
+ if (updatedEcmpSpgMap == null) {
+ updatedEcmpSpgMap = new HashMap<>();
+ }
+ currentEcmpSpgMap.entrySet().forEach(entry -> {
+ updatedEcmpSpgMap.put(entry.getKey(), entry.getValue());
+ if (log.isTraceEnabled()) {
+ log.trace("Root switch: {}", entry.getKey());
+ log.trace(" Current/Existing SPG: {}", entry.getValue());
+ }
+ });
+ Set<EdgePair> edgePairs = new HashSet<>();
+ Set<ArrayList<DeviceId>> routeChanges = new HashSet<>();
+ boolean handleRouting = false;
+
+ if (cpts.size() == 2) {
+ // ensure connect points are edge-pairs
+ Iterator<ConnectPoint> iter = cpts.iterator();
+ DeviceId dev1 = iter.next().deviceId();
+ DeviceId pairDev = getPairDev(dev1);
+ if (iter.next().deviceId().equals(pairDev)) {
+ edgePairs.add(new EdgePair(dev1, pairDev));
+ } else {
+ log.warn("Connectpoints {} for subnets {} not on "
+ + "pair-devices.. aborting populateSubnet", cpts, subnets);
+ populationStatus = Status.ABORTED;
+ return;
+ }
+ for (ConnectPoint cp : cpts) {
+ if (updatedEcmpSpgMap.get(cp.deviceId()) == null) {
+ EcmpShortestPathGraph ecmpSpgUpdated =
+ new EcmpShortestPathGraph(cp.deviceId(), srManager);
+ updatedEcmpSpgMap.put(cp.deviceId(), ecmpSpgUpdated);
+ log.warn("populateSubnet: no updated graph for dev:{}"
+ + " ... creating", cp.deviceId());
+ }
+ DeviceId retId = shouldHandleRouting(cp.deviceId());
+ if (retId == null) {
+ continue;
+ }
+ handleRouting = true;
+ }
+ } else {
+ // single connect point
+ DeviceId dstSw = cpts.iterator().next().deviceId();
+ if (updatedEcmpSpgMap.get(dstSw) == null) {
+ EcmpShortestPathGraph ecmpSpgUpdated =
+ new EcmpShortestPathGraph(dstSw, srManager);
+ updatedEcmpSpgMap.put(dstSw, ecmpSpgUpdated);
+ log.warn("populateSubnet: no updated graph for dev:{}"
+ + " ... creating", dstSw);
+ }
+ if (srManager.mastershipService.isLocalMaster(dstSw)) {
+ handleRouting = true;
+ }
+ }
+
+ if (!handleRouting) {
+ log.debug("This instance is not handling ecmp routing to the "
+ + "connectPoint(s) {}", cpts);
+ populationStatus = Status.ABORTED;
+ return;
+ }
+
+ // if it gets here, this instance should handle routing for the
+ // connectpoint(s). Assume all route-paths have to be updated to
+ // the connectpoint(s) with the following exceptions
+ // 1. if target is non-edge no need for routing rules
+ // 2. if target is one of the connectpoints
+ for (ConnectPoint cp : cpts) {
+ DeviceId dstSw = cp.deviceId();
+ for (Device targetSw : srManager.deviceService.getDevices()) {
+ boolean isEdge = false;
+ try {
+ isEdge = config.isEdgeDevice(targetSw.id());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + "aborting populateSubnet on targetSw {}", targetSw.id());
+ continue;
+ }
+ if (dstSw.equals(targetSw.id()) || !isEdge ||
+ (cpts.size() == 2 &&
+ targetSw.id().equals(getPairDev(dstSw)))) {
+ continue;
+ }
+ routeChanges.add(Lists.newArrayList(targetSw.id(), dstSw));
+ }
+ }
+
+ if (!redoRouting(routeChanges, edgePairs, subnets)) {
+ log.debug("populateSubnet: populationStatus is ABORTED");
+ populationStatus = Status.ABORTED;
+ log.warn("Failed to repopulate the rules for subnet.");
+ return;
+ }
+
+ log.debug("populateSubnet: populationStatus is SUCCEEDED");
+ populationStatus = Status.SUCCEEDED;
+ log.info("Completed subnet population. Total # of rules pushed : {}",
+ rulePopulator.getCounter());
+ return;
+
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ /**
+ * Populates the routing rules or makes hash group changes according to the
+ * route-path changes due to link failure, switch failure or link up. This
+ * method should only be called for one of these three possible event-types.
+ * Note that when a switch goes away, all of its links fail as well,
+ * but this is handled as a single switch removal event.
+ *
+ * @param linkDown the single failed link, or null for other conditions
+ * such as link-up or a removed switch
+ * @param linkUp the single link up, or null for other conditions such as
+ * link-down or a removed switch
+ * @param switchDown the removed switch, or null for other conditions such as
+ * link-down or link-up
+ */ // refactor
+ public void populateRoutingRulesForLinkStatusChange(Link linkDown,
+ Link linkUp,
+ DeviceId switchDown) {
+ if ((linkDown != null && (linkUp != null || switchDown != null)) ||
+ (linkUp != null && (linkDown != null || switchDown != null)) ||
+ (switchDown != null && (linkUp != null || linkDown != null))) {
+ log.warn("Only one event can be handled for link status change .. aborting");
+ return;
+ }
+ lastRoutingChange = Instant.now();
+ statusLock.lock();
+ try {
+
+ if (populationStatus == Status.STARTED) {
+ log.warn("Previous rule population is not finished. Cannot"
+ + " proceeed with routingRules for Topology change");
+ return;
+ }
+
+ // Take snapshots of the topology
+ updatedEcmpSpgMap = new HashMap<>();
+ Set<EdgePair> edgePairs = new HashSet<>();
+ for (Device sw : srManager.deviceService.getDevices()) {
+ EcmpShortestPathGraph ecmpSpgUpdated =
+ new EcmpShortestPathGraph(sw.id(), srManager);
+ updatedEcmpSpgMap.put(sw.id(), ecmpSpgUpdated);
+ DeviceId pairDev = getPairDev(sw.id());
+ if (pairDev != null) {
+ // pairDev may not be available yet, but we still need to add
+ ecmpSpgUpdated = new EcmpShortestPathGraph(pairDev, srManager);
+ updatedEcmpSpgMap.put(pairDev, ecmpSpgUpdated);
+ edgePairs.add(new EdgePair(sw.id(), pairDev));
+ }
+ }
+
+ log.info("Starting to populate routing rules from Topology change");
+
+ Set<ArrayList<DeviceId>> routeChanges;
+ log.debug("populateRoutingRulesForLinkStatusChange: "
+ + "populationStatus is STARTED");
+ populationStatus = Status.STARTED;
+ rulePopulator.resetCounter(); //XXX maybe useful to have a rehash ctr
+ boolean hashGroupsChanged = false;
+ // try optimized re-routing
+ if (linkDown == null) {
+ // either a linkUp or a switchDown - compute all route changes by
+ // comparing all routes of existing ECMP SPG to new ECMP SPG
+ routeChanges = computeRouteChange();
+
+ // deal with linkUp of a seen-before link
+ if (linkUp != null && srManager.linkHandler.isSeenLink(linkUp)) {
+ if (!srManager.linkHandler.isBidirectional(linkUp)) {
+ log.warn("Not a bidirectional link yet .. not "
+ + "processing link {}", linkUp);
+ srManager.linkHandler.updateSeenLink(linkUp, true);
+ populationStatus = Status.ABORTED;
+ return;
+ }
+ // link previously seen before
+ // do hash-bucket changes instead of a re-route
+ processHashGroupChange(routeChanges, false, null);
+ // clear out routesChanges so a re-route is not attempted
+ routeChanges = ImmutableSet.of();
+ hashGroupsChanged = true;
+ }
+ // for a linkUp of a never-seen-before link
+ // let it fall through to a reroute of the routeChanges
+
+ // now that we are past the check for a previously seen link
+ // it is safe to update the store for the linkUp
+ if (linkUp != null) {
+ srManager.linkHandler.updateSeenLink(linkUp, true);
+ }
+
+ //deal with switchDown
+ if (switchDown != null) {
+ processHashGroupChange(routeChanges, true, switchDown);
+ // clear out routesChanges so a re-route is not attempted
+ routeChanges = ImmutableSet.of();
+ hashGroupsChanged = true;
+ }
+ } else {
+ // link has gone down
+ // Compare existing ECMP SPG only with the link that went down
+ routeChanges = computeDamagedRoutes(linkDown);
+ if (routeChanges != null) {
+ processHashGroupChange(routeChanges, true, null);
+ // clear out routesChanges so a re-route is not attempted
+ routeChanges = ImmutableSet.of();
+ hashGroupsChanged = true;
+ }
+ }
+
+ // do full re-routing if optimized routing returns null routeChanges
+ if (routeChanges == null) {
+ log.warn("Optimized routing failed... opting for full reroute");
+ populationStatus = Status.ABORTED;
+ populateAllRoutingRules();
+ return;
+ }
+
+ if (routeChanges.isEmpty()) {
+ if (hashGroupsChanged) {
+ log.info("Hash-groups changed for link status change");
+ } else {
+ log.info("No re-route or re-hash attempted for the link"
+ + " status change");
+ updatedEcmpSpgMap.keySet().forEach(devId -> {
+ currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+ log.debug("Updating ECMPspg for remaining dev:{}", devId);
+ });
+ }
+ log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is SUCCEEDED");
+ populationStatus = Status.SUCCEEDED;
+ return;
+ }
+
+ // reroute of routeChanges
+ if (redoRouting(routeChanges, edgePairs, null)) {
+ log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is SUCCEEDED");
+ populationStatus = Status.SUCCEEDED;
+ log.info("Completed repopulation of rules for link-status change."
+ + " # of rules populated : {}", rulePopulator.getCounter());
+ return;
+ } else {
+ log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is ABORTED");
+ populationStatus = Status.ABORTED;
+ log.warn("Failed to repopulate the rules for link status change.");
+ return;
+ }
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ /**
+ * Processes a set a route-path changes by reprogramming routing rules and
+ * creating new hash-groups or editing them if necessary. This method also
+ * determines the next-hops for the route-path from the src-switch (target)
+ * of the path towards the dst-switch of the path.
+ *
+ * @param routeChanges a set of route-path changes, where each route-path is
+ * a list with its first element the src-switch (target)
+ * of the path, and the second element the dst-switch of
+ * the path.
+ * @param edgePairs a set of edge-switches that are paired by configuration
+ * @param subnets a set of prefixes that need to be populated in the routing
+ * table of the target switch in the route-path. Can be null,
+ * in which case all the prefixes belonging to the dst-switch
+ * will be populated in the target switch
+ * @return true if successful in repopulating all routes
+ */
+ private boolean redoRouting(Set<ArrayList<DeviceId>> routeChanges,
+ Set<EdgePair> edgePairs, Set<IpPrefix> subnets) {
+ // first make every entry two-elements
+ Set<ArrayList<DeviceId>> changedRoutes = new HashSet<>();
+ for (ArrayList<DeviceId> route : routeChanges) {
+ if (route.size() == 1) {
+ DeviceId dstSw = route.get(0);
+ EcmpShortestPathGraph ec = updatedEcmpSpgMap.get(dstSw);
+ if (ec == null) {
+ log.warn("No graph found for {} .. aborting redoRouting", dstSw);
+ return false;
+ }
+ ec.getAllLearnedSwitchesAndVia().keySet().forEach(key -> {
+ ec.getAllLearnedSwitchesAndVia().get(key).keySet().forEach(target -> {
+ changedRoutes.add(Lists.newArrayList(target, dstSw));
+ });
+ });
+ } else {
+ DeviceId targetSw = route.get(0);
+ DeviceId dstSw = route.get(1);
+ changedRoutes.add(Lists.newArrayList(targetSw, dstSw));
+ }
+ }
+
+ // now process changedRoutes according to edgePairs
+ if (!redoRoutingEdgePairs(edgePairs, subnets, changedRoutes)) {
+ return false; //abort routing and fail fast
+ }
+
+ // whatever is left in changedRoutes is now processed for individual dsts.
+ Set<DeviceId> updatedDevices = Sets.newHashSet();
+ if (!redoRoutingIndividualDests(subnets, changedRoutes,
+ updatedDevices)) {
+ return false; //abort routing and fail fast
+ }
+
+ // update ecmpSPG for all edge-pairs
+ for (EdgePair ep : edgePairs) {
+ currentEcmpSpgMap.put(ep.dev1, updatedEcmpSpgMap.get(ep.dev1));
+ currentEcmpSpgMap.put(ep.dev2, updatedEcmpSpgMap.get(ep.dev2));
+ log.debug("Updating ECMPspg for edge-pair:{}-{}", ep.dev1, ep.dev2);
+ }
+
+ // here is where we update all devices not touched by this instance
+ updatedEcmpSpgMap.keySet().stream()
+ .filter(devId -> !edgePairs.stream().anyMatch(ep -> ep.includes(devId)))
+ .filter(devId -> !updatedDevices.contains(devId))
+ .forEach(devId -> {
+ currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+ log.debug("Updating ECMPspg for remaining dev:{}", devId);
+ });
+ return true;
+ }
+
+ /**
+ * Programs targetSw in the changedRoutes for given prefixes reachable by
+ * an edgePair. If no prefixes are given, the method will use configured
+ * subnets/prefixes. If some configured subnets belong only to a specific
+ * destination in the edgePair, then the target switch will be programmed
+ * only to that destination.
+ *
+ * @param edgePairs set of edge-pairs for which target will be programmed
+ * @param subnets a set of prefixes that need to be populated in the routing
+ * table of the target switch in the changedRoutes. Can be null,
+ * in which case all the configured prefixes belonging to the
+ * paired switches will be populated in the target switch
+ * @param changedRoutes a set of route-path changes, where each route-path is
+ * a list with its first element the src-switch (target)
+ * of the path, and the second element the dst-switch of
+ * the path.
+ * @return true if successful
+ */
+ private boolean redoRoutingEdgePairs(Set<EdgePair> edgePairs,
+ Set<IpPrefix> subnets,
+ Set<ArrayList<DeviceId>> changedRoutes) {
+ for (EdgePair ep : edgePairs) {
+ // temp store for a target's changedRoutes to this edge-pair
+ Map<DeviceId, Set<ArrayList<DeviceId>>> targetRoutes = new HashMap<>();
+ Iterator<ArrayList<DeviceId>> i = changedRoutes.iterator();
+ while (i.hasNext()) {
+ ArrayList<DeviceId> route = i.next();
+ DeviceId dstSw = route.get(1);
+ if (ep.includes(dstSw)) {
+ // routeChange for edge pair found
+ // sort by target iff target is edge and remove from changedRoutes
+ DeviceId targetSw = route.get(0);
+ try {
+ if (!srManager.deviceConfiguration.isEdgeDevice(targetSw)) {
+ continue;
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + "aborting redoRouting");
+ return false;
+ }
+ // route is from another edge to this edge-pair
+ if (targetRoutes.containsKey(targetSw)) {
+ targetRoutes.get(targetSw).add(route);
+ } else {
+ Set<ArrayList<DeviceId>> temp = new HashSet<>();
+ temp.add(route);
+ targetRoutes.put(targetSw, temp);
+ }
+ i.remove();
+ }
+ }
+ // so now for this edgepair we have a per target set of routechanges
+ // process target->edgePair route
+ for (Map.Entry<DeviceId, Set<ArrayList<DeviceId>>> entry :
+ targetRoutes.entrySet()) {
+ log.debug("* redoRoutingDstPair Target:{} -> edge-pair {}",
+ entry.getKey(), ep);
+ DeviceId targetSw = entry.getKey();
+ Map<DeviceId, Set<DeviceId>> perDstNextHops = new HashMap<>();
+ entry.getValue().forEach(route -> {
+ Set<DeviceId> nhops = getNextHops(route.get(0), route.get(1));
+ log.debug("route: target {} -> dst {} found with next-hops {}",
+ route.get(0), route.get(1), nhops);
+ perDstNextHops.put(route.get(1), nhops);
+ });
+ Set<IpPrefix> ipDev1 = (subnets == null) ? config.getSubnets(ep.dev1)
+ : subnets;
+ Set<IpPrefix> ipDev2 = (subnets == null) ? config.getSubnets(ep.dev2)
+ : subnets;
+ ipDev1 = (ipDev1 == null) ? Sets.newHashSet() : ipDev1;
+ ipDev2 = (ipDev2 == null) ? Sets.newHashSet() : ipDev2;
+ Set<DeviceId> nhDev1 = perDstNextHops.get(ep.dev1);
+ Set<DeviceId> nhDev2 = perDstNextHops.get(ep.dev2);
+ // handle routing to subnets common to edge-pair
+ // only if the targetSw is not part of the edge-pair and there
+ // exists a next hop to at least one of the devices in the edge-pair
+ if (!ep.includes(targetSw)
+ && ((nhDev1 != null && !nhDev1.isEmpty())
+ || (nhDev2 != null && !nhDev2.isEmpty()))) {
+ if (!populateEcmpRoutingRulePartial(
+ targetSw,
+ ep.dev1, ep.dev2,
+ perDstNextHops,
+ Sets.intersection(ipDev1, ipDev2))) {
+ return false; // abort everything and fail fast
+ }
+ }
+ // handle routing to subnets that only belong to dev1 only if
+ // a next-hop exists from the target to dev1
+ Set<IpPrefix> onlyDev1Subnets = Sets.difference(ipDev1, ipDev2);
+ if (!onlyDev1Subnets.isEmpty()
+ && nhDev1 != null && !nhDev1.isEmpty()) {
+ Map<DeviceId, Set<DeviceId>> onlyDev1NextHops = new HashMap<>();
+ onlyDev1NextHops.put(ep.dev1, nhDev1);
+ if (!populateEcmpRoutingRulePartial(
+ targetSw,
+ ep.dev1, null,
+ onlyDev1NextHops,
+ onlyDev1Subnets)) {
+ return false; // abort everything and fail fast
+ }
+ }
+ // handle routing to subnets that only belong to dev2 only if
+ // a next-hop exists from the target to dev2
+ Set<IpPrefix> onlyDev2Subnets = Sets.difference(ipDev2, ipDev1);
+ if (!onlyDev2Subnets.isEmpty()
+ && nhDev2 != null && !nhDev2.isEmpty()) {
+ Map<DeviceId, Set<DeviceId>> onlyDev2NextHops = new HashMap<>();
+ onlyDev2NextHops.put(ep.dev2, nhDev2);
+ if (!populateEcmpRoutingRulePartial(
+ targetSw,
+ ep.dev2, null,
+ onlyDev2NextHops,
+ onlyDev2Subnets)) {
+ return false; // abort everything and fail fast
+ }
+ }
+ }
+ // if it gets here it has succeeded for all targets to this edge-pair
+ }
+ return true;
+ }
+
+ /**
+ * Programs targetSw in the changedRoutes for given prefixes reachable by
+ * a destination switch that is not part of an edge-pair.
+ * If no prefixes are given, the method will use configured subnets/prefixes.
+ *
+ * @param subnets a set of prefixes that need to be populated in the routing
+ * table of the target switch in the changedRoutes. Can be null,
+ * in which case all the configured prefixes belonging to the
+ * paired switches will be populated in the target switch
+ * @param changedRoutes a set of route-path changes, where each route-path is
+ * a list with its first element the src-switch (target)
+ * of the path, and the second element the dst-switch of
+ * the path.
+ * @return true if successful
+ */
+ private boolean redoRoutingIndividualDests(Set<IpPrefix> subnets,
+ Set<ArrayList<DeviceId>> changedRoutes,
+ Set<DeviceId> updatedDevices) {
+ // aggregate route-path changes for each dst device
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> routesBydevice =
+ new HashMap<>();
+ for (ArrayList<DeviceId> route: changedRoutes) {
+ DeviceId dstSw = route.get(1);
+ ArrayList<ArrayList<DeviceId>> deviceRoutes =
+ routesBydevice.get(dstSw);
+ if (deviceRoutes == null) {
+ deviceRoutes = new ArrayList<>();
+ routesBydevice.put(dstSw, deviceRoutes);
+ }
+ deviceRoutes.add(route);
+ }
+ for (DeviceId impactedDstDevice : routesBydevice.keySet()) {
+ ArrayList<ArrayList<DeviceId>> deviceRoutes =
+ routesBydevice.get(impactedDstDevice);
+ for (ArrayList<DeviceId> route: deviceRoutes) {
+ log.debug("* redoRoutingIndiDst Target: {} -> dst: {}",
+ route.get(0), route.get(1));
+ DeviceId targetSw = route.get(0);
+ DeviceId dstSw = route.get(1); // same as impactedDstDevice
+ Set<DeviceId> nextHops = getNextHops(targetSw, dstSw);
+ if (nextHops.isEmpty()) {
+ log.warn("Could not find next hop from target:{} --> dst {} "
+ + "skipping this route", targetSw, dstSw);
+ continue;
+ }
+ Map<DeviceId, Set<DeviceId>> nhops = new HashMap<>();
+ nhops.put(dstSw, nextHops);
+ if (!populateEcmpRoutingRulePartial(targetSw, dstSw, null, nhops,
+ (subnets == null) ? Sets.newHashSet() : subnets)) {
+ return false; // abort routing and fail fast
+ }
+ log.debug("Populating flow rules from target: {} to dst: {}"
+ + " is successful", targetSw, dstSw);
+ }
+ //Only if all the flows for all impacted routes to a
+ //specific target are pushed successfully, update the
+ //ECMP graph for that target. Or else the next event
+ //would not see any changes in the ECMP graphs.
+ //In another case, the target switch has gone away, so
+ //routes can't be installed. In that case, the current map
+ //is updated here, without any flows being pushed.
+ currentEcmpSpgMap.put(impactedDstDevice,
+ updatedEcmpSpgMap.get(impactedDstDevice));
+ updatedDevices.add(impactedDstDevice);
+ log.debug("Updating ECMPspg for impacted dev:{}", impactedDstDevice);
+ }
+ return true;
+ }
+
+ /**
+ * Populate ECMP rules for subnets from target to destination via nexthops.
+ *
+ * @param targetSw Device ID of target switch in which rules will be programmed
+ * @param destSw1 Device ID of final destination switch to which the rules will forward
+ * @param destSw2 Device ID of paired destination switch to which the rules will forward
+ * A null deviceId indicates packets should only be sent to destSw1
+ * @param nextHops Map indication a list of next hops per destSw
+ * @param subnets Subnets to be populated. If empty, populate all configured subnets.
+ * @return true if it succeeds in populating rules
+ */ // refactor
+ private boolean populateEcmpRoutingRulePartial(DeviceId targetSw,
+ DeviceId destSw1,
+ DeviceId destSw2,
+ Map<DeviceId, Set<DeviceId>> nextHops,
+ Set<IpPrefix> subnets) {
+ boolean result;
+ // If both target switch and dest switch are edge routers, then set IP
+ // rule for both subnet and router IP.
+ boolean targetIsEdge;
+ boolean dest1IsEdge;
+ Ip4Address dest1RouterIpv4, dest2RouterIpv4 = null;
+ Ip6Address dest1RouterIpv6, dest2RouterIpv6 = null;
+
+ try {
+ targetIsEdge = config.isEdgeDevice(targetSw);
+ dest1IsEdge = config.isEdgeDevice(destSw1);
+ dest1RouterIpv4 = config.getRouterIpv4(destSw1);
+ dest1RouterIpv6 = config.getRouterIpv6(destSw1);
+ if (destSw2 != null) {
+ dest2RouterIpv4 = config.getRouterIpv4(destSw2);
+ dest2RouterIpv6 = config.getRouterIpv6(destSw2);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting populateEcmpRoutingRulePartial.");
+ return false;
+ }
+
+ if (targetIsEdge && dest1IsEdge) {
+ subnets = (subnets != null && !subnets.isEmpty())
+ ? Sets.newHashSet(subnets)
+ : Sets.newHashSet(config.getSubnets(destSw1));
+ // XXX - Rethink this
+ /*subnets.add(dest1RouterIpv4.toIpPrefix());
+ if (dest1RouterIpv6 != null) {
+ subnets.add(dest1RouterIpv6.toIpPrefix());
+ }
+ if (destSw2 != null && dest2RouterIpv4 != null) {
+ subnets.add(dest2RouterIpv4.toIpPrefix());
+ if (dest2RouterIpv6 != null) {
+ subnets.add(dest2RouterIpv6.toIpPrefix());
+ }
+ }*/
+ log.debug(". populateEcmpRoutingRulePartial in device {} towards {} {} "
+ + "for subnets {}", targetSw, destSw1,
+ (destSw2 != null) ? ("& " + destSw2) : "",
+ subnets);
+ result = rulePopulator.populateIpRuleForSubnet(targetSw, subnets,
+ destSw1, destSw2,
+ nextHops);
+ if (!result) {
+ return false;
+ }
+ /* XXX rethink this
+ IpPrefix routerIpPrefix = destRouterIpv4.toIpPrefix();
+ log.debug("* populateEcmpRoutingRulePartial in device {} towards {} "
+ + "for router IP {}", targetSw, destSw, routerIpPrefix);
+ result = rulePopulator.populateIpRuleForRouter(targetSw, routerIpPrefix,
+ destSw, nextHops);
+ if (!result) {
+ return false;
+ }
+ // If present we deal with IPv6 loopback.
+ if (destRouterIpv6 != null) {
+ routerIpPrefix = destRouterIpv6.toIpPrefix();
+ log.debug("* populateEcmpRoutingRulePartial in device {} towards {}"
+ + " for v6 router IP {}", targetSw, destSw, routerIpPrefix);
+ result = rulePopulator.populateIpRuleForRouter(targetSw, routerIpPrefix,
+ destSw, nextHops);
+ if (!result) {
+ return false;
+ }
+ }*/
+ }
+
+ if (!targetIsEdge && dest1IsEdge) {
+ // MPLS rules in all non-edge target devices. These rules are for
+ // individual destinations, even if the dsts are part of edge-pairs.
+ log.debug(". populateEcmpRoutingRulePartial in device{} towards {} for "
+ + "all MPLS rules", targetSw, destSw1);
+ result = rulePopulator.populateMplsRule(targetSw, destSw1,
+ nextHops.get(destSw1),
+ dest1RouterIpv4);
+ if (!result) {
+ return false;
+ }
+ if (dest1RouterIpv6 != null) {
+ result = rulePopulator.populateMplsRule(targetSw, destSw1,
+ nextHops.get(destSw1),
+ dest1RouterIpv6);
+ if (!result) {
+ return false;
+ }
+ }
+ }
+
+ // To save on ECMP groups
+ // avoid MPLS rules in non-edge-devices to non-edge-devices
+ // avoid MPLS transit rules in edge-devices
+ // avoid loopback IP rules in edge-devices to non-edge-devices
+ return true;
+ }
+
+ /**
+ * Processes a set a route-path changes by editing hash groups.
+ *
+ * @param routeChanges a set of route-path changes, where each route-path is
+ * a list with its first element the src-switch of the path
+ * and the second element the dst-switch of the path.
+ * @param linkOrSwitchFailed true if the route changes are for a failed
+ * switch or linkDown event
+ * @param failedSwitch the switchId if the route changes are for a failed switch,
+ * otherwise null
+ */
+ private void processHashGroupChange(Set<ArrayList<DeviceId>> routeChanges,
+ boolean linkOrSwitchFailed,
+ DeviceId failedSwitch) {
+ Set<ArrayList<DeviceId>> changedRoutes = new HashSet<>();
+ // first, ensure each routeChanges entry has two elements
+ for (ArrayList<DeviceId> route : routeChanges) {
+ if (route.size() == 1) {
+ // route-path changes are from everyone else to this switch
+ DeviceId dstSw = route.get(0);
+ srManager.deviceService.getAvailableDevices().forEach(sw -> {
+ if (!sw.id().equals(dstSw)) {
+ changedRoutes.add(Lists.newArrayList(sw.id(), dstSw));
+ }
+ });
+ } else {
+ changedRoutes.add(route);
+ }
+ }
+ boolean someFailed = false;
+ Set<DeviceId> updatedDevices = Sets.newHashSet();
+ for (ArrayList<DeviceId> route : changedRoutes) {
+ DeviceId targetSw = route.get(0);
+ DeviceId dstSw = route.get(1);
+ if (linkOrSwitchFailed) {
+ boolean success = fixHashGroupsForRoute(route, true);
+ // it's possible that we cannot fix hash groups for a route
+ // if the target switch has failed. Nevertheless the ecmp graph
+ // for the impacted switch must still be updated.
+ if (!success && failedSwitch != null && targetSw.equals(failedSwitch)) {
+ currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+ currentEcmpSpgMap.remove(targetSw);
+ log.debug("Updating ECMPspg for dst:{} removing failed switch "
+ + "target:{}", dstSw, targetSw);
+ updatedDevices.add(targetSw);
+ updatedDevices.add(dstSw);
+ continue;
+ }
+ //linkfailed - update both sides
+ if (success) {
+ currentEcmpSpgMap.put(targetSw, updatedEcmpSpgMap.get(targetSw));
+ currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+ log.debug("Updating ECMPspg for dst:{} and target:{} for linkdown"
+ + " or switchdown", dstSw, targetSw);
+ updatedDevices.add(targetSw);
+ updatedDevices.add(dstSw);
+ } else {
+ someFailed = true;
+ }
+ } else {
+ //linkup of seen before link
+ boolean success = fixHashGroupsForRoute(route, false);
+ if (success) {
+ currentEcmpSpgMap.put(targetSw, updatedEcmpSpgMap.get(targetSw));
+ currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+ log.debug("Updating ECMPspg for target:{} and dst:{} for linkup",
+ targetSw, dstSw);
+ updatedDevices.add(targetSw);
+ updatedDevices.add(dstSw);
+ } else {
+ someFailed = true;
+ }
+ }
+ }
+ if (!someFailed) {
+ // here is where we update all devices not touched by this instance
+ updatedEcmpSpgMap.keySet().stream()
+ .filter(devId -> !updatedDevices.contains(devId))
+ .forEach(devId -> {
+ currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+ log.debug("Updating ECMPspg for remaining dev:{}", devId);
+ });
+ }
+ }
+
+ /**
+ * Edits hash groups in the src-switch (targetSw) of a route-path by
+ * calling the groupHandler to either add or remove buckets in an existing
+ * hash group.
+ *
+ * @param route a single list representing a route-path where the first element
+ * is the src-switch (targetSw) of the route-path and the
+ * second element is the dst-switch
+ * @param revoke true if buckets in the hash-groups need to be removed;
+ * false if buckets in the hash-groups need to be added
+ * @return true if the hash group editing is successful
+ */
+ private boolean fixHashGroupsForRoute(ArrayList<DeviceId> route,
+ boolean revoke) {
+ DeviceId targetSw = route.get(0);
+ if (route.size() < 2) {
+ log.warn("Cannot fixHashGroupsForRoute - no dstSw in route {}", route);
+ return false;
+ }
+ DeviceId destSw = route.get(1);
+ log.debug("* processing fixHashGroupsForRoute: Target {} -> Dest {}",
+ targetSw, destSw);
+ // figure out the new next hops at the targetSw towards the destSw
+ Set<DeviceId> nextHops = getNextHops(targetSw, destSw);
+ // call group handler to change hash group at targetSw
+ DefaultGroupHandler grpHandler = srManager.getGroupHandler(targetSw);
+ if (grpHandler == null) {
+ log.warn("Cannot find grouphandler for dev:{} .. aborting"
+ + " {} hash group buckets for route:{} ", targetSw,
+ (revoke) ? "revoke" : "repopulate", route);
+ return false;
+ }
+ log.debug("{} hash-groups buckets For Route {} -> {} to next-hops {}",
+ (revoke) ? "revoke" : "repopulating",
+ targetSw, destSw, nextHops);
+ return (revoke) ? grpHandler.fixHashGroups(targetSw, nextHops,
+ destSw, true)
+ : grpHandler.fixHashGroups(targetSw, nextHops,
+ destSw, false);
+ }
+
+ /**
+ * Start the flow rule population process if it was never started. The
+ * process finishes successfully when all flow rules are set and stops with
+ * ABORTED status when any groups required for flows is not set yet.
+ */
+ public void startPopulationProcess() {
+ statusLock.lock();
+ try {
+ if (populationStatus == Status.IDLE
+ || populationStatus == Status.SUCCEEDED
+ || populationStatus == Status.ABORTED) {
+ populateAllRoutingRules();
+ } else {
+ log.warn("Not initiating startPopulationProcess as populationStatus is {}",
+ populationStatus);
+ }
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ /**
+ * Revoke rules of given subnet in all edge switches.
+ *
+ * @param subnets subnet being removed
+ * @return true if succeed
+ */
+ protected boolean revokeSubnet(Set<IpPrefix> subnets) {
+ statusLock.lock();
+ try {
+ return srManager.routingRulePopulator.revokeIpRuleForSubnet(subnets);
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ /**
+ * Populates IP rules for a route that has direct connection to the switch
+ * if the current instance is the master of the switch.
+ *
+ * @param deviceId device ID of the device that next hop attaches to
+ * @param prefix IP prefix of the route
+ * @param hostMac MAC address of the next hop
+ * @param hostVlanId Vlan ID of the nexthop
+ * @param outPort port where the next hop attaches to
+ */
+ void populateRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ if (srManager.mastershipService.isLocalMaster(deviceId)) {
+ srManager.routingRulePopulator.populateRoute(deviceId, prefix, hostMac, hostVlanId, outPort);
+ }
+ }
+
+ /**
+ * Removes IP rules for a route when the next hop is gone.
+ * if the current instance is the master of the switch.
+ *
+ * @param deviceId device ID of the device that next hop attaches to
+ * @param prefix IP prefix of the route
+ * @param hostMac MAC address of the next hop
+ * @param hostVlanId Vlan ID of the nexthop
+ * @param outPort port that next hop attaches to
+ */
+ void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ if (srManager.mastershipService.isLocalMaster(deviceId)) {
+ srManager.routingRulePopulator.revokeRoute(deviceId, prefix, hostMac, hostVlanId, outPort);
+ }
+ }
+
+ /**
+ * Remove ECMP graph entry for the given device. Typically called when
+ * device is no longer available.
+ *
+ * @param deviceId the device for which graphs need to be purged
+ */
+ protected void purgeEcmpGraph(DeviceId deviceId) {
+ statusLock.lock();
+ try {
+
+ if (populationStatus == Status.STARTED) {
+ log.warn("Previous rule population is not finished. Cannot"
+ + " proceeed with purgeEcmpGraph for {}", deviceId);
+ return;
+ }
+ log.debug("Updating ECMPspg for unavailable dev:{}", deviceId);
+ currentEcmpSpgMap.remove(deviceId);
+ if (updatedEcmpSpgMap != null) {
+ updatedEcmpSpgMap.remove(deviceId);
+ }
+ } finally {
+ statusLock.unlock();
+ }
+ }
+
+ //////////////////////////////////////
+ // Routing helper methods and classes
+ //////////////////////////////////////
+
+ /**
+ * Computes set of affected routes due to failed link. Assumes
+ * previous ecmp shortest-path graph exists for a switch in order to compute
+ * affected routes. If such a graph does not exist, the method returns null.
+ *
+ * @param linkFail the failed link
+ * @return the set of affected routes which may be empty if no routes were
+ * affected, or null if no previous ecmp spg was found for comparison
+ */
+ private Set<ArrayList<DeviceId>> computeDamagedRoutes(Link linkFail) {
+ Set<ArrayList<DeviceId>> routes = new HashSet<>();
+
+ for (Device sw : srManager.deviceService.getDevices()) {
+ log.debug("Computing the impacted routes for device {} due to link fail",
+ sw.id());
+ DeviceId retId = shouldHandleRouting(sw.id());
+ if (retId == null) {
+ continue;
+ }
+ Set<DeviceId> devicesToProcess = Sets.newHashSet(retId, sw.id());
+ for (DeviceId rootSw : devicesToProcess) {
+ EcmpShortestPathGraph ecmpSpg = currentEcmpSpgMap.get(rootSw);
+ if (ecmpSpg == null) {
+ log.warn("No existing ECMP graph for switch {}. Aborting optimized"
+ + " rerouting and opting for full-reroute", rootSw);
+ return null;
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Root switch: {}", rootSw);
+ log.debug(" Current/Existing SPG: {}", ecmpSpg);
+ log.debug(" New/Updated SPG: {}", updatedEcmpSpgMap.get(rootSw));
+ }
+ HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>>
+ switchVia = ecmpSpg.getAllLearnedSwitchesAndVia();
+ // figure out if the broken link affected any route-paths in this graph
+ for (Integer itrIdx : switchVia.keySet()) {
+ log.trace("Current/Exiting SPG Iterindex# {}", itrIdx);
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+ switchVia.get(itrIdx);
+ for (DeviceId targetSw : swViaMap.keySet()) {
+ log.trace("TargetSwitch {} --> RootSwitch {}",
+ targetSw, rootSw);
+ for (ArrayList<DeviceId> via : swViaMap.get(targetSw)) {
+ log.trace(" Via:");
+ via.forEach(e -> log.trace(" {}", e));
+ }
+ Set<ArrayList<DeviceId>> subLinks =
+ computeLinks(targetSw, rootSw, swViaMap);
+ for (ArrayList<DeviceId> alink: subLinks) {
+ if ((alink.get(0).equals(linkFail.src().deviceId()) &&
+ alink.get(1).equals(linkFail.dst().deviceId()))
+ ||
+ (alink.get(0).equals(linkFail.dst().deviceId()) &&
+ alink.get(1).equals(linkFail.src().deviceId()))) {
+ log.debug("Impacted route:{}->{}", targetSw, rootSw);
+ ArrayList<DeviceId> aRoute = new ArrayList<>();
+ aRoute.add(targetSw); // switch with rules to populate
+ aRoute.add(rootSw); // towards this destination
+ routes.add(aRoute);
+ break;
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+ return routes;
+ }
+
+ /**
+ * Computes set of affected routes due to new links or failed switches.
+ *
+ * @return the set of affected routes which may be empty if no routes were
+ * affected
+ */
+ private Set<ArrayList<DeviceId>> computeRouteChange() {
+ ImmutableSet.Builder<ArrayList<DeviceId>> changedRtBldr =
+ ImmutableSet.builder();
+
+ for (Device sw : srManager.deviceService.getDevices()) {
+ log.debug("Computing the impacted routes for device {}", sw.id());
+ DeviceId retId = shouldHandleRouting(sw.id());
+ if (retId == null) {
+ continue;
+ }
+ Set<DeviceId> devicesToProcess = Sets.newHashSet(retId, sw.id());
+ for (DeviceId rootSw : devicesToProcess) {
+ if (log.isTraceEnabled()) {
+ log.trace("Device links for dev: {}", rootSw);
+ for (Link link: srManager.linkService.getDeviceLinks(rootSw)) {
+ log.trace("{} -> {} ", link.src().deviceId(),
+ link.dst().deviceId());
+ }
+ }
+ EcmpShortestPathGraph currEcmpSpg = currentEcmpSpgMap.get(rootSw);
+ if (currEcmpSpg == null) {
+ log.debug("No existing ECMP graph for device {}.. adding self as "
+ + "changed route", rootSw);
+ changedRtBldr.add(Lists.newArrayList(rootSw));
+ continue;
+ }
+ EcmpShortestPathGraph newEcmpSpg = updatedEcmpSpgMap.get(rootSw);
+ if (log.isDebugEnabled()) {
+ log.debug("Root switch: {}", rootSw);
+ log.debug(" Current/Existing SPG: {}", currEcmpSpg);
+ log.debug(" New/Updated SPG: {}", newEcmpSpg);
+ }
+ // first use the updated/new map to compare to current/existing map
+ // as new links may have come up
+ changedRtBldr.addAll(compareGraphs(newEcmpSpg, currEcmpSpg, rootSw));
+ // then use the current/existing map to compare to updated/new map
+ // as switch may have been removed
+ changedRtBldr.addAll(compareGraphs(currEcmpSpg, newEcmpSpg, rootSw));
+ }
+ }
+
+ Set<ArrayList<DeviceId>> changedRoutes = changedRtBldr.build();
+ for (ArrayList<DeviceId> route: changedRoutes) {
+ log.debug("Route changes Target -> Root");
+ if (route.size() == 1) {
+ log.debug(" : all -> {}", route.get(0));
+ } else {
+ log.debug(" : {} -> {}", route.get(0), route.get(1));
+ }
+ }
+ return changedRoutes;
+ }
+
+ /**
+ * For the root switch, searches all the target nodes reachable in the base
+ * graph, and compares paths to the ones in the comp graph.
+ *
+ * @param base the graph that is indexed for all reachable target nodes
+ * from the root node
+ * @param comp the graph that the base graph is compared to
+ * @param rootSw both ecmp graphs are calculated for the root node
+ * @return all the routes that have changed in the base graph
+ */
+ private Set<ArrayList<DeviceId>> compareGraphs(EcmpShortestPathGraph base,
+ EcmpShortestPathGraph comp,
+ DeviceId rootSw) {
+ ImmutableSet.Builder<ArrayList<DeviceId>> changedRoutesBuilder =
+ ImmutableSet.builder();
+ HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> baseMap =
+ base.getAllLearnedSwitchesAndVia();
+ HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> compMap =
+ comp.getAllLearnedSwitchesAndVia();
+ for (Integer itrIdx : baseMap.keySet()) {
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> baseViaMap =
+ baseMap.get(itrIdx);
+ for (DeviceId targetSw : baseViaMap.keySet()) {
+ ArrayList<ArrayList<DeviceId>> basePath = baseViaMap.get(targetSw);
+ ArrayList<ArrayList<DeviceId>> compPath = getVia(compMap, targetSw);
+ if ((compPath == null) || !basePath.equals(compPath)) {
+ log.trace("Impacted route:{} -> {}", targetSw, rootSw);
+ ArrayList<DeviceId> route = new ArrayList<>();
+ route.add(targetSw); // switch with rules to populate
+ route.add(rootSw); // towards this destination
+ changedRoutesBuilder.add(route);
+ }
+ }
+ }
+ return changedRoutesBuilder.build();
+ }
+
+ /**
+ * Returns the ECMP paths traversed to reach the target switch.
+ *
+ * @param switchVia a per-iteration view of the ECMP graph for a root switch
+ * @param targetSw the switch to reach from the root switch
+ * @return the nodes traversed on ECMP paths to the target switch
+ */
+ private ArrayList<ArrayList<DeviceId>> getVia(HashMap<Integer, HashMap<DeviceId,
+ ArrayList<ArrayList<DeviceId>>>> switchVia, DeviceId targetSw) {
+ for (Integer itrIdx : switchVia.keySet()) {
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+ switchVia.get(itrIdx);
+ if (swViaMap.get(targetSw) == null) {
+ continue;
+ } else {
+ return swViaMap.get(targetSw);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method to break down a path from src to dst device into a collection
+ * of links.
+ *
+ * @param src src device of the path
+ * @param dst dst device of the path
+ * @param viaMap path taken from src to dst device
+ * @return collection of links in the path
+ */
+ private Set<ArrayList<DeviceId>> computeLinks(DeviceId src,
+ DeviceId dst,
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> viaMap) {
+ Set<ArrayList<DeviceId>> subLinks = Sets.newHashSet();
+ for (ArrayList<DeviceId> via : viaMap.get(src)) {
+ DeviceId linkSrc = src;
+ DeviceId linkDst = dst;
+ for (DeviceId viaDevice: via) {
+ ArrayList<DeviceId> link = new ArrayList<>();
+ linkDst = viaDevice;
+ link.add(linkSrc);
+ link.add(linkDst);
+ subLinks.add(link);
+ linkSrc = viaDevice;
+ }
+ ArrayList<DeviceId> link = new ArrayList<>();
+ link.add(linkSrc);
+ link.add(dst);
+ subLinks.add(link);
+ }
+
+ return subLinks;
+ }
+
+ /**
+ * Determines whether this controller instance should handle routing for the
+ * given {@code deviceId}, based on mastership and pairDeviceId if one exists.
+ * Returns null if this instance should not handle routing for given {@code deviceId}.
+ * Otherwise the returned value could be the given deviceId itself, or the
+ * deviceId for the paired edge device. In the latter case, this instance
+ * should handle routing for both the given device and the paired device.
+ *
+ * @param deviceId device identifier to consider for routing
+ * @return null or deviceId which could be the same as the given deviceId
+ * or the deviceId of a paired edge device
+ */
+ private DeviceId shouldHandleRouting(DeviceId deviceId) {
+ if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+ log.debug("Not master for dev:{} .. skipping routing, may get handled "
+ + "elsewhere as part of paired devices", deviceId);
+ return null;
+ }
+ NodeId myNode = srManager.mastershipService.getMasterFor(deviceId);
+ DeviceId pairDev = getPairDev(deviceId);
+
+ if (pairDev != null) {
+ if (!srManager.deviceService.isAvailable(pairDev)) {
+ log.warn("pairedDev {} not available .. routing this dev:{} "
+ + "without mastership check",
+ pairDev, deviceId);
+ return pairDev; // handle both temporarily
+ }
+ NodeId pairMasterNode = srManager.mastershipService.getMasterFor(pairDev);
+ if (myNode.compareTo(pairMasterNode) <= 0) {
+ log.debug("Handling routing for both dev:{} pair-dev:{}; myNode: {}"
+ + " pairMaster:{} compare:{}", deviceId, pairDev,
+ myNode, pairMasterNode,
+ myNode.compareTo(pairMasterNode));
+ return pairDev; // handle both
+ } else {
+ log.debug("PairDev node: {} should handle routing for dev:{} and "
+ + "pair-dev:{}", pairMasterNode, deviceId, pairDev);
+ return null; // handle neither
+ }
+ }
+ return deviceId; // not paired, just handle given device
+ }
+
+ /**
+ * Returns the configured paired DeviceId for the given Device, or null
+ * if no such paired device has been configured.
+ *
+ * @param deviceId
+ * @return configured pair deviceId or null
+ */
+ private DeviceId getPairDev(DeviceId deviceId) {
+ DeviceId pairDev;
+ try {
+ pairDev = srManager.deviceConfiguration.getPairDeviceId(deviceId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " .. cannot continue routing for dev: {}");
+ return null;
+ }
+ return pairDev;
+ }
+
+ /**
+ * Returns the set of deviceIds which are the next hops from the targetSw
+ * to the dstSw according to the latest ECMP spg.
+ *
+ * @param targetSw the switch for which the next-hops are desired
+ * @param dstSw the switch to which the next-hops lead to from the targetSw
+ * @return set of next hop deviceIds, could be empty if no next hops are found
+ */
+ private Set<DeviceId> getNextHops(DeviceId targetSw, DeviceId dstSw) {
+ boolean targetIsEdge = false;
+ try {
+ targetIsEdge = srManager.deviceConfiguration.isEdgeDevice(targetSw);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + "Cannot determine if targetIsEdge {}.. "
+ + "continuing to getNextHops", targetSw);
+ }
+
+ EcmpShortestPathGraph ecmpSpg = updatedEcmpSpgMap.get(dstSw);
+ if (ecmpSpg == null) {
+ log.debug("No ecmpSpg found for dstSw: {}", dstSw);
+ return ImmutableSet.of();
+ }
+ HashMap<Integer,
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> switchVia =
+ ecmpSpg.getAllLearnedSwitchesAndVia();
+ for (Integer itrIdx : switchVia.keySet()) {
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+ switchVia.get(itrIdx);
+ for (DeviceId target : swViaMap.keySet()) {
+ if (!target.equals(targetSw)) {
+ continue;
+ }
+ if (!targetIsEdge && itrIdx > 1) {
+ // optimization for spines to not use other leaves to get
+ // to a leaf to avoid loops
+ log.debug("Avoiding {} hop path for non-edge targetSw:{}"
+ + " --> dstSw:{}", itrIdx, targetSw, dstSw);
+ break;
+ }
+ Set<DeviceId> nextHops = new HashSet<>();
+ for (ArrayList<DeviceId> via : swViaMap.get(targetSw)) {
+ if (via.isEmpty()) {
+ // the dstSw is the next-hop from the targetSw
+ nextHops.add(dstSw);
+ } else {
+ // first elem is next-hop in each ECMP path
+ nextHops.add(via.get(0));
+ }
+ }
+ return nextHops;
+ }
+ }
+ return ImmutableSet.of(); //no next-hops found
+ }
+
+ /**
+ * Represents two devices that are paired by configuration. An EdgePair for
+ * (dev1, dev2) is the same as as EdgePair for (dev2, dev1)
+ */
+ protected final class EdgePair {
+ DeviceId dev1;
+ DeviceId dev2;
+
+ EdgePair(DeviceId dev1, DeviceId dev2) {
+ this.dev1 = dev1;
+ this.dev2 = dev2;
+ }
+
+ boolean includes(DeviceId dev) {
+ return dev1.equals(dev) || dev2.equals(dev);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof EdgePair)) {
+ return false;
+ }
+ EdgePair that = (EdgePair) o;
+ return ((this.dev1.equals(that.dev1) && this.dev2.equals(that.dev2)) ||
+ (this.dev1.equals(that.dev2) && this.dev2.equals(that.dev1)));
+ }
+
+ @Override
+ public int hashCode() {
+ if (dev1.toString().compareTo(dev2.toString()) <= 0) {
+ return Objects.hash(dev1, dev2);
+ } else {
+ return Objects.hash(dev2, dev1);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("Dev1", dev1)
+ .add("Dev2", dev2)
+ .toString();
+ }
+ }
+
+ //////////////////////////////////////
+ // Filtering rule creation
+ //////////////////////////////////////
+
+ /**
+ * Populates filtering rules for port, and punting rules
+ * for gateway IPs, loopback IPs and arp/ndp traffic.
+ * Should only be called by the master instance for this device/port.
+ *
+ * @param deviceId Switch ID to set the rules
+ */
+ public void populatePortAddressingRules(DeviceId deviceId) {
+ // Although device is added, sometimes device store does not have the
+ // ports for this device yet. It results in missing filtering rules in the
+ // switch. We will attempt it a few times. If it still does not work,
+ // user can manually repopulate using CLI command sr-reroute-network
+ PortFilterInfo firstRun = rulePopulator.populateVlanMacFilters(deviceId);
+ if (firstRun == null) {
+ firstRun = new PortFilterInfo(0, 0, 0);
+ }
+ executorService.schedule(new RetryFilters(deviceId, firstRun),
+ RETRY_INTERVAL_MS, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Utility class used to temporarily store information about the ports on a
+ * device processed for filtering objectives.
+ */
+ public final class PortFilterInfo {
+ int disabledPorts = 0, errorPorts = 0, filteredPorts = 0;
+
+ public PortFilterInfo(int disabledPorts, int errorPorts,
+ int filteredPorts) {
+ this.disabledPorts = disabledPorts;
+ this.filteredPorts = filteredPorts;
+ this.errorPorts = errorPorts;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(disabledPorts, filteredPorts, errorPorts);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if ((obj == null) || (!(obj instanceof PortFilterInfo))) {
+ return false;
+ }
+ PortFilterInfo other = (PortFilterInfo) obj;
+ return ((disabledPorts == other.disabledPorts) &&
+ (filteredPorts == other.filteredPorts) &&
+ (errorPorts == other.errorPorts));
+ }
+
+ @Override
+ public String toString() {
+ MoreObjects.ToStringHelper helper = toStringHelper(this)
+ .add("disabledPorts", disabledPorts)
+ .add("errorPorts", errorPorts)
+ .add("filteredPorts", filteredPorts);
+ return helper.toString();
+ }
+ }
+
+ /**
+ * RetryFilters populates filtering objectives for a device and keeps retrying
+ * till the number of ports filtered are constant for a predefined number
+ * of attempts.
+ */
+ protected final class RetryFilters implements Runnable {
+ int constantAttempts = MAX_CONSTANT_RETRY_ATTEMPTS;
+ DeviceId devId;
+ int counter;
+ PortFilterInfo prevRun;
+
+ private RetryFilters(DeviceId deviceId, PortFilterInfo previousRun) {
+ devId = deviceId;
+ prevRun = previousRun;
+ counter = 0;
+ }
+
+ @Override
+ public void run() {
+ log.debug("RETRY FILTER ATTEMPT {} ** dev:{}", ++counter, devId);
+ PortFilterInfo thisRun = rulePopulator.populateVlanMacFilters(devId);
+ boolean sameResult = prevRun.equals(thisRun);
+ log.debug("dev:{} prevRun:{} thisRun:{} sameResult:{}", devId, prevRun,
+ thisRun, sameResult);
+ if (thisRun == null || !sameResult || (sameResult && --constantAttempts > 0)) {
+ // exponentially increasing intervals for retries
+ executorService.schedule(this,
+ RETRY_INTERVAL_MS * (int) Math.pow(counter, RETRY_INTERVAL_SCALE),
+ TimeUnit.MILLISECONDS);
+ if (!sameResult) {
+ constantAttempts = MAX_CONSTANT_RETRY_ATTEMPTS; //reset
+ }
+ }
+ prevRun = (thisRun == null) ? prevRun : thisRun;
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java
new file mode 100644
index 0000000..0fd7c077
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2015-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;
+
+import java.util.List;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default Tunnel class.
+ */
+public class DefaultTunnel implements Tunnel {
+
+ private final String id;
+ private final List<Integer> labelIds;
+
+ private int groupId;
+ private boolean allowedToRemoveGroup;
+
+ /**
+ * Creates a Tunnel reference.
+ *
+ * @param tid Tunnel ID
+ * @param labelIds Label stack of the tunnel
+ */
+ public DefaultTunnel(String tid, List<Integer> labelIds) {
+ this.id = checkNotNull(tid);
+ this.labelIds = labelIds;
+ //TODO: need to register the class in Kryo for this
+ //this.labelIds = Collections.unmodifiableList(labelIds);
+ this.groupId = -1;
+ }
+
+ /**
+ * Creates a new DefaultTunnel reference using the tunnel reference.
+ *
+ * @param tunnel DefaultTunnel reference
+ */
+ public DefaultTunnel(DefaultTunnel tunnel) {
+ this.id = tunnel.id;
+ this.labelIds = tunnel.labelIds;
+ this.groupId = tunnel.groupId;
+ }
+
+ @Override
+ public String id() {
+ return this.id;
+ }
+
+ @Override
+ public List<Integer> labelIds() {
+ return this.labelIds;
+ }
+
+ @Override
+ public int groupId() {
+ return this.groupId;
+ }
+
+ @Override
+ public void setGroupId(int id) {
+ this.groupId = id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o instanceof DefaultTunnel) {
+ DefaultTunnel tunnel = (DefaultTunnel) o;
+ // We compare only the tunnel paths.
+ if (tunnel.labelIds.equals(this.labelIds)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return labelIds.hashCode();
+ }
+
+ @Override
+ public boolean isAllowedToRemoveGroup() {
+ return this.allowedToRemoveGroup;
+ }
+
+ @Override
+ public void allowToRemoveGroup(boolean b) {
+ this.allowedToRemoveGroup = b;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java
new file mode 100644
index 0000000..c9bd596
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.graph.ScalarWeight;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DefaultPath;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.provider.ProviderId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Set;
+
+/**
+ * This class creates breadth-first-search (BFS) tree for a given root device
+ * and returns paths from the root Device to leaf Devices (target devices).
+ * The paths are snapshot paths at the point of the class instantiation.
+ */
+public class EcmpShortestPathGraph {
+ LinkedList<DeviceId> deviceQueue = new LinkedList<>();
+ LinkedList<Integer> distanceQueue = new LinkedList<>();
+ HashMap<DeviceId, Integer> deviceSearched = new HashMap<>();
+ HashMap<DeviceId, ArrayList<Link>> upstreamLinks = new HashMap<>();
+ HashMap<DeviceId, ArrayList<Path>> paths = new HashMap<>();
+ HashMap<Integer, ArrayList<DeviceId>> distanceDeviceMap = new HashMap<>();
+ DeviceId rootDevice;
+ private SegmentRoutingManager srManager;
+ private static final Logger log = LoggerFactory.getLogger(EcmpShortestPathGraph.class);
+
+ /**
+ * Constructor.
+ *
+ * @param rootDevice root of the BFS tree
+ * @param srManager SegmentRoutingManager object
+ */
+ public EcmpShortestPathGraph(DeviceId rootDevice, SegmentRoutingManager srManager) {
+ this.rootDevice = rootDevice;
+ this.srManager = srManager;
+ calcECMPShortestPathGraph();
+ }
+
+ /**
+ * Calculates the BFS tree.
+ */
+ private void calcECMPShortestPathGraph() {
+ deviceQueue.add(rootDevice);
+ int currDistance = 0;
+ distanceQueue.add(currDistance);
+ deviceSearched.put(rootDevice, currDistance);
+ while (!deviceQueue.isEmpty()) {
+ DeviceId sw = deviceQueue.poll();
+ Set<DeviceId> prevSw = Sets.newHashSet();
+ currDistance = distanceQueue.poll();
+
+ for (Link link : srManager.linkService.getDeviceEgressLinks(sw)) {
+ if (srManager.linkHandler.avoidLink(link)) {
+ continue;
+ }
+ DeviceId reachedDevice = link.dst().deviceId();
+ if (prevSw.contains(reachedDevice)) {
+ // Ignore LAG links between the same set of Devices
+ continue;
+ } else {
+ prevSw.add(reachedDevice);
+ }
+
+ Integer distance = deviceSearched.get(reachedDevice);
+ if ((distance != null) && (distance < (currDistance + 1))) {
+ continue;
+ }
+ if (distance == null) {
+ // First time visiting this Device node
+ deviceQueue.add(reachedDevice);
+ distanceQueue.add(currDistance + 1);
+ deviceSearched.put(reachedDevice, currDistance + 1);
+
+ ArrayList<DeviceId> distanceSwArray = distanceDeviceMap
+ .get(currDistance + 1);
+ if (distanceSwArray == null) {
+ distanceSwArray = new ArrayList<>();
+ distanceSwArray.add(reachedDevice);
+ distanceDeviceMap.put(currDistance + 1, distanceSwArray);
+ } else {
+ distanceSwArray.add(reachedDevice);
+ }
+ }
+
+ ArrayList<Link> upstreamLinkArray =
+ upstreamLinks.get(reachedDevice);
+ if (upstreamLinkArray == null) {
+ upstreamLinkArray = new ArrayList<>();
+ upstreamLinkArray.add(copyDefaultLink(link));
+ //upstreamLinkArray.add(link);
+ upstreamLinks.put(reachedDevice, upstreamLinkArray);
+ } else {
+ // ECMP links
+ upstreamLinkArray.add(copyDefaultLink(link));
+ }
+ }
+ }
+ }
+
+ private void getDFSPaths(DeviceId dstDeviceDeviceId, Path path, ArrayList<Path> paths) {
+ DeviceId rootDeviceDeviceId = rootDevice;
+ for (Link upstreamLink : upstreamLinks.get(dstDeviceDeviceId)) {
+ /* Deep clone the path object */
+ Path sofarPath;
+ ArrayList<Link> sofarLinks = new ArrayList<>();
+ if (path != null && !path.links().isEmpty()) {
+ sofarLinks.addAll(path.links());
+ }
+ sofarLinks.add(upstreamLink);
+ sofarPath = new DefaultPath(ProviderId.NONE, sofarLinks, ScalarWeight.toWeight(0));
+ if (upstreamLink.src().deviceId().equals(rootDeviceDeviceId)) {
+ paths.add(sofarPath);
+ return;
+ } else {
+ getDFSPaths(upstreamLink.src().deviceId(), sofarPath, paths);
+ }
+ }
+ }
+
+ /**
+ * Return root Device for the graph.
+ *
+ * @return root Device
+ */
+ public DeviceId getRootDevice() {
+ return rootDevice;
+ }
+
+ /**
+ * Return the computed ECMP paths from the root Device to a given Device in
+ * the network.
+ *
+ * @param targetDevice the target Device
+ * @return the list of ECMP Paths from the root Device to the target Device
+ */
+ public ArrayList<Path> getECMPPaths(DeviceId targetDevice) {
+ ArrayList<Path> pathArray = paths.get(targetDevice);
+ if (pathArray == null && deviceSearched.containsKey(
+ targetDevice)) {
+ pathArray = new ArrayList<>();
+ DeviceId sw = targetDevice;
+ getDFSPaths(sw, null, pathArray);
+ paths.put(targetDevice, pathArray);
+ }
+ return pathArray;
+ }
+
+ /**
+ * Return the complete info of the computed ECMP paths for each Device
+ * learned in multiple iterations from the root Device.
+ *
+ * @return the hash table of Devices learned in multiple Dijkstra
+ * iterations and corresponding ECMP paths to it from the root
+ * Device
+ */
+ public HashMap<Integer, HashMap<DeviceId,
+ ArrayList<Path>>> getCompleteLearnedDeviceesAndPaths() {
+
+ HashMap<Integer, HashMap<DeviceId, ArrayList<Path>>> pathGraph = new HashMap<>();
+
+ for (Integer itrIndx : distanceDeviceMap.keySet()) {
+ HashMap<DeviceId, ArrayList<Path>> swMap = new HashMap<>();
+ for (DeviceId sw : distanceDeviceMap.get(itrIndx)) {
+ swMap.put(sw, getECMPPaths(sw));
+ }
+ pathGraph.put(itrIndx, swMap);
+ }
+
+ return pathGraph;
+ }
+
+ /**
+ * Returns the complete info of the computed ECMP paths for each target device
+ * learned in multiple iterations from the root Device. The computed info
+ * returned is per iteration (Integer key of outer HashMap). In each
+ * iteration, for the target devices reached (DeviceId key of inner HashMap),
+ * the ECMP paths are detailed (2D array).
+ *
+ * @return the hash table of target Devices learned in multiple Dijkstra
+ * iterations and corresponding ECMP paths in terms of Devices to
+ * be traversed (via) from the root Device to the target Device
+ */
+ public HashMap<Integer, HashMap<DeviceId,
+ ArrayList<ArrayList<DeviceId>>>> getAllLearnedSwitchesAndVia() {
+
+ HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> deviceViaMap = new HashMap<>();
+
+ for (Integer itrIndx : distanceDeviceMap.keySet()) {
+ HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swMap = new HashMap<>();
+
+ for (DeviceId sw : distanceDeviceMap.get(itrIndx)) {
+ ArrayList<ArrayList<DeviceId>> swViaArray = new ArrayList<>();
+ for (Path path : getECMPPaths(sw)) {
+ ArrayList<DeviceId> swVia = new ArrayList<>();
+ for (Link link : path.links()) {
+ if (link.src().deviceId().equals(rootDevice)) {
+ /* No need to add the root Device again in
+ * the Via list
+ */
+ continue;
+ }
+ swVia.add(link.src().deviceId());
+ }
+ swViaArray.add(swVia);
+ }
+ swMap.put(sw, swViaArray);
+ }
+ deviceViaMap.put(itrIndx, swMap);
+ }
+ return deviceViaMap;
+ }
+
+
+ private Link copyDefaultLink(Link link) {
+ DefaultLink src = (DefaultLink) link;
+ DefaultLink defaultLink = DefaultLink.builder()
+ .providerId(src.providerId())
+ .src(src.src())
+ .dst(src.dst())
+ .type(src.type())
+ .annotations(src.annotations())
+ .build();
+
+ return defaultLink;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sBuilder = new StringBuilder();
+ for (Device device: srManager.deviceService.getDevices()) {
+ if (!device.id().equals(rootDevice)) {
+ sBuilder.append("\r\n Paths from " + rootDevice + " to "
+ + device.id());
+ ArrayList<Path> paths = getECMPPaths(device.id());
+ if (paths != null) {
+ for (Path path : paths) {
+ sBuilder.append("\r\n == "); // equal cost paths delimiter
+ for (int i = path.links().size() - 1; i >= 0; i--) {
+ Link link = path.links().get(i);
+ sBuilder.append(" : " + link.src() + " -> " + link.dst());
+ }
+ }
+ } else {
+ sBuilder.append("\r\n == no paths");
+ }
+ }
+ }
+ return sBuilder.toString();
+ }
+}
+
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
new file mode 100644
index 0000000..908a339
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+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.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostLocationProbingService.ProbeMode;
+import org.onosproject.net.host.HostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Handles host-related events.
+ */
+public class HostHandler {
+ private static final Logger log = LoggerFactory.getLogger(HostHandler.class);
+ static final int HOST_MOVED_DELAY_MS = 1000;
+
+ protected final SegmentRoutingManager srManager;
+ private HostService hostService;
+ private FlowObjectiveService flowObjectiveService;
+
+ /**
+ * Constructs the HostHandler.
+ *
+ * @param srManager Segment Routing manager
+ */
+ HostHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ hostService = srManager.hostService;
+ flowObjectiveService = srManager.flowObjectiveService;
+ }
+
+ protected void init(DeviceId devId) {
+ hostService.getHosts().forEach(host ->
+ host.locations().stream()
+ .filter(location -> location.deviceId().equals(devId) ||
+ location.deviceId().equals(srManager.getPairDeviceId(devId).orElse(null)))
+ .forEach(location -> processHostAddedAtLocation(host, location))
+ );
+ }
+
+ void processHostAddedEvent(HostEvent event) {
+ processHostAdded(event.subject());
+ }
+
+ private void processHostAdded(Host host) {
+ host.locations().forEach(location -> processHostAddedAtLocation(host, location));
+ // ensure dual-homed host locations have viable uplinks
+ if (host.locations().size() > 1) {
+ host.locations().forEach(loc -> {
+ if (srManager.mastershipService.isLocalMaster(loc.deviceId())) {
+ srManager.linkHandler.checkUplinksForDualHomedHosts(loc);
+ }
+ });
+ }
+ }
+
+ void processHostAddedAtLocation(Host host, HostLocation location) {
+ checkArgument(host.locations().contains(location), "{} is not a location of {}", location, host);
+
+ MacAddress hostMac = host.mac();
+ VlanId hostVlanId = host.vlan();
+ Set<HostLocation> locations = host.locations();
+ Set<IpAddress> ips = host.ipAddresses();
+ log.info("Host {}/{} is added at {}", hostMac, hostVlanId, locations);
+
+ if (srManager.isMasterOf(location)) {
+ processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, false);
+ ips.forEach(ip ->
+ processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
+ );
+ }
+
+ // Use the pair link temporarily before the second location of a dual-homed host shows up.
+ // This do not affect single-homed hosts since the flow will be blocked in
+ // processBridgingRule or processRoutingRule due to VLAN or IP mismatch respectively
+ srManager.getPairDeviceId(location.deviceId()).ifPresent(pairDeviceId -> {
+ if (srManager.mastershipService.isLocalMaster(pairDeviceId) &&
+ host.locations().stream().noneMatch(l -> l.deviceId().equals(pairDeviceId))) {
+ srManager.getPairLocalPorts(pairDeviceId).ifPresent(pairRemotePort -> {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+ processBridgingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, false);
+ ips.forEach(ip -> processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId,
+ ip, false));
+
+ if (srManager.activeProbing) {
+ probe(host, location, pairDeviceId, pairRemotePort);
+ }
+ });
+ }
+ });
+ }
+
+ void processHostRemovedEvent(HostEvent event) {
+ processHostRemoved(event.subject());
+ }
+
+ private void processHostRemoved(Host host) {
+ MacAddress hostMac = host.mac();
+ VlanId hostVlanId = host.vlan();
+ Set<HostLocation> locations = host.locations();
+ Set<IpAddress> ips = host.ipAddresses();
+ log.info("Host {}/{} is removed from {}", hostMac, hostVlanId, locations);
+
+ locations.forEach(location -> {
+ if (srManager.isMasterOf(location)) {
+ processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, true);
+ ips.forEach(ip ->
+ processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
+ );
+ }
+
+ // Also remove redirection flows on the pair device if exists.
+ Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
+ Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(location.deviceId());
+ if (pairDeviceId.isPresent() && pairLocalPort.isPresent() &&
+ srManager.mastershipService.isLocalMaster(pairDeviceId.get())) {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+ processBridgingRule(pairDeviceId.get(), pairLocalPort.get(), hostMac, vlanId, true);
+ ips.forEach(ip ->
+ processRoutingRule(pairDeviceId.get(), pairLocalPort.get(), hostMac, vlanId,
+ ip, true));
+ }
+ });
+ }
+
+ void processHostMovedEvent(HostEvent event) {
+ MacAddress hostMac = event.subject().mac();
+ VlanId hostVlanId = event.subject().vlan();
+ Set<HostLocation> prevLocations = event.prevSubject().locations();
+ Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
+ Set<HostLocation> newLocations = event.subject().locations();
+ Set<IpAddress> newIps = event.subject().ipAddresses();
+
+ // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+ // The permanent solution would be introducing CompletableFuture and wait for it
+ if (prevLocations.size() == 1 && newLocations.size() == 2) {
+ log.debug("Delay event handling when host {}/{} moves from 1 to 2 locations", hostMac, hostVlanId);
+ ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+ executorService.schedule(() ->
+ processHostMoved(hostMac, hostVlanId, prevLocations, prevIps, newLocations, newIps),
+ HOST_MOVED_DELAY_MS, TimeUnit.MILLISECONDS);
+ } else {
+ processHostMoved(hostMac, hostVlanId, prevLocations, prevIps, newLocations, newIps);
+ }
+ }
+
+ private void processHostMoved(MacAddress hostMac, VlanId hostVlanId, Set<HostLocation> prevLocations,
+ Set<IpAddress> prevIps, Set<HostLocation> newLocations, Set<IpAddress> newIps) {
+ log.info("Host {}/{} is moved from {} to {}", hostMac, hostVlanId, prevLocations, newLocations);
+ Set<DeviceId> newDeviceIds = newLocations.stream().map(HostLocation::deviceId)
+ .collect(Collectors.toSet());
+
+ // For each old location
+ Sets.difference(prevLocations, newLocations).stream().filter(srManager::isMasterOf)
+ .forEach(prevLocation -> {
+ // Remove routing rules for old IPs
+ Sets.difference(prevIps, newIps).forEach(ip ->
+ processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+ ip, true)
+ );
+
+ // Redirect the flows to pair link if configured
+ // Note: Do not continue removing any rule
+ Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(prevLocation.deviceId());
+ Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(prevLocation.deviceId());
+ if (pairDeviceId.isPresent() && pairLocalPort.isPresent() && newLocations.stream()
+ .anyMatch(location -> location.deviceId().equals(pairDeviceId.get()))) {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(prevLocation)).orElse(hostVlanId);
+
+ processBridgingRule(prevLocation.deviceId(), pairLocalPort.get(), hostMac, vlanId, false);
+ newIps.forEach(ip ->
+ processRoutingRule(prevLocation.deviceId(), pairLocalPort.get(), hostMac, vlanId,
+ ip, false));
+ return;
+ }
+
+ // Remove bridging rule and routing rules for unchanged IPs if the host moves from a switch to another.
+ // Otherwise, do not remove and let the adding part update the old flow
+ if (!newDeviceIds.contains(prevLocation.deviceId())) {
+ processBridgingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId, true);
+ Sets.intersection(prevIps, newIps).forEach(ip ->
+ processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+ ip, true)
+ );
+ }
+
+ // Remove bridging rules if new interface vlan is different from old interface vlan
+ // Otherwise, do not remove and let the adding part update the old flow
+ if (newLocations.stream().noneMatch(newLocation -> {
+ VlanId oldAssignedVlan = srManager.getInternalVlanId(prevLocation);
+ VlanId newAssignedVlan = srManager.getInternalVlanId(newLocation);
+ // Host is tagged and the new location has the host vlan in vlan-tagged
+ return srManager.getTaggedVlanId(newLocation).contains(hostVlanId) ||
+ (oldAssignedVlan != null && newAssignedVlan != null &&
+ // Host is untagged and the new location has the same assigned vlan
+ oldAssignedVlan.equals(newAssignedVlan));
+ })) {
+ processBridgingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId, true);
+ }
+
+ // Remove routing rules for unchanged IPs if none of the subnet of new location contains
+ // the IP. Otherwise, do not remove and let the adding part update the old flow
+ Sets.intersection(prevIps, newIps).forEach(ip -> {
+ if (newLocations.stream().noneMatch(newLocation ->
+ srManager.deviceConfiguration.inSameSubnet(newLocation, ip))) {
+ processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+ ip, true);
+ }
+ });
+ });
+
+ // For each new location, add all new IPs.
+ Sets.difference(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+ .forEach(newLocation -> {
+ processBridgingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId, false);
+ newIps.forEach(ip ->
+ processRoutingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId,
+ ip, false)
+ );
+ });
+
+ // For each unchanged location, add new IPs and remove old IPs.
+ Sets.intersection(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+ .forEach(unchangedLocation -> {
+ Sets.difference(prevIps, newIps).forEach(ip ->
+ processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
+ hostVlanId, ip, true)
+ );
+
+ Sets.difference(newIps, prevIps).forEach(ip ->
+ processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
+ hostVlanId, ip, false)
+ );
+ });
+
+ // ensure dual-homed host locations have viable uplinks
+ if (newLocations.size() > prevLocations.size()) {
+ newLocations.forEach(loc -> {
+ if (srManager.mastershipService.isLocalMaster(loc.deviceId())) {
+ srManager.linkHandler.checkUplinksForDualHomedHosts(loc);
+ }
+ });
+ }
+ }
+
+ void processHostUpdatedEvent(HostEvent event) {
+ Host host = event.subject();
+ MacAddress hostMac = host.mac();
+ VlanId hostVlanId = host.vlan();
+ Set<HostLocation> locations = host.locations();
+ Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
+ Set<IpAddress> newIps = host.ipAddresses();
+ log.info("Host {}/{} is updated", hostMac, hostVlanId);
+
+ locations.stream().filter(srManager::isMasterOf).forEach(location -> {
+ Sets.difference(prevIps, newIps).forEach(ip ->
+ processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
+ );
+ Sets.difference(newIps, prevIps).forEach(ip ->
+ processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
+ );
+ });
+
+ // Use the pair link temporarily before the second location of a dual-homed host shows up.
+ // This do not affect single-homed hosts since the flow will be blocked in
+ // processBridgingRule or processRoutingRule due to VLAN or IP mismatch respectively
+ locations.forEach(location ->
+ srManager.getPairDeviceId(location.deviceId()).ifPresent(pairDeviceId -> {
+ if (srManager.mastershipService.isLocalMaster(pairDeviceId) &&
+ locations.stream().noneMatch(l -> l.deviceId().equals(pairDeviceId))) {
+ Set<IpAddress> ipsToAdd = Sets.difference(newIps, prevIps);
+ Set<IpAddress> ipsToRemove = Sets.difference(prevIps, newIps);
+
+ srManager.getPairLocalPorts(pairDeviceId).ifPresent(pairRemotePort -> {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+ ipsToRemove.forEach(ip ->
+ processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, ip, true)
+ );
+ ipsToAdd.forEach(ip ->
+ processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, ip, false)
+ );
+
+ if (srManager.activeProbing) {
+ probe(host, location, pairDeviceId, pairRemotePort);
+ }
+ });
+ }
+ })
+ );
+ }
+
+ /**
+ * When a non-pair port comes up, probe each host on the pair device if
+ * (1) the host is tagged and the tagged vlan of current port contains host vlan; or
+ * (2) the host is untagged and the internal vlan is the same on the host port and current port.
+ *
+ * @param cp connect point
+ */
+ void processPortUp(ConnectPoint cp) {
+ if (cp.port().equals(srManager.getPairLocalPorts(cp.deviceId()).orElse(null))) {
+ return;
+ }
+ if (srManager.activeProbing) {
+ srManager.getPairDeviceId(cp.deviceId())
+ .ifPresent(pairDeviceId -> srManager.hostService.getConnectedHosts(pairDeviceId).stream()
+ .filter(host -> isHostInVlanOfPort(host, pairDeviceId, cp))
+ .forEach(host -> srManager.probingService.probeHostLocation(host, cp, ProbeMode.DISCOVER))
+ );
+ }
+ }
+
+ /**
+ * Checks if given host located on given device id matches VLAN config of current port.
+ *
+ * @param host host to check
+ * @param deviceId device id to check
+ * @param cp current connect point
+ * @return true if the host located at deviceId matches the VLAN config on cp
+ */
+ private boolean isHostInVlanOfPort(Host host, DeviceId deviceId, ConnectPoint cp) {
+ VlanId internalVlan = srManager.getInternalVlanId(cp);
+ Set<VlanId> taggedVlan = srManager.getTaggedVlanId(cp);
+
+ return taggedVlan.contains(host.vlan()) ||
+ (internalVlan != null && host.locations().stream()
+ .filter(l -> l.deviceId().equals(deviceId))
+ .map(srManager::getInternalVlanId)
+ .anyMatch(internalVlan::equals));
+ }
+
+ /**
+ * Send a probe on all locations with the same VLAN on pair device, excluding pair port.
+ *
+ * @param host host to probe
+ * @param location newly discovered host location
+ * @param pairDeviceId pair device id
+ * @param pairRemotePort pair remote port
+ */
+ private void probe(Host host, ConnectPoint location, DeviceId pairDeviceId, PortNumber pairRemotePort) {
+ VlanId vlanToProbe = host.vlan().equals(VlanId.NONE) ?
+ srManager.getInternalVlanId(location) : host.vlan();
+ srManager.interfaceService.getInterfaces().stream()
+ .filter(i -> i.vlanTagged().contains(vlanToProbe) ||
+ i.vlanUntagged().equals(vlanToProbe) ||
+ i.vlanNative().equals(vlanToProbe))
+ .filter(i -> i.connectPoint().deviceId().equals(pairDeviceId))
+ .filter(i -> !i.connectPoint().port().equals(pairRemotePort))
+ .forEach(i -> {
+ log.debug("Probing host {} on pair device {}", host.id(), i.connectPoint());
+ srManager.probingService.probeHostLocation(host, i.connectPoint(), ProbeMode.DISCOVER);
+ });
+ }
+
+ /**
+ * Generates a forwarding objective builder for bridging rules.
+ * <p>
+ * The forwarding objective bridges packets destined to a given MAC to
+ * given port on given device.
+ *
+ * @param deviceId Device that host attaches to
+ * @param mac MAC address of the host
+ * @param hostVlanId VLAN ID of the host
+ * @param outport Port that host attaches to
+ * @param revoke true if forwarding objective is meant to revoke forwarding rule
+ * @return Forwarding objective builder
+ */
+ ForwardingObjective.Builder bridgingFwdObjBuilder(
+ DeviceId deviceId, MacAddress mac, VlanId hostVlanId,
+ PortNumber outport, boolean revoke) {
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, outport);
+ VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+ Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+ VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+ // Create host selector
+ TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+ sbuilder.matchEthDst(mac);
+
+ // Create host treatment
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.immediate().setOutput(outport);
+
+ // Create host meta
+ TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+ // Adjust the selector, treatment and meta according to VLAN configuration
+ if (taggedVlans.contains(hostVlanId)) {
+ sbuilder.matchVlanId(hostVlanId);
+ mbuilder.matchVlanId(hostVlanId);
+ } else if (hostVlanId.equals(VlanId.NONE)) {
+ if (untaggedVlan != null) {
+ sbuilder.matchVlanId(untaggedVlan);
+ mbuilder.matchVlanId(untaggedVlan);
+ tbuilder.immediate().popVlan();
+ } else if (nativeVlan != null) {
+ sbuilder.matchVlanId(nativeVlan);
+ mbuilder.matchVlanId(nativeVlan);
+ tbuilder.immediate().popVlan();
+ } else {
+ log.warn("Untagged host {}/{} is not allowed on {} without untagged or native" +
+ "vlan config", mac, hostVlanId, connectPoint);
+ return null;
+ }
+ } else {
+ log.warn("Tagged host {}/{} is not allowed on {} without VLAN listed in tagged vlan",
+ mac, hostVlanId, connectPoint);
+ return null;
+ }
+
+ // All forwarding is via Groups. Drivers can re-purpose to flow-actions if needed.
+ // If the objective is to revoke an existing rule, and for some reason
+ // the next-objective does not exist, then a new one should not be created
+ int portNextObjId = srManager.getPortNextObjectiveId(deviceId, outport,
+ tbuilder.build(), mbuilder.build(), !revoke);
+ if (portNextObjId == -1) {
+ // Warning log will come from getPortNextObjective method
+ return null;
+ }
+
+ return DefaultForwardingObjective.builder()
+ .withFlag(ForwardingObjective.Flag.SPECIFIC)
+ .withSelector(sbuilder.build())
+ .nextStep(portNextObjId)
+ .withPriority(100)
+ .fromApp(srManager.appId)
+ .makePermanent();
+ }
+
+ /**
+ * Populate or revoke a bridging rule on given deviceId that matches given mac, given vlan and
+ * output to given port.
+ *
+ * @param deviceId device ID
+ * @param port port
+ * @param mac mac address
+ * @param vlanId VLAN ID
+ * @param revoke true to revoke the rule; false to populate
+ */
+ private void processBridgingRule(DeviceId deviceId, PortNumber port, MacAddress mac,
+ VlanId vlanId, boolean revoke) {
+ log.debug("{} bridging entry for host {}/{} at {}:{}", revoke ? "Revoking" : "Populating",
+ mac, vlanId, deviceId, port);
+
+ ForwardingObjective.Builder fob = bridgingFwdObjBuilder(deviceId, mac, vlanId, port, revoke);
+ if (fob == null) {
+ log.warn("Fail to build fwd obj for host {}/{}. Abort.", mac, vlanId);
+ return;
+ }
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Brigding rule for {}/{} {}", mac, vlanId,
+ revoke ? "revoked" : "populated"),
+ (objective, error) -> log.warn("Failed to {} bridging rule for {}/{}: {}",
+ revoke ? "revoked" : "populated", mac, vlanId, error));
+ flowObjectiveService.forward(deviceId, revoke ? fob.remove(context) : fob.add(context));
+ }
+
+ /**
+ * Populate or revoke a routing rule on given deviceId that matches given ip,
+ * set destination mac to given mac, set vlan to given vlan and output to given port.
+ *
+ * @param deviceId device ID
+ * @param port port
+ * @param mac mac address
+ * @param vlanId VLAN ID
+ * @param ip IP address
+ * @param revoke true to revoke the rule; false to populate
+ */
+ private void processRoutingRule(DeviceId deviceId, PortNumber port, MacAddress mac,
+ VlanId vlanId, IpAddress ip, boolean revoke) {
+ ConnectPoint location = new ConnectPoint(deviceId, port);
+ if (!srManager.deviceConfiguration.inSameSubnet(location, ip)) {
+ log.info("{} is not included in the subnet config of {}/{}. Ignored.", ip, deviceId, port);
+ return;
+ }
+
+ log.info("{} routing rule for {} at {}", revoke ? "Revoking" : "Populating", ip, location);
+ if (revoke) {
+ srManager.defaultRoutingHandler.revokeRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
+ } else {
+ srManager.defaultRoutingHandler.populateRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
+ }
+ }
+
+ /**
+ * Populate or revoke a bridging rule on given deviceId that matches given vlanId,
+ * and hostMAC connected to given port, and output to given port only when
+ * vlan information is valid.
+ *
+ * @param deviceId device ID that host attaches to
+ * @param portNum port number that host attaches to
+ * @param hostMac mac address of the host connected to the switch port
+ * @param vlanId Vlan ID configured on the switch port
+ * @param popVlan true to pop Vlan tag at TrafficTreatment, false otherwise
+ * @param install true to populate the objective, false to revoke
+ */
+ private void updateBridgingRule(DeviceId deviceId, PortNumber portNum, MacAddress hostMac,
+ VlanId vlanId, boolean popVlan, boolean install) {
+ // Create host selector
+ TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+ sbuilder.matchEthDst(hostMac);
+
+ // Create host meta
+ TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+ sbuilder.matchVlanId(vlanId);
+ mbuilder.matchVlanId(vlanId);
+
+ // Create host treatment
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.immediate().setOutput(portNum);
+
+ if (popVlan) {
+ tbuilder.immediate().popVlan();
+ }
+
+ int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNum,
+ tbuilder.build(), mbuilder.build(), install);
+ if (portNextObjId != -1) {
+ ForwardingObjective.Builder fob = DefaultForwardingObjective.builder()
+ .withFlag(ForwardingObjective.Flag.SPECIFIC)
+ .withSelector(sbuilder.build())
+ .nextStep(portNextObjId)
+ .withPriority(100)
+ .fromApp(srManager.appId)
+ .makePermanent();
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Brigding rule for {}/{} {}", hostMac, vlanId,
+ install ? "populated" : "revoked"),
+ (objective, error) -> log.warn("Failed to {} bridging rule for {}/{}: {}",
+ install ? "populate" : "revoke", hostMac, vlanId, error));
+ flowObjectiveService.forward(deviceId, install ? fob.add(context) : fob.remove(context));
+ } else {
+ log.warn("Failed to retrieve next objective for {}/{}", hostMac, vlanId);
+ }
+ }
+
+ /**
+ * Update forwarding objective for unicast bridging and unicast routing.
+ * Also check the validity of updated interface configuration on VLAN.
+ *
+ * @param deviceId device ID that host attaches to
+ * @param portNum port number that host attaches to
+ * @param vlanId Vlan ID configured on the switch port
+ * @param popVlan true to pop Vlan tag at TrafficTreatment, false otherwise
+ * @param install true to populate the objective, false to revoke
+ */
+ void processIntfVlanUpdatedEvent(DeviceId deviceId, PortNumber portNum, VlanId vlanId,
+ boolean popVlan, boolean install) {
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, portNum);
+ Set<Host> hosts = hostService.getConnectedHosts(connectPoint);
+
+ if (hosts == null || hosts.size() == 0) {
+ return;
+ }
+
+ hosts.forEach(host -> {
+ MacAddress mac = host.mac();
+ VlanId hostVlanId = host.vlan();
+
+ // Check whether the host vlan is valid for new interface configuration
+ if ((!popVlan && hostVlanId.equals(vlanId)) ||
+ (popVlan && hostVlanId.equals(VlanId.NONE))) {
+ updateBridgingRule(deviceId, portNum, mac, vlanId, popVlan, install);
+ // Update Forwarding objective and corresponding simple Next objective
+ // for each host and IP address connected to given port
+ host.ipAddresses().forEach(ipAddress ->
+ srManager.routingRulePopulator.updateFwdObj(deviceId, portNum, ipAddress.toIpPrefix(),
+ mac, vlanId, popVlan, install)
+ );
+ }
+ });
+ }
+
+ /**
+ * Populate or revoke routing rule for each host, according to the updated
+ * subnet configuration on the interface.
+ * @param cp connect point of the updated interface
+ * @param ipPrefixSet IP Prefixes added or removed
+ * @param install true if IP Prefixes added, false otherwise
+ */
+ void processIntfIpUpdatedEvent(ConnectPoint cp, Set<IpPrefix> ipPrefixSet, boolean install) {
+ Set<Host> hosts = hostService.getConnectedHosts(cp);
+
+ if (hosts == null || hosts.size() == 0) {
+ log.warn("processIntfIpUpdatedEvent: No hosts connected to {}", cp);
+ return;
+ }
+
+ // Check whether the host IP address is in the interface's subnet
+ hosts.forEach(host ->
+ host.ipAddresses().forEach(hostIpAddress -> {
+ ipPrefixSet.forEach(ipPrefix -> {
+ if (install && ipPrefix.contains(hostIpAddress)) {
+ srManager.routingRulePopulator.populateRoute(cp.deviceId(), hostIpAddress.toIpPrefix(),
+ host.mac(), host.vlan(), cp.port());
+ } else if (!install && ipPrefix.contains(hostIpAddress)) {
+ srManager.routingRulePopulator.revokeRoute(cp.deviceId(), hostIpAddress.toIpPrefix(),
+ host.mac(), host.vlan(), cp.port());
+ }
+ });
+ }));
+ }
+
+ /**
+ * Returns the set of portnumbers on the given device that are part of the
+ * locations for dual-homed hosts.
+ *
+ * @param deviceId the given deviceId
+ * @return set of port numbers on given device that are dual-homed host
+ * locations. May be empty if no dual homed hosts are connected to
+ * the given device
+ */
+ Set<PortNumber> getDualHomedHostPorts(DeviceId deviceId) {
+ Set<PortNumber> dualHomedLocations = new HashSet<>();
+ srManager.hostService.getConnectedHosts(deviceId).stream()
+ .filter(host -> host.locations().size() == 2)
+ .forEach(host -> host.locations().stream()
+ .filter(loc -> loc.deviceId().equals(deviceId))
+ .forEach(loc -> dualHomedLocations.add(loc.port())));
+ return dualHomedLocations;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
new file mode 100644
index 0000000..8228bb5
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP;
+import org.onlab.packet.ICMP6;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MPLS;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.packet.ndp.NeighborSolicitation;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.neighbour.NeighbourMessageType;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Handler of ICMP packets that responses or forwards ICMP packets that
+ * are sent to the controller.
+ */
+public class IcmpHandler extends SegmentRoutingNeighbourHandler {
+
+ private static Logger log = LoggerFactory.getLogger(IcmpHandler.class);
+
+ /**
+ * Creates an IcmpHandler object.
+ *
+ * @param srManager SegmentRoutingManager object
+ */
+ public IcmpHandler(SegmentRoutingManager srManager) {
+ super(srManager);
+ }
+
+ /**
+ * Utility function to send packet out.
+ *
+ * @param outport the output port
+ * @param payload the packet to send
+ * @param sid the segment id
+ * @param destIpAddress the destination ip address
+ * @param allowedHops the hop limit/ttl
+ */
+ private void sendPacketOut(ConnectPoint outport,
+ Ethernet payload,
+ int sid,
+ IpAddress destIpAddress,
+ byte allowedHops) {
+ int destSid;
+ if (destIpAddress.isIp4()) {
+ destSid = config.getIPv4SegmentId(payload.getDestinationMAC());
+ } else {
+ destSid = config.getIPv6SegmentId(payload.getDestinationMAC());
+ }
+
+ if (sid == -1 || destSid == sid ||
+ config.inSameSubnet(outport.deviceId(), destIpAddress)) {
+ TrafficTreatment treatment = DefaultTrafficTreatment.builder().
+ setOutput(outport.port()).build();
+ OutboundPacket packet = new DefaultOutboundPacket(outport.deviceId(),
+ treatment, ByteBuffer.wrap(payload.serialize()));
+ srManager.packetService.emit(packet);
+ } else {
+ log.trace("Send a MPLS packet as a ICMP response");
+ TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+ .setOutput(outport.port())
+ .build();
+
+ payload.setEtherType(Ethernet.MPLS_UNICAST);
+ MPLS mplsPkt = new MPLS();
+ mplsPkt.setLabel(sid);
+ mplsPkt.setTtl(allowedHops);
+ mplsPkt.setPayload(payload.getPayload());
+ payload.setPayload(mplsPkt);
+
+ OutboundPacket packet = new DefaultOutboundPacket(outport.deviceId(),
+ treatment, ByteBuffer.wrap(payload.serialize()));
+
+ srManager.packetService.emit(packet);
+ }
+ }
+
+ //////////////////////////////////////
+ // ICMP Echo/Reply Protocol //
+ //////////////////////////////////////
+
+ /**
+ * Process incoming ICMP packet.
+ * If it is an ICMP request to router, then sends an ICMP response.
+ * Otherwise ignore the packet.
+ *
+ * @param eth inbound ICMP packet
+ * @param inPort the input port
+ */
+ public void processIcmp(Ethernet eth, ConnectPoint inPort) {
+ DeviceId deviceId = inPort.deviceId();
+ IPv4 ipv4Packet = (IPv4) eth.getPayload();
+ Ip4Address destinationAddress = Ip4Address.valueOf(ipv4Packet.getDestinationAddress());
+ Set<IpAddress> gatewayIpAddresses = config.getPortIPs(deviceId);
+ IpAddress routerIp;
+ try {
+ routerIp = config.getRouterIpv4(deviceId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting processPacketIn.");
+ return;
+ }
+ // ICMP to the router IP or gateway IP
+ if (((ICMP) ipv4Packet.getPayload()).getIcmpType() == ICMP.TYPE_ECHO_REQUEST &&
+ (destinationAddress.equals(routerIp.getIp4Address()) ||
+ gatewayIpAddresses.contains(destinationAddress))) {
+ sendIcmpResponse(eth, inPort);
+ } else {
+ log.trace("Ignore ICMP that targets for {}", destinationAddress);
+ }
+ // We remove the packet from the queue
+ srManager.ipHandler.dequeuePacket(ipv4Packet, destinationAddress);
+ }
+
+ /**
+ * Sends an ICMP reply message.
+ *
+ * @param icmpRequest the original ICMP request
+ * @param outport the output port where the ICMP reply should be sent to
+ */
+ private void sendIcmpResponse(Ethernet icmpRequest, ConnectPoint outport) {
+ Ethernet icmpReplyEth = ICMP.buildIcmpReply(icmpRequest);
+ IPv4 icmpRequestIpv4 = (IPv4) icmpRequest.getPayload();
+ IPv4 icmpReplyIpv4 = (IPv4) icmpReplyEth.getPayload();
+ Ip4Address destIpAddress = Ip4Address.valueOf(icmpRequestIpv4.getSourceAddress());
+ Ip4Address destRouterAddress = config.getRouterIpAddressForASubnetHost(destIpAddress);
+
+ // Note: Source IP of the ICMP request doesn't belong to any configured subnet.
+ // The source might be an indirectly attached host (e.g. behind a router)
+ // Lookup the route store for the nexthop instead.
+ if (destRouterAddress == null) {
+ Optional<DeviceId> deviceId = srManager.routeService
+ .longestPrefixLookup(destIpAddress).map(srManager::nextHopLocations)
+ .flatMap(locations -> locations.stream().findFirst())
+ .map(ConnectPoint::deviceId);
+ if (deviceId.isPresent()) {
+ try {
+ destRouterAddress = config.getRouterIpv4(deviceId.get());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Device config for {} not found. Abort ICMP processing", deviceId);
+ return;
+ }
+ }
+ }
+
+ int destSid = config.getIPv4SegmentId(destRouterAddress);
+ if (destSid < 0) {
+ log.warn("Failed to lookup SID of the switch that {} attaches to. " +
+ "Unable to process ICMP request.", destIpAddress);
+ return;
+ }
+ sendPacketOut(outport, icmpReplyEth, destSid, destIpAddress, icmpReplyIpv4.getTtl());
+ }
+
+ ///////////////////////////////////////////
+ // ICMPv6 Echo/Reply Protocol //
+ ///////////////////////////////////////////
+
+ /**
+ * Process incoming ICMPv6 packet.
+ * If it is an ICMPv6 request to router, then sends an ICMPv6 response.
+ * Otherwise ignore the packet.
+ *
+ * @param eth the incoming ICMPv6 packet
+ * @param inPort the input port
+ */
+ public void processIcmpv6(Ethernet eth, ConnectPoint inPort) {
+ DeviceId deviceId = inPort.deviceId();
+ IPv6 ipv6Packet = (IPv6) eth.getPayload();
+ ICMP6 icmp6 = (ICMP6) ipv6Packet.getPayload();
+ Ip6Address destinationAddress = Ip6Address.valueOf(ipv6Packet.getDestinationAddress());
+ Set<IpAddress> gatewayIpAddresses = config.getPortIPs(deviceId);
+ IpAddress routerIp;
+
+ // Only proceed with echo request
+ if (icmp6.getIcmpType() != ICMP6.ECHO_REQUEST) {
+ return;
+ }
+
+ try {
+ routerIp = config.getRouterIpv6(deviceId);
+
+ Optional<Ip6Address> linkLocalIp = srManager.interfaceService.getInterfacesByPort(inPort)
+ .stream()
+ .map(Interface::mac)
+ .map(MacAddress::toBytes)
+ .map(IPv6::getLinkLocalAddress)
+ .map(Ip6Address::valueOf)
+ .findFirst();
+
+ // Ensure ICMP to the router IP, EUI-64 link-local IP, or gateway IP
+ if (destinationAddress.equals(routerIp.getIp6Address()) ||
+ (linkLocalIp.isPresent() && destinationAddress.equals(linkLocalIp.get())) ||
+ gatewayIpAddresses.contains(destinationAddress)) {
+ sendIcmpv6Response(eth, inPort);
+ } else {
+ log.trace("Ignore ICMPv6 that targets for {}", destinationAddress);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Ignore ICMPv6 that targets to {}.", destinationAddress);
+ }
+ }
+
+ /**
+ * Sends an ICMPv6 reply message.
+ *
+ * Note: we assume that packets sending from the edge switches to the hosts
+ * have untagged VLAN.
+ * @param ethRequest the original ICMP request
+ * @param outport the output port where the ICMP reply should be sent to
+ */
+ private void sendIcmpv6Response(Ethernet ethRequest, ConnectPoint outport) {
+ // Note: We assume that packets arrive at the edge switches have untagged VLAN.
+ Ethernet ethReply = ICMP6.buildIcmp6Reply(ethRequest);
+ IPv6 icmpRequestIpv6 = (IPv6) ethRequest.getPayload();
+ IPv6 icmpReplyIpv6 = (IPv6) ethRequest.getPayload();
+ Ip6Address destIpAddress = Ip6Address.valueOf(icmpRequestIpv6.getSourceAddress());
+ Ip6Address destRouterAddress = config.getRouterIpAddressForASubnetHost(destIpAddress);
+
+ // Note: Source IP of the ICMP request doesn't belong to any configured subnet.
+ // The source might be an indirect host behind a router.
+ // Lookup the route store for the nexthop instead.
+ if (destRouterAddress == null) {
+ Optional<DeviceId> deviceId = srManager.routeService
+ .longestPrefixLookup(destIpAddress).map(srManager::nextHopLocations)
+ .flatMap(locations -> locations.stream().findFirst())
+ .map(ConnectPoint::deviceId);
+ if (deviceId.isPresent()) {
+ try {
+ destRouterAddress = config.getRouterIpv6(deviceId.get());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Device config for {} not found. Abort ICMPv6 processing", deviceId);
+ return;
+ }
+ }
+ }
+
+ // Search SID only if store lookup is success otherwise proceed with "sid=-1"
+ int sid = -1;
+ if (destRouterAddress != null) {
+ sid = config.getIPv6SegmentId(destRouterAddress);
+ if (sid < 0) {
+ log.warn("Failed to lookup SID of the switch that {} attaches to. " +
+ "Unable to process ICMPv6 request.", destIpAddress);
+ return;
+ }
+ }
+ sendPacketOut(outport, ethReply, sid, destIpAddress, icmpReplyIpv6.getHopLimit());
+ }
+
+ ///////////////////////////////////////////
+ // ICMPv6 Neighbour Discovery Protocol //
+ ///////////////////////////////////////////
+
+ /**
+ * Process incoming NDP packet.
+ *
+ * If it is an NDP request for the router or for the gateway, then sends a NDP reply.
+ * If it is an NDP request to unknown host flood in the subnet.
+ * If it is an NDP packet to known host forward the packet to the host.
+ *
+ * FIXME If the NDP packets use link local addresses we fail.
+ *
+ * @param pkt inbound packet
+ * @param hostService the host service
+ */
+ public void processPacketIn(NeighbourMessageContext pkt, HostService hostService) {
+ // First we validate the ndp packet
+ SegmentRoutingAppConfig appConfig = srManager.cfgService
+ .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+ if (appConfig != null && appConfig.suppressSubnet().contains(pkt.inPort())) {
+ // Ignore NDP packets come from suppressed ports
+ pkt.drop();
+ return;
+ }
+
+ if (pkt.type() == NeighbourMessageType.REQUEST) {
+ handleNdpRequest(pkt, hostService);
+ } else {
+ handleNdpReply(pkt, hostService);
+ }
+
+ }
+
+ /**
+ * Helper method to handle the ndp requests.
+ * @param pkt the ndp packet request and context information
+ * @param hostService the host service
+ */
+ private void handleNdpRequest(NeighbourMessageContext pkt, HostService hostService) {
+ // ND request for the gateway. We have to reply on behalf of the gateway.
+ if (isNdpForGateway(pkt)) {
+ log.trace("Sending NDP reply on behalf of gateway IP for pkt: {}", pkt.target());
+ MacAddress routerMac = config.getRouterMacForAGatewayIp(pkt.target());
+ sendResponse(pkt, routerMac, hostService);
+ } else {
+
+ // Process NDP targets towards EUI-64 address.
+ try {
+ DeviceId deviceId = pkt.inPort().deviceId();
+
+ Optional<Ip6Address> linkLocalIp = srManager.interfaceService.getInterfacesByPort(pkt.inPort())
+ .stream()
+ .map(Interface::mac)
+ .map(MacAddress::toBytes)
+ .map(IPv6::getLinkLocalAddress)
+ .map(Ip6Address::valueOf)
+ .findFirst();
+
+ if (linkLocalIp.isPresent() && pkt.target().equals(linkLocalIp.get())) {
+ MacAddress routerMac = config.getDeviceMac(deviceId);
+ sendResponse(pkt, routerMac, hostService);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Unable to handle NDP packet to {}. Aborting.", pkt.target());
+ return;
+ }
+
+ // NOTE: Ignore NDP packets except those target for the router
+ // We will reconsider enabling this when we have host learning support
+ /*
+ // ND request for an host. We do a search by Ip.
+ Set<Host> hosts = hostService.getHostsByIp(pkt.target());
+ // Possible misconfiguration ? In future this case
+ // should be handled we can have same hosts in different VLANs.
+ if (hosts.size() > 1) {
+ log.warn("More than one host with IP {}", pkt.target());
+ }
+ Host targetHost = hosts.stream().findFirst().orElse(null);
+ // If we know the host forward to its attachment point.
+ if (targetHost != null) {
+ log.debug("Forward NDP request to the target host");
+ pkt.forward(targetHost.location());
+ } else {
+ // Flood otherwise.
+ log.debug("Flood NDP request to the target subnet");
+ flood(pkt);
+ }
+ */
+ }
+ }
+
+ /**
+ * Helper method to handle the ndp replies.
+ *
+ * @param pkt the ndp packet reply and context information
+ * @param hostService the host service
+ */
+ private void handleNdpReply(NeighbourMessageContext pkt, HostService hostService) {
+ if (isNdpForGateway(pkt)) {
+ log.debug("Forwarding all the ip packets we stored");
+ Ip6Address hostIpAddress = pkt.sender().getIp6Address();
+ srManager.ipHandler.forwardPackets(pkt.inPort().deviceId(), hostIpAddress);
+ } else {
+ // NOTE: Ignore NDP packets except those target for the router
+ // We will reconsider enabling this when we have host learning support
+ /*
+ HostId hostId = HostId.hostId(pkt.dstMac(), pkt.vlan());
+ Host targetHost = hostService.getHost(hostId);
+ if (targetHost != null) {
+ log.debug("Forwarding the reply to the host");
+ pkt.forward(targetHost.location());
+ } else {
+ // We don't have to flood towards spine facing ports.
+ if (pkt.vlan().equals(SegmentRoutingManager.INTERNAL_VLAN)) {
+ return;
+ }
+ log.debug("Flooding the reply to the subnet");
+ flood(pkt);
+ }
+ */
+ }
+ }
+
+ /**
+ * Utility to verify if the ND are for the gateway.
+ *
+ * @param pkt the ndp packet
+ * @return true if the ndp is for the gateway. False otherwise
+ */
+ private boolean isNdpForGateway(NeighbourMessageContext pkt) {
+ DeviceId deviceId = pkt.inPort().deviceId();
+ Set<IpAddress> gatewayIpAddresses = null;
+
+ try {
+ if (pkt.target().equals(config.getRouterIpv6(deviceId))) {
+ return true;
+ }
+ gatewayIpAddresses = config.getPortIPs(deviceId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting check for router IP in processing ndp");
+ return false;
+ }
+ return gatewayIpAddresses != null && gatewayIpAddresses.stream()
+ .filter(IpAddress::isIp6)
+ .anyMatch(gatewayIp -> gatewayIp.equals(pkt.target()) ||
+ Arrays.equals(IPv6.getSolicitNodeAddress(gatewayIp.toOctets()),
+ pkt.target().toOctets()));
+ }
+
+ /**
+ * Sends a NDP request for the target IP address to all ports except in-port.
+ *
+ * @param deviceId Switch device ID
+ * @param targetAddress target IP address for ARP
+ * @param inPort in-port
+ */
+ public void sendNdpRequest(DeviceId deviceId, IpAddress targetAddress, ConnectPoint inPort) {
+ byte[] senderMacAddress = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+ byte[] senderIpAddress = new byte[Ip6Address.BYTE_LENGTH];
+ // Retrieves device info.
+ if (!getSenderInfo(senderMacAddress, senderIpAddress, deviceId, targetAddress)) {
+ log.warn("Aborting sendNdpRequest, we cannot get all the information needed");
+ return;
+ }
+ // We have to compute the dst mac address and dst ip address.
+ byte[] dstIp = IPv6.getSolicitNodeAddress(targetAddress.toOctets());
+ byte[] dstMac = IPv6.getMCastMacAddress(dstIp);
+ // Creates the request.
+ Ethernet ndpRequest = NeighborSolicitation.buildNdpSolicit(
+ targetAddress.toOctets(),
+ senderIpAddress,
+ dstIp,
+ senderMacAddress,
+ dstMac,
+ VlanId.NONE
+ );
+ flood(ndpRequest, inPort, targetAddress);
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java
new file mode 100644
index 0000000..c445ac4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IP;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.IpAddress.Version.INET6;
+
+/**
+ * Handler of IP packets that forwards IP packets that are sent to the controller,
+ * except the ICMP packets which are processed by @link{IcmpHandler}.
+ */
+public class IpHandler {
+
+ private static Logger log = LoggerFactory.getLogger(IpHandler.class);
+ private SegmentRoutingManager srManager;
+ private DeviceConfiguration config;
+ private ConcurrentHashMap<IpAddress, ConcurrentLinkedQueue<IP>> ipPacketQueue;
+
+ /**
+ * Creates an IpHandler object.
+ *
+ * @param srManager SegmentRoutingManager object
+ */
+ public IpHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.config = checkNotNull(srManager.deviceConfiguration);
+ ipPacketQueue = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Enqueues the packet using the destination address as key.
+ *
+ * @param ipPacket the ip packet to store
+ * @param destinationAddress the destination address
+ */
+ private void enqueuePacket(IP ipPacket, IpAddress destinationAddress) {
+
+ ipPacketQueue
+ .computeIfAbsent(destinationAddress, a -> new ConcurrentLinkedQueue<>())
+ .add(ipPacket);
+
+ }
+
+ /**
+ * Dequeues the packet using the destination address as key.
+ *
+ * @param ipPacket the ip packet to remove
+ * @param destinationAddress the destination address
+ */
+ public void dequeuePacket(IP ipPacket, IpAddress destinationAddress) {
+
+ if (ipPacketQueue.get(destinationAddress) == null) {
+ return;
+ }
+ ipPacketQueue.get(destinationAddress).remove(ipPacket);
+ }
+
+ /**
+ * Forwards the packet to a given host and deque the packet.
+ *
+ * @param deviceId the target device
+ * @param eth the packet to send
+ * @param dest the target host
+ */
+ private void forwardToHost(DeviceId deviceId, Ethernet eth, Host dest) {
+ TrafficTreatment treatment = DefaultTrafficTreatment.builder().
+ setOutput(dest.location().port()).build();
+ OutboundPacket packet = new DefaultOutboundPacket(deviceId,
+ treatment, ByteBuffer.wrap(eth.serialize()));
+ srManager.packetService.emit(packet);
+ }
+
+ //////////////////////
+ // IPv4 Handling //
+ ////////////////////
+
+ /**
+ * Processes incoming IP packets.
+ *
+ * If it is an IP packet for known host, then forward it to the host.
+ * If it is an IP packet for unknown host in subnet, then send an ARP request
+ * to the subnet.
+ *
+ * @param pkt incoming packet
+ * @param connectPoint the target device
+ */
+ public void processPacketIn(IPv4 pkt, ConnectPoint connectPoint) {
+
+ DeviceId deviceId = connectPoint.deviceId();
+ Ip4Address destinationAddress = Ip4Address.valueOf(pkt.getDestinationAddress());
+
+ // IP packet for know hosts
+ if (!srManager.hostService.getHostsByIp(destinationAddress).isEmpty()) {
+ forwardPackets(deviceId, destinationAddress);
+
+ // IP packet for unknown host in one of the configured subnets of the router
+ } else if (config.inSameSubnet(deviceId, destinationAddress)) {
+ srManager.arpHandler.sendArpRequest(deviceId, destinationAddress, connectPoint);
+
+ // IP packets for unknown host
+ } else {
+ log.debug("IPv4 packet for unknown host {} which is not in the subnet",
+ destinationAddress);
+ // Do nothing
+ }
+ }
+
+ /**
+ * Adds the IP packet to a buffer.
+ * The packets are forwarded to corresponding destination when the destination
+ * MAC address is known via ARP response.
+ *
+ * @param ipPacket IP packet to add to the buffer
+ */
+ public void addToPacketBuffer(IPv4 ipPacket) {
+
+ // Better not buffer TCP packets due to out-of-order packet transfer
+ if (ipPacket.getProtocol() == IPv4.PROTOCOL_TCP) {
+ return;
+ }
+ IpAddress destIpAddress = IpAddress.valueOf(ipPacket.getDestinationAddress());
+ enqueuePacket(ipPacket, destIpAddress);
+ }
+
+ /**
+ * Forwards IP packets in the buffer to the destination IP address.
+ * It is called when the controller finds the destination MAC address
+ * via ARP responses.
+ *
+ * @param deviceId switch device ID
+ * @param destIpAddress destination IP address
+ */
+ public void forwardPackets(DeviceId deviceId, Ip4Address destIpAddress) {
+ if (ipPacketQueue.get(destIpAddress) == null) {
+ return;
+ }
+ for (IP ipPacket : ipPacketQueue.get(destIpAddress)) {
+ if (ipPacket.getVersion() == ((byte) 4)) {
+ IPv4 ipv4Packet = (IPv4) ipPacket;
+ Ip4Address destAddress = Ip4Address.valueOf(ipv4Packet.getDestinationAddress());
+ if (config.inSameSubnet(deviceId, destAddress)) {
+ ipv4Packet.setTtl((byte) (ipv4Packet.getTtl() - 1));
+ ipv4Packet.setChecksum((short) 0);
+ for (Host dest : srManager.hostService.getHostsByIp(destIpAddress)) {
+ Ethernet eth = new Ethernet();
+ eth.setDestinationMACAddress(dest.mac());
+ try {
+ eth.setSourceMACAddress(config.getDeviceMac(deviceId));
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Skipping forwardPackets for this destination.");
+ continue;
+ }
+ eth.setEtherType(Ethernet.TYPE_IPV4);
+ eth.setPayload(ipv4Packet);
+ forwardToHost(deviceId, eth, dest);
+ ipPacketQueue.get(destIpAddress).remove(ipPacket);
+ }
+ ipPacketQueue.get(destIpAddress).remove(ipPacket);
+ }
+ }
+ }
+ }
+
+ //////////////////////
+ // IPv6 Handling //
+ ////////////////////
+
+ /**
+ * Processes incoming IPv6 packets.
+ *
+ * If it is an IPv6 packet for known host, then forward it to the host.
+ * If it is an IPv6 packet for unknown host in subnet, then send an NDP request
+ * to the subnet.
+ *
+ * @param pkt incoming packet
+ * @param connectPoint the target device
+ */
+ public void processPacketIn(IPv6 pkt, ConnectPoint connectPoint) {
+
+ DeviceId deviceId = connectPoint.deviceId();
+ Ip6Address destinationAddress = Ip6Address.valueOf(pkt.getDestinationAddress());
+
+ // IPv6 packet for know hosts
+ if (!srManager.hostService.getHostsByIp(destinationAddress).isEmpty()) {
+ forwardPackets(deviceId, destinationAddress);
+
+ // IPv6 packet for unknown host in one of the configured subnets of the router
+ } else if (config.inSameSubnet(deviceId, destinationAddress)) {
+ srManager.icmpHandler.sendNdpRequest(deviceId, destinationAddress, connectPoint);
+
+ // IPv6 packets for unknown host
+ } else {
+ log.debug("IPv6 packet for unknown host {} which is not in the subnet",
+ destinationAddress);
+ }
+ }
+
+ /**
+ * Adds the IPv6 packet to a buffer.
+ * The packets are forwarded to corresponding destination when the destination
+ * MAC address is known via NDP response.
+ *
+ * @param ipPacket IP packet to add to the buffer
+ */
+ public void addToPacketBuffer(IPv6 ipPacket) {
+
+ // Better not buffer TCP packets due to out-of-order packet transfer
+ if (ipPacket.getNextHeader() == IPv6.PROTOCOL_TCP) {
+ return;
+ }
+ IpAddress destIpAddress = IpAddress.valueOf(INET6, ipPacket.getDestinationAddress());
+ enqueuePacket(ipPacket, destIpAddress);
+ }
+
+ /**
+ * Forwards IP packets in the buffer to the destination IP address.
+ * It is called when the controller finds the destination MAC address
+ * via NDP replies.
+ *
+ * @param deviceId the target device
+ * @param destIpAddress the destination ip address
+ */
+ public void forwardPackets(DeviceId deviceId, Ip6Address destIpAddress) {
+ if (ipPacketQueue.get(destIpAddress) == null) {
+ return;
+ }
+ for (IP ipPacket : ipPacketQueue.get(destIpAddress)) {
+ if (ipPacket.getVersion() == ((byte) 6)) {
+ IPv6 ipv6Packet = (IPv6) ipPacket;
+ Ip6Address destAddress = Ip6Address.valueOf(ipv6Packet.getDestinationAddress());
+ if (config.inSameSubnet(deviceId, destAddress)) {
+ ipv6Packet.setHopLimit((byte) (ipv6Packet.getHopLimit() - 1));
+ for (Host dest : srManager.hostService.getHostsByIp(destIpAddress)) {
+ Ethernet eth = new Ethernet();
+ eth.setDestinationMACAddress(dest.mac());
+ try {
+ eth.setSourceMACAddress(config.getDeviceMac(deviceId));
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Skipping forwardPackets for this destination.");
+ continue;
+ }
+ eth.setEtherType(Ethernet.TYPE_IPV6);
+ eth.setPayload(ipv6Packet);
+ forwardToHost(deviceId, eth, dest);
+ ipPacketQueue.get(destIpAddress).remove(ipPacket);
+ }
+ ipPacketQueue.get(destIpAddress).remove(ipPacket);
+ }
+ }
+ ipPacketQueue.get(destIpAddress).remove(ipPacket);
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java
new file mode 100644
index 0000000..c643991
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java
@@ -0,0 +1,586 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapBuilder;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
+public class LinkHandler {
+ private static final Logger log = LoggerFactory.getLogger(LinkHandler.class);
+ protected final SegmentRoutingManager srManager;
+ protected LinkService linkService;
+
+ // Local store for all links seen and their present status, used for
+ // optimized routing. The existence of the link in the keys is enough to know
+ // if the link has been "seen-before" by this instance of the controller.
+ // The boolean value indicates if the link is currently up or not.
+ // Currently the optimized routing logic depends on "forgetting" a link
+ // when a switch goes down, but "remembering" it when only the link goes down.
+ private Map<Link, Boolean> seenLinks = new ConcurrentHashMap<>();
+
+ private EventuallyConsistentMap<DeviceId, Set<PortNumber>> downedPortStore = null;
+
+ /**
+ * Constructs the LinkHandler.
+ *
+ * @param srManager Segment Routing manager
+ */
+ LinkHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ linkService = srManager.linkService;
+ log.debug("Creating EC map downedportstore");
+ EventuallyConsistentMapBuilder<DeviceId, Set<PortNumber>> downedPortsMapBuilder
+ = srManager.storageService.eventuallyConsistentMapBuilder();
+ downedPortStore = downedPortsMapBuilder.withName("downedportstore")
+ .withSerializer(srManager.createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+ log.trace("Current size {}", downedPortStore.size());
+ init();
+ }
+
+ /**
+ * Constructs the LinkHandler for unit-testing.
+ *
+ * @param srManager SegmentRoutingManager
+ * @param linkService LinkService
+ */
+ LinkHandler(SegmentRoutingManager srManager, LinkService linkService) {
+ this.srManager = srManager;
+ this.linkService = linkService;
+ }
+
+ /**
+ * Initialize LinkHandler.
+ */
+ private void init() {
+ log.info("Loading stored links");
+ srManager.linkService.getActiveLinks()
+ .forEach(link -> processLinkAdded(link));
+ }
+
+ /**
+ * Preprocessing of added link before being sent for route-path handling.
+ * Also performs post processing of link.
+ *
+ * @param link the link to be processed
+ */
+ void processLinkAdded(Link link) {
+ log.info("** LINK ADDED {}", link.toString());
+ if (!isLinkValid(link)) {
+ return;
+ }
+ if (srManager.deviceConfiguration == null ||
+ !srManager.deviceConfiguration.isConfigured(link.src().deviceId())) {
+ updateSeenLink(link, true);
+ // XXX revisit - what about devicePortMap
+ log.warn("Source device of this link is not configured.. "
+ + "not processing further");
+ return;
+ }
+
+ // Irrespective of whether the local is a MASTER or not for this device,
+ // create group handler instance and push default TTP flow rules if needed,
+ // as in a multi-instance setup, instances can initiate groups for any
+ // device.
+ DefaultGroupHandler groupHandler = srManager.groupHandlerMap
+ .get(link.src().deviceId());
+ if (groupHandler != null) {
+ groupHandler.portUpForLink(link);
+ } else {
+ // XXX revisit/cleanup
+ Device device = srManager.deviceService.getDevice(link.src().deviceId());
+ if (device != null) {
+ log.warn("processLinkAdded: Link Added "
+ + "Notification without Device Added "
+ + "event, still handling it");
+ srManager.processDeviceAdded(device);
+ groupHandler = srManager.groupHandlerMap.get(link.src().deviceId());
+ groupHandler.portUpForLink(link);
+ }
+ }
+
+ /*
+ // process link only if it is bidirectional
+ if (!isBidirectional(link)) {
+ log.debug("Link not bidirectional.. waiting for other direction " +
+ "src {} --> dst {} ", link.dst(), link.src());
+ // note that if we are not processing for routing, it should at least
+ // be considered a seen-link
+ updateSeenLink(link, true); return;
+ }
+ //TODO ensure that rehash is still done correctly even if link is not processed for
+ //rerouting - perhaps rehash in both directions when it ultimately becomes bidi?
+ */
+
+ log.debug("Starting optimized route-path processing for added link "
+ + "{} --> {}", link.src(), link.dst());
+ boolean seenBefore = isSeenLink(link);
+ // seenLink updates will be done after route-path changes
+ srManager.defaultRoutingHandler
+ .populateRoutingRulesForLinkStatusChange(null, link, null);
+
+ if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
+ // handle edge-ports for dual-homed hosts
+ updateDualHomedHostPorts(link, true);
+
+ // It's possible that linkUp causes no route-path change as ECMP graph does
+ // not change if the link is a parallel link (same src-dst as
+ // another link). However we still need to update ECMP hash groups to include new buckets
+ // for the link that has come up.
+ if (groupHandler != null) {
+ if (!seenBefore && isParallelLink(link)) {
+ // if link seen first time, we need to ensure hash-groups have
+ // all ports
+ log.debug("Attempting retryHash for paralled first-time link {}",
+ link);
+ groupHandler.retryHash(link, false, true);
+ } else {
+ // seen before-link
+ if (isParallelLink(link)) {
+ log.debug("Attempting retryHash for paralled seen-before "
+ + "link {}", link);
+ groupHandler.retryHash(link, false, false);
+ }
+ }
+ }
+ }
+
+ srManager.mcastHandler.init();
+ }
+
+ /**
+ * Preprocessing of removed link before being sent for route-path handling.
+ * Also performs post processing of link.
+ *
+ * @param link the link to be processed
+ */
+ void processLinkRemoved(Link link) {
+ log.info("** LINK REMOVED {}", link.toString());
+ if (!isLinkValid(link)) {
+ return;
+ }
+ // when removing links, update seen links first, before doing route-path
+ // changes
+ updateSeenLink(link, false);
+ // handle edge-ports for dual-homed hosts
+ if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
+ updateDualHomedHostPorts(link, false);
+ }
+
+ // device availability check helps to ensure that multiple link-removed
+ // events are actually treated as a single switch removed event.
+ // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
+ // when switch comes back.
+ if (link.src().elementId() instanceof DeviceId
+ && !srManager.deviceService.isAvailable(link.src().deviceId())) {
+ purgeSeenLink(link);
+ return;
+ }
+ if (link.dst().elementId() instanceof DeviceId
+ && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
+ purgeSeenLink(link);
+ return;
+ }
+
+ log.debug("Starting optimized route-path processing for removed link "
+ + "{} --> {}", link.src(), link.dst());
+ srManager.defaultRoutingHandler
+ .populateRoutingRulesForLinkStatusChange(link, null, null);
+
+ // update local groupHandler stores
+ DefaultGroupHandler groupHandler = srManager.groupHandlerMap
+ .get(link.src().deviceId());
+ if (groupHandler != null) {
+ if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
+ && isParallelLink(link)) {
+ log.debug("* retrying hash for parallel link removed:{}", link);
+ groupHandler.retryHash(link, true, false);
+ } else {
+ log.debug("Not attempting retry-hash for link removed: {} .. {}",
+ link,
+ (srManager.mastershipService.isLocalMaster(link.src()
+ .deviceId())) ? "not parallel"
+ : "not master");
+ }
+ // ensure local stores are updated
+ groupHandler.portDown(link.src().port());
+ } else {
+ log.warn("group handler not found for dev:{} when removing link: {}",
+ link.src().deviceId(), link);
+ }
+
+ srManager.mcastHandler.processLinkDown(link);
+ }
+
+ /**
+ * Checks validity of link. Examples of invalid links include
+ * indirect-links, links between ports on the same switch, and more.
+ *
+ * @param link the link to be processed
+ * @return true if valid link
+ */
+ private boolean isLinkValid(Link link) {
+ if (link.type() != Link.Type.DIRECT) {
+ // NOTE: A DIRECT link might be transiently marked as INDIRECT
+ // if BDDP is received before LLDP. We can safely ignore that
+ // until the LLDP is received and the link is marked as DIRECT.
+ log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
+ link.src(), link.dst(), link.type());
+ return false;
+ }
+ DeviceId srcId = link.src().deviceId();
+ DeviceId dstId = link.dst().deviceId();
+ if (srcId.equals(dstId)) {
+ log.warn("Links between ports on the same switch are not "
+ + "allowed .. ignoring link {}", link);
+ return false;
+ }
+ DeviceConfiguration devConfig = srManager.deviceConfiguration;
+ if (devConfig == null) {
+ log.warn("Cannot check validity of link without device config");
+ return true;
+ }
+ try {
+ if (!devConfig.isEdgeDevice(srcId)
+ && !devConfig.isEdgeDevice(dstId)) {
+ // ignore links between spines
+ // XXX revisit when handling multi-stage fabrics
+ log.warn("Links between spines not allowed...ignoring "
+ + "link {}", link);
+ return false;
+ }
+ if (devConfig.isEdgeDevice(srcId)
+ && devConfig.isEdgeDevice(dstId)) {
+ // ignore links between leaves if they are not pair-links
+ // XXX revisit if removing pair-link config or allowing more than
+ // one pair-link
+ if (devConfig.getPairDeviceId(srcId).equals(dstId)
+ && devConfig.getPairLocalPort(srcId)
+ .equals(link.src().port())
+ && devConfig.getPairLocalPort(dstId)
+ .equals(link.dst().port())) {
+ // found pair link - allow it
+ return true;
+ } else {
+ log.warn("Links between leaves other than pair-links are "
+ + "not allowed...ignoring link {}", link);
+ return false;
+ }
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ // We still want to count the links in seenLinks even though there
+ // is no config. So we let it return true
+ log.warn("Could not check validity of link {} as subtending devices "
+ + "are not yet configured", link);
+ }
+ return true;
+ }
+
+ /**
+ * Administratively enables or disables edge ports if the link that was
+ * added or removed was the only uplink port from an edge device. Only edge
+ * ports that belong to dual-homed hosts are considered.
+ *
+ * @param link the link to be processed
+ * @param added true if link was added, false if link was removed
+ */
+ private void updateDualHomedHostPorts(Link link, boolean added) {
+ if (!onlyUplink(link)) {
+ return;
+ }
+ if (added) {
+ // re-enable previously disabled ports on this dev
+ Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
+ if (p != null) {
+ log.warn("Link src {} -->dst {} added is the first uplink, "
+ + "enabling dual homed ports: {}", link.src().deviceId(),
+ link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
+ p.forEach(pnum -> srManager.deviceAdminService
+ .changePortState(link.src().deviceId(), pnum, true));
+ }
+ } else {
+ // find dual homed hosts on this dev to disable
+ Set<PortNumber> dhp = srManager.hostHandler
+ .getDualHomedHostPorts(link.src().deviceId());
+ log.warn("Link src {} -->dst {} removed was the last uplink, "
+ + "disabling dual homed ports: {}", link.src().deviceId(),
+ link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
+ dhp.forEach(pnum -> srManager.deviceAdminService
+ .changePortState(link.src().deviceId(), pnum, false));
+ if (!dhp.isEmpty()) {
+ // update global store
+ Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
+ if (p == null) {
+ p = dhp;
+ } else {
+ p.addAll(dhp);
+ }
+ downedPortStore.put(link.src().deviceId(), p);
+ }
+ }
+ }
+
+ /**
+ * Returns true if given link is the only active uplink from src-device of
+ * link. An uplink is defined as a unidirectional link with src as
+ * edgeRouter and dst as non-edgeRouter.
+ *
+ * @param link
+ * @return true if given link is-the-first/was-the-last uplink from the src
+ * device
+ */
+ private boolean onlyUplink(Link link) {
+ DeviceConfiguration devConfig = srManager.deviceConfiguration;
+ try {
+ if (!devConfig.isEdgeDevice(link.src().deviceId())
+ || devConfig.isEdgeDevice(link.dst().deviceId())) {
+ return false;
+ }
+ // note that using linkservice here would cause race conditions as
+ // more links can show up while the app is still processing the first one
+ Set<Link> devLinks = seenLinks.entrySet().stream()
+ .filter(entry -> entry.getKey().src().deviceId()
+ .equals(link.src().deviceId()))
+ .filter(entry -> entry.getValue())
+ .filter(entry -> !entry.getKey().equals(link))
+ .map(entry -> entry.getKey())
+ .collect(Collectors.toSet());
+
+ for (Link l : devLinks) {
+ if (devConfig.isEdgeDevice(l.dst().deviceId())) {
+ continue;
+ }
+ log.debug("Link {} is not the only active uplink. Found another"
+ + "link {}", link, l);
+ return false;
+ }
+ log.debug("Link {} is the only uplink", link);
+ return true;
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Unable to determine if link is only uplink"
+ + e.getMessage());
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this controller instance has seen this link before. The
+ * link may not be currently up, but as long as the link had been seen
+ * before this method will return true. The one exception is when the link
+ * was indeed seen before, but this controller instance was forced to forget
+ * it by a call to purgeSeenLink method.
+ *
+ * @param link the infrastructure link being queried
+ * @return true if this controller instance has seen this link before
+ */
+ boolean isSeenLink(Link link) {
+ return seenLinks.containsKey(link);
+ }
+
+ /**
+ * Updates the seen link store. Updates can be for links that are currently
+ * available or not.
+ *
+ * @param link the link to update in the seen-link local store
+ * @param up the status of the link, true if up, false if down
+ */
+ void updateSeenLink(Link link, boolean up) {
+ seenLinks.put(link, up);
+ }
+
+ /**
+ * Returns the status of a seen-link (up or down). If the link has not been
+ * seen-before, a null object is returned.
+ *
+ * @param link the infrastructure link being queried
+ * @return null if the link was not seen-before; true if the seen-link is
+ * up; false if the seen-link is down
+ */
+ private Boolean isSeenLinkUp(Link link) {
+ return seenLinks.get(link);
+ }
+
+ /**
+ * Makes this controller instance forget a previously seen before link.
+ *
+ * @param link the infrastructure link to purge
+ */
+ private void purgeSeenLink(Link link) {
+ seenLinks.remove(link);
+ }
+
+ /**
+ * Returns the status of a link as parallel link. A parallel link is defined
+ * as a link which has common src and dst switches as another seen-link that
+ * is currently enabled. It is not necessary for the link being queried to
+ * be a seen-link.
+ *
+ * @param link the infrastructure link being queried
+ * @return true if a seen-link exists that is up, and shares the same src
+ * and dst switches as the link being queried
+ */
+ private boolean isParallelLink(Link link) {
+ for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
+ Link seenLink = seen.getKey();
+ if (seenLink.equals(link)) {
+ continue;
+ }
+ if (seenLink.src().deviceId().equals(link.src().deviceId())
+ && seenLink.dst().deviceId().equals(link.dst().deviceId())
+ && seen.getValue()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the link being queried is a bidirectional link. A bidi
+ * link is defined as a link, whose reverse link - ie. the link in the
+ * reverse direction - has been seen-before and is up. It is not necessary
+ * for the link being queried to be a seen-link.
+ *
+ * @param link the infrastructure link being queried
+ * @return true if another unidirectional link exists in the reverse
+ * direction, has been seen-before and is up
+ */
+ boolean isBidirectional(Link link) {
+ Link reverseLink = linkService.getLink(link.dst(), link.src());
+ if (reverseLink == null) {
+ return false;
+ }
+ Boolean result = isSeenLinkUp(reverseLink);
+ if (result == null) {
+ return false;
+ }
+ return result.booleanValue();
+ }
+
+ /**
+ * Determines if the given link should be avoided in routing calculations by
+ * policy or design.
+ *
+ * @param link the infrastructure link being queried
+ * @return true if link should be avoided
+ */
+ boolean avoidLink(Link link) {
+ // XXX currently only avoids all pair-links. In the future can be
+ // extended to avoid any generic link
+ DeviceId src = link.src().deviceId();
+ PortNumber srcPort = link.src().port();
+ DeviceConfiguration devConfig = srManager.deviceConfiguration;
+ if (devConfig == null || !devConfig.isConfigured(src)) {
+ log.warn("Device {} not configured..cannot avoid link {}", src,
+ link);
+ return false;
+ }
+ DeviceId pairDev;
+ PortNumber pairLocalPort, pairRemotePort = null;
+ try {
+ pairDev = devConfig.getPairDeviceId(src);
+ pairLocalPort = devConfig.getPairLocalPort(src);
+ if (pairDev != null) {
+ pairRemotePort = devConfig
+ .getPairLocalPort(pairDev);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
+ src, link);
+ return false;
+ }
+
+ return srcPort.equals(pairLocalPort)
+ && link.dst().deviceId().equals(pairDev)
+ && link.dst().port().equals(pairRemotePort);
+ }
+
+ /**
+ * Cleans up internal LinkHandler stores.
+ *
+ * @param device the device that has been removed
+ */
+ void processDeviceRemoved(Device device) {
+ seenLinks.keySet()
+ .removeIf(key -> key.src().deviceId().equals(device.id())
+ || key.dst().deviceId().equals(device.id()));
+ }
+
+ /**
+ * Administratively disables the host location switchport if the edge device
+ * has no viable uplinks.
+ *
+ * @param loc one of the locations of the dual-homed host
+ */
+ void checkUplinksForDualHomedHosts(HostLocation loc) {
+ try {
+ for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
+ if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
+ || l.state() == Link.State.INACTIVE) {
+ continue;
+ }
+ // found valid uplink - so, nothing to do
+ return;
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Could not check for valid uplinks due to missing device"
+ + "config " + e.getMessage());
+ return;
+ }
+ log.warn("Dual homed host location {} has no valid uplinks; "
+ + "disabling dual homed port", loc);
+ srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
+ false);
+ Set<PortNumber> p = downedPortStore.get(loc.deviceId());
+ if (p == null) {
+ p = Sets.newHashSet(loc.port());
+ } else {
+ p.add(loc.port());
+ }
+ downedPortStore.put(loc.deviceId(), p);
+ }
+
+ ImmutableMap<Link, Boolean> getSeenLinks() {
+ return ImmutableMap.copyOf(seenLinks);
+ }
+
+ ImmutableMap<DeviceId, Set<PortNumber>> getDownedPorts() {
+ return ImmutableMap.copyOf(downedPortStore.entrySet());
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java
new file mode 100644
index 0000000..fff2c92
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java
@@ -0,0 +1,1453 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.config.basics.McastConfig;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+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.flow.instructions.Instructions.OutputInstruction;
+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.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastRoute;
+import org.onosproject.net.mcast.McastRouteInfo;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+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.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+
+/**
+ * Handles multicast-related events.
+ */
+public class McastHandler {
+ private static final Logger log = LoggerFactory.getLogger(McastHandler.class);
+ private final SegmentRoutingManager srManager;
+ private final ApplicationId coreAppId;
+ private final StorageService storageService;
+ private final TopologyService topologyService;
+ private final ConsistentMap<McastStoreKey, NextObjective> mcastNextObjStore;
+ private final KryoNamespace.Builder mcastKryo;
+ private final ConsistentMap<McastStoreKey, McastRole> mcastRoleStore;
+
+ // Mcast lock to serialize local operations
+ private final Lock mcastLock = new ReentrantLock();
+
+ /**
+ * Acquires the lock used when making mcast changes.
+ */
+ private void mcastLock() {
+ mcastLock.lock();
+ }
+
+ /**
+ * Releases the lock used when making mcast changes.
+ */
+ private void mcastUnlock() {
+ mcastLock.unlock();
+ }
+
+ // Stability threshold for Mcast. Seconds
+ private static final long MCAST_STABLITY_THRESHOLD = 5;
+ // Last change done
+ private Instant lastMcastChange = Instant.now();
+
+ /**
+ * Determines if mcast in the network has been stable in the last
+ * MCAST_STABLITY_THRESHOLD seconds, by comparing the current time
+ * to the last mcast change timestamp.
+ *
+ * @return true if stable
+ */
+ private boolean isMcastStable() {
+ long last = (long) (lastMcastChange.toEpochMilli() / 1000.0);
+ long now = (long) (Instant.now().toEpochMilli() / 1000.0);
+ log.debug("Mcast stable since {}s", now - last);
+ return (now - last) > MCAST_STABLITY_THRESHOLD;
+ }
+
+ // Verify interval for Mcast
+ private static final long MCAST_VERIFY_INTERVAL = 30;
+
+ // Executor for mcast bucket corrector
+ private ScheduledExecutorService executorService
+ = newScheduledThreadPool(1, groupedThreads("mcastBktCorrector", "mcastbktC-%d", log));
+
+ /**
+ * Role in the multicast tree.
+ */
+ public enum McastRole {
+ /**
+ * The device is the ingress device of this group.
+ */
+ INGRESS,
+ /**
+ * The device is the transit device of this group.
+ */
+ TRANSIT,
+ /**
+ * The device is the egress device of this group.
+ */
+ EGRESS
+ }
+
+ /**
+ * Constructs the McastEventHandler.
+ *
+ * @param srManager Segment Routing manager
+ */
+ public McastHandler(SegmentRoutingManager srManager) {
+ coreAppId = srManager.coreService.getAppId(CoreService.CORE_APP_NAME);
+ this.srManager = srManager;
+ this.storageService = srManager.storageService;
+ this.topologyService = srManager.topologyService;
+ mcastKryo = new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .register(McastStoreKey.class)
+ .register(McastRole.class);
+ mcastNextObjStore = storageService
+ .<McastStoreKey, NextObjective>consistentMapBuilder()
+ .withName("onos-mcast-nextobj-store")
+ .withSerializer(Serializer.using(mcastKryo.build("McastHandler-NextObj")))
+ .build();
+ mcastRoleStore = storageService
+ .<McastStoreKey, McastRole>consistentMapBuilder()
+ .withName("onos-mcast-role-store")
+ .withSerializer(Serializer.using(mcastKryo.build("McastHandler-Role")))
+ .build();
+ // Init the executor service and the buckets corrector
+ executorService.scheduleWithFixedDelay(new McastBucketCorrector(), 10,
+ MCAST_VERIFY_INTERVAL,
+ TimeUnit.SECONDS);
+ }
+
+ /**
+ * Read initial multicast from mcast store.
+ */
+ protected void init() {
+ srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
+ ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
+ Set<ConnectPoint> sinks = srManager.multicastRouteService.fetchSinks(mcastRoute);
+ sinks.forEach(sink -> {
+ processSinkAddedInternal(source, sink, mcastRoute.group());
+ });
+ });
+ }
+
+ /**
+ * Clean up when deactivating the application.
+ */
+ protected void terminate() {
+ executorService.shutdown();
+ }
+
+ /**
+ * Processes the SOURCE_ADDED event.
+ *
+ * @param event McastEvent with SOURCE_ADDED type
+ */
+ protected void processSourceAdded(McastEvent event) {
+ log.info("processSourceAdded {}", event);
+ McastRouteInfo mcastRouteInfo = event.subject();
+ if (!mcastRouteInfo.isComplete()) {
+ log.info("Incompleted McastRouteInfo. Abort.");
+ return;
+ }
+ ConnectPoint source = mcastRouteInfo.source().orElse(null);
+ Set<ConnectPoint> sinks = mcastRouteInfo.sinks();
+ IpAddress mcastIp = mcastRouteInfo.route().group();
+
+ sinks.forEach(sink -> processSinkAddedInternal(source, sink, mcastIp));
+ }
+
+ /**
+ * Processes the SINK_ADDED event.
+ *
+ * @param event McastEvent with SINK_ADDED type
+ */
+ protected void processSinkAdded(McastEvent event) {
+ log.info("processSinkAdded {}", event);
+ McastRouteInfo mcastRouteInfo = event.subject();
+ if (!mcastRouteInfo.isComplete()) {
+ log.info("Incompleted McastRouteInfo. Abort.");
+ return;
+ }
+ ConnectPoint source = mcastRouteInfo.source().orElse(null);
+ ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+ IpAddress mcastIp = mcastRouteInfo.route().group();
+
+ processSinkAddedInternal(source, sink, mcastIp);
+ }
+
+ /**
+ * Processes the SINK_REMOVED event.
+ *
+ * @param event McastEvent with SINK_REMOVED type
+ */
+ protected void processSinkRemoved(McastEvent event) {
+ log.info("processSinkRemoved {}", event);
+ McastRouteInfo mcastRouteInfo = event.subject();
+ if (!mcastRouteInfo.isComplete()) {
+ log.info("Incompleted McastRouteInfo. Abort.");
+ return;
+ }
+ ConnectPoint source = mcastRouteInfo.source().orElse(null);
+ ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+ IpAddress mcastIp = mcastRouteInfo.route().group();
+
+ processSinkRemovedInternal(source, sink, mcastIp);
+ }
+
+ /**
+ * Processes the ROUTE_REMOVED event.
+ *
+ * @param event McastEvent with ROUTE_REMOVED type
+ */
+ protected void processRouteRemoved(McastEvent event) {
+ log.info("processRouteRemoved {}", event);
+ McastRouteInfo mcastRouteInfo = event.subject();
+ if (!mcastRouteInfo.source().isPresent()) {
+ log.info("Incompleted McastRouteInfo. Abort.");
+ return;
+ }
+ // Get group ip and ingress connect point
+ IpAddress mcastIp = mcastRouteInfo.route().group();
+ ConnectPoint source = mcastRouteInfo.source().get();
+
+ processRouteRemovedInternal(source, mcastIp);
+ }
+
+ /**
+ * Removes the entire mcast tree related to this group.
+ *
+ * @param mcastIp multicast group IP address
+ */
+ private void processRouteRemovedInternal(ConnectPoint source, IpAddress mcastIp) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ log.debug("Processing route down for group {}", mcastIp);
+
+ // Find out the ingress, transit and egress device of the affected group
+ DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+ .stream().findAny().orElse(null);
+ DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+ .stream().findAny().orElse(null);
+ Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+
+ // Verify leadership on the operation
+ if (!isLeader(source)) {
+ log.debug("Skip {} due to lack of leadership", mcastIp);
+ return;
+ }
+
+ // If there are egress devices, sinks could be only on the ingress
+ if (!egressDevices.isEmpty()) {
+ egressDevices.forEach(
+ deviceId -> removeGroupFromDevice(deviceId, mcastIp, assignedVlan(null))
+ );
+ }
+ // Transit could be null
+ if (transitDevice != null) {
+ removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+ }
+ // Ingress device should be not null
+ if (ingressDevice != null) {
+ removeGroupFromDevice(ingressDevice, mcastIp, assignedVlan(source));
+ }
+
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ /**
+ * Removes a path from source to sink for given multicast group.
+ *
+ * @param source connect point of the multicast source
+ * @param sink connection point of the multicast sink
+ * @param mcastIp multicast group IP address
+ */
+ private void processSinkRemovedInternal(ConnectPoint source, ConnectPoint sink,
+ IpAddress mcastIp) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ // Verify leadership on the operation
+ if (!isLeader(source)) {
+ log.debug("Skip {} due to lack of leadership", mcastIp);
+ return;
+ }
+
+ // When source and sink are on the same device
+ if (source.deviceId().equals(sink.deviceId())) {
+ // Source and sink are on even the same port. There must be something wrong.
+ if (source.port().equals(sink.port())) {
+ log.warn("Skip {} since sink {} is on the same port of source {}. Abort",
+ mcastIp, sink, source);
+ return;
+ }
+ removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(source));
+ return;
+ }
+
+ // Process the egress device
+ boolean isLast = removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(null));
+ if (isLast) {
+ mcastRoleStore.remove(new McastStoreKey(mcastIp, sink.deviceId()));
+ }
+
+ // If this is the last sink on the device, also update upstream
+ Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+ if (mcastPath.isPresent()) {
+ List<Link> links = Lists.newArrayList(mcastPath.get().links());
+ Collections.reverse(links);
+ for (Link link : links) {
+ if (isLast) {
+ isLast = removePortFromDevice(
+ link.src().deviceId(),
+ link.src().port(),
+ mcastIp,
+ assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null)
+ );
+ mcastRoleStore.remove(new McastStoreKey(mcastIp, link.src().deviceId()));
+ }
+ }
+ }
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ /**
+ * Establishes a path from source to sink for given multicast group.
+ *
+ * @param source connect point of the multicast source
+ * @param sink connection point of the multicast sink
+ * @param mcastIp multicast group IP address
+ */
+ private void processSinkAddedInternal(ConnectPoint source, ConnectPoint sink,
+ IpAddress mcastIp) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ // Continue only when this instance is the master of source device
+ if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+ log.debug("Skip {} due to lack of mastership of the source device {}",
+ mcastIp, source.deviceId());
+ return;
+ }
+
+ // Process the ingress device
+ addFilterToDevice(source.deviceId(), source.port(), assignedVlan(source), mcastIp);
+
+ // When source and sink are on the same device
+ if (source.deviceId().equals(sink.deviceId())) {
+ // Source and sink are on even the same port. There must be something wrong.
+ if (source.port().equals(sink.port())) {
+ log.warn("Skip {} since sink {} is on the same port of source {}. Abort",
+ mcastIp, sink, source);
+ return;
+ }
+ addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(source));
+ mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()), McastRole.INGRESS);
+ return;
+ }
+
+ // Find a path. If present, create/update groups and flows for each hop
+ Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+ if (mcastPath.isPresent()) {
+ List<Link> links = mcastPath.get().links();
+ checkState(links.size() == 2,
+ "Path in leaf-spine topology should always be two hops: ", links);
+
+ links.forEach(link -> {
+ addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp,
+ assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null));
+ addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan(null), mcastIp);
+ });
+
+ // Process the egress device
+ addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(null));
+
+ // Setup mcast roles
+ mcastRoleStore.put(new McastStoreKey(mcastIp, source.deviceId()),
+ McastRole.INGRESS);
+ mcastRoleStore.put(new McastStoreKey(mcastIp, links.get(0).dst().deviceId()),
+ McastRole.TRANSIT);
+ mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()),
+ McastRole.EGRESS);
+ } else {
+ log.warn("Unable to find a path from {} to {}. Abort sinkAdded",
+ source.deviceId(), sink.deviceId());
+ }
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ /**
+ * Processes the LINK_DOWN event.
+ *
+ * @param affectedLink Link that is going down
+ */
+ protected void processLinkDown(Link affectedLink) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ // Get groups affected by the link down event
+ getAffectedGroups(affectedLink).forEach(mcastIp -> {
+ // TODO Optimize when the group editing is in place
+ log.debug("Processing link down {} for group {}",
+ affectedLink, mcastIp);
+
+ // Find out the ingress, transit and egress device of affected group
+ DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+ .stream().findAny().orElse(null);
+ DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+ .stream().findAny().orElse(null);
+ Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+ ConnectPoint source = getSource(mcastIp);
+
+ // Do not proceed if any of these info is missing
+ if (ingressDevice == null || transitDevice == null
+ || egressDevices == null || source == null) {
+ log.warn("Missing ingress {}, transit {}, egress {} devices or source {}",
+ ingressDevice, transitDevice, egressDevices, source);
+ return;
+ }
+
+ // Continue only when this instance is the master of source device
+ if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+ log.debug("Skip {} due to lack of mastership of the source device {}",
+ source.deviceId());
+ return;
+ }
+
+ // Remove entire transit
+ removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+
+ // Remove transit-facing port on ingress device
+ PortNumber ingressTransitPort = ingressTransitPort(mcastIp);
+ if (ingressTransitPort != null) {
+ removePortFromDevice(ingressDevice, ingressTransitPort, mcastIp, assignedVlan(source));
+ mcastRoleStore.remove(new McastStoreKey(mcastIp, transitDevice));
+ }
+
+ // Construct a new path for each egress device
+ egressDevices.forEach(egressDevice -> {
+ Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+ if (mcastPath.isPresent()) {
+ installPath(mcastIp, source, mcastPath.get());
+ } else {
+ log.warn("Fail to recover egress device {} from link failure {}",
+ egressDevice, affectedLink);
+ removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null));
+ }
+ });
+ });
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ /**
+ * Process the DEVICE_DOWN event.
+ *
+ * @param deviceDown device going down
+ */
+ protected void processDeviceDown(DeviceId deviceDown) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ // Get the mcast groups affected by the device going down
+ getAffectedGroups(deviceDown).forEach(mcastIp -> {
+ // TODO Optimize when the group editing is in place
+ log.debug("Processing device down {} for group {}",
+ deviceDown, mcastIp);
+
+ // Find out the ingress, transit and egress device of affected group
+ DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+ .stream().findAny().orElse(null);
+ DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+ .stream().findAny().orElse(null);
+ Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+ ConnectPoint source = getSource(mcastIp);
+
+ // Do not proceed if ingress device or source of this group are missing
+ // If sinks are in other leafs, we have ingress, transit, egress, and source
+ // If sinks are in the same leaf, we have just ingress and source
+ if (ingressDevice == null || source == null) {
+ log.warn("Missing ingress {} or source {} for group {}",
+ ingressDevice, source, mcastIp);
+ return;
+ }
+
+ // Verify leadership on the operation
+ if (!isLeader(source)) {
+ log.debug("Skip {} due to lack of leadership", mcastIp);
+ return;
+ }
+
+ // If it exists, we have to remove it in any case
+ if (transitDevice != null) {
+ // Remove entire transit
+ removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+ }
+ // If the ingress is down
+ if (ingressDevice.equals(deviceDown)) {
+ // Remove entire ingress
+ removeGroupFromDevice(ingressDevice, mcastIp, assignedVlan(source));
+ // If other sinks different from the ingress exist
+ if (!egressDevices.isEmpty()) {
+ // Remove all the remaining egress
+ egressDevices.forEach(
+ egressDevice -> removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null))
+ );
+ }
+ } else {
+ // Egress or transit could be down at this point
+ // Get the ingress-transit port if it exists
+ PortNumber ingressTransitPort = ingressTransitPort(mcastIp);
+ if (ingressTransitPort != null) {
+ // Remove transit-facing port on ingress device
+ removePortFromDevice(ingressDevice, ingressTransitPort, mcastIp, assignedVlan(source));
+ }
+ // One of the egress device is down
+ if (egressDevices.contains(deviceDown)) {
+ // Remove entire device down
+ removeGroupFromDevice(deviceDown, mcastIp, assignedVlan(null));
+ // Remove the device down from egress
+ egressDevices.remove(deviceDown);
+ // If there are no more egress and ingress does not have sinks
+ if (egressDevices.isEmpty() && !hasSinks(ingressDevice, mcastIp)) {
+ // Remove entire ingress
+ mcastRoleStore.remove(new McastStoreKey(mcastIp, ingressDevice));
+ // We have done
+ return;
+ }
+ }
+ // Construct a new path for each egress device
+ egressDevices.forEach(egressDevice -> {
+ Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+ // If there is a new path
+ if (mcastPath.isPresent()) {
+ // Let's install the new mcast path for this egress
+ installPath(mcastIp, source, mcastPath.get());
+ } else {
+ // We were not able to find an alternative path for this egress
+ log.warn("Fail to recover egress device {} from device down {}",
+ egressDevice, deviceDown);
+ removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null));
+ }
+ });
+ }
+ });
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ /**
+ * Adds filtering objective for given device and port.
+ *
+ * @param deviceId device ID
+ * @param port ingress port number
+ * @param assignedVlan assigned VLAN ID
+ */
+ private void addFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan, IpAddress mcastIp) {
+ // Do nothing if the port is configured as suppressed
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+ SegmentRoutingAppConfig appConfig = srManager.cfgService
+ .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+ if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+ log.info("Ignore suppressed port {}", connectPoint);
+ return;
+ }
+
+ FilteringObjective.Builder filtObjBuilder =
+ filterObjBuilder(deviceId, port, assignedVlan, mcastIp);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully add filter on {}/{}, vlan {}",
+ deviceId, port.toLong(), assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to add filter on {}/{}, vlan {}: {}",
+ deviceId, port.toLong(), assignedVlan, error));
+ srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add(context));
+ }
+
+ /**
+ * Adds a port to given multicast group on given device. This involves the
+ * update of L3 multicast group and multicast routing table entry.
+ *
+ * @param deviceId device ID
+ * @param port port to be added
+ * @param mcastIp multicast group
+ * @param assignedVlan assigned VLAN ID
+ */
+ private void addPortToDevice(DeviceId deviceId, PortNumber port,
+ IpAddress mcastIp, VlanId assignedVlan) {
+ McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
+ ImmutableSet.Builder<PortNumber> portBuilder = ImmutableSet.builder();
+ NextObjective newNextObj;
+ if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+ // First time someone request this mcast group via this device
+ portBuilder.add(port);
+ // New nextObj
+ newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+ portBuilder.build(), null).add();
+ // Store the new port
+ mcastNextObjStore.put(mcastStoreKey, newNextObj);
+ } else {
+ // This device already serves some subscribers of this mcast group
+ NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+ // Stop if the port is already in the nextobj
+ Set<PortNumber> existingPorts = getPorts(nextObj.next());
+ if (existingPorts.contains(port)) {
+ log.info("NextObj for {}/{} already exists. Abort", deviceId, port);
+ return;
+ }
+ // Let's add the port and reuse the previous one
+ portBuilder.addAll(existingPorts).add(port);
+ // Reuse previous nextObj
+ newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+ portBuilder.build(), nextObj.id()).addToExisting();
+ // Store the final next objective and send only the difference to the driver
+ mcastNextObjStore.put(mcastStoreKey, newNextObj);
+ // Add just the new port
+ portBuilder = ImmutableSet.builder();
+ portBuilder.add(port);
+ newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+ portBuilder.build(), nextObj.id()).addToExisting();
+ }
+ // Create, store and apply the new nextObj and fwdObj
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully add {} on {}/{}, vlan {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to add {} on {}/{}, vlan {}: {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan, error));
+ ForwardingObjective fwdObj =
+ fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add(context);
+ srManager.flowObjectiveService.next(deviceId, newNextObj);
+ srManager.flowObjectiveService.forward(deviceId, fwdObj);
+ }
+
+ /**
+ * Removes a port from given multicast group on given device.
+ * This involves the update of L3 multicast group and multicast routing
+ * table entry.
+ *
+ * @param deviceId device ID
+ * @param port port to be added
+ * @param mcastIp multicast group
+ * @param assignedVlan assigned VLAN ID
+ * @return true if this is the last sink on this device
+ */
+ private boolean removePortFromDevice(DeviceId deviceId, PortNumber port,
+ IpAddress mcastIp, VlanId assignedVlan) {
+ McastStoreKey mcastStoreKey =
+ new McastStoreKey(mcastIp, deviceId);
+ // This device is not serving this multicast group
+ if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+ log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+ return false;
+ }
+ NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+
+ Set<PortNumber> existingPorts = getPorts(nextObj.next());
+ // This port does not serve this multicast group
+ if (!existingPorts.contains(port)) {
+ log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+ return false;
+ }
+ // Copy and modify the ImmutableSet
+ existingPorts = Sets.newHashSet(existingPorts);
+ existingPorts.remove(port);
+
+ NextObjective newNextObj;
+ ObjectiveContext context;
+ ForwardingObjective fwdObj;
+ if (existingPorts.isEmpty()) {
+ // If this is the last sink, remove flows and last bucket
+ // NOTE: Rely on GroupStore garbage collection rather than explicitly
+ // remove L3MG since there might be other flows/groups refer to
+ // the same L2IG
+ context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully remove {} on {}/{}, vlan {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to remove {} on {}/{}, vlan {}: {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan, error));
+ fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+ mcastNextObjStore.remove(mcastStoreKey);
+ } else {
+ // If this is not the last sink, update flows and groups
+ context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully update {} on {}/{}, vlan {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to update {} on {}/{}, vlan {}: {}",
+ mcastIp, deviceId, port.toLong(), assignedVlan, error));
+ // Here we store the next objective with the remaining port
+ newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+ existingPorts, nextObj.id()).removeFromExisting();
+ fwdObj = fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add(context);
+ mcastNextObjStore.put(mcastStoreKey, newNextObj);
+ }
+ // Let's modify the next objective removing the bucket
+ newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+ ImmutableSet.of(port), nextObj.id()).removeFromExisting();
+ srManager.flowObjectiveService.next(deviceId, newNextObj);
+ srManager.flowObjectiveService.forward(deviceId, fwdObj);
+ return existingPorts.isEmpty();
+ }
+
+ /**
+ * Removes entire group on given device.
+ *
+ * @param deviceId device ID
+ * @param mcastIp multicast group to be removed
+ * @param assignedVlan assigned VLAN ID
+ */
+ private void removeGroupFromDevice(DeviceId deviceId, IpAddress mcastIp,
+ VlanId assignedVlan) {
+ McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
+ // This device is not serving this multicast group
+ if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+ log.warn("{} is not serving {}. Abort.", deviceId, mcastIp);
+ return;
+ }
+ NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+ // NOTE: Rely on GroupStore garbage collection rather than explicitly
+ // remove L3MG since there might be other flows/groups refer to
+ // the same L2IG
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully remove {} on {}, vlan {}",
+ mcastIp, deviceId, assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to remove {} on {}, vlan {}: {}",
+ mcastIp, deviceId, assignedVlan, error));
+ ForwardingObjective fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+ srManager.flowObjectiveService.forward(deviceId, fwdObj);
+ mcastNextObjStore.remove(mcastStoreKey);
+ mcastRoleStore.remove(mcastStoreKey);
+ }
+
+ private void installPath(IpAddress mcastIp, ConnectPoint source, Path mcastPath) {
+ // Get Links
+ List<Link> links = mcastPath.links();
+ // For each link, modify the next on the source device adding the src port
+ // and a new filter objective on the destination port
+ links.forEach(link -> {
+ addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp,
+ assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null));
+ addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan(null),
+ mcastIp);
+ });
+ // Setup new transit mcast role
+ mcastRoleStore.put(new McastStoreKey(mcastIp, links.get(0).dst().deviceId()),
+ McastRole.TRANSIT);
+ }
+
+ /**
+ * Creates a next objective builder for multicast.
+ *
+ * @param mcastIp multicast group
+ * @param assignedVlan assigned VLAN ID
+ * @param outPorts set of output port numbers
+ * @return next objective builder
+ */
+ private NextObjective.Builder nextObjBuilder(IpAddress mcastIp,
+ VlanId assignedVlan, Set<PortNumber> outPorts, Integer nextId) {
+ // If nextId is null allocate a new one
+ if (nextId == null) {
+ nextId = srManager.flowObjectiveService.allocateNextId();
+ }
+
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder()
+ .matchVlanId(assignedVlan)
+ .matchIPDst(mcastIp.toIpPrefix())
+ .build();
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(srManager.appId)
+ .withMeta(metadata);
+
+ outPorts.forEach(port -> {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (egressVlan().equals(VlanId.NONE)) {
+ tBuilder.popVlan();
+ }
+ tBuilder.setOutput(port);
+ nextObjBuilder.addTreatment(tBuilder.build());
+ });
+
+ return nextObjBuilder;
+ }
+
+ /**
+ * Creates a forwarding objective builder for multicast.
+ *
+ * @param mcastIp multicast group
+ * @param assignedVlan assigned VLAN ID
+ * @param nextId next ID of the L3 multicast group
+ * @return forwarding objective builder
+ */
+ private ForwardingObjective.Builder fwdObjBuilder(IpAddress mcastIp,
+ VlanId assignedVlan, int nextId) {
+ TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+ IpPrefix mcastPrefix = mcastIp.toIpPrefix();
+
+ if (mcastIp.isIp4()) {
+ sbuilder.matchEthType(Ethernet.TYPE_IPV4);
+ sbuilder.matchIPDst(mcastPrefix);
+ } else {
+ sbuilder.matchEthType(Ethernet.TYPE_IPV6);
+ sbuilder.matchIPv6Dst(mcastPrefix);
+ }
+
+
+ TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+ metabuilder.matchVlanId(assignedVlan);
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective.builder();
+ fwdBuilder.withSelector(sbuilder.build())
+ .withMeta(metabuilder.build())
+ .nextStep(nextId)
+ .withFlag(ForwardingObjective.Flag.SPECIFIC)
+ .fromApp(srManager.appId)
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+ return fwdBuilder;
+ }
+
+ /**
+ * Creates a filtering objective builder for multicast.
+ *
+ * @param deviceId Device ID
+ * @param ingressPort ingress port of the multicast stream
+ * @param assignedVlan assigned VLAN ID
+ * @return filtering objective builder
+ */
+ private FilteringObjective.Builder filterObjBuilder(DeviceId deviceId, PortNumber ingressPort,
+ VlanId assignedVlan, IpAddress mcastIp) {
+ FilteringObjective.Builder filtBuilder = DefaultFilteringObjective.builder();
+
+ if (mcastIp.isIp4()) {
+ filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+ .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV4_MULTICAST,
+ MacAddress.IPV4_MULTICAST_MASK))
+ .addCondition(Criteria.matchVlanId(egressVlan()))
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+ } else {
+ filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+ .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV6_MULTICAST,
+ MacAddress.IPV6_MULTICAST_MASK))
+ .addCondition(Criteria.matchVlanId(egressVlan()))
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+ }
+ TrafficTreatment tt = DefaultTrafficTreatment.builder()
+ .pushVlan().setVlanId(assignedVlan).build();
+ filtBuilder.withMeta(tt);
+
+ return filtBuilder.permit().fromApp(srManager.appId);
+ }
+
+ /**
+ * Gets output ports information from treatments.
+ *
+ * @param treatments collection of traffic treatments
+ * @return set of output port numbers
+ */
+ private Set<PortNumber> getPorts(Collection<TrafficTreatment> treatments) {
+ ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+ treatments.forEach(treatment -> {
+ treatment.allInstructions().stream()
+ .filter(instr -> instr instanceof OutputInstruction)
+ .forEach(instr -> {
+ builder.add(((OutputInstruction) instr).port());
+ });
+ });
+ return builder.build();
+ }
+
+ /**
+ * Gets a path from src to dst.
+ * If a path was allocated before, returns the allocated path.
+ * Otherwise, randomly pick one from available paths.
+ *
+ * @param src source device ID
+ * @param dst destination device ID
+ * @param mcastIp multicast group
+ * @return an optional path from src to dst
+ */
+ private Optional<Path> getPath(DeviceId src, DeviceId dst, IpAddress mcastIp) {
+ List<Path> allPaths = Lists.newArrayList(
+ topologyService.getPaths(topologyService.currentTopology(), src, dst));
+ log.debug("{} path(s) found from {} to {}", allPaths.size(), src, dst);
+ if (allPaths.isEmpty()) {
+ return Optional.empty();
+ }
+
+ // Create a map index of suitablity-to-list of paths. For example
+ // a path in the list associated to the index 1 shares only the
+ // first hop and it is less suitable of a path belonging to the index
+ // 2 that shares leaf-spine.
+ Map<Integer, List<Path>> eligiblePaths = Maps.newHashMap();
+ // Some init steps
+ int nhop;
+ McastStoreKey mcastStoreKey;
+ Link hop;
+ PortNumber srcPort;
+ Set<PortNumber> existingPorts;
+ NextObjective nextObj;
+ // Iterate over paths looking for eligible paths
+ for (Path path : allPaths) {
+ // Unlikely, it will happen...
+ if (!src.equals(path.links().get(0).src().deviceId())) {
+ continue;
+ }
+ nhop = 0;
+ // Iterate over the links
+ while (nhop < path.links().size()) {
+ // Get the link and verify if a next related
+ // to the src device exist in the store
+ hop = path.links().get(nhop);
+ mcastStoreKey = new McastStoreKey(mcastIp, hop.src().deviceId());
+ // It does not exist in the store, exit
+ if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+ break;
+ }
+ // Get the output ports on the next
+ nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+ existingPorts = getPorts(nextObj.next());
+ // And the src port on the link
+ srcPort = hop.src().port();
+ // the src port is not used as output, exit
+ if (!existingPorts.contains(srcPort)) {
+ break;
+ }
+ nhop++;
+ }
+ // n_hop defines the index
+ if (nhop > 0) {
+ eligiblePaths.compute(nhop, (index, paths) -> {
+ paths = paths == null ? Lists.newArrayList() : paths;
+ paths.add(path);
+ return paths;
+ });
+ }
+ }
+
+ // No suitable paths
+ if (eligiblePaths.isEmpty()) {
+ log.debug("No eligiblePath(s) found from {} to {}", src, dst);
+ // Otherwise, randomly pick a path
+ Collections.shuffle(allPaths);
+ return allPaths.stream().findFirst();
+ }
+
+ // Let's take the best ones
+ Integer bestIndex = eligiblePaths.keySet()
+ .stream()
+ .sorted(Comparator.reverseOrder())
+ .findFirst().orElse(null);
+ List<Path> bestPaths = eligiblePaths.get(bestIndex);
+ log.debug("{} eligiblePath(s) found from {} to {}",
+ bestPaths.size(), src, dst);
+ // randomly pick a path on the highest index
+ Collections.shuffle(bestPaths);
+ return bestPaths.stream().findFirst();
+ }
+
+ /**
+ * Gets device(s) of given role in given multicast group.
+ *
+ * @param mcastIp multicast IP
+ * @param role multicast role
+ * @return set of device ID or empty set if not found
+ */
+ private Set<DeviceId> getDevice(IpAddress mcastIp, McastRole role) {
+ return mcastRoleStore.entrySet().stream()
+ .filter(entry -> entry.getKey().mcastIp().equals(mcastIp) &&
+ entry.getValue().value() == role)
+ .map(Map.Entry::getKey).map(McastStoreKey::deviceId)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Gets source connect point of given multicast group.
+ *
+ * @param mcastIp multicast IP
+ * @return source connect point or null if not found
+ */
+ private ConnectPoint getSource(IpAddress mcastIp) {
+ return srManager.multicastRouteService.getRoutes().stream()
+ .filter(mcastRoute -> mcastRoute.group().equals(mcastIp))
+ .map(mcastRoute -> srManager.multicastRouteService.fetchSource(mcastRoute))
+ .findAny().orElse(null);
+ }
+
+ /**
+ * Gets groups which is affected by the link down event.
+ *
+ * @param link link going down
+ * @return a set of multicast IpAddress
+ */
+ private Set<IpAddress> getAffectedGroups(Link link) {
+ DeviceId deviceId = link.src().deviceId();
+ PortNumber port = link.src().port();
+ return mcastNextObjStore.entrySet().stream()
+ .filter(entry -> entry.getKey().deviceId().equals(deviceId) &&
+ getPorts(entry.getValue().value().next()).contains(port))
+ .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Gets groups which are affected by the device down event.
+ *
+ * @param deviceId device going down
+ * @return a set of multicast IpAddress
+ */
+ private Set<IpAddress> getAffectedGroups(DeviceId deviceId) {
+ return mcastNextObjStore.entrySet().stream()
+ .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+ .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Gets egress VLAN from McastConfig.
+ *
+ * @return egress VLAN or VlanId.NONE if not configured
+ */
+ private VlanId egressVlan() {
+ McastConfig mcastConfig =
+ srManager.cfgService.getConfig(coreAppId, McastConfig.class);
+ return (mcastConfig != null) ? mcastConfig.egressVlan() : VlanId.NONE;
+ }
+
+ /**
+ * Gets assigned VLAN according to the value of egress VLAN.
+ * If connect point is specified, try to reuse the assigned VLAN on the connect point.
+ *
+ * @param cp connect point; Can be null if not specified
+ * @return assigned VLAN ID
+ */
+ private VlanId assignedVlan(ConnectPoint cp) {
+ // Use the egressVlan if it is tagged
+ if (!egressVlan().equals(VlanId.NONE)) {
+ return egressVlan();
+ }
+ // Reuse unicast VLAN if the port has subnet configured
+ if (cp != null) {
+ VlanId untaggedVlan = srManager.getInternalVlanId(cp);
+ return (untaggedVlan != null) ? untaggedVlan : INTERNAL_VLAN;
+ }
+ // Use DEFAULT_VLAN if none of the above matches
+ return SegmentRoutingManager.INTERNAL_VLAN;
+ }
+
+ /**
+ * Gets the spine-facing port on ingress device of given multicast group.
+ *
+ * @param mcastIp multicast IP
+ * @return spine-facing port on ingress device
+ */
+ private PortNumber ingressTransitPort(IpAddress mcastIp) {
+ DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+ .stream().findAny().orElse(null);
+ if (ingressDevice != null) {
+ NextObjective nextObj = mcastNextObjStore
+ .get(new McastStoreKey(mcastIp, ingressDevice)).value();
+ Set<PortNumber> ports = getPorts(nextObj.next());
+
+ for (PortNumber port : ports) {
+ // 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))) {
+ return port;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Verify if the given device has sinks
+ * for the multicast group.
+ *
+ * @param deviceId device Id
+ * @param mcastIp multicast IP
+ * @return true if the device has sink for the group.
+ * False otherwise.
+ */
+ private boolean hasSinks(DeviceId deviceId, IpAddress mcastIp) {
+ if (deviceId != null) {
+ // Get the nextobjective
+ Versioned<NextObjective> versionedNextObj = mcastNextObjStore.get(
+ new McastStoreKey(mcastIp, deviceId)
+ );
+ // If it exists
+ if (versionedNextObj != null) {
+ NextObjective nextObj = versionedNextObj.value();
+ // Retrieves all the output ports
+ Set<PortNumber> ports = getPorts(nextObj.next());
+ // Tries to find at least one port that is not spine-facing
+ for (PortNumber port : ports) {
+ // Spine-facing port should have no subnet and no xconnect
+ if (srManager.deviceConfiguration != null &&
+ (!srManager.deviceConfiguration.getPortSubnets(deviceId, port).isEmpty() ||
+ srManager.xConnectHandler.hasXConnect(new ConnectPoint(deviceId, port)))) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes filtering objective for given device and port.
+ *
+ * @param deviceId device ID
+ * @param port ingress port number
+ * @param assignedVlan assigned VLAN ID
+ * @param mcastIp multicast IP address
+ */
+ private void removeFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan, IpAddress mcastIp) {
+ // Do nothing if the port is configured as suppressed
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+ SegmentRoutingAppConfig appConfig = srManager.cfgService
+ .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+ if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+ log.info("Ignore suppressed port {}", connectPoint);
+ return;
+ }
+
+ FilteringObjective.Builder filtObjBuilder =
+ filterObjBuilder(deviceId, port, assignedVlan, mcastIp);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Successfully removed filter on {}/{}, vlan {}",
+ deviceId, port.toLong(), assignedVlan),
+ (objective, error) ->
+ log.warn("Failed to remove filter on {}/{}, vlan {}: {}",
+ deviceId, port.toLong(), assignedVlan, error));
+ srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.remove(context));
+ }
+
+ /**
+ * Updates filtering objective for given device and port.
+ * It is called in general when the mcast config has been
+ * changed.
+ *
+ * @param deviceId device ID
+ * @param portNum ingress port number
+ * @param vlanId assigned VLAN ID
+ * @param install true to add, false to remove
+ */
+ protected void updateFilterToDevice(DeviceId deviceId, PortNumber portNum,
+ VlanId vlanId, boolean install) {
+ lastMcastChange = Instant.now();
+ mcastLock();
+ try {
+ // Iterates over the route and updates properly the filtering objective
+ // on the source device.
+ srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
+ ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
+ if (source.deviceId().equals(deviceId) && source.port().equals(portNum)) {
+ if (install) {
+ addFilterToDevice(deviceId, portNum, vlanId, mcastRoute.group());
+ } else {
+ removeFilterToDevice(deviceId, portNum, vlanId, mcastRoute.group());
+ }
+ }
+ });
+ } finally {
+ mcastUnlock();
+ }
+ }
+
+ private boolean isLeader(ConnectPoint source) {
+ // Continue only when we have the mastership on the operation
+ if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+ // When the source is available we just check the mastership
+ if (srManager.deviceService.isAvailable(source.deviceId())) {
+ return false;
+ }
+ // Fallback with Leadership service
+ // source id is used a topic
+ NodeId leader = srManager.leadershipService.runForLeadership(
+ source.deviceId().toString()).leaderNodeId();
+ // Verify if this node is the leader
+ if (!srManager.clusterService.getLocalNode().id().equals(leader)) {
+ return false;
+ }
+ }
+ // Done
+ return true;
+ }
+
+ /**
+ * Performs bucket verification operation for all mcast groups in the devices.
+ * Firstly, it verifies that mcast is stable before trying verification operation.
+ * Verification consists in creating new nexts with VERIFY operation. Actually,
+ * the operation is totally delegated to the driver.
+ */
+ private final class McastBucketCorrector implements Runnable {
+
+ @Override
+ public void run() {
+ // Verify if the Mcast has been stable for MCAST_STABLITY_THRESHOLD
+ if (!isMcastStable()) {
+ return;
+ }
+ // Acquires lock
+ mcastLock();
+ try {
+ // Iterates over the routes and verify the related next objectives
+ srManager.multicastRouteService.getRoutes()
+ .stream()
+ .map(McastRoute::group)
+ .forEach(mcastIp -> {
+ log.trace("Running mcast buckets corrector for mcast group: {}",
+ mcastIp);
+
+ // For each group we get current information in the store
+ // and issue a check of the next objectives in place
+ DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+ .stream().findAny().orElse(null);
+ DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+ .stream().findAny().orElse(null);
+ Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+ ConnectPoint source = getSource(mcastIp);
+
+ // Do not proceed if ingress device or source of this group are missing
+ if (ingressDevice == null || source == null) {
+ log.warn("Unable to run buckets corrector. " +
+ "Missing ingress {} or source {} for group {}",
+ ingressDevice, source, mcastIp);
+ return;
+ }
+
+ // Continue only when this instance is the master of source device
+ if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+ log.trace("Unable to run buckets corrector. " +
+ "Skip {} due to lack of mastership " +
+ "of the source device {}",
+ mcastIp, source.deviceId());
+ return;
+ }
+
+ // Create the set of the devices to be processed
+ ImmutableSet.Builder<DeviceId> devicesBuilder = ImmutableSet.builder();
+ devicesBuilder.add(ingressDevice);
+ if (transitDevice != null) {
+ devicesBuilder.add(transitDevice);
+ }
+ if (!egressDevices.isEmpty()) {
+ devicesBuilder.addAll(egressDevices);
+ }
+ Set<DeviceId> devicesToProcess = devicesBuilder.build();
+
+ // Iterate over the devices
+ devicesToProcess.forEach(deviceId -> {
+ McastStoreKey currentKey = new McastStoreKey(mcastIp, deviceId);
+ // If next exists in our store verify related next objective
+ if (mcastNextObjStore.containsKey(currentKey)) {
+ NextObjective currentNext = mcastNextObjStore.get(currentKey).value();
+ // Get current ports
+ Set<PortNumber> currentPorts = getPorts(currentNext.next());
+ // Rebuild the next objective
+ currentNext = nextObjBuilder(
+ mcastIp,
+ assignedVlan(deviceId.equals(source.deviceId()) ? source : null),
+ currentPorts,
+ currentNext.id()
+ ).verify();
+ // Send to the flowobjective service
+ srManager.flowObjectiveService.next(deviceId, currentNext);
+ } else {
+ log.warn("Unable to run buckets corrector." +
+ "Missing next for {} and group {}",
+ deviceId, mcastIp);
+ }
+ });
+
+ });
+ } finally {
+ // Finally, it releases the lock
+ mcastUnlock();
+ }
+
+ }
+ }
+
+ public Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp) {
+ // If mcast ip is present
+ if (mcastIp != null) {
+ return mcastNextObjStore.entrySet().stream()
+ .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().value().id()));
+ }
+ // Otherwise take all the groups
+ return mcastNextObjStore.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().value().id()));
+ }
+
+ public Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp) {
+ // If mcast ip is present
+ if (mcastIp != null) {
+ return mcastRoleStore.entrySet().stream()
+ .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().value()));
+ }
+ // Otherwise take all the groups
+ return mcastRoleStore.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().value()));
+ }
+
+ public Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp) {
+ Map<ConnectPoint, List<ConnectPoint>> mcastPaths = Maps.newHashMap();
+ // Get the source
+ ConnectPoint source = getSource(mcastIp);
+ // Source cannot be null, we don't know the starting point
+ if (source != null) {
+ // Init steps
+ Set<DeviceId> visited = Sets.newHashSet();
+ List<ConnectPoint> currentPath = Lists.newArrayList(
+ source
+ );
+ // Build recursively the mcast paths
+ buildMcastPaths(source.deviceId(), visited, mcastPaths, currentPath, mcastIp);
+ }
+ return mcastPaths;
+ }
+
+ private void buildMcastPaths(DeviceId toVisit, Set<DeviceId> visited,
+ Map<ConnectPoint, List<ConnectPoint>> mcastPaths,
+ List<ConnectPoint> currentPath, IpAddress mcastIp) {
+ // If we have visited the node to visit
+ // there is a loop
+ if (visited.contains(toVisit)) {
+ return;
+ }
+ // Visit next-hop
+ visited.add(toVisit);
+ McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, toVisit);
+ // Looking for next-hops
+ if (mcastNextObjStore.containsKey(mcastStoreKey)) {
+ // Build egress connectpoints
+ NextObjective nextObjective = mcastNextObjStore.get(mcastStoreKey).value();
+ // Get Ports
+ Set<PortNumber> outputPorts = getPorts(nextObjective.next());
+ // Build relative cps
+ ImmutableSet.Builder<ConnectPoint> cpBuilder = ImmutableSet.builder();
+ outputPorts.forEach(portNumber -> cpBuilder.add(new ConnectPoint(toVisit, portNumber)));
+ Set<ConnectPoint> egressPoints = cpBuilder.build();
+ // Define other variables for the next steps
+ Set<Link> egressLinks;
+ List<ConnectPoint> newCurrentPath;
+ Set<DeviceId> newVisited;
+ DeviceId newToVisit;
+ for (ConnectPoint egressPoint : egressPoints) {
+ egressLinks = srManager.linkService.getEgressLinks(egressPoint);
+ // If it does not have egress links, stop
+ if (egressLinks.isEmpty()) {
+ // Add the connect points to the path
+ newCurrentPath = Lists.newArrayList(currentPath);
+ newCurrentPath.add(0, egressPoint);
+ // Save in the map
+ mcastPaths.put(egressPoint, newCurrentPath);
+ } else {
+ newVisited = Sets.newHashSet(visited);
+ // Iterate over the egress links for the next hops
+ for (Link egressLink : egressLinks) {
+ // Update to visit
+ newToVisit = egressLink.dst().deviceId();
+ // Add the connect points to the path
+ newCurrentPath = Lists.newArrayList(currentPath);
+ newCurrentPath.add(0, egressPoint);
+ newCurrentPath.add(0, egressLink.dst());
+ // Go to the next hop
+ buildMcastPaths(newToVisit, newVisited, mcastPaths, newCurrentPath, mcastIp);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java
new file mode 100644
index 0000000..c9baf93
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2015-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;
+
+/**
+ * Interface for Segment Routing Policy.
+ */
+public interface Policy {
+ /**
+ * Enums for policy type.
+ */
+ enum Type {
+ /**
+ * Tunnel flow policy type.
+ */
+ TUNNEL_FLOW,
+
+ /**
+ * Load balancing policy type.
+ */
+ LOADBALANCE,
+
+ /**
+ * policy to avoid specific routers or links.
+ */
+ AVOID,
+
+ /**
+ * Access Control policy type.
+ */
+ DENY
+ }
+
+ /**
+ * Returns the policy ID.
+ *
+ * @return policy ID
+ */
+ String id();
+
+ /**
+ * Returns the priority of the policy.
+ *
+ * @return priority
+ */
+ int priority();
+
+ /**
+ * Returns the policy type.
+ *
+ * @return policy type
+ */
+ Type type();
+
+ /**
+ * Returns the source IP address of the policy.
+ *
+ * @return source IP address
+ */
+ String srcIp();
+
+ /**
+ * Returns the destination IP address of the policy.
+ *
+ * @return destination IP address
+ */
+ String dstIp();
+
+ /**
+ * Returns the IP protocol of the policy.
+ *
+ * @return IP protocol
+ */
+ String ipProto();
+
+ /**
+ * Returns the source port of the policy.
+ *
+ * @return source port
+ */
+ short srcPort();
+
+ /**
+ * Returns the destination of the policy.
+ *
+ * @return destination port
+ */
+ short dstPort();
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java
new file mode 100644
index 0000000..a341bb1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.TpPort;
+import org.onosproject.cli.net.IpProtocol;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Segment Routing Policy Handler.
+ */
+public class PolicyHandler {
+
+ protected final Logger log = getLogger(getClass());
+
+ private ApplicationId appId;
+ private DeviceConfiguration deviceConfiguration;
+ private FlowObjectiveService flowObjectiveService;
+ private TunnelHandler tunnelHandler;
+ private final EventuallyConsistentMap<String, Policy> policyStore;
+ /**
+ * Result of policy creation.
+ */
+ public enum Result {
+ /**
+ * Success.
+ */
+ SUCCESS,
+
+ /**
+ * The same policy exists already.
+ */
+ POLICY_EXISTS,
+
+ /**
+ * The policy ID exists already.
+ */
+ ID_EXISTS,
+
+ /**
+ * Cannot find associated tunnel.
+ */
+ TUNNEL_NOT_FOUND,
+
+ /**
+ * Policy was not found.
+ */
+ POLICY_NOT_FOUND,
+
+ /**
+ * Policy type {} is not supported yet.
+ */
+ UNSUPPORTED_TYPE
+ }
+
+ /**
+ * Constructs policy handler.
+ *
+ * @param appId segment routing application ID
+ * @param deviceConfiguration DeviceConfiguration reference
+ * @param flowObjectiveService FlowObjectiveService reference
+ * @param tunnelHandler tunnel handler reference
+ * @param policyStore policy store
+ */
+ public PolicyHandler(ApplicationId appId,
+ DeviceConfiguration deviceConfiguration,
+ FlowObjectiveService flowObjectiveService,
+ TunnelHandler tunnelHandler,
+ EventuallyConsistentMap<String, Policy> policyStore) {
+ this.appId = appId;
+ this.deviceConfiguration = deviceConfiguration;
+ this.flowObjectiveService = flowObjectiveService;
+ this.tunnelHandler = tunnelHandler;
+ this.policyStore = policyStore;
+ }
+
+ /**
+ * Returns the policies.
+ *
+ * @return policy list
+ */
+ public List<Policy> getPolicies() {
+ return policyStore.values()
+ .stream()
+ .filter(policy -> policy instanceof TunnelPolicy)
+ .map(policy -> new TunnelPolicy((TunnelPolicy) policy))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Creates a policy using the policy information given.
+ * @param policy policy reference to create
+ * @return ID_EXISTS if the same policy ID exists,
+ * POLICY_EXISTS if the same policy exists, TUNNEL_NOT_FOUND if the tunnel
+ * does not exists, UNSUPPORTED_TYPE if the policy type is not supported,
+ * SUCCESS if the policy is created successfully
+ */
+ public Result createPolicy(Policy policy) {
+
+ if (policyStore.containsKey(policy.id())) {
+ log.warn("The policy id {} exists already", policy.id());
+ return Result.ID_EXISTS;
+ }
+
+ if (policyStore.containsValue(policy)) {
+ log.warn("The same policy exists already");
+ return Result.POLICY_EXISTS;
+ }
+
+ if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+
+ TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+ Tunnel tunnel = tunnelHandler.getTunnel(tunnelPolicy.tunnelId());
+ if (tunnel == null) {
+ return Result.TUNNEL_NOT_FOUND;
+ }
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+ .builder()
+ .fromApp(appId)
+ .makePermanent()
+ .nextStep(tunnel.groupId())
+ .withPriority(tunnelPolicy.priority())
+ .withSelector(buildSelector(policy))
+ .withFlag(ForwardingObjective.Flag.VERSATILE);
+
+ DeviceId source = deviceConfiguration.getDeviceId(tunnel.labelIds().get(0));
+ flowObjectiveService.forward(source, fwdBuilder.add());
+
+ } else {
+ log.warn("Policy type {} is not supported yet.", policy.type());
+ return Result.UNSUPPORTED_TYPE;
+ }
+
+ policyStore.put(policy.id(), policy);
+
+ return Result.SUCCESS;
+ }
+
+ /**
+ * Removes the policy given.
+ *
+ * @param policyInfo policy information to remove
+ * @return POLICY_NOT_FOUND if the policy to remove does not exists,
+ * SUCCESS if it is removed successfully
+ */
+ public Result removePolicy(Policy policyInfo) {
+
+ if (policyStore.get(policyInfo.id()) != null) {
+ Policy policy = policyStore.get(policyInfo.id());
+ if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+ TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+ Tunnel tunnel = tunnelHandler.getTunnel(tunnelPolicy.tunnelId());
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+ .builder()
+ .fromApp(appId)
+ .makePermanent()
+ .withSelector(buildSelector(policy))
+ .withPriority(tunnelPolicy.priority())
+ .nextStep(tunnel.groupId())
+ .withFlag(ForwardingObjective.Flag.VERSATILE);
+
+ DeviceId source = deviceConfiguration.getDeviceId(tunnel.labelIds().get(0));
+ flowObjectiveService.forward(source, fwdBuilder.remove());
+
+ policyStore.remove(policyInfo.id());
+ }
+ } else {
+ log.warn("Policy {} was not found", policyInfo.id());
+ return Result.POLICY_NOT_FOUND;
+ }
+
+ return Result.SUCCESS;
+ }
+
+
+ private TrafficSelector buildSelector(Policy policy) {
+
+ TrafficSelector.Builder tsb = DefaultTrafficSelector.builder();
+ tsb.matchEthType(Ethernet.TYPE_IPV4);
+ if (policy.dstIp() != null && !policy.dstIp().isEmpty()) {
+ tsb.matchIPDst(IpPrefix.valueOf(policy.dstIp()));
+ }
+ if (policy.srcIp() != null && !policy.srcIp().isEmpty()) {
+ tsb.matchIPSrc(IpPrefix.valueOf(policy.srcIp()));
+ }
+ if (policy.ipProto() != null && !policy.ipProto().isEmpty()) {
+ Short ipProto = IpProtocol.valueOf(policy.ipProto()).value();
+ tsb.matchIPProtocol(ipProto.byteValue());
+ if (IpProtocol.valueOf(policy.ipProto()).equals(IpProtocol.TCP)) {
+ if (policy.srcPort() != 0) {
+ tsb.matchTcpSrc(TpPort.tpPort(policy.srcPort()));
+ }
+ if (policy.dstPort() != 0) {
+ tsb.matchTcpDst(TpPort.tpPort(policy.dstPort()));
+ }
+ } else if (IpProtocol.valueOf(policy.ipProto()).equals(IpProtocol.UDP)) {
+ if (policy.srcPort() != 0) {
+ tsb.matchUdpSrc(TpPort.tpPort(policy.srcPort()));
+ }
+ if (policy.dstPort() != 0) {
+ tsb.matchUdpDst(TpPort.tpPort(policy.dstPort()));
+ }
+ }
+ }
+
+ return tsb.build();
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java
new file mode 100644
index 0000000..2127019
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java
@@ -0,0 +1,357 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+
+import com.google.common.base.Objects;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.config.BlockedPortsConfig;
+import org.onosproject.utils.Comparators;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Keeps track of ports that have been configured for blocking,
+ * and their current authentication state.
+ */
+public class PortAuthTracker {
+
+ private static final Logger log = getLogger(PortAuthTracker.class);
+
+ private Map<DeviceId, Map<PortNumber, BlockState>> blockedPorts = new HashMap<>();
+ private Map<DeviceId, Map<PortNumber, BlockState>> oldMap;
+
+ @Override
+ public String toString() {
+ return "PortAuthTracker{entries = " + blockedPorts.size() + "}";
+ }
+
+ /**
+ * Changes the state of the given device id / port number pair to the
+ * specified state.
+ *
+ * @param d device identifier
+ * @param p port number
+ * @param newState the updated state
+ * @return true, if the state changed from what was previously mapped
+ */
+ private boolean changeStateTo(DeviceId d, PortNumber p, BlockState newState) {
+ Map<PortNumber, BlockState> portMap =
+ blockedPorts.computeIfAbsent(d, k -> new HashMap<>());
+ BlockState oldState =
+ portMap.computeIfAbsent(p, k -> BlockState.UNCHECKED);
+ portMap.put(p, newState);
+ return (oldState != newState);
+ }
+
+ /**
+ * Radius has authorized the supplicant at this connect point. If
+ * we are tracking this port, clear the blocking flow and mark the
+ * port as authorized.
+ *
+ * @param connectPoint supplicant connect point
+ */
+ void radiusAuthorize(ConnectPoint connectPoint) {
+ DeviceId d = connectPoint.deviceId();
+ PortNumber p = connectPoint.port();
+ if (configured(d, p)) {
+ clearBlockingFlow(d, p);
+ markAsAuthenticated(d, p);
+ }
+ }
+
+ /**
+ * Supplicant at specified connect point has logged off Radius. If
+ * we are tracking this port, install a blocking flow and mark the
+ * port as blocked.
+ *
+ * @param connectPoint supplicant connect point
+ */
+ void radiusLogoff(ConnectPoint connectPoint) {
+ DeviceId d = connectPoint.deviceId();
+ PortNumber p = connectPoint.port();
+ if (configured(d, p)) {
+ installBlockingFlow(d, p);
+ markAsBlocked(d, p);
+ }
+ }
+
+ /**
+ * Marks the specified device/port as blocked.
+ *
+ * @param d device id
+ * @param p port number
+ * @return true if the state changed (was not already blocked)
+ */
+ private boolean markAsBlocked(DeviceId d, PortNumber p) {
+ return changeStateTo(d, p, BlockState.BLOCKED);
+ }
+
+ /**
+ * Marks the specified device/port as authenticated.
+ *
+ * @param d device id
+ * @param p port number
+ * @return true if the state changed (was not already authenticated)
+ */
+ private boolean markAsAuthenticated(DeviceId d, PortNumber p) {
+ return changeStateTo(d, p, BlockState.AUTHENTICATED);
+ }
+
+ /**
+ * Returns true if the given device/port are configured for blocking.
+ *
+ * @param d device id
+ * @param p port number
+ * @return true if this device/port configured for blocking
+ */
+ private boolean configured(DeviceId d, PortNumber p) {
+ Map<PortNumber, BlockState> portMap = blockedPorts.get(d);
+ return portMap != null && portMap.get(p) != null;
+ }
+
+ private BlockState whatState(DeviceId d, PortNumber p,
+ Map<DeviceId, Map<PortNumber, BlockState>> m) {
+ Map<PortNumber, BlockState> portMap = m.get(d);
+ if (portMap == null) {
+ return BlockState.UNCHECKED;
+ }
+ BlockState state = portMap.get(p);
+ if (state == null) {
+ return BlockState.UNCHECKED;
+ }
+ return state;
+ }
+
+ /**
+ * Returns the current state of the given device/port.
+ *
+ * @param d device id
+ * @param p port number
+ * @return current block-state
+ */
+ BlockState currentState(DeviceId d, PortNumber p) {
+ return whatState(d, p, blockedPorts);
+ }
+
+ /**
+ * Returns the current state of the given connect point.
+ *
+ * @param cp connect point
+ * @return current block-state
+ */
+
+ BlockState currentState(ConnectPoint cp) {
+ return whatState(cp.deviceId(), cp.port(), blockedPorts);
+ }
+
+ /**
+ * Returns the number of entries being tracked.
+ *
+ * @return the number of tracked entries
+ */
+ int entryCount() {
+ int count = 0;
+ for (Map<PortNumber, BlockState> m : blockedPorts.values()) {
+ count += m.size();
+ }
+ return count;
+ }
+
+ /**
+ * Returns the previously recorded state of the given device/port.
+ *
+ * @param d device id
+ * @param p port number
+ * @return previous block-state
+ */
+ private BlockState oldState(DeviceId d, PortNumber p) {
+ return whatState(d, p, oldMap);
+ }
+
+ private void configurePort(DeviceId d, PortNumber p) {
+ boolean alreadyAuthenticated =
+ oldState(d, p) == BlockState.AUTHENTICATED;
+
+ if (alreadyAuthenticated) {
+ clearBlockingFlow(d, p);
+ markAsAuthenticated(d, p);
+ } else {
+ installBlockingFlow(d, p);
+ markAsBlocked(d, p);
+ }
+ log.info("Configuring port {}/{} as {}", d, p,
+ alreadyAuthenticated ? "AUTHENTICATED" : "BLOCKED");
+ }
+
+ private boolean notInMap(DeviceId deviceId, PortNumber portNumber) {
+ Map<PortNumber, BlockState> m = blockedPorts.get(deviceId);
+ return m == null || m.get(portNumber) == null;
+ }
+
+ private void logPortsNoLongerBlocked() {
+ for (Map.Entry<DeviceId, Map<PortNumber, BlockState>> entry :
+ oldMap.entrySet()) {
+ DeviceId d = entry.getKey();
+ Map<PortNumber, BlockState> portMap = entry.getValue();
+
+ for (PortNumber p : portMap.keySet()) {
+ if (notInMap(d, p)) {
+ clearBlockingFlow(d, p);
+ log.info("De-configuring port {}/{} (UNCHECKED)", d, p);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Reconfigures the port tracker using the supplied configuration.
+ *
+ * @param cfg the new configuration
+ */
+ void configurePortBlocking(BlockedPortsConfig cfg) {
+ // remember the old map; prepare a new map
+ oldMap = blockedPorts;
+ blockedPorts = new HashMap<>();
+
+ // for each configured device, add configured ports to map
+ for (String devId : cfg.deviceIds()) {
+ cfg.portIterator(devId)
+ .forEachRemaining(p -> configurePort(deviceId(devId),
+ portNumber(p)));
+ }
+
+ // have we de-configured any ports?
+ logPortsNoLongerBlocked();
+
+ // allow old map to be garbage collected
+ oldMap = null;
+ }
+
+ private List<PortAuthState> reportPortsAuthState() {
+ List<PortAuthState> result = new ArrayList<>();
+
+ for (Map.Entry<DeviceId, Map<PortNumber, BlockState>> entry :
+ blockedPorts.entrySet()) {
+ DeviceId d = entry.getKey();
+ Map<PortNumber, BlockState> portMap = entry.getValue();
+
+ for (PortNumber p : portMap.keySet()) {
+ result.add(new PortAuthState(d, p, portMap.get(p)));
+ }
+ }
+ Collections.sort(result);
+ return result;
+ }
+
+ /**
+ * Installs a "blocking" flow for device/port specified.
+ *
+ * @param d device id
+ * @param p port number
+ */
+ void installBlockingFlow(DeviceId d, PortNumber p) {
+ log.debug("Installing Blocking Flow at {}/{}", d, p);
+ // TODO: invoke SegmentRoutingService.block(...) appropriately
+ log.info("TODO >> Installing Blocking Flow at {}/{}", d, p);
+ }
+
+ /**
+ * Removes the "blocking" flow from device/port specified.
+ *
+ * @param d device id
+ * @param p port number
+ */
+ void clearBlockingFlow(DeviceId d, PortNumber p) {
+ log.debug("Clearing Blocking Flow from {}/{}", d, p);
+ // TODO: invoke SegmentRoutingService.block(...) appropriately
+ log.info("TODO >> Clearing Blocking Flow from {}/{}", d, p);
+ }
+
+
+ /**
+ * Designates the state of a given port. One of:
+ * <ul>
+ * <li> UNCHECKED: not configured for blocking </li>
+ * <li> BLOCKED: configured for blocking, and not yet authenticated </li>
+ * <li> AUTHENTICATED: configured for blocking, but authenticated </li>
+ * </ul>
+ */
+ public enum BlockState {
+ UNCHECKED,
+ BLOCKED,
+ AUTHENTICATED
+ }
+
+ /**
+ * A simple DTO binding of device identifier, port number, and block state.
+ */
+ public static final class PortAuthState implements Comparable<PortAuthState> {
+ private final DeviceId d;
+ private final PortNumber p;
+ private final BlockState s;
+
+ private PortAuthState(DeviceId d, PortNumber p, BlockState s) {
+ this.d = d;
+ this.p = p;
+ this.s = s;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(d) + "/" + p + " -- " + s;
+ }
+
+ @Override
+ public int compareTo(PortAuthState o) {
+ // NOTE: only compare against "deviceid/port"
+ int result = Comparators.ELEMENT_ID_COMPARATOR.compare(d, o.d);
+ return (result != 0) ? result : Long.signum(p.toLong() - o.p.toLong());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final PortAuthTracker.PortAuthState that = (PortAuthTracker.PortAuthState) obj;
+
+ return Comparators.ELEMENT_ID_COMPARATOR.compare(this.d, that.d) == 0 &&
+ p.toLong() == that.p.toLong();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(this.d, this.p);
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
new file mode 100644
index 0000000..21870ee
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalCause;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.Sets;
+
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.net.DeviceId;
+import org.onosproject.routeservice.RouteInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Handles RouteEvent and manages routing entries.
+ */
+public class RouteHandler {
+ private static final Logger log = LoggerFactory.getLogger(RouteHandler.class);
+ private final SegmentRoutingManager srManager;
+
+ private static final int WAIT_TIME_MS = 1000;
+ /**
+ * The routeEventCache is implemented to avoid race condition by giving more time to the
+ * underlying flow subsystem to process previous populateSubnet call.
+ */
+ private Cache<IpPrefix, RouteEvent> routeEventCache = CacheBuilder.newBuilder()
+ .expireAfterWrite(WAIT_TIME_MS, TimeUnit.MILLISECONDS)
+ .removalListener((RemovalNotification<IpPrefix, RouteEvent> notification) -> {
+ IpPrefix prefix = notification.getKey();
+ RouteEvent routeEvent = notification.getValue();
+ RemovalCause cause = notification.getCause();
+ log.debug("routeEventCache removal event. prefix={}, routeEvent={}, cause={}",
+ prefix, routeEvent, cause);
+
+ switch (notification.getCause()) {
+ case REPLACED:
+ case EXPIRED:
+ dequeueRouteEvent(routeEvent);
+ break;
+ default:
+ break;
+ }
+ }).build();
+
+ RouteHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+
+ Executors.newSingleThreadScheduledExecutor()
+ .scheduleAtFixedRate(routeEventCache::cleanUp, 0, WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ }
+
+ protected void init(DeviceId deviceId) {
+ srManager.routeService.getRouteTables().forEach(routeTableId ->
+ srManager.routeService.getRoutes(routeTableId).forEach(routeInfo ->
+ routeInfo.allRoutes().forEach(resolvedRoute ->
+ srManager.nextHopLocations(resolvedRoute).stream()
+ .filter(location -> deviceId.equals(location.deviceId()))
+ .forEach(location -> processRouteAddedInternal(resolvedRoute)
+ )
+ )
+ )
+ );
+ }
+
+ void processRouteAdded(RouteEvent event) {
+ enqueueRouteEvent(event);
+ }
+
+ private void processRouteAddedInternal(ResolvedRoute route) {
+ processRouteAddedInternal(Sets.newHashSet(route));
+ }
+
+ private void processRouteAddedInternal(Collection<ResolvedRoute> routes) {
+ if (!isReady()) {
+ log.info("System is not ready. Skip adding route for {}", routes);
+ return;
+ }
+
+ log.info("processRouteAddedInternal. routes={}", routes);
+
+ Set<ConnectPoint> allLocations = Sets.newHashSet();
+ Set<IpPrefix> allPrefixes = Sets.newHashSet();
+ routes.forEach(route -> {
+ allLocations.addAll(srManager.nextHopLocations(route));
+ allPrefixes.add(route.prefix());
+ });
+ log.debug("RouteAdded. populateSubnet {}, {}", allLocations, allPrefixes);
+ srManager.defaultRoutingHandler.populateSubnet(allLocations, allPrefixes);
+
+ routes.forEach(route -> {
+ IpPrefix prefix = route.prefix();
+ MacAddress nextHopMac = route.nextHopMac();
+ VlanId nextHopVlan = route.nextHopVlan();
+ Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+ locations.forEach(location -> {
+ log.debug("RouteAdded. addSubnet {}, {}", location, prefix);
+ srManager.deviceConfiguration.addSubnet(location, prefix);
+ log.debug("RouteAdded populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+ srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
+ nextHopMac, nextHopVlan, location.port());
+ });
+ });
+ }
+
+ void processRouteUpdated(RouteEvent event) {
+ enqueueRouteEvent(event);
+ }
+
+ void processAlternativeRoutesChanged(RouteEvent event) {
+ enqueueRouteEvent(event);
+ }
+
+ private void processRouteUpdatedInternal(Set<ResolvedRoute> routes, Set<ResolvedRoute> oldRoutes) {
+ if (!isReady()) {
+ log.info("System is not ready. Skip updating route for {} -> {}", oldRoutes, routes);
+ return;
+ }
+
+ log.info("processRouteUpdatedInternal. routes={}, oldRoutes={}", routes, oldRoutes);
+
+ Set<ConnectPoint> allLocations = Sets.newHashSet();
+ Set<IpPrefix> allPrefixes = Sets.newHashSet();
+ routes.forEach(route -> {
+ allLocations.addAll(srManager.nextHopLocations(route));
+ allPrefixes.add(route.prefix());
+ });
+ log.debug("RouteUpdated. populateSubnet {}, {}", allLocations, allPrefixes);
+ srManager.defaultRoutingHandler.populateSubnet(allLocations, allPrefixes);
+
+ Set<ResolvedRoute> toBeRemoved = Sets.difference(oldRoutes, routes).immutableCopy();
+ Set<ResolvedRoute> toBeAdded = Sets.difference(routes, oldRoutes).immutableCopy();
+
+ toBeRemoved.forEach(route -> {
+ srManager.nextHopLocations(route).forEach(oldLocation -> {
+ if (toBeAdded.stream().map(srManager::nextHopLocations)
+ .flatMap(Set::stream).map(ConnectPoint::deviceId)
+ .noneMatch(deviceId -> deviceId.equals(oldLocation.deviceId()))) {
+ IpPrefix prefix = route.prefix();
+ log.debug("RouteUpdated. removeSubnet {}, {}", oldLocation, prefix);
+ srManager.deviceConfiguration.removeSubnet(oldLocation, prefix);
+ // We don't remove the flow on the old location in occasion of two next hops becoming one
+ // since the populateSubnet will point the old location to the new location via spine.
+ }
+ });
+ });
+
+ toBeAdded.forEach(route -> {
+ IpPrefix prefix = route.prefix();
+ MacAddress nextHopMac = route.nextHopMac();
+ VlanId nextHopVlan = route.nextHopVlan();
+ Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+ locations.forEach(location -> {
+ log.debug("RouteUpdated. addSubnet {}, {}", location, prefix);
+ srManager.deviceConfiguration.addSubnet(location, prefix);
+ log.debug("RouteUpdated. populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+ srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
+ nextHopMac, nextHopVlan, location.port());
+ });
+ });
+ }
+
+ void processRouteRemoved(RouteEvent event) {
+ enqueueRouteEvent(event);
+ }
+
+ private void processRouteRemovedInternal(Collection<ResolvedRoute> routes) {
+ if (!isReady()) {
+ log.info("System is not ready. Skip removing route for {}", routes);
+ return;
+ }
+
+ log.info("processRouteRemovedInternal. routes={}", routes);
+
+ Set<IpPrefix> allPrefixes = Sets.newHashSet();
+ routes.forEach(route -> {
+ allPrefixes.add(route.prefix());
+ });
+ log.debug("RouteRemoved. revokeSubnet {}", allPrefixes);
+ srManager.defaultRoutingHandler.revokeSubnet(allPrefixes);
+
+ routes.forEach(route -> {
+ IpPrefix prefix = route.prefix();
+ MacAddress nextHopMac = route.nextHopMac();
+ VlanId nextHopVlan = route.nextHopVlan();
+ Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+ locations.forEach(location -> {
+ log.debug("RouteRemoved. removeSubnet {}, {}", location, prefix);
+ srManager.deviceConfiguration.removeSubnet(location, prefix);
+ log.debug("RouteRemoved. revokeRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+ srManager.defaultRoutingHandler.revokeRoute(location.deviceId(), prefix,
+ nextHopMac, nextHopVlan, location.port());
+
+ // Also remove redirection flows on the pair device if exists.
+ Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
+ Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(location.deviceId());
+ if (pairDeviceId.isPresent() && pairLocalPort.isPresent()) {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(nextHopVlan);
+
+ log.debug("RouteRemoved. revokeRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+ srManager.defaultRoutingHandler.revokeRoute(pairDeviceId.get(), prefix,
+ nextHopMac, vlanId, pairLocalPort.get());
+ }
+ });
+ });
+ }
+
+ void processHostMovedEvent(HostEvent event) {
+ log.info("processHostMovedEvent {}", event);
+ MacAddress hostMac = event.subject().mac();
+ VlanId hostVlanId = event.subject().vlan();
+
+ affectedRoutes(hostMac, hostVlanId).forEach(affectedRoute -> {
+ IpPrefix prefix = affectedRoute.prefix();
+ Set<HostLocation> prevLocations = event.prevSubject().locations();
+ Set<HostLocation> newLocations = event.subject().locations();
+
+ // For each old location
+ Sets.difference(prevLocations, newLocations).stream().filter(srManager::isMasterOf)
+ .forEach(prevLocation -> {
+ // Redirect the flows to pair link if configured
+ // Note: Do not continue removing any rule
+ Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(prevLocation.deviceId());
+ Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(prevLocation.deviceId());
+ if (pairDeviceId.isPresent() && pairLocalPort.isPresent() && newLocations.stream()
+ .anyMatch(location -> location.deviceId().equals(pairDeviceId.get()))) {
+ // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+ // when the host is untagged
+ VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(prevLocation)).orElse(hostVlanId);
+ log.debug("HostMoved. populateRoute {}, {}, {}, {}", prevLocation, prefix, hostMac, vlanId);
+ srManager.defaultRoutingHandler.populateRoute(prevLocation.deviceId(), prefix,
+ hostMac, vlanId, pairLocalPort.get());
+ return;
+ }
+
+ // No pair information supplied. Remove route
+ log.debug("HostMoved. revokeRoute {}, {}, {}, {}", prevLocation, prefix, hostMac, hostVlanId);
+ srManager.defaultRoutingHandler.revokeRoute(prevLocation.deviceId(), prefix,
+ hostMac, hostVlanId, prevLocation.port());
+ });
+
+ // For each new location, add all new IPs.
+ Sets.difference(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+ .forEach(newLocation -> {
+ log.debug("HostMoved. populateRoute {}, {}, {}, {}", newLocation, prefix, hostMac, hostVlanId);
+ srManager.defaultRoutingHandler.populateRoute(newLocation.deviceId(), prefix,
+ hostMac, hostVlanId, newLocation.port());
+ });
+
+ });
+ }
+
+ private Set<ResolvedRoute> affectedRoutes(MacAddress mac, VlanId vlanId) {
+ return srManager.routeService.getRouteTables().stream()
+ .map(routeTableId -> srManager.routeService.getRoutes(routeTableId))
+ .flatMap(Collection::stream)
+ .map(RouteInfo::allRoutes)
+ .flatMap(Collection::stream)
+ .filter(resolvedRoute -> mac.equals(resolvedRoute.nextHopMac()) &&
+ vlanId.equals(resolvedRoute.nextHopVlan())).collect(Collectors.toSet());
+ }
+
+ private boolean isReady() {
+ return Objects.nonNull(srManager.deviceConfiguration) &&
+ Objects.nonNull(srManager.defaultRoutingHandler);
+ }
+
+ void enqueueRouteEvent(RouteEvent routeEvent) {
+ log.debug("Enqueue routeEvent {}", routeEvent);
+ routeEventCache.put(routeEvent.subject().prefix(), routeEvent);
+ }
+
+ void dequeueRouteEvent(RouteEvent routeEvent) {
+ log.debug("Dequeue routeEvent {}", routeEvent);
+ switch (routeEvent.type()) {
+ case ROUTE_ADDED:
+ processRouteAddedInternal(routeEvent.alternatives());
+ break;
+ case ROUTE_REMOVED:
+ processRouteRemovedInternal(routeEvent.alternatives());
+ break;
+ case ROUTE_UPDATED:
+ case ALTERNATIVE_ROUTES_CHANGED:
+ processRouteUpdatedInternal(Sets.newHashSet(routeEvent.alternatives()),
+ Sets.newHashSet(routeEvent.prevAlternatives()));
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
new file mode 100644
index 0000000..f37caea
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
@@ -0,0 +1,1250 @@
+/*
+ * Copyright 2015-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;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.onlab.packet.EthType;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.segmentrouting.DefaultRoutingHandler.PortFilterInfo;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+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.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective.Builder;
+import org.onosproject.net.flowobjective.ForwardingObjective.Flag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.Ethernet.TYPE_ARP;
+import static org.onlab.packet.Ethernet.TYPE_IPV6;
+import static org.onlab.packet.ICMP6.NEIGHBOR_ADVERTISEMENT;
+import static org.onlab.packet.ICMP6.NEIGHBOR_SOLICITATION;
+import static org.onlab.packet.ICMP6.ROUTER_ADVERTISEMENT;
+import static org.onlab.packet.ICMP6.ROUTER_SOLICITATION;
+import static org.onlab.packet.IPv6.PROTOCOL_ICMP6;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+
+/**
+ * Populator of segment routing flow rules.
+ */
+public class RoutingRulePopulator {
+ private static final Logger log = LoggerFactory.getLogger(RoutingRulePopulator.class);
+
+ private static final int ARP_NDP_PRIORITY = 30000;
+
+ private AtomicLong rulePopulationCounter;
+ private SegmentRoutingManager srManager;
+ private DeviceConfiguration config;
+
+ /**
+ * Creates a RoutingRulePopulator object.
+ *
+ * @param srManager segment routing manager reference
+ */
+ RoutingRulePopulator(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.config = checkNotNull(srManager.deviceConfiguration);
+ this.rulePopulationCounter = new AtomicLong(0);
+ }
+
+ /**
+ * Resets the population counter.
+ */
+ void resetCounter() {
+ rulePopulationCounter.set(0);
+ }
+
+ /**
+ * Returns the number of rules populated.
+ *
+ * @return number of rules
+ */
+ long getCounter() {
+ return rulePopulationCounter.get();
+ }
+
+ /**
+ * Populates IP rules for a route that has direct connection to the
+ * switch.
+ *
+ * @param deviceId device ID of the device that next hop attaches to
+ * @param prefix IP prefix of the route
+ * @param hostMac MAC address of the next hop
+ * @param hostVlanId Vlan ID of the nexthop
+ * @param outPort port where the next hop attaches to
+ */
+ void populateRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ log.debug("Populate direct routing entry for route {} at {}:{}",
+ prefix, deviceId, outPort);
+ ForwardingObjective.Builder fwdBuilder;
+ try {
+ fwdBuilder = routingFwdObjBuilder(deviceId, prefix, hostMac,
+ hostVlanId, outPort, false);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting direct populateRoute");
+ return;
+ }
+ if (fwdBuilder == null) {
+ log.warn("Aborting host routing table entry due "
+ + "to error for dev:{} route:{}", deviceId, prefix);
+ return;
+ }
+
+ int nextId = fwdBuilder.add().nextId();
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Direct routing rule for route {} populated. nextId={}",
+ prefix, nextId),
+ (objective, error) ->
+ log.warn("Failed to populate direct routing rule for route {}: {}",
+ prefix, error));
+ srManager.flowObjectiveService.forward(deviceId, fwdBuilder.add(context));
+ rulePopulationCounter.incrementAndGet();
+ }
+
+ /**
+ * Removes IP rules for a route when the next hop is gone.
+ *
+ * @param deviceId device ID of the device that next hop attaches to
+ * @param prefix IP prefix of the route
+ * @param hostMac MAC address of the next hop
+ * @param hostVlanId Vlan ID of the nexthop
+ * @param outPort port that next hop attaches to
+ */
+ void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ log.debug("Revoke IP table entry for route {} at {}:{}",
+ prefix, deviceId, outPort);
+ ForwardingObjective.Builder fwdBuilder;
+ try {
+ fwdBuilder = routingFwdObjBuilder(deviceId, prefix, hostMac,
+ hostVlanId, outPort, true);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting revokeIpRuleForHost.");
+ return;
+ }
+ if (fwdBuilder == null) {
+ log.warn("Aborting host routing table entries due "
+ + "to error for dev:{} route:{}", deviceId, prefix);
+ return;
+ }
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("IP rule for route {} revoked", prefix),
+ (objective, error) ->
+ log.warn("Failed to revoke IP rule for route {}: {}", prefix, error));
+ srManager.flowObjectiveService.forward(deviceId, fwdBuilder.remove(context));
+ }
+
+ /**
+ * Returns a forwarding objective builder for routing rules.
+ * <p>
+ * The forwarding objective routes packets destined to a given prefix to
+ * given port on given device with given destination MAC.
+ *
+ * @param deviceId device ID
+ * @param prefix prefix that need to be routed
+ * @param hostMac MAC address of the nexthop
+ * @param hostVlanId Vlan ID of the nexthop
+ * @param outPort port where the nexthop attaches to
+ * @param revoke true if forwarding objective is meant to revoke forwarding rule
+ * @return forwarding objective builder
+ * @throws DeviceConfigNotFoundException if given device is not configured
+ */
+ private ForwardingObjective.Builder routingFwdObjBuilder(
+ DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort,
+ boolean revoke)
+ throws DeviceConfigNotFoundException {
+ MacAddress deviceMac;
+ deviceMac = config.getDeviceMac(deviceId);
+
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, outPort);
+ VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+ Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+ VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+ // Create route selector
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(prefix);
+
+ // Create route treatment
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.deferred()
+ .setEthDst(hostMac)
+ .setEthSrc(deviceMac)
+ .setOutput(outPort);
+
+ // Create route meta
+ TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+ // Adjust the meta according to VLAN configuration
+ if (taggedVlans.contains(hostVlanId)) {
+ tbuilder.setVlanId(hostVlanId);
+ } else if (hostVlanId.equals(VlanId.NONE)) {
+ if (untaggedVlan != null) {
+ mbuilder.matchVlanId(untaggedVlan);
+ } else if (nativeVlan != null) {
+ mbuilder.matchVlanId(nativeVlan);
+ } else {
+ log.warn("Untagged nexthop {}/{} is not allowed on {} without untagged or native vlan",
+ hostMac, hostVlanId, connectPoint);
+ return null;
+ }
+ } else {
+ log.warn("Tagged nexthop {}/{} is not allowed on {} without VLAN listed"
+ + " in tagged vlan", hostMac, hostVlanId, connectPoint);
+ return null;
+ }
+ // if the objective is to revoke an existing rule, and for some reason
+ // the next-objective does not exist, then a new one should not be created
+ int portNextObjId = srManager.getPortNextObjectiveId(deviceId, outPort,
+ tbuilder.build(), mbuilder.build(), !revoke);
+ if (portNextObjId == -1) {
+ // Warning log will come from getPortNextObjective method
+ return null;
+ }
+
+ return DefaultForwardingObjective.builder()
+ .withSelector(sbuilder.build())
+ .nextStep(portNextObjId)
+ .fromApp(srManager.appId).makePermanent()
+ .withPriority(getPriorityFromPrefix(prefix))
+ .withFlag(ForwardingObjective.Flag.SPECIFIC);
+ }
+
+ /**
+ * Populates IP flow rules for all the given prefixes reachable from the
+ * destination switch(es).
+ *
+ * @param targetSw switch where rules are to be programmed
+ * @param subnets subnets/prefixes being added
+ * @param destSw1 destination switch where the prefixes are reachable
+ * @param destSw2 paired destination switch if one exists for the subnets/prefixes.
+ * Should be null if there is no paired destination switch (by config)
+ * or if the given prefixes are reachable only via destSw1
+ * @param nextHops a map containing a set of next-hops for each destination switch.
+ * If destSw2 is not null, then this map must contain an
+ * entry for destSw2 with its next-hops from the targetSw
+ * (although the next-hop set may be empty in certain scenarios).
+ * If destSw2 is null, there should not be an entry in this
+ * map for destSw2.
+ * @return true if all rules are set successfully, false otherwise
+ */
+ boolean populateIpRuleForSubnet(DeviceId targetSw, Set<IpPrefix> subnets,
+ DeviceId destSw1, DeviceId destSw2, Map<DeviceId, Set<DeviceId>> nextHops) {
+ for (IpPrefix subnet : subnets) {
+ if (!populateIpRuleForRouter(targetSw, subnet, destSw1, destSw2, nextHops)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Revokes IP flow rules for the subnets in each edge switch.
+ *
+ * @param subnets subnet being removed
+ * @return true if all rules are removed successfully, false otherwise
+ */
+ boolean revokeIpRuleForSubnet(Set<IpPrefix> subnets) {
+ for (IpPrefix subnet : subnets) {
+ if (!revokeIpRuleForRouter(subnet)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Populates IP flow rules for an IP prefix in the target device. The prefix
+ * is reachable via destination device(s).
+ *
+ * @param targetSw target device ID to set the rules
+ * @param ipPrefix the IP prefix
+ * @param destSw1 destination switch where the prefixes are reachable
+ * @param destSw2 paired destination switch if one exists for the subnets/prefixes.
+ * Should be null if there is no paired destination switch (by config)
+ * or if the given prefixes are reachable only via destSw1
+ * @param nextHops map of destination switches and their next-hops.
+ * Should only contain destination switches that are
+ * actually meant to be routed to. If destSw2 is null, there
+ * should not be an entry for destSw2 in this map.
+ * @return true if all rules are set successfully, false otherwise
+ */
+ private boolean populateIpRuleForRouter(DeviceId targetSw,
+ IpPrefix ipPrefix, DeviceId destSw1,
+ DeviceId destSw2,
+ Map<DeviceId, Set<DeviceId>> nextHops) {
+ int segmentId1, segmentId2 = -1;
+ try {
+ if (ipPrefix.isIp4()) {
+ segmentId1 = config.getIPv4SegmentId(destSw1);
+ if (destSw2 != null) {
+ segmentId2 = config.getIPv4SegmentId(destSw2);
+ }
+ } else {
+ segmentId1 = config.getIPv6SegmentId(destSw1);
+ if (destSw2 != null) {
+ segmentId2 = config.getIPv6SegmentId(destSw2);
+ }
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting populateIpRuleForRouter.");
+ return false;
+ }
+
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(ipPrefix);
+ TrafficSelector selector = sbuilder.build();
+
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ DestinationSet ds;
+ TrafficTreatment treatment;
+
+ // If the next hop is the same as the final destination, then MPLS label
+ // is not set.
+ /*if (nextHops.size() == 1 && nextHops.toArray()[0].equals(destSw)) {
+ tbuilder.immediate().decNwTtl();
+ ds = new DestinationSet(false, destSw);
+ treatment = tbuilder.build();
+ } else {
+ ds = new DestinationSet(false, segmentId, destSw);
+ treatment = null;
+ }*/
+ if (destSw2 == null) {
+ // single dst - create destination set based on next-hop
+ Set<DeviceId> nhd1 = nextHops.get(destSw1);
+ if (nhd1.size() == 1 && nhd1.iterator().next().equals(destSw1)) {
+ tbuilder.immediate().decNwTtl();
+ ds = new DestinationSet(false, destSw1);
+ treatment = tbuilder.build();
+ } else {
+ ds = new DestinationSet(false, segmentId1, destSw1);
+ treatment = null;
+ }
+ } else {
+ // dst pair - IP rules for dst-pairs are always from other edge nodes
+ // the destination set needs to have both destinations, even if there
+ // are no next hops to one of them
+ ds = new DestinationSet(false, segmentId1, destSw1, segmentId2, destSw2);
+ treatment = null;
+ }
+
+ // setup metadata to pass to nextObjective - indicate the vlan on egress
+ // if needed by the switch pipeline. Since neighbor sets are always to
+ // other neighboring routers, there is no subnet assigned on those ports.
+ TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder(selector);
+ metabuilder.matchVlanId(SegmentRoutingManager.INTERNAL_VLAN);
+ DefaultGroupHandler grpHandler = srManager.getGroupHandler(targetSw);
+ if (grpHandler == null) {
+ log.warn("populateIPRuleForRouter: groupHandler for device {} "
+ + "not found", targetSw);
+ return false;
+ }
+
+ int nextId = grpHandler.getNextObjectiveId(ds, nextHops,
+ metabuilder.build(), true);
+ if (nextId <= 0) {
+ log.warn("No next objective in {} for ds: {}", targetSw, ds);
+ return false;
+ }
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+ .builder()
+ .fromApp(srManager.appId)
+ .makePermanent()
+ .nextStep(nextId)
+ .withSelector(selector)
+ .withPriority(getPriorityFromPrefix(ipPrefix))
+ .withFlag(ForwardingObjective.Flag.SPECIFIC);
+ if (treatment != null) {
+ fwdBuilder.withTreatment(treatment);
+ }
+ log.debug("Installing IPv4 forwarding objective for router IP/subnet {} "
+ + "in switch {} with nextId: {}", ipPrefix, targetSw, nextId);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("IP rule for router {} populated in dev:{}",
+ ipPrefix, targetSw),
+ (objective, error) ->
+ log.warn("Failed to populate IP rule for router {}: {} in dev:{}",
+ ipPrefix, error, targetSw));
+ srManager.flowObjectiveService.forward(targetSw, fwdBuilder.add(context));
+ rulePopulationCounter.incrementAndGet();
+
+ return true;
+ }
+
+ /**
+ * Revokes IP flow rules for the router IP address.
+ *
+ * @param ipPrefix the IP address of the destination router
+ * @return true if all rules are removed successfully, false otherwise
+ */
+ private boolean revokeIpRuleForRouter(IpPrefix ipPrefix) {
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(ipPrefix);
+ TrafficSelector selector = sbuilder.build();
+ TrafficTreatment dummyTreatment = DefaultTrafficTreatment.builder().build();
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+ .builder()
+ .fromApp(srManager.appId)
+ .makePermanent()
+ .withSelector(selector)
+ .withTreatment(dummyTreatment)
+ .withPriority(getPriorityFromPrefix(ipPrefix))
+ .withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+ srManager.deviceService.getAvailableDevices().forEach(device -> {
+ if (srManager.mastershipService.isLocalMaster(device.id())) {
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("IP rule for router {} revoked from {}", ipPrefix, device.id()),
+ (objective, error) -> log.warn("Failed to revoke IP rule for router {} from {}: {}",
+ ipPrefix, device.id(), error));
+ srManager.flowObjectiveService.forward(device.id(), fwdBuilder.remove(context));
+ } else {
+ log.debug("Not the master of {}. Abort route {} removal", device.id(), ipPrefix);
+ }
+ });
+
+ return true;
+ }
+
+ /**
+ * Deals with !MPLS Bos use case.
+ *
+ * @param targetSwId the target sw
+ * @param destSwId the destination sw
+ * @param nextHops the set of next hops
+ * @param segmentId the segmentId to match
+ * @param routerIp the router ip
+ * @return a collection of fwdobjective
+ */
+ private Collection<ForwardingObjective> handleMpls(
+ DeviceId targetSwId,
+ DeviceId destSwId,
+ Set<DeviceId> nextHops,
+ int segmentId,
+ IpAddress routerIp,
+ boolean isMplsBos) {
+
+ TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+ List<ForwardingObjective.Builder> fwdObjBuilders = Lists.newArrayList();
+ // For the transport of Pwaas we can use two or three MPLS label
+ sbuilder.matchEthType(Ethernet.MPLS_UNICAST);
+ sbuilder.matchMplsLabel(MplsLabel.mplsLabel(segmentId));
+ sbuilder.matchMplsBos(isMplsBos);
+ TrafficSelector selector = sbuilder.build();
+
+ // setup metadata to pass to nextObjective - indicate the vlan on egress
+ // if needed by the switch pipeline. Since mpls next-hops are always to
+ // other neighboring routers, there is no subnet assigned on those ports.
+ TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder(selector);
+ metabuilder.matchVlanId(SegmentRoutingManager.INTERNAL_VLAN);
+
+ if (nextHops.size() == 1 && destSwId.equals(nextHops.toArray()[0])) {
+ // If the next hop is the destination router for the segment, do pop
+ log.debug("populateMplsRule: Installing MPLS forwarding objective for "
+ + "label {} in switch {} with pop to next-hops {}",
+ segmentId, targetSwId, nextHops);
+ // Not-bos pop case (php for the current label). If MPLS-ECMP
+ // has been configured, the application we will request the
+ // installation for an MPLS-ECMP group.
+ ForwardingObjective.Builder fwdObjNoBosBuilder =
+ getMplsForwardingObjective(targetSwId,
+ nextHops,
+ true,
+ isMplsBos,
+ metabuilder.build(),
+ routerIp,
+ destSwId);
+ // Error case, we cannot handle, exit.
+ if (fwdObjNoBosBuilder == null) {
+ return Collections.emptyList();
+ }
+ fwdObjBuilders.add(fwdObjNoBosBuilder);
+
+ } else {
+ // next hop is not destination, SR CONTINUE case (swap with self)
+ log.debug("Installing MPLS forwarding objective for "
+ + "label {} in switch {} without pop to next-hops {}",
+ segmentId, targetSwId, nextHops);
+ // Not-bos pop case. If MPLS-ECMP has been configured, the
+ // application we will request the installation for an MPLS-ECMP
+ // group.
+ ForwardingObjective.Builder fwdObjNoBosBuilder =
+ getMplsForwardingObjective(targetSwId,
+ nextHops,
+ false,
+ isMplsBos,
+ metabuilder.build(),
+ routerIp,
+ destSwId);
+ // Error case, we cannot handle, exit.
+ if (fwdObjNoBosBuilder == null) {
+ return Collections.emptyList();
+ }
+ fwdObjBuilders.add(fwdObjNoBosBuilder);
+
+ }
+
+ List<ForwardingObjective> fwdObjs = Lists.newArrayList();
+ // We add the final property to the fwdObjs.
+ for (ForwardingObjective.Builder fwdObjBuilder : fwdObjBuilders) {
+
+ ((Builder) ((Builder) fwdObjBuilder
+ .fromApp(srManager.appId)
+ .makePermanent())
+ .withSelector(selector)
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY))
+ .withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.debug("MPLS rule {} for SID {} populated in dev:{} ",
+ objective.id(), segmentId, targetSwId),
+ (objective, error) ->
+ log.warn("Failed to populate MPLS rule {} for SID {}: {} in dev:{}",
+ objective.id(), segmentId, error, targetSwId));
+
+ ForwardingObjective fob = fwdObjBuilder.add(context);
+ fwdObjs.add(fob);
+
+ }
+
+ return fwdObjs;
+ }
+
+ /**
+ * Populates MPLS flow rules in the target device to point towards the
+ * destination device.
+ *
+ * @param targetSwId target device ID of the switch to set the rules
+ * @param destSwId destination switch device ID
+ * @param nextHops next hops switch ID list
+ * @param routerIp the router ip
+ * @return true if all rules are set successfully, false otherwise
+ */
+ boolean populateMplsRule(DeviceId targetSwId, DeviceId destSwId,
+ Set<DeviceId> nextHops,
+ IpAddress routerIp) {
+
+ int segmentId;
+ try {
+ if (routerIp.isIp4()) {
+ segmentId = config.getIPv4SegmentId(destSwId);
+ } else {
+ segmentId = config.getIPv6SegmentId(destSwId);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting populateMplsRule.");
+ return false;
+ }
+
+ List<ForwardingObjective> fwdObjs = new ArrayList<>();
+ Collection<ForwardingObjective> fwdObjsMpls;
+ // Generates the transit rules used by the standard "routing".
+ fwdObjsMpls = handleMpls(targetSwId, destSwId, nextHops, segmentId, routerIp, true);
+ if (fwdObjsMpls.isEmpty()) {
+ return false;
+ }
+ fwdObjs.addAll(fwdObjsMpls);
+
+ // Generates the transit rules used by the MPLS Pwaas.
+ int pwSrLabel;
+ try {
+ pwSrLabel = config.getPWRoutingLabel(destSwId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting populateMplsRule. No label for PseudoWire traffic.");
+ return false;
+ }
+ fwdObjsMpls = handleMpls(targetSwId, destSwId, nextHops, pwSrLabel, routerIp, false);
+ if (fwdObjsMpls.isEmpty()) {
+ return false;
+ }
+ fwdObjs.addAll(fwdObjsMpls);
+
+ for (ForwardingObjective fwdObj : fwdObjs) {
+ log.debug("Sending MPLS fwd obj {} for SID {}-> next {} in sw: {}",
+ fwdObj.id(), segmentId, fwdObj.nextId(), targetSwId);
+ srManager.flowObjectiveService.forward(targetSwId, fwdObj);
+ rulePopulationCounter.incrementAndGet();
+ }
+
+ return true;
+ }
+
+ private ForwardingObjective.Builder getMplsForwardingObjective(
+ DeviceId targetSw,
+ Set<DeviceId> nextHops,
+ boolean phpRequired,
+ boolean isBos,
+ TrafficSelector meta,
+ IpAddress routerIp,
+ DeviceId destSw) {
+
+ ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+ .builder().withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+
+ if (phpRequired) {
+ // php case - pop should always be flow-action
+ log.debug("getMplsForwardingObjective: php required");
+ tbuilder.deferred().copyTtlIn();
+ if (isBos) {
+ if (routerIp.isIp4()) {
+ tbuilder.deferred().popMpls(EthType.EtherType.IPV4.ethType());
+ } else {
+ tbuilder.deferred().popMpls(EthType.EtherType.IPV6.ethType());
+ }
+ tbuilder.decNwTtl();
+ } else {
+ tbuilder.deferred().popMpls(EthType.EtherType.MPLS_UNICAST.ethType())
+ .decMplsTtl();
+ }
+ } else {
+ // swap with self case - SR CONTINUE
+ log.debug("getMplsForwardingObjective: php not required");
+ tbuilder.deferred().decMplsTtl();
+ }
+
+ fwdBuilder.withTreatment(tbuilder.build());
+ // if MPLS-ECMP == True we will build a standard NeighborSet.
+ // Otherwise a RandomNeighborSet.
+ DestinationSet ns = DestinationSet.destinationSet(false, false, destSw);
+ if (!isBos && this.srManager.getMplsEcmp()) {
+ ns = DestinationSet.destinationSet(false, true, destSw);
+ } else if (!isBos && !this.srManager.getMplsEcmp()) {
+ ns = DestinationSet.destinationSet(true, true, destSw);
+ }
+
+ log.debug("Trying to get a nextObjId for mpls rule on device:{} to ns:{}",
+ targetSw, ns);
+ DefaultGroupHandler gh = srManager.getGroupHandler(targetSw);
+ if (gh == null) {
+ log.warn("getNextObjectiveId query - groupHandler for device {} "
+ + "not found", targetSw);
+ return null;
+ }
+ // If BoS == True, all forwarding is via L3 ECMP group.
+ // If Bos == False, the forwarding can be via MPLS-ECMP group or through
+ // MPLS-Interface group. This depends on the configuration of the option
+ // MPLS-ECMP.
+ // The metadata informs the driver that the next-Objective will be used
+ // by MPLS flows and if Bos == False the driver will use MPLS groups.
+ Map<DeviceId, Set<DeviceId>> dstNextHops = new HashMap<>();
+ dstNextHops.put(destSw, nextHops);
+ int nextId = gh.getNextObjectiveId(ns, dstNextHops, meta, isBos);
+ if (nextId <= 0) {
+ log.warn("No next objective in {} for ns: {}", targetSw, ns);
+ return null;
+ } else {
+ log.debug("nextObjId found:{} for mpls rule on device:{} to ns:{}",
+ nextId, targetSw, ns);
+ }
+
+ fwdBuilder.nextStep(nextId);
+ return fwdBuilder;
+ }
+
+ /**
+ * Creates a filtering objective to permit all untagged packets with a
+ * dstMac corresponding to the router's MAC address. For those pipelines
+ * that need to internally assign vlans to untagged packets, this method
+ * provides per-subnet vlan-ids as metadata.
+ * <p>
+ * Note that the vlan assignment and filter programming should only be done by
+ * the master for a switch. This method is typically called at deviceAdd and
+ * programs filters only for the enabled ports of the device. For port-updates,
+ * that enable/disable ports after device add, singlePortFilter methods should
+ * be called.
+ *
+ * @param deviceId the switch dpid for the router
+ * @return PortFilterInfo information about the processed ports
+ */
+ PortFilterInfo populateVlanMacFilters(DeviceId deviceId) {
+ log.debug("Installing per-port filtering objective for untagged "
+ + "packets in device {}", deviceId);
+
+ List<Port> devPorts = srManager.deviceService.getPorts(deviceId);
+ if (devPorts == null || devPorts.isEmpty()) {
+ log.warn("Device {} ports not available. Unable to add MacVlan filters",
+ deviceId);
+ return null;
+ }
+ int disabledPorts = 0, errorPorts = 0, filteredPorts = 0;
+ for (Port port : devPorts) {
+ if (!port.isEnabled()) {
+ disabledPorts++;
+ continue;
+ }
+ if (processSinglePortFilters(deviceId, port.number(), true)) {
+ filteredPorts++;
+ } else {
+ errorPorts++;
+ }
+ }
+ log.debug("Filtering on dev:{}, disabledPorts:{}, errorPorts:{}, filteredPorts:{}",
+ deviceId, disabledPorts, errorPorts, filteredPorts);
+ return srManager.defaultRoutingHandler.new PortFilterInfo(disabledPorts,
+ errorPorts, filteredPorts);
+ }
+
+ /**
+ * Creates or removes filtering objectives for a single port. Should only be
+ * called by the master for a switch.
+ *
+ * @param deviceId device identifier
+ * @param portnum port identifier for port to be filtered
+ * @param install true to install the filtering objective, false to remove
+ * @return true if no errors occurred during the build of the filtering objective
+ */
+ boolean processSinglePortFilters(DeviceId deviceId, PortNumber portnum, boolean install) {
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, portnum);
+ VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+ Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+ VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+ if (taggedVlans.size() != 0) {
+ // Filter for tagged vlans
+ if (!srManager.getTaggedVlanId(connectPoint).stream().allMatch(taggedVlanId ->
+ processSinglePortFiltersInternal(deviceId, portnum, false, taggedVlanId, install))) {
+ return false;
+ }
+ if (nativeVlan != null) {
+ // Filter for native vlan
+ if (!processSinglePortFiltersInternal(deviceId, portnum, true, nativeVlan, install)) {
+ return false;
+ }
+ }
+ } else if (untaggedVlan != null) {
+ // Filter for untagged vlan
+ if (!processSinglePortFiltersInternal(deviceId, portnum, true, untaggedVlan, install)) {
+ return false;
+ }
+ } else {
+ // Unconfigured port, use INTERNAL_VLAN
+ if (!processSinglePortFiltersInternal(deviceId, portnum, true, INTERNAL_VLAN, install)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Updates filtering objectives for a single port. Should only be called by
+ * the master for a switch
+ * @param deviceId device identifier
+ * @param portNum port identifier for port to be filtered
+ * @param pushVlan true to push vlan, false otherwise
+ * @param vlanId vlan identifier
+ * @param install true to install the filtering objective, false to remove
+ */
+ void updateSinglePortFilters(DeviceId deviceId, PortNumber portNum,
+ boolean pushVlan, VlanId vlanId, boolean install) {
+ if (!processSinglePortFiltersInternal(deviceId, portNum, pushVlan, vlanId, install)) {
+ log.warn("Failed to update FilteringObjective for {}/{} with vlan {}",
+ deviceId, portNum, vlanId);
+ }
+ }
+
+ private boolean processSinglePortFiltersInternal(DeviceId deviceId, PortNumber portnum,
+ boolean pushVlan, VlanId vlanId, boolean install) {
+ FilteringObjective.Builder fob = buildFilteringObjective(deviceId, portnum, pushVlan, vlanId);
+ if (fob == null) {
+ // error encountered during build
+ return false;
+ }
+ log.debug("{} filtering objectives for dev/port: {}/{}",
+ install ? "Installing" : "Removing", deviceId, portnum);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Filter for {}/{} {}", deviceId, portnum,
+ install ? "installed" : "removed"),
+ (objective, error) -> log.warn("Failed to {} filter for {}/{}: {}",
+ install ? "install" : "remove", deviceId, portnum, error));
+ if (install) {
+ srManager.flowObjectiveService.filter(deviceId, fob.add(context));
+ } else {
+ srManager.flowObjectiveService.filter(deviceId, fob.remove(context));
+ }
+ return true;
+ }
+
+ private FilteringObjective.Builder buildFilteringObjective(DeviceId deviceId, PortNumber portnum,
+ boolean pushVlan, VlanId vlanId) {
+ MacAddress deviceMac;
+ try {
+ deviceMac = config.getDeviceMac(deviceId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Processing SinglePortFilters aborted");
+ return null;
+ }
+ FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+ fob.withKey(Criteria.matchInPort(portnum))
+ .addCondition(Criteria.matchEthDst(deviceMac))
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+
+ if (pushVlan) {
+ fob.addCondition(Criteria.matchVlanId(VlanId.NONE));
+ tBuilder.pushVlan().setVlanId(vlanId);
+ } else {
+ fob.addCondition(Criteria.matchVlanId(vlanId));
+ }
+
+ // NOTE: Some switch hardware share the same filtering flow among different ports.
+ // We use this metadata to let the driver know that there is no more enabled port
+ // within the same VLAN on this device.
+ boolean noMoreEnabledPort = srManager.interfaceService.getInterfaces().stream()
+ .filter(intf -> intf.connectPoint().deviceId().equals(deviceId))
+ .filter(intf -> intf.vlanTagged().contains(vlanId) ||
+ intf.vlanUntagged().equals(vlanId) ||
+ intf.vlanNative().equals(vlanId))
+ .noneMatch(intf -> {
+ Port port = srManager.deviceService.getPort(intf.connectPoint());
+ return port != null && port.isEnabled();
+ });
+ if (noMoreEnabledPort) {
+ tBuilder.wipeDeferred();
+ }
+
+ fob.withMeta(tBuilder.build());
+
+ fob.permit().fromApp(srManager.appId);
+ return fob;
+ }
+
+ /**
+ * Creates a forwarding objective to punt all IP packets, destined to the
+ * router's port IP addresses, to the controller. Note that the input
+ * port should not be matched on, as these packets can come from any input.
+ * Furthermore, these are applied only by the master instance.
+ *
+ * @param deviceId the switch dpid for the router
+ */
+ void populateIpPunts(DeviceId deviceId) {
+ Ip4Address routerIpv4, pairRouterIpv4 = null;
+ Ip6Address routerIpv6, routerLinkLocalIpv6, pairRouterIpv6 = null;
+ try {
+ routerIpv4 = config.getRouterIpv4(deviceId);
+ routerIpv6 = config.getRouterIpv6(deviceId);
+ routerLinkLocalIpv6 = Ip6Address.valueOf(
+ IPv6.getLinkLocalAddress(config.getDeviceMac(deviceId).toBytes()));
+
+ if (config.isPairedEdge(deviceId)) {
+ pairRouterIpv4 = config.getRouterIpv4(config.getPairDeviceId(deviceId));
+ pairRouterIpv6 = config.getRouterIpv6(config.getPairDeviceId(deviceId));
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting populateIpPunts.");
+ return;
+ }
+
+ if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+ log.debug("Not installing port-IP punts - not the master for dev:{} ",
+ deviceId);
+ return;
+ }
+ Set<IpAddress> allIps = new HashSet<>(config.getPortIPs(deviceId));
+ allIps.add(routerIpv4);
+ allIps.add(routerLinkLocalIpv6);
+ if (routerIpv6 != null) {
+ allIps.add(routerIpv6);
+ }
+ if (pairRouterIpv4 != null) {
+ allIps.add(pairRouterIpv4);
+ }
+ if (pairRouterIpv6 != null) {
+ allIps.add(pairRouterIpv6);
+ }
+ for (IpAddress ipaddr : allIps) {
+ populateSingleIpPunts(deviceId, ipaddr);
+ }
+ }
+
+ /**
+ * Creates a forwarding objective to punt all IP packets, destined to the
+ * specified IP address, which should be router's port IP address.
+ *
+ * @param deviceId the switch dpid for the router
+ * @param ipAddress the IP address of the router's port
+ */
+ void populateSingleIpPunts(DeviceId deviceId, IpAddress ipAddress) {
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpAddress(ipAddress);
+ Optional<DeviceId> optDeviceId = Optional.of(deviceId);
+
+ srManager.packetService.requestPackets(sbuilder.build(),
+ PacketPriority.CONTROL, srManager.appId, optDeviceId);
+ }
+
+ /**
+ * Removes a forwarding objective to punt all IP packets, destined to the
+ * specified IP address, which should be router's port IP address.
+ *
+ * @param deviceId the switch dpid for the router
+ * @param ipAddress the IP address of the router's port
+ */
+ void revokeSingleIpPunts(DeviceId deviceId, IpAddress ipAddress) {
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpAddress(ipAddress);
+ Optional<DeviceId> optDeviceId = Optional.of(deviceId);
+
+ try {
+ if (!ipAddress.equals(config.getRouterIpv4(deviceId)) &&
+ !ipAddress.equals(config.getRouterIpv6(deviceId))) {
+ srManager.packetService.cancelPackets(sbuilder.build(),
+ PacketPriority.CONTROL, srManager.appId, optDeviceId);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting revokeSingleIpPunts");
+ }
+ }
+
+ /**
+ * Method to build IPv4 or IPv6 selector.
+ *
+ * @param addressToMatch the address to match
+ */
+ private TrafficSelector.Builder buildIpSelectorFromIpAddress(IpAddress addressToMatch) {
+ return buildIpSelectorFromIpPrefix(addressToMatch.toIpPrefix());
+ }
+
+ /**
+ * Method to build IPv4 or IPv6 selector.
+ *
+ * @param prefixToMatch the prefix to match
+ */
+ private TrafficSelector.Builder buildIpSelectorFromIpPrefix(IpPrefix prefixToMatch) {
+ TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
+ // If the prefix is IPv4
+ if (prefixToMatch.isIp4()) {
+ selectorBuilder.matchEthType(Ethernet.TYPE_IPV4);
+ selectorBuilder.matchIPDst(prefixToMatch.getIp4Prefix());
+ return selectorBuilder;
+ }
+ // If the prefix is IPv6
+ selectorBuilder.matchEthType(Ethernet.TYPE_IPV6);
+ selectorBuilder.matchIPv6Dst(prefixToMatch.getIp6Prefix());
+ return selectorBuilder;
+ }
+
+ /**
+ * Creates forwarding objectives to punt ARP and NDP packets, to the controller.
+ * Furthermore, these are applied only by the master instance. Deferred actions
+ * are not cleared such that packets can be flooded in the cross connect use case
+ *
+ * @param deviceId the switch dpid for the router
+ */
+ void populateArpNdpPunts(DeviceId deviceId) {
+ // We are not the master just skip.
+ if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+ log.debug("Not installing ARP/NDP punts - not the master for dev:{} ",
+ deviceId);
+ return;
+ }
+
+ ForwardingObjective fwdObj;
+ // We punt all ARP packets towards the controller.
+ fwdObj = arpFwdObjective(null, true, ARP_NDP_PRIORITY)
+ .add(new ObjectiveContext() {
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to install forwarding objective to punt ARP to {}: {}",
+ deviceId, error);
+ }
+ });
+ srManager.flowObjectiveService.forward(deviceId, fwdObj);
+
+ // We punt all NDP packets towards the controller.
+ ndpFwdObjective(null, true, ARP_NDP_PRIORITY).forEach(builder -> {
+ ForwardingObjective obj = builder.add(new ObjectiveContext() {
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to install forwarding objective to punt NDP to {}: {}",
+ deviceId, error);
+ }
+ });
+ srManager.flowObjectiveService.forward(deviceId, obj);
+ });
+
+ srManager.getPairLocalPorts(deviceId).ifPresent(port -> {
+ ForwardingObjective pairFwdObj;
+ // Do not punt ARP packets from pair port
+ pairFwdObj = arpFwdObjective(port, false, PacketPriority.CONTROL.priorityValue() + 1)
+ .add(new ObjectiveContext() {
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to install forwarding objective to ignore ARP to {}: {}",
+ deviceId, error);
+ }
+ });
+ srManager.flowObjectiveService.forward(deviceId, pairFwdObj);
+
+ // Do not punt NDP packets from pair port
+ ndpFwdObjective(port, false, PacketPriority.CONTROL.priorityValue() + 1).forEach(builder -> {
+ ForwardingObjective obj = builder.add(new ObjectiveContext() {
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to install forwarding objective to ignore ARP to {}: {}",
+ deviceId, error);
+ }
+ });
+ srManager.flowObjectiveService.forward(deviceId, obj);
+ });
+
+ // Do not forward DAD packets from pair port
+ pairFwdObj = dad6FwdObjective(port, PacketPriority.CONTROL.priorityValue() + 2)
+ .add(new ObjectiveContext() {
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to install forwarding objective to drop DAD to {}: {}",
+ deviceId, error);
+ }
+ });
+ srManager.flowObjectiveService.forward(deviceId, pairFwdObj);
+ });
+ }
+
+ private ForwardingObjective.Builder fwdObjBuilder(TrafficSelector selector,
+ TrafficTreatment treatment, int priority) {
+ return DefaultForwardingObjective.builder()
+ .withPriority(priority)
+ .withSelector(selector)
+ .fromApp(srManager.appId)
+ .withFlag(ForwardingObjective.Flag.VERSATILE)
+ .withTreatment(treatment)
+ .makePermanent();
+ }
+
+ private ForwardingObjective.Builder arpFwdObjective(PortNumber port, boolean punt, int priority) {
+ TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+ sBuilder.matchEthType(TYPE_ARP);
+ if (port != null) {
+ sBuilder.matchInPort(port);
+ }
+
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (punt) {
+ tBuilder.punt();
+ }
+ return fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority);
+ }
+
+ private Set<ForwardingObjective.Builder> ndpFwdObjective(PortNumber port, boolean punt, int priority) {
+ Set<ForwardingObjective.Builder> result = Sets.newHashSet();
+
+ Lists.newArrayList(NEIGHBOR_SOLICITATION, NEIGHBOR_ADVERTISEMENT, ROUTER_SOLICITATION, ROUTER_ADVERTISEMENT)
+ .forEach(type -> {
+ TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+ sBuilder.matchEthType(TYPE_IPV6)
+ .matchIPProtocol(PROTOCOL_ICMP6)
+ .matchIcmpv6Type(type);
+ if (port != null) {
+ sBuilder.matchInPort(port);
+ }
+
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (punt) {
+ tBuilder.punt();
+ }
+
+ result.add(fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority));
+ });
+
+ return result;
+ }
+
+ private ForwardingObjective.Builder dad6FwdObjective(PortNumber port, int priority) {
+ TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+ sBuilder.matchEthType(TYPE_IPV6)
+ .matchIPv6Src(Ip6Address.ZERO.toIpPrefix());
+ // TODO CORD-1672 Fix this when OFDPA can distinguish ::/0 and ::/128 correctly
+ // .matchIPProtocol(PROTOCOL_ICMP6)
+ // .matchIcmpv6Type(NEIGHBOR_SOLICITATION);
+ if (port != null) {
+ sBuilder.matchInPort(port);
+ }
+
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ tBuilder.wipeDeferred();
+ return fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority);
+ }
+
+ /**
+ * Populates a forwarding objective to send packets that miss other high
+ * priority Bridging Table entries to a group that contains all ports of
+ * its subnet.
+ *
+ * @param deviceId switch ID to set the rules
+ */
+ void populateSubnetBroadcastRule(DeviceId deviceId) {
+ srManager.getVlanPortMap(deviceId).asMap().forEach((vlanId, ports) -> {
+ updateSubnetBroadcastRule(deviceId, vlanId, true);
+ });
+ }
+
+ /**
+ * Creates or removes a forwarding objective to broadcast packets to its subnet.
+ * @param deviceId switch ID to set the rule
+ * @param vlanId vlan ID to specify the subnet
+ * @param install true to install the rule, false to revoke the rule
+ */
+ void updateSubnetBroadcastRule(DeviceId deviceId, VlanId vlanId, boolean install) {
+ int nextId = srManager.getVlanNextObjectiveId(deviceId, vlanId);
+
+ if (nextId < 0) {
+ log.error("Cannot install vlan {} broadcast rule in dev:{} due"
+ + " to vlanId:{} or nextId:{}", vlanId, deviceId, vlanId, nextId);
+ return;
+ }
+
+ // Driver should treat objective with MacAddress.NONE as the
+ // subnet broadcast rule
+ TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+ sbuilder.matchVlanId(vlanId);
+ sbuilder.matchEthDst(MacAddress.NONE);
+
+ ForwardingObjective.Builder fob = DefaultForwardingObjective.builder();
+ fob.withFlag(Flag.SPECIFIC)
+ .withSelector(sbuilder.build())
+ .nextStep(nextId)
+ .withPriority(SegmentRoutingService.FLOOD_PRIORITY)
+ .fromApp(srManager.appId)
+ .makePermanent();
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("Vlan broadcast rule for {} populated", vlanId),
+ (objective, error) ->
+ log.warn("Failed to populate vlan broadcast rule for {}: {}", vlanId, error));
+
+ if (install) {
+ srManager.flowObjectiveService.forward(deviceId, fob.add(context));
+ } else {
+ srManager.flowObjectiveService.forward(deviceId, fob.remove(context));
+ }
+ }
+
+ private int getPriorityFromPrefix(IpPrefix prefix) {
+ return (prefix.isIp4()) ?
+ 2000 * prefix.prefixLength() + SegmentRoutingService.MIN_IP_PRIORITY :
+ 500 * prefix.prefixLength() + SegmentRoutingService.MIN_IP_PRIORITY;
+ }
+
+ /**
+ * Update Forwarding objective for each host and IP address connected to given port.
+ * And create corresponding Simple Next objective if it does not exist.
+ * Applied only when populating Forwarding objective
+ * @param deviceId switch ID to set the rule
+ * @param portNumber port number
+ * @param prefix IP prefix of the route
+ * @param hostMac MAC address of the next hop
+ * @param vlanId Vlan ID of the port
+ * @param popVlan true to pop vlan tag in TrafficTreatment
+ * @param install true to populate the forwarding objective, false to revoke
+ */
+ void updateFwdObj(DeviceId deviceId, PortNumber portNumber, IpPrefix prefix, MacAddress hostMac,
+ VlanId vlanId, boolean popVlan, boolean install) {
+ ForwardingObjective.Builder fob;
+ TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(prefix);
+ MacAddress deviceMac;
+ try {
+ deviceMac = config.getDeviceMac(deviceId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting updateFwdObj.");
+ return;
+ }
+
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.deferred()
+ .setEthDst(hostMac)
+ .setEthSrc(deviceMac)
+ .setOutput(portNumber);
+
+ TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+ if (!popVlan) {
+ tbuilder.setVlanId(vlanId);
+ } else {
+ mbuilder.matchVlanId(vlanId);
+ }
+
+ // if the objective is to revoke an existing rule, and for some reason
+ // the next-objective does not exist, then a new one should not be created
+ int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNumber,
+ tbuilder.build(), mbuilder.build(), install);
+ if (portNextObjId == -1) {
+ // Warning log will come from getPortNextObjective method
+ return;
+ }
+
+ fob = DefaultForwardingObjective.builder().withSelector(sbuilder.build())
+ .nextStep(portNextObjId).fromApp(srManager.appId).makePermanent()
+ .withPriority(getPriorityFromPrefix(prefix)).withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("IP rule for route {} {}", prefix, install ? "installed" : "revoked"),
+ (objective, error) ->
+ log.warn("Failed to {} IP rule for route {}: {}",
+ install ? "install" : "revoke", prefix, error));
+ srManager.flowObjectiveService.forward(deviceId, install ? fob.add(context) : fob.remove(context));
+
+ if (!install) {
+ DefaultGroupHandler grpHandler = srManager.getGroupHandler(deviceId);
+ if (grpHandler == null) {
+ log.warn("updateFwdObj: groupHandler for device {} not found", deviceId);
+ } else {
+ // Remove L3UG for the given port and host
+ grpHandler.removeGroupFromPort(portNumber, tbuilder.build(), mbuilder.build());
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..d951b53
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -0,0 +1,1727 @@
+/*
+ * Copyright 2015-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;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+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.Modified;
+import org.apache.felix.scr.annotations.Property;
+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.Ethernet;
+import org.onlab.packet.ICMP6;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.Event;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.ConfigException;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.config.basics.InterfaceConfig;
+import org.onosproject.net.config.basics.McastConfig;
+import org.onosproject.net.config.basics.SubjectFactories;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostLocationProbingService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastListener;
+import org.onosproject.net.mcast.MulticastRouteService;
+import org.onosproject.net.neighbour.NeighbourResolutionService;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteListener;
+import org.onosproject.routeservice.RouteService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+import org.onosproject.segmentrouting.config.XConnectConfig;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapBuilder;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.onlab.packet.Ethernet.TYPE_ARP;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_REGISTERED;
+import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_UNREGISTERED;
+
+/**
+ * Segment routing manager.
+ */
+@Service
+@Component(immediate = true)
+public class SegmentRoutingManager implements SegmentRoutingService {
+
+ private static Logger log = LoggerFactory.getLogger(SegmentRoutingManager.class);
+ private static final String NOT_MASTER = "Current instance is not the master of {}. Ignore.";
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ private ComponentConfigService compCfgService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ private NeighbourResolutionService neighbourResolutionService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ CoreService coreService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ PacketService packetService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ HostService hostService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ HostLocationProbingService probingService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ DeviceService deviceService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ DeviceAdminService deviceAdminService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public FlowObjectiveService flowObjectiveService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ LinkService linkService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public MastershipService mastershipService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public StorageService storageService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ MulticastRouteService multicastRouteService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public TopologyService topologyService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ RouteService routeService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public NetworkConfigRegistry cfgService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public InterfaceService interfaceService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public ClusterService clusterService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public LeadershipService leadershipService;
+
+ @Property(name = "activeProbing", boolValue = true,
+ label = "Enable active probing to discover dual-homed hosts.")
+ boolean activeProbing = true;
+
+ ArpHandler arpHandler = null;
+ IcmpHandler icmpHandler = null;
+ IpHandler ipHandler = null;
+ RoutingRulePopulator routingRulePopulator = null;
+ ApplicationId appId;
+ DeviceConfiguration deviceConfiguration = null;
+
+ DefaultRoutingHandler defaultRoutingHandler = null;
+ private TunnelHandler tunnelHandler = null;
+ private PolicyHandler policyHandler = null;
+ private InternalPacketProcessor processor = null;
+ private InternalLinkListener linkListener = null;
+ private InternalDeviceListener deviceListener = null;
+ private AppConfigHandler appCfgHandler = null;
+ XConnectHandler xConnectHandler = null;
+ McastHandler mcastHandler = null;
+ HostHandler hostHandler = null;
+ private RouteHandler routeHandler = null;
+ LinkHandler linkHandler = null;
+ private SegmentRoutingNeighbourDispatcher neighbourHandler = null;
+ private L2TunnelHandler l2TunnelHandler = null;
+ private InternalEventHandler eventHandler = new InternalEventHandler();
+ private final InternalHostListener hostListener = new InternalHostListener();
+ private final InternalConfigListener cfgListener = new InternalConfigListener(this);
+ private final InternalMcastListener mcastListener = new InternalMcastListener();
+ private final InternalRouteEventListener routeListener = new InternalRouteEventListener();
+
+ private ScheduledExecutorService executorService = Executors
+ .newScheduledThreadPool(1, groupedThreads("SegmentRoutingManager", "event-%d", log));
+
+ @SuppressWarnings("unused")
+ private static ScheduledFuture<?> eventHandlerFuture = null;
+ @SuppressWarnings("rawtypes")
+ private ConcurrentLinkedQueue<Event> eventQueue = new ConcurrentLinkedQueue<>();
+ Map<DeviceId, DefaultGroupHandler> groupHandlerMap =
+ new ConcurrentHashMap<>();
+ /**
+ * Per device next objective ID store with (device id + destination set) as key.
+ * Used to keep track on MPLS group information.
+ */
+ private EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+ dsNextObjStore = null;
+ /**
+ * Per device next objective ID store with (device id + vlanid) as key.
+ * Used to keep track on L2 flood group information.
+ */
+ private EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer>
+ vlanNextObjStore = null;
+ /**
+ * Per device next objective ID store with (device id + port + treatment + meta) as key.
+ * Used to keep track on L2 interface group and L3 unicast group information.
+ */
+ private EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer>
+ portNextObjStore = null;
+
+ private EventuallyConsistentMap<String, Tunnel> tunnelStore = null;
+ private EventuallyConsistentMap<String, Policy> policyStore = null;
+
+ private AtomicBoolean programmingScheduled = new AtomicBoolean();
+
+ private final ConfigFactory<DeviceId, SegmentRoutingDeviceConfig> deviceConfigFactory =
+ new ConfigFactory<DeviceId, SegmentRoutingDeviceConfig>(
+ SubjectFactories.DEVICE_SUBJECT_FACTORY,
+ SegmentRoutingDeviceConfig.class, "segmentrouting") {
+ @Override
+ public SegmentRoutingDeviceConfig createConfig() {
+ return new SegmentRoutingDeviceConfig();
+ }
+ };
+
+ private final ConfigFactory<ApplicationId, SegmentRoutingAppConfig> appConfigFactory =
+ new ConfigFactory<ApplicationId, SegmentRoutingAppConfig>(
+ SubjectFactories.APP_SUBJECT_FACTORY,
+ SegmentRoutingAppConfig.class, "segmentrouting") {
+ @Override
+ public SegmentRoutingAppConfig createConfig() {
+ return new SegmentRoutingAppConfig();
+ }
+ };
+
+ private final ConfigFactory<ApplicationId, XConnectConfig> xConnectConfigFactory =
+ new ConfigFactory<ApplicationId, XConnectConfig>(
+ SubjectFactories.APP_SUBJECT_FACTORY,
+ XConnectConfig.class, "xconnect") {
+ @Override
+ public XConnectConfig createConfig() {
+ return new XConnectConfig();
+ }
+ };
+
+ private ConfigFactory<ApplicationId, McastConfig> mcastConfigFactory =
+ new ConfigFactory<ApplicationId, McastConfig>(
+ SubjectFactories.APP_SUBJECT_FACTORY,
+ McastConfig.class, "multicast") {
+ @Override
+ public McastConfig createConfig() {
+ return new McastConfig();
+ }
+ };
+
+ private final ConfigFactory<ApplicationId, PwaasConfig> pwaasConfigFactory =
+ new ConfigFactory<ApplicationId, PwaasConfig>(
+ SubjectFactories.APP_SUBJECT_FACTORY,
+ PwaasConfig.class, "pwaas") {
+ @Override
+ public PwaasConfig createConfig() {
+ return new PwaasConfig();
+ }
+ };
+
+ private static final Object THREAD_SCHED_LOCK = new Object();
+ private static int numOfEventsQueued = 0;
+ private static int numOfEventsExecuted = 0;
+ private static int numOfHandlerExecution = 0;
+ private static int numOfHandlerScheduled = 0;
+
+ /**
+ * Segment Routing App ID.
+ */
+ public static final String APP_NAME = "org.onosproject.segmentrouting";
+
+ /**
+ * The default VLAN ID assigned to the interfaces without subnet config.
+ */
+ public static final VlanId INTERNAL_VLAN = VlanId.vlanId((short) 4094);
+
+ @Activate
+ protected void activate(ComponentContext context) {
+ appId = coreService.registerApplication(APP_NAME);
+
+ log.debug("Creating EC map nsnextobjectivestore");
+ EventuallyConsistentMapBuilder<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+ nsNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+ dsNextObjStore = nsNextObjMapBuilder
+ .withName("nsnextobjectivestore")
+ .withSerializer(createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+ log.trace("Current size {}", dsNextObjStore.size());
+
+ log.debug("Creating EC map vlannextobjectivestore");
+ EventuallyConsistentMapBuilder<VlanNextObjectiveStoreKey, Integer>
+ vlanNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+ vlanNextObjStore = vlanNextObjMapBuilder
+ .withName("vlannextobjectivestore")
+ .withSerializer(createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+
+ log.debug("Creating EC map subnetnextobjectivestore");
+ EventuallyConsistentMapBuilder<PortNextObjectiveStoreKey, Integer>
+ portNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+ portNextObjStore = portNextObjMapBuilder
+ .withName("portnextobjectivestore")
+ .withSerializer(createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+
+ EventuallyConsistentMapBuilder<String, Tunnel> tunnelMapBuilder =
+ storageService.eventuallyConsistentMapBuilder();
+ tunnelStore = tunnelMapBuilder
+ .withName("tunnelstore")
+ .withSerializer(createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+
+ EventuallyConsistentMapBuilder<String, Policy> policyMapBuilder =
+ storageService.eventuallyConsistentMapBuilder();
+ policyStore = policyMapBuilder
+ .withName("policystore")
+ .withSerializer(createSerializer())
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .build();
+
+ compCfgService.preSetProperty("org.onosproject.net.group.impl.GroupManager",
+ "purgeOnDisconnection", "true");
+ compCfgService.preSetProperty("org.onosproject.net.flow.impl.FlowRuleManager",
+ "purgeOnDisconnection", "true");
+ compCfgService.preSetProperty("org.onosproject.provider.host.impl.HostLocationProvider",
+ "requestInterceptsEnabled", "false");
+ compCfgService.preSetProperty("org.onosproject.net.neighbour.impl.NeighbourResolutionManager",
+ "requestInterceptsEnabled", "false");
+ compCfgService.preSetProperty("org.onosproject.dhcprelay.DhcpRelayManager",
+ "arpEnabled", "false");
+ compCfgService.preSetProperty("org.onosproject.net.host.impl.HostManager",
+ "greedyLearningIpv6", "true");
+ compCfgService.preSetProperty("org.onosproject.routing.cpr.ControlPlaneRedirectManager",
+ "forceUnprovision", "true");
+ compCfgService.preSetProperty("org.onosproject.routeservice.store.RouteStoreImpl",
+ "distributed", "true");
+ compCfgService.preSetProperty("org.onosproject.provider.host.impl.HostLocationProvider",
+ "multihomingEnabled", "true");
+ compCfgService.preSetProperty("org.onosproject.provider.lldp.impl.LldpLinkProvider",
+ "staleLinkAge", "15000");
+ compCfgService.preSetProperty("org.onosproject.net.host.impl.HostManager",
+ "allowDuplicateIps", "false");
+ compCfgService.registerProperties(getClass());
+ modified(context);
+
+ processor = new InternalPacketProcessor();
+ linkListener = new InternalLinkListener();
+ deviceListener = new InternalDeviceListener();
+ appCfgHandler = new AppConfigHandler(this);
+ xConnectHandler = new XConnectHandler(this);
+ mcastHandler = new McastHandler(this);
+ hostHandler = new HostHandler(this);
+ linkHandler = new LinkHandler(this);
+ routeHandler = new RouteHandler(this);
+ neighbourHandler = new SegmentRoutingNeighbourDispatcher(this);
+ l2TunnelHandler = new DefaultL2TunnelHandler(this);
+
+ cfgService.addListener(cfgListener);
+ cfgService.registerConfigFactory(deviceConfigFactory);
+ cfgService.registerConfigFactory(appConfigFactory);
+ cfgService.registerConfigFactory(xConnectConfigFactory);
+ cfgService.registerConfigFactory(mcastConfigFactory);
+ cfgService.registerConfigFactory(pwaasConfigFactory);
+ log.info("Configuring network before adding listeners");
+ cfgListener.configureNetwork();
+
+ hostService.addListener(hostListener);
+ packetService.addProcessor(processor, PacketProcessor.director(2));
+ linkService.addListener(linkListener);
+ deviceService.addListener(deviceListener);
+ multicastRouteService.addListener(mcastListener);
+ routeService.addListener(routeListener);
+
+ l2TunnelHandler.init();
+
+ log.info("Started");
+ }
+
+ KryoNamespace.Builder createSerializer() {
+ return new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .register(DestinationSetNextObjectiveStoreKey.class,
+ VlanNextObjectiveStoreKey.class,
+ DestinationSet.class,
+ NextNeighbors.class,
+ Tunnel.class,
+ DefaultTunnel.class,
+ Policy.class,
+ TunnelPolicy.class,
+ Policy.Type.class,
+ PortNextObjectiveStoreKey.class,
+ XConnectStoreKey.class,
+ L2Tunnel.class,
+ L2TunnelPolicy.class,
+ DefaultL2Tunnel.class,
+ DefaultL2TunnelPolicy.class
+ );
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ cfgService.removeListener(cfgListener);
+ cfgService.unregisterConfigFactory(deviceConfigFactory);
+ cfgService.unregisterConfigFactory(appConfigFactory);
+ cfgService.unregisterConfigFactory(xConnectConfigFactory);
+ cfgService.unregisterConfigFactory(mcastConfigFactory);
+ cfgService.unregisterConfigFactory(pwaasConfigFactory);
+ compCfgService.unregisterProperties(getClass(), false);
+
+ hostService.removeListener(hostListener);
+ packetService.removeProcessor(processor);
+ linkService.removeListener(linkListener);
+ deviceService.removeListener(deviceListener);
+ multicastRouteService.removeListener(mcastListener);
+ routeService.removeListener(routeListener);
+
+ neighbourResolutionService.unregisterNeighbourHandlers(appId);
+
+ processor = null;
+ linkListener = null;
+ deviceListener = null;
+ groupHandlerMap.clear();
+
+ dsNextObjStore.destroy();
+ vlanNextObjStore.destroy();
+ portNextObjStore.destroy();
+ tunnelStore.destroy();
+ policyStore.destroy();
+
+ mcastHandler.terminate();
+ log.info("Stopped");
+ }
+
+ @Modified
+ private void modified(ComponentContext context) {
+ Dictionary<?, ?> properties = context.getProperties();
+ if (properties == null) {
+ return;
+ }
+
+ String strActiveProving = Tools.get(properties, "activeProbing");
+ boolean expectActiveProbing = Boolean.parseBoolean(strActiveProving);
+
+ if (expectActiveProbing != activeProbing) {
+ activeProbing = expectActiveProbing;
+ log.info("{} active probing", activeProbing ? "Enabling" : "Disabling");
+ }
+ }
+
+ @Override
+ public List<Tunnel> getTunnels() {
+ return tunnelHandler.getTunnels();
+ }
+
+ @Override
+ public TunnelHandler.Result createTunnel(Tunnel tunnel) {
+ return tunnelHandler.createTunnel(tunnel);
+ }
+
+ @Override
+ public TunnelHandler.Result removeTunnel(Tunnel tunnel) {
+ for (Policy policy: policyHandler.getPolicies()) {
+ if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+ TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+ if (tunnelPolicy.tunnelId().equals(tunnel.id())) {
+ log.warn("Cannot remove the tunnel used by a policy");
+ return TunnelHandler.Result.TUNNEL_IN_USE;
+ }
+ }
+ }
+ return tunnelHandler.removeTunnel(tunnel);
+ }
+
+ @Override
+ public PolicyHandler.Result removePolicy(Policy policy) {
+ return policyHandler.removePolicy(policy);
+ }
+
+ @Override
+ public PolicyHandler.Result createPolicy(Policy policy) {
+ return policyHandler.createPolicy(policy);
+ }
+
+ @Override
+ public List<Policy> getPolicies() {
+ return policyHandler.getPolicies();
+ }
+
+ @Override
+ public List<L2Tunnel> getL2Tunnels() {
+ return l2TunnelHandler.getL2Tunnels();
+ }
+
+ @Override
+ public List<L2TunnelPolicy> getL2Policies() {
+ return l2TunnelHandler.getL2Policies();
+ }
+
+ @Override
+ public L2TunnelHandler.Result addPseudowire(String tunnelId, String pwLabel, String cP1,
+ String cP1InnerVlan, String cP1OuterVlan, String cP2,
+ String cP2InnerVlan, String cP2OuterVlan,
+ String mode, String sdTag) {
+ // Try to inject an empty Pwaas config if it is not found for the first time
+ PwaasConfig config = cfgService.getConfig(appId(), PwaasConfig.class);
+ if (config == null) {
+ log.debug("Pwaas config not found. Try to create an empty one.");
+ cfgService.applyConfig(appId(), PwaasConfig.class, new ObjectMapper().createObjectNode());
+ config = cfgService.getConfig(appId(), PwaasConfig.class);
+ }
+
+ ObjectNode object = config.addPseudowire(tunnelId, pwLabel,
+ cP1, cP1InnerVlan, cP1OuterVlan,
+ cP2, cP2InnerVlan, cP2OuterVlan,
+ mode, sdTag);
+ if (object == null) {
+ log.warn("Could not add pseudowire to the configuration!");
+ return L2TunnelHandler.Result.ADDITION_ERROR;
+ }
+
+ // inform everyone about the valid change in the pw configuration
+ cfgService.applyConfig(appId(), PwaasConfig.class, object);
+ return L2TunnelHandler.Result.SUCCESS;
+ }
+
+ @Override
+ public L2TunnelHandler.Result removePseudowire(String pwId) {
+
+ PwaasConfig config = cfgService.getConfig(appId(), PwaasConfig.class);
+ if (config == null) {
+ log.warn("Configuration for Pwaas class could not be found!");
+ return L2TunnelHandler.Result.CONFIG_NOT_FOUND;
+ }
+
+ ObjectNode object = config.removePseudowire(pwId);
+ if (object == null) {
+ log.warn("Could not delete pseudowire from configuration!");
+ return L2TunnelHandler.Result.REMOVAL_ERROR;
+ }
+
+ // sanity check, this should never fail since we removed a pw
+ // and we always check when we update the configuration
+ config.isValid();
+
+ // inform everyone
+ cfgService.applyConfig(appId(), PwaasConfig.class, object);
+
+ return L2TunnelHandler.Result.SUCCESS;
+ }
+
+ @Override
+ public void rerouteNetwork() {
+ cfgListener.configureNetwork();
+ }
+
+ @Override
+ public Map<DeviceId, Set<IpPrefix>> getDeviceSubnetMap() {
+ Map<DeviceId, Set<IpPrefix>> deviceSubnetMap = Maps.newHashMap();
+ deviceConfiguration.getRouters().forEach(device ->
+ deviceSubnetMap.put(device, deviceConfiguration.getSubnets(device)));
+ return deviceSubnetMap;
+ }
+
+
+ @Override
+ public ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEcmpSpg() {
+ if (defaultRoutingHandler != null) {
+ return defaultRoutingHandler.getCurrentEmcpSpgMap();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ImmutableMap<DestinationSetNextObjectiveStoreKey, NextNeighbors> getDestinationSet() {
+ if (dsNextObjStore != null) {
+ return ImmutableMap.copyOf(dsNextObjStore.entrySet());
+ } else {
+ return ImmutableMap.of();
+ }
+ }
+
+ @Override
+ public void verifyGroups(DeviceId id) {
+ DefaultGroupHandler gh = groupHandlerMap.get(id);
+ if (gh != null) {
+ gh.triggerBucketCorrector();
+ }
+ }
+
+ @Override
+ public ImmutableMap<Link, Boolean> getSeenLinks() {
+ return linkHandler.getSeenLinks();
+ }
+
+ @Override
+ public ImmutableMap<DeviceId, Set<PortNumber>> getDownedPortState() {
+ return linkHandler.getDownedPorts();
+ }
+
+ @Override
+ public Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp) {
+ return mcastHandler.getMcastNextIds(mcastIp);
+ }
+
+ @Override
+ public Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp) {
+ return mcastHandler.getMcastRoles(mcastIp);
+ }
+
+ @Override
+ public Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp) {
+ return mcastHandler.getMcastPaths(mcastIp);
+ }
+
+ /**
+ * Extracts the application ID from the manager.
+ *
+ * @return application ID
+ */
+ public ApplicationId appId() {
+ return appId;
+ }
+
+ /**
+ * Returns the device configuration.
+ *
+ * @return device configuration
+ */
+ public DeviceConfiguration deviceConfiguration() {
+ return deviceConfiguration;
+ }
+
+ /**
+ * Per device next objective ID store with (device id + destination set) as key.
+ * Used to keep track on MPLS group information.
+ *
+ * @return next objective ID store
+ */
+ public EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+ dsNextObjStore() {
+ return dsNextObjStore;
+ }
+
+ /**
+ * Per device next objective ID store with (device id + vlanid) as key.
+ * Used to keep track on L2 flood group information.
+ *
+ * @return vlan next object store
+ */
+ public EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer> vlanNextObjStore() {
+ return vlanNextObjStore;
+ }
+
+ /**
+ * Per device next objective ID store with (device id + port + treatment + meta) as key.
+ * Used to keep track on L2 interface group and L3 unicast group information.
+ *
+ * @return port next object store.
+ */
+ public EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer> portNextObjStore() {
+ return portNextObjStore;
+ }
+
+ /**
+ * Returns the MPLS-ECMP configuration which indicates whether ECMP on
+ * labeled packets should be programmed or not.
+ *
+ * @return MPLS-ECMP value
+ */
+ public boolean getMplsEcmp() {
+ SegmentRoutingAppConfig segmentRoutingAppConfig = cfgService
+ .getConfig(this.appId, SegmentRoutingAppConfig.class);
+ return segmentRoutingAppConfig != null && segmentRoutingAppConfig.mplsEcmp();
+ }
+
+ /**
+ * Returns the tunnel object with the tunnel ID.
+ *
+ * @param tunnelId Tunnel ID
+ * @return Tunnel reference
+ */
+ public Tunnel getTunnel(String tunnelId) {
+ return tunnelHandler.getTunnel(tunnelId);
+ }
+
+ // TODO Consider moving these to InterfaceService
+ /**
+ * Returns untagged VLAN configured on given connect point.
+ * <p>
+ * Only returns the first match if there are multiple untagged VLAN configured
+ * on the connect point.
+ *
+ * @param connectPoint connect point
+ * @return untagged VLAN or null if not configured
+ */
+ VlanId getUntaggedVlanId(ConnectPoint connectPoint) {
+ return interfaceService.getInterfacesByPort(connectPoint).stream()
+ .filter(intf -> !intf.vlanUntagged().equals(VlanId.NONE))
+ .map(Interface::vlanUntagged)
+ .findFirst().orElse(null);
+ }
+
+ /**
+ * Returns tagged VLAN configured on given connect point.
+ * <p>
+ * Returns all matches if there are multiple tagged VLAN configured
+ * on the connect point.
+ *
+ * @param connectPoint connect point
+ * @return tagged VLAN or empty set if not configured
+ */
+ Set<VlanId> getTaggedVlanId(ConnectPoint connectPoint) {
+ Set<Interface> interfaces = interfaceService.getInterfacesByPort(connectPoint);
+ return interfaces.stream()
+ .map(Interface::vlanTagged)
+ .flatMap(Set::stream)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns native VLAN configured on given connect point.
+ * <p>
+ * Only returns the first match if there are multiple native VLAN configured
+ * on the connect point.
+ *
+ * @param connectPoint connect point
+ * @return native VLAN or null if not configured
+ */
+ VlanId getNativeVlanId(ConnectPoint connectPoint) {
+ Set<Interface> interfaces = interfaceService.getInterfacesByPort(connectPoint);
+ return interfaces.stream()
+ .filter(intf -> !intf.vlanNative().equals(VlanId.NONE))
+ .map(Interface::vlanNative)
+ .findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * 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
+ */
+ VlanId getInternalVlanId(ConnectPoint connectPoint) {
+ VlanId untaggedVlanId = getUntaggedVlanId(connectPoint);
+ VlanId nativeVlanId = 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) {
+ 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
+ */
+ Optional<PortNumber> getPairLocalPorts(DeviceId deviceId) {
+ SegmentRoutingDeviceConfig deviceConfig =
+ cfgService.getConfig(deviceId, SegmentRoutingDeviceConfig.class);
+ return Optional.ofNullable(deviceConfig).map(SegmentRoutingDeviceConfig::pairLocalPort);
+ }
+
+ /**
+ * Determine if current instance is the master of given connect point.
+ *
+ * @param cp connect point
+ * @return true if current instance is the master of given connect point
+ */
+ public boolean isMasterOf(ConnectPoint cp) {
+ boolean isMaster = mastershipService.isLocalMaster(cp.deviceId());
+ if (!isMaster) {
+ log.debug(NOT_MASTER, cp);
+ }
+ return isMaster;
+ }
+
+ /**
+ * Returns locations of given resolved route.
+ *
+ * @param resolvedRoute resolved route
+ * @return locations of nexthop. Might be empty if next hop is not found
+ */
+ Set<ConnectPoint> nextHopLocations(ResolvedRoute resolvedRoute) {
+ HostId hostId = HostId.hostId(resolvedRoute.nextHopMac(), resolvedRoute.nextHopVlan());
+ return Optional.ofNullable(hostService.getHost(hostId))
+ .map(Host::locations).orElse(Sets.newHashSet())
+ .stream().map(l -> (ConnectPoint) l).collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns vlan port map of given device.
+ *
+ * @param deviceId device id
+ * @return vlan-port multimap
+ */
+ public Multimap<VlanId, PortNumber> getVlanPortMap(DeviceId deviceId) {
+ HashMultimap<VlanId, PortNumber> vlanPortMap = HashMultimap.create();
+
+ interfaceService.getInterfaces().stream()
+ .filter(intf -> intf.connectPoint().deviceId().equals(deviceId))
+ .forEach(intf -> {
+ vlanPortMap.put(intf.vlanUntagged(), intf.connectPoint().port());
+ intf.vlanTagged().forEach(vlanTagged ->
+ vlanPortMap.put(vlanTagged, intf.connectPoint().port())
+ );
+ vlanPortMap.put(intf.vlanNative(), intf.connectPoint().port());
+ });
+ vlanPortMap.removeAll(VlanId.NONE);
+
+ return vlanPortMap;
+ }
+
+ /**
+ * Returns the next objective ID for the given vlan id. It is expected
+ * that the next-objective has been pre-created from configuration.
+ *
+ * @param deviceId Device ID
+ * @param vlanId VLAN ID
+ * @return next objective ID or -1 if it was not found
+ */
+ int getVlanNextObjectiveId(DeviceId deviceId, VlanId vlanId) {
+ if (groupHandlerMap.get(deviceId) != null) {
+ log.trace("getVlanNextObjectiveId query in device {}", deviceId);
+ return groupHandlerMap.get(deviceId).getVlanNextObjectiveId(vlanId);
+ } else {
+ log.warn("getVlanNextObjectiveId query - groupHandler for "
+ + "device {} not found", deviceId);
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the next objective ID for the given portNumber, given the treatment.
+ * There could be multiple different treatments to the same outport, which
+ * would result in different objectives. If the next object does not exist,
+ * and should be created, a new one is created and its id is returned.
+ *
+ * @param deviceId Device ID
+ * @param portNum port number on device for which NextObjective is queried
+ * @param treatment the actions to apply on the packets (should include outport)
+ * @param meta metadata passed into the creation of a Next Objective if necessary
+ * @param createIfMissing true if a next object should be created if not found
+ * @return next objective ID or -1 if an error occurred during retrieval or creation
+ */
+ public int getPortNextObjectiveId(DeviceId deviceId, PortNumber portNum,
+ TrafficTreatment treatment,
+ TrafficSelector meta,
+ boolean createIfMissing) {
+ DefaultGroupHandler ghdlr = groupHandlerMap.get(deviceId);
+ if (ghdlr != null) {
+ return ghdlr.getPortNextObjectiveId(portNum, treatment, meta, createIfMissing);
+ } else {
+ log.warn("getPortNextObjectiveId query - groupHandler for device {}"
+ + " not found", deviceId);
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the group handler object for the specified device id.
+ *
+ * @param devId the device identifier
+ * @return the groupHandler object for the device id, or null if not found
+ */
+ DefaultGroupHandler getGroupHandler(DeviceId devId) {
+ return groupHandlerMap.get(devId);
+ }
+
+ /**
+ * Returns the default routing handler object.
+ *
+ * @return the default routing handler object
+ */
+ public DefaultRoutingHandler getRoutingHandler() {
+ return defaultRoutingHandler;
+ }
+
+ private class InternalPacketProcessor implements PacketProcessor {
+ @Override
+ public void process(PacketContext context) {
+
+ if (context.isHandled()) {
+ return;
+ }
+
+ InboundPacket pkt = context.inPacket();
+ Ethernet ethernet = pkt.parsed();
+
+ if (ethernet == null) {
+ return;
+ }
+
+ log.trace("Rcvd pktin from {}: {}", context.inPacket().receivedFrom(),
+ ethernet);
+ if (ethernet.getEtherType() == TYPE_ARP) {
+ log.warn("Received unexpected ARP packet on {}",
+ context.inPacket().receivedFrom());
+ log.trace("{}", ethernet);
+ return;
+ } else if (ethernet.getEtherType() == Ethernet.TYPE_IPV4) {
+ IPv4 ipv4Packet = (IPv4) ethernet.getPayload();
+ //ipHandler.addToPacketBuffer(ipv4Packet);
+ if (ipv4Packet.getProtocol() == IPv4.PROTOCOL_ICMP) {
+ icmpHandler.processIcmp(ethernet, pkt.receivedFrom());
+ } else {
+ // NOTE: We don't support IP learning at this moment so this
+ // is not necessary. Also it causes duplication of DHCP packets.
+ // ipHandler.processPacketIn(ipv4Packet, pkt.receivedFrom());
+ }
+ } else if (ethernet.getEtherType() == Ethernet.TYPE_IPV6) {
+ IPv6 ipv6Packet = (IPv6) ethernet.getPayload();
+ //ipHandler.addToPacketBuffer(ipv6Packet);
+ // We deal with the packet only if the packet is a ICMP6 ECHO/REPLY
+ if (ipv6Packet.getNextHeader() == IPv6.PROTOCOL_ICMP6) {
+ ICMP6 icmp6Packet = (ICMP6) ipv6Packet.getPayload();
+ if (icmp6Packet.getIcmpType() == ICMP6.ECHO_REQUEST ||
+ icmp6Packet.getIcmpType() == ICMP6.ECHO_REPLY) {
+ icmpHandler.processIcmpv6(ethernet, pkt.receivedFrom());
+ } else {
+ log.trace("Received ICMPv6 0x{} - not handled",
+ Integer.toHexString(icmp6Packet.getIcmpType() & 0xff));
+ }
+ } else {
+ // NOTE: We don't support IP learning at this moment so this
+ // is not necessary. Also it causes duplication of DHCPv6 packets.
+ // ipHandler.processPacketIn(ipv6Packet, pkt.receivedFrom());
+ }
+ }
+ }
+ }
+
+ private class InternalLinkListener implements LinkListener {
+ @Override
+ public void event(LinkEvent event) {
+ if (event.type() == LinkEvent.Type.LINK_ADDED ||
+ event.type() == LinkEvent.Type.LINK_UPDATED ||
+ event.type() == LinkEvent.Type.LINK_REMOVED) {
+ log.debug("Event {} received from Link Service", event.type());
+ scheduleEventHandlerIfNotScheduled(event);
+ }
+ }
+ }
+
+ private class InternalDeviceListener implements DeviceListener {
+ @Override
+ public void event(DeviceEvent event) {
+ switch (event.type()) {
+ case DEVICE_ADDED:
+ case PORT_UPDATED:
+ case PORT_ADDED:
+ case DEVICE_UPDATED:
+ case DEVICE_AVAILABILITY_CHANGED:
+ log.trace("Event {} received from Device Service", event.type());
+ scheduleEventHandlerIfNotScheduled(event);
+ break;
+ default:
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void scheduleEventHandlerIfNotScheduled(Event event) {
+ synchronized (THREAD_SCHED_LOCK) {
+ eventQueue.add(event);
+ numOfEventsQueued++;
+
+ if ((numOfHandlerScheduled - numOfHandlerExecution) == 0) {
+ //No pending scheduled event handling threads. So start a new one.
+ eventHandlerFuture = executorService
+ .schedule(eventHandler, 100, TimeUnit.MILLISECONDS);
+ numOfHandlerScheduled++;
+ }
+ log.trace("numOfEventsQueued {}, numOfEventHandlerScheduled {}",
+ numOfEventsQueued,
+ numOfHandlerScheduled);
+ }
+ }
+
+ private class InternalEventHandler implements Runnable {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ @SuppressWarnings("rawtypes")
+ Event event;
+ synchronized (THREAD_SCHED_LOCK) {
+ if (!eventQueue.isEmpty()) {
+ event = eventQueue.poll();
+ numOfEventsExecuted++;
+ } else {
+ numOfHandlerExecution++;
+ log.debug("numOfHandlerExecution {} numOfEventsExecuted {}",
+ numOfHandlerExecution, numOfEventsExecuted);
+ break;
+ }
+ }
+ if (event.type() == LinkEvent.Type.LINK_ADDED ||
+ event.type() == LinkEvent.Type.LINK_UPDATED) {
+ linkHandler.processLinkAdded((Link) event.subject());
+ } else if (event.type() == LinkEvent.Type.LINK_REMOVED) {
+ linkHandler.processLinkRemoved((Link) event.subject());
+ } else if (event.type() == DeviceEvent.Type.DEVICE_ADDED ||
+ event.type() == DeviceEvent.Type.DEVICE_AVAILABILITY_CHANGED ||
+ event.type() == DeviceEvent.Type.DEVICE_UPDATED) {
+ DeviceId deviceId = ((Device) event.subject()).id();
+ if (deviceService.isAvailable(deviceId)) {
+ log.info("Processing device event {} for available device {}",
+ event.type(), ((Device) event.subject()).id());
+ processDeviceAdded((Device) event.subject());
+ } else {
+ log.info("Processing device event {} for unavailable device {}",
+ event.type(), ((Device) event.subject()).id());
+ processDeviceRemoved((Device) event.subject());
+ }
+ } else if (event.type() == DeviceEvent.Type.PORT_ADDED) {
+ // typically these calls come when device is added first time
+ // so port filtering rules are handled at the device_added event.
+ // port added calls represent all ports on the device,
+ // enabled or not.
+ log.trace("** PORT ADDED {}/{} -> {}",
+ ((DeviceEvent) event).subject().id(),
+ ((DeviceEvent) event).port().number(),
+ event.type());
+ } else if (event.type() == DeviceEvent.Type.PORT_UPDATED) {
+ // these calls happen for every subsequent event
+ // ports enabled, disabled, switch goes away, comes back
+ log.info("** PORT UPDATED {}/{} -> {}",
+ event.subject(),
+ ((DeviceEvent) event).port(),
+ event.type());
+ processPortUpdated(((Device) event.subject()),
+ ((DeviceEvent) event).port());
+ } else {
+ log.warn("Unhandled event type: {}", event.type());
+ }
+ } catch (Exception e) {
+ log.error("SegmentRouting event handler thread thrown an exception: {}",
+ e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ void processDeviceAdded(Device device) {
+ log.info("** DEVICE ADDED with ID {}", device.id());
+
+ // NOTE: Punt ARP/NDP even when the device is not configured.
+ // Host learning without network config is required for CORD config generator.
+ routingRulePopulator.populateIpPunts(device.id());
+ routingRulePopulator.populateArpNdpPunts(device.id());
+
+ if (deviceConfiguration == null || !deviceConfiguration.isConfigured(device.id())) {
+ log.warn("Device configuration unavailable. Device {} will be "
+ + "processed after configuration.", device.id());
+ return;
+ }
+ processDeviceAddedInternal(device.id());
+ }
+
+ private void processDeviceAddedInternal(DeviceId deviceId) {
+ // Irrespective of whether the local is a MASTER or not for this device,
+ // we need to create a SR-group-handler instance. This is because in a
+ // multi-instance setup, any instance can initiate forwarding/next-objectives
+ // for any switch (even if this instance is a SLAVE or not even connected
+ // to the switch). To handle this, a default-group-handler instance is necessary
+ // per switch.
+ log.debug("Current groupHandlerMap devs: {}", groupHandlerMap.keySet());
+ if (groupHandlerMap.get(deviceId) == null) {
+ DefaultGroupHandler groupHandler;
+ try {
+ groupHandler = DefaultGroupHandler.
+ createGroupHandler(deviceId,
+ appId,
+ deviceConfiguration,
+ linkService,
+ flowObjectiveService,
+ this);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting processDeviceAdded.");
+ return;
+ }
+ log.debug("updating groupHandlerMap with new grpHdlr for device: {}",
+ deviceId);
+ groupHandlerMap.put(deviceId, groupHandler);
+ }
+
+ if (mastershipService.isLocalMaster(deviceId)) {
+ defaultRoutingHandler.populatePortAddressingRules(deviceId);
+ hostHandler.init(deviceId);
+ xConnectHandler.init(deviceId);
+ DefaultGroupHandler groupHandler = groupHandlerMap.get(deviceId);
+ groupHandler.createGroupsFromVlanConfig();
+ routingRulePopulator.populateSubnetBroadcastRule(deviceId);
+ }
+
+ appCfgHandler.init(deviceId);
+ routeHandler.init(deviceId);
+ }
+
+ private void processDeviceRemoved(Device device) {
+ dsNextObjStore.entrySet().stream()
+ .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+ .forEach(entry -> dsNextObjStore.remove(entry.getKey()));
+ vlanNextObjStore.entrySet().stream()
+ .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+ .forEach(entry -> vlanNextObjStore.remove(entry.getKey()));
+ portNextObjStore.entrySet().stream()
+ .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+ .forEach(entry -> portNextObjStore.remove(entry.getKey()));
+ linkHandler.processDeviceRemoved(device);
+
+ DefaultGroupHandler gh = groupHandlerMap.remove(device.id());
+ if (gh != null) {
+ gh.shutdown();
+ }
+ // Note that a switch going down is associated with all of its links
+ // going down as well, but it is treated as a single switch down event
+ // while the link-downs are ignored.
+ defaultRoutingHandler
+ .populateRoutingRulesForLinkStatusChange(null, null, device.id());
+ defaultRoutingHandler.purgeEcmpGraph(device.id());
+ mcastHandler.processDeviceDown(device.id());
+ xConnectHandler.removeDevice(device.id());
+ }
+
+ private void processPortUpdated(Device device, Port port) {
+ if (deviceConfiguration == null || !deviceConfiguration.isConfigured(device.id())) {
+ log.warn("Device configuration uploading. Not handling port event for"
+ + "dev: {} port: {}", device.id(), port.number());
+ return;
+ }
+
+ if (!mastershipService.isLocalMaster(device.id())) {
+ log.debug("Not master for dev:{} .. not handling port updated event"
+ + "for port {}", device.id(), port.number());
+ return;
+ }
+
+ // first we handle filtering rules associated with the port
+ if (port.isEnabled()) {
+ log.info("Switchport {}/{} enabled..programming filters",
+ device.id(), port.number());
+ routingRulePopulator.processSinglePortFilters(device.id(), port.number(), true);
+ } else {
+ log.info("Switchport {}/{} disabled..removing filters",
+ device.id(), port.number());
+ routingRulePopulator.processSinglePortFilters(device.id(), port.number(), false);
+ }
+
+ // portUpdated calls are for ports that have gone down or up. For switch
+ // to switch ports, link-events should take care of any re-routing or
+ // group editing necessary for port up/down. Here we only process edge ports
+ // that are already configured.
+ ConnectPoint cp = new ConnectPoint(device.id(), port.number());
+ VlanId untaggedVlan = getUntaggedVlanId(cp);
+ VlanId nativeVlan = getNativeVlanId(cp);
+ Set<VlanId> taggedVlans = getTaggedVlanId(cp);
+
+ if (untaggedVlan == null && nativeVlan == null && taggedVlans.isEmpty()) {
+ log.debug("Not handling port updated event for non-edge port (unconfigured) "
+ + "dev/port: {}/{}", device.id(), port.number());
+ return;
+ }
+ if (untaggedVlan != null) {
+ processEdgePort(device, port, untaggedVlan, true);
+ }
+ if (nativeVlan != null) {
+ processEdgePort(device, port, nativeVlan, true);
+ }
+ if (!taggedVlans.isEmpty()) {
+ taggedVlans.forEach(tag -> processEdgePort(device, port, tag, false));
+ }
+ }
+
+ private void processEdgePort(Device device, Port port, VlanId vlanId,
+ boolean popVlan) {
+ boolean portUp = port.isEnabled();
+ if (portUp) {
+ log.info("Device:EdgePort {}:{} is enabled in vlan: {}", device.id(),
+ port.number(), vlanId);
+ hostHandler.processPortUp(new ConnectPoint(device.id(), port.number()));
+ } else {
+ log.info("Device:EdgePort {}:{} is disabled in vlan: {}", device.id(),
+ port.number(), vlanId);
+ }
+
+ DefaultGroupHandler groupHandler = groupHandlerMap.get(device.id());
+ if (groupHandler != null) {
+ groupHandler.processEdgePort(port.number(), vlanId, popVlan, portUp);
+ } else {
+ log.warn("Group handler not found for dev:{}. Not handling edge port"
+ + " {} event for port:{}", device.id(),
+ (portUp) ? "UP" : "DOWN", port.number());
+ }
+ }
+
+ private void createOrUpdateDeviceConfiguration() {
+ if (deviceConfiguration == null) {
+ log.info("Creating new DeviceConfiguration");
+ deviceConfiguration = new DeviceConfiguration(this);
+ } else {
+ log.info("Updating DeviceConfiguration");
+ deviceConfiguration.updateConfig();
+ }
+ }
+
+ /**
+ * Registers the given connect point with the NRS, this is necessary
+ * to receive the NDP and ARP packets from the NRS.
+ *
+ * @param portToRegister connect point to register
+ */
+ public void registerConnectPoint(ConnectPoint portToRegister) {
+ neighbourResolutionService.registerNeighbourHandler(
+ portToRegister,
+ neighbourHandler,
+ appId
+ );
+ }
+
+ private class InternalConfigListener implements NetworkConfigListener {
+ private static final long PROGRAM_DELAY = 2;
+ SegmentRoutingManager srManager;
+
+ /**
+ * Constructs the internal network config listener.
+ *
+ * @param srManager segment routing manager
+ */
+ InternalConfigListener(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ }
+
+ /**
+ * Reads network config and initializes related data structure accordingly.
+ */
+ void configureNetwork() {
+ log.info("Configuring network ...");
+ createOrUpdateDeviceConfiguration();
+
+ arpHandler = new ArpHandler(srManager);
+ icmpHandler = new IcmpHandler(srManager);
+ ipHandler = new IpHandler(srManager);
+ routingRulePopulator = new RoutingRulePopulator(srManager);
+ defaultRoutingHandler = new DefaultRoutingHandler(srManager);
+
+ tunnelHandler = new TunnelHandler(linkService, deviceConfiguration,
+ groupHandlerMap, tunnelStore);
+ policyHandler = new PolicyHandler(appId, deviceConfiguration,
+ flowObjectiveService,
+ tunnelHandler, policyStore);
+ // add a small delay to absorb multiple network config added notifications
+ if (!programmingScheduled.get()) {
+ log.info("Buffering config calls for {} secs", PROGRAM_DELAY);
+ programmingScheduled.set(true);
+ executorService.schedule(new ConfigChange(), PROGRAM_DELAY,
+ TimeUnit.SECONDS);
+ }
+ mcastHandler.init();
+ }
+
+ @Override
+ public void event(NetworkConfigEvent event) {
+ // TODO move this part to NetworkConfigEventHandler
+ if (event.configClass().equals(SegmentRoutingDeviceConfig.class)) {
+ switch (event.type()) {
+ case CONFIG_ADDED:
+ log.info("Segment Routing Device Config added for {}", event.subject());
+ configureNetwork();
+ break;
+ case CONFIG_UPDATED:
+ log.info("Segment Routing Config updated for {}", event.subject());
+ createOrUpdateDeviceConfiguration();
+ // TODO support dynamic configuration
+ break;
+ default:
+ break;
+ }
+ } else if (event.configClass().equals(InterfaceConfig.class)) {
+ switch (event.type()) {
+ case CONFIG_ADDED:
+ log.info("Interface Config added for {}", event.subject());
+ configureNetwork();
+ break;
+ case CONFIG_UPDATED:
+ log.info("Interface Config updated for {}", event.subject());
+ createOrUpdateDeviceConfiguration();
+
+ // Following code will be uncommented when [CORD-634] is fully implemented.
+ // [CORD-634] Add dynamic config support for interfaces
+ updateInterface((InterfaceConfig) event.config().get(),
+ (InterfaceConfig) event.prevConfig().get());
+ // TODO support dynamic configuration
+ break;
+ default:
+ break;
+ }
+ } else if (event.configClass().equals(SegmentRoutingAppConfig.class)) {
+ checkState(appCfgHandler != null, "NetworkConfigEventHandler is not initialized");
+ switch (event.type()) {
+ case CONFIG_ADDED:
+ appCfgHandler.processAppConfigAdded(event);
+ break;
+ case CONFIG_UPDATED:
+ appCfgHandler.processAppConfigUpdated(event);
+ break;
+ case CONFIG_REMOVED:
+ appCfgHandler.processAppConfigRemoved(event);
+ break;
+ default:
+ break;
+ }
+ log.info("App config event .. configuring network");
+ configureNetwork();
+ } else if (event.configClass().equals(XConnectConfig.class)) {
+ checkState(xConnectHandler != null, "XConnectHandler is not initialized");
+ switch (event.type()) {
+ case CONFIG_ADDED:
+ xConnectHandler.processXConnectConfigAdded(event);
+ break;
+ case CONFIG_UPDATED:
+ xConnectHandler.processXConnectConfigUpdated(event);
+ break;
+ case CONFIG_REMOVED:
+ xConnectHandler.processXConnectConfigRemoved(event);
+ break;
+ default:
+ break;
+ }
+ } else if (event.configClass().equals(PwaasConfig.class)) {
+ checkState(l2TunnelHandler != null, "DefaultL2TunnelHandler is not initialized");
+ switch (event.type()) {
+ case CONFIG_ADDED:
+ l2TunnelHandler.processPwaasConfigAdded(event);
+ break;
+ case CONFIG_UPDATED:
+ l2TunnelHandler.processPwaasConfigUpdated(event);
+ break;
+ case CONFIG_REMOVED:
+ l2TunnelHandler.processPwaasConfigRemoved(event);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean isRelevant(NetworkConfigEvent event) {
+ if (event.type() == CONFIG_REGISTERED ||
+ event.type() == CONFIG_UNREGISTERED) {
+ log.debug("Ignore event {} due to type mismatch", event);
+ return false;
+ }
+
+ if (!event.configClass().equals(SegmentRoutingDeviceConfig.class) &&
+ !event.configClass().equals(SegmentRoutingAppConfig.class) &&
+ !event.configClass().equals(InterfaceConfig.class) &&
+ !event.configClass().equals(XConnectConfig.class) &&
+ !event.configClass().equals(PwaasConfig.class)) {
+ log.debug("Ignore event {} due to class mismatch", event);
+ return false;
+ }
+
+ return true;
+ }
+
+ private final class ConfigChange implements Runnable {
+ @Override
+ public void run() {
+ programmingScheduled.set(false);
+ log.info("Reacting to config changes after buffer delay");
+ for (Device device : deviceService.getDevices()) {
+ processDeviceAdded(device);
+ }
+ defaultRoutingHandler.startPopulationProcess();
+ }
+ }
+ }
+
+ private class InternalHostListener implements HostListener {
+ @Override
+ public void event(HostEvent event) {
+ switch (event.type()) {
+ case HOST_ADDED:
+ hostHandler.processHostAddedEvent(event);
+ break;
+ case HOST_MOVED:
+ hostHandler.processHostMovedEvent(event);
+ routeHandler.processHostMovedEvent(event);
+ break;
+ case HOST_REMOVED:
+ hostHandler.processHostRemovedEvent(event);
+ break;
+ case HOST_UPDATED:
+ hostHandler.processHostUpdatedEvent(event);
+ break;
+ default:
+ log.warn("Unsupported host event type: {}", event.type());
+ break;
+ }
+ }
+ }
+
+ private class InternalMcastListener implements McastListener {
+ @Override
+ public void event(McastEvent event) {
+ switch (event.type()) {
+ case SOURCE_ADDED:
+ mcastHandler.processSourceAdded(event);
+ break;
+ case SINK_ADDED:
+ mcastHandler.processSinkAdded(event);
+ break;
+ case SINK_REMOVED:
+ mcastHandler.processSinkRemoved(event);
+ break;
+ case ROUTE_REMOVED:
+ mcastHandler.processRouteRemoved(event);
+ break;
+ case ROUTE_ADDED:
+ default:
+ break;
+ }
+ }
+ }
+
+ private class InternalRouteEventListener implements RouteListener {
+ @Override
+ public void event(RouteEvent event) {
+ switch (event.type()) {
+ case ROUTE_ADDED:
+ routeHandler.processRouteAdded(event);
+ break;
+ case ROUTE_UPDATED:
+ routeHandler.processRouteUpdated(event);
+ break;
+ case ROUTE_REMOVED:
+ routeHandler.processRouteRemoved(event);
+ break;
+ case ALTERNATIVE_ROUTES_CHANGED:
+ routeHandler.processAlternativeRoutesChanged(event);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void updateInterface(InterfaceConfig conf, InterfaceConfig prevConf) {
+ try {
+ Set<Interface> intfs = conf.getInterfaces();
+ Set<Interface> prevIntfs = prevConf.getInterfaces();
+
+ // Now we only handle one interface config at each port.
+ if (intfs.size() != 1 || prevIntfs.size() != 1) {
+ log.warn("Interface update aborted - one at a time is allowed, " +
+ "but {} / {}(prev) received.", intfs.size(), prevIntfs.size());
+ return;
+ }
+
+ Interface intf = intfs.stream().findFirst().get();
+ Interface prevIntf = prevIntfs.stream().findFirst().get();
+
+ DeviceId deviceId = intf.connectPoint().deviceId();
+ PortNumber portNum = intf.connectPoint().port();
+
+ if (!mastershipService.isLocalMaster(deviceId)) {
+ log.debug("CONFIG_UPDATED event for interfaces should be " +
+ "handled by master node for device {}", deviceId);
+ return;
+ }
+
+ removeSubnetConfig(prevIntf.connectPoint(),
+ Sets.difference(new HashSet<>(prevIntf.ipAddressesList()),
+ new HashSet<>(intf.ipAddressesList())));
+
+ if (prevIntf.vlanNative() != VlanId.NONE && !intf.vlanNative().equals(prevIntf.vlanNative())) {
+ // RemoveVlanNative
+ updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanNative(), true, false);
+ }
+
+ if (!prevIntf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
+ // RemoveVlanTagged
+ prevIntf.vlanTagged().stream().filter(i -> !intf.vlanTagged().contains(i)).forEach(
+ vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, false)
+ );
+ }
+
+ if (prevIntf.vlanUntagged() != VlanId.NONE && !intf.vlanUntagged().equals(prevIntf.vlanUntagged())) {
+ // RemoveVlanUntagged
+ updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanUntagged(), true, false);
+ }
+
+ if (intf.vlanNative() != VlanId.NONE && !prevIntf.vlanNative().equals(intf.vlanNative())) {
+ // AddVlanNative
+ updateVlanConfigInternal(deviceId, portNum, intf.vlanNative(), true, true);
+ }
+
+ if (!intf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
+ // AddVlanTagged
+ intf.vlanTagged().stream().filter(i -> !prevIntf.vlanTagged().contains(i)).forEach(
+ vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, true)
+ );
+ }
+
+ if (intf.vlanUntagged() != VlanId.NONE && !prevIntf.vlanUntagged().equals(intf.vlanUntagged())) {
+ // AddVlanUntagged
+ updateVlanConfigInternal(deviceId, portNum, intf.vlanUntagged(), true, true);
+ }
+ addSubnetConfig(prevIntf.connectPoint(),
+ Sets.difference(new HashSet<>(intf.ipAddressesList()),
+ new HashSet<>(prevIntf.ipAddressesList())));
+ } catch (ConfigException e) {
+ log.error("Error in configuration");
+ }
+ }
+
+ private void updateVlanConfigInternal(DeviceId deviceId, PortNumber portNum,
+ VlanId vlanId, boolean pushVlan, boolean install) {
+ DefaultGroupHandler grpHandler = getGroupHandler(deviceId);
+ if (grpHandler == null) {
+ log.warn("Failed to retrieve group handler for device {}", deviceId);
+ return;
+ }
+
+ // Update filtering objective for a single port
+ routingRulePopulator.updateSinglePortFilters(deviceId, portNum, pushVlan, vlanId, install);
+
+ // Update filtering objective for multicast ingress port
+ mcastHandler.updateFilterToDevice(deviceId, portNum, vlanId, install);
+
+ int nextId = getVlanNextObjectiveId(deviceId, vlanId);
+
+ if (nextId != -1 && !install) {
+ // Update next objective for a single port as an output port
+ // Remove a single port from L2FG
+ grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+ // Remove L2 Bridging rule and L3 Unicast rule to the host
+ hostHandler.processIntfVlanUpdatedEvent(deviceId, portNum, vlanId, pushVlan, install);
+ // Remove broadcast forwarding rule and corresponding L2FG for VLAN
+ // only if there is no port configured on that VLAN ID
+ if (!getVlanPortMap(deviceId).containsKey(vlanId)) {
+ // Remove broadcast forwarding rule for the VLAN
+ routingRulePopulator.updateSubnetBroadcastRule(deviceId, vlanId, install);
+ // Remove L2FG for VLAN
+ grpHandler.removeBcastGroupFromVlan(deviceId, portNum, vlanId, pushVlan);
+ } else {
+ // Remove L2IG of the port
+ grpHandler.removePortNextObjective(deviceId, portNum, vlanId, pushVlan);
+ }
+ } else if (install) {
+ if (nextId != -1) {
+ // Add a single port to L2FG
+ grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+ } else {
+ // Create L2FG for VLAN
+ grpHandler.createBcastGroupFromVlan(vlanId, Collections.singleton(portNum));
+ routingRulePopulator.updateSubnetBroadcastRule(deviceId, vlanId, install);
+ }
+ hostHandler.processIntfVlanUpdatedEvent(deviceId, portNum, vlanId, pushVlan, install);
+ } else {
+ log.warn("Failed to retrieve next objective for vlan {} in device {}:{}", vlanId, deviceId, portNum);
+ }
+ }
+
+ private void removeSubnetConfig(ConnectPoint cp, Set<InterfaceIpAddress> ipAddressSet) {
+ Set<IpPrefix> ipPrefixSet = ipAddressSet.stream().
+ map(InterfaceIpAddress::subnetAddress).collect(Collectors.toSet());
+
+ Set<InterfaceIpAddress> deviceIntfIpAddrs = interfaceService.getInterfaces().stream()
+ .filter(intf -> intf.connectPoint().deviceId().equals(cp.deviceId()))
+ .filter(intf -> !intf.connectPoint().equals(cp))
+ .flatMap(intf -> intf.ipAddressesList().stream())
+ .collect(Collectors.toSet());
+ // 1. Partial subnet population
+ // Remove routing rules for removed subnet from previous configuration,
+ // which does not also exist in other interfaces in the same device
+ Set<IpPrefix> deviceIpPrefixSet = deviceIntfIpAddrs.stream()
+ .map(InterfaceIpAddress::subnetAddress)
+ .collect(Collectors.toSet());
+
+ defaultRoutingHandler.revokeSubnet(
+ ipPrefixSet.stream()
+ .filter(ipPrefix -> !deviceIpPrefixSet.contains(ipPrefix))
+ .collect(Collectors.toSet()));
+
+ // 2. Interface IP punts
+ // Remove IP punts for old Intf address
+ Set<IpAddress> deviceIpAddrs = deviceIntfIpAddrs.stream()
+ .map(InterfaceIpAddress::ipAddress)
+ .collect(Collectors.toSet());
+ ipAddressSet.stream()
+ .map(InterfaceIpAddress::ipAddress)
+ .filter(interfaceIpAddress -> !deviceIpAddrs.contains(interfaceIpAddress))
+ .forEach(interfaceIpAddress ->
+ routingRulePopulator.revokeSingleIpPunts(
+ cp.deviceId(), interfaceIpAddress));
+
+ // 3. Host unicast routing rule
+ // Remove unicast routing rule
+ hostHandler.processIntfIpUpdatedEvent(cp, ipPrefixSet, false);
+ }
+
+ private void addSubnetConfig(ConnectPoint cp, Set<InterfaceIpAddress> ipAddressSet) {
+ Set<IpPrefix> ipPrefixSet = ipAddressSet.stream().
+ map(InterfaceIpAddress::subnetAddress).collect(Collectors.toSet());
+
+ Set<InterfaceIpAddress> deviceIntfIpAddrs = interfaceService.getInterfaces().stream()
+ .filter(intf -> intf.connectPoint().deviceId().equals(cp.deviceId()))
+ .filter(intf -> !intf.connectPoint().equals(cp))
+ .flatMap(intf -> intf.ipAddressesList().stream())
+ .collect(Collectors.toSet());
+ // 1. Partial subnet population
+ // Add routing rules for newly added subnet, which does not also exist in
+ // other interfaces in the same device
+ Set<IpPrefix> deviceIpPrefixSet = deviceIntfIpAddrs.stream()
+ .map(InterfaceIpAddress::subnetAddress)
+ .collect(Collectors.toSet());
+
+ defaultRoutingHandler.populateSubnet(
+ Collections.singleton(cp),
+ ipPrefixSet.stream()
+ .filter(ipPrefix -> !deviceIpPrefixSet.contains(ipPrefix))
+ .collect(Collectors.toSet()));
+
+ // 2. Interface IP punts
+ // Add IP punts for new Intf address
+ Set<IpAddress> deviceIpAddrs = deviceIntfIpAddrs.stream()
+ .map(InterfaceIpAddress::ipAddress)
+ .collect(Collectors.toSet());
+ ipAddressSet.stream()
+ .map(InterfaceIpAddress::ipAddress)
+ .filter(interfaceIpAddress -> !deviceIpAddrs.contains(interfaceIpAddress))
+ .forEach(interfaceIpAddress ->
+ routingRulePopulator.populateSingleIpPunts(
+ cp.deviceId(), interfaceIpAddress));
+
+ // 3. Host unicast routing rule
+ // Add unicast routing rule
+ hostHandler.processIntfIpUpdatedEvent(cp, ipPrefixSet, true);
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java
new file mode 100644
index 0000000..f170fae
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.neighbour.NeighbourMessageHandler;
+import org.onosproject.net.host.HostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This handler dispatches to the appropriate handlers the
+ * neighbour discovery protocols.
+ */
+public class SegmentRoutingNeighbourDispatcher implements NeighbourMessageHandler {
+
+ private static Logger log = LoggerFactory.getLogger(SegmentRoutingNeighbourDispatcher.class);
+ private SegmentRoutingManager manager;
+
+ /**
+ * Create a segment routing neighbour dispatcher.
+ *
+ * @param segmentRoutingManager the segment routing manager
+ */
+ public SegmentRoutingNeighbourDispatcher(SegmentRoutingManager segmentRoutingManager) {
+ this.manager = segmentRoutingManager;
+ }
+
+ @Override
+ public void handleMessage(NeighbourMessageContext context, HostService hostService) {
+ log.trace("Received {} packet on {}: {}", context.protocol(),
+ context.inPort(), context.packet());
+ switch (context.protocol()) {
+ case ARP:
+ if (this.manager.arpHandler != null) {
+ this.manager.arpHandler.processPacketIn(context, hostService);
+ }
+ break;
+ case NDP:
+ if (this.manager.icmpHandler != null) {
+ this.manager.icmpHandler.processPacketIn(context, hostService);
+ }
+ break;
+ default:
+ log.warn("Unknown protocol", context.protocol());
+ }
+ }
+
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
new file mode 100644
index 0000000..f03e0be
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * This handler provides provides useful functions to the
+ * neighbour handlers (ARP, NDP).
+ */
+public class SegmentRoutingNeighbourHandler {
+
+ private static Logger log = LoggerFactory.getLogger(SegmentRoutingNeighbourHandler.class);
+
+ protected SegmentRoutingManager srManager;
+ protected DeviceConfiguration config;
+
+ /**
+ * Creates an SegmentRoutingNeighbourHandler object.
+ *
+ * @param srManager SegmentRoutingManager object
+ */
+ public SegmentRoutingNeighbourHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.config = checkNotNull(srManager.deviceConfiguration);
+ }
+
+ /**
+ * Creates an SegmentRoutingNeighbourHandler object.
+ */
+ public SegmentRoutingNeighbourHandler() {
+ this.srManager = null;
+ this.config = null;
+ }
+
+ /**
+ * Retrieve router (device) info.
+ *
+ * @param mac where to copy the mac
+ * @param ip where to copy the ip
+ * @param deviceId the device id
+ * @param targetAddress the target address
+ * @return true if it was possible to get the necessary info.
+ * False for errors
+ */
+ protected boolean getSenderInfo(byte[] mac,
+ byte[] ip,
+ DeviceId deviceId,
+ IpAddress targetAddress) {
+ byte[] senderMacAddress;
+ byte[] senderIpAddress;
+ IpAddress sender;
+ try {
+ senderMacAddress = config.getDeviceMac(deviceId).toBytes();
+ if (targetAddress.isIp4()) {
+ sender = config.getRouterIpAddressForASubnetHost(targetAddress.getIp4Address());
+ } else {
+ sender = config.getRouterIpAddressForASubnetHost(targetAddress.getIp6Address());
+ }
+ // If sender is null we abort.
+ if (sender == null) {
+ log.warn("Sender ip is null. Aborting getSenderInfo");
+ return false;
+ }
+ senderIpAddress = sender.toOctets();
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting getSenderInfo");
+ return false;
+ }
+ System.arraycopy(senderMacAddress, 0, mac, 0, senderMacAddress.length);
+ System.arraycopy(senderIpAddress, 0, ip, 0, senderIpAddress.length);
+ return true;
+ }
+
+ /**
+ * Utility to send a ND reply using the supplied information.
+ *
+ * @param pkt the request
+ * @param targetMac the target mac
+ * @param hostService the host service
+ */
+ protected void sendResponse(NeighbourMessageContext pkt, MacAddress targetMac, HostService hostService) {
+ HostId dstId = HostId.hostId(pkt.srcMac(), pkt.vlan());
+ Host dst = hostService.getHost(dstId);
+ if (dst == null) {
+ log.warn("Cannot send {} response to host {} - does not exist in the store",
+ pkt.protocol(), dstId);
+ return;
+ }
+ pkt.reply(targetMac);
+ }
+
+ /**
+ * Flood to all ports in the same subnet.
+ *
+ * @param packet packet to be flooded
+ * @param inPort where the packet comes from
+ * @param targetAddress the target address
+ */
+ protected void flood(Ethernet packet, ConnectPoint inPort, IpAddress targetAddress) {
+ try {
+ srManager.deviceConfiguration
+ .getSubnetPortsMap(inPort.deviceId()).forEach((subnet, ports) -> {
+ if (subnet.contains(targetAddress)) {
+ ports.stream()
+ .filter(port -> port != inPort.port())
+ .forEach(port -> {
+ forward(packet, new ConnectPoint(inPort.deviceId(), port));
+ });
+ }
+ });
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Cannot flood in subnet as device config not available"
+ + " for device: " + inPort.deviceId());
+ }
+ }
+
+ /*
+ * Floods only on the port which have been configured with the subnet
+ * of the target address. The in port is excluded.
+ *
+ * @param pkt the ndp/arp packet and context information
+ */
+ protected void flood(NeighbourMessageContext pkt) {
+ try {
+ srManager.deviceConfiguration
+ .getSubnetPortsMap(pkt.inPort().deviceId()).forEach((subnet, ports) -> {
+ if (subnet.contains(pkt.target())) {
+ ports.stream()
+ .filter(port -> port != pkt.inPort().port())
+ .forEach(port -> {
+ ConnectPoint outPoint = new ConnectPoint(
+ pkt.inPort().deviceId(),
+ port
+ );
+ pkt.forward(outPoint);
+ });
+ }
+ });
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Cannot flood in subnet as device config not available"
+ + " for device: " + pkt.inPort().deviceId());
+ }
+ }
+
+ /**
+ * Packet out to given port.
+ *
+ * Note: In current implementation, we expect all communication with
+ * end hosts within a subnet to be untagged.
+ * <p>
+ * For those pipelines that internally assigns a VLAN, the VLAN tag will be
+ * removed before egress.
+ * <p>
+ * For those pipelines that do not assign internal VLAN, the packet remains
+ * untagged.
+ *
+ * @param packet packet to be forwarded
+ * @param outPort where the packet should be forwarded
+ */
+ private void forward(Ethernet packet, ConnectPoint outPort) {
+ ByteBuffer buf = ByteBuffer.wrap(packet.serialize());
+
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.setOutput(outPort.port());
+ srManager.packetService.emit(new DefaultOutboundPacket(outPort.deviceId(),
+ tbuilder.build(), buf));
+ }
+
+}
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
new file mode 100644
index 0000000..6f115e4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2015-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;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+
+import com.google.common.collect.ImmutableMap;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Segment Routing Service for REST API.
+ */
+public interface SegmentRoutingService {
+ /**
+ * VLAN cross-connect priority.
+ */
+ int XCONNECT_PRIORITY = 1000;
+
+ /**
+ * Default flow priority.
+ */
+ int DEFAULT_PRIORITY = 100;
+
+ /**
+ * Minimum IP priority.
+ *
+ * Should < 0 such that priority of /0 will not conflict with lowest
+ * priority default entries.
+ */
+ int MIN_IP_PRIORITY = 10;
+
+ /**
+ * Subnet flooding flow priority.
+ */
+ int FLOOD_PRIORITY = 5;
+
+ /**
+ * Returns all tunnels.
+ *
+ * @return list of tunnels
+ */
+ List<Tunnel> getTunnels();
+
+ /**
+ * Creates a tunnel.
+ *
+ * @param tunnel tunnel reference to create
+ * @return WRONG_PATH if the tunnel path is wrong, ID_EXISTS if the tunnel ID
+ * exists already, TUNNEL_EXISTS if the same tunnel exists, INTERNAL_ERROR
+ * if the tunnel creation failed internally, SUCCESS if the tunnel is created
+ * successfully
+ */
+ TunnelHandler.Result createTunnel(Tunnel tunnel);
+
+ /**
+ * Returns all policies.
+ *
+ * @return list of policy
+ */
+ List<Policy> getPolicies();
+
+ /**
+ * Returns all l2 tunnels of pseudowires.
+ *
+ * @return list of l2 tunnels
+ */
+ List<L2Tunnel> getL2Tunnels();
+
+ /**
+ * Returns all l2 policie of pseudowires.
+ *
+ * @return list of l2 policies.
+ */
+ List<L2TunnelPolicy> getL2Policies();
+
+ /**
+ * Removes pw. Essentially updates configuration for PwaasConfig
+ * and sends event for removal. The rest are handled by DefaultL2TunnelHandler
+ *
+ * @param pwId The pseudowire id
+ * @return SUCCESS if operation successful or a descriptive error otherwise.
+ */
+ L2TunnelHandler.Result removePseudowire(String pwId);
+
+ /**
+ * Adds a Pseudowire to the configuration.
+ *
+ * @param tunnelId The pseudowire id
+ * @param pwLabel Pw label
+ * @param cP1 Connection Point 1 of pw
+ * @param cP1InnerVlan Outer vlan of cp2
+ * @param cP1OuterVlan Outer vlan of cp1
+ * @param cP2 Connection Point 2 of pw
+ * @param cP2InnerVlan Inner vlan of cp2
+ * @param cP2OuterVlan Outer vlan of cp1
+ * @param mode Mode of pw
+ * @param sdTag Service Delimiting tag of pw
+ * @return SUCCESS if operation is successful or a descriptive error otherwise.
+ */
+ L2TunnelHandler.Result addPseudowire(String tunnelId, String pwLabel, String cP1,
+ String cP1InnerVlan, String cP1OuterVlan, String cP2,
+ String cP2InnerVlan, String cP2OuterVlan,
+ String mode, String sdTag);
+
+ /**
+ * Creates a policy.
+ *
+ * @param policy policy reference to create
+ * @return ID_EXISTS if the same policy ID exists,
+ * POLICY_EXISTS if the same policy exists, TUNNEL_NOT_FOUND if the tunnel
+ * does not exists, UNSUPPORTED_TYPE if the policy type is not supported,
+ * SUCCESS if the policy is created successfully.
+ */
+ PolicyHandler.Result createPolicy(Policy policy);
+
+ /**
+ * Removes a tunnel.
+ *
+ * @param tunnel tunnel reference to remove
+ * @return TUNNEL_NOT_FOUND if the tunnel to remove does not exists,
+ * INTERNAL_ERROR if the tunnel creation failed internally, SUCCESS
+ * if the tunnel is created successfully.
+ */
+ TunnelHandler.Result removeTunnel(Tunnel tunnel);
+
+ /**
+ * Removes a policy.
+ *
+ * @param policy policy reference to remove
+ * @return POLICY_NOT_FOUND if the policy to remove does not exists,
+ * SUCCESS if it is removed successfully
+ */
+ PolicyHandler.Result removePolicy(Policy policy);
+
+ /**
+ * Use current state of the network to repopulate forwarding rules.
+ *
+ */
+ void rerouteNetwork();
+
+ /**
+ * Returns device-subnet mapping.
+ *
+ * @return device-subnet mapping
+ */
+ Map<DeviceId, Set<IpPrefix>> getDeviceSubnetMap();
+
+ /**
+ * Returns the current ECMP shortest path graph in this controller instance.
+ *
+ * @return ECMP shortest path graph
+ */
+ ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEcmpSpg();
+
+ /**
+ * Returns the destinatiomSet-NextObjective store contents.
+ *
+ * @return current contents of the destinationSetNextObjectiveStore
+ */
+ ImmutableMap<DestinationSetNextObjectiveStoreKey, NextNeighbors> getDestinationSet();
+
+ /**
+ * Triggers the verification of all ECMP groups in the specified device.
+ * Adjusts the group buckets if verification finds that there are more or less
+ * buckets than what should be there.
+ *
+ * @param id the device identifier
+ */
+ void verifyGroups(DeviceId id);
+
+ /**
+ * Returns the internal link state as seen by this instance of the
+ * controller.
+ *
+ * @return the internal link state
+ */
+ ImmutableMap<Link, Boolean> getSeenLinks();
+
+ /**
+ * Returns the ports administratively disabled by the controller.
+ *
+ * @return a map of devices and port numbers for administratively disabled
+ * ports. Does not include ports manually disabled by the operator.
+ */
+ ImmutableMap<DeviceId, Set<PortNumber>> getDownedPortState();
+
+ /**
+ * Returns the associated next ids to the mcast groups or to the single
+ * group if mcastIp is present.
+ *
+ * @param mcastIp the group ip
+ * @return the mapping mcastIp-device to next id
+ */
+ Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp);
+
+ /**
+ * Returns the associated roles to the mcast groups or to the single
+ * group if mcastIp is present.
+ *
+ * @param mcastIp the group ip
+ * @return the mapping mcastIp-device to mcast role
+ */
+ Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp);
+
+ /**
+ * Returns the associated paths to the mcast group.
+ *
+ * @param mcastIp the group ip
+ * @return the mapping egress point to mcast path
+ */
+ Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp);
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java
new file mode 100644
index 0000000..470662f
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015-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;
+
+import java.util.List;
+
+/**
+ * Tunnel interface.
+ */
+public interface Tunnel {
+
+ /**
+ * Returns the tunnel ID.
+ *
+ * @return tunnel ID
+ */
+ String id();
+
+ /**
+ * Returns Segment IDs for the tunnel including source and destination.
+ *
+ * @return List of Node ID
+ */
+ List<Integer> labelIds();
+
+ /**
+ * Returns the group ID for the tunnel.
+ *
+ * @return group ID
+ */
+ int groupId();
+
+ /**
+ * Sets group ID for the tunnel.
+ *
+ * @param groupId group identifier
+ */
+ void setGroupId(int groupId);
+
+ /**
+ * Sets the flag to allow to remove the group or not.
+ *
+ * @param ok the flag; true - allow to remove
+ */
+ void allowToRemoveGroup(boolean ok);
+
+ /**
+ * Checks if it is allowed to remove the group for the tunnel.
+ *
+ * @return true if allowed, false otherwise
+ */
+ boolean isAllowedToRemoveGroup();
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java
new file mode 100644
index 0000000..e18b920
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2015-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;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Tunnel Handler.
+ */
+public class TunnelHandler {
+ protected final Logger log = getLogger(getClass());
+
+ private final DeviceConfiguration config;
+ private final EventuallyConsistentMap<String, Tunnel> tunnelStore;
+ private Map<DeviceId, DefaultGroupHandler> groupHandlerMap;
+ private LinkService linkService;
+
+ /**
+ * Result of tunnel creation or removal.
+ */
+ public enum Result {
+ /**
+ * Success.
+ */
+ SUCCESS,
+
+ /**
+ * More than one router needs to specified to created a tunnel.
+ */
+ WRONG_PATH,
+
+ /**
+ * The same tunnel exists already.
+ */
+ TUNNEL_EXISTS,
+
+ /**
+ * The same tunnel ID exists already.
+ */
+ ID_EXISTS,
+
+ /**
+ * Tunnel not found.
+ */
+ TUNNEL_NOT_FOUND,
+
+ /**
+ * Cannot remove the tunnel used by a policy.
+ */
+ TUNNEL_IN_USE,
+
+ /**
+ * Failed to create/remove groups for the tunnel.
+ */
+ INTERNAL_ERROR
+ }
+
+ /**
+ * Constructs tunnel handler.
+ *
+ * @param linkService link service
+ * @param deviceConfiguration device configuration
+ * @param groupHandlerMap group handler map
+ * @param tunnelStore tunnel store
+ */
+ public TunnelHandler(LinkService linkService,
+ DeviceConfiguration deviceConfiguration,
+ Map<DeviceId, DefaultGroupHandler> groupHandlerMap,
+ EventuallyConsistentMap<String, Tunnel> tunnelStore) {
+ this.linkService = linkService;
+ this.config = deviceConfiguration;
+ this.groupHandlerMap = groupHandlerMap;
+ this.tunnelStore = tunnelStore;
+ }
+
+ /**
+ * Creates a tunnel.
+ *
+ * @param tunnel tunnel reference to create a tunnel
+ * @return WRONG_PATH if the tunnel path is wrong, ID_EXISTS if the tunnel ID
+ * exists already, TUNNEL_EXISTS if the same tunnel exists, INTERNAL_ERROR
+ * if the tunnel creation failed internally, SUCCESS if the tunnel is created
+ * successfully
+ */
+ public Result createTunnel(Tunnel tunnel) {
+
+ if (tunnel.labelIds().isEmpty() || tunnel.labelIds().size() < 3) {
+ log.error("More than one router needs to specified to created a tunnel");
+ return Result.WRONG_PATH;
+ }
+
+ if (tunnelStore.containsKey(tunnel.id())) {
+ log.warn("The same tunnel ID exists already");
+ return Result.ID_EXISTS;
+ }
+
+ if (tunnelStore.containsValue(tunnel)) {
+ log.warn("The same tunnel exists already");
+ return Result.TUNNEL_EXISTS;
+ }
+
+ int groupId = createGroupsForTunnel(tunnel);
+ if (groupId < 0) {
+ log.error("Failed to create groups for the tunnel");
+ return Result.INTERNAL_ERROR;
+ }
+
+ tunnel.setGroupId(groupId);
+ tunnelStore.put(tunnel.id(), tunnel);
+
+ return Result.SUCCESS;
+ }
+
+ /**
+ * Removes the tunnel with the tunnel ID given.
+ *
+ * @param tunnelInfo tunnel information to delete tunnels
+ * @return TUNNEL_NOT_FOUND if the tunnel to remove does not exists,
+ * INTERNAL_ERROR if the tunnel creation failed internally, SUCCESS
+ * if the tunnel is created successfully.
+ */
+ public Result removeTunnel(Tunnel tunnelInfo) {
+
+ Tunnel tunnel = tunnelStore.get(tunnelInfo.id());
+ if (tunnel != null) {
+ DeviceId deviceId = config.getDeviceId(tunnel.labelIds().get(0));
+ if (tunnel.isAllowedToRemoveGroup()) {
+ if (groupHandlerMap.get(deviceId).removeGroup(tunnel.groupId())) {
+ tunnelStore.remove(tunnel.id());
+ } else {
+ log.error("Failed to remove the tunnel {}", tunnelInfo.id());
+ return Result.INTERNAL_ERROR;
+ }
+ } else {
+ log.debug("The group is not removed because it is being used.");
+ tunnelStore.remove(tunnel.id());
+ }
+ } else {
+ log.error("No tunnel found for tunnel ID {}", tunnelInfo.id());
+ return Result.TUNNEL_NOT_FOUND;
+ }
+
+ return Result.SUCCESS;
+ }
+
+ /**
+ * Returns the tunnel with the tunnel ID given.
+ *
+ * @param tid Tunnel ID
+ * @return Tunnel reference
+ */
+ public Tunnel getTunnel(String tid) {
+ return tunnelStore.get(tid);
+ }
+
+ /**
+ * Returns all tunnels.
+ *
+ * @return list of Tunnels
+ */
+ public List<Tunnel> getTunnels() {
+ List<Tunnel> tunnels = new ArrayList<>();
+ tunnelStore.values().forEach(tunnel -> tunnels.add(
+ new DefaultTunnel((DefaultTunnel) tunnel)));
+
+ return tunnels;
+ }
+
+ private int createGroupsForTunnel(Tunnel tunnel) {
+
+ Set<Integer> portNumbers;
+ final int groupError = -1;
+
+ DeviceId deviceId = config.getDeviceId(tunnel.labelIds().get(0));
+ if (deviceId == null) {
+ log.warn("No device found for SID {}", tunnel.labelIds().get(0));
+ return groupError;
+ } else if (groupHandlerMap.get(deviceId) == null) {
+ log.warn("group handler not found for {}", deviceId);
+ return groupError;
+ }
+ Set<DeviceId> deviceIds = new HashSet<>();
+ int sid = tunnel.labelIds().get(1);
+ if (config.isAdjacencySid(deviceId, sid)) {
+ portNumbers = config.getPortsForAdjacencySid(deviceId, sid);
+ for (Link link: linkService.getDeviceEgressLinks(deviceId)) {
+ for (Integer port: portNumbers) {
+ if (link.src().port().toLong() == port) {
+ deviceIds.add(link.dst().deviceId());
+ }
+ }
+ }
+ } else {
+ deviceIds.add(config.getDeviceId(sid));
+ }
+ // For these NeighborSet isMpls is meaningless.
+ DestinationSet ns = new DestinationSet(false,
+ tunnel.labelIds().get(2),
+ DeviceId.NONE);
+
+ // If the tunnel reuses any existing groups, then tunnel handler
+ // should not remove the group.
+ if (groupHandlerMap.get(deviceId).hasNextObjectiveId(ns)) {
+ tunnel.allowToRemoveGroup(false);
+ } else {
+ tunnel.allowToRemoveGroup(true);
+ }
+
+ return groupHandlerMap.get(deviceId).getNextObjectiveId(ns, null, null, true);
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java
new file mode 100644
index 0000000..5df82af
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2015-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;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Tunnel Policy.
+ */
+public final class TunnelPolicy implements Policy {
+
+ private final Type type;
+ private final String id;
+ private final int priority;
+ private final String tunnelId;
+ private String dstIp;
+ private String srcIp;
+ private String ipProto;
+ private short srcPort;
+ private short dstPort;
+
+ private TunnelPolicy(String policyId, Type type, int priority, String tunnelId, String srcIp,
+ String dstIp, String ipProto, short srcPort, short dstPort) {
+ this.id = checkNotNull(policyId);
+ this.type = type;
+ this.tunnelId = tunnelId;
+ this.priority = priority;
+ this.dstIp = dstIp;
+ this.srcIp = srcIp;
+ this.ipProto = ipProto;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+
+ }
+
+ /**
+ * Creates a TunnelPolicy reference.
+ *
+ * @param p TunnelPolicy reference
+ */
+ public TunnelPolicy(TunnelPolicy p) {
+ this.id = p.id;
+ this.type = p.type;
+ this.tunnelId = p.tunnelId;
+ this.priority = p.priority;
+ this.srcIp = p.srcIp;
+ this.dstIp = p.dstIp;
+ this.ipProto = p.ipProto;
+ this.srcPort = p.srcPort;
+ this.dstPort = p.dstPort;
+ }
+
+ /**
+ * Returns the TunnelPolicy builder reference.
+ *
+ * @return TunnelPolicy builder
+ */
+ public static TunnelPolicy.Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public String id() {
+ return this.id;
+ }
+
+ @Override
+ public int priority() {
+ return priority;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ @Override
+ public String srcIp() {
+ return srcIp;
+ }
+
+ @Override
+ public String dstIp() {
+ return dstIp;
+ }
+
+ @Override
+ public String ipProto() {
+ return ipProto;
+ }
+
+ @Override
+ public short srcPort() {
+ return srcPort;
+ }
+
+ @Override
+ public short dstPort() {
+ return dstPort;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o instanceof TunnelPolicy) {
+ TunnelPolicy that = (TunnelPolicy) o;
+ // We do not compare the policy ID
+ if (this.type.equals(that.type) &&
+ this.tunnelId.equals(that.tunnelId) &&
+ this.priority == that.priority &&
+ this.srcIp.equals(that.srcIp) &&
+ this.dstIp.equals(that.dstIp) &&
+ this.srcPort == that.srcPort &&
+ this.dstPort == that.dstPort &&
+ this.ipProto.equals(that.ipProto)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, tunnelId, srcIp, dstIp, ipProto,
+ srcPort, dstPort, priority);
+ }
+
+ /**
+ * Returns the tunnel ID of the policy.
+ *
+ * @return Tunnel ID
+ */
+ public String tunnelId() {
+ return this.tunnelId;
+ }
+
+
+ /**
+ * Tunnel Policy Builder.
+ */
+ public static final class Builder {
+
+ private String id;
+ private Type type;
+ private int priority;
+ private String tunnelId;
+ private String dstIp;
+ private String srcIp;
+ private String ipProto;
+ private short srcPort;
+ private short dstPort;
+
+ /**
+ * Sets the policy Id.
+ *
+ * @param id policy Id
+ * @return Builder object
+ */
+ public Builder setPolicyId(String id) {
+ this.id = id;
+
+ return this;
+ }
+
+ /**
+ * Sets the policy type.
+ *
+ * @param type policy type
+ * @return Builder object
+ */
+ public Builder setType(Type type) {
+ this.type = type;
+
+ return this;
+ }
+
+ /**
+ * Sets the source IP address.
+ *
+ * @param srcIp source IP address
+ * @return Builder object
+ */
+ public Builder setSrcIp(String srcIp) {
+ this.srcIp = srcIp;
+
+ return this;
+ }
+
+ /**
+ * Sets the destination IP address.
+ *
+ * @param dstIp destination IP address
+ * @return Builder object
+ */
+ public Builder setDstIp(String dstIp) {
+ this.dstIp = dstIp;
+
+ return this;
+ }
+
+ /**
+ * Sets the IP protocol.
+ *
+ * @param proto IP protocol
+ * @return Builder object
+ */
+ public Builder setIpProto(String proto) {
+ this.ipProto = proto;
+
+ return this;
+ }
+
+ /**
+ * Sets the source port.
+ *
+ * @param srcPort source port
+ * @return Builder object
+ */
+ public Builder setSrcPort(short srcPort) {
+ this.srcPort = srcPort;
+
+ return this;
+ }
+
+ /**
+ * Sets the destination port.
+ *
+ * @param dstPort destination port
+ * @return Builder object
+ */
+ public Builder setDstPort(short dstPort) {
+ this.dstPort = dstPort;
+
+ return this;
+ }
+
+ /**
+ * Sets the priority of the policy.
+ *
+ * @param p priority
+ * @return Builder object
+ */
+ public Builder setPriority(int p) {
+ this.priority = p;
+
+ return this;
+ }
+
+ /**
+ * Sets the tunnel Id.
+ *
+ * @param tunnelId tunnel Id
+ * @return Builder object
+ */
+ public Builder setTunnelId(String tunnelId) {
+ this.tunnelId = tunnelId;
+
+ return this;
+ }
+
+ /**
+ * Builds the policy.
+ *
+ * @return Tunnel Policy reference
+ */
+ public Policy build() {
+ return new TunnelPolicy(id, type, priority, tunnelId, srcIp, dstIp,
+ ipProto, srcPort, dstPort);
+ }
+ }
+}
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
new file mode 100644
index 0000000..aa60684
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+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.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.config.XConnectConfig;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+/**
+ * Handles cross connect related events.
+ */
+public class XConnectHandler {
+ private static final Logger log = LoggerFactory.getLogger(XConnectHandler.class);
+ private static final String CONFIG_NOT_FOUND = "XConnect config not found";
+ private static final String NOT_MASTER = "Not master controller";
+ private final SegmentRoutingManager srManager;
+ private final StorageService storageService;
+ private final ConsistentMap<XConnectStoreKey, NextObjective> xConnectNextObjStore;
+ private final KryoNamespace.Builder xConnectKryo;
+
+ protected XConnectHandler(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ this.storageService = srManager.storageService;
+ xConnectKryo = new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .register(XConnectStoreKey.class)
+ .register(NextObjContext.class);
+ xConnectNextObjStore = storageService
+ .<XConnectStoreKey, NextObjective>consistentMapBuilder()
+ .withName("onos-xconnect-nextobj-store")
+ .withSerializer(Serializer.using(xConnectKryo.build()))
+ .build();
+ }
+
+ /**
+ * Read initial XConnect for given device.
+ *
+ * @param deviceId ID of the device to be initialized
+ */
+ public void init(DeviceId deviceId) {
+ // Try to read XConnect config
+ XConnectConfig config =
+ srManager.cfgService.getConfig(srManager.appId, XConnectConfig.class);
+ if (config == null) {
+ log.info("Skip XConnect initialization: {}", CONFIG_NOT_FOUND);
+ return;
+ }
+
+ config.getXconnects(deviceId).forEach(key -> {
+ populateXConnect(key, config.getPorts(key));
+ });
+ }
+
+ /**
+ * Processes Segment Routing App Config added event.
+ *
+ * @param event network config added event
+ */
+ protected void processXConnectConfigAdded(NetworkConfigEvent event) {
+ log.info("Processing XConnect CONFIG_ADDED");
+ XConnectConfig config = (XConnectConfig) event.config().get();
+ config.getXconnects().forEach(key -> {
+ populateXConnect(key, config.getPorts(key));
+ });
+ }
+
+ /**
+ * Processes Segment Routing App Config updated event.
+ *
+ * @param event network config updated event
+ */
+ protected void processXConnectConfigUpdated(NetworkConfigEvent event) {
+ log.info("Processing XConnect CONFIG_UPDATED");
+ XConnectConfig prevConfig = (XConnectConfig) event.prevConfig().get();
+ XConnectConfig config = (XConnectConfig) event.config().get();
+ Set<XConnectStoreKey> prevKeys = prevConfig.getXconnects();
+ Set<XConnectStoreKey> keys = config.getXconnects();
+
+ Set<XConnectStoreKey> pendingRemove = prevKeys.stream()
+ .filter(key -> !keys.contains(key)).collect(Collectors.toSet());
+ Set<XConnectStoreKey> pendingAdd = keys.stream()
+ .filter(key -> !prevKeys.contains(key)).collect(Collectors.toSet());
+ Set<XConnectStoreKey> pendingUpdate = keys.stream()
+ .filter(key -> prevKeys.contains(key) &&
+ !config.getPorts(key).equals(prevConfig.getPorts(key)))
+ .collect(Collectors.toSet());
+
+ pendingRemove.forEach(key -> {
+ revokeXConnect(key, prevConfig.getPorts(key));
+ });
+ pendingAdd.forEach(key -> {
+ populateXConnect(key, config.getPorts(key));
+ });
+ pendingUpdate.forEach(key -> {
+ updateXConnect(key, prevConfig.getPorts(key), config.getPorts(key));
+ });
+ }
+
+ /**
+ * Processes Segment Routing App Config removed event.
+ *
+ * @param event network config removed event
+ */
+ protected void processXConnectConfigRemoved(NetworkConfigEvent event) {
+ log.info("Processing XConnect CONFIG_REMOVED");
+ XConnectConfig prevConfig = (XConnectConfig) event.prevConfig().get();
+ prevConfig.getXconnects().forEach(key -> {
+ revokeXConnect(key, prevConfig.getPorts(key));
+ });
+ }
+
+ /**
+ * Checks if there is any XConnect configured on given connect point.
+ *
+ * @param cp connect point
+ * @return true if there is XConnect configured on given connect point.
+ */
+ public boolean hasXConnect(ConnectPoint cp) {
+ // Try to read XConnect config
+ XConnectConfig config =
+ srManager.cfgService.getConfig(srManager.appId, XConnectConfig.class);
+ if (config == null) {
+ log.warn("Failed to read XConnect config: {}", CONFIG_NOT_FOUND);
+ return false;
+ }
+ return config.getXconnects(cp.deviceId()).stream()
+ .anyMatch(key -> config.getPorts(key).contains(cp.port()));
+ }
+
+ /**
+ * 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(XConnectStoreKey key, Set<PortNumber> ports) {
+ if (!srManager.mastershipService.isLocalMaster(key.deviceId())) {
+ log.info("Abort populating XConnect {}: {}", key, NOT_MASTER);
+ return;
+ }
+ populateFilter(key, ports);
+ populateFwd(key, populateNext(key, ports));
+ }
+
+ /**
+ * Populates filtering objectives for given XConnect.
+ *
+ * @param key XConnect store key
+ * @param ports XConnect ports
+ */
+ private void populateFilter(XConnectStoreKey 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));
+ srManager.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(XConnectStoreKey key, Set<PortNumber> ports) {
+ NextObjective nextObj = null;
+ 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 NextObjContext(Objective.Operation.ADD, key);
+ nextObj = nextObjBuilder.add(nextContext);
+ srManager.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 forwarding objectives for given XConnect.
+ *
+ * @param key XConnect store key
+ * @param nextObj next objective
+ */
+ private void populateFwd(XConnectStoreKey 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));
+ srManager.flowObjectiveService.forward(key.deviceId(), fwdObjBuilder.add(fwdContext));
+ }
+
+ /**
+ * Revokes XConnect groups and flows for given key.
+ *
+ * @param key XConnect key
+ * @param ports XConnect ports
+ */
+ private void revokeXConnect(XConnectStoreKey key, Set<PortNumber> ports) {
+ if (!srManager.mastershipService.isLocalMaster(key.deviceId())) {
+ log.info("Abort populating XConnect {}: {}", key, NOT_MASTER);
+ return;
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Revokes filtering objectives for given XConnect.
+ *
+ * @param key XConnect store key
+ * @param ports XConnect ports
+ */
+ private void revokeFilter(XConnectStoreKey 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));
+ srManager.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(XConnectStoreKey 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);
+ }
+ }
+ };
+ srManager.flowObjectiveService.next(key.deviceId(),
+ (NextObjective) nextObj.copy().remove(context));
+ xConnectNextObjStore.remove(key);
+ }
+
+ /**
+ * Revokes 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(XConnectStoreKey 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);
+ }
+ }
+ };
+ srManager.flowObjectiveService
+ .forward(key.deviceId(), fwdObjBuilder.remove(context));
+ }
+
+ /**
+ * 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(XConnectStoreKey key, Set<PortNumber> prevPorts,
+ Set<PortNumber> ports) {
+ // 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);
+ }
+ }
+
+ /**
+ * Remove all groups on given device.
+ *
+ * @param deviceId device ID
+ */
+ protected void removeDevice(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);
+ }
+
+ /**
+ * 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(XConnectStoreKey key, Set<PortNumber> ports) {
+ int nextId = srManager.flowObjectiveService.allocateNextId();
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder().matchVlanId(key.vlanId()).build();
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(srManager.appId)
+ .withMeta(metadata);
+ ports.forEach(port -> {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(port);
+ nextObjBuilder.addTreatment(tBuilder.build());
+ });
+ return nextObjBuilder;
+ }
+
+ /**
+ * Creates a forwarding objective builder for XConnect.
+ *
+ * @param key XConnect key
+ * @param nextId next ID of the broadcast group for this XConnect key
+ * @return next objective builder
+ */
+ private ForwardingObjective.Builder fwdObjBuilder(XConnectStoreKey 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(SegmentRoutingService.XCONNECT_PRIORITY)
+ .fromApp(srManager.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(XConnectStoreKey key, PortNumber port) {
+ FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+ fob.withKey(Criteria.matchInPort(port))
+ .addCondition(Criteria.matchVlanId(key.vlanId()))
+ .addCondition(Criteria.matchEthDst(MacAddress.NONE))
+ .withPriority(SegmentRoutingService.XCONNECT_PRIORITY);
+ return fob.permit().fromApp(srManager.appId);
+ }
+
+ // TODO: Lambda closure in DefaultObjectiveContext cannot be serialized properly
+ // with Kryo 3.0.3. It will be fixed in 3.0.4. By then we can use
+ // DefaultObjectiveContext again.
+ private final class NextObjContext implements ObjectiveContext {
+ Objective.Operation op;
+ XConnectStoreKey key;
+
+ private NextObjContext(Objective.Operation op, XConnectStoreKey key) {
+ this.op = op;
+ this.key = key;
+ }
+
+ @Override
+ public void onSuccess(Objective objective) {
+ log.debug("XConnect NextObj for {} {}ED", key, op);
+ }
+
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to {} XConnect NextObj for {}: {}", op, key, error);
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java
new file mode 100644
index 0000000..a364b6e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Command to list device-subnet mapping in Segment Routing.
+ */
+@Command(scope = "onos", name = "sr-device-subnets",
+ description = "List device-subnet mapping in Segment Routing")
+public class DeviceSubnetListCommand extends AbstractShellCommand {
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+ printDeviceSubnetMap(srService.getDeviceSubnetMap());
+ }
+
+ private void printDeviceSubnetMap(Map<DeviceId, Set<IpPrefix>> deviceSubnetMap) {
+ deviceSubnetMap.forEach(((deviceId, ipPrefices) -> {
+ print("%s", deviceId);
+ ipPrefices.forEach(ipPrefix -> print(" %s", ipPrefix));
+ }));
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java
new file mode 100644
index 0000000..678114a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.cli;
+
+
+import java.util.Map;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.EcmpShortestPathGraph;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Command to read the current state of the ECMP shortest-path graph.
+ *
+ */
+@Command(scope = "onos", name = "sr-ecmp-spg",
+ description = "Displays the current ecmp shortest-path-graph in this "
+ + "controller instance")
+public class EcmpGraphCommand extends AbstractShellCommand {
+
+ private static final String FORMAT_MAPPING = " %s";
+
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+ printEcmpGraph(srService.getCurrentEcmpSpg());
+ }
+
+ private void printEcmpGraph(Map<DeviceId, EcmpShortestPathGraph> currentEcmpSpg) {
+ if (currentEcmpSpg == null) {
+ print(FORMAT_MAPPING, "No ECMP graph found");
+ return;
+ }
+ StringBuilder ecmp = new StringBuilder();
+ currentEcmpSpg.forEach((key, value) -> {
+ ecmp.append("\n\nRoot Device: " + key + " ECMP Paths: " + value);
+ });
+ print(FORMAT_MAPPING, ecmp.toString());
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java
new file mode 100644
index 0000000..ded4ae6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+
+/**
+ * Command to read the current state of the DestinationSetNextObjectiveStore.
+ *
+ */
+@Command(scope = "onos", name = "sr-link-state", description = "Displays the current internal link state "
+ + "noted by this instance of the controller")
+public class LinkStateCommand extends AbstractShellCommand {
+ private static final String FORMAT_MAPPING = " %s";
+
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService = AbstractShellCommand
+ .get(SegmentRoutingService.class);
+ printLinkState(srService.getSeenLinks(),
+ srService.getDownedPortState());
+ }
+
+ private void printLinkState(ImmutableMap<Link, Boolean> seenLinks,
+ ImmutableMap<DeviceId, Set<PortNumber>> downedPortState) {
+ List<Link> a = Lists.newArrayList();
+ a.addAll(seenLinks.keySet());
+ a.sort(new CompLinks());
+
+ StringBuilder slbldr = new StringBuilder();
+ slbldr.append("\n Seen Links: ");
+ for (int i = 0; i < a.size(); i++) {
+ slbldr.append("\n "
+ + (seenLinks.get(a.get(i)) == Boolean.TRUE ? " up : "
+ : "down : "));
+ slbldr.append(a.get(i).src() + " --> " + a.get(i).dst());
+ }
+ print(FORMAT_MAPPING, slbldr.toString());
+
+ StringBuilder dpbldr = new StringBuilder();
+ dpbldr.append("\n\n Administratively Disabled Ports: ");
+ downedPortState.entrySet().forEach(entry -> dpbldr
+ .append("\n " + entry.getKey() + entry.getValue()));
+ print(FORMAT_MAPPING, dpbldr.toString());
+ }
+
+ static class CompLinks implements Comparator<Link> {
+
+ @Override
+ public int compare(Link o1, Link o2) {
+ int res = o1.src().deviceId().toString()
+ .compareTo(o2.src().deviceId().toString());
+ if (res < 0) {
+ return -1;
+ } else if (res > 0) {
+ return +1;
+ }
+ if (o1.src().port().toLong() < o2.src().port().toLong()) {
+ return -1;
+ }
+ return +1;
+ }
+
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
new file mode 100644
index 0000000..341c87d
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
@@ -0,0 +1,81 @@
+/*
+ * 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.Maps;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Command to show the list of mcast nextids.
+ */
+@Command(scope = "onos", name = "sr-mcast-next",
+ description = "Lists all mcast nextids")
+public class McastNextListCommand extends AbstractShellCommand {
+
+ // Format for group line
+ private static final String FORMAT_MAPPING = "group=%s, deviceIds-nextIds=%s";
+
+ @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+ required = false, multiValued = false)
+ String mcastIp;
+
+ @Override
+ protected void execute() {
+ // Verify mcast group
+ IpAddress mcastGroup = null;
+ if (!isNullOrEmpty(mcastIp)) {
+ mcastGroup = IpAddress.valueOf(mcastIp);
+
+ }
+ // Get SR service
+ SegmentRoutingService srService = get(SegmentRoutingService.class);
+ // Get the mapping
+ Map<McastStoreKey, Integer> keyToNextId = srService.getMcastNextIds(mcastGroup);
+ // Reduce to the set of mcast groups
+ Set<IpAddress> mcastGroups = keyToNextId.keySet().stream()
+ .map(McastStoreKey::mcastIp)
+ .collect(Collectors.toSet());
+ // Print the nextids for each group
+ mcastGroups.forEach(group -> {
+ // Create a new map for the group
+ Map<DeviceId, Integer> deviceIdNextMap = Maps.newHashMap();
+ keyToNextId.entrySet()
+ .stream()
+ // Filter only the elements related to this group
+ .filter(entry -> entry.getKey().mcastIp().equals(group))
+ // For each create a new entry in the group related map
+ .forEach(entry -> deviceIdNextMap.put(entry.getKey().deviceId(), entry.getValue()));
+ // Print the map
+ printMcastNext(group, deviceIdNextMap);
+ });
+ }
+
+ private void printMcastNext(IpAddress mcastGroup, Map<DeviceId, Integer> deviceIdNextMap) {
+ print(FORMAT_MAPPING, mcastGroup, deviceIdNextMap);
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
new file mode 100644
index 0000000..73cbb1a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
@@ -0,0 +1,107 @@
+/*
+ * 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.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.McastHandler.McastRole;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.EGRESS;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.INGRESS;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.TRANSIT;
+
+/**
+ * Command to show the list of mcast trees.
+ */
+@Command(scope = "onos", name = "sr-mcast-tree",
+ description = "Lists all mcast trees")
+public class McastTreeListCommand extends AbstractShellCommand {
+
+ // Format for group line
+ private static final String G_FORMAT_MAPPING = "group=%s, ingress=%s, transit=%s, egress=%s";
+ // Format for sink line
+ private static final String S_FORMAT_MAPPING = "\tsink=%s\tpath=%s";
+
+ @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+ required = false, multiValued = false)
+ String mcastIp;
+
+ @Override
+ protected void execute() {
+ // Verify mcast group
+ IpAddress mcastGroup = null;
+ if (!isNullOrEmpty(mcastIp)) {
+ mcastGroup = IpAddress.valueOf(mcastIp);
+
+ }
+ // Get SR service
+ SegmentRoutingService srService = get(SegmentRoutingService.class);
+ // Get the mapping
+ Map<McastStoreKey, McastRole> keyToRole = srService.getMcastRoles(mcastGroup);
+ // Reduce to the set of mcast groups
+ Set<IpAddress> mcastGroups = keyToRole.keySet().stream()
+ .map(McastStoreKey::mcastIp)
+ .collect(Collectors.toSet());
+ // Print the trees for each group
+ mcastGroups.forEach(group -> {
+ // Create a new map for the group
+ Multimap<McastRole, DeviceId> roleDeviceIdMap = ArrayListMultimap.create();
+ keyToRole.entrySet()
+ .stream()
+ // Filter only the elements related to this group
+ .filter(entry -> entry.getKey().mcastIp().equals(group))
+ // For each create a new entry in the group related map
+ .forEach(entry -> roleDeviceIdMap.put(entry.getValue(), entry.getKey().deviceId()));
+ // Print the map
+ printMcastRole(group,
+ roleDeviceIdMap.get(INGRESS),
+ roleDeviceIdMap.get(TRANSIT),
+ roleDeviceIdMap.get(EGRESS)
+ );
+ // Get sinks paths
+ Map<ConnectPoint, List<ConnectPoint>> mcastPaths = srService.getMcastPaths(group);
+ // Print the paths
+ mcastPaths.forEach(this::printMcastSink);
+ });
+ }
+
+ private void printMcastRole(IpAddress mcastGroup,
+ Collection<DeviceId> ingress,
+ Collection<DeviceId> transit,
+ Collection<DeviceId> egress) {
+ print(G_FORMAT_MAPPING, mcastGroup, ingress, transit, egress);
+ }
+
+ private void printMcastSink(ConnectPoint sink, List<ConnectPoint> path) {
+ print(S_FORMAT_MAPPING, sink, path);
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java
new file mode 100644
index 0000000..2b43de5
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.cli;
+
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Map;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+
+/**
+ * Command to read the current state of the DestinationSetNextObjectiveStore.
+ *
+ */
+@Command(scope = "onos", name = "sr-next-hops",
+ description = "Displays the current next-hops seen by each switch "
+ + "towards a set of destinations and the next-id it maps to")
+public class NextHopCommand extends AbstractShellCommand {
+
+ private static final String FORMAT_MAPPING = " %s";
+
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+ printDestinationSet(srService.getDestinationSet());
+ }
+
+ private void printDestinationSet(Map<DestinationSetNextObjectiveStoreKey,
+ NextNeighbors> ds) {
+ ArrayList<DestinationSetNextObjectiveStoreKey> a = new ArrayList<>();
+ ds.keySet().forEach(key -> a.add(key));
+ a.sort(new Comp());
+
+ StringBuilder dsbldr = new StringBuilder();
+ for (int i = 0; i < a.size(); i++) {
+ dsbldr.append("\n " + a.get(i));
+ dsbldr.append(" --> via: " + ds.get(a.get(i)));
+ }
+ print(FORMAT_MAPPING, dsbldr.toString());
+ }
+
+ static class Comp implements Comparator<DestinationSetNextObjectiveStoreKey> {
+
+ @Override
+ public int compare(DestinationSetNextObjectiveStoreKey o1,
+ DestinationSetNextObjectiveStoreKey o2) {
+ int res = o1.deviceId().toString().compareTo(o2.deviceId().toString());
+ if (res < 0) {
+ return -1;
+ } else if (res > 0) {
+ return +1;
+ }
+ return 0;
+ }
+
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java
new file mode 100644
index 0000000..605ba83
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015-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.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.Policy;
+import org.onosproject.segmentrouting.PolicyHandler;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to add a new policy.
+ */
+@Command(scope = "onos", name = "sr-policy-add",
+ description = "Create a new policy")
+public class PolicyAddCommand extends AbstractShellCommand {
+
+ // TODO: Need to support skipping some parameters
+
+ @Argument(index = 0, name = "ID",
+ description = "policy ID",
+ required = true, multiValued = false)
+ String policyId;
+
+ @Argument(index = 1, name = "priority",
+ description = "priority",
+ required = true, multiValued = false)
+ int priority;
+
+ @Argument(index = 2, name = "src_IP",
+ description = "src IP",
+ required = false, multiValued = false)
+ String srcIp;
+
+ @Argument(index = 3, name = "src_port",
+ description = "src port",
+ required = false, multiValued = false)
+ short srcPort;
+
+ @Argument(index = 4, name = "dst_IP",
+ description = "dst IP",
+ required = false, multiValued = false)
+ String dstIp;
+
+ @Argument(index = 5, name = "dst_port",
+ description = "dst port",
+ required = false, multiValued = false)
+ short dstPort;
+
+ @Argument(index = 6, name = "proto",
+ description = "IP protocol",
+ required = false, multiValued = false)
+ String proto;
+
+ @Argument(index = 7, name = "policy_type",
+ description = "policy type",
+ required = true, multiValued = false)
+ String policyType;
+
+ @Argument(index = 8, name = "tunnel_ID",
+ description = "tunnel ID",
+ required = false, multiValued = false)
+ String tunnelId;
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ TunnelPolicy.Builder tpb = TunnelPolicy.builder().setPolicyId(policyId);
+ tpb.setPriority(priority);
+ tpb.setType(Policy.Type.valueOf(policyType));
+
+ if (srcIp != null) {
+ tpb.setSrcIp(srcIp);
+ }
+ if (dstIp != null) {
+ tpb.setDstIp(dstIp);
+ }
+ if (srcPort != 0) {
+ tpb.setSrcPort(srcPort);
+ }
+ if (dstPort != 0) {
+ tpb.setDstPort(dstPort);
+ }
+ if (!"ip".equals(proto)) {
+ tpb.setIpProto(proto);
+ }
+ if (Policy.Type.valueOf(policyType) == Policy.Type.TUNNEL_FLOW) {
+ if (tunnelId == null) {
+ error("tunnel ID must be specified for TUNNEL_FLOW policy");
+ return;
+ }
+ tpb.setTunnelId(tunnelId);
+ }
+ PolicyHandler.Result result = srService.createPolicy(tpb.build());
+
+ switch (result) {
+ case POLICY_EXISTS:
+ error("the same policy exists");
+ break;
+ case ID_EXISTS:
+ error("the same policy ID exists");
+ break;
+ case TUNNEL_NOT_FOUND:
+ error("the tunnel is not found");
+ break;
+ case UNSUPPORTED_TYPE:
+ error("the policy type specified is not supported");
+ break;
+ default:
+ break;
+ }
+
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java
new file mode 100644
index 0000000..6c91b8b
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015-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.Policy;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to show the list of policies.
+ */
+@Command(scope = "onos", name = "sr-policy-list",
+ description = "Lists all policies")
+public class PolicyListCommand extends AbstractShellCommand {
+
+ private static final String FORMAT_MAPPING_TUNNEL =
+ " id=%s, type=%s, prio=%d, src=%s, port=%d, dst=%s, port=%d, proto=%s, tunnel=%s";
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ srService.getPolicies().forEach(policy -> printPolicy(policy));
+ }
+
+ private void printPolicy(Policy policy) {
+ if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+ print(FORMAT_MAPPING_TUNNEL, policy.id(), policy.type(), policy.priority(),
+ policy.srcIp(), policy.srcPort(), policy.dstIp(), policy.dstPort(),
+ (policy.ipProto() == null) ? "" : policy.ipProto(),
+ ((TunnelPolicy) policy).tunnelId());
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java
new file mode 100644
index 0000000..cdc3034
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2015-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.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.PolicyHandler;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to remove a policy.
+ */
+@Command(scope = "onos", name = "sr-policy-remove",
+ description = "Remove a policy")
+public class PolicyRemoveCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "policy ID",
+ description = "policy ID",
+ required = true, multiValued = false)
+ String policyId;
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ TunnelPolicy.Builder tpb = TunnelPolicy.builder().setPolicyId(policyId);
+ PolicyHandler.Result result = srService.removePolicy(tpb.build());
+ if (result == PolicyHandler.Result.POLICY_NOT_FOUND) {
+ print("ERROR: the policy is not found");
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java
new file mode 100644
index 0000000..d916823
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java
@@ -0,0 +1,105 @@
+/*
+ * 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.onosproject.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+
+
+/**
+ * Command to add a pseuwodire.
+ */
+@Command(scope = "onos", name = "pseudowire-add",
+ description = "Add a pseudowire to the network configuration, if it already exists update it.")
+public class PseudowireAddCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "pwId",
+ description = "Pseudowire ID",
+ required = true, multiValued = false)
+ String pwId;
+
+ @Argument(index = 1, name = "pwLabel",
+ description = "Pseudowire Label",
+ required = true, multiValued = false)
+ String pwLabel;
+
+ @Argument(index = 2, name = "mode",
+ description = "Mode used for pseudowire",
+ required = true, multiValued = false)
+ String mode;
+
+ @Argument(index = 3, name = "sDTag",
+ description = "Service delimiting tag",
+ required = true, multiValued = false)
+ String sDTag;
+
+ @Argument(index = 4, name = "cP1",
+ description = "Connection Point 1",
+ required = true, multiValued = false)
+ String cP1;
+
+ @Argument(index = 5, name = "cP1InnerVlan",
+ description = "Inner Vlan of Connection Point 1",
+ required = true, multiValued = false)
+ String cP1InnerVlan;
+
+ @Argument(index = 6, name = "cP1OuterVlan",
+ description = "Outer Vlan of Connection Point 1",
+ required = true, multiValued = false)
+ String cP1OuterVlan;
+
+ @Argument(index = 7, name = "cP2",
+ description = "Connection Point 2",
+ required = true, multiValued = false)
+ String cP2;
+
+ @Argument(index = 8, name = "cP2InnerVlan",
+ description = "Inner Vlan of Connection Point 2",
+ required = true, multiValued = false)
+ String cP2InnerVlan;
+
+ @Argument(index = 9, name = "cP2OuterVlan",
+ description = "Outer Vlan of Connection Point 2",
+ required = true, multiValued = false)
+ String cP2OuterVlan;
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ L2TunnelHandler.Result res = srService.addPseudowire(pwId, pwLabel,
+ cP1, cP1InnerVlan, cP1OuterVlan,
+ cP2, cP2InnerVlan, cP2OuterVlan,
+ mode, sDTag);
+ switch (res) {
+ case ADDITION_ERROR:
+ print("Pseudowire could not be added, error in configuration, please check logs for more details!");
+ break;
+ case CONFIG_NOT_FOUND:
+ print("Configuration for pwaas was not found! Initialize the configuration first through netcfg.");
+ break;
+ default:
+ break;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java
new file mode 100644
index 0000000..b44ca76
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014-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.console.Completer;
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.stream.Collectors;
+
+/**
+ * Device ID completer.
+ */
+public class PseudowireIdCompleter implements Completer {
+ @Override
+ public int complete(String buffer, int cursor, List<String> candidates) {
+ // Delegate string completer
+ StringsCompleter delegate = new StringsCompleter();
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+
+ List<L2Tunnel> tunnels = srService.getL2Tunnels();
+
+ // combine polices and tunnels to pseudowires
+ Iterator<String> pseudowires = tunnels.stream()
+ .map(l2Tunnel -> Long.toString(l2Tunnel.tunnelId()))
+ .collect(Collectors.toList()).iterator();
+
+ SortedSet<String> strings = delegate.getStrings();
+ while (pseudowires.hasNext()) {
+ strings.add(pseudowires.next());
+ }
+
+ // Now let the completer do the work for figuring out what to offer.
+ return delegate.complete(buffer, cursor, candidates);
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java
new file mode 100644
index 0000000..c48f633
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java
@@ -0,0 +1,84 @@
+/*
+ * 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.onosproject.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Command to show the pseudowires.
+ */
+@Command(scope = "onos", name = "pseudowires",
+ description = "Lists all pseudowires")
+public class PseudowireListCommand extends AbstractShellCommand {
+
+ private static final String FORMAT_PSEUDOWIRE =
+ "Pseudowire id = %s \n" +
+ " mode : %s, sdTag : %s, pwLabel : %s \n" +
+ " cP1 : %s , cP1OuterTag : %s, cP1InnerTag : %s \n" +
+ " cP2 : %s , cP2OuterTag : %s, cP2InnerTag : %s \n" +
+ " transportVlan : %s";
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ List<L2Tunnel> tunnels = srService.getL2Tunnels();
+ List<L2TunnelPolicy> policies = srService.getL2Policies();
+
+ // combine polices and tunnels to pseudowires
+ List<L2TunnelDescription> pseudowires = tunnels.stream()
+ .map(l2Tunnel -> {
+ L2TunnelPolicy policy = null;
+ for (L2TunnelPolicy l2Policy : policies) {
+ if (l2Policy.tunnelId() == l2Tunnel.tunnelId()) {
+ policy = l2Policy;
+ break;
+ }
+ }
+
+ return new DefaultL2TunnelDescription(l2Tunnel, policy);
+ })
+ .collect(Collectors.toList());
+
+ pseudowires.forEach(pw -> printPseudowire(pw));
+ }
+
+ private void printPseudowire(L2TunnelDescription pseudowire) {
+
+
+ VlanId vlan = pseudowire.l2Tunnel().transportVlan().equals(VlanId.vlanId((short) 4094)) ?
+ VlanId.NONE : pseudowire.l2Tunnel().transportVlan();
+
+ print(FORMAT_PSEUDOWIRE, pseudowire.l2Tunnel().tunnelId(), pseudowire.l2Tunnel().pwMode(),
+ pseudowire.l2Tunnel().sdTag(), pseudowire.l2Tunnel().pwLabel(),
+ pseudowire.l2TunnelPolicy().cP1(), pseudowire.l2TunnelPolicy().cP1OuterTag(),
+ pseudowire.l2TunnelPolicy().cP1InnerTag(), pseudowire.l2TunnelPolicy().cP2(),
+ pseudowire.l2TunnelPolicy().cP2OuterTag(), pseudowire.l2TunnelPolicy().cP2InnerTag(),
+ vlan);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.java
new file mode 100644
index 0000000..098bbf9
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.java
@@ -0,0 +1,60 @@
+/*
+ * 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.onosproject.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+
+
+/**
+ * Command to remove a pseudowire.
+ */
+@Command(scope = "onos", name = "pseudowire-remove",
+ description = "Remove a pseudowire")
+public class PseudowireRemoveCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "pwId",
+ description = "pseudowire ID",
+ required = true, multiValued = false)
+ String pwId;
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ // remove the pseudowire
+ SegmentRoutingManager mngr = (SegmentRoutingManager) srService;
+ L2TunnelHandler.Result res = mngr.removePseudowire(pwId);
+
+ switch (res) {
+ case REMOVAL_ERROR:
+ error("Error in deletion, pseudowire not found!");
+ break;
+ case CONFIG_NOT_FOUND:
+ error("Could not fetch pseudowire class configuration!");
+ break;
+ default:
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java
new file mode 100644
index 0000000..40525f2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Command to manually trigger routing and rule-population in the network.
+ *
+ */
+@Command(scope = "onos", name = "sr-reroute-network",
+ description = "Repopulate routing rules given current network state")
+public class RerouteNetworkCommand extends AbstractShellCommand {
+
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+ srService.rerouteNetwork();
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java
new file mode 100644
index 0000000..6b3e1fc
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015-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.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.DefaultTunnel;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+import org.onosproject.segmentrouting.TunnelHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Command to add a new tunnel.
+ */
+@Command(scope = "onos", name = "sr-tunnel-add",
+ description = "Create a new tunnel")
+public class TunnelAddCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "tunnel ID",
+ description = "tunnel ID",
+ required = true, multiValued = false)
+ String tunnelId;
+
+ @Argument(index = 1, name = "label path",
+ description = "label path",
+ required = true, multiValued = false)
+ String labels;
+
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ List<Integer> labelIds = new ArrayList<>();
+ StringTokenizer strToken = new StringTokenizer(labels, ",");
+ while (strToken.hasMoreTokens()) {
+ labelIds.add(Integer.valueOf(strToken.nextToken()));
+ }
+ Tunnel tunnel = new DefaultTunnel(tunnelId, labelIds);
+
+ TunnelHandler.Result result = srService.createTunnel(tunnel);
+ switch (result) {
+ case ID_EXISTS:
+ print("ERROR: the same tunnel ID exists");
+ break;
+ case TUNNEL_EXISTS:
+ print("ERROR: the same tunnel exists");
+ break;
+ case INTERNAL_ERROR:
+ print("ERROR: internal tunnel creation error");
+ break;
+ case WRONG_PATH:
+ print("ERROR: the tunnel path is wrong");
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java
new file mode 100644
index 0000000..7984eb4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015-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.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+
+/**
+ * Command to show the list of tunnels.
+ */
+@Command(scope = "onos", name = "sr-tunnel-list",
+ description = "Lists all tunnels")
+public class TunnelListCommand extends AbstractShellCommand {
+
+ private static final String FORMAT_MAPPING =
+ " id=%s, path=%s";
+
+ @Override
+ protected void execute() {
+
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ srService.getTunnels().forEach(tunnel -> printTunnel(tunnel));
+ }
+
+ private void printTunnel(Tunnel tunnel) {
+ print(FORMAT_MAPPING, tunnel.id(), tunnel.labelIds());
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java
new file mode 100644
index 0000000..b9a7edb
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-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.Lists;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.DefaultTunnel;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+import org.onosproject.segmentrouting.TunnelHandler;
+
+/**
+ * Command to remove a tunnel.
+ */
+@Command(scope = "onos", name = "sr-tunnel-remove",
+ description = "Remove a tunnel")
+public class TunnelRemoveCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "tunnel ID",
+ description = "tunnel ID",
+ required = true, multiValued = false)
+ String tunnelId;
+
+ @Override
+ protected void execute() {
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ Tunnel tunnel = new DefaultTunnel(tunnelId, Lists.newArrayList());
+ TunnelHandler.Result result = srService.removeTunnel(tunnel);
+ switch (result) {
+ case TUNNEL_IN_USE:
+ print("ERROR: the tunnel is still in use");
+ break;
+ case TUNNEL_NOT_FOUND:
+ print("ERROR: the tunnel is not found");
+ break;
+ default:
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java
new file mode 100644
index 0000000..3e89ca4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java
@@ -0,0 +1,55 @@
+/*
+ * 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.onosproject.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Triggers the verification of hashed group buckets in the specified device,
+ * and corrects the buckets if necessary. Outcome can be viewed in the 'groups'
+ * command.
+ */
+@Command(scope = "onos", name = "sr-verify-groups",
+ description = "Triggers the verification of hashed groups in the specified "
+ + "device. Does not return any output; users can query the results "
+ + "in the 'groups' command")
+public class VerifyGroupsCommand extends AbstractShellCommand {
+
+ @Argument(index = 0, name = "uri", description = "Device ID",
+ required = true, multiValued = false)
+ String uri = null;
+
+ @Override
+ protected void execute() {
+ DeviceService deviceService = get(DeviceService.class);
+ SegmentRoutingService srService =
+ AbstractShellCommand.get(SegmentRoutingService.class);
+
+ if (uri != null) {
+ Device dev = deviceService.getDevice(DeviceId.deviceId(uri));
+ if (dev != null) {
+ srService.verifyGroups(dev.id());
+ }
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java
new file mode 100644
index 0000000..0f2cea6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing application CLI handlers.
+ */
+package org.onosproject.segmentrouting.cli;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java
new file mode 100644
index 0000000..a5c5557
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java
@@ -0,0 +1,213 @@
+/*
+ * 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.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.config.Config;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Network config to describe ports that should be blocked until authenticated.
+ */
+public class BlockedPortsConfig extends Config<ApplicationId> {
+
+ /**
+ * Returns the top level keys to the config,
+ * which should be device ID strings.
+ *
+ * @return this list of top level keys
+ */
+ public List<String> deviceIds() {
+ List<String> devIds = new ArrayList<>();
+ if (object != null) {
+ Iterator<String> it = object.fieldNames();
+ if (it != null) {
+ it.forEachRemaining(devIds::add);
+ }
+ }
+ return devIds;
+ }
+
+ /**
+ * Returns the port range strings associated with the given device id key.
+ *
+ * @param deviceId the device id key
+ * @return the associated port range strings
+ */
+ public List<String> portRanges(String deviceId) {
+ List<String> portRanges = new ArrayList<>();
+ if (object != null) {
+ JsonNode jnode = object.get(deviceId);
+ if (ArrayNode.class.isInstance(jnode)) {
+ ArrayNode array = (ArrayNode) jnode;
+ array.forEach(pr -> portRanges.add(pr.asText()));
+ }
+ }
+ return portRanges;
+ }
+
+ /**
+ * Returns an iterator over the port numbers defined by the port ranges
+ * defined in the configuration, for the given device.
+ *
+ * @param deviceId the specific device
+ * @return an iterator over the configured ports
+ */
+ public Iterator<Long> portIterator(String deviceId) {
+ List<String> ranges = portRanges(deviceId);
+ return new PortIterator(ranges);
+ }
+
+ /**
+ * Private implementation of an iterator that aggregates several range
+ * iterators into a single iterator.
+ */
+ class PortIterator implements Iterator<Long> {
+ private final List<Range> ranges;
+ private final int nRanges;
+ private int currentRange = 0;
+ private Iterator<Long> iterator;
+
+ PortIterator(List<String> rangeSpecs) {
+ nRanges = rangeSpecs.size();
+ ranges = new ArrayList<>(nRanges);
+ if (nRanges > 0) {
+ for (String rs : rangeSpecs) {
+ ranges.add(new Range(rs));
+ }
+ iterator = ranges.get(0).iterator();
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return nRanges > 0 &&
+ (currentRange < nRanges - 1 ||
+ (currentRange < nRanges && iterator.hasNext()));
+ }
+
+ @Override
+ public Long next() {
+ if (nRanges == 0) {
+ throw new NoSuchElementException();
+ }
+
+ Long value;
+ if (iterator.hasNext()) {
+ value = iterator.next();
+ } else {
+ currentRange++;
+ if (currentRange < nRanges) {
+ iterator = ranges.get(currentRange).iterator();
+ value = iterator.next();
+ } else {
+ throw new NoSuchElementException();
+ }
+ }
+ return value;
+ }
+ }
+
+ /**
+ * Private implementation of a "range" of long numbers, defined by a
+ * string of the form {@code "<lo>-<hi>"}, for example, "17-32".
+ */
+ static final class Range {
+ private static final Pattern RE_SINGLE = Pattern.compile("(\\d+)");
+ private static final Pattern RE_RANGE = Pattern.compile("(\\d+)-(\\d+)");
+ private static final String E_BAD_FORMAT = "Bad Range Format ";
+
+ private final long lo;
+ private final long hi;
+
+ /**
+ * Constructs a range from the given string definition.
+ * For example:
+ * <pre>
+ * Range r = new Range("17-32");
+ * </pre>
+ *
+ * @param s the string representation of the range
+ * @throws IllegalArgumentException if the range string is malformed
+ */
+ Range(String s) {
+ String lohi = s;
+ Matcher m = RE_SINGLE.matcher(s);
+ if (m.matches()) {
+ lohi = s + "-" + s;
+ }
+ m = RE_RANGE.matcher(lohi);
+ if (!m.matches()) {
+ throw new IllegalArgumentException(E_BAD_FORMAT + s);
+ }
+ try {
+ lo = Long.parseLong(m.group(1));
+ hi = Long.parseLong(m.group(2));
+
+ if (hi < lo) {
+ throw new IllegalArgumentException(E_BAD_FORMAT + s);
+ }
+ } catch (NumberFormatException nfe) {
+ // unlikely to be thrown, since the matcher will have failed first
+ throw new IllegalArgumentException(E_BAD_FORMAT + s, nfe);
+ }
+ }
+
+
+ /**
+ * Returns an iterator over this range, starting from the lowest value
+ * and iterating up to the highest value (inclusive).
+ *
+ * @return an iterator over this range
+ */
+ Iterator<Long> iterator() {
+ return new RangeIterator();
+ }
+
+ /**
+ * Private implementation of an iterator over the range.
+ */
+ class RangeIterator implements Iterator<Long> {
+ long current;
+
+ RangeIterator() {
+ current = lo - 1;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return current < hi;
+ }
+
+ @Override
+ public Long next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return ++current;
+ }
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java
new file mode 100644
index 0000000..773c7f6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015-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.config;
+
+/**
+ * Signals that an error occurred during reading device configuration.
+ */
+public class DeviceConfigNotFoundException extends Exception {
+
+ /**
+ * Creates a new ConfigNotFoundException with the given message.
+ *
+ * @param message exception message
+ */
+ public DeviceConfigNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
new file mode 100644
index 0000000..7ea430d
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright 2015-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.config;
+
+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.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.SetMultimap;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.ConfigException;
+import org.onosproject.net.config.basics.InterfaceConfig;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Segment Routing configuration component that reads the
+ * segment routing related configuration from Network Configuration Manager
+ * component and organizes in more accessible formats.
+ */
+public class DeviceConfiguration implements DeviceProperties {
+
+ private static final String NO_SUBNET = "No subnet configured on {}";
+
+ private static final Logger log = LoggerFactory.getLogger(DeviceConfiguration.class);
+ private final List<Integer> allSegmentIds = new ArrayList<>();
+ private final Map<DeviceId, SegmentRouterInfo> deviceConfigMap = new ConcurrentHashMap<>();
+ private SegmentRoutingManager srManager;
+
+ private class SegmentRouterInfo {
+ int ipv4NodeSid;
+ int ipv6NodeSid;
+ DeviceId deviceId;
+ Ip4Address ipv4Loopback;
+ Ip6Address ipv6Loopback;
+ MacAddress mac;
+ boolean isEdge;
+ SetMultimap<PortNumber, IpAddress> gatewayIps;
+ SetMultimap<PortNumber, IpPrefix> subnets;
+ Map<Integer, Set<Integer>> adjacencySids;
+ DeviceId pairDeviceId;
+ PortNumber pairLocalPort;
+ int pwRoutingLabel;
+
+ public SegmentRouterInfo() {
+ gatewayIps = HashMultimap.create();
+ subnets = HashMultimap.create();
+ }
+ }
+
+ /**
+ * Constructs device configuration for all Segment Router devices,
+ * organizing the data into various maps for easier access.
+ *
+ * @param srManager Segment Routing Manager
+ */
+ public DeviceConfiguration(SegmentRoutingManager srManager) {
+ this.srManager = srManager;
+ updateConfig();
+ }
+
+ public void updateConfig() {
+ // Read config from device subject, excluding gatewayIps and subnets.
+ Set<DeviceId> deviceSubjects =
+ srManager.cfgService.getSubjects(DeviceId.class, SegmentRoutingDeviceConfig.class);
+ deviceSubjects.forEach(subject -> {
+ SegmentRoutingDeviceConfig config =
+ srManager.cfgService.getConfig(subject, SegmentRoutingDeviceConfig.class);
+ SegmentRouterInfo info = new SegmentRouterInfo();
+ info.deviceId = subject;
+ info.ipv4NodeSid = config.nodeSidIPv4();
+ info.ipv6NodeSid = config.nodeSidIPv6();
+ info.ipv4Loopback = config.routerIpv4();
+ info.ipv6Loopback = config.routerIpv6();
+ info.mac = config.routerMac();
+ info.isEdge = config.isEdgeRouter();
+ info.adjacencySids = config.adjacencySids();
+ info.pairDeviceId = config.pairDeviceId();
+ info.pairLocalPort = config.pairLocalPort();
+ info.pwRoutingLabel = info.ipv4NodeSid + 1000;
+ deviceConfigMap.put(info.deviceId, info);
+ log.debug("Read device config for device: {}", info.deviceId);
+ /*
+ * IPv6 sid is not inserted. this part of the code is not used for now.
+ */
+ allSegmentIds.add(info.ipv4NodeSid);
+ });
+
+ // Read gatewayIps and subnets from port subject. Ignore suppressed ports.
+ Set<ConnectPoint> portSubjects = srManager.cfgService
+ .getSubjects(ConnectPoint.class, InterfaceConfig.class);
+ portSubjects.stream()
+ .filter(subject -> deviceConfigMap.containsKey(subject.deviceId()))
+ .filter(subject -> !isSuppressedPort(subject)).forEach(subject -> {
+ InterfaceConfig config =
+ srManager.cfgService.getConfig(subject, InterfaceConfig.class);
+ Set<Interface> networkInterfaces;
+ try {
+ networkInterfaces = config.getInterfaces();
+ } catch (ConfigException e) {
+ log.error("Error loading port configuration");
+ return;
+ }
+ networkInterfaces.forEach(networkInterface -> {
+ VlanId vlanId = networkInterface.vlan();
+ ConnectPoint connectPoint = networkInterface.connectPoint();
+ DeviceId dpid = connectPoint.deviceId();
+ PortNumber port = connectPoint.port();
+ MacAddress mac = networkInterface.mac();
+ SegmentRouterInfo info = deviceConfigMap.get(dpid);
+
+ // skip if there is no corresponding device for this ConenctPoint
+ if (info != null) {
+ // Extract subnet information
+ List<InterfaceIpAddress> interfaceAddresses = networkInterface.ipAddressesList();
+ interfaceAddresses.forEach(interfaceAddress -> {
+ // Do not add /0, /32 and /128 to gateway IP list
+ int prefixLength = interfaceAddress.subnetAddress().prefixLength();
+ IpPrefix ipPrefix = interfaceAddress.subnetAddress();
+ if (ipPrefix.isIp4()) {
+ if (prefixLength != 0 && prefixLength != IpPrefix.MAX_INET_MASK_LENGTH) {
+ info.gatewayIps.put(port, interfaceAddress.ipAddress());
+ }
+ info.subnets.put(port, interfaceAddress.subnetAddress());
+ } else {
+ if (prefixLength != 0 && prefixLength != IpPrefix.MAX_INET6_MASK_LENGTH) {
+ info.gatewayIps.put(port, interfaceAddress.ipAddress());
+ }
+ info.subnets.put(port, interfaceAddress.subnetAddress());
+ }
+ });
+
+ // Override interface mac with router mac
+ if (!mac.equals(info.mac)) {
+ ArrayNode array = (ArrayNode) config.node();
+ for (JsonNode intfNode : array) {
+ ObjectNode objNode = (ObjectNode) intfNode;
+ objNode.put(InterfaceConfig.MAC, info.mac.toString());
+ }
+ srManager.cfgService.applyConfig(connectPoint, InterfaceConfig.class, array);
+ }
+ }
+ });
+ // We register the connect point with the NRS.
+ srManager.registerConnectPoint(subject);
+ });
+ }
+
+ public Collection<DeviceId> getRouters() {
+ return deviceConfigMap.keySet();
+ }
+
+ @Override
+ public boolean isConfigured(DeviceId deviceId) {
+ return deviceConfigMap.get(deviceId) != null;
+ }
+
+ @Override
+ public int getIPv4SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ log.info("DEVICE MAP IS {}", deviceConfigMap);
+ if (srinfo != null) {
+ log.trace("getIPv4SegmentId for device{} is {}", deviceId, srinfo.ipv4NodeSid);
+ return srinfo.ipv4NodeSid;
+ } else {
+ String message = "getIPv4SegmentId fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public int getIPv6SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("getIPv6SegmentId for device{} is {}", deviceId, srinfo.ipv6NodeSid);
+ return srinfo.ipv6NodeSid;
+ } else {
+ String message = "getIPv6SegmentId fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public int getPWRoutingLabel(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("pwRoutingLabel for device{} is {}", deviceId, srinfo.pwRoutingLabel);
+ return srinfo.pwRoutingLabel;
+ } else {
+ String message = "getPWRoutingLabel fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ /**
+ * Returns the IPv4 Node segment id of a segment router given its Router mac address.
+ *
+ * @param routerMac router mac address
+ * @return node segment id, or -1 if not found in config
+ */
+ public int getIPv4SegmentId(MacAddress routerMac) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().mac.equals(routerMac)) {
+ return entry.getValue().ipv4NodeSid;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the IPv6 Node segment id of a segment router given its Router mac address.
+ *
+ * @param routerMac router mac address
+ * @return node segment id, or -1 if not found in config
+ */
+ public int getIPv6SegmentId(MacAddress routerMac) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().mac.equals(routerMac)) {
+ return entry.getValue().ipv6NodeSid;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the IPv4 Node segment id of a segment router given its Router ip address.
+ *
+ * @param routerAddress router ip address
+ * @return node segment id, or -1 if not found in config
+ */
+ public int getIPv4SegmentId(Ip4Address routerAddress) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().ipv4Loopback.equals(routerAddress)) {
+ return entry.getValue().ipv4NodeSid;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the IPv6 Node segment id of a segment router given its Router ip address.
+ *
+ * @param routerAddress router ip address
+ * @return node segment id, or -1 if not found in config
+ */
+ public int getIPv6SegmentId(Ip6Address routerAddress) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().ipv6Loopback.equals(routerAddress)) {
+ return entry.getValue().ipv6NodeSid;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public MacAddress getDeviceMac(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ return srinfo.mac;
+ } else {
+ String message = "getDeviceMac fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public Ip4Address getRouterIpv4(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("getRouterIpv4 for device{} is {}", deviceId, srinfo.ipv4Loopback);
+ return srinfo.ipv4Loopback;
+ } else {
+ String message = "getRouterIpv4 fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public Ip6Address getRouterIpv6(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("getRouterIpv6 for device{} is {}", deviceId, srinfo.ipv6Loopback);
+ return srinfo.ipv6Loopback;
+ } else {
+ String message = "getRouterIpv6 fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public boolean isEdgeDevice(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("isEdgeDevice for device{} is {}", deviceId, srinfo.isEdge);
+ return srinfo.isEdge;
+ } else {
+ String message = "isEdgeDevice fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ @Override
+ public List<Integer> getAllDeviceSegmentIds() {
+ return allSegmentIds;
+ }
+
+ @Override
+ public Map<IpPrefix, List<PortNumber>> getSubnetPortsMap(DeviceId deviceId)
+ throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo == null) {
+ String message = "getSubnetPortsMap fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ // Construct subnet-port mapping from port-subnet mapping
+ SetMultimap<PortNumber, IpPrefix> portSubnetMap = srinfo.subnets;
+ Map<IpPrefix, List<PortNumber>> subnetPortMap = new HashMap<>();
+
+ portSubnetMap.entries().forEach(entry -> {
+ PortNumber port = entry.getKey();
+ IpPrefix subnet = entry.getValue();
+
+ if (subnet.prefixLength() == IpPrefix.MAX_INET_MASK_LENGTH ||
+ subnet.prefixLength() == IpPrefix.MAX_INET6_MASK_LENGTH) {
+ return;
+ }
+
+ if (subnetPortMap.containsKey(subnet)) {
+ subnetPortMap.get(subnet).add(port);
+ } else {
+ ArrayList<PortNumber> ports = new ArrayList<>();
+ ports.add(port);
+ subnetPortMap.put(subnet, ports);
+ }
+ });
+ return subnetPortMap;
+ }
+
+ /**
+ * Returns the device identifier or data plane identifier (dpid)
+ * of a segment router given its segment id.
+ *
+ * @param sid segment id
+ * @return deviceId device identifier
+ */
+ public DeviceId getDeviceId(int sid) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().ipv4NodeSid == sid ||
+ entry.getValue().ipv6NodeSid == sid) {
+ return entry.getValue().deviceId;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the device identifier or data plane identifier (dpid)
+ * of a segment router given its router ip address.
+ *
+ * @param ipAddress router ip address
+ * @return deviceId device identifier
+ */
+ public DeviceId getDeviceId(Ip4Address ipAddress) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().ipv4Loopback.equals(ipAddress)) {
+ return entry.getValue().deviceId;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the device identifier or data plane identifier (dpid)
+ * of a segment router given its router ipv6 address.
+ *
+ * @param ipAddress router ipv6 address
+ * @return deviceId device identifier
+ */
+ public DeviceId getDeviceId(Ip6Address ipAddress) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().ipv6Loopback.equals(ipAddress)) {
+ return entry.getValue().deviceId;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the configured port ip addresses for a segment router.
+ * These addresses serve as gateway IP addresses for the subnets configured
+ * on those ports.
+ *
+ * @param deviceId device identifier
+ * @return immutable set of ip addresses configured on the ports or null if not found
+ */
+ public Set<IpAddress> getPortIPs(DeviceId deviceId) {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ log.trace("getSubnetGatewayIps for device{} is {}", deviceId,
+ srinfo.gatewayIps.values());
+ return ImmutableSet.copyOf(srinfo.gatewayIps.values());
+ }
+ return null;
+ }
+
+ /**
+ * Returns the configured subnet prefixes for a segment router.
+ *
+ * @param deviceId device identifier
+ * @return list of ip prefixes or null if not found
+ */
+ public Set<IpPrefix> getSubnets(DeviceId deviceId) {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ ImmutableSet.Builder<IpPrefix> builder = ImmutableSet.builder();
+ return builder.addAll(srinfo.subnets.values()).build();
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns the subnet configuration of given device and port.
+ *
+ * @param deviceId Device ID
+ * @param port Port number
+ * @return The subnets configured on given port or empty set if
+ * the port is unconfigured or suppressed.
+ */
+ public Set<IpPrefix> getPortSubnets(DeviceId deviceId, PortNumber port) {
+ ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+
+ if (isSuppressedPort(connectPoint)) {
+ return Collections.emptySet();
+ }
+
+ Set<IpPrefix> subnets =
+ srManager.interfaceService.getInterfacesByPort(connectPoint).stream()
+ .flatMap(intf -> intf.ipAddressesList().stream())
+ .map(InterfaceIpAddress::subnetAddress)
+ .collect(Collectors.toSet());
+
+ if (subnets.isEmpty()) {
+ log.debug(NO_SUBNET, connectPoint);
+ return Collections.emptySet();
+ }
+
+ return subnets;
+ }
+
+ /**
+ * Returns all ports that has a subnet that contains any of the given IP addresses.
+ *
+ * @param ips a set of IP addresses
+ * @return a set of connect point that has a subnet that contains any of the given IP addresses
+ */
+ public Set<ConnectPoint> getPortByIps(Set<IpAddress> ips) {
+ return srManager.interfaceService.getInterfaces().stream()
+ .filter(intf -> intf.ipAddressesList().stream().anyMatch(intfAddress ->
+ ips.stream().anyMatch(ip -> intfAddress.subnetAddress().contains(ip))))
+ .map(Interface::connectPoint)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the router ip address of segment router that has the
+ * specified ip address in its subnets.
+ *
+ * @param destIpAddress target ip address
+ * @return router ip address
+ */
+ public Ip4Address getRouterIpAddressForASubnetHost(Ip4Address destIpAddress) {
+ Interface matchIntf = srManager.interfaceService.getMatchingInterface(destIpAddress);
+
+ if (matchIntf == null) {
+ log.debug("No router was found for {}", destIpAddress);
+ return null;
+ }
+
+ DeviceId routerDeviceId = matchIntf.connectPoint().deviceId();
+ SegmentRouterInfo srInfo = deviceConfigMap.get(routerDeviceId);
+ if (srInfo == null) {
+ log.debug("No device config was found for {}", routerDeviceId);
+ return null;
+ }
+
+ return srInfo.ipv4Loopback;
+ }
+
+ /**
+ * Returns the router ipv6 address of segment router that has the
+ * specified ip address in its subnets.
+ *
+ * @param destIpAddress target ip address
+ * @return router ip address
+ */
+ public Ip6Address getRouterIpAddressForASubnetHost(Ip6Address destIpAddress) {
+ Interface matchIntf = srManager.interfaceService.getMatchingInterface(destIpAddress);
+
+ if (matchIntf == null) {
+ log.debug("No router was found for {}", destIpAddress);
+ return null;
+ }
+
+ DeviceId routerDeviceId = matchIntf.connectPoint().deviceId();
+ SegmentRouterInfo srInfo = deviceConfigMap.get(routerDeviceId);
+ if (srInfo == null) {
+ log.debug("No device config was found for {}", routerDeviceId);
+ return null;
+ }
+
+ return srInfo.ipv6Loopback;
+ }
+
+ /**
+ * Returns the router mac address of segment router that has the
+ * specified ip address as one of its subnet gateway ip address.
+ *
+ * @param gatewayIpAddress router gateway ip address
+ * @return router mac address or null if not found
+ */
+ public MacAddress getRouterMacForAGatewayIp(IpAddress gatewayIpAddress) {
+ for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+ deviceConfigMap.entrySet()) {
+ if (entry.getValue().gatewayIps.
+ values().contains(gatewayIpAddress)) {
+ return entry.getValue().mac;
+ }
+ }
+
+ log.debug("Cannot find a router for {}", gatewayIpAddress);
+ return null;
+ }
+
+ /**
+ * Checks if the host IP is in any of the subnet defined in the router with the
+ * device ID given.
+ *
+ * @param deviceId device identification of the router
+ * @param hostIp host IP address to check
+ * @return true if the given IP is within any of the subnet defined in the router,
+ * false if no subnet is defined in the router or if the host is not
+ * within any subnet defined in the router
+ */
+ public boolean inSameSubnet(DeviceId deviceId, IpAddress hostIp) {
+
+ Set<IpPrefix> subnets = getSubnets(deviceId);
+ if (subnets == null) {
+ return false;
+ }
+
+ for (IpPrefix subnet: subnets) {
+ // Exclude /0 since it is a special case used for default route
+ if (subnet.prefixLength() != 0 && subnet.contains(hostIp)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if the IP is in the subnet defined on given connect point.
+ *
+ * @param connectPoint Connect point
+ * @param ip The IP address to check
+ * @return True if the IP belongs to the subnet.
+ * False if the IP does not belong to the subnet, or
+ * there is no subnet configuration on given connect point.
+ */
+ public boolean inSameSubnet(ConnectPoint connectPoint, IpAddress ip) {
+ return getPortSubnets(connectPoint.deviceId(), connectPoint.port()).stream()
+ .anyMatch(ipPrefix -> ipPrefix.contains(ip));
+ }
+
+ /**
+ * Returns the ports corresponding to the adjacency Sid given.
+ *
+ * @param deviceId device identification of the router
+ * @param sid adjacency Sid
+ * @return set of port numbers
+ */
+ public Set<Integer> getPortsForAdjacencySid(DeviceId deviceId, int sid) {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ return srinfo != null ?
+ ImmutableSet.copyOf(srinfo.adjacencySids.get(sid)) :
+ ImmutableSet.copyOf(new HashSet<>());
+ }
+
+ /**
+ * Check if the Sid given is whether adjacency Sid of the router device or not.
+ *
+ * @param deviceId device identification of the router
+ * @param sid Sid to check
+ * @return true if the Sid given is the adjacency Sid of the device,
+ * otherwise false
+ */
+ public boolean isAdjacencySid(DeviceId deviceId, int sid) {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ return srinfo != null && srinfo.adjacencySids.containsKey(sid);
+ }
+
+ /**
+ * Add subnet to specific connect point.
+ *
+ * @param cp connect point
+ * @param ipPrefix subnet being added to the device
+ */
+ public void addSubnet(ConnectPoint cp, IpPrefix ipPrefix) {
+ checkNotNull(cp);
+ checkNotNull(ipPrefix);
+ SegmentRouterInfo srinfo = deviceConfigMap.get(cp.deviceId());
+ if (srinfo == null) {
+ log.warn("Device {} is not configured. Abort.", cp.deviceId());
+ return;
+ }
+ srinfo.subnets.put(cp.port(), ipPrefix);
+ }
+
+ /**
+ * Remove subnet from specific connect point.
+ *
+ * @param cp connect point
+ * @param ipPrefix subnet being removed to the device
+ */
+ public void removeSubnet(ConnectPoint cp, IpPrefix ipPrefix) {
+ checkNotNull(cp);
+ checkNotNull(ipPrefix);
+ SegmentRouterInfo srinfo = deviceConfigMap.get(cp.deviceId());
+ if (srinfo == null) {
+ log.warn("Device {} is not configured. Abort.", cp.deviceId());
+ return;
+ }
+ srinfo.subnets.remove(cp.port(), ipPrefix);
+ }
+
+ private boolean isSuppressedPort(ConnectPoint connectPoint) {
+ SegmentRoutingAppConfig appConfig = srManager.cfgService
+ .getConfig(srManager.appId(), SegmentRoutingAppConfig.class);
+ if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+ log.info("Interface configuration on port {} is ignored", connectPoint);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isPairedEdge(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ if (!isEdgeDevice(deviceId)) {
+ return false;
+ }
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ return (srinfo.pairDeviceId == null) ? false : true;
+ }
+
+ public DeviceId getPairDeviceId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ return srinfo.pairDeviceId;
+ } else {
+ String message = "getPairDeviceId fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+ public PortNumber getPairLocalPort(DeviceId deviceId)
+ throws DeviceConfigNotFoundException {
+ SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+ if (srinfo != null) {
+ return srinfo.pairLocalPort;
+ } else {
+ String message = "getPairLocalPort fails for device: " + deviceId + ".";
+ throw new DeviceConfigNotFoundException(message);
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java
new file mode 100644
index 0000000..2d0bbd2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2015-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.config;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mechanism through which group handler module retrieves
+ * the device specific attributes such as segment ID,
+ * Mac address...etc from group handler applications.
+ */
+public interface DeviceProperties {
+ /**
+ * Checks if the device is configured.
+ *
+ * @param deviceId device identifier
+ * @return true if the device is configured
+ */
+ boolean isConfigured(DeviceId deviceId);
+
+ /**
+ * Returns the IPv4 segment id of a device to be used in group creation.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return segment id of a device
+ */
+ int getIPv4SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Returns the IPv6 segment id of a device to be used in group creation.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return segment id of a device
+ */
+ int getIPv6SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Returns the Mac address of a device to be used in group creation.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return mac address of a device
+ */
+ MacAddress getDeviceMac(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return the pseudowire routing label for a leaf node
+ */
+ int getPWRoutingLabel(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Returns the router ipv4 address of a segment router.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return router ip address
+ */
+ IpAddress getRouterIpv4(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Returns the router ipv6 address of a segment router.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return router ip address
+ */
+ IpAddress getRouterIpv6(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Indicates whether a device is edge device or transit/core device.
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return boolean
+ */
+ boolean isEdgeDevice(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+ /**
+ * Returns all segment IDs to be considered in building auto
+ *
+ * created groups.
+ * @return list of segment IDs
+ */
+ List<Integer> getAllDeviceSegmentIds();
+
+ /**
+ * Returns subnet-to-ports mapping of given device.
+ *
+ * For each entry of the map
+ * Key: a subnet
+ * Value: a list of ports, which are bound to the subnet
+ *
+ * @param deviceId device identifier
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return a map that contains all subnet-to-ports mapping of given device
+ */
+ Map<IpPrefix, List<PortNumber>> getSubnetPortsMap(DeviceId deviceId)
+ throws DeviceConfigNotFoundException;
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java
new file mode 100644
index 0000000..31cad7e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java
@@ -0,0 +1,671 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Mode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * App configuration object for Pwaas.
+ */
+public class PwaasConfig extends Config<ApplicationId> {
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public DeviceService deviceService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ public InterfaceService intfService;
+
+ private static Logger log = LoggerFactory
+ .getLogger(PwaasConfig.class);
+
+ private static final String SRC_CP = "cP1";
+ private static final String DST_CP = "cP2";
+ private static final String SRC_OUTER_TAG = "cP1OuterTag";
+ private static final String DST_OUTER_TAG = "cP2OuterTag";
+ private static final String SRC_INNER_TAG = "cP1InnerTag";
+ private static final String DST_INNER_TAG = "cP2InnerTag";
+ private static final String MODE = "mode";
+ private static final String SD_TAG = "sdTag";
+ private static final String PW_LABEL = "pwLabel";
+
+ public PwaasConfig(DeviceService devS, InterfaceService intfS) {
+
+ super();
+
+ deviceService = devS;
+ intfService = intfS;
+ }
+
+ public PwaasConfig() {
+
+ super();
+
+ deviceService = AbstractShellCommand.get(DeviceService.class);
+ intfService = AbstractShellCommand.get(InterfaceService.class);
+ }
+ /**
+ * Error message for missing parameters.
+ */
+ private static final String MISSING_PARAMS = "Missing parameters in pseudo wire description";
+
+ /**
+ * Error message for invalid l2 mode.
+ */
+ private static final String INVALID_L2_MODE = "Invalid pseudo wire mode";
+
+ /**
+ * Error message for invalid VLAN.
+ */
+ private static final String INVALID_VLAN = "Vlan should be either int or */-";
+
+ /**
+ * Error message for invalid PW label.
+ */
+ private static final String INVALID_PW_LABEL = "Pseudowire label should be an integer";
+
+ /**
+ * Verify if the pwaas configuration block is valid.
+ *
+ * Here we try to ensure that the provided pseudowires will get instantiated
+ * correctly in the network. We also check for any collisions with already used
+ * interfaces and also between different pseudowires. Most of the restrictions stem
+ * from the fact that all vlan matching is done in table 10 of ofdpa.
+ *
+ * @return true, if the configuration block is valid.
+ * False otherwise.
+ */
+ @Override
+ public boolean isValid() {
+
+ Set<L2TunnelDescription> pseudowires;
+ try {
+ pseudowires = getPwIds().stream()
+ .map(this::getPwDescription)
+ .collect(Collectors.toSet());
+
+ // check semantics now and return
+ return configurationValidity(pseudowires);
+
+ } catch (IllegalArgumentException e) {
+ log.warn("{}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Helper method to verify if the tunnel is whether or not
+ * supported.
+ *
+ * @param l2Tunnel the tunnel to verify
+ * @return the result of the verification
+ */
+ private void verifyTunnel(L2Tunnel l2Tunnel) {
+
+ // Service delimiting tag not supported yet.
+ if (!l2Tunnel.sdTag().equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.format("Service delimiting tag not supported yet for " +
+ "pseudowire %d.", l2Tunnel.tunnelId()));
+ }
+
+ // Tag mode not supported yet.
+ if (l2Tunnel.pwMode() == L2Mode.TAGGED) {
+ throw new IllegalArgumentException(String.format("Tagged mode not supported yet for pseudowire %d.",
+ l2Tunnel.tunnelId()));
+ }
+
+ // Raw mode without service delimiting tag
+ // is the only mode supported for now.
+ }
+
+ /**
+ * Helper method to verify if the policy is whether or not
+ * supported and if policy will be successfully instantiated in the
+ * network.
+ *
+ * @param ingressInner the ingress inner tag
+ * @param ingressOuter the ingress outer tag
+ * @param egressInner the egress inner tag
+ * @param egressOuter the egress outer tag
+ * @return the result of verification
+ */
+ private void verifyPolicy(ConnectPoint cP1,
+ ConnectPoint cP2,
+ VlanId ingressInner,
+ VlanId ingressOuter,
+ VlanId egressInner,
+ VlanId egressOuter,
+ Long tunnelId) {
+
+ if (cP1.deviceId().equals(cP2.deviceId())) {
+ throw new IllegalArgumentException(String.format("Pseudowire connection points can not reside in the " +
+ "same node, in pseudowire %d.", tunnelId));
+ }
+
+ // We can have multiple tags, all of them can be NONE,
+ // indicating untagged traffic, however, the outer tag can
+ // not have value if the inner tag is None
+ if (ingressInner.equals(VlanId.NONE) && !ingressOuter.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.format("Inner tag should not be empty when " +
+ "outer tag is set for pseudowire %d for cP1.",
+ tunnelId));
+ }
+
+ if (egressInner.equals(VlanId.NONE) && !egressOuter.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Inner tag should not be empty when" +
+ " outer tag is set for pseudowire %d " +
+ "for cP2.", tunnelId)));
+ }
+
+ if (ingressInner.equals(VlanId.ANY) ||
+ ingressOuter.equals(VlanId.ANY) ||
+ egressInner.equals(VlanId.ANY) ||
+ egressOuter.equals(VlanId.ANY)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Wildcard VLAN matching not yet " +
+ "supported for pseudowire %d.",
+ tunnelId)));
+ }
+
+ if (((!ingressOuter.equals(VlanId.NONE) && !ingressInner.equals(VlanId.NONE)) &&
+ (egressOuter.equals(VlanId.NONE) && egressInner.equals(VlanId.NONE)))
+ || ((ingressOuter.equals(VlanId.NONE) && ingressInner.equals(VlanId.NONE)) &&
+ (!egressOuter.equals(VlanId.NONE) && !egressInner.equals(VlanId.NONE)))) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Support for double tag <-> untag is not" +
+ "supported for pseudowire %d.",
+ tunnelId)));
+ }
+ if ((!ingressInner.equals(VlanId.NONE) &&
+ ingressOuter.equals(VlanId.NONE) &&
+ !egressOuter.equals(VlanId.NONE))
+ || (egressOuter.equals(VlanId.NONE) &&
+ !egressInner.equals(VlanId.NONE) &&
+ !ingressOuter.equals(VlanId.NONE))) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Support for double-tag<->" +
+ "single-tag is not supported" +
+ " for pseudowire %d.", tunnelId)));
+ }
+
+ if ((ingressInner.equals(VlanId.NONE) && !egressInner.equals(VlanId.NONE))
+ || (!ingressInner.equals(VlanId.NONE) && egressInner.equals(VlanId.NONE))) {
+ throw new IllegalArgumentException(String.valueOf(String.format("single-tag <-> untag is not supported" +
+ " for pseudowire %d.", tunnelId)));
+ }
+
+
+ if (!ingressInner.equals(egressInner) && !ingressOuter.equals(egressOuter)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("We do not support changing both tags " +
+ "in double tagged pws, only the outer," +
+ " for pseudowire %d.", tunnelId)));
+ }
+
+ // check if cp1 and port of cp1 exist
+ if (deviceService.getDevice(cP1.deviceId()) == null) {
+ throw new IllegalArgumentException(String.valueOf(String.format("cP1 device %s does not exist for" +
+ " pseudowire %d.", cP1.deviceId(),
+ tunnelId)));
+ }
+
+ if (deviceService.getPort(cP1) == null) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Port %s for cP1 device %s does not" +
+ " exist for pseudowire %d.", cP1.port(),
+ cP1.deviceId(), tunnelId)));
+ }
+
+ // check if cp2 and port of cp2 exist
+ if (deviceService.getDevice(cP2.deviceId()) == null) {
+ throw new IllegalArgumentException(String.valueOf(String.format("cP2 device %s does not exist for" +
+ " pseudowire %d.", cP2.deviceId(),
+ tunnelId)));
+ }
+
+ if (deviceService.getPort(cP2) == null) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Port %s for cP2 device %s does " +
+ "not exist for pseudowire %d.",
+ cP2.port(), cP2.deviceId(), tunnelId)));
+ }
+ }
+
+ /**
+ * Verifies that the pseudowires will not conflict with each other.
+ *
+ * Further, check if vlans for connect points are already used.
+ *
+ * @param tunnel Tunnel for pw
+ * @param policy Policy for pw
+ * @param labelSet Label set used so far with this configuration
+ * @param vlanSet Vlan set used with this configuration
+ * @param tunnelSet Tunnel set used with this configuration
+ */
+ private void verifyGlobalValidity(L2Tunnel tunnel,
+ L2TunnelPolicy policy,
+ Set<MplsLabel> labelSet,
+ Map<ConnectPoint, Set<VlanId>> vlanSet,
+ Set<Long> tunnelSet) {
+
+ if (tunnelSet.contains(tunnel.tunnelId())) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Tunnel Id %d already used by" +
+ " another pseudowire, in " +
+ "pseudowire %d!", tunnel.tunnelId(),
+ tunnel.tunnelId())));
+ }
+ tunnelSet.add(tunnel.tunnelId());
+
+ // check if tunnel id is used again
+ ConnectPoint cP1 = policy.cP1();
+ ConnectPoint cP2 = policy.cP2();
+
+ // insert cps to hashmap if this is the first time seen
+ if (!vlanSet.containsKey(cP1)) {
+ vlanSet.put(cP1, new HashSet<VlanId>());
+ }
+ if (!vlanSet.containsKey(cP2)) {
+ vlanSet.put(cP2, new HashSet<VlanId>());
+ }
+
+ // if single tagged or untagged vlan is the inner
+ // if double tagged vlan is the outer
+ VlanId vlanToCheckCP1;
+ if (policy.cP1OuterTag().equals(VlanId.NONE)) {
+ vlanToCheckCP1 = policy.cP1InnerTag();
+ } else {
+ vlanToCheckCP1 = policy.cP1OuterTag();
+ }
+
+ VlanId vlanToCheckCP2;
+ if (policy.cP2OuterTag().equals(VlanId.NONE)) {
+ vlanToCheckCP2 = policy.cP2InnerTag();
+ } else {
+ vlanToCheckCP2 = policy.cP2OuterTag();
+ }
+
+ if (labelSet.contains(tunnel.pwLabel())) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Label %s already used by another" +
+ " pseudowire, in pseudowire %d!",
+ tunnel.pwLabel(), tunnel.tunnelId())));
+ }
+ labelSet.add(tunnel.pwLabel());
+
+ if (vlanSet.get(cP1).contains(vlanToCheckCP1)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP1 %s already used " +
+ "by another pseudowire, in pseudowire" +
+ " %d!", vlanToCheckCP1, cP1,
+ tunnel.tunnelId())));
+ }
+ vlanSet.get(cP1).add(vlanToCheckCP1);
+
+ if (vlanSet.get(cP2).contains(vlanToCheckCP2)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP2 %s already used" +
+ " by another pseudowire, in" +
+ " pseudowire %d!", vlanToCheckCP2, cP2,
+ tunnel.tunnelId())));
+ }
+ vlanSet.get(cP2).add(vlanToCheckCP2);
+
+ // check that vlans for the connect points are not used
+ intfService.getInterfacesByPort(cP1).stream()
+ .forEach(intf -> {
+
+ // check if tagged pw affects tagged interface
+ if (intf.vlanTagged().contains(vlanToCheckCP1)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP1 %s already" +
+ " used for this interface, in" +
+ " pseudowire %d!",
+ vlanToCheckCP1, cP1,
+ tunnel.tunnelId())));
+ }
+
+ // if vlanNative != null this interface is configured with untagged traffic also
+ // check if it collides with untagged interface
+ if ((intf.vlanNative() != null) && vlanToCheckCP1.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP1 " +
+ "%s already used for this " +
+ "interface, in pseudowire " +
+ "%d!", cP1,
+ tunnel.tunnelId())));
+ }
+
+ // if vlanUntagged != null this interface is configured only with untagged traffic
+ // check if it collides with untagged interface
+ if ((intf.vlanUntagged() != null) && vlanToCheckCP1.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for " +
+ "cP1 %s already" +
+ " used for this interface," +
+ " in pseudowire %d!",
+ cP1, tunnel.tunnelId())));
+ }
+ });
+
+ intfService.getInterfacesByPort(cP2).stream()
+ .forEach(intf -> {
+ if (intf.vlanTagged().contains(vlanToCheckCP2)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP2 %s already" +
+ " used for this interface, " +
+ "in pseudowire %d!",
+ vlanToCheckCP2, cP2,
+ tunnel.tunnelId())));
+ }
+
+ // if vlanNative != null this interface is configured with untagged traffic also
+ // check if it collides with untagged interface
+ if ((intf.vlanNative() != null) && vlanToCheckCP2.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP2 %s " +
+ "already used for this" +
+ " interface, " +
+ "in pseudowire %d!",
+ cP2, tunnel.tunnelId())));
+ }
+
+ // if vlanUntagged != null this interface is configured only with untagged traffic
+ // check if it collides with untagged interface
+ if ((intf.vlanUntagged() != null) && vlanToCheckCP2.equals(VlanId.NONE)) {
+ throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP2 %s" +
+ " already" +
+ " used for this interface, " +
+ "in pseudowire %d!",
+ cP2, tunnel.tunnelId())));
+ }
+ });
+
+ }
+
+ /**
+ * Helper method to verify the integrity of the pseudo wire.
+ *
+ * @param l2TunnelDescription the pseudo wire description
+ * @return the result of the check
+ */
+ private void verifyPseudoWire(L2TunnelDescription l2TunnelDescription,
+ Set<MplsLabel> labelSet,
+ Map<ConnectPoint, Set<VlanId>> vlanset,
+ Set<Long> tunnelSet) {
+
+ L2Tunnel l2Tunnel = l2TunnelDescription.l2Tunnel();
+ L2TunnelPolicy l2TunnelPolicy = l2TunnelDescription.l2TunnelPolicy();
+
+ verifyTunnel(l2Tunnel);
+
+ verifyPolicy(
+ l2TunnelPolicy.cP1(),
+ l2TunnelPolicy.cP2(),
+ l2TunnelPolicy.cP1InnerTag(),
+ l2TunnelPolicy.cP1OuterTag(),
+ l2TunnelPolicy.cP2InnerTag(),
+ l2TunnelPolicy.cP2OuterTag(),
+ l2Tunnel.tunnelId()
+ );
+
+ verifyGlobalValidity(l2Tunnel,
+ l2TunnelPolicy,
+ labelSet,
+ vlanset,
+ tunnelSet);
+
+ }
+
+ /**
+ * Checks if the configured pseudowires will create problems in the network.
+ * If yes, then no pseudowires is deployed from this configuration.
+ *
+ * @param pseudowires Set of pseudowries to validate
+ * @return returns true if everything goes well.
+ */
+ public boolean configurationValidity(Set<L2TunnelDescription> pseudowires) {
+
+ // structures to keep pw information
+ // in order to see if instantiating them will create
+ // problems
+ Set<Long> tunIds = new HashSet<>();
+ Set<MplsLabel> labelsUsed = new HashSet<>();
+ Map<ConnectPoint, Set<VlanId>> vlanIds = new HashMap<>();
+
+ // check that pseudowires can be instantiated in the network
+ // we try to guarantee that all the pws will work before
+ // instantiating any of them
+ for (L2TunnelDescription pw : pseudowires) {
+ verifyPseudoWire(pw, labelsUsed, vlanIds, tunIds);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns all pseudo wire keys.
+ *
+ * @return all keys (tunnels id)
+ * @throws IllegalArgumentException if wrong format
+ */
+ public Set<Long> getPwIds() {
+ ImmutableSet.Builder<Long> builder = ImmutableSet.builder();
+ object.fields().forEachRemaining(entry -> {
+ Long tunnelId = Long.parseLong(entry.getKey());
+ builder.add(tunnelId);
+ });
+ return builder.build();
+ }
+
+ /**
+ * Parses a vlan as a string. Returns the VlanId if
+ * provided String can be parsed as an integer or is '' / '*'
+ *
+ * @param vlan string as read from configuration
+ * @return VlanId
+ * @throws IllegalArgumentException if wrong format of vlan
+ */
+ public VlanId parseVlan(String vlan) {
+
+ if (vlan.equals("*") || vlan.equals("Any")) {
+ return VlanId.vlanId("Any");
+ } else if (vlan.equals("") || vlan.equals("None")) {
+ return VlanId.vlanId("None");
+ } else {
+ try {
+ VlanId newVlan = VlanId.vlanId(vlan);
+ return newVlan;
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(INVALID_VLAN);
+ }
+ }
+ }
+
+ /**
+ *
+ * @param mode RAW or TAGGED
+ * @return the L2Mode if input is correct
+ * @throws IllegalArgumentException if not supported mode
+ */
+ public L2Mode parseMode(String mode) {
+
+ if (!mode.equals("RAW") && !mode.equals("TAGGED")) {
+ throw new IllegalArgumentException(INVALID_L2_MODE);
+ }
+
+ return L2Mode.valueOf(mode);
+ }
+
+ /**
+ *
+ * @param label the mpls label of the pseudowire
+ * @return the MplsLabel
+ * @throws IllegalArgumentException if label is invalid
+ */
+ public MplsLabel parsePWLabel(String label) {
+
+ try {
+ MplsLabel pwLabel = MplsLabel.mplsLabel(label);
+ return pwLabel;
+ } catch (Exception e) {
+ throw new IllegalArgumentException(INVALID_PW_LABEL);
+ }
+ }
+
+ /**
+ * Returns pw description of given pseudo wire id.
+ *
+ * @param tunnelId pseudo wire key
+ * @return set of l2 tunnel descriptions
+ * @throws IllegalArgumentException if wrong format
+ */
+ public L2TunnelDescription getPwDescription(Long tunnelId) {
+ JsonNode pwDescription = object.get(tunnelId.toString());
+ if (!hasFields((ObjectNode) pwDescription,
+ SRC_CP, SRC_INNER_TAG, SRC_OUTER_TAG,
+ DST_CP, DST_INNER_TAG, DST_OUTER_TAG,
+ MODE, SD_TAG, PW_LABEL)) {
+ throw new IllegalArgumentException(MISSING_PARAMS);
+ }
+ String tempString;
+
+ tempString = pwDescription.get(SRC_CP).asText();
+ ConnectPoint srcCp = ConnectPoint.deviceConnectPoint(tempString);
+
+ tempString = pwDescription.get(DST_CP).asText();
+ ConnectPoint dstCp = ConnectPoint.deviceConnectPoint(tempString);
+
+ tempString = pwDescription.get(SRC_INNER_TAG).asText();
+ VlanId srcInnerTag = parseVlan(tempString);
+
+ tempString = pwDescription.get(SRC_OUTER_TAG).asText();
+ VlanId srcOuterTag = parseVlan(tempString);
+
+ tempString = pwDescription.get(DST_INNER_TAG).asText();
+ VlanId dstInnerTag = parseVlan(tempString);
+
+ tempString = pwDescription.get(DST_OUTER_TAG).asText();
+ VlanId dstOuterTag = parseVlan(tempString);
+
+ tempString = pwDescription.get(MODE).asText();
+ L2Mode l2Mode = parseMode(tempString);
+
+ tempString = pwDescription.get(SD_TAG).asText();
+ VlanId sdTag = parseVlan(tempString);
+
+ tempString = pwDescription.get(PW_LABEL).asText();
+ MplsLabel pwLabel = parsePWLabel(tempString);
+
+ L2Tunnel l2Tunnel = new DefaultL2Tunnel(
+ l2Mode,
+ sdTag,
+ tunnelId,
+ pwLabel
+ );
+
+ L2TunnelPolicy l2TunnelPolicy = new DefaultL2TunnelPolicy(
+ tunnelId,
+ srcCp,
+ srcInnerTag,
+ srcOuterTag,
+ dstCp,
+ dstInnerTag,
+ dstOuterTag
+ );
+
+ return new DefaultL2TunnelDescription(l2Tunnel, l2TunnelPolicy);
+ }
+
+ /**
+ * Removes a pseudowire from the configuration tree.
+ * @param pwId Pseudowire id
+ * @return null if pwId did not exist, or the object representing the
+ * udpated configuration tree
+ */
+ public ObjectNode removePseudowire(String pwId) {
+
+ JsonNode value = object.remove(pwId);
+ if (value == null) {
+ return (ObjectNode) value;
+ } else {
+ return object;
+ }
+ }
+
+ /**
+ * Adds a pseudowire to the configuration tree of pwwas. It also checks
+ * if the configuration is valid, if not return null and does not add the node,
+ * if yes return the new configuration. Caller will propagate update events.
+ *
+ * If the pseudowire already exists in the configuration it gets updated.
+ *
+ * @param tunnelId Id of tunnel
+ * @param pwLabel PW label of tunnel
+ * @param cP1 Connection point 1
+ * @param cP1InnerVlan Inner vlan of cp1
+ * @param cP1OuterVlan Outer vlan of cp2
+ * @param cP2 Connection point 2
+ * @param cP2InnerVlan Inner vlan of cp2
+ * @param cP2OuterVlan Outer vlan of cp2
+ * @param mode Mode for the pw
+ * @param sdTag Service delimiting tag for the pw
+ * @return The ObjectNode config if configuration is valid with the new pseudowire
+ * or null.
+ */
+ public ObjectNode addPseudowire(String tunnelId, String pwLabel, String cP1,
+ String cP1InnerVlan, String cP1OuterVlan, String cP2,
+ String cP2InnerVlan, String cP2OuterVlan,
+ String mode, String sdTag) {
+
+
+ ObjectNode newPw = new ObjectNode(JsonNodeFactory.instance);
+
+ // add fields for pseudowire
+ newPw.put(SRC_CP, cP1);
+ newPw.put(DST_CP, cP2);
+ newPw.put(PW_LABEL, pwLabel);
+ newPw.put(SRC_INNER_TAG, cP1InnerVlan);
+ newPw.put(SRC_OUTER_TAG, cP1OuterVlan);
+ newPw.put(DST_INNER_TAG, cP2InnerVlan);
+ newPw.put(DST_OUTER_TAG, cP2OuterVlan);
+ newPw.put(SD_TAG, sdTag);
+ newPw.put(MODE, mode);
+
+ object.set(tunnelId, newPw);
+
+ if (!isValid()) {
+ log.info("Pseudowire could not be created : {}");
+ object.remove(tunnelId);
+ return null;
+ }
+
+ return object;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java
new file mode 100644
index 0000000..3e4d17c
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.Config;
+
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * App configuration object for Segment Routing.
+ */
+public class SegmentRoutingAppConfig extends Config<ApplicationId> {
+ private static final String VROUTER_MACS = "vRouterMacs";
+ private static final String SUPPRESS_SUBNET = "suppressSubnet";
+ private static final String SUPPRESS_HOST_BY_PORT = "suppressHostByPort";
+ // TODO We might want to move SUPPRESS_HOST_BY_PROVIDER to Component Config
+ private static final String SUPPRESS_HOST_BY_PROVIDER = "suppressHostByProvider";
+ private static final String MPLS_ECMP = "MPLS-ECMP";
+
+ @Override
+ public boolean isValid() {
+ return hasOnlyFields(VROUTER_MACS, SUPPRESS_SUBNET,
+ SUPPRESS_HOST_BY_PORT, SUPPRESS_HOST_BY_PROVIDER, MPLS_ECMP) &&
+ vRouterMacs() != null &&
+ suppressSubnet() != null && suppressHostByPort() != null &&
+ suppressHostByProvider() != null;
+ }
+
+ /**
+ * Gets MPLS-ECMP configuration from the config.
+ *
+ * @return the configuration of MPLS-ECMP. If it is not
+ * specified, the default behavior is false.
+ */
+ public boolean mplsEcmp() {
+ return get(MPLS_ECMP, false);
+ }
+
+ /**
+ * Sets MPLS-ECMP to the config.
+ *
+ * @param mplsEcmp the MPLS-ECMP configuration
+ * @return this {@link SegmentRoutingAppConfig}
+ */
+ public SegmentRoutingAppConfig setMplsEcmp(boolean mplsEcmp) {
+ object.put(MPLS_ECMP, mplsEcmp);
+ return this;
+ }
+
+ /**
+ * Gets vRouters from the config.
+ *
+ * @return Set of vRouter MAC addresses, empty is not specified,
+ * or null if not valid
+ */
+ public Set<MacAddress> vRouterMacs() {
+ if (!object.has(VROUTER_MACS)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<MacAddress> builder = ImmutableSet.builder();
+ ArrayNode arrayNode = (ArrayNode) object.path(VROUTER_MACS);
+ for (JsonNode jsonNode : arrayNode) {
+ MacAddress mac;
+
+ String macStr = jsonNode.asText(null);
+ if (macStr == null) {
+ return null;
+ }
+ try {
+ mac = MacAddress.valueOf(macStr);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+
+ builder.add(mac);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Sets vRouters to the config.
+ *
+ * @param vRouterMacs a set of vRouter MAC addresses
+ * @return this {@link SegmentRoutingAppConfig}
+ */
+ public SegmentRoutingAppConfig setVRouterMacs(Set<MacAddress> vRouterMacs) {
+ if (vRouterMacs == null) {
+ object.remove(VROUTER_MACS);
+ } else {
+ ArrayNode arrayNode = mapper.createArrayNode();
+
+ vRouterMacs.forEach(mac -> {
+ arrayNode.add(mac.toString());
+ });
+
+ object.set(VROUTER_MACS, arrayNode);
+ }
+ return this;
+ }
+
+ /**
+ * Gets names of ports to which SegmentRouting does not push subnet rules.
+ *
+ * @return Set of port names, empty if not specified, or null
+ * if not valid
+ */
+ public Set<ConnectPoint> suppressSubnet() {
+ if (!object.has(SUPPRESS_SUBNET)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+ ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_SUBNET);
+ for (JsonNode jsonNode : arrayNode) {
+ String portName = jsonNode.asText(null);
+ if (portName == null) {
+ return null;
+ }
+ try {
+ builder.add(ConnectPoint.deviceConnectPoint(portName));
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Sets names of ports to which SegmentRouting does not push subnet rules.
+ *
+ * @param suppressSubnet names of ports to which SegmentRouting does not push
+ * subnet rules
+ * @return this {@link SegmentRoutingAppConfig}
+ */
+ public SegmentRoutingAppConfig setSuppressSubnet(Set<ConnectPoint> suppressSubnet) {
+ if (suppressSubnet == null) {
+ object.remove(SUPPRESS_SUBNET);
+ } else {
+ ArrayNode arrayNode = mapper.createArrayNode();
+ suppressSubnet.forEach(connectPoint -> {
+ arrayNode.add(connectPoint.deviceId() + "/" + connectPoint.port());
+ });
+ object.set(SUPPRESS_SUBNET, arrayNode);
+ }
+ return this;
+ }
+
+ /**
+ * Gets connect points to which SegmentRouting does not push host rules.
+ *
+ * @return Set of connect points, empty if not specified, or null
+ * if not valid
+ */
+ public Set<ConnectPoint> suppressHostByPort() {
+ if (!object.has(SUPPRESS_HOST_BY_PORT)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+ ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_HOST_BY_PORT);
+ for (JsonNode jsonNode : arrayNode) {
+ String portName = jsonNode.asText(null);
+ if (portName == null) {
+ return null;
+ }
+ try {
+ builder.add(ConnectPoint.deviceConnectPoint(portName));
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Sets connect points to which SegmentRouting does not push host rules.
+ *
+ * @param connectPoints connect points to which SegmentRouting does not push
+ * host rules
+ * @return this {@link SegmentRoutingAppConfig}
+ */
+ public SegmentRoutingAppConfig setSuppressHostByPort(Set<ConnectPoint> connectPoints) {
+ if (connectPoints == null) {
+ object.remove(SUPPRESS_HOST_BY_PORT);
+ } else {
+ ArrayNode arrayNode = mapper.createArrayNode();
+ connectPoints.forEach(connectPoint -> {
+ arrayNode.add(connectPoint.deviceId() + "/" + connectPoint.port());
+ });
+ object.set(SUPPRESS_HOST_BY_PORT, arrayNode);
+ }
+ return this;
+ }
+
+ /**
+ * Gets provider names from which SegmentRouting does not learn host info.
+ *
+ * @return array of provider names that need to be ignored
+ */
+ public Set<String> suppressHostByProvider() {
+ if (!object.has(SUPPRESS_HOST_BY_PROVIDER)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_HOST_BY_PROVIDER);
+ for (JsonNode jsonNode : arrayNode) {
+ String providerName = jsonNode.asText(null);
+ if (providerName == null) {
+ return null;
+ }
+ builder.add(providerName);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Sets provider names from which SegmentRouting does not learn host info.
+ *
+ * @param providers set of provider names
+ * @return this {@link SegmentRoutingAppConfig}
+ */
+ public SegmentRoutingAppConfig setSuppressHostByProvider(Set<String> providers) {
+ if (providers == null) {
+ object.remove(SUPPRESS_HOST_BY_PROVIDER);
+ } else {
+ ArrayNode arrayNode = mapper.createArrayNode();
+ providers.forEach(arrayNode::add);
+ object.set(SUPPRESS_HOST_BY_PROVIDER, arrayNode);
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("vRouterMacs", vRouterMacs())
+ .add("suppressSubnet", suppressSubnet())
+ .add("suppressHostByPort", suppressHostByPort())
+ .add("suppressHostByProvider", suppressHostByProvider())
+ .add("mplsEcmp", mplsEcmp())
+ .toString();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java
new file mode 100644
index 0000000..7959df6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+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.ImmutableMap;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Configuration object for Segment Routing Application.
+ */
+public class SegmentRoutingDeviceConfig extends Config<DeviceId> {
+ private static final String NAME = "name";
+ private static final String IP4 = "ipv4Loopback";
+ private static final String IP6 = "ipv6Loopback";
+ private static final String MAC = "routerMac";
+ private static final String IP4_SID = "ipv4NodeSid";
+ private static final String IP6_SID = "ipv6NodeSid";
+ private static final String EDGE = "isEdgeRouter";
+ /**
+ * Adjancency SIDs config.
+ *
+ * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+ */
+ @Deprecated
+ private static final String ADJSIDS = "adjacencySids";
+
+ /**
+ * Adjancency SID config.
+ *
+ * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+ */
+ @Deprecated
+ private static final String ADJSID = "adjSid";
+
+ /**
+ * Adjancency port config.
+ *
+ * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+ */
+ @Deprecated
+ private static final String PORTS = "ports";
+
+ private static final String PAIR_DEVICE_ID = "pairDeviceId";
+ private static final String PAIR_LOCAL_PORT = "pairLocalPort";
+
+ @Override
+ public boolean isValid() {
+ return hasOnlyFields(NAME, IP4, IP6, MAC, IP4_SID, IP6_SID, EDGE, ADJSIDS, ADJSID, PORTS,
+ PAIR_DEVICE_ID, PAIR_LOCAL_PORT) &&
+ name() != null &&
+ routerIpv4() != null && (!hasField(IP6) || routerIpv6() != null) &&
+ routerMac() != null &&
+ nodeSidIPv4() != -1 && (!hasField(IP6_SID) || nodeSidIPv6() != -1) &&
+ isEdgeRouter() != null &&
+ adjacencySids() != null &&
+ // pairDeviceId and pairLocalPort must be both configured or both omitted
+ (hasField(PAIR_DEVICE_ID) == hasField(PAIR_LOCAL_PORT)) &&
+ (!hasField(PAIR_DEVICE_ID) || pairDeviceId() != null) &&
+ (!hasField(PAIR_LOCAL_PORT) || pairLocalPort() != null);
+ }
+
+ /**
+ * Gets the name of the router.
+ *
+ * @return Optional name of the router. May be empty if not configured.
+ */
+ public Optional<String> name() {
+ String name = get(NAME, null);
+ return name != null ? Optional.of(name) : Optional.empty();
+ }
+
+ /**
+ * Sets the name of the router.
+ *
+ * @param name name of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setName(String name) {
+ return (SegmentRoutingDeviceConfig) setOrClear(NAME, name);
+ }
+
+ /**
+ * Gets the IPv4 address of the router.
+ *
+ * @return IP address of the router. Or null if not configured.
+ */
+ public Ip4Address routerIpv4() {
+ String ip = get(IP4, null);
+ return ip != null ? Ip4Address.valueOf(ip) : null;
+ }
+
+ /**
+ * Gets the IPv6 address of the router.
+ *
+ * @return IP address of the router. Or null if not configured.
+ */
+ public Ip6Address routerIpv6() {
+ String ip = get(IP6, null);
+ return ip != null ? Ip6Address.valueOf(ip) : null;
+ }
+
+ /**
+ * Sets the IPv4 address of the router.
+ *
+ * @param ip IPv4 address of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setRouterIpv4(String ip) {
+ return (SegmentRoutingDeviceConfig) setOrClear(IP4, ip);
+ }
+
+ /**
+ * Sets the IPv6 address of the router.
+ *
+ * @param ip IPv6 address of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setRouterIpv6(String ip) {
+ return (SegmentRoutingDeviceConfig) setOrClear(IP6, ip);
+ }
+
+ /**
+ * Gets the MAC address of the router.
+ *
+ * @return MAC address of the router. Or null if not configured.
+ */
+ public MacAddress routerMac() {
+ String mac = get(MAC, null);
+ return mac != null ? MacAddress.valueOf(mac) : null;
+ }
+
+ /**
+ * Sets the MAC address of the router.
+ *
+ * @param mac MAC address of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setRouterMac(String mac) {
+ return (SegmentRoutingDeviceConfig) setOrClear(MAC, mac);
+ }
+
+ /**
+ * Gets the IPv4 node SID of the router.
+ *
+ * @return node SID of the router. Or -1 if not configured.
+ */
+ public int nodeSidIPv4() {
+ return get(IP4_SID, -1);
+ }
+
+ /**
+ * Gets the IPv6 node SID of the router.
+ *
+ * @return node SID of the router. Or -1 if not configured.
+ */
+ public int nodeSidIPv6() {
+ return get(IP6_SID, -1);
+ }
+
+ /**
+ * Sets the node IPv4 node SID of the router.
+ *
+ * @param sid node SID of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setNodeSidIPv4(int sid) {
+ return (SegmentRoutingDeviceConfig) setOrClear(IP4_SID, sid);
+ }
+
+ /**
+ * Sets the node IPv6 node SID of the router.
+ *
+ * @param sid node SID of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setNodeSidIPv6(int sid) {
+ return (SegmentRoutingDeviceConfig) setOrClear(IP6_SID, sid);
+ }
+
+ /**
+ * Checks if the router is an edge router.
+ *
+ * @return true if the router is an edge router.
+ * false if the router is not an edge router.
+ * null if the value is not configured.
+ */
+ public Boolean isEdgeRouter() {
+ String isEdgeRouter = get(EDGE, null);
+ return isEdgeRouter != null ?
+ Boolean.valueOf(isEdgeRouter) :
+ null;
+ }
+
+ /**
+ * Specifies if the router is an edge router.
+ *
+ * @param isEdgeRouter true if the router is an edge router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setIsEdgeRouter(boolean isEdgeRouter) {
+ return (SegmentRoutingDeviceConfig) setOrClear(EDGE, isEdgeRouter);
+ }
+
+ /**
+ * Gets the adjacency SIDs of the router.
+ *
+ * @return adjacency SIDs of the router. Or null if not configured.
+ */
+ public Map<Integer, Set<Integer>> adjacencySids() {
+ if (!object.has(ADJSIDS)) {
+ return null;
+ }
+
+ Map<Integer, Set<Integer>> adjacencySids = new HashMap<>();
+ ArrayNode adjacencySidsNode = (ArrayNode) object.path(ADJSIDS);
+ for (JsonNode adjacencySidNode : adjacencySidsNode) {
+ int asid = adjacencySidNode.path(ADJSID).asInt(-1);
+ if (asid == -1) {
+ return null;
+ }
+
+ HashSet<Integer> ports = new HashSet<>();
+ ArrayNode portsNode = (ArrayNode) adjacencySidNode.path(PORTS);
+ for (JsonNode portNode : portsNode) {
+ int port = portNode.asInt(-1);
+ if (port == -1) {
+ return null;
+ }
+ ports.add(port);
+ }
+ adjacencySids.put(asid, ports);
+ }
+
+ return ImmutableMap.copyOf(adjacencySids);
+ }
+
+ /**
+ * Sets the adjacency SIDs of the router.
+ *
+ * @param adjacencySids adjacency SIDs of the router.
+ * @return the config of the router.
+ */
+ public SegmentRoutingDeviceConfig setAdjacencySids(Map<Integer, Set<Integer>> adjacencySids) {
+ if (adjacencySids == null) {
+ object.remove(ADJSIDS);
+ } else {
+ ArrayNode adjacencySidsNode = mapper.createArrayNode();
+
+ adjacencySids.forEach((sid, ports) -> {
+ ObjectNode adjacencySidNode = mapper.createObjectNode();
+
+ adjacencySidNode.put(ADJSID, sid);
+
+ ArrayNode portsNode = mapper.createArrayNode();
+ ports.forEach(port -> {
+ portsNode.add(port.toString());
+ });
+ adjacencySidNode.set(PORTS, portsNode);
+
+ adjacencySidsNode.add(adjacencySidNode);
+ });
+
+ object.set(ADJSIDS, adjacencySidsNode);
+ }
+
+ return this;
+ }
+
+ /**
+ * Gets the pair device id.
+ *
+ * @return pair device id; Or null if not configured.
+ */
+ public DeviceId pairDeviceId() {
+ String pairDeviceId = get(PAIR_DEVICE_ID, null);
+ return pairDeviceId != null ? DeviceId.deviceId(pairDeviceId) : null;
+ }
+
+ /**
+ * Sets the pair device id.
+ *
+ * @param deviceId pair device id
+ * @return this configuration
+ */
+ public SegmentRoutingDeviceConfig setPairDeviceId(DeviceId deviceId) {
+ return (SegmentRoutingDeviceConfig) setOrClear(PAIR_DEVICE_ID, deviceId.toString());
+ }
+
+ /**
+ * Gets the pair local port.
+ *
+ * @return pair local port; Or null if not configured.
+ */
+ public PortNumber pairLocalPort() {
+ long pairLocalPort = get(PAIR_LOCAL_PORT, -1L);
+ return pairLocalPort != -1L ? PortNumber.portNumber(pairLocalPort) : null;
+ }
+
+ /**
+ * Sets the pair local port.
+ *
+ * @param portNumber pair local port
+ * @return this configuration
+ */
+ public SegmentRoutingDeviceConfig setPairLocalPort(PortNumber portNumber) {
+ return (SegmentRoutingDeviceConfig) setOrClear(PAIR_LOCAL_PORT, portNumber.toLong());
+ }
+}
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
new file mode 100644
index 0000000..76a3917
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Configuration object for cross-connect.
+ */
+public class XConnectConfig extends Config<ApplicationId> {
+ private static final String VLAN = "vlan";
+ private static final String PORTS = "ports";
+ private static final String NAME = "name"; // dummy field for naming
+
+ private static final String UNEXPECTED_FIELD_NAME = "Unexpected field name";
+
+ @Override
+ public boolean isValid() {
+ try {
+ getXconnects().forEach(this::getPorts);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns all xconnect keys.
+ *
+ * @return all keys (device/vlan pairs)
+ * @throws IllegalArgumentException if wrong format
+ */
+ public Set<XConnectStoreKey> getXconnects() {
+ ImmutableSet.Builder<XConnectStoreKey> builder = ImmutableSet.builder();
+ object.fields().forEachRemaining(entry -> {
+ DeviceId deviceId = DeviceId.deviceId(entry.getKey());
+ builder.addAll(getXconnects(deviceId));
+ });
+ return builder.build();
+ }
+
+ /**
+ * Returns xconnect keys of given device.
+ *
+ * @param deviceId ID of the device from which we want to get XConnect info
+ * @return xconnect keys (device/vlan pairs) of given device
+ * @throws IllegalArgumentException if wrong format
+ */
+ public Set<XConnectStoreKey> getXconnects(DeviceId deviceId) {
+ ImmutableSet.Builder<XConnectStoreKey> builder = ImmutableSet.builder();
+ JsonNode vlanPortPair = object.get(deviceId.toString());
+ if (vlanPortPair != null) {
+ vlanPortPair.forEach(jsonNode -> {
+ if (!hasOnlyFields((ObjectNode) jsonNode, VLAN, PORTS, NAME)) {
+ throw new IllegalArgumentException(UNEXPECTED_FIELD_NAME);
+ }
+ VlanId vlanId = VlanId.vlanId((short) jsonNode.get(VLAN).asInt());
+ builder.add(new XConnectStoreKey(deviceId, vlanId));
+ });
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns ports of given xconnect key.
+ *
+ * @param xconnect xconnect key
+ * @return set of two ports associated with given xconnect key
+ * @throws IllegalArgumentException if wrong format
+ */
+ public Set<PortNumber> getPorts(XConnectStoreKey xconnect) {
+ ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+ object.get(xconnect.deviceId().toString()).forEach(vlanPortsPair -> {
+ if (xconnect.vlanId().toShort() == vlanPortsPair.get(VLAN).asInt()) {
+ int portCount = vlanPortsPair.get(PORTS).size();
+ checkArgument(portCount == 2,
+ "Expect 2 ports but found " + portCount + " on " + xconnect);
+ vlanPortsPair.get(PORTS).forEach(portNode -> {
+ builder.add(PortNumber.portNumber(portNode.asInt()));
+ });
+ }
+ });
+ return builder.build();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java
new file mode 100644
index 0000000..a664a8f
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing network configuration mechanism.
+ */
+package org.onosproject.segmentrouting.config;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
new file mode 100644
index 0000000..75cab32
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
@@ -0,0 +1,1424 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+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.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.DefaultRoutingHandler;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceProperties;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Default ECMP group handler creation module. This component creates a set of
+ * ECMP groups for every neighbor that this device is connected to based on
+ * whether the current device is an edge device or a transit device.
+ */
+public class DefaultGroupHandler {
+ private static final Logger log = getLogger(DefaultGroupHandler.class);
+
+ private static final long VERIFY_INTERVAL = 30; // secs
+
+ protected final DeviceId deviceId;
+ protected final ApplicationId appId;
+ protected final DeviceProperties deviceConfig;
+ protected final List<Integer> allSegmentIds;
+ protected int ipv4NodeSegmentId = -1;
+ protected int ipv6NodeSegmentId = -1;
+ protected boolean isEdgeRouter = false;
+ protected MacAddress nodeMacAddr = null;
+ protected LinkService linkService;
+ protected FlowObjectiveService flowObjectiveService;
+ /**
+ * local store for neighbor-device-ids and the set of ports on this device
+ * that connect to the same neighbor.
+ */
+ protected ConcurrentHashMap<DeviceId, Set<PortNumber>> devicePortMap =
+ new ConcurrentHashMap<>();
+ /**
+ * local store for ports on this device connected to neighbor-device-id.
+ */
+ protected ConcurrentHashMap<PortNumber, DeviceId> portDeviceMap =
+ new ConcurrentHashMap<>();
+
+ // distributed store for (device+destination-set) mapped to next-id and neighbors
+ protected EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+ dsNextObjStore = null;
+ // distributed store for (device+subnet-ip-prefix) mapped to next-id
+ protected EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer>
+ vlanNextObjStore = null;
+ // distributed store for (device+port+treatment) mapped to next-id
+ protected EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer>
+ portNextObjStore = null;
+ private SegmentRoutingManager srManager;
+
+ private ScheduledExecutorService executorService
+ = newScheduledThreadPool(1, groupedThreads("bktCorrector", "bktC-%d", log));
+
+ protected KryoNamespace.Builder kryo = new KryoNamespace.Builder()
+ .register(URI.class).register(HashSet.class)
+ .register(DeviceId.class).register(PortNumber.class)
+ .register(DestinationSet.class).register(PolicyGroupIdentifier.class)
+ .register(PolicyGroupParams.class)
+ .register(GroupBucketIdentifier.class)
+ .register(GroupBucketIdentifier.BucketOutputType.class);
+
+ protected DefaultGroupHandler(DeviceId deviceId, ApplicationId appId,
+ DeviceProperties config,
+ LinkService linkService,
+ FlowObjectiveService flowObjService,
+ SegmentRoutingManager srManager) {
+ this.deviceId = checkNotNull(deviceId);
+ this.appId = checkNotNull(appId);
+ this.deviceConfig = checkNotNull(config);
+ this.linkService = checkNotNull(linkService);
+ this.allSegmentIds = checkNotNull(config.getAllDeviceSegmentIds());
+ try {
+ this.ipv4NodeSegmentId = config.getIPv4SegmentId(deviceId);
+ this.ipv6NodeSegmentId = config.getIPv6SegmentId(deviceId);
+ this.isEdgeRouter = config.isEdgeDevice(deviceId);
+ this.nodeMacAddr = checkNotNull(config.getDeviceMac(deviceId));
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Skipping value assignment in DefaultGroupHandler");
+ }
+ this.flowObjectiveService = flowObjService;
+ this.dsNextObjStore = srManager.dsNextObjStore();
+ this.vlanNextObjStore = srManager.vlanNextObjStore();
+ this.portNextObjStore = srManager.portNextObjStore();
+ this.srManager = srManager;
+ executorService.scheduleWithFixedDelay(new BucketCorrector(), 10,
+ VERIFY_INTERVAL,
+ TimeUnit.SECONDS);
+ populateNeighborMaps();
+ }
+
+ /**
+ * Gracefully shuts down a groupHandler. Typically called when the handler is
+ * no longer needed.
+ */
+ public void shutdown() {
+ executorService.shutdown();
+ }
+
+ /**
+ * Creates a group handler object.
+ *
+ * @param deviceId device identifier
+ * @param appId application identifier
+ * @param config interface to retrieve the device properties
+ * @param linkService link service object
+ * @param flowObjService flow objective service object
+ * @param srManager segment routing manager
+ * @throws DeviceConfigNotFoundException if the device configuration is not found
+ * @return default group handler type
+ */
+ public static DefaultGroupHandler createGroupHandler(
+ DeviceId deviceId,
+ ApplicationId appId,
+ DeviceProperties config,
+ LinkService linkService,
+ FlowObjectiveService flowObjService,
+ SegmentRoutingManager srManager)
+ throws DeviceConfigNotFoundException {
+ return new DefaultGroupHandler(deviceId, appId, config,
+ linkService,
+ flowObjService,
+ srManager);
+ }
+
+ /**
+ * Updates local stores for link-src device/port to neighbor (link-dst).
+ *
+ * @param link the infrastructure link
+ */
+ public void portUpForLink(Link link) {
+ if (!link.src().deviceId().equals(deviceId)) {
+ log.warn("linkUp: deviceId{} doesn't match with link src {}",
+ deviceId, link.src().deviceId());
+ return;
+ }
+
+ log.info("* portUpForLink: Device {} linkUp at local port {} to "
+ + "neighbor {}", deviceId, link.src().port(), link.dst().deviceId());
+ // ensure local state is updated even if linkup is aborted later on
+ addNeighborAtPort(link.dst().deviceId(),
+ link.src().port());
+ }
+
+ /**
+ * Updates local stores for port that has gone down.
+ *
+ * @param port port number that has gone down
+ */
+ public void portDown(PortNumber port) {
+ if (portDeviceMap.get(port) == null) {
+ log.warn("portDown: unknown port");
+ return;
+ }
+
+ log.debug("Device {} portDown {} to neighbor {}", deviceId, port,
+ portDeviceMap.get(port));
+ devicePortMap.get(portDeviceMap.get(port)).remove(port);
+ portDeviceMap.remove(port);
+ }
+
+ /**
+ * Checks all groups in the src-device of link for neighbor sets that include
+ * the dst-device of link, and edits the hash groups according to link up
+ * or down. Should only be called by the master instance of the src-switch
+ * of link. Typically used when there are no route-path changes due to the
+ * link up or down, as the ECMPspg does not change.
+ *
+ * @param link the infrastructure link that has gone down or come up
+ * @param linkDown true if link has gone down
+ * @param firstTime true if link has come up for the first time i.e a link
+ * not seen-before
+ */
+ public void retryHash(Link link, boolean linkDown, boolean firstTime) {
+ MacAddress neighborMac;
+ try {
+ neighborMac = deviceConfig.getDeviceMac(link.dst().deviceId());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting retryHash.");
+ return;
+ }
+ // find all the destinationSets related to link
+ Set<DestinationSetNextObjectiveStoreKey> dsKeySet = dsNextObjStore.entrySet()
+ .stream()
+ .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+ // Filter out PW transit groups or include them if MPLS ECMP is supported
+ .filter(entry -> !entry.getKey().destinationSet().mplsSet() ||
+ (entry.getKey().destinationSet().mplsSet() && srManager.getMplsEcmp()))
+ .filter(entry -> entry.getValue().containsNextHop(link.dst().deviceId()))
+ .map(entry -> entry.getKey())
+ .collect(Collectors.toSet());
+
+ log.debug("retryHash: dsNextObjStore contents for linkSrc {} -> linkDst {}: {}",
+ deviceId, link.dst().deviceId(), dsKeySet);
+
+ for (DestinationSetNextObjectiveStoreKey dsKey : dsKeySet) {
+ NextNeighbors nextHops = dsNextObjStore.get(dsKey);
+ if (nextHops == null) {
+ log.warn("retryHash in device {}, but global store has no record "
+ + "for dsKey:{}", deviceId, dsKey);
+ continue;
+ }
+ int nextId = nextHops.nextId();
+ Set<DeviceId> dstSet = nextHops.getDstForNextHop(link.dst().deviceId());
+ if (!linkDown) {
+ List<PortLabel> pl = Lists.newArrayList();
+ if (firstTime) {
+ // some links may have come up before the next-objective was created
+ // we take this opportunity to ensure other ports to same next-hop-dst
+ // are part of the hash group (see CORD-1180). Duplicate additions
+ // to the same hash group are avoided by the driver.
+ for (PortNumber p : devicePortMap.get(link.dst().deviceId())) {
+ dstSet.forEach(dst -> {
+ int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+ pl.add(new PortLabel(p, edgeLabel));
+ });
+ }
+ addToHashedNextObjective(pl, neighborMac, nextId);
+ } else {
+ // handle only the port that came up
+ dstSet.forEach(dst -> {
+ int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+ pl.add(new PortLabel(link.src().port(), edgeLabel));
+ });
+ addToHashedNextObjective(pl, neighborMac, nextId);
+ }
+ } else {
+ // linkdown
+ List<PortLabel> pl = Lists.newArrayList();
+ dstSet.forEach(dst -> {
+ int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+ pl.add(new PortLabel(link.src().port(), edgeLabel));
+ });
+ removeFromHashedNextObjective(pl, neighborMac, nextId);
+ }
+ }
+ }
+
+ /**
+ * Utility class for associating output ports and the corresponding MPLS
+ * labels to push. In dual-homing, there are different labels to push
+ * corresponding to the destination switches in an edge-pair. If both
+ * destinations are reachable via the same spine, then the output-port to
+ * the spine will be associated with two labels i.e. there will be two
+ * PortLabel objects for the same port but with different labels.
+ */
+ private class PortLabel {
+ PortNumber port;
+ int edgeLabel;
+
+ PortLabel(PortNumber port, int edgeLabel) {
+ this.port = port;
+ this.edgeLabel = edgeLabel;
+ }
+
+ @Override
+ public String toString() {
+ return port.toString() + "/" + String.valueOf(edgeLabel);
+ }
+ }
+
+ /**
+ * Makes a call to the FlowObjective service to add buckets to
+ * a hashed group. User must ensure that all the ports & labels are meant
+ * same neighbor (ie. dstMac).
+ *
+ * @param portLabels a collection of port & label combinations to add
+ * to the hash group identified by the nextId
+ * @param dstMac destination mac address of next-hop
+ * @param nextId id for next-objective to which buckets will be added
+ *
+ */
+ private void addToHashedNextObjective(Collection<PortLabel> portLabels,
+ MacAddress dstMac, Integer nextId) {
+ // setup metadata to pass to nextObjective - indicate the vlan on egress
+ // if needed by the switch pipeline. Since hashed next-hops are always to
+ // other neighboring routers, there is no subnet assigned on those ports.
+ TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+ metabuilder.matchVlanId(INTERNAL_VLAN);
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective.builder()
+ .withId(nextId)
+ .withType(NextObjective.Type.HASHED)
+ .withMeta(metabuilder.build())
+ .fromApp(appId);
+ // Create the new buckets to be updated
+ portLabels.forEach(pl -> {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(pl.port)
+ .setEthDst(dstMac)
+ .setEthSrc(nodeMacAddr);
+ if (pl.edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+ tBuilder.pushMpls()
+ .copyTtlOut()
+ .setMpls(MplsLabel.mplsLabel(pl.edgeLabel));
+ }
+ nextObjBuilder.addTreatment(tBuilder.build());
+ });
+
+ log.debug("addToHash in device {}: Adding Bucket with port/label {} "
+ + "to nextId {}", deviceId, portLabels, nextId);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("addToHash port/label {} addedTo "
+ + "NextObj {} on {}", portLabels, nextId, deviceId),
+ (objective, error) ->
+ log.warn("addToHash failed to add port/label {} to"
+ + " NextObj {} on {}: {}", portLabels,
+ nextId, deviceId, error));
+ NextObjective nextObjective = nextObjBuilder.addToExisting(context);
+ flowObjectiveService.next(deviceId, nextObjective);
+ }
+
+ /**
+ * Makes a call to the FlowObjective service to remove buckets from
+ * a hash group. User must ensure that all the ports & labels are meant
+ * same neighbor (ie. dstMac).
+ *
+ * @param portLabels a collection of port & label combinations to remove
+ * from the hash group identified by the nextId
+ * @param dstMac destination mac address of next-hop
+ * @param nextId id for next-objective from which buckets will be removed
+ */
+ private void removeFromHashedNextObjective(Collection<PortLabel> portLabels,
+ MacAddress dstMac, Integer nextId) {
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder()
+ .withType(NextObjective.Type.HASHED) //same as original
+ .withId(nextId)
+ .fromApp(appId);
+ // Create the buckets to be removed
+ portLabels.forEach(pl -> {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(pl.port)
+ .setEthDst(dstMac)
+ .setEthSrc(nodeMacAddr);
+ if (pl.edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+ tBuilder.pushMpls()
+ .copyTtlOut()
+ .setMpls(MplsLabel.mplsLabel(pl.edgeLabel));
+ }
+ nextObjBuilder.addTreatment(tBuilder.build());
+ });
+ log.debug("removeFromHash in device {}: Removing Bucket with port/label"
+ + " {} from nextId {}", deviceId, portLabels, nextId);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("port/label {} removedFrom NextObj"
+ + " {} on {}", portLabels, nextId, deviceId),
+ (objective, error) ->
+ log.warn("port/label {} failed to removeFrom NextObj {} on "
+ + "{}: {}", portLabels, nextId, deviceId, error));
+ NextObjective nextObjective = nextObjBuilder.removeFromExisting(context);
+ flowObjectiveService.next(deviceId, nextObjective);
+ }
+
+ /**
+ * Checks all the hash-groups in the target-switch meant for the destination
+ * switch, and either adds or removes buckets to make the neighbor-set
+ * match the given next-hops. Typically called by the master instance of the
+ * destination switch, which may be different from the master instance of the
+ * target switch where hash-group changes are made.
+ *
+ * @param targetSw the switch in which the hash groups will be edited
+ * @param nextHops the current next hops for the target switch to reach
+ * the dest sw
+ * @param destSw the destination switch
+ * @param revoke true if hash groups need to remove buckets from the
+ * the groups to match the current next hops
+ * @return true if calls are made to edit buckets, or if no edits are required
+ */
+ public boolean fixHashGroups(DeviceId targetSw, Set<DeviceId> nextHops,
+ DeviceId destSw, boolean revoke) {
+ // temporary storage of keys to be updated
+ Map<DestinationSetNextObjectiveStoreKey, Set<DeviceId>> tempStore =
+ new HashMap<>();
+ boolean foundNextObjective = false, success = true;
+
+ // retrieve hash-groups meant for destSw, which have destinationSets
+ // with different neighbors than the given next-hops
+ for (DestinationSetNextObjectiveStoreKey dskey : dsNextObjStore.keySet()) {
+ if (!dskey.deviceId().equals(targetSw) ||
+ !dskey.destinationSet().getDestinationSwitches().contains(destSw)) {
+ continue;
+ }
+ foundNextObjective = true;
+ NextNeighbors nhops = dsNextObjStore.get(dskey);
+ Set<DeviceId> currNeighbors = nhops.nextHops(destSw);
+ int edgeLabel = dskey.destinationSet().getEdgeLabel(destSw);
+ Integer nextId = nhops.nextId();
+
+ if (currNeighbors == null || nextHops == null) {
+ log.warn("fixing hash groups but found currNeighbors:{} or nextHops:{}"
+ + " in targetSw:{} for dstSw:{}", currNeighbors, nextHops,
+ targetSw, destSw);
+ success &= false;
+ continue;
+ }
+
+ Set<DeviceId> diff;
+ if (revoke) {
+ diff = Sets.difference(currNeighbors, nextHops);
+ log.debug("targetSw:{} -> dstSw:{} in nextId:{} has current next "
+ + "hops:{} ..removing {}", targetSw, destSw, nextId,
+ currNeighbors, diff);
+ } else {
+ diff = Sets.difference(nextHops, currNeighbors);
+ log.debug("targetSw:{} -> dstSw:{} in nextId:{} has current next "
+ + "hops:{} ..adding {}", targetSw, destSw, nextId,
+ currNeighbors, diff);
+ }
+ boolean suc = updateAllPortsToNextHop(diff, edgeLabel, nextId,
+ revoke);
+ if (suc) {
+ // to update neighbor set with changes made
+ if (revoke) {
+ tempStore.put(dskey, Sets.difference(currNeighbors, diff));
+ } else {
+ tempStore.put(dskey, Sets.union(currNeighbors, diff));
+ }
+ }
+ success &= suc;
+ }
+
+ if (!foundNextObjective) {
+ log.debug("Cannot find any nextObjectives for route targetSw:{} "
+ + "-> dstSw:{}", targetSw, destSw);
+ return true; // nothing to do, return true so ECMPspg is updated
+ }
+
+ // update the dsNextObjectiveStore with new destinationSet to nextId mappings
+ for (DestinationSetNextObjectiveStoreKey key : tempStore.keySet()) {
+ NextNeighbors currentNextHops = dsNextObjStore.get(key);
+ if (currentNextHops == null) {
+ log.warn("fixHashGroups could not update global store in "
+ + "device {} .. missing nextNeighbors for key {}",
+ deviceId, key);
+ continue;
+ }
+ Set<DeviceId> newNeighbors = new HashSet<>();
+ newNeighbors.addAll(tempStore.get(key));
+ Map<DeviceId, Set<DeviceId>> oldDstNextHops =
+ ImmutableMap.copyOf(currentNextHops.dstNextHops());
+ currentNextHops.dstNextHops().put(destSw, newNeighbors); //local change
+ log.debug("Updating nsNextObjStore target:{} -> dst:{} in key:{} nextId:{}",
+ targetSw, destSw, key, currentNextHops.nextId());
+ log.debug("Old dstNextHops: {}", oldDstNextHops);
+ log.debug("New dstNextHops: {}", currentNextHops.dstNextHops());
+ // update global store
+ dsNextObjStore.put(key,
+ new NextNeighbors(currentNextHops.dstNextHops(),
+ currentNextHops.nextId()));
+ }
+ // even if one fails and others succeed, return false so ECMPspg not updated
+ return success;
+ }
+
+ /**
+ * Updates the DestinationSetNextObjectiveStore with any per-destination nexthops
+ * that are not already in the store for the given DestinationSet. Note that
+ * this method does not remove existing next hops for the destinations in the
+ * DestinationSet.
+ *
+ * @param ds the DestinationSet for which the next hops need to be updated
+ * @param newDstNextHops a map of per-destination next hops to update the
+ * destinationSet with
+ * @return true if successful in updating all next hops
+ */
+ private boolean updateNextHops(DestinationSet ds,
+ Map<DeviceId, Set<DeviceId>> newDstNextHops) {
+ DestinationSetNextObjectiveStoreKey key =
+ new DestinationSetNextObjectiveStoreKey(deviceId, ds);
+ NextNeighbors currNext = dsNextObjStore.get(key);
+ Map<DeviceId, Set<DeviceId>> currDstNextHops = currNext.dstNextHops();
+
+ // add newDstNextHops to currDstNextHops for each dst
+ boolean success = true;
+ for (DeviceId dstSw : ds.getDestinationSwitches()) {
+ Set<DeviceId> currNhops = currDstNextHops.get(dstSw);
+ Set<DeviceId> newNhops = newDstNextHops.get(dstSw);
+ currNhops = (currNhops == null) ? Sets.newHashSet() : currNhops;
+ newNhops = (newNhops == null) ? Sets.newHashSet() : newNhops;
+ int edgeLabel = ds.getEdgeLabel(dstSw);
+ int nextId = currNext.nextId();
+
+ // new next hops should be added
+ boolean suc = updateAllPortsToNextHop(Sets.difference(newNhops, currNhops),
+ edgeLabel, nextId, false);
+ if (suc) {
+ currNhops.addAll(newNhops);
+ currDstNextHops.put(dstSw, currNhops); // this is only a local change
+ }
+ success &= suc;
+ }
+
+ if (success) {
+ // update global store
+ dsNextObjStore.put(key, new NextNeighbors(currDstNextHops,
+ currNext.nextId()));
+ log.debug("Updated device:{} ds:{} new next-hops: {}", deviceId, ds,
+ dsNextObjStore.get(key));
+ }
+ return success;
+ }
+
+ /**
+ * Adds or removes buckets for all ports to a set of neighbor devices. Caller
+ * needs to ensure that the given neighbors are all next hops towards the
+ * same destination (represented by the given edgeLabel).
+ *
+ * @param neighbors set of neighbor device ids
+ * @param edgeLabel MPLS label to use in buckets
+ * @param nextId the nextObjective to change
+ * @param revoke true if buckets need to be removed, false if they need to
+ * be added
+ * @return true if successful in adding or removing buckets for all ports
+ * to the neighbors
+ */
+ private boolean updateAllPortsToNextHop(Set<DeviceId> neighbors, int edgeLabel,
+ int nextId, boolean revoke) {
+ for (DeviceId neighbor : neighbors) {
+ MacAddress neighborMac;
+ try {
+ neighborMac = deviceConfig.getDeviceMac(neighbor);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting updateAllPortsToNextHop"
+ + " for nextId:" + nextId);
+ return false;
+ }
+ Collection<PortNumber> portsToNeighbor = devicePortMap.get(neighbor);
+ if (portsToNeighbor == null || portsToNeighbor.isEmpty()) {
+ log.warn("No ports found in dev:{} for neighbor:{} .. cannot "
+ + "updateAllPortsToNextHop for nextId: {}",
+ deviceId, neighbor, nextId);
+ return false;
+ }
+ List<PortLabel> pl = Lists.newArrayList();
+ portsToNeighbor.forEach(p -> pl.add(new PortLabel(p, edgeLabel)));
+ if (revoke) {
+ log.debug("updateAllPortsToNextHops in device {}: Removing Bucket(s) "
+ + "with Port/Label:{} to next object id {}",
+ deviceId, pl, nextId);
+ removeFromHashedNextObjective(pl, neighborMac, nextId);
+ } else {
+ log.debug("fixHashGroup in device {}: Adding Bucket(s) "
+ + "with Port/Label: {} to next object id {}",
+ deviceId, pl, nextId);
+ addToHashedNextObjective(pl, neighborMac, nextId);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Adds or removes a port that has been configured with a vlan to a broadcast group
+ * for bridging. Should only be called by the master instance for this device.
+ *
+ * @param port the port on this device that needs to be added/removed to a bcast group
+ * @param vlanId the vlan id corresponding to the broadcast domain/group
+ * @param popVlan indicates if packets should be sent out untagged or not out
+ * of the port. If true, indicates an access (untagged) or native vlan
+ * configuration. If false, indicates a trunk (tagged) vlan config.
+ * @param portUp true if port is enabled, false if disabled
+ */
+ public void processEdgePort(PortNumber port, VlanId vlanId,
+ boolean popVlan, boolean portUp) {
+ //get the next id for the subnet and edit it.
+ Integer nextId = getVlanNextObjectiveId(vlanId);
+ if (nextId == -1) {
+ if (portUp) {
+ log.debug("**Creating flooding group for first port enabled in"
+ + " vlan {} on dev {} port {}", vlanId, deviceId, port);
+ createBcastGroupFromVlan(vlanId, Collections.singleton(port));
+ } else {
+ log.warn("Could not find flooding group for subnet {} on dev:{} when"
+ + " removing port:{}", vlanId, deviceId, port);
+ }
+ return;
+ }
+
+ log.info("**port{} in device {}: {} Bucket with Port {} to"
+ + " next-id {}", (portUp) ? "UP" : "DOWN", deviceId,
+ (portUp) ? "Adding" : "Removing",
+ port, nextId);
+ // Create the bucket to be added or removed
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (popVlan) {
+ tBuilder.popVlan();
+ }
+ tBuilder.setOutput(port);
+
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+ .addTreatment(tBuilder.build())
+ .withMeta(metadata);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("port {} successfully {} NextObj {} on {}",
+ port, (portUp) ? "addedTo" : "removedFrom",
+ nextId, deviceId),
+ (objective, error) ->
+ log.warn("port {} failed to {} NextObj {} on {}: {}",
+ port, (portUp) ? "addTo" : "removeFrom",
+ nextId, deviceId, error));
+
+ NextObjective nextObj = (portUp) ? nextObjBuilder.addToExisting(context)
+ : nextObjBuilder.removeFromExisting(context);
+ log.debug("edgePort processed: Submited next objective {} in device {}",
+ nextId, deviceId);
+ flowObjectiveService.next(deviceId, nextObj);
+ }
+
+ /**
+ * Returns the next objective of type hashed associated with the destination set.
+ * In addition, updates the existing next-objective if new route-route paths found
+ * have resulted in the addition of new next-hops to a particular destination.
+ * If there is no existing next objective for this destination set, this method
+ * would create a next objective and return the nextId. Optionally metadata can be
+ * passed in for the creation of the next objective.
+ *
+ * @param ds destination set
+ * @param nextHops a map of per destination next hops
+ * @param meta metadata passed into the creation of a Next Objective
+ * @param isBos if Bos is set
+ * @return int if found or -1 if there are errors in the creation of the
+ * neighbor set.
+ */
+ public int getNextObjectiveId(DestinationSet ds,
+ Map<DeviceId, Set<DeviceId>> nextHops,
+ TrafficSelector meta, boolean isBos) {
+ NextNeighbors next = dsNextObjStore.
+ get(new DestinationSetNextObjectiveStoreKey(deviceId, ds));
+ if (next == null) {
+ log.debug("getNextObjectiveId in device{}: Next objective id "
+ + "not found for {} ... creating", deviceId, ds);
+ log.trace("getNextObjectiveId: nsNextObjStore contents for device {}: {}",
+ deviceId,
+ dsNextObjStore.entrySet()
+ .stream()
+ .filter((nsStoreEntry) ->
+ (nsStoreEntry.getKey().deviceId().equals(deviceId)))
+ .collect(Collectors.toList()));
+
+ createGroupFromDestinationSet(ds, nextHops, meta, isBos);
+ next = dsNextObjStore.
+ get(new DestinationSetNextObjectiveStoreKey(deviceId, ds));
+ if (next == null) {
+ log.warn("getNextObjectiveId: unable to create next objective");
+ // failure in creating group
+ return -1;
+ } else {
+ log.debug("getNextObjectiveId in device{}: Next objective id {} "
+ + "created for {}", deviceId, next.nextId(), ds);
+ }
+ } else {
+ log.trace("getNextObjectiveId in device{}: Next objective id {} "
+ + "found for {}", deviceId, next.nextId(), ds);
+ // should fix hash groups too if next-hops have changed
+ if (!next.dstNextHops().equals(nextHops)) {
+ log.debug("Nexthops have changed for dev:{} nextId:{} ..updating",
+ deviceId, next.nextId());
+ if (!updateNextHops(ds, nextHops)) {
+ // failure in updating group
+ return -1;
+ }
+ }
+ }
+ return next.nextId();
+ }
+
+ /**
+ * Returns the next objective of type broadcast associated with the vlan,
+ * or -1 if no such objective exists. Note that this method does NOT create
+ * the next objective as a side-effect. It is expected that is objective is
+ * created at startup from network configuration. Typically this is used
+ * for L2 flooding within the subnet configured on the switch.
+ *
+ * @param vlanId vlan id
+ * @return int if found or -1
+ */
+ public int getVlanNextObjectiveId(VlanId vlanId) {
+ Integer nextId = vlanNextObjStore.
+ get(new VlanNextObjectiveStoreKey(deviceId, vlanId));
+
+ return (nextId != null) ? nextId : -1;
+ }
+
+ /**
+ * Returns the next objective of type simple associated with the port on the
+ * device, given the treatment. Different treatments to the same port result
+ * in different next objectives. If no such objective exists, this method
+ * creates one (if requested) and returns the id. Optionally metadata can be passed in for
+ * the creation of the objective. Typically this is used for L2 and L3 forwarding
+ * to compute nodes and containers/VMs on the compute nodes directly attached
+ * to the switch.
+ *
+ * @param portNum the port number for the simple next objective
+ * @param treatment the actions to apply on the packets (should include outport)
+ * @param meta optional metadata passed into the creation of the next objective
+ * @param createIfMissing true if a next object should be created if not found
+ * @return int if found or created, -1 if there are errors during the
+ * creation of the next objective.
+ */
+ public int getPortNextObjectiveId(PortNumber portNum, TrafficTreatment treatment,
+ TrafficSelector meta, boolean createIfMissing) {
+ Integer nextId = portNextObjStore
+ .get(new PortNextObjectiveStoreKey(deviceId, portNum, treatment, meta));
+ if (nextId != null) {
+ return nextId;
+ }
+ log.debug("getPortNextObjectiveId in device {}: Next objective id "
+ + "not found for port: {} .. {}", deviceId, portNum,
+ (createIfMissing) ? "creating" : "aborting");
+ if (!createIfMissing) {
+ return -1;
+ }
+ // create missing next objective
+ createGroupFromPort(portNum, treatment, meta);
+ nextId = portNextObjStore.get(new PortNextObjectiveStoreKey(deviceId, portNum,
+ treatment, meta));
+ if (nextId == null) {
+ log.warn("getPortNextObjectiveId: unable to create next obj"
+ + "for dev:{} port:{}", deviceId, portNum);
+ return -1;
+ }
+ return nextId;
+ }
+
+ /**
+ * Checks if the next objective ID (group) for the neighbor set exists or not.
+ *
+ * @param ns neighbor set to check
+ * @return true if it exists, false otherwise
+ */
+ public boolean hasNextObjectiveId(DestinationSet ns) {
+ NextNeighbors nextHops = dsNextObjStore.
+ get(new DestinationSetNextObjectiveStoreKey(deviceId, ns));
+ if (nextHops == null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void populateNeighborMaps() {
+ Set<Link> outgoingLinks = linkService.getDeviceEgressLinks(deviceId);
+ for (Link link : outgoingLinks) {
+ if (link.type() != Link.Type.DIRECT) {
+ continue;
+ }
+ addNeighborAtPort(link.dst().deviceId(), link.src().port());
+ }
+ }
+
+ protected void addNeighborAtPort(DeviceId neighborId,
+ PortNumber portToNeighbor) {
+ // Update DeviceToPort database
+ log.debug("Device {} addNeighborAtPort: neighbor {} at port {}",
+ deviceId, neighborId, portToNeighbor);
+ Set<PortNumber> ports = Collections
+ .newSetFromMap(new ConcurrentHashMap<PortNumber, Boolean>());
+ ports.add(portToNeighbor);
+ Set<PortNumber> portnums = devicePortMap.putIfAbsent(neighborId, ports);
+ if (portnums != null) {
+ portnums.add(portToNeighbor);
+ }
+
+ // Update portToDevice database
+ DeviceId prev = portDeviceMap.putIfAbsent(portToNeighbor, neighborId);
+ if (prev != null) {
+ log.debug("Device: {} port: {} already has neighbor: {} ",
+ deviceId, portToNeighbor, prev, neighborId);
+ }
+ }
+
+ /**
+ * Creates a NextObjective for a hash group in this device from a given
+ * DestinationSet.
+ *
+ * @param ds the DestinationSet
+ * @param neighbors a map for each destination and its next-hops
+ * @param meta metadata passed into the creation of a Next Objective
+ * @param isBos if BoS is set
+ */
+ public void createGroupFromDestinationSet(DestinationSet ds,
+ Map<DeviceId, Set<DeviceId>> neighbors,
+ TrafficSelector meta,
+ boolean isBos) {
+ int nextId = flowObjectiveService.allocateNextId();
+ NextObjective.Type type = NextObjective.Type.HASHED;
+ if (neighbors == null || neighbors.isEmpty()) {
+ log.warn("createGroupsFromDestinationSet: needs at least one neighbor"
+ + "to create group in dev:{} for ds: {} with next-hops {}",
+ deviceId, ds, neighbors);
+ return;
+ }
+ // If Bos == False and MPLS-ECMP == false, we have
+ // to use simple group and we will pick a single neighbor for a single dest.
+ if (!isBos && !srManager.getMplsEcmp()) {
+ type = NextObjective.Type.SIMPLE;
+ }
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder()
+ .withId(nextId)
+ .withType(type)
+ .fromApp(appId);
+ if (meta != null) {
+ nextObjBuilder.withMeta(meta);
+ }
+
+ // create treatment buckets for each neighbor for each dst Device
+ // except in the special case where we only want to pick a single
+ // neighbor for a simple group
+ boolean foundSingleNeighbor = false;
+ boolean treatmentAdded = false;
+ Map<DeviceId, Set<DeviceId>> dstNextHops = new ConcurrentHashMap<>();
+ for (DeviceId dst : ds.getDestinationSwitches()) {
+ Set<DeviceId> nextHops = neighbors.get(dst);
+ if (nextHops == null || nextHops.isEmpty()) {
+ continue;
+ }
+
+ if (foundSingleNeighbor) {
+ break;
+ }
+
+ for (DeviceId neighborId : nextHops) {
+ if (devicePortMap.get(neighborId) == null) {
+ log.warn("Neighbor {} is not in the port map yet for dev:{}",
+ neighborId, deviceId);
+ return;
+ } else if (devicePortMap.get(neighborId).isEmpty()) {
+ log.warn("There are no ports for "
+ + "the Device {} in the port map yet", neighborId);
+ return;
+ }
+
+ MacAddress neighborMac;
+ try {
+ neighborMac = deviceConfig.getDeviceMac(neighborId);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting createGroupsFromDestinationset.");
+ return;
+ }
+ // For each port to the neighbor, we create a new treatment
+ Set<PortNumber> neighborPorts = devicePortMap.get(neighborId);
+ // In this case we are using a SIMPLE group. We randomly pick a port
+ if (!isBos && !srManager.getMplsEcmp()) {
+ int size = devicePortMap.get(neighborId).size();
+ int index = RandomUtils.nextInt(0, size);
+ neighborPorts = Collections.singleton(
+ Iterables.get(devicePortMap.get(neighborId),
+ index));
+ foundSingleNeighbor = true;
+ }
+
+ for (PortNumber sp : neighborPorts) {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment
+ .builder();
+ tBuilder.setEthDst(neighborMac).setEthSrc(nodeMacAddr);
+ int edgeLabel = ds.getEdgeLabel(dst);
+ if (edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+ tBuilder.pushMpls()
+ .copyTtlOut()
+ .setMpls(MplsLabel.mplsLabel(edgeLabel));
+ }
+ tBuilder.setOutput(sp);
+ nextObjBuilder.addTreatment(tBuilder.build());
+ treatmentAdded = true;
+ //update store
+ Set<DeviceId> existingNeighbors = dstNextHops.get(dst);
+ if (existingNeighbors == null) {
+ existingNeighbors = new HashSet<>();
+ }
+ existingNeighbors.add(neighborId);
+ dstNextHops.put(dst, existingNeighbors);
+ log.debug("creating treatment for port/label {}/{} in next:{}",
+ sp, edgeLabel, nextId);
+ }
+
+ if (foundSingleNeighbor) {
+ break;
+ }
+ }
+ }
+
+ if (!treatmentAdded) {
+ log.warn("Could not createGroup from DestinationSet {} without any"
+ + "next hops {}", ds, neighbors);
+ return;
+ }
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.debug("createGroupsFromDestinationSet installed "
+ + "NextObj {} on {}", nextId, deviceId),
+ (objective, error) ->
+ log.warn("createGroupsFromDestinationSet failed to install"
+ + " NextObj {} on {}: {}", nextId, deviceId, error)
+ );
+ NextObjective nextObj = nextObjBuilder.add(context);
+ log.debug(".. createGroupsFromDestinationSet: Submitted "
+ + "next objective {} in device {}", nextId, deviceId);
+ flowObjectiveService.next(deviceId, nextObj);
+ //update store
+ dsNextObjStore.put(new DestinationSetNextObjectiveStoreKey(deviceId, ds),
+ new NextNeighbors(dstNextHops, nextId));
+ }
+
+ /**
+ * Creates broadcast groups for all ports in the same subnet for
+ * all configured subnets.
+ */
+ public void createGroupsFromVlanConfig() {
+ srManager.getVlanPortMap(deviceId).asMap().forEach((vlanId, ports) -> {
+ createBcastGroupFromVlan(vlanId, ports);
+ });
+ }
+
+ /**
+ * Creates a single broadcast group from a given vlan id and list of ports.
+ *
+ * @param vlanId vlan id
+ * @param ports list of ports in the subnet
+ */
+ public void createBcastGroupFromVlan(VlanId vlanId, Collection<PortNumber> ports) {
+ VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+
+ if (vlanNextObjStore.containsKey(key)) {
+ log.debug("Broadcast group for device {} and subnet {} exists",
+ deviceId, vlanId);
+ return;
+ }
+
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+ int nextId = flowObjectiveService.allocateNextId();
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+ .withMeta(metadata);
+
+ ports.forEach(port -> {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (toPopVlan(port, vlanId)) {
+ tBuilder.popVlan();
+ }
+ tBuilder.setOutput(port);
+ nextObjBuilder.addTreatment(tBuilder.build());
+ });
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.debug("createBroadcastGroupFromVlan installed "
+ + "NextObj {} on {}", nextId, deviceId),
+ (objective, error) ->
+ log.warn("createBroadcastGroupFromVlan failed to install"
+ + " NextObj {} on {}: {}", nextId, deviceId, error)
+ );
+ NextObjective nextObj = nextObjBuilder.add(context);
+ flowObjectiveService.next(deviceId, nextObj);
+ log.debug("createBcastGroupFromVlan: Submitted next objective {} "
+ + "for vlan: {} in device {}", nextId, vlanId, deviceId);
+
+ vlanNextObjStore.put(key, nextId);
+ }
+
+ /**
+ * Removes a single broadcast group from a given vlan id.
+ * The group should be empty.
+ * @param deviceId device Id to remove the group
+ * @param portNum port number related to the group
+ * @param vlanId vlan id of the broadcast group to remove
+ * @param popVlan true if the TrafficTreatment involves pop vlan tag action
+ */
+ public void removeBcastGroupFromVlan(DeviceId deviceId, PortNumber portNum,
+ VlanId vlanId, boolean popVlan) {
+ VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+
+ if (!vlanNextObjStore.containsKey(key)) {
+ log.debug("Broadcast group for device {} and subnet {} does not exist",
+ deviceId, vlanId);
+ return;
+ }
+
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+ int nextId = vlanNextObjStore.get(key);
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+ .withMeta(metadata);
+
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (popVlan) {
+ tBuilder.popVlan();
+ }
+ tBuilder.setOutput(portNum);
+ nextObjBuilder.addTreatment(tBuilder.build());
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.debug("removeBroadcastGroupFromVlan removed "
+ + "NextObj {} on {}", nextId, deviceId),
+ (objective, error) ->
+ log.warn("removeBroadcastGroupFromVlan failed to remove "
+ + " NextObj {} on {}: {}", nextId, deviceId, error)
+ );
+ NextObjective nextObj = nextObjBuilder.remove(context);
+ flowObjectiveService.next(deviceId, nextObj);
+ log.debug("removeBcastGroupFromVlan: Submited next objective {} in device {}",
+ nextId, deviceId);
+
+ vlanNextObjStore.remove(key, nextId);
+ }
+
+ /**
+ * Determine if we should pop given vlan before sending packets to the given port.
+ *
+ * @param portNumber port number
+ * @param vlanId vlan id
+ * @return true if the vlan id is not contained in any vlanTagged config
+ */
+ private boolean toPopVlan(PortNumber portNumber, VlanId vlanId) {
+ return srManager.interfaceService
+ .getInterfacesByPort(new ConnectPoint(deviceId, portNumber))
+ .stream().noneMatch(intf -> intf.vlanTagged().contains(vlanId));
+ }
+
+ /**
+ * Create simple next objective for a single port. The treatments can include
+ * all outgoing actions that need to happen on the packet.
+ *
+ * @param portNum the outgoing port on the device
+ * @param treatment the actions to apply on the packets (should include outport)
+ * @param meta optional data to pass to the driver
+ */
+ public void createGroupFromPort(PortNumber portNum, TrafficTreatment treatment,
+ TrafficSelector meta) {
+ int nextId = flowObjectiveService.allocateNextId();
+ PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+ deviceId, portNum, treatment, meta);
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.SIMPLE)
+ .addTreatment(treatment)
+ .fromApp(appId)
+ .withMeta(meta);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.debug("createGroupFromPort installed "
+ + "NextObj {} on {}", nextId, deviceId),
+ (objective, error) ->
+ log.warn("createGroupFromPort failed to install"
+ + " NextObj {} on {}: {}", nextId, deviceId, error)
+ );
+ NextObjective nextObj = nextObjBuilder.add(context);
+ flowObjectiveService.next(deviceId, nextObj);
+ log.debug("createGroupFromPort: Submited next objective {} in device {} "
+ + "for port {}", nextId, deviceId, portNum);
+
+ portNextObjStore.put(key, nextId);
+ }
+
+ /**
+ * Removes simple next objective for a single port.
+ *
+ * @param deviceId device id that has the port to deal with
+ * @param portNum the outgoing port on the device
+ * @param vlanId vlan id associated with the port
+ * @param popVlan true if POP_VLAN action is applied on the packets, false otherwise
+ */
+ public void removePortNextObjective(DeviceId deviceId, PortNumber portNum, VlanId vlanId, boolean popVlan) {
+ TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+ mbuilder.matchVlanId(vlanId);
+
+ TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+ tbuilder.immediate().setOutput(portNum);
+ if (popVlan) {
+ tbuilder.immediate().popVlan();
+ }
+
+ int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNum,
+ tbuilder.build(), mbuilder.build(), false);
+
+ PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+ deviceId, portNum, tbuilder.build(), mbuilder.build());
+ if (portNextObjId != -1 && portNextObjStore.containsKey(key)) {
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(portNextObjId)
+ .withType(NextObjective.Type.SIMPLE).fromApp(appId);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("removePortNextObjective removes NextObj {} on {}",
+ portNextObjId, deviceId),
+ (objective, error) ->
+ log.warn("removePortNextObjective failed to remove NextObj {} on {}: {}",
+ portNextObjId, deviceId, error));
+ NextObjective nextObjective = nextObjBuilder.remove(context);
+ log.info("**removePortNextObjective: Submitted "
+ + "next objective {} in device {}",
+ portNextObjId, deviceId);
+ flowObjectiveService.next(deviceId, nextObjective);
+
+ portNextObjStore.remove(key);
+ }
+ }
+ /**
+ * Removes groups for the next objective ID given.
+ *
+ * @param objectiveId next objective ID to remove
+ * @return true if succeeds, false otherwise
+ */
+ public boolean removeGroup(int objectiveId) {
+ for (Map.Entry<DestinationSetNextObjectiveStoreKey, NextNeighbors> e :
+ dsNextObjStore.entrySet()) {
+ if (e.getValue().nextId() != objectiveId) {
+ continue;
+ }
+ // Right now it is just used in TunnelHandler
+ // remember in future that PW transit groups could
+ // be Indirect groups
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(objectiveId)
+ .withType(NextObjective.Type.HASHED).fromApp(appId);
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("RemoveGroup removes NextObj {} on {}",
+ objectiveId, deviceId),
+ (objective, error) ->
+ log.warn("RemoveGroup failed to remove NextObj {} on {}: {}",
+ objectiveId, deviceId, error));
+ NextObjective nextObjective = nextObjBuilder.remove(context);
+ log.info("**removeGroup: Submited "
+ + "next objective {} in device {}",
+ objectiveId, deviceId);
+ flowObjectiveService.next(deviceId, nextObjective);
+
+ dsNextObjStore.remove(e.getKey());
+ return true;
+ }
+
+ return false;
+ }
+ /**
+ * Remove simple next objective for a single port. The treatments can include
+ * all outgoing actions that need to happen on the packet.
+ *
+ * @param portNum the outgoing port on the device
+ * @param treatment the actions applied on the packets (should include outport)
+ * @param meta optional data to pass to the driver
+ */
+ public void removeGroupFromPort(PortNumber portNum, TrafficTreatment treatment,
+ TrafficSelector meta) {
+ PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+ deviceId, portNum, treatment, meta);
+ Integer nextId = portNextObjStore.get(key);
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.SIMPLE)
+ .addTreatment(treatment)
+ .fromApp(appId)
+ .withMeta(meta);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) ->
+ log.info("removeGroupFromPort installed "
+ + "NextObj {} on {}", nextId, deviceId),
+ (objective, error) ->
+ log.warn("removeGroupFromPort failed to install"
+ + " NextObj {} on {}: {}", nextId, deviceId, error)
+ );
+ NextObjective nextObj = nextObjBuilder.remove(context);
+ flowObjectiveService.next(deviceId, nextObj);
+ log.info("removeGroupFromPort: Submitted next objective {} in device {} "
+ + "for port {}", nextId, deviceId, portNum);
+
+ portNextObjStore.remove(key);
+ }
+
+ /**
+ * Removes all groups from all next objective stores.
+ */
+ /*public void removeAllGroups() {
+ for (Map.Entry<NeighborSetNextObjectiveStoreKey, NextNeighbors> entry:
+ nsNextObjStore.entrySet()) {
+ removeGroup(entry.getValue().nextId());
+ }
+ for (Map.Entry<PortNextObjectiveStoreKey, Integer> entry:
+ portNextObjStore.entrySet()) {
+ removeGroup(entry.getValue());
+ }
+ for (Map.Entry<VlanNextObjectiveStoreKey, Integer> entry:
+ vlanNextObjStore.entrySet()) {
+ removeGroup(entry.getValue());
+ }
+ }*/ //XXX revisit
+
+ /**
+ * Triggers a one time bucket verification operation on all hash groups
+ * on this device.
+ */
+ public void triggerBucketCorrector() {
+ BucketCorrector bc = new BucketCorrector();
+ bc.run();
+ }
+
+ public void updateGroupFromVlanConfiguration(PortNumber portNumber, Collection<VlanId> vlanIds,
+ int nextId, boolean install) {
+ vlanIds.forEach(vlanId -> updateGroupFromVlanInternal(vlanId, portNumber, nextId, install));
+ }
+
+ private void updateGroupFromVlanInternal(VlanId vlanId, PortNumber portNum, int nextId, boolean install) {
+ TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+ if (toPopVlan(portNum, vlanId)) {
+ tBuilder.popVlan();
+ }
+ tBuilder.setOutput(portNum);
+
+ TrafficSelector metadata =
+ DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective
+ .builder().withId(nextId)
+ .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+ .addTreatment(tBuilder.build())
+ .withMeta(metadata);
+
+ ObjectiveContext context = new DefaultObjectiveContext(
+ (objective) -> log.debug("port {} successfully removedFrom NextObj {} on {}",
+ portNum, nextId, deviceId),
+ (objective, error) ->
+ log.warn("port {} failed to removedFrom NextObj {} on {}: {}",
+ portNum, nextId, deviceId, error));
+
+ if (install) {
+ flowObjectiveService.next(deviceId, nextObjBuilder.addToExisting(context));
+ } else {
+ flowObjectiveService.next(deviceId, nextObjBuilder.removeFromExisting(context));
+ }
+ }
+
+ /**
+ * Performs bucket verification operation for all hash groups in this device.
+ * Checks RouteHandler to ensure that routing is stable before attempting
+ * verification. Verification involves creating a nextObjective with
+ * operation VERIFY for existing next objectives in the store, and passing
+ * it to the driver. It is the driver that actually performs the verification
+ * by adding or removing buckets to match the verification next objective
+ * created here.
+ */
+ protected final class BucketCorrector implements Runnable {
+ Integer nextId;
+
+ BucketCorrector() {
+ this.nextId = null;
+ }
+
+ BucketCorrector(Integer nextId) {
+ this.nextId = nextId;
+ }
+
+ @Override
+ public void run() {
+ if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+ return;
+ }
+ DefaultRoutingHandler rh = srManager.getRoutingHandler();
+ if (rh == null) {
+ return;
+ }
+ if (!rh.isRoutingStable()) {
+ return;
+ }
+ rh.acquireRoutingLock();
+ try {
+ log.trace("running bucket corrector for dev: {}", deviceId);
+ Set<DestinationSetNextObjectiveStoreKey> dsKeySet = dsNextObjStore.entrySet()
+ .stream()
+ .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+ // Filter out PW transit groups or include them if MPLS ECMP is supported
+ .filter(entry -> !entry.getKey().destinationSet().mplsSet() ||
+ (entry.getKey().destinationSet().mplsSet() && srManager.getMplsEcmp()))
+ .map(entry -> entry.getKey())
+ .collect(Collectors.toSet());
+ for (DestinationSetNextObjectiveStoreKey dsKey : dsKeySet) {
+ NextNeighbors next = dsNextObjStore.get(dsKey);
+ if (next == null) {
+ continue;
+ }
+ int nid = next.nextId();
+ if (nextId != null && nextId != nid) {
+ continue;
+ }
+ log.trace("bkt-corr: dsNextObjStore for device {}: {}",
+ deviceId, dsKey, next);
+ TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+ metabuilder.matchVlanId(INTERNAL_VLAN);
+ NextObjective.Builder nextObjBuilder = DefaultNextObjective.builder()
+ .withId(nid)
+ .withType(NextObjective.Type.HASHED)
+ .withMeta(metabuilder.build())
+ .fromApp(appId);
+
+ next.dstNextHops().forEach((dstDev, nextHops) -> {
+ int edgeLabel = dsKey.destinationSet().getEdgeLabel(dstDev);
+ nextHops.forEach(neighbor -> {
+ MacAddress neighborMac;
+ try {
+ neighborMac = deviceConfig.getDeviceMac(neighbor);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage() + " Aborting neighbor"
+ + neighbor);
+ return;
+ }
+ devicePortMap.get(neighbor).forEach(port -> {
+ log.trace("verify in device {} nextId {}: bucket with"
+ + " port/label {}/{} to dst {} via {}",
+ deviceId, nid, port, edgeLabel,
+ dstDev, neighbor);
+ nextObjBuilder.addTreatment(treatmentBuilder(port,
+ neighborMac, edgeLabel));
+ });
+ });
+ });
+
+ NextObjective nextObjective = nextObjBuilder.verify();
+ flowObjectiveService.next(deviceId, nextObjective);
+ }
+ } finally {
+ rh.releaseRoutingLock();
+ }
+
+ }
+
+ TrafficTreatment treatmentBuilder(PortNumber outport, MacAddress dstMac,
+ int edgeLabel) {
+ TrafficTreatment.Builder tBuilder =
+ DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(outport)
+ .setEthDst(dstMac)
+ .setEthSrc(nodeMacAddr);
+ if (edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+ tBuilder.pushMpls()
+ .copyTtlOut()
+ .setMpls(MplsLabel.mplsLabel(edgeLabel));
+ }
+ return tBuilder.build();
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java
new file mode 100644
index 0000000..7f839cf
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+
+import com.google.common.base.MoreObjects.ToStringHelper;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Representation of a set of destination switch dpids along with their edge-node
+ * labels. Meant to be used as a lookup-key in a hash-map to retrieve an ECMP-group
+ * that hashes packets towards a specific destination switch,
+ * or paired-destination switches.
+ */
+public class DestinationSet {
+ public static final int NO_EDGE_LABEL = -1;
+ private static final int NOT_ASSIGNED = 0;
+ private boolean mplsSet;
+ private final DeviceId dstSw1;
+ private final int edgeLabel1;
+ private final DeviceId dstSw2;
+ private final int edgeLabel2;
+
+
+ private static final Logger log = getLogger(DestinationSet.class);
+
+ /**
+ * Constructor for a single destination with no Edge label.
+ *
+ * @param isMplsSet indicates if it is a mpls destination set
+ * @param dstSw the destination switch
+ */
+ public DestinationSet(boolean isMplsSet, DeviceId dstSw) {
+ this.edgeLabel1 = NO_EDGE_LABEL;
+ this.mplsSet = isMplsSet;
+ this.dstSw1 = dstSw;
+ this.edgeLabel2 = NOT_ASSIGNED;
+ this.dstSw2 = null;
+ }
+
+ /**
+ * Constructor for a single destination with Edge label.
+ *
+ * @param isMplsSet indicates if it is a mpls destination set
+ * @param edgeLabel label to be pushed as part of group operation
+ * @param dstSw the destination switch
+ */
+ public DestinationSet(boolean isMplsSet,
+ int edgeLabel, DeviceId dstSw) {
+ this.mplsSet = isMplsSet;
+ this.edgeLabel1 = edgeLabel;
+ this.dstSw1 = dstSw;
+ this.edgeLabel2 = NOT_ASSIGNED;
+ this.dstSw2 = null;
+ }
+
+ /**
+ * Constructor for paired destination switches and their associated
+ * edge labels.
+ *
+ * @param isMplsSet indicates if it is a mpls destination set
+ * @param edgeLabel1 label to be pushed as part of group operation for dstSw1
+ * @param dstSw1 one of the paired destination switches
+ * @param edgeLabel2 label to be pushed as part of group operation for dstSw2
+ * @param dstSw2 the other paired destination switch
+ */
+ public DestinationSet(boolean isMplsSet,
+ int edgeLabel1, DeviceId dstSw1,
+ int edgeLabel2, DeviceId dstSw2) {
+ this.mplsSet = isMplsSet;
+ if (dstSw1.toString().compareTo(dstSw2.toString()) <= 0) {
+ this.edgeLabel1 = edgeLabel1;
+ this.dstSw1 = dstSw1;
+ this.edgeLabel2 = edgeLabel2;
+ this.dstSw2 = dstSw2;
+ } else {
+ this.edgeLabel1 = edgeLabel2;
+ this.dstSw1 = dstSw2;
+ this.edgeLabel2 = edgeLabel1;
+ this.dstSw2 = dstSw1;
+ }
+ }
+
+ /**
+ * Default constructor for kryo serialization.
+ */
+ public DestinationSet() {
+ this.edgeLabel1 = NOT_ASSIGNED;
+ this.edgeLabel2 = NOT_ASSIGNED;
+ this.mplsSet = true;
+ this.dstSw1 = DeviceId.NONE;
+ this.dstSw2 = DeviceId.NONE;
+ }
+
+ /**
+ * Factory method for DestinationSet hierarchy.
+ *
+ * @param random the expected behavior.
+ * @param isMplsSet indicates if it is a mpls destination set
+ * @param dstSw the destination switch
+ * @return the destination set object.
+ */
+ public static DestinationSet destinationSet(boolean random,
+ boolean isMplsSet, DeviceId dstSw) {
+ return random ? new RandomDestinationSet(dstSw)
+ : new DestinationSet(isMplsSet, dstSw);
+ }
+
+ /**
+ * Factory method for DestinationSet hierarchy.
+ *
+ * @param random the expected behavior.
+ * @param isMplsSet indicates if it is a mpls destination set
+ * @param edgeLabel label to be pushed as part of group operation
+ * @param dstSw the destination switch
+ * @return the destination set object
+ */
+ public static DestinationSet destinationSet(boolean random,
+ boolean isMplsSet, int edgeLabel,
+ DeviceId dstSw) {
+ return random ? new RandomDestinationSet(edgeLabel, dstSw)
+ : new DestinationSet(isMplsSet, edgeLabel, dstSw);
+ }
+
+ /**
+ * Factory method for DestinationSet hierarchy.
+ *
+ * @param random the expected behavior.
+ * @return the destination set object
+ */
+ public static DestinationSet destinationSet(boolean random) {
+ return random ? new RandomDestinationSet() : new DestinationSet();
+ }
+
+ /**
+ * Gets the label associated with given destination switch.
+ *
+ * @param dstSw the destination switch
+ * @return integer the label associated with the destination switch
+ */
+ public int getEdgeLabel(DeviceId dstSw) {
+ if (dstSw.equals(dstSw1)) {
+ return edgeLabel1;
+ } else if (dstSw.equals(dstSw2)) {
+ return edgeLabel2;
+ }
+ return NOT_ASSIGNED;
+ }
+
+ /**
+ * Gets all the destination switches in this destination set.
+ *
+ * @return a set of destination switch ids
+ */
+ public Set<DeviceId> getDestinationSwitches() {
+ Set<DeviceId> dests = new HashSet<>();
+ dests.add(dstSw1);
+ if (dstSw2 != null) {
+ dests.add(dstSw2);
+ }
+ return dests;
+ }
+
+ /**
+ * Gets the value of mplsSet.
+ *
+ * @return the value of mplsSet
+ */
+ public boolean mplsSet() {
+ return mplsSet;
+ }
+
+ // The list of destination ids and label are used for comparison.
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DestinationSet)) {
+ return false;
+ }
+ DestinationSet that = (DestinationSet) o;
+ boolean equal = (this.edgeLabel1 == that.edgeLabel1 &&
+ this.mplsSet == that.mplsSet &&
+ this.dstSw1.equals(that.dstSw1));
+ if (this.dstSw2 != null && that.dstSw2 == null ||
+ this.dstSw2 == null && that.dstSw2 != null) {
+ return false;
+ }
+ if (this.dstSw2 != null && that.dstSw2 != null) {
+ equal = equal && (this.edgeLabel2 == that.edgeLabel2 &&
+ this.dstSw2.equals(that.dstSw2));
+ }
+ return equal;
+ }
+
+ // The list of destination ids and label are used for comparison.
+ @Override
+ public int hashCode() {
+ if (dstSw2 == null) {
+ return Objects.hash(mplsSet, edgeLabel1, dstSw1);
+ }
+ return Objects.hash(mplsSet, edgeLabel1, dstSw1, edgeLabel2, dstSw2);
+ }
+
+ @Override
+ public String toString() {
+ ToStringHelper h = toStringHelper(this)
+ .add("MplsSet", mplsSet)
+ .add("DstSw1", dstSw1)
+ .add("Label1", edgeLabel1);
+ if (dstSw2 != null) {
+ h.add("DstSw2", dstSw2)
+ .add("Label2", edgeLabel2);
+ }
+ return h.toString();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java
new file mode 100644
index 0000000..1c2e1ac
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onosproject.net.PortNumber;
+
+/**
+ * Representation of policy group bucket identifier. Not exposed to
+ * the application and only to be used internally.
+ */
+public class GroupBucketIdentifier {
+ private int label;
+ private BucketOutputType type;
+ private PortNumber outPort;
+ private PolicyGroupIdentifier outGroup;
+
+ protected enum BucketOutputType {
+ PORT,
+ GROUP
+ }
+
+ protected GroupBucketIdentifier(int label,
+ PortNumber outPort) {
+ this.label = label;
+ this.type = BucketOutputType.PORT;
+ this.outPort = checkNotNull(outPort);
+ this.outGroup = null;
+ }
+
+ protected GroupBucketIdentifier(int label,
+ PolicyGroupIdentifier outGroup) {
+ this.label = label;
+ this.type = BucketOutputType.GROUP;
+ this.outPort = null;
+ this.outGroup = checkNotNull(outGroup);
+ }
+
+ protected int label() {
+ return this.label;
+ }
+
+ protected BucketOutputType type() {
+ return this.type;
+ }
+
+ protected PortNumber outPort() {
+ return this.outPort;
+ }
+
+ protected PolicyGroupIdentifier outGroup() {
+ return this.outGroup;
+ }
+}
+
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java
new file mode 100644
index 0000000..eaca1aa
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.onosproject.net.DeviceId;
+
+/**
+ * Represents the nexthop information associated with a route-path towards a
+ * set of destinations.
+ */
+public class NextNeighbors {
+ private final Map<DeviceId, Set<DeviceId>> dstNextHops;
+ private final int nextId;
+
+ /**
+ * Constructor.
+ *
+ * @param dstNextHops map of destinations and the next-hops towards each dest
+ * @param nextId id of nextObjective that manifests the next-hop info
+ */
+ public NextNeighbors(Map<DeviceId, Set<DeviceId>> dstNextHops, int nextId) {
+ this.dstNextHops = dstNextHops;
+ this.nextId = nextId;
+ }
+
+ /**
+ * Returns a map of destinations and the next-hops towards them.
+ *
+ * @return map of destinations and the next-hops towards them
+ */
+ public Map<DeviceId, Set<DeviceId>> dstNextHops() {
+ return dstNextHops;
+ }
+
+ /**
+ * Set of next-hops towards the given destination.
+ *
+ * @param deviceId the destination
+ * @return set of nexthops towards the destination
+ */
+ public Set<DeviceId> nextHops(DeviceId deviceId) {
+ return dstNextHops.get(deviceId);
+ }
+
+ /**
+ * Return the nextId representing the nextObjective towards the next-hops.
+ *
+ * @return nextId representing the nextObjective towards the next-hops
+ */
+ public int nextId() {
+ return nextId;
+ }
+
+ /**
+ * Checks if the given nextHopId is a valid next hop to any one of the
+ * destinations.
+ *
+ * @param nextHopId the deviceId for the next hop
+ * @return true if given next
+ */
+ public boolean containsNextHop(DeviceId nextHopId) {
+ for (Set<DeviceId> nextHops : dstNextHops.values()) {
+ if (nextHops != null && nextHops.contains(nextHopId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a set of destinations which have the given nextHopId as one
+ * of the next-hops to that destination.
+ *
+ * @param nextHopId the deviceId for the next hop
+ * @return set of deviceIds that have the given nextHopId as a next-hop
+ * which could be empty if no destinations were found
+ */
+ public Set<DeviceId> getDstForNextHop(DeviceId nextHopId) {
+ Set<DeviceId> dstSet = new HashSet<>();
+ for (DeviceId dstKey : dstNextHops.keySet()) {
+ if (dstNextHops.get(dstKey).contains(nextHopId)) {
+ dstSet.add(dstKey);
+ }
+ }
+ return dstSet;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof NextNeighbors)) {
+ return false;
+ }
+ NextNeighbors that = (NextNeighbors) o;
+ return (this.nextId == that.nextId) &&
+ this.dstNextHops.equals(that.dstNextHops);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nextId, dstNextHops);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("nextId", nextId)
+ .add("dstNextHops", dstNextHops)
+ .toString();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java
new file mode 100644
index 0000000..c0935ce
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceProperties;
+import org.onosproject.segmentrouting.grouphandler.GroupBucketIdentifier.BucketOutputType;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.link.LinkService;
+import org.slf4j.Logger;
+
+/**
+ * A module to create group chains based on the specified device
+ * ports and label stack to be applied on each port.
+ */
+public class PolicyGroupHandler extends DefaultGroupHandler {
+
+ private final Logger log = getLogger(getClass());
+ private HashMap<PolicyGroupIdentifier, PolicyGroupIdentifier> dependentGroups = new HashMap<>();
+
+ /**
+ * Constructs policy group handler.
+ *
+ * @param deviceId device identifier
+ * @param appId application identifier
+ * @param config interface to retrieve the device properties
+ * @param linkService link service object
+ * @param flowObjService flow objective service object
+ * @param srManager segment routing manager
+ */
+ public PolicyGroupHandler(DeviceId deviceId,
+ ApplicationId appId,
+ DeviceProperties config,
+ LinkService linkService,
+ FlowObjectiveService flowObjService,
+ SegmentRoutingManager srManager) {
+ super(deviceId, appId, config, linkService, flowObjService, srManager);
+ }
+
+ /**
+ * Creates policy group chain.
+ *
+ * @param id unique identifier associated with the policy group
+ * @param params a list of policy group params
+ * @return policy group identifier
+ */
+ public PolicyGroupIdentifier createPolicyGroupChain(String id,
+ List<PolicyGroupParams> params) {
+ List<GroupBucketIdentifier> bucketIds = new ArrayList<>();
+ for (PolicyGroupParams param: params) {
+ List<PortNumber> ports = param.getPorts();
+ if (ports == null) {
+ log.warn("createPolicyGroupChain in sw {} with wrong "
+ + "input parameters", deviceId);
+ return null;
+ }
+
+ int labelStackSize = (param.getLabelStack() != null) ?
+ param.getLabelStack().size() : 0;
+
+ if (labelStackSize > 1) {
+ for (PortNumber sp : ports) {
+ PolicyGroupIdentifier previousGroupkey = null;
+ DeviceId neighbor = portDeviceMap.get(sp);
+ for (int idx = 0; idx < param.getLabelStack().size(); idx++) {
+ int label = param.getLabelStack().get(idx);
+ if (idx == (labelStackSize - 1)) {
+ // Innermost Group
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label,
+ previousGroupkey);
+ bucketIds.add(bucketId);
+ } else if (idx == 0) {
+ // Outermost Group
+ List<GroupBucket> outBuckets = new ArrayList<>();
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label, sp);
+ PolicyGroupIdentifier key = new
+ PolicyGroupIdentifier(id,
+ Collections.singletonList(param),
+ Collections.singletonList(bucketId));
+ MacAddress neighborEthDst;
+ try {
+ neighborEthDst = deviceConfig.getDeviceMac(neighbor);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Skipping createPolicyGroupChain for this label.");
+ continue;
+ }
+
+ TrafficTreatment.Builder tBuilder =
+ DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(sp)
+ .setEthDst(neighborEthDst)
+ .setEthSrc(nodeMacAddr)
+ .pushMpls()
+ .setMpls(MplsLabel.mplsLabel(label));
+ /*outBuckets.add(DefaultGroupBucket.
+ createSelectGroupBucket(tBuilder.build()));
+ GroupDescription desc = new
+ DefaultGroupDescription(deviceId,
+ GroupDescription.Type.INDIRECT,
+ new GroupBuckets(outBuckets));
+ //TODO: BoS*/
+ previousGroupkey = key;
+ //groupService.addGroup(desc);
+ //TODO: Use nextObjective APIs here
+ } else {
+ // Intermediate Groups
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label,
+ previousGroupkey);
+ PolicyGroupIdentifier key = new
+ PolicyGroupIdentifier(id,
+ Collections.singletonList(param),
+ Collections.singletonList(bucketId));
+ // Add to group dependency list
+ dependentGroups.put(previousGroupkey, key);
+ previousGroupkey = key;
+ }
+ }
+ }
+ } else {
+ int label = -1;
+ if (labelStackSize == 1) {
+ label = param.getLabelStack().get(0);
+ }
+ for (PortNumber sp : ports) {
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label, sp);
+ bucketIds.add(bucketId);
+ }
+ }
+ }
+ PolicyGroupIdentifier innermostGroupkey = null;
+ if (!bucketIds.isEmpty()) {
+ innermostGroupkey = new
+ PolicyGroupIdentifier(id,
+ params,
+ bucketIds);
+ // Add to group dependency list
+ boolean fullyResolved = true;
+ for (GroupBucketIdentifier bucketId:bucketIds) {
+ if (bucketId.type() == BucketOutputType.GROUP) {
+ dependentGroups.put(bucketId.outGroup(),
+ innermostGroupkey);
+ fullyResolved = false;
+ }
+ }
+
+ if (fullyResolved) {
+ List<GroupBucket> outBuckets = new ArrayList<>();
+ for (GroupBucketIdentifier bucketId : bucketIds) {
+ DeviceId neighbor = portDeviceMap.
+ get(bucketId.outPort());
+
+ MacAddress neighborEthDst;
+ try {
+ neighborEthDst = deviceConfig.getDeviceMac(neighbor);
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn(e.getMessage()
+ + " Skipping createPolicyGroupChain for this bucketId.");
+ continue;
+ }
+
+ TrafficTreatment.Builder tBuilder =
+ DefaultTrafficTreatment.builder();
+ tBuilder.setOutput(bucketId.outPort())
+ .setEthDst(neighborEthDst)
+ .setEthSrc(nodeMacAddr);
+ if (bucketId.label() != DestinationSet.NO_EDGE_LABEL) {
+ tBuilder.pushMpls()
+ .setMpls(MplsLabel.mplsLabel(bucketId.label()));
+ }
+ //TODO: BoS
+ /*outBuckets.add(DefaultGroupBucket.
+ createSelectGroupBucket(tBuilder.build()));*/
+ }
+ /*GroupDescription desc = new
+ DefaultGroupDescription(deviceId,
+ GroupDescription.Type.SELECT,
+ new GroupBuckets(outBuckets));
+ groupService.addGroup(desc);*/
+ //TODO: Use nextObjective APIs here
+ }
+ }
+ return innermostGroupkey;
+ }
+
+ //TODO: Use nextObjective APIs to handle the group chains
+ /*
+ @Override
+ protected void handleGroupEvent(GroupEvent event) {}
+ */
+
+ /**
+ * Generates policy group key.
+ *
+ * @param id unique identifier associated with the policy group
+ * @param params a list of policy group params
+ * @return policy group identifier
+ */
+ public PolicyGroupIdentifier generatePolicyGroupKey(String id,
+ List<PolicyGroupParams> params) {
+ List<GroupBucketIdentifier> bucketIds = new ArrayList<>();
+ for (PolicyGroupParams param: params) {
+ List<PortNumber> ports = param.getPorts();
+ if (ports == null) {
+ log.warn("generateGroupKey in sw {} with wrong "
+ + "input parameters", deviceId);
+ return null;
+ }
+
+ int labelStackSize = (param.getLabelStack() != null)
+ ? param.getLabelStack().size() : 0;
+
+ if (labelStackSize > 1) {
+ for (PortNumber sp : ports) {
+ PolicyGroupIdentifier previousGroupkey = null;
+ for (int idx = 0; idx < param.getLabelStack().size(); idx++) {
+ int label = param.getLabelStack().get(idx);
+ if (idx == (labelStackSize - 1)) {
+ // Innermost Group
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label,
+ previousGroupkey);
+ bucketIds.add(bucketId);
+ } else if (idx == 0) {
+ // Outermost Group
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label, sp);
+ PolicyGroupIdentifier key = new
+ PolicyGroupIdentifier(id,
+ Collections.singletonList(param),
+ Collections.singletonList(bucketId));
+ previousGroupkey = key;
+ } else {
+ // Intermediate Groups
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label,
+ previousGroupkey);
+ PolicyGroupIdentifier key = new
+ PolicyGroupIdentifier(id,
+ Collections.singletonList(param),
+ Collections.singletonList(bucketId));
+ previousGroupkey = key;
+ }
+ }
+ }
+ } else {
+ int label = -1;
+ if (labelStackSize == 1) {
+ label = param.getLabelStack().get(0);
+ }
+ for (PortNumber sp : ports) {
+ GroupBucketIdentifier bucketId =
+ new GroupBucketIdentifier(label, sp);
+ bucketIds.add(bucketId);
+ }
+ }
+ }
+ PolicyGroupIdentifier innermostGroupkey = null;
+ if (!bucketIds.isEmpty()) {
+ innermostGroupkey = new
+ PolicyGroupIdentifier(id,
+ params,
+ bucketIds);
+ }
+ return innermostGroupkey;
+ }
+
+ /**
+ * Removes policy group chain.
+ *
+ * @param key policy group identifier
+ */
+ public void removeGroupChain(PolicyGroupIdentifier key) {
+ checkArgument(key != null);
+ List<PolicyGroupIdentifier> groupsToBeDeleted = new ArrayList<>();
+ groupsToBeDeleted.add(key);
+
+ Iterator<PolicyGroupIdentifier> it =
+ groupsToBeDeleted.iterator();
+
+ while (it.hasNext()) {
+ PolicyGroupIdentifier innerMostGroupKey = it.next();
+ for (GroupBucketIdentifier bucketId:
+ innerMostGroupKey.bucketIds()) {
+ if (bucketId.type() != BucketOutputType.GROUP) {
+ groupsToBeDeleted.add(bucketId.outGroup());
+ }
+ }
+ /*groupService.removeGroup(deviceId,
+ getGroupKey(innerMostGroupKey),
+ appId);*/
+ //TODO: Use nextObjective APIs here
+ it.remove();
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java
new file mode 100644
index 0000000..fdc6dea
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import java.util.List;
+
+/**
+ * Representation of policy based group identifiers.
+ * Opaque to group handler applications and only the outermost
+ * policy group identifier in a chain is visible to the applications.
+ */
+public class PolicyGroupIdentifier {
+ private String id;
+ private List<PolicyGroupParams> inputParams;
+ private List<GroupBucketIdentifier> bucketIds;
+
+ /**
+ * Constructs policy group identifier.
+ *
+ * @param id unique identifier associated with the policy group
+ * @param input policy group params associated with this group
+ * @param bucketIds buckets associated with this group
+ */
+ protected PolicyGroupIdentifier(String id,
+ List<PolicyGroupParams> input,
+ List<GroupBucketIdentifier> bucketIds) {
+ this.id = id;
+ this.inputParams = input;
+ this.bucketIds = bucketIds;
+ }
+
+ /**
+ * Returns the bucket identifier list associated with the policy
+ * group identifier.
+ *
+ * @return list of bucket identifier
+ */
+ protected List<GroupBucketIdentifier> bucketIds() {
+ return this.bucketIds;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ int combinedHash = 0;
+ for (PolicyGroupParams input:inputParams) {
+ combinedHash = combinedHash + input.hashCode();
+ }
+ for (GroupBucketIdentifier bucketId:bucketIds) {
+ combinedHash = combinedHash + bucketId.hashCode();
+ }
+ result = 31 * result + combinedHash;
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj instanceof PolicyGroupIdentifier) {
+ PolicyGroupIdentifier that = (PolicyGroupIdentifier) obj;
+ boolean result = this.id.equals(that.id);
+ result = result &&
+ this.inputParams.containsAll(that.inputParams) &&
+ that.inputParams.containsAll(this.inputParams);
+ result = result &&
+ this.bucketIds.containsAll(that.bucketIds) &&
+ that.bucketIds.containsAll(this.bucketIds);
+ return result;
+ }
+
+ return false;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java
new file mode 100644
index 0000000..5baf849
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015-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.grouphandler;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.onosproject.net.PortNumber;
+
+/**
+ * Representation of parameters used to create policy based groups.
+ */
+public class PolicyGroupParams {
+ private final List<PortNumber> ports;
+ private final List<Integer> labelStack;
+
+ /**
+ * Constructor.
+ *
+ * @param labelStack mpls label stack to be applied on the ports
+ * @param ports ports to be part of the policy group
+ */
+ public PolicyGroupParams(List<Integer> labelStack,
+ List<PortNumber> ports) {
+ this.ports = checkNotNull(ports);
+ this.labelStack = checkNotNull(labelStack);
+ }
+
+ /**
+ * Returns the ports associated with the policy group params.
+ *
+ * @return list of port numbers
+ */
+ public List<PortNumber> getPorts() {
+ return ports;
+ }
+
+ /**
+ * Returns the label stack associated with the policy group params.
+ *
+ * @return list of integers
+ */
+ public List<Integer> getLabelStack() {
+ return labelStack;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ int combinedHash = 0;
+ for (PortNumber port:ports) {
+ combinedHash = combinedHash + port.hashCode();
+ }
+ combinedHash = combinedHash + Objects.hash(labelStack);
+ result = 31 * result + combinedHash;
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj instanceof PolicyGroupParams) {
+ PolicyGroupParams that = (PolicyGroupParams) obj;
+ boolean result = this.labelStack.equals(that.labelStack);
+ result = result &&
+ this.ports.containsAll(that.ports) &&
+ that.ports.containsAll(this.ports);
+ return result;
+ }
+
+ return false;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java
new file mode 100644
index 0000000..3671b03
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.grouphandler;
+
+import org.onosproject.net.DeviceId;
+
+/**
+ * Extends its super class modifying its internal behavior.
+ * Pick a neighbor will pick a random neighbor.
+ */
+public class RandomDestinationSet extends DestinationSet {
+
+ public RandomDestinationSet(DeviceId dstSw) {
+ super(true, dstSw);
+ }
+
+ public RandomDestinationSet(int edgeLabel,
+ DeviceId dstSw) {
+ super(true, edgeLabel, dstSw);
+ }
+
+ public RandomDestinationSet() {
+ super();
+ }
+
+ // XXX revisit the need for this class since neighbors no longer stored here
+ // will be handled when we fix pseudowires for dual-Tor scenarios
+
+
+ /*@Override
+ public DeviceId getFirstNeighbor() {
+ if (getDeviceIds().isEmpty()) {
+ return DeviceId.NONE;
+ }
+ int size = getDeviceIds().size();
+ int index = RandomUtils.nextInt(0, size);
+ return Iterables.get(getDeviceIds(), index);
+ }*/
+
+ @Override
+ public String toString() {
+ return " RandomNeighborset Sw: " //+ getDeviceIds()
+ + " and Label: " //+ getEdgeLabel()
+ + " for destination: "; // + getDestinationSw();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java
new file mode 100644
index 0000000..d99f1d1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing group handling.
+ */
+package org.onosproject.segmentrouting.grouphandler;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java
new file mode 100644
index 0000000..137ba3c
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing application components.
+ */
+package org.onosproject.segmentrouting;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java
new file mode 100644
index 0000000..417a795
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.Link;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the default l2 tunnel.
+ */
+public class DefaultL2Tunnel implements L2Tunnel {
+
+ /**
+ * Mode of the pseudo wire.
+ */
+ private L2Mode pwMode;
+ /**
+ * Service delimiting tag.
+ */
+ private VlanId sdTag;
+ /**
+ * Tunnel id.
+ */
+ private long tunnelId;
+ /**
+ * Pseudo wire label.
+ */
+ private MplsLabel pwLabel;
+ /**
+ * Inter-CO label.
+ */
+ private MplsLabel interCoLabel;
+
+ private List<Link> pathUsed;
+
+ /**
+ * Vlan which will be used for the encapsualted
+ * vlan traffic.
+ */
+ private VlanId transportVlan;
+
+ /**
+ * Creates a inter-co l2 tunnel using the
+ * supplied parameters.
+ *
+ * @param mode the tunnel mode
+ * @param sdtag the service delimiting tag
+ * @param tunnelId the tunnel id
+ * @param pwLabel the pseudo wire label
+ * @param interCoLabel the inter central office label
+ */
+ public DefaultL2Tunnel(L2Mode mode, VlanId sdtag, long tunnelId, MplsLabel pwLabel, MplsLabel interCoLabel) {
+ checkNotNull(mode);
+ checkArgument(tunnelId > 0);
+ checkNotNull(pwLabel);
+ checkNotNull(interCoLabel);
+
+ this.pwMode = mode;
+ this.sdTag = sdtag;
+ this.tunnelId = tunnelId;
+ this.pwLabel = pwLabel;
+ this.interCoLabel = interCoLabel;
+ }
+
+ /**
+ * Creates a l2Tunnel from a given tunnel.
+ *
+ * @param l2Tunnel to replicate
+ */
+ public DefaultL2Tunnel(DefaultL2Tunnel l2Tunnel) {
+
+ this.pwMode = l2Tunnel.pwMode();
+ this.sdTag = l2Tunnel.sdTag();
+ this.tunnelId = l2Tunnel.tunnelId();
+ this.pwLabel = l2Tunnel.pwLabel();
+ this.interCoLabel = l2Tunnel.interCoLabel();
+ this.pathUsed = l2Tunnel.pathUsed();
+ this.transportVlan = l2Tunnel.transportVlan;
+ }
+
+ /**
+ * Creates a intra-co l2 tunnel using the
+ * supplied parameters.
+ *
+ * @param mode the tunnel mode
+ * @param sdtag the service delimiting tag
+ * @param tunnelId the tunnel id
+ * @param pwLabel the pseudo wire label
+ */
+ public DefaultL2Tunnel(L2Mode mode, VlanId sdtag, long tunnelId, MplsLabel pwLabel) {
+ this(mode, sdtag, tunnelId, pwLabel, MplsLabel.mplsLabel(MplsLabel.MAX_MPLS));
+ }
+
+
+ /**
+ * Creates an empty l2 tunnel.
+ **/
+ public DefaultL2Tunnel() {
+ this.pwMode = null;
+ this.sdTag = null;
+ this.tunnelId = 0;
+ this.pwLabel = null;
+ this.interCoLabel = null;
+ }
+
+ /**
+ * Returns the mode of the pseudo wire.
+ *
+ * @return the pseudo wire mode
+ */
+ @Override
+ public L2Mode pwMode() {
+ return pwMode;
+ }
+
+ /**
+ * Returns the service delimitation
+ * tag.
+ *
+ * @return the service delimitation vlan id
+ */
+ @Override
+ public VlanId sdTag() {
+ return sdTag;
+ }
+
+ /**
+ * Returns the tunnel id of the pseudo wire.
+ *
+ * @return the pseudo wire tunnel id
+ */
+ @Override
+ public long tunnelId() {
+ return tunnelId;
+ }
+
+ /**
+ * Returns the pw label.
+ *
+ * @return the mpls pw label
+ */
+ @Override
+ public MplsLabel pwLabel() {
+ return pwLabel;
+ }
+
+ /**
+ * Set the path for the pseudowire.
+ *
+ * @param path The path to set
+ */
+ @Override
+ public void setPath(List<Link> path) {
+ pathUsed = new ArrayList<>(path);
+ }
+
+ /**
+ * Set the transport vlan for the pseudowire.
+ *
+ * @param vlan the vlan to use.
+ */
+ @Override
+ public void setTransportVlan(VlanId vlan) {
+ transportVlan = vlan;
+ }
+
+ /**
+ * Returns the used path of the pseudowire.
+ *
+ * @return pathUsed
+ */
+ @Override
+ public List<Link> pathUsed() {
+ return pathUsed;
+ }
+
+ @Override
+ public VlanId transportVlan() {
+ return transportVlan;
+ }
+
+
+ /**
+ * Returns the inter-co label.
+ *
+ * @return the mpls inter-co label
+ */
+ @Override
+ public MplsLabel interCoLabel() {
+ return interCoLabel;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.tunnelId, this.pwMode, this.sdTag, this.pwLabel, this.interCoLabel);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o instanceof DefaultL2Tunnel) {
+ DefaultL2Tunnel that = (DefaultL2Tunnel) o;
+ return this.tunnelId == that.tunnelId &&
+ this.pwMode.equals(that.pwMode) &&
+ this.sdTag.equals(that.sdTag) &&
+ this.pwLabel.equals(that.pwLabel) &&
+ this.interCoLabel.equals(that.interCoLabel);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("pwMode", pwMode())
+ .add("sdTag", sdTag())
+ .add("tunnelId", tunnelId())
+ .add("pwLabel", pwLabel())
+ .add("interCoLabel", interCoLabel())
+ .add("transportVlan", transportVlan())
+ .toString();
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java
new file mode 100644
index 0000000..7a47972
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import com.google.common.base.MoreObjects;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Helper class to carry the l2 tunnel
+ * and its policy.
+ */
+public class DefaultL2TunnelDescription implements L2TunnelDescription {
+
+ /**
+ * The l2 tunnel.
+ */
+ private L2Tunnel l2Tunnel;
+
+ /**
+ * The l2 tunnel policy.
+ */
+ private L2TunnelPolicy l2TunnelPolicy;
+
+ /**
+ * Creates a l2 tunnel description using the given info.
+ *
+ * @param l2Tunnel the l2 tunnel
+ * @param l2TunnelPolicy the l2 tunnel description
+ */
+ public DefaultL2TunnelDescription(L2Tunnel l2Tunnel,
+ L2TunnelPolicy l2TunnelPolicy) {
+ checkNotNull(l2Tunnel);
+ checkNotNull(l2TunnelPolicy);
+
+ this.l2Tunnel = l2Tunnel;
+ this.l2TunnelPolicy = l2TunnelPolicy;
+ }
+
+ /**
+ * Creates an empty l2 tunnel description.
+ */
+ public DefaultL2TunnelDescription() {
+ this.l2Tunnel = null;
+ this.l2TunnelPolicy = null;
+ }
+
+ @Override
+ public L2Tunnel l2Tunnel() {
+ return l2Tunnel;
+ }
+
+ @Override
+ public L2TunnelPolicy l2TunnelPolicy() {
+ return l2TunnelPolicy;
+ }
+
+ @Override
+ public void setL2Tunnel(L2Tunnel tunnel) {
+ l2Tunnel = tunnel;
+ }
+
+ @Override
+ public void setL2TunnelPolicy(L2TunnelPolicy policy) {
+ l2TunnelPolicy = policy;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.l2Tunnel, this.l2TunnelPolicy);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o instanceof DefaultL2TunnelDescription) {
+ DefaultL2TunnelDescription that = (DefaultL2TunnelDescription) o;
+ // Equality is based on tunnel id and pw label
+ // which is always the last label.
+ return this.l2Tunnel.equals(that.l2Tunnel) &&
+ this.l2TunnelPolicy.equals(that.l2TunnelPolicy);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("l2Tunnel", l2Tunnel())
+ .add("l2TunnelPolicy", l2TunnelPolicy())
+ .toString();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java
new file mode 100644
index 0000000..2dc9de2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java
@@ -0,0 +1,1733 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.RandomUtils;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+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.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.SegmentRoutingManager;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.DistributedSet;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.Versioned;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.onosproject.net.flowobjective.ForwardingObjective.Flag.VERSATILE;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Pipeline.INITIATION;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Pipeline.TERMINATION;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Result.*;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Direction.FWD;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Direction.REV;
+
+/**
+ * Handles pwaas related events.
+ */
+public class DefaultL2TunnelHandler implements L2TunnelHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultL2TunnelHandler.class);
+
+ private final SegmentRoutingManager srManager;
+ /**
+ * To store the next objectives related to the initiation.
+ */
+ private final ConsistentMap<String, NextObjective> l2InitiationNextObjStore;
+ /**
+ * To store the next objectives related to the termination.
+ */
+ private final ConsistentMap<String, NextObjective> l2TerminationNextObjStore;
+
+ /**
+ * To store policies.
+ */
+ private final ConsistentMap<String, L2TunnelPolicy> l2PolicyStore;
+
+ /**
+ * To store tunnels.
+ */
+ private final ConsistentMap<String, L2Tunnel> l2TunnelStore;
+
+ private final KryoNamespace.Builder l2TunnelKryo;
+
+ /**
+ * Contains transport vlans used for spine-leaf pseudowires.
+ */
+ private final DistributedSet<VlanId> vlanStore;
+
+ /**
+ * Used for determining transport vlans for leaf-spine.
+ */
+ private short transportVlanUpper = 4093, transportVlanLower = 3500;
+
+ private static final VlanId UNTAGGED_TRANSPORT_VLAN = VlanId.vlanId((short) 4094);
+
+ /**
+ * Create a l2 tunnel handler for the deploy and
+ * for the tear down of pseudo wires.
+ *
+ * @param segmentRoutingManager the segment routing manager
+ */
+ public DefaultL2TunnelHandler(SegmentRoutingManager segmentRoutingManager) {
+ srManager = segmentRoutingManager;
+ l2TunnelKryo = new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .register(L2Tunnel.class,
+ L2TunnelPolicy.class,
+ DefaultL2Tunnel.class,
+ DefaultL2TunnelPolicy.class,
+ L2Mode.class,
+ MplsLabel.class,
+ VlanId.class,
+ ConnectPoint.class);
+
+ l2InitiationNextObjStore = srManager.
+ storageService.
+ <String, NextObjective>consistentMapBuilder().
+ withName("onos-l2initiation-nextobj-store").
+ withSerializer(Serializer.using(l2TunnelKryo.build())).
+ build();
+
+ l2TerminationNextObjStore = srManager.storageService.
+ <String, NextObjective>consistentMapBuilder()
+ .withName("onos-l2termination-nextobj-store")
+ .withSerializer(Serializer.using(l2TunnelKryo.build()))
+ .build();
+
+ l2PolicyStore = srManager.storageService
+ .<String, L2TunnelPolicy>consistentMapBuilder()
+ .withName("onos-l2-policy-store")
+ .withSerializer(Serializer.using(l2TunnelKryo.build()))
+ .build();
+
+ l2TunnelStore = srManager.storageService
+ .<String, L2Tunnel>consistentMapBuilder()
+ .withName("onos-l2-tunnel-store")
+ .withSerializer(Serializer.using(l2TunnelKryo.build()))
+ .build();
+
+ vlanStore = srManager.storageService.<VlanId>setBuilder()
+ .withName("onos-transport-vlan-store")
+ .withSerializer(Serializer.using(
+ new KryoNamespace.Builder()
+ .register(KryoNamespaces.API)
+ .build()))
+ .build()
+ .asDistributedSet();
+ }
+
+ /**
+ * Deploys any pre-existing pseudowires in the configuration.
+ * Used by manager only in initialization.
+ */
+ @Override
+ public void init() {
+
+ PwaasConfig config = srManager.cfgService.getConfig(srManager.appId(), PwaasConfig.class);
+ if (config == null) {
+ return;
+ }
+
+ log.info("Deploying existing pseudowires");
+
+ // gather pseudowires
+ Set<L2TunnelDescription> pwToAdd = config
+ .getPwIds()
+ .stream()
+ .map(config::getPwDescription)
+ .collect(Collectors.toSet());
+
+ // deploy pseudowires
+ deploy(pwToAdd);
+ }
+
+ /**
+ * Returns all L2 Policies.
+ *
+ * @return List of policies
+ */
+ @Override
+ public List<L2TunnelPolicy> getL2Policies() {
+
+ return new ArrayList<>(l2PolicyStore
+ .values()
+ .stream()
+ .map(Versioned::value)
+ .collect(Collectors.toList()));
+
+ }
+
+ /**
+ * Returns all L2 Tunnels.
+ *
+ * @return List of tunnels.
+ */
+ @Override
+ public List<L2Tunnel> getL2Tunnels() {
+
+ return new ArrayList<>(l2TunnelStore
+ .values()
+ .stream()
+ .map(Versioned::value)
+ .collect(Collectors.toList()));
+
+ }
+
+ @Override
+ public void processLinkDown(Link link) {
+
+ List<L2Tunnel> tunnels = getL2Tunnels();
+ List<L2TunnelPolicy> policies = getL2Policies();
+
+ // determine affected pseudowires and update them at once
+ Set<L2TunnelDescription> pwToUpdate = tunnels
+ .stream()
+ .filter(tun -> tun.pathUsed().contains(link))
+ .map(l2Tunnel -> {
+ L2TunnelPolicy policy = null;
+ for (L2TunnelPolicy l2Policy : policies) {
+ if (l2Policy.tunnelId() == l2Tunnel.tunnelId()) {
+ policy = l2Policy;
+ break;
+ }
+ }
+
+ return new DefaultL2TunnelDescription(l2Tunnel, policy);
+ })
+ .collect(Collectors.toSet());
+
+
+ log.info("Pseudowires affected by link failure : {}, rerouting them...", pwToUpdate);
+
+ // update all pseudowires
+ pwToUpdate.forEach(tun -> updatePw(tun, tun));
+ }
+
+ @Override
+ public void processPwaasConfigAdded(NetworkConfigEvent event) {
+ checkArgument(event.config().isPresent(),
+ "Config is not presented in PwaasConfigAdded event {}", event);
+
+ log.info("Network event : Pseudowire configuration added!");
+ PwaasConfig config = (PwaasConfig) event.config().get();
+
+ // gather pseudowires
+ Set<L2TunnelDescription> pwToAdd = config
+ .getPwIds()
+ .stream()
+ .map(config::getPwDescription)
+ .collect(Collectors.toSet());
+
+ // deploy pseudowires
+ deploy(pwToAdd);
+ }
+
+ /**
+ * Returns the new vlan id for an ingress point of a
+ * pseudowire. For double tagged, it is the outer,
+ * For single tagged it is the single tag, and for
+ * inner it is None.
+ *
+ * @param ingressOuter vlanid of ingress outer
+ * @param ingressInner vlanid of ingress inner
+ * @param egressOuter vlanid of egress outer
+ * @param egressInner vlanid of egress inner
+ * @return returns the vlan id which will be installed at vlan table 1.
+ */
+ private VlanId determineEgressVlan(VlanId ingressOuter, VlanId ingressInner,
+ VlanId egressOuter, VlanId egressInner) {
+
+ // validity of vlan combinations was checked at verifyPseudowire
+ if (!(ingressOuter.equals(VlanId.NONE))) {
+ return egressOuter;
+ } else if (!(ingressInner.equals(VlanId.NONE))) {
+ return egressInner;
+ } else {
+ return VlanId.vlanId("None");
+ }
+ }
+
+ /**
+ * Determines vlan used for transporting the pw traffic.
+ *
+ * Leaf-Leaf traffic is transferred untagged, thus we choose the UNTAGGED_TRANSPORT_VLAN
+ * and also make sure to add the popVlan instruction.
+ * For spine-leaf pws we choose the highest vlan value available from a certain range.
+ *
+ * @param spinePw if the pw is leaf-spine.
+ * @return The vlan id chossen to transport this pseudowire. If vlan is UNTAGGED_TRANSPORT_VLAN
+ * then the pw is transported untagged.
+ */
+ private VlanId determineTransportVlan(boolean spinePw) {
+
+ if (!spinePw) {
+
+ log.info("Untagged transport with internal vlan {} for pseudowire!", UNTAGGED_TRANSPORT_VLAN);
+ return UNTAGGED_TRANSPORT_VLAN;
+ } else {
+ for (short i = transportVlanUpper; i > transportVlanLower; i--) {
+
+ VlanId vlanToUse = VlanId.vlanId((short) i);
+ if (!vlanStore.contains(vlanToUse)) {
+
+ vlanStore.add(vlanToUse);
+ log.info("Transport vlan {} for pseudowire!", vlanToUse);
+ return vlanToUse;
+ }
+ }
+
+ log.info("No available transport vlan found, pseudowire traffic will be carried untagged " +
+ "with internal vlan {}!", UNTAGGED_TRANSPORT_VLAN);
+ return UNTAGGED_TRANSPORT_VLAN;
+ }
+ }
+
+ /**
+ * Adds a single pseudowire from leaf to a leaf.
+ * This method can be called from cli commands
+ * without configuration updates, thus it does not check for mastership
+ * of the ingress pseudowire device.
+ *
+ * @param pw The pseudowire
+ * @param spinePw True if pseudowire is from leaf to spine
+ * @return result of pseudowire deployment
+ */
+ private Result deployPseudowire(L2TunnelDescription pw, boolean spinePw) {
+
+ Result result;
+ long l2TunnelId;
+
+ l2TunnelId = pw.l2Tunnel().tunnelId();
+
+ // The tunnel id cannot be 0.
+ if (l2TunnelId == 0) {
+ log.warn("Tunnel id id must be > 0");
+ return Result.ADDITION_ERROR;
+ }
+
+ // get path here, need to use the same for fwd and rev direction
+ List<Link> path = getPath(pw.l2TunnelPolicy().cP1(),
+ pw.l2TunnelPolicy().cP2());
+ if (path == null) {
+ log.info("Deploying process : No path between the connection points for pseudowire {}", l2TunnelId);
+ return WRONG_PARAMETERS;
+ }
+
+ Link fwdNextHop;
+ Link revNextHop;
+ if (!spinePw) {
+ if (path.size() != 2) {
+ log.info("Deploying process : Path between two leafs should have size of 2, for pseudowire {}",
+ l2TunnelId);
+ return INTERNAL_ERROR;
+ }
+
+ fwdNextHop = path.get(0);
+ revNextHop = reverseLink(path.get(1));
+ } else {
+ if (path.size() != 1) {
+ log.info("Deploying process : Path between leaf spine should equal to 1, for pseudowire {}",
+ l2TunnelId);
+ return INTERNAL_ERROR;
+ }
+
+ fwdNextHop = path.get(0);
+ revNextHop = reverseLink(path.get(0));
+ }
+
+ pw.l2Tunnel().setPath(path);
+ pw.l2Tunnel().setTransportVlan(determineTransportVlan(spinePw));
+
+ // next hops for next objectives
+
+ log.info("Deploying process : Establishing forward direction for pseudowire {}", l2TunnelId);
+
+ VlanId egressVlan = determineEgressVlan(pw.l2TunnelPolicy().cP1OuterTag(),
+ pw.l2TunnelPolicy().cP1InnerTag(),
+ pw.l2TunnelPolicy().cP2OuterTag(),
+ pw.l2TunnelPolicy().cP2InnerTag());
+ // We establish the tunnel.
+ // result.nextId will be used in fwd
+ result = deployPseudoWireInit(pw.l2Tunnel(),
+ pw.l2TunnelPolicy().cP1(),
+ pw.l2TunnelPolicy().cP2(),
+ FWD,
+ fwdNextHop,
+ spinePw,
+ egressVlan);
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying pseudowire initiation for CP1");
+ return Result.ADDITION_ERROR;
+ }
+
+ // We create the policy.
+ result = deployPolicy(l2TunnelId,
+ pw.l2TunnelPolicy().cP1(),
+ pw.l2TunnelPolicy().cP1InnerTag(),
+ pw.l2TunnelPolicy().cP1OuterTag(),
+ egressVlan,
+ result.nextId);
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying pseudowire policy for CP1");
+ return Result.ADDITION_ERROR;
+ }
+
+ // We terminate the tunnel
+ result = deployPseudoWireTerm(pw.l2Tunnel(),
+ pw.l2TunnelPolicy().cP2(),
+ egressVlan,
+ FWD,
+ spinePw);
+
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying pseudowire termination for CP1");
+ return Result.ADDITION_ERROR;
+
+ }
+
+ log.info("Deploying process : Establishing reverse direction for pseudowire {}", l2TunnelId);
+
+ egressVlan = determineEgressVlan(pw.l2TunnelPolicy().cP2OuterTag(),
+ pw.l2TunnelPolicy().cP2InnerTag(),
+ pw.l2TunnelPolicy().cP1OuterTag(),
+ pw.l2TunnelPolicy().cP1InnerTag());
+
+ // We establish the reverse tunnel.
+ result = deployPseudoWireInit(pw.l2Tunnel(),
+ pw.l2TunnelPolicy().cP2(),
+ pw.l2TunnelPolicy().cP1(),
+ REV,
+ revNextHop,
+ spinePw,
+ egressVlan);
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying pseudowire initiation for CP2");
+ return Result.ADDITION_ERROR;
+ }
+
+
+ result = deployPolicy(l2TunnelId,
+ pw.l2TunnelPolicy().cP2(),
+ pw.l2TunnelPolicy().cP2InnerTag(),
+ pw.l2TunnelPolicy().cP2OuterTag(),
+ egressVlan,
+ result.nextId);
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying policy for CP2");
+ return Result.ADDITION_ERROR;
+ }
+
+ result = deployPseudoWireTerm(pw.l2Tunnel(),
+ pw.l2TunnelPolicy().cP1(),
+ egressVlan,
+ REV,
+ spinePw);
+
+ if (result != SUCCESS) {
+ log.info("Deploying process : Error in deploying pseudowire termination for CP2");
+ return Result.ADDITION_ERROR;
+ }
+
+ log.info("Deploying process : Updating relevant information for pseudowire {}", l2TunnelId);
+
+ // Populate stores
+ l2TunnelStore.put(Long.toString(l2TunnelId), pw.l2Tunnel());
+ l2PolicyStore.put(Long.toString(l2TunnelId), pw.l2TunnelPolicy());
+
+ return Result.SUCCESS;
+ }
+
+ /**
+ * To deploy a number of pseudo wires.
+ * <p>
+ * Called ONLY when configuration changes, thus the check
+ * for the mastership of the device.
+ * <p>
+ * Only the master of CP1 will deploy this pseudowire.
+ *
+ * @param pwToAdd the set of pseudo wires to add
+ */
+ private void deploy(Set<L2TunnelDescription> pwToAdd) {
+
+ Result result;
+
+ for (L2TunnelDescription currentL2Tunnel : pwToAdd) {
+ ConnectPoint cp1 = currentL2Tunnel.l2TunnelPolicy().cP1();
+ ConnectPoint cp2 = currentL2Tunnel.l2TunnelPolicy().cP2();
+ long tunnelId = currentL2Tunnel.l2TunnelPolicy().tunnelId();
+
+ // only the master of CP1 will program this pseudowire
+ if (!srManager.isMasterOf(cp1)) {
+ log.debug("Not the master of {}. Ignore pseudo wire deployment id={}", cp1, tunnelId);
+ continue;
+ }
+
+ try {
+ // differentiate between leaf-leaf pseudowires and leaf-spine
+ // and pass the appropriate flag in them.
+ if (!srManager.deviceConfiguration().isEdgeDevice(cp1.deviceId()) &&
+ !srManager.deviceConfiguration().isEdgeDevice(cp2.deviceId())) {
+ log.warn("Can not deploy pseudowire from spine to spine!");
+ result = Result.INTERNAL_ERROR;
+ } else if (srManager.deviceConfiguration().isEdgeDevice(cp1.deviceId()) &&
+ srManager.deviceConfiguration().isEdgeDevice(cp2.deviceId())) {
+ log.info("Deploying a leaf-leaf pseudowire {}", tunnelId);
+ result = deployPseudowire(currentL2Tunnel, false);
+ } else {
+ log.info("Deploying a leaf-spine pseudowire {}", tunnelId);
+ result = deployPseudowire(currentL2Tunnel, true);
+ }
+ } catch (DeviceConfigNotFoundException e) {
+ log.error("Exception caught when deploying pseudowire", e.toString());
+ result = Result.INTERNAL_ERROR;
+ }
+
+ switch (result) {
+ case INTERNAL_ERROR:
+ log.warn("Could not deploy pseudowire {}, internal error!", tunnelId);
+ break;
+ case WRONG_PARAMETERS:
+ log.warn("Could not deploy pseudowire {}, wrong parameters!", tunnelId);
+ break;
+ case ADDITION_ERROR:
+ log.warn("Could not deploy pseudowire {}, error in populating rules!", tunnelId);
+ break;
+ default:
+ log.info("Pseudowire with {} succesfully deployed!", tunnelId);
+ break;
+ }
+ }
+ }
+
+
+ @Override
+ public void processPwaasConfigUpdated(NetworkConfigEvent event) {
+ checkArgument(event.config().isPresent(),
+ "Config is not presented in PwaasConfigUpdated event {}", event);
+ checkArgument(event.prevConfig().isPresent(),
+ "PrevConfig is not presented in PwaasConfigUpdated event {}", event);
+
+ log.info("Pseudowire configuration updated.");
+
+ // We retrieve the old pseudo wires.
+ PwaasConfig prevConfig = (PwaasConfig) event.prevConfig().get();
+ Set<Long> prevPws = prevConfig.getPwIds();
+
+ // We retrieve the new pseudo wires.
+ PwaasConfig config = (PwaasConfig) event.config().get();
+ Set<Long> newPws = config.getPwIds();
+
+ // We compute the pseudo wires to update.
+ Set<Long> updPws = newPws.stream()
+ .filter(tunnelId -> prevPws.contains(tunnelId)
+ && !config.getPwDescription(tunnelId).equals(prevConfig.getPwDescription(tunnelId)))
+ .collect(Collectors.toSet());
+
+ // The pseudo wires to remove.
+ Set<Long> rmvPWs = prevPws.stream()
+ .filter(tunnelId -> !newPws.contains(tunnelId)).collect(Collectors.toSet());
+
+ Set<L2TunnelDescription> pwToRemove = rmvPWs.stream()
+ .map(prevConfig::getPwDescription)
+ .collect(Collectors.toSet());
+ tearDown(pwToRemove);
+
+ // The pseudo wires to add.
+ Set<Long> addedPWs = newPws.stream()
+ .filter(tunnelId -> !prevPws.contains(tunnelId))
+ .collect(Collectors.toSet());
+ Set<L2TunnelDescription> pwToAdd = addedPWs.stream()
+ .map(config::getPwDescription)
+ .collect(Collectors.toSet());
+ deploy(pwToAdd);
+
+
+ // The pseudo wires to update.
+ updPws.forEach(tunnelId -> updatePw(prevConfig.getPwDescription(tunnelId),
+ config.getPwDescription(tunnelId)));
+
+ log.info("Pseudowires removed : {}, Pseudowires updated : {}, Pseudowires added : {}", rmvPWs,
+ updPws, addedPWs);
+ }
+
+ /**
+ * Helper function to update a pw.
+ * <p>
+ * Called upon configuration changes that update existing pseudowires and
+ * when links fail. Checking of mastership for CP1 is mandatory because it is
+ * called in multiple instances for both cases.
+ * <p>
+ * Meant to call asynchronously for various events, thus this call can not block and need
+ * to perform asynchronous operations.
+ * <p>
+ * For this reason error checking is omitted.
+ *
+ * @param oldPw the pseudo wire to remove
+ * @param newPw the pseudo wire to add
+ */
+ private void updatePw(L2TunnelDescription oldPw,
+ L2TunnelDescription newPw) {
+ ConnectPoint oldCp1 = oldPw.l2TunnelPolicy().cP1();
+ long tunnelId = oldPw.l2Tunnel().tunnelId();
+
+ // only the master of CP1 will update this pseudowire
+ if (!srManager.isMasterOf(oldPw.l2TunnelPolicy().cP1())) {
+ log.debug("Not the master of {}. Ignore pseudo wire update id={}", oldCp1, tunnelId);
+ return;
+ }
+ // only determine if the new pseudowire is leaf-spine, because
+ // removal process is the same for both leaf-leaf and leaf-spine pws
+ boolean newPwSpine;
+ try {
+ newPwSpine = !srManager.deviceConfiguration().isEdgeDevice(newPw.l2TunnelPolicy().cP1().deviceId()) ||
+ !srManager.deviceConfiguration().isEdgeDevice(newPw.l2TunnelPolicy().cP2().deviceId());
+ } catch (DeviceConfigNotFoundException e) {
+ // if exception is caught treat the new pw as leaf-leaf
+ newPwSpine = false;
+ }
+
+ // copy the variable here because we need to use it in lambda thus it needs to be final
+ boolean finalNewPwSpine = newPwSpine;
+
+ log.info("Updating pseudowire {}", oldPw.l2Tunnel().tunnelId());
+
+ // The async tasks to orchestrate the next and forwarding update
+ CompletableFuture<ObjectiveError> fwdInitNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> revInitNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> fwdTermNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> revTermNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> fwdPwFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> revPwFuture = new CompletableFuture<>();
+
+ // first delete all information from our stores, we can not do it asynchronously
+ l2PolicyStore.remove(Long.toString(tunnelId));
+
+ // grab the old l2 tunnel from the store, since it carries information which is not exposed
+ // to the user configuration and set it to oldPw.
+ oldPw.setL2Tunnel(l2TunnelStore.get(Long.toString(tunnelId)).value());
+ VlanId transportVlan = l2TunnelStore.get(Long.toString(tunnelId)).value().transportVlan();
+ l2TunnelStore.remove(Long.toString(tunnelId));
+
+ // remove the reserved transport vlan, if one is used
+ if (!transportVlan.equals(UNTAGGED_TRANSPORT_VLAN)) {
+ vlanStore.remove(transportVlan);
+ }
+
+ // First we remove both policy.
+ log.debug("Start deleting fwd policy for {}", tunnelId);
+ VlanId egressVlan = determineEgressVlan(oldPw.l2TunnelPolicy().cP1OuterTag(),
+ oldPw.l2TunnelPolicy().cP1InnerTag(),
+ oldPw.l2TunnelPolicy().cP2OuterTag(),
+ oldPw.l2TunnelPolicy().cP2InnerTag());
+ deletePolicy(tunnelId, oldPw.l2TunnelPolicy().cP1(),
+ oldPw.l2TunnelPolicy().cP1InnerTag(),
+ oldPw.l2TunnelPolicy().cP1OuterTag(),
+ egressVlan,
+ fwdInitNextFuture,
+ FWD);
+
+ deletePolicy(tunnelId, oldPw.l2TunnelPolicy().cP2(),
+ oldPw.l2TunnelPolicy().cP2InnerTag(),
+ oldPw.l2TunnelPolicy().cP2OuterTag(),
+ egressVlan, revInitNextFuture,
+ REV);
+
+ // Finally we remove both the tunnels.
+ fwdInitNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ log.debug("Update process : Fwd policy removed. " +
+ "Now remove fwd {} for {}", INITIATION, tunnelId);
+ tearDownPseudoWireInit(tunnelId, oldPw.l2TunnelPolicy().cP1(), fwdTermNextFuture, FWD);
+ }
+ });
+ revInitNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ log.debug("Update process : Rev policy removed. " +
+ "Now remove rev {} for {}", INITIATION, tunnelId);
+ tearDownPseudoWireInit(tunnelId, oldPw.l2TunnelPolicy().cP2(), revTermNextFuture, REV);
+ }
+ });
+ fwdTermNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ log.debug("Update process : Fwd {} removed. " +
+ "Now remove fwd {} for {}", INITIATION, TERMINATION, tunnelId);
+ tearDownPseudoWireTerm(oldPw.l2Tunnel(), oldPw.l2TunnelPolicy().cP2(), fwdPwFuture, FWD);
+ }
+ });
+ revTermNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ log.debug("Update process : Rev {} removed. " +
+ "Now remove rev {} for {}", INITIATION, TERMINATION, tunnelId);
+ tearDownPseudoWireTerm(oldPw.l2Tunnel(), oldPw.l2TunnelPolicy().cP1(), revPwFuture, REV);
+ }
+ });
+
+ // get path here, need to use the same for fwd and rev direction
+ List<Link> path = getPath(newPw.l2TunnelPolicy().cP1(),
+ newPw.l2TunnelPolicy().cP2());
+ if (path == null) {
+ log.error("Update process : " +
+ "No path between the connection points for pseudowire {}", newPw.l2Tunnel().tunnelId());
+ return;
+ }
+
+ Link fwdNextHop, revNextHop;
+ if (!finalNewPwSpine) {
+ if (path.size() != 2) {
+ log.error("Update process : Error, path between two leafs should have size of 2, for pseudowire {}",
+ newPw.l2Tunnel().tunnelId());
+ return;
+ }
+ fwdNextHop = path.get(0);
+ revNextHop = reverseLink(path.get(1));
+ } else {
+ if (path.size() != 1) {
+ log.error("Update process : Error, path between leaf spine should equal to 1, for pseudowire {}",
+ newPw.l2Tunnel().tunnelId());
+ return;
+ }
+ fwdNextHop = path.get(0);
+ revNextHop = reverseLink(path.get(0));
+ }
+
+ // set new path and transport vlan.
+ newPw.l2Tunnel().setPath(path);
+ newPw.l2Tunnel().setTransportVlan(determineTransportVlan(newPwSpine));
+
+ // At the end we install the updated PW.
+ fwdPwFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+
+ // Upgrade stores and book keeping information, need to move this here
+ // cause this call is asynchronous.
+ l2PolicyStore.put(Long.toString(tunnelId), newPw.l2TunnelPolicy());
+ l2TunnelStore.put(Long.toString(tunnelId), newPw.l2Tunnel());
+
+ VlanId egressVlanId = determineEgressVlan(newPw.l2TunnelPolicy().cP1OuterTag(),
+ newPw.l2TunnelPolicy().cP1InnerTag(),
+ newPw.l2TunnelPolicy().cP2OuterTag(),
+ newPw.l2TunnelPolicy().cP2InnerTag());
+
+ log.debug("Update process : Deploying new fwd pw for {}", tunnelId);
+ Result lamdaResult = deployPseudoWireInit(newPw.l2Tunnel(), newPw.l2TunnelPolicy().cP1(),
+ newPw.l2TunnelPolicy().cP2(), FWD,
+ fwdNextHop, finalNewPwSpine, egressVlanId);
+ if (lamdaResult != SUCCESS) {
+ return;
+ }
+
+ lamdaResult = deployPolicy(tunnelId, newPw.l2TunnelPolicy().cP1(),
+ newPw.l2TunnelPolicy().cP1InnerTag(),
+ newPw.l2TunnelPolicy().cP1OuterTag(),
+ egressVlanId, lamdaResult.nextId);
+ if (lamdaResult != SUCCESS) {
+ return;
+ }
+ deployPseudoWireTerm(newPw.l2Tunnel(), newPw.l2TunnelPolicy().cP2(),
+ egressVlanId, FWD, finalNewPwSpine);
+
+ }
+ });
+ revPwFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+
+ log.debug("Update process : Deploying new rev pw for {}", tunnelId);
+
+ VlanId egressVlanId = determineEgressVlan(newPw.l2TunnelPolicy().cP2OuterTag(),
+ newPw.l2TunnelPolicy().cP2InnerTag(),
+ newPw.l2TunnelPolicy().cP1OuterTag(),
+ newPw.l2TunnelPolicy().cP1InnerTag());
+
+ Result lamdaResult = deployPseudoWireInit(newPw.l2Tunnel(),
+ newPw.l2TunnelPolicy().cP2(),
+ newPw.l2TunnelPolicy().cP1(),
+ REV,
+ revNextHop, finalNewPwSpine, egressVlanId);
+ if (lamdaResult != SUCCESS) {
+ return;
+ }
+
+ lamdaResult = deployPolicy(tunnelId,
+ newPw.l2TunnelPolicy().cP2(),
+ newPw.l2TunnelPolicy().cP2InnerTag(),
+ newPw.l2TunnelPolicy().cP2OuterTag(),
+ egressVlanId,
+ lamdaResult.nextId);
+ if (lamdaResult != SUCCESS) {
+ return;
+ }
+ deployPseudoWireTerm(newPw.l2Tunnel(),
+ newPw.l2TunnelPolicy().cP1(),
+ egressVlanId,
+ REV, finalNewPwSpine);
+ }
+ });
+ }
+
+ @Override
+ public void processPwaasConfigRemoved(NetworkConfigEvent event) {
+ checkArgument(event.prevConfig().isPresent(),
+ "PrevConfig is not presented in PwaasConfigRemoved event {}", event);
+
+ log.info("Network event : Pseudowire configuration removed!");
+ PwaasConfig config = (PwaasConfig) event.prevConfig().get();
+
+ Set<L2TunnelDescription> pwToRemove = config
+ .getPwIds()
+ .stream()
+ .map(config::getPwDescription)
+ .collect(Collectors.toSet());
+
+ // We teardown all the pseudo wire deployed
+ tearDown(pwToRemove);
+ }
+
+ /**
+ * Helper function for removing a single pseudowire.
+ * <p>
+ * No mastership of CP1 is checked, because it can be called from
+ * the CLI for removal of pseudowires.
+ *
+ * @param l2TunnelId the id of the pseudowire to tear down
+ * @return Returns SUCCESS if no error is obeserved or an appropriate
+ * error on a failure
+ */
+ private Result tearDownPseudowire(long l2TunnelId) {
+
+ CompletableFuture<ObjectiveError> fwdInitNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> fwdTermNextFuture = new CompletableFuture<>();
+
+ CompletableFuture<ObjectiveError> revInitNextFuture = new CompletableFuture<>();
+ CompletableFuture<ObjectiveError> revTermNextFuture = new CompletableFuture<>();
+
+ if (l2TunnelId == 0) {
+ log.warn("Removal process : Tunnel id cannot be 0");
+ return Result.WRONG_PARAMETERS;
+ }
+
+ // check existence of tunnels/policy in the store, if one is missing abort!
+ Versioned<L2Tunnel> l2TunnelVersioned = l2TunnelStore.get(Long.toString(l2TunnelId));
+ Versioned<L2TunnelPolicy> l2TunnelPolicyVersioned = l2PolicyStore.get(Long.toString(l2TunnelId));
+ if ((l2TunnelVersioned == null) || (l2TunnelPolicyVersioned == null)) {
+ log.warn("Removal process : Policy and/or tunnel missing for tunnel id {}", l2TunnelId);
+ return Result.REMOVAL_ERROR;
+ }
+
+ L2TunnelDescription pwToRemove = new DefaultL2TunnelDescription(l2TunnelVersioned.value(),
+ l2TunnelPolicyVersioned.value());
+
+ // remove the tunnels and the policies from the store
+ l2PolicyStore.remove(Long.toString(l2TunnelId));
+ l2TunnelStore.remove(Long.toString(l2TunnelId));
+
+ // remove the reserved transport vlan
+ if (!pwToRemove.l2Tunnel().transportVlan().equals(UNTAGGED_TRANSPORT_VLAN)) {
+ vlanStore.remove(pwToRemove.l2Tunnel().transportVlan());
+ }
+
+ log.info("Removal process : Tearing down forward direction of pseudowire {}", l2TunnelId);
+
+ VlanId egressVlan = determineEgressVlan(pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+ pwToRemove.l2TunnelPolicy().cP1InnerTag(),
+ pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+ pwToRemove.l2TunnelPolicy().cP2InnerTag());
+ deletePolicy(l2TunnelId,
+ pwToRemove.l2TunnelPolicy().cP1(),
+ pwToRemove.l2TunnelPolicy().cP1InnerTag(),
+ pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+ egressVlan,
+ fwdInitNextFuture,
+ FWD);
+
+ fwdInitNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ // Finally we will tear down the pseudo wire.
+ tearDownPseudoWireInit(l2TunnelId,
+ pwToRemove.l2TunnelPolicy().cP1(),
+ fwdTermNextFuture,
+ FWD);
+ }
+ });
+
+ fwdTermNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ tearDownPseudoWireTerm(pwToRemove.l2Tunnel(),
+ pwToRemove.l2TunnelPolicy().cP2(),
+ null,
+ FWD);
+ }
+ });
+
+ log.info("Removal process : Tearing down reverse direction of pseudowire {}", l2TunnelId);
+
+ egressVlan = determineEgressVlan(pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+ pwToRemove.l2TunnelPolicy().cP2InnerTag(),
+ pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+ pwToRemove.l2TunnelPolicy().cP1InnerTag());
+
+ // We do the same operations on the reverse side.
+ deletePolicy(l2TunnelId,
+ pwToRemove.l2TunnelPolicy().cP2(),
+ pwToRemove.l2TunnelPolicy().cP2InnerTag(),
+ pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+ egressVlan,
+ revInitNextFuture,
+ REV);
+
+ revInitNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ tearDownPseudoWireInit(l2TunnelId,
+ pwToRemove.l2TunnelPolicy().cP2(),
+ revTermNextFuture,
+ REV);
+ }
+ });
+
+ revTermNextFuture.thenAcceptAsync(status -> {
+ if (status == null) {
+ tearDownPseudoWireTerm(pwToRemove.l2Tunnel(),
+ pwToRemove.l2TunnelPolicy().cP1(),
+ null,
+ REV);
+ }
+ });
+
+ return Result.SUCCESS;
+ }
+
+ @Override
+ public void tearDown(Set<L2TunnelDescription> pwToRemove) {
+
+ Result result;
+
+ // We remove all the pw in the configuration file.
+ for (L2TunnelDescription currentL2Tunnel : pwToRemove) {
+ ConnectPoint cp1 = currentL2Tunnel.l2TunnelPolicy().cP1();
+ ConnectPoint cp2 = currentL2Tunnel.l2TunnelPolicy().cP2();
+ long tunnelId = currentL2Tunnel.l2TunnelPolicy().tunnelId();
+
+ // only the master of CP1 will program this pseudowire
+ if (!srManager.isMasterOf(cp1)) {
+ log.debug("Not the master of {}. Ignore pseudo wire removal id={}", cp1, tunnelId);
+ continue;
+ }
+
+ // no need to differentiate here between leaf-leaf and leaf-spine, because
+ // the only change is in the groups, which we do not remove either way
+ log.info("Removing pseudowire {}", tunnelId);
+
+ result = tearDownPseudowire(tunnelId);
+ switch (result) {
+ case WRONG_PARAMETERS:
+ log.warn("Error in supplied parameters for the pseudowire removal with tunnel id {}!",
+ tunnelId);
+ break;
+ case REMOVAL_ERROR:
+ log.warn("Error in pseudowire removal with tunnel id {}!", tunnelId);
+ break;
+ default:
+ log.warn("Pseudowire with tunnel id {} was removed successfully", tunnelId);
+ }
+ }
+ }
+
+ /**
+ * Handles the policy establishment which consists in
+ * create the filtering and forwarding objectives related
+ * to the initiation and termination.
+ *
+ * @param tunnelId the tunnel id
+ * @param ingress the ingress point
+ * @param ingressInner the ingress inner tag
+ * @param ingressOuter the ingress outer tag
+ * @param nextId the next objective id
+ * @param egressVlan Vlan-id to set, depends on ingress vlan
+ * combinations. For example, if pw is double tagged
+ * then this is the value of the outer vlan, if single
+ * tagged then it is the new value of the single tag.
+ * Should be None for untagged traffic.
+ * @return the result of the operation
+ */
+ private Result deployPolicy(long tunnelId, ConnectPoint ingress, VlanId ingressInner,
+ VlanId ingressOuter, VlanId egressVlan, int nextId) {
+
+ List<Objective> objectives = Lists.newArrayList();
+ // We create the forwarding objective for supporting
+ // the l2 tunnel.
+ ForwardingObjective.Builder fwdBuilder = createInitFwdObjective(tunnelId, ingress.port(), nextId);
+ // We create and add objective context.
+ ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+ log.debug("FwdObj for tunnel {} populated", tunnelId),
+ (objective, error) ->
+ log.warn("Failed to populate fwdrObj " +
+ "for tunnel {}", tunnelId, error));
+ objectives.add(fwdBuilder.add(context));
+
+ // We create the filtering objective to define the
+ // permit traffic in the switch
+ FilteringObjective.Builder filtBuilder = createFiltObjective(ingress.port(), ingressInner, ingressOuter);
+
+ // We add the metadata.
+ TrafficTreatment.Builder treatment = DefaultTrafficTreatment
+ .builder()
+ .setTunnelId(tunnelId)
+ .setVlanId(egressVlan);
+ filtBuilder.withMeta(treatment.build());
+
+ // We create and add objective context.
+ context = new DefaultObjectiveContext((objective) -> log.debug("FilterObj for tunnel {} populated", tunnelId),
+ (objective, error) -> log.warn("Failed to populate filterObj for " +
+ "tunnel {}", tunnelId, error));
+ objectives.add(filtBuilder.add(context));
+
+ for (Objective objective : objectives) {
+ if (objective instanceof ForwardingObjective) {
+ srManager.flowObjectiveService.forward(ingress.deviceId(), (ForwardingObjective) objective);
+ log.debug("Creating new FwdObj for initiation NextObj with id={} for tunnel {}", nextId, tunnelId);
+ } else {
+ srManager.flowObjectiveService.filter(ingress.deviceId(), (FilteringObjective) objective);
+ log.debug("Creating new FiltObj for tunnel {}", tunnelId);
+ }
+ }
+ return SUCCESS;
+ }
+
+ /**
+ * Handles the tunnel establishment which consists in
+ * create the next objectives related to the initiation.
+ *
+ * @param l2Tunnel the tunnel to deploy
+ * @param ingress the ingress connect point
+ * @param egress the egress connect point
+ * @param direction the direction of the pw
+ * @param spinePw if the pseudowire involves a spine switch
+ * @return the result of the operation
+ */
+ private Result deployPseudoWireInit(L2Tunnel l2Tunnel, ConnectPoint ingress,
+ ConnectPoint egress, Direction direction,
+ Link nextHop, boolean spinePw, VlanId termVlanId) {
+
+ if (nextHop == null) {
+ log.warn("No path between ingress and egress cps for tunnel {}", l2Tunnel.tunnelId());
+ return WRONG_PARAMETERS;
+ }
+
+ // We create the next objective without the metadata
+ // context and id. We check if it already exists in the
+ // store. If not we store as it is in the store.
+ NextObjective.Builder nextObjectiveBuilder = createNextObjective(INITIATION,
+ nextHop.src(),
+ nextHop.dst(),
+ l2Tunnel,
+ egress.deviceId(),
+ spinePw,
+ termVlanId);
+
+ if (nextObjectiveBuilder == null) {
+ return INTERNAL_ERROR;
+ }
+ // We set the metadata. We will use this metadata
+ // to inform the driver we are doing a l2 tunnel.
+ TrafficSelector metadata = DefaultTrafficSelector
+ .builder()
+ .matchTunnelId(l2Tunnel.tunnelId())
+ .build();
+ nextObjectiveBuilder.withMeta(metadata);
+ int nextId = srManager.flowObjectiveService.allocateNextId();
+ if (nextId < 0) {
+ log.warn("Not able to allocate a next id for initiation");
+ return INTERNAL_ERROR;
+ }
+ nextObjectiveBuilder.withId(nextId);
+ String key = generateKey(l2Tunnel.tunnelId(), direction);
+ l2InitiationNextObjStore.put(key, nextObjectiveBuilder.add());
+ ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+ log.debug("Initiation l2 tunnel rule " +
+ "for {} populated",
+ l2Tunnel.tunnelId()),
+ (objective, error) ->
+ log.warn("Failed to populate Initiation " +
+ "l2 tunnel rule for {}: {}",
+ l2Tunnel.tunnelId(), error));
+ NextObjective nextObjective = nextObjectiveBuilder.add(context);
+ srManager.flowObjectiveService.next(ingress.deviceId(), nextObjective);
+ log.debug("Initiation next objective for {} not found. Creating new NextObj with id={}",
+ l2Tunnel.tunnelId(), nextObjective.id());
+ Result result = SUCCESS;
+ result.nextId = nextObjective.id();
+ return result;
+ }
+
+ /**
+ * Handles the tunnel termination, which consists in the creation
+ * of a forwarding objective and a next objective.
+ *
+ * @param l2Tunnel the tunnel to terminate
+ * @param egress the egress point
+ * @param egressVlan the expected vlan at egress
+ * @param direction the direction
+ * @param spinePw if the pseudowire involves a spine switch
+ * @return the result of the operation
+ */
+ private Result deployPseudoWireTerm(L2Tunnel l2Tunnel, ConnectPoint egress,
+ VlanId egressVlan, Direction direction, boolean spinePw) {
+
+ // We create the group relative to the termination.
+ NextObjective.Builder nextObjectiveBuilder = createNextObjective(TERMINATION, egress, null,
+ l2Tunnel, egress.deviceId(),
+ spinePw,
+ egressVlan);
+ if (nextObjectiveBuilder == null) {
+ return INTERNAL_ERROR;
+ }
+ TrafficSelector metadata = DefaultTrafficSelector
+ .builder()
+ .matchVlanId(egressVlan)
+ .build();
+ nextObjectiveBuilder.withMeta(metadata);
+ int nextId = srManager.flowObjectiveService.allocateNextId();
+ if (nextId < 0) {
+ log.warn("Not able to allocate a next id for initiation");
+ return INTERNAL_ERROR;
+ }
+ nextObjectiveBuilder.withId(nextId);
+ String key = generateKey(l2Tunnel.tunnelId(), direction);
+ l2TerminationNextObjStore.put(key, nextObjectiveBuilder.add());
+ ObjectiveContext context = new DefaultObjectiveContext((objective) -> log.debug("Termination l2 tunnel rule " +
+ "for {} populated",
+ l2Tunnel.tunnelId()),
+ (objective, error) -> log.warn("Failed to populate " +
+ "termination l2 tunnel " +
+ "rule for {}: {}",
+ l2Tunnel.tunnelId(),
+ error));
+ NextObjective nextObjective = nextObjectiveBuilder.add(context);
+ srManager.flowObjectiveService.next(egress.deviceId(), nextObjective);
+ log.debug("Termination next objective for {} not found. Creating new NextObj with id={}",
+ l2Tunnel.tunnelId(), nextObjective.id());
+
+ // We create the flow relative to the termination.
+ ForwardingObjective.Builder fwdBuilder = createTermFwdObjective(l2Tunnel.pwLabel(), l2Tunnel.tunnelId(),
+ egress.port(), nextObjective.id());
+ context = new DefaultObjectiveContext((objective) -> log.debug("FwdObj for tunnel termination {} populated",
+ l2Tunnel.tunnelId()),
+ (objective, error) -> log.warn("Failed to populate fwdrObj" +
+ " for tunnel termination {}",
+ l2Tunnel.tunnelId(), error));
+ srManager.flowObjectiveService.forward(egress.deviceId(), fwdBuilder.add(context));
+ log.debug("Creating new FwdObj for termination NextObj with id={} for tunnel {}",
+ nextId, l2Tunnel.tunnelId());
+
+ if (spinePw) {
+
+ // determine the input port at the
+ PortNumber inPort;
+
+ if (egress.deviceId().
+ equals(l2Tunnel.pathUsed().get(0).dst().deviceId())) {
+ inPort = l2Tunnel.pathUsed().get(0).dst().port();
+ } else {
+ inPort = l2Tunnel.pathUsed().get(0).src().port();
+ }
+
+ MacAddress dstMac;
+ try {
+ dstMac = srManager.deviceConfiguration().getDeviceMac(egress.deviceId());
+ } catch (Exception e) {
+ log.info("Device not found in configuration, no programming of MAC address");
+ dstMac = null;
+ }
+
+ log.info("Populating filtering objective for pseudowire transport" +
+ " with vlan = {}, port = {}, mac = {}",
+ l2Tunnel.transportVlan(),
+ inPort,
+ dstMac);
+ FilteringObjective.Builder filteringObjectiveBuilder =
+ createNormalPipelineFiltObjective(inPort, l2Tunnel.transportVlan(), dstMac);
+ context = new DefaultObjectiveContext(( objective ) ->
+ log.debug("Special filtObj for " + "for {} populated",
+ l2Tunnel.tunnelId()),
+ ( objective, error ) ->
+ log.warn("Failed to populate " +
+ "special filtObj " +
+ "rule for {}: {}",
+ l2Tunnel.tunnelId(), error));
+ TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
+ filteringObjectiveBuilder.withMeta(treatment.build());
+ srManager.flowObjectiveService.filter(egress.deviceId(), filteringObjectiveBuilder.add(context));
+ log.debug("Creating new special FiltObj for termination point with tunnel {} for port {}",
+ l2Tunnel.tunnelId(),
+ inPort);
+ }
+
+ return SUCCESS;
+ }
+
+
+ /**
+ * Creates the filtering objective according to a given port and vlanid.
+ *
+ * @param inPort the in port
+ * @param vlanId the inner vlan tag
+ * @return the filtering objective
+ */
+ private FilteringObjective.Builder createNormalPipelineFiltObjective(PortNumber inPort,
+ VlanId vlanId,
+ MacAddress dstMac) {
+
+ log.info("Creating filtering objective for pseudowire transport with vlan={}, port={}, mac={}",
+ vlanId,
+ inPort,
+ dstMac);
+ FilteringObjective.Builder fwdBuilder = DefaultFilteringObjective
+ .builder()
+ .withKey(Criteria.matchInPort(inPort))
+ .addCondition(Criteria.matchVlanId(vlanId))
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+ .permit()
+ .fromApp(srManager.appId());
+
+ if (dstMac != null) {
+ fwdBuilder.addCondition(Criteria.matchEthDst(dstMac));
+ }
+
+ return fwdBuilder;
+ }
+
+ /**
+ * Creates the filtering objective according to a given policy.
+ *
+ * @param inPort the in port
+ * @param innerTag the inner vlan tag
+ * @param outerTag the outer vlan tag
+ * @return the filtering objective
+ */
+ private FilteringObjective.Builder createFiltObjective(PortNumber inPort, VlanId innerTag, VlanId outerTag) {
+
+ log.info("Creating filtering objective for vlans {} / {}", outerTag, innerTag);
+ return DefaultFilteringObjective
+ .builder()
+ .withKey(Criteria.matchInPort(inPort))
+ .addCondition(Criteria.matchInnerVlanId(innerTag))
+ .addCondition(Criteria.matchVlanId(outerTag))
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+ .permit()
+ .fromApp(srManager.appId());
+ }
+
+ /**
+ * Creates the forwarding objective for the termination.
+ *
+ * @param pwLabel the pseudo wire label
+ * @param tunnelId the tunnel id
+ * @param egressPort the egress port
+ * @param nextId the next step
+ * @return the forwarding objective to support the termination
+ */
+ private ForwardingObjective.Builder createTermFwdObjective(MplsLabel pwLabel, long tunnelId,
+ PortNumber egressPort, int nextId) {
+
+ TrafficSelector.Builder trafficSelector = DefaultTrafficSelector.builder();
+ TrafficTreatment.Builder trafficTreatment = DefaultTrafficTreatment.builder();
+ // The flow has to match on the pw label and bos
+ trafficSelector.matchEthType(Ethernet.MPLS_UNICAST);
+ trafficSelector.matchMplsLabel(pwLabel);
+ trafficSelector.matchMplsBos(true);
+ // The flow has to decrement ttl, restore ttl in
+ // pop mpls, set tunnel id and port.
+ trafficTreatment.decMplsTtl();
+ trafficTreatment.copyTtlIn();
+ trafficTreatment.popMpls();
+ trafficTreatment.setTunnelId(tunnelId);
+ trafficTreatment.setOutput(egressPort);
+
+ return DefaultForwardingObjective
+ .builder()
+ .fromApp(srManager.appId())
+ .makePermanent()
+ .nextStep(nextId)
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+ .withSelector(trafficSelector.build())
+ .withTreatment(trafficTreatment.build())
+ .withFlag(VERSATILE);
+ }
+
+ /**
+ * Creates the forwarding objective for the initiation.
+ *
+ * @param tunnelId the tunnel id
+ * @param inPort the input port
+ * @param nextId the next step
+ * @return the forwarding objective to support the initiation.
+ */
+ private ForwardingObjective.Builder createInitFwdObjective(long tunnelId, PortNumber inPort, int nextId) {
+
+ TrafficSelector.Builder trafficSelector = DefaultTrafficSelector.builder();
+
+ // The flow has to match on the mpls logical
+ // port and the tunnel id.
+ trafficSelector.matchTunnelId(tunnelId);
+ trafficSelector.matchInPort(inPort);
+
+ return DefaultForwardingObjective
+ .builder()
+ .fromApp(srManager.appId())
+ .makePermanent()
+ .nextStep(nextId)
+ .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+ .withSelector(trafficSelector.build())
+ .withFlag(VERSATILE);
+
+ }
+
+ /**
+ * Creates the next objective according to a given
+ * pipeline. We don't set the next id and we don't
+ * create the final meta to check if we are re-using
+ * the same next objective for different tunnels.
+ *
+ * @param pipeline the pipeline to support
+ * @param srcCp the source port
+ * @param dstCp the destination port
+ * @param l2Tunnel the tunnel to support
+ * @param egressId the egress device id
+ * @param spinePw if the pw involves a spine switch
+ * @return the next objective to support the pipeline
+ */
+ private NextObjective.Builder createNextObjective(Pipeline pipeline, ConnectPoint srcCp,
+ ConnectPoint dstCp, L2Tunnel l2Tunnel,
+ DeviceId egressId, boolean spinePw, VlanId termVlanId) {
+ NextObjective.Builder nextObjBuilder;
+ TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
+ if (pipeline == INITIATION) {
+ nextObjBuilder = DefaultNextObjective
+ .builder()
+ .withType(NextObjective.Type.SIMPLE)
+ .fromApp(srManager.appId());
+ // The pw label is the bottom of stack. It has to
+ // be different -1.
+ if (l2Tunnel.pwLabel().toInt() == MplsLabel.MAX_MPLS) {
+ log.warn("Pw label not configured");
+ return null;
+ }
+ treatmentBuilder.pushMpls();
+ treatmentBuilder.setMpls(l2Tunnel.pwLabel());
+ treatmentBuilder.setMplsBos(true);
+ treatmentBuilder.copyTtlOut();
+
+ // If the inter-co label is present we have to set the label.
+ if (l2Tunnel.interCoLabel().toInt() != MplsLabel.MAX_MPLS) {
+ treatmentBuilder.pushMpls();
+ treatmentBuilder.setMpls(l2Tunnel.interCoLabel());
+ treatmentBuilder.setMplsBos(false);
+ treatmentBuilder.copyTtlOut();
+ }
+
+ // if pw is leaf-to-leaf we need to
+ // add the routing label also
+ if (!spinePw) {
+ // We retrieve the sr label from the config
+ // specific for pseudowire traffic
+ // using the egress leaf device id.
+ MplsLabel srLabel;
+ try {
+ srLabel = MplsLabel.mplsLabel(srManager.deviceConfiguration().getPWRoutingLabel(egressId));
+
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Sr label for pw traffic not configured");
+ return null;
+ }
+
+ treatmentBuilder.pushMpls();
+ treatmentBuilder.setMpls(srLabel);
+ treatmentBuilder.setMplsBos(false);
+ treatmentBuilder.copyTtlOut();
+ }
+
+ // We have to rewrite the src and dst mac address.
+ MacAddress ingressMac;
+ try {
+ ingressMac = srManager.deviceConfiguration().getDeviceMac(srcCp.deviceId());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Was not able to find the ingress mac");
+ return null;
+ }
+ treatmentBuilder.setEthSrc(ingressMac);
+ MacAddress neighborMac;
+ try {
+ neighborMac = srManager.deviceConfiguration().getDeviceMac(dstCp.deviceId());
+ } catch (DeviceConfigNotFoundException e) {
+ log.warn("Was not able to find the neighbor mac");
+ return null;
+ }
+ treatmentBuilder.setEthDst(neighborMac);
+
+ // if not a leaf-spine pw we need to POP the vlan at the output
+ // since we carry this traffic untagged.
+ if (!spinePw) {
+ treatmentBuilder.popVlan();
+ }
+
+ // set the appropriate transport vlan
+ treatmentBuilder.setVlanId(l2Tunnel.transportVlan());
+ } else {
+ // We create the next objective which
+ // will be a simple l2 group.
+ nextObjBuilder = DefaultNextObjective
+ .builder()
+ .withType(NextObjective.Type.SIMPLE)
+ .fromApp(srManager.appId());
+
+ // for termination point we use the outer vlan of the
+ // encapsulated packet
+ treatmentBuilder.setVlanId(termVlanId);
+ }
+
+ treatmentBuilder.setOutput(srcCp.port());
+ nextObjBuilder.addTreatment(treatmentBuilder.build());
+ return nextObjBuilder;
+ }
+
+ /**
+ * Reverses a link.
+ *
+ * @param link link to be reversed
+ * @return the reversed link
+ */
+ private Link reverseLink(Link link) {
+
+ DefaultLink.Builder linkBuilder = DefaultLink.builder();
+
+ linkBuilder.src(link.dst());
+ linkBuilder.dst(link.src());
+ linkBuilder.type(link.type());
+ linkBuilder.providerId(link.providerId());
+
+ return linkBuilder.build();
+ }
+
+ /**
+ * Returns the path betwwen two connect points.
+ *
+ * @param srcCp source connect point
+ * @param dstCp destination connect point
+ * @return the path
+ */
+ private List<Link> getPath(ConnectPoint srcCp, ConnectPoint dstCp) {
+ /* TODO We retrieve a set of paths in case of a link failure, what happens
+ * if the TopologyService gets the link notification AFTER us and has not updated the paths?
+ *
+ * TODO This has the potential to act on old topology.
+ * Maybe we should make SRManager be a listener on topology events instead raw link events.
+ */
+ Set<Path> paths = srManager.topologyService.getPaths(
+ srManager.topologyService.currentTopology(),
+ srcCp.deviceId(), dstCp.deviceId());
+
+ log.debug("Paths obtained from topology service {}", paths);
+
+ // We randomly pick a path.
+ if (paths.isEmpty()) {
+ return null;
+ }
+ int size = paths.size();
+ int index = RandomUtils.nextInt(0, size);
+
+ List<Link> result = Iterables.get(paths, index).links();
+ log.debug("Randomly picked a path {}", result);
+
+ return result;
+ }
+
+ /**
+ * Deletes a given policy using the parameter supplied.
+ *
+ * @param tunnelId the tunnel id
+ * @param ingress the ingress point
+ * @param ingressInner the ingress inner vlan id
+ * @param ingressOuter the ingress outer vlan id
+ * @param future to perform the async operation
+ * @param direction the direction: forward or reverse
+ */
+ private void deletePolicy(long tunnelId, ConnectPoint ingress, VlanId ingressInner, VlanId ingressOuter,
+ VlanId egressVlan, CompletableFuture<ObjectiveError> future, Direction direction) {
+
+ String key = generateKey(tunnelId, direction);
+ if (!l2InitiationNextObjStore.containsKey(key)) {
+ log.warn("Abort delete of policy for tunnel {}: next does not exist in the store", tunnelId);
+ if (future != null) {
+ future.complete(null);
+ }
+ return;
+ }
+ NextObjective nextObjective = l2InitiationNextObjStore.get(key).value();
+ int nextId = nextObjective.id();
+ List<Objective> objectives = Lists.newArrayList();
+ // We create the forwarding objective.
+ ForwardingObjective.Builder fwdBuilder = createInitFwdObjective(tunnelId, ingress.port(), nextId);
+ ObjectiveContext context = new ObjectiveContext() {
+ @Override
+ public void onSuccess(Objective objective) {
+ log.debug("Previous fwdObj for policy {} removed", tunnelId);
+ if (future != null) {
+ future.complete(null);
+ }
+ }
+
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to remove previous fwdObj for policy {}: {}", tunnelId, error);
+ if (future != null) {
+ future.complete(error);
+ }
+ }
+ };
+ objectives.add(fwdBuilder.remove(context));
+ // We create the filtering objective to define the
+ // permit traffic in the switch
+ FilteringObjective.Builder filtBuilder = createFiltObjective(ingress.port(), ingressInner, ingressOuter);
+ TrafficTreatment.Builder treatment = DefaultTrafficTreatment
+ .builder()
+ .setTunnelId(tunnelId)
+ .setVlanId(egressVlan);
+ filtBuilder.withMeta(treatment.build());
+ context = new DefaultObjectiveContext((objective) -> log.debug("FilterObj for policy {} revoked", tunnelId),
+ (objective, error) ->
+ log.warn("Failed to revoke filterObj for policy {}",
+ tunnelId, error));
+ objectives.add(filtBuilder.remove(context));
+
+ for (Objective objective : objectives) {
+ if (objective instanceof ForwardingObjective) {
+ srManager.flowObjectiveService.forward(ingress.deviceId(), (ForwardingObjective) objective);
+ } else {
+ srManager.flowObjectiveService.filter(ingress.deviceId(), (FilteringObjective) objective);
+ }
+ }
+ }
+
+ /**
+ * Deletes the pseudo wire initiation.
+ *
+ * @param l2TunnelId the tunnel id
+ * @param ingress the ingress connect point
+ * @param future to perform an async operation
+ * @param direction the direction: reverse of forward
+ */
+ private void tearDownPseudoWireInit(long l2TunnelId, ConnectPoint ingress,
+ CompletableFuture<ObjectiveError> future, Direction direction) {
+
+ String key = generateKey(l2TunnelId, direction);
+ if (!l2InitiationNextObjStore.containsKey(key)) {
+ log.info("Abort delete of {} for {}: next does not exist in the store", INITIATION, key);
+ if (future != null) {
+ future.complete(null);
+ }
+ return;
+ }
+ NextObjective nextObjective = l2InitiationNextObjStore.get(key).value();
+
+ // un-comment in case you want to delete groups used by the pw
+ // however, this will break the update of pseudowires cause the L2 interface group can
+ // not be deleted (it is referenced by other groups)
+ /*
+ ObjectiveContext context = new ObjectiveContext() {
+ @Override
+ public void onSuccess(Objective objective) {
+ log.debug("Previous {} next for {} removed", INITIATION, key);
+ if (future != null) {
+ future.complete(null);
+ }
+ }
+
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to remove previous {} next for {}: {}", INITIATION, key, error);
+ if (future != null) {
+ future.complete(error);
+ }
+ }
+ };
+ srManager.flowObjectiveService.next(ingress.deviceId(), (NextObjective) nextObjective.copy().remove(context));
+ */
+
+ future.complete(null);
+ l2InitiationNextObjStore.remove(key);
+ }
+
+ /**
+ * Deletes the pseudo wire termination.
+ *
+ * @param l2Tunnel the tunnel
+ * @param egress the egress connect point
+ * @param future the async task
+ * @param direction the direction of the tunnel
+ */
+ private void tearDownPseudoWireTerm(L2Tunnel l2Tunnel,
+ ConnectPoint egress,
+ CompletableFuture<ObjectiveError> future,
+ Direction direction) {
+
+ String key = generateKey(l2Tunnel.tunnelId(), direction);
+ if (!l2TerminationNextObjStore.containsKey(key)) {
+ log.info("Abort delete of {} for {}: next does not exist in the store", TERMINATION, key);
+ if (future != null) {
+ future.complete(null);
+ }
+ return;
+ }
+ NextObjective nextObjective = l2TerminationNextObjStore.get(key).value();
+ ForwardingObjective.Builder fwdBuilder = createTermFwdObjective(l2Tunnel.pwLabel(),
+ l2Tunnel.tunnelId(),
+ egress.port(),
+ nextObjective.id());
+ ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+ log.debug("FwdObj for {} {}, " +
+ "direction {} removed",
+ TERMINATION,
+ l2Tunnel.tunnelId(),
+ direction),
+ (objective, error) ->
+ log.warn("Failed to remove fwdObj " +
+ "for {} {}" +
+ ", direction {}",
+ TERMINATION,
+ l2Tunnel.tunnelId(),
+ error,
+ direction));
+ srManager.flowObjectiveService.forward(egress.deviceId(), fwdBuilder.remove(context));
+
+ // un-comment in case you want to delete groups used by the pw
+ // however, this will break the update of pseudowires cause the L2 interface group can
+ // not be deleted (it is referenced by other groups)
+ /*
+ context = new ObjectiveContext() {
+ @Override
+ public void onSuccess(Objective objective) {
+ log.debug("Previous {} next for {} removed", TERMINATION, key);
+ if (future != null) {
+ future.complete(null);
+ }
+ }
+
+ @Override
+ public void onError(Objective objective, ObjectiveError error) {
+ log.warn("Failed to remove previous {} next for {}: {}", TERMINATION, key, error);
+ if (future != null) {
+ future.complete(error);
+ }
+ }
+ };
+ srManager.flowObjectiveService.next(egress.deviceId(), (NextObjective) nextObjective.copy().remove(context));
+ */
+
+ // delete the extra filtering objective for terminating
+ // spine-spine pws
+ if (!l2Tunnel.transportVlan().equals(UNTAGGED_TRANSPORT_VLAN)) {
+
+ // determine the input port at the
+ PortNumber inPort;
+
+ if (egress.deviceId().
+ equals(l2Tunnel.pathUsed().get(0).dst().deviceId())) {
+ inPort = l2Tunnel.pathUsed().get(0).dst().port();
+ } else {
+ inPort = l2Tunnel.pathUsed().get(0).src().port();
+ }
+
+ MacAddress dstMac;
+ try {
+ dstMac = srManager.deviceConfiguration().getDeviceMac(egress.deviceId());
+ } catch (Exception e) {
+ log.info("Device not found in configuration, no programming of MAC address");
+ dstMac = null;
+ }
+
+ log.info("Removing filtering objective for pseudowire transport" +
+ " with vlan = {}, port = {}, mac = {}",
+ l2Tunnel.transportVlan(),
+ inPort,
+ dstMac);
+ FilteringObjective.Builder filteringObjectiveBuilder =
+ createNormalPipelineFiltObjective(inPort, l2Tunnel.transportVlan(), dstMac);
+ context = new DefaultObjectiveContext(( objective ) ->
+ log.debug("Special filtObj for " + "for {} removed",
+ l2Tunnel.tunnelId()), ( objective, error ) ->
+ log.warn("Failed to populate " + "special filtObj " +
+ "rule for {}: {}", l2Tunnel.tunnelId(), error));
+ TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
+ filteringObjectiveBuilder.withMeta(treatment.build());
+ srManager.flowObjectiveService.filter(egress.deviceId(), filteringObjectiveBuilder.remove(context));
+ log.debug("Removing special FiltObj for termination point with tunnel {} for port {}",
+ l2Tunnel.tunnelId(),
+ inPort);
+ }
+
+ l2TerminationNextObjStore.remove(key);
+ future.complete(null);
+ }
+
+ /**
+ * Utilities to generate pw key.
+ *
+ * @param tunnelId the tunnel id
+ * @param direction the direction of the pw
+ * @return the key of the store
+ */
+ private String generateKey(long tunnelId, Direction direction) {
+ return String.format("%s-%s", tunnelId, direction);
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java
new file mode 100644
index 0000000..4738744
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the default l2 tunnel policy.
+ */
+public class DefaultL2TunnelPolicy implements L2TunnelPolicy {
+
+ /**
+ * Id of the tunnel associated to this policy.
+ */
+ private long tunnelId;
+ /**
+ * First connect point.
+ */
+ private ConnectPoint cP1;
+ /**
+ * Second connect point.
+ */
+ private ConnectPoint cP2;
+ /**
+ * cP1 inner vlan tag. Used in QinQ packets.
+ */
+ private VlanId cP1InnerTag;
+ /**
+ * cP1 outer vlan tag.
+ */
+ private VlanId cP1OuterTag;
+ /**
+ * cP2 inner vlan tag. Used in QinQ packets.
+ */
+ private VlanId cP2InnerTag;
+ /**
+ * cP2 outer vlan tag.
+ */
+ private VlanId cP2OuterTag;
+
+ /**
+ * Creates a default l2 tunnel policy using
+ * the given parameters.
+ *
+ * @param tunnelId the tunnel id
+ * @param cP1 the first connect point
+ * @param cP1InnerTag the cP1 inner tag
+ * @param cP1OuterTag the cP1 outer tag
+ * @param cP2 the second connect point
+ * @param cP2InnerTag the cP2 inner tag
+ * @param cP2OuterTag the cP2 outer tag
+ */
+ public DefaultL2TunnelPolicy(long tunnelId,
+ ConnectPoint cP1, VlanId cP1InnerTag, VlanId cP1OuterTag,
+ ConnectPoint cP2, VlanId cP2InnerTag, VlanId cP2OuterTag) {
+ this.cP1 = checkNotNull(cP1);
+ this.cP2 = checkNotNull(cP2);
+ this.tunnelId = tunnelId;
+ this.cP1InnerTag = cP1InnerTag;
+ this.cP1OuterTag = cP1OuterTag;
+ this.cP2InnerTag = cP2InnerTag;
+ this.cP2OuterTag = cP2OuterTag;
+ }
+
+ /**
+ * Creates a default l2 policy given the provided policy.
+ * @param policy L2policy to replicate
+ */
+ public DefaultL2TunnelPolicy(DefaultL2TunnelPolicy policy) {
+
+ this.cP1 = policy.cP1;
+ this.cP2 = policy.cP2;
+ this.tunnelId = policy.tunnelId;
+ this.cP1InnerTag = policy.cP1InnerTag;
+ this.cP1OuterTag = policy.cP1OuterTag;
+ this.cP2InnerTag = policy.cP2InnerTag;
+ this.cP2OuterTag = policy.cP2OuterTag;
+ }
+
+ @Override
+ public ConnectPoint cP1() {
+ return cP1;
+ }
+
+ @Override
+ public ConnectPoint cP2() {
+ return cP2;
+ }
+
+ @Override
+ public VlanId cP1InnerTag() {
+ return cP1InnerTag;
+ }
+
+ @Override
+ public VlanId cP1OuterTag() {
+ return cP1OuterTag;
+ }
+
+ @Override
+ public VlanId cP2InnerTag() {
+ return cP2InnerTag;
+ }
+
+ @Override
+ public VlanId cP2OuterTag() {
+ return cP2OuterTag;
+ }
+
+ @Override
+ public long tunnelId() {
+ return this.tunnelId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tunnelId,
+ cP1,
+ cP2,
+ cP1InnerTag,
+ cP1OuterTag,
+ cP2InnerTag,
+ cP2OuterTag
+ );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o instanceof DefaultL2TunnelPolicy) {
+ DefaultL2TunnelPolicy that = (DefaultL2TunnelPolicy) o;
+ if (this.tunnelId == that.tunnelId &&
+ this.cP1.equals(that.cP1) &&
+ this.cP2.equals(that.cP2) &&
+ this.cP1InnerTag.equals(that.cP1InnerTag) &&
+ this.cP1OuterTag.equals(that.cP1OuterTag) &&
+ this.cP2InnerTag.equals(that.cP2InnerTag) &&
+ this.cP2OuterTag.equals(that.cP2OuterTag)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("tunnelId", tunnelId())
+ .add("cP1", cP1())
+ .add("cP2", cP2())
+ .add("cP1InnerTag", cP1InnerTag())
+ .add("cP1OuterTag", cP1OuterTag())
+ .add("cP2InnerTag", cP2InnerTag())
+ .add("cP2OuterTag", cP2OuterTag())
+ .toString();
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java
new file mode 100644
index 0000000..685a606
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+/**
+ * Enum to identify mode of the pwaas.
+ */
+public enum L2Mode {
+ /**
+ * Raw mode.
+ */
+ RAW,
+ /**
+ * Tagged mode. In this case the packet need
+ * the sd tag.
+ */
+ TAGGED
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java
new file mode 100644
index 0000000..3e98480
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.Link;
+
+import java.util.List;
+
+public interface L2Tunnel {
+
+ /**
+ * Return the mode of the l2 tunnel.
+ *
+ * @return The pw mode.
+ */
+ L2Mode pwMode();
+
+ /**
+ * Returns the service delimiting tag.
+ *
+ * @return the sd tag
+ */
+ VlanId sdTag();
+
+ /**
+ * Returns the id of the tunnel.
+ *
+ * @return the tunnel id
+ */
+ long tunnelId();
+
+ /**
+ * Return the label of the pseudowire.
+ *
+ * @return the pw label.
+ */
+ MplsLabel pwLabel();
+
+ /**
+ * Returns the path used by the pseudowire.
+ *
+ * @return The path that is used
+ */
+ List<Link> pathUsed();
+
+ /**
+ * Returns the transport vlan used by the pseudowire.
+ *
+ * @return The transport vlan
+ */
+ VlanId transportVlan();
+
+ /**
+ * Returns the inter-co label used by the pseudowire.
+ *
+ * @return The inter CO label.
+ */
+ MplsLabel interCoLabel();
+
+ /**
+ * Sets the path that this pw uses.
+ *
+ * @param path The apth to use
+ */
+ void setPath(List<Link> path);
+
+ /**
+ * Set the transport vlan that this pw will use.
+ *
+ * @param vlan The vlan to use.
+ */
+ void setTransportVlan(VlanId vlan);
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java
new file mode 100644
index 0000000..7be187a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+public interface L2TunnelDescription {
+
+ /**
+ * Returns the l2 tunnel.
+ *
+ * @return the l2 tunnel
+ */
+ L2Tunnel l2Tunnel();
+
+ /**
+ * Returns the l2 tunnel policy.
+ *
+ * @return the l2 tunnel policy.
+ */
+ L2TunnelPolicy l2TunnelPolicy();
+
+ /**
+ * Sets the l2 tunnel.
+ *
+ * @param tunnel the l2 tunnel to set.
+ */
+ void setL2Tunnel(L2Tunnel tunnel);
+
+ /**
+ * Sets the l2 policy.
+ *
+ * @param policy the policy to set.
+ */
+ void setL2TunnelPolicy(L2TunnelPolicy policy);
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java
new file mode 100644
index 0000000..3a89a21
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.config.NetworkConfigEvent;
+
+import java.util.List;
+import java.util.Set;
+
+public interface L2TunnelHandler {
+ void init();
+
+ /**
+ * Returns a copy of the l2 policies that exist in the store.
+ *
+ * @return The l2 policies
+ */
+ List<L2TunnelPolicy> getL2Policies();
+
+ /**
+ * Returns a copy of the l2 tunnels that exist in the store.
+ *
+ * @return The l2 tunnels.
+ */
+ List<L2Tunnel> getL2Tunnels();
+
+ /**
+ * Processes a link removal. Finds affected pseudowires and rewires them.
+ * TODO: Make it also take into account failures of links that are used for pw
+ * traffic in the spine.
+ * @param link The link that failed
+ */
+ void processLinkDown(Link link);
+
+ /**
+ * Processes Pwaas Config added event.
+ *
+ * @param event network config add event
+ */
+ void processPwaasConfigAdded(NetworkConfigEvent event);
+
+ /**
+ * Processes PWaaS Config updated event.
+ *
+ * @param event network config updated event
+ */
+ void processPwaasConfigUpdated(NetworkConfigEvent event);
+
+ /**
+ * Processes Pwaas Config removed event.
+ *
+ * @param event network config removed event
+ */
+ void processPwaasConfigRemoved(NetworkConfigEvent event);
+
+ /**
+ * Helper function to handle the pw removal.
+ * <p>
+ * This method should for the mastership of the device because it is
+ * used only from network configuration updates, thus we only want
+ * one instance only to program each pseudowire.
+ *
+ * @param pwToRemove the pseudo wires to remove
+ */
+ void tearDown(Set<L2TunnelDescription> pwToRemove);
+
+ /**
+ * Pwaas pipelines.
+ */
+ enum Pipeline {
+ /**
+ * The initiation pipeline.
+ */
+ INITIATION, /**
+ * The termination pipeline.
+ */
+ TERMINATION
+ }
+
+ /**
+ * Enum helper to carry results of various operations.
+ */
+ enum Result {
+ /**
+ * Happy ending scenario.
+ */
+ SUCCESS(0, "No error occurred"),
+
+ /**
+ * We have problems with the supplied parameters.
+ */
+ WRONG_PARAMETERS(1, "Wrong parameters"),
+
+ /**
+ * We have an internal error during the deployment
+ * or removal phase.
+ */
+ INTERNAL_ERROR(3, "Internal error"),
+
+ /**
+ *
+ */
+ REMOVAL_ERROR(5, "Can not remove pseudowire from network configuration"),
+
+ /**
+ *
+ */
+ ADDITION_ERROR(6, "Can not add pseudowire in network configuration"),
+
+ /**
+ *
+ */
+ CONFIG_NOT_FOUND(7, "Can not find configuration class for pseudowires");
+
+ private final int code;
+ private final String description;
+ public int nextId;
+
+ Result(int code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return code + ": " + description;
+ }
+ }
+
+ /**
+ * Enum helper for handling the direction of the pw.
+ */
+ enum Direction {
+ /**
+ * The forward direction of the pseudo wire.
+ */
+ FWD, /**
+ * The reverse direction of the pseudo wire.
+ */
+ REV
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java
new file mode 100644
index 0000000..17be45b
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.pwaas;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+public interface L2TunnelPolicy {
+
+ /**
+ * Returns the first connect point of the policy.
+ *
+ * @return first connect point
+ */
+ ConnectPoint cP1();
+
+ /**
+ * Returns the second connect point of the policy.
+ *
+ * @return second connect point
+ */
+ ConnectPoint cP2();
+
+ /**
+ * Returns the cP1 inner vlan tag of the policy.
+ *
+ * @return cP1 inner vlan tag
+ */
+ VlanId cP1InnerTag();
+
+ /**
+ * Returns the cP1 outer vlan tag of the policy.
+ *
+ * @return cP1 outer vlan tag
+ */
+ VlanId cP1OuterTag();
+
+ /**
+ * Returns the cP2 inner vlan tag of the policy.
+ *
+ * @return cP2 inner vlan tag
+ */
+ VlanId cP2InnerTag();
+
+ /**
+ * Returns the cP2 outer vlan tag of the policy.
+ *
+ * @return cP2 outer vlan tag
+ */
+ VlanId cP2OuterTag();
+
+ /**
+ * Returns the tunnel ID of the policy.
+ *
+ * @return Tunnel ID
+ */
+ long tunnelId();
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java
new file mode 100644
index 0000000..463b163
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Set of resources implementing the Pwaas.
+ */
+package org.onosproject.segmentrouting.pwaas;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java
new file mode 100644
index 0000000..9b6e621
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licedses/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.storekey;
+
+import java.util.Objects;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+
+/**
+ * Key of Destination set next objective store.
+ */
+public class DestinationSetNextObjectiveStoreKey {
+ private final DeviceId deviceId;
+ private final DestinationSet ds;
+
+ /**
+ * Constructs the key of destination set next objective store.
+ *
+ * @param deviceId device ID
+ * @param ds destination set
+ */
+ public DestinationSetNextObjectiveStoreKey(DeviceId deviceId,
+ DestinationSet ds) {
+ this.deviceId = deviceId;
+ this.ds = ds;
+ }
+
+ /**
+ * Returns the device ID in the key.
+ *
+ * @return device ID
+ */
+ public DeviceId deviceId() {
+ return this.deviceId;
+ }
+
+ /**
+ * Returns the destination set in the key.
+ *
+ * @return destination set
+ */
+ public DestinationSet destinationSet() {
+ return this.ds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof DestinationSetNextObjectiveStoreKey)) {
+ return false;
+ }
+ DestinationSetNextObjectiveStoreKey that =
+ (DestinationSetNextObjectiveStoreKey) o;
+ return (Objects.equals(this.deviceId, that.deviceId) &&
+ Objects.equals(this.ds, that.ds));
+ }
+
+ // The list of destination ids and label are used for comparison.
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Objects.hashCode(this.deviceId)
+ + Objects.hashCode(this.ds);
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Device: " + deviceId + " " + ds;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
new file mode 100644
index 0000000..6891b77
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.storekey;
+
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.DeviceId;
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.Objects;
+
+/**
+ * Key of multicast next objective store.
+ */
+public class McastStoreKey {
+ private final IpAddress mcastIp;
+ private final DeviceId deviceId;
+
+ /**
+ * Constructs the key of multicast next objective store.
+ *
+ * @param mcastIp multicast group IP address
+ * @param deviceId device ID
+ */
+ public McastStoreKey(IpAddress mcastIp, DeviceId deviceId) {
+ checkNotNull(mcastIp, "mcastIp cannot be null");
+ checkNotNull(deviceId, "deviceId cannot be null");
+ checkArgument(mcastIp.isMulticast(), "mcastIp must be a multicast address");
+ this.mcastIp = mcastIp;
+ this.deviceId = deviceId;
+ }
+
+ /**
+ * Returns the multicast IP address of this key.
+ *
+ * @return multicast IP
+ */
+ public IpAddress mcastIp() {
+ return mcastIp;
+ }
+
+ /**
+ * Returns the device ID of this key.
+ *
+ * @return device ID
+ */
+ public DeviceId deviceId() {
+ return deviceId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof McastStoreKey)) {
+ return false;
+ }
+ McastStoreKey that =
+ (McastStoreKey) o;
+ return (Objects.equals(this.mcastIp, that.mcastIp) &&
+ Objects.equals(this.deviceId, that.deviceId));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mcastIp, deviceId);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(getClass())
+ .add("mcastIp", mcastIp)
+ .add("deviceId", deviceId)
+ .toString();
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java
new file mode 100644
index 0000000..0429bd1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.segmentrouting.storekey;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+
+import java.util.Objects;
+
+/**
+ * Key of Device/Port to NextObjective store.
+ *
+ * Since there can be multiple next objectives to the same physical port,
+ * we differentiate between them by including the treatment in the key.
+ */
+public class PortNextObjectiveStoreKey {
+ private final DeviceId deviceId;
+ private final PortNumber portNum;
+ private final TrafficTreatment treatment;
+ private final TrafficSelector meta;
+
+ /**
+ * Constructs the key of port next objective store.
+ *
+ * @param deviceId device ID
+ * @param portNum port number
+ * @param treatment treatment that will be applied to the interface
+ * @param meta optional data to pass to the driver
+ */
+ public PortNextObjectiveStoreKey(DeviceId deviceId, PortNumber portNum,
+ TrafficTreatment treatment,
+ TrafficSelector meta) {
+ this.deviceId = deviceId;
+ this.portNum = portNum;
+ this.treatment = treatment;
+ this.meta = meta;
+ }
+
+ /**
+ * Gets device id in this PortNextObjectiveStoreKey.
+ *
+ * @return device id
+ */
+ public DeviceId deviceId() {
+ return deviceId;
+ }
+
+ /**
+ * Gets port information in this PortNextObjectiveStoreKey.
+ *
+ * @return port information
+ */
+ public PortNumber portNumber() {
+ return portNum;
+ }
+
+ /**
+ * Gets treatment information in this PortNextObjectiveStoreKey.
+ *
+ * @return treatment information
+ */
+ public TrafficTreatment treatment() {
+ return treatment;
+ }
+
+ /**
+ * Gets metadata information in this PortNextObjectiveStoreKey.
+ *
+ * @return meta information
+ */
+ public TrafficSelector meta() {
+ return meta;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PortNextObjectiveStoreKey)) {
+ return false;
+ }
+ PortNextObjectiveStoreKey that =
+ (PortNextObjectiveStoreKey) o;
+ return (Objects.equals(this.deviceId, that.deviceId) &&
+ Objects.equals(this.portNum, that.portNum) &&
+ Objects.equals(this.treatment, that.treatment) &&
+ Objects.equals(this.meta, that.meta));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceId, portNum, treatment, meta);
+ }
+
+ @Override
+ public String toString() {
+ return "Device: " + deviceId + " Port: " + portNum +
+ " Treatment: " + treatment +
+ " Meta: " + meta;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java
new file mode 100644
index 0000000..5d9f2fd
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.storekey;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Key of VLAN to NextObjective store.
+ */
+public class VlanNextObjectiveStoreKey {
+ private final DeviceId deviceId;
+ private final VlanId vlanId;
+
+ /**
+ * Constructs the key of VLAN next objective store.
+ *
+ * @param deviceId device ID
+ * @param vlanId VLAN information
+ */
+ public VlanNextObjectiveStoreKey(DeviceId deviceId,
+ VlanId vlanId) {
+ this.deviceId = deviceId;
+ this.vlanId = vlanId;
+ }
+
+ /**
+ * Gets device id in this VlanNextObjectiveStoreKey.
+ *
+ * @return device id
+ */
+ public DeviceId deviceId() {
+ return this.deviceId;
+ }
+
+ /**
+ * Gets vlan information in this VlanNextObjectiveStoreKey.
+ *
+ * @return vlan id
+ */
+ public VlanId vlanId() {
+ return this.vlanId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof VlanNextObjectiveStoreKey)) {
+ return false;
+ }
+ VlanNextObjectiveStoreKey that =
+ (VlanNextObjectiveStoreKey) o;
+ return (Objects.equals(this.deviceId, that.deviceId) &&
+ Objects.equals(this.vlanId, that.vlanId));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceId, vlanId);
+ }
+
+ @Override
+ public String toString() {
+ return "deviceId: " + deviceId + " vlanId: " + vlanId;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java
new file mode 100644
index 0000000..0e90a22
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.storekey;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Key of VLAN cross-connect next objective store.
+ */
+public class XConnectStoreKey {
+ private final DeviceId deviceId;
+ private final VlanId vlanId;
+
+ /**
+ * Constructs the key of cross-connect next objective store.
+ *
+ * @param deviceId device ID of the VLAN cross-connection
+ * @param vlanId VLAN ID of the VLAN cross-connection
+ */
+ public XConnectStoreKey(DeviceId deviceId, VlanId vlanId) {
+ this.deviceId = deviceId;
+ this.vlanId = vlanId;
+ }
+
+ /**
+ * Returns the device ID of this key.
+ *
+ * @return device ID
+ */
+ public DeviceId deviceId() {
+ return this.deviceId;
+ }
+
+ /**
+ * Returns the VLAN ID of this key.
+ *
+ * @return VLAN ID
+ */
+ public VlanId vlanId() {
+ return this.vlanId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof XConnectStoreKey)) {
+ return false;
+ }
+ XConnectStoreKey that =
+ (XConnectStoreKey) o;
+ return (Objects.equals(this.deviceId, that.deviceId) &&
+ Objects.equals(this.vlanId, that.vlanId));
+ }
+
+ // The list of neighbor ids and label are used for comparison.
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceId, vlanId);
+ }
+
+ @Override
+ public String toString() {
+ return "Device: " + deviceId + " VlanId: " + vlanId;
+ }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java
new file mode 100644
index 0000000..44fc6a7
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Key data structure of various stores used by Segment Routing.
+ */
+package org.onosproject.segmentrouting.storekey;
\ 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
new file mode 100644
index 0000000..cfd7502
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,95 @@
+<!--
+ ~ Copyright 2015-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.
+ -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+ <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+ <!-- XXX revisit when we formally add policies
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.TunnelListCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PolicyListCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PolicyAddCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PolicyRemoveCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.TunnelAddCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.TunnelRemoveCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.RerouteNetworkCommand"/>
+ </command>
+ -->
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.DeviceSubnetListCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.EcmpGraphCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.NextHopCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.VerifyGroupsCommand"/>
+ <completers>
+ <ref component-id="deviceIdCompleter"/>
+ </completers>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PseudowireListCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PseudowireRemoveCommand"/>
+ <completers>
+ <ref component-id="pseudowireIdCompleter"/>
+ </completers>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.PseudowireAddCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.LinkStateCommand"/>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.McastNextListCommand"/>
+ <completers>
+ <ref component-id="mcastGroupCompleter"/>
+ <ref component-id="nullCompleter"/>
+ </completers>
+ </command>
+ <command>
+ <action class="org.onosproject.segmentrouting.cli.McastTreeListCommand"/>
+ <completers>
+ <ref component-id="mcastGroupCompleter"/>
+ <ref component-id="nullCompleter"/>
+ </completers>
+ </command>
+ </command-bundle>
+
+ <bean id="nullCompleter" class="org.apache.karaf.shell.console.completer.NullCompleter"/>
+ <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
+ <bean id="pseudowireIdCompleter" class="org.onosproject.segmentrouting.cli.PseudowireIdCompleter"/>
+ <bean id="mcastGroupCompleter" class="org.onosproject.cli.net.McastGroupCompleter"/>
+
+</blueprint>
+
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java
new file mode 100644
index 0000000..cc214e3
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java
@@ -0,0 +1,53 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An augmented implementation of {@link PortAuthTracker}, so that we can
+ * instrument its behavior for unit test assertions.
+ */
+class AugmentedPortAuthTracker extends PortAuthTracker {
+
+ // instrument blocking flow activity, so we can see when we get hits
+ final List<ConnectPoint> installed = new ArrayList<>();
+ final List<ConnectPoint> cleared = new ArrayList<>();
+
+
+ void resetMetrics() {
+ installed.clear();
+ cleared.clear();
+ }
+
+ @Override
+ void installBlockingFlow(DeviceId d, PortNumber p) {
+ super.installBlockingFlow(d, p);
+ installed.add(new ConnectPoint(d, p));
+ }
+
+ @Override
+ void clearBlockingFlow(DeviceId d, PortNumber p) {
+ super.clearBlockingFlow(d, p);
+ cleared.add(new ConnectPoint(d, p));
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
new file mode 100644
index 0000000..e33a36d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
@@ -0,0 +1,855 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.host.HostLocationProbingService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**r
+ * Unit test for {@link HostHandler}.
+ */
+public class HostHandlerTest {
+ private HostHandler hostHandler;
+
+ // Mocked routing and bridging tables
+ private static final Map<MockBridgingTableKey, MockBridgingTableValue> BRIDGING_TABLE =
+ Maps.newConcurrentMap();
+ private static final Map<MockRoutingTableKey, MockRoutingTableValue> ROUTING_TABLE =
+ Maps.newConcurrentMap();
+ private static final Map<ConnectPoint, Set<IpPrefix>> SUBNET_TABLE = Maps.newConcurrentMap();
+ // Mocked Next Id
+ private static final Map<Integer, TrafficTreatment> NEXT_TABLE = Maps.newConcurrentMap();
+
+ // Host Mac, VLAN
+ private static final ProviderId PROVIDER_ID = ProviderId.NONE;
+ private static final MacAddress HOST_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+ private static final VlanId HOST_VLAN_UNTAGGED = VlanId.NONE;
+ private static final HostId HOST_ID_UNTAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_UNTAGGED);
+ private static final VlanId HOST_VLAN_TAGGED = VlanId.vlanId((short) 20);
+ private static final HostId HOST_ID_TAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_TAGGED);
+ // Host IP
+ private static final IpAddress HOST_IP11 = IpAddress.valueOf("10.0.1.1");
+ private static final IpAddress HOST_IP21 = IpAddress.valueOf("10.0.2.1");
+ private static final IpAddress HOST_IP12 = IpAddress.valueOf("10.0.1.2");
+ private static final IpAddress HOST_IP13 = IpAddress.valueOf("10.0.1.3");
+ private static final IpAddress HOST_IP14 = IpAddress.valueOf("10.0.1.4");
+ private static final IpAddress HOST_IP32 = IpAddress.valueOf("10.0.3.2");
+ // Device
+ private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+ private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+ private static final DeviceId DEV3 = DeviceId.deviceId("of:0000000000000003");
+ private static final DeviceId DEV4 = DeviceId.deviceId("of:0000000000000004");
+ private static final DeviceId DEV5 = DeviceId.deviceId("of:0000000000000005");
+ private static final DeviceId DEV6 = DeviceId.deviceId("of:0000000000000006");
+ // Port
+ private static final PortNumber P1 = PortNumber.portNumber(1);
+ private static final PortNumber P2 = PortNumber.portNumber(2);
+ private static final PortNumber P3 = PortNumber.portNumber(3);
+ private static final PortNumber P9 = PortNumber.portNumber(9);
+ // Connect Point
+ private static final ConnectPoint CP11 = new ConnectPoint(DEV1, P1);
+ private static final HostLocation HOST_LOC11 = new HostLocation(CP11, 0);
+ private static final ConnectPoint CP12 = new ConnectPoint(DEV1, P2);
+ private static final HostLocation HOST_LOC12 = new HostLocation(CP12, 0);
+ private static final ConnectPoint CP13 = new ConnectPoint(DEV1, P3);
+ private static final HostLocation HOST_LOC13 = new HostLocation(CP13, 0);
+ private static final ConnectPoint CP21 = new ConnectPoint(DEV2, P1);
+ private static final HostLocation HOST_LOC21 = new HostLocation(CP21, 0);
+ private static final ConnectPoint CP22 = new ConnectPoint(DEV2, P2);
+ private static final HostLocation HOST_LOC22 = new HostLocation(CP22, 0);
+ // Connect Point for dual-homed host failover
+ private static final ConnectPoint CP31 = new ConnectPoint(DEV3, P1);
+ private static final HostLocation HOST_LOC31 = new HostLocation(CP31, 0);
+ private static final ConnectPoint CP32 = new ConnectPoint(DEV3, P2);
+ private static final HostLocation HOST_LOC32 = new HostLocation(CP32, 0);
+ private static final ConnectPoint CP41 = new ConnectPoint(DEV4, P1);
+ private static final HostLocation HOST_LOC41 = new HostLocation(CP41, 0);
+ private static final ConnectPoint CP39 = new ConnectPoint(DEV3, P9);
+ private static final ConnectPoint CP49 = new ConnectPoint(DEV4, P9);
+ // Conenct Point for mastership test
+ private static final ConnectPoint CP51 = new ConnectPoint(DEV5, P1);
+ private static final HostLocation HOST_LOC51 = new HostLocation(CP51, 0);
+ private static final ConnectPoint CP61 = new ConnectPoint(DEV6, P1);
+ private static final HostLocation HOST_LOC61 = new HostLocation(CP61, 0);
+ // Interface VLAN
+ private static final VlanId INTF_VLAN_UNTAGGED = VlanId.vlanId((short) 10);
+ private static final Set<VlanId> INTF_VLAN_TAGGED = Sets.newHashSet(VlanId.vlanId((short) 20));
+ private static final VlanId INTF_VLAN_NATIVE = VlanId.vlanId((short) 30);
+ private static final Set<VlanId> INTF_VLAN_PAIR = Sets.newHashSet(VlanId.vlanId((short) 10),
+ VlanId.vlanId((short) 20), VlanId.vlanId((short) 30));
+ private static final VlanId INTF_VLAN_OTHER = VlanId.vlanId((short) 40);
+ // Interface subnet
+ private static final IpPrefix INTF_PREFIX1 = IpPrefix.valueOf("10.0.1.254/24");
+ private static final IpPrefix INTF_PREFIX2 = IpPrefix.valueOf("10.0.2.254/24");
+ private static final IpPrefix INTF_PREFIX3 = IpPrefix.valueOf("10.0.3.254/24");
+ private static final InterfaceIpAddress INTF_IP1 =
+ new InterfaceIpAddress(INTF_PREFIX1.address(), INTF_PREFIX1);
+ private static final InterfaceIpAddress INTF_IP2 =
+ new InterfaceIpAddress(INTF_PREFIX2.address(), INTF_PREFIX2);
+ private static final InterfaceIpAddress INTF_IP3 =
+ new InterfaceIpAddress(INTF_PREFIX3.address(), INTF_PREFIX3);
+ // Interfaces
+ private static final Interface INTF11 =
+ new Interface(null, CP11, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF12 =
+ new Interface(null, CP12, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF13 =
+ new Interface(null, CP13, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
+ null, INTF_VLAN_TAGGED, INTF_VLAN_NATIVE);
+ private static final Interface INTF21 =
+ new Interface(null, CP21, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF22 =
+ new Interface(null, CP22, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF31 =
+ new Interface(null, CP31, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF32 =
+ new Interface(null, CP32, Lists.newArrayList(INTF_IP3), MacAddress.NONE, null,
+ INTF_VLAN_OTHER, null, null);
+ private static final Interface INTF39 =
+ new Interface(null, CP39, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ null, INTF_VLAN_PAIR, null);
+ private static final Interface INTF41 =
+ new Interface(null, CP41, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF49 =
+ new Interface(null, CP49, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ null, INTF_VLAN_PAIR, null);
+ // Host
+ private static final Host HOST1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC,
+ HOST_VLAN_UNTAGGED, Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11),
+ false);
+
+ // A set of hosts
+ private static final Set<Host> HOSTS = Sets.newHashSet(HOST1);
+ // A set of devices of which we have mastership
+ private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(DEV1, DEV2, DEV3, DEV4);
+ // A set of interfaces
+ private static final Set<Interface> INTERFACES = Sets.newHashSet(INTF11, INTF12, INTF13, INTF21,
+ INTF22, INTF31, INTF32, INTF39, INTF41, INTF49);
+
+ private MockLocationProbingService mockLocationProbingService;
+
+ @Before
+ public void setUp() throws Exception {
+ // Initialize pairDevice and pairLocalPort config
+ ObjectMapper mapper = new ObjectMapper();
+ ConfigApplyDelegate delegate = config -> { };
+
+ SegmentRoutingDeviceConfig dev3Config = new SegmentRoutingDeviceConfig();
+ JsonNode dev3Tree = mapper.createObjectNode();
+ dev3Config.init(DEV3, "host-handler-test", dev3Tree, mapper, delegate);
+ dev3Config.setPairDeviceId(DEV4).setPairLocalPort(P9);
+
+ SegmentRoutingDeviceConfig dev4Config = new SegmentRoutingDeviceConfig();
+ JsonNode dev4Tree = mapper.createObjectNode();
+ dev4Config.init(DEV4, "host-handler-test", dev4Tree, mapper, delegate);
+ dev4Config.setPairDeviceId(DEV3).setPairLocalPort(P9);
+
+ MockNetworkConfigRegistry mockNetworkConfigRegistry = new MockNetworkConfigRegistry();
+ mockNetworkConfigRegistry.applyConfig(dev3Config);
+ mockNetworkConfigRegistry.applyConfig(dev4Config);
+
+ // Initialize Segment Routing Manager
+ SegmentRoutingManager srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+ srManager.cfgService = new NetworkConfigRegistryAdapter();
+ srManager.deviceConfiguration = new DeviceConfiguration(srManager);
+ srManager.flowObjectiveService = new MockFlowObjectiveService(BRIDGING_TABLE, NEXT_TABLE);
+ srManager.routingRulePopulator = new MockRoutingRulePopulator(srManager, ROUTING_TABLE);
+ srManager.defaultRoutingHandler = new MockDefaultRoutingHandler(srManager, SUBNET_TABLE);
+ srManager.interfaceService = new MockInterfaceService(INTERFACES);
+ srManager.mastershipService = new MockMastershipService(LOCAL_DEVICES);
+ srManager.hostService = new MockHostService(HOSTS);
+ srManager.cfgService = mockNetworkConfigRegistry;
+ mockLocationProbingService = new MockLocationProbingService();
+ srManager.probingService = mockLocationProbingService;
+ srManager.linkHandler = new MockLinkHandler(srManager);
+
+ hostHandler = new HostHandler(srManager);
+
+ ROUTING_TABLE.clear();
+ BRIDGING_TABLE.clear();
+ }
+
+ @Test
+ public void init() throws Exception {
+ hostHandler.init(DEV1);
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ hostHandler.init(DEV2);
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testHostAddedAtWrongLocation() throws Exception {
+ hostHandler.processHostAddedAtLocation(HOST1, HOST_LOC13);
+ }
+
+
+ @Test()
+ public void testHostAddedAtCorrectLocation() throws Exception {
+ hostHandler.processHostAddedAtLocation(HOST1, HOST_LOC11);
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testHostAdded() throws Exception {
+ Host subject;
+
+ // Untagged host discovered on untagged port
+ // Expect: add one routing rule and one bridging rule
+ subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Untagged host discovered on tagged/native port
+ // Expect: add one routing rule and one bridging rule
+ subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_NATIVE)));
+
+ // Tagged host discovered on untagged port
+ // Expect: ignore the host. No rule is added.
+ subject = new DefaultHost(PROVIDER_ID, HOST_ID_TAGGED, HOST_MAC, HOST_VLAN_TAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(2, BRIDGING_TABLE.size());
+
+ // Tagged host discovered on tagged port with the same IP
+ // Expect: update existing route, add one bridging rule
+ subject = new DefaultHost(PROVIDER_ID, HOST_ID_TAGGED, HOST_MAC, HOST_VLAN_TAGGED,
+ Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertEquals(HOST_VLAN_TAGGED, ROUTING_TABLE.get(new MockRoutingTableKey(HOST_LOC13.deviceId(),
+ HOST_IP21.toIpPrefix())).vlanId);
+ assertEquals(3, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, HOST_VLAN_TAGGED)));
+ }
+
+ @Test
+ public void testDualHomedHostAdded() throws Exception {
+ // Add a dual-homed host that has 2 locations
+ // Expect: add two routing rules and two bridging rules
+ Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testSingleHomedHostAddedOnPairLeaf() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC32), Sets.newHashSet(HOST_IP32), false);
+
+ // Add a single-homed host with one location
+ // Expect: the pair link should not be utilized
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP32.toIpPrefix())).portNumber);
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_OTHER)).portNumber);
+ }
+
+ @Test
+ public void testDualHomedHostAddedOneByOne() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a dual-homed host with one location
+ // Expect: the pair link is utilized temporarily before the second location is discovered
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ // Expect probe to be sent out on pair device
+ assertTrue(mockLocationProbingService.verifyProbe(host1, CP41, HostLocationProbingService.ProbeMode.DISCOVER));
+
+ // Add the second location of dual-homed host
+ // Expect: no longer use the pair link
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+ // The permanent solution would be introducing CompletableFuture and wait for it
+ Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testHostRemoved() throws Exception {
+ Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a host
+ // Expect: add one routing rule and one bridging rule
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Remove the host
+ // Expect: add the routing rule and the bridging rule
+ hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, BRIDGING_TABLE.size());
+ }
+
+ @Test
+ public void testDualHomedHostRemoved() throws Exception {
+ // Add a dual-homed host that has 2 locations
+ // Expect: add two routing rules and two bridging rules
+ Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Remove a dual-homed host that has 2 locations
+ // Expect: all routing and bridging rules are removed
+ hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, BRIDGING_TABLE.size());
+ }
+
+ @Test
+ public void testHostMoved() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a host
+ // Expect: add one new routing rule, one new bridging rule
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Move the host to CP13, which has different subnet setting
+ // Expect: remove routing rule. Change vlan in bridging rule.
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host1));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_NATIVE)));
+ assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Move the host to CP21, which has same subnet setting
+ // Expect: add a new routing rule. Change vlan in bridging rule.
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host3));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testDualHomedHostMoved() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC12, HOST_LOC22), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP13, HOST_IP14), false);
+ Host host4 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC22), Sets.newHashSet(HOST_IP12, HOST_IP13), false);
+
+ // Add a host with IP11, IP12 and LOC11, LOC21
+ // Expect: 4 routing rules and 2 bridging rules
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move the host to LOC12, LOC22 and keep the IP
+ // Expect: 4 routing rules and 2 bridging rules all at the new location
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move the host to LOC11, LOC21 and change the IP to IP13, IP14 at the same time
+ // Expect: 4 routing rules and 2 bridging rules all at the new location
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP14.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP14.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move the host to LOC11, LOC22 and change the IP to IP12, IP13 at the same time
+ // Expect: 4 routing rules and 2 bridging rules all at the new location
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host4, host3));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testHostMoveToInvalidLocation() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a host
+ // Expect: add one new routing rule, one new bridging rule
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Move the host to an invalid location
+ // Expect: Old flow is removed. New flow is not created
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, BRIDGING_TABLE.size());
+
+ // Move the host to a valid location
+ // Expect: add one new routing rule, one new bridging rule
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testDualHomedHostMoveToInvalidLocation() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC61, HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a host
+ // Expect: add two new routing rules, two new bridging rules
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move first host location to an invalid location
+ // Expect: One routing and one bridging flow
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move second host location to an invalid location
+ // Expect: No routing or bridging rule
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, BRIDGING_TABLE.size());
+
+ // Move second host location back to a valid location
+ // Expect: One routing and one bridging flow
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host3));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Move first host location back to a valid location
+ // Expect: Two routing and two bridging flow
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testDualHomingSingleLocationFail() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+
+ // Add a host
+ // Expect: add four new routing rules, two new bridging rules
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Host becomes single-homed
+ // Expect: redirect flows from host location to pair link
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Host becomes dual-homed again
+ // Expect: Redirect flows from pair link back to host location
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+ // The permanent solution would be introducing CompletableFuture and wait for it
+ Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testDualHomingBothLocationFail() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+
+ // Add a host
+ // Expect: add four new routing rules, two new bridging rules
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Host becomes single-homed
+ // Expect: redirect flows from host location to pair link
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Host loses both locations
+ // Expect: Remove last location and all previous redirection flows
+ hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, host2));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, BRIDGING_TABLE.size());
+ }
+
+ @Test
+ public void testHostUpdated() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP21), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP12), false);
+
+ // Add a host
+ // Expect: add one new routing rule. Add one new bridging rule.
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(HOST_LOC11.deviceId(), HOST_MAC,
+ INTF_VLAN_UNTAGGED)));
+
+ // Update the host IP to same subnet
+ // Expect: update routing rule with new IP. No change to bridging rule.
+ hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host1));
+ assertEquals(1, ROUTING_TABLE.size());
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Update the host IP to different subnet
+ // Expect: Remove routing rule. No change to bridging rule.
+ hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host3));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(1, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testDelayedIpAndLocation() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a dual-home host with only one location and no IP
+ // Expect: only bridging redirection works
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Discover IP
+ // Expect: routing redirection should also work
+ hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host1));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ // Expect probe to be sent out on pair device
+ assertTrue(mockLocationProbingService.verifyProbe(host2, CP41, HostLocationProbingService.ProbeMode.DISCOVER));
+
+ // Discover location
+ // Expect: cancel all redirections
+ hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+ // The permanent solution would be introducing CompletableFuture and wait for it
+ Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testDelayedIpAndLocation2() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31), Sets.newHashSet(), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+ // Add a dual-home host with only one location and no IP
+ // Expect: only bridging redirection works
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Discover Location
+ // Expect: cancel bridging redirections
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+ // Discover IP
+ // Expect: add IP rules to both location
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host2));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+ }
+
+ @Test
+ public void testDualHomedHostUpdated() throws Exception {
+ Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+ Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP21), false);
+ Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP13, HOST_IP14), false);
+
+ // Add a dual-homed host with two locations and two IPs
+ // Expect: add four new routing rules. Add two new bridging rules
+ hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Update both host IPs
+ // Expect: update routing rules with new IP. No change to bridging rule.
+ hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host1));
+ assertEquals(4, ROUTING_TABLE.size());
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP14.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP14.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+ // Update one of the host IP to different subnet
+ // Expect: update routing rule with new IP. No change to bridging rule.
+ hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host3));
+ assertEquals(2, ROUTING_TABLE.size());
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+ assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP21.toIpPrefix())));
+ assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+ assertEquals(2, BRIDGING_TABLE.size());
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+ }
+
+ @Test
+ public void testBridgingFwdObjBuilder() throws Exception {
+ assertNotNull(hostHandler.bridgingFwdObjBuilder(DEV2, HOST_MAC, HOST_VLAN_UNTAGGED, P1, false));
+ assertNull(hostHandler.bridgingFwdObjBuilder(DEV2, HOST_MAC, HOST_VLAN_UNTAGGED, P3, false));
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.java
new file mode 100644
index 0000000..a90b1cf
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.java
@@ -0,0 +1,57 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Mock Bridging Table Key.
+ */
+class MockBridgingTableKey {
+ DeviceId deviceId;
+ MacAddress macAddress;
+ VlanId vlanId;
+
+ MockBridgingTableKey(DeviceId deviceId, MacAddress macAddress, VlanId vlanId) {
+ this.deviceId = deviceId;
+ this.macAddress = macAddress;
+ this.vlanId = vlanId;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MockBridgingTableKey)) {
+ return false;
+ }
+ final MockBridgingTableKey other = (MockBridgingTableKey) obj;
+ return Objects.equals(this.macAddress, other.macAddress) &&
+ Objects.equals(this.deviceId, other.deviceId) &&
+ Objects.equals(this.vlanId, other.vlanId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(macAddress, vlanId);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java
new file mode 100644
index 0000000..68b0db8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java
@@ -0,0 +1,52 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+/**
+ * Mock Bridging Table Value.
+ */
+class MockBridgingTableValue {
+ boolean popVlan;
+ PortNumber portNumber;
+
+ MockBridgingTableValue(boolean popVlan, PortNumber portNumber) {
+ this.popVlan = popVlan;
+ this.portNumber = portNumber;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MockBridgingTableValue)) {
+ return false;
+ }
+ final MockBridgingTableValue other = (MockBridgingTableValue) obj;
+ return Objects.equals(this.popVlan, other.popVlan) &&
+ Objects.equals(this.portNumber, other.portNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(popVlan, portNumber);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java
new file mode 100644
index 0000000..ec07238
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Mock Default Routing Handler.
+ */
+public class MockDefaultRoutingHandler extends DefaultRoutingHandler {
+ private Map<ConnectPoint, Set<IpPrefix>> subnetTable;
+
+ MockDefaultRoutingHandler(SegmentRoutingManager srManager,
+ Map<ConnectPoint, Set<IpPrefix>> subnetTable) {
+ super(srManager);
+ this.subnetTable = subnetTable;
+ }
+
+ @Override
+ protected void populateSubnet(Set<ConnectPoint> cpts, Set<IpPrefix> subnets) {
+ subnetTable.forEach((k, v) -> {
+ if (!cpts.contains(k)) {
+ subnetTable.get(k).removeAll(subnets);
+ if (subnetTable.get(k).isEmpty()) {
+ subnetTable.remove(k);
+ }
+ }
+ });
+
+ cpts.forEach(cpt -> subnetTable.put(cpt, subnets));
+ }
+
+ @Override
+ protected boolean revokeSubnet(Set<IpPrefix> subnets) {
+ for (Map.Entry<ConnectPoint, Set<IpPrefix>> entry : subnetTable.entrySet()) {
+ entry.getValue().removeAll(subnets);
+ if (entry.getValue().isEmpty()) {
+ subnetTable.remove(entry.getKey());
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java
new file mode 100644
index 0000000..0abbf2a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.segmentrouting;
+
+import org.onosproject.net.Annotations;
+import org.onosproject.net.DefaultDevice;
+import org.onosproject.net.DeviceId;
+/**
+ * Test fixture for the device service.
+ */
+public class MockDevice extends DefaultDevice {
+
+ public MockDevice(DeviceId id, Annotations annotations) {
+ super(null, id, null, null, null, null, null, null, annotations);
+ }
+ }
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java
new file mode 100644
index 0000000..1d173f6
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.segmentrouting;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultPort;
+import org.onosproject.net.Port;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceServiceAdapter;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Test fixture for the device service.
+ */
+public class MockDeviceService extends DeviceServiceAdapter {
+ private List<Device> devices = new LinkedList<>();
+ private DeviceListener listener;
+
+ public void addDevice(Device dev) {
+ devices.add(dev);
+ }
+
+ public void addMultipleDevices(Set<Device> devicesToAdd) {
+ devicesToAdd.forEach(dev -> devices.add(dev));
+ }
+
+ @Override
+ public Device getDevice(DeviceId deviceId) {
+ for (Device dev : devices) {
+ if (dev.id().equals(deviceId)) {
+ return dev;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Port getPort(ConnectPoint cp) {
+ return new DefaultPort(null, null, false);
+ }
+
+ @Override
+ public Iterable<Device> getAvailableDevices() {
+ return devices;
+ }
+
+ @Override
+ public void addListener(DeviceListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Get the listener.
+ */
+ public DeviceListener getListener() {
+ return listener;
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java
new file mode 100644
index 0000000..138b98a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.EthCriterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction;
+import org.onosproject.net.flowobjective.FlowObjectiveServiceAdapter;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+
+import java.util.Map;
+
+/**
+ * Mock Flow Objective Service.
+ */
+public class MockFlowObjectiveService extends FlowObjectiveServiceAdapter {
+ private Map<MockBridgingTableKey, MockBridgingTableValue> bridgingTable;
+ private Map<Integer, TrafficTreatment> nextTable;
+
+ MockFlowObjectiveService(Map<MockBridgingTableKey, MockBridgingTableValue> bridgingTable,
+ Map<Integer, TrafficTreatment> nextTable) {
+ this.bridgingTable = bridgingTable;
+ this.nextTable = nextTable;
+ }
+
+ @Override
+ public void forward(DeviceId deviceId, ForwardingObjective forwardingObjective) {
+ TrafficSelector selector = forwardingObjective.selector();
+ TrafficTreatment treatment = nextTable.get(forwardingObjective.nextId());
+ MacAddress macAddress = ((EthCriterion) selector.getCriterion(Criterion.Type.ETH_DST)).mac();
+ VlanId vlanId = ((VlanIdCriterion) selector.getCriterion(Criterion.Type.VLAN_VID)).vlanId();
+
+ boolean popVlan = treatment.allInstructions().stream()
+ .filter(instruction -> instruction.type().equals(Instruction.Type.L2MODIFICATION))
+ .anyMatch(instruction -> ((L2ModificationInstruction) instruction).subtype()
+ .equals(L2ModificationInstruction.L2SubType.VLAN_POP));
+ PortNumber portNumber = treatment.allInstructions().stream()
+ .filter(instruction -> instruction.type().equals(Instruction.Type.OUTPUT))
+ .map(instruction -> ((Instructions.OutputInstruction) instruction).port()).findFirst().orElse(null);
+ if (portNumber == null) {
+ throw new IllegalArgumentException();
+ }
+
+ Objective.Operation op = forwardingObjective.op();
+
+ MockBridgingTableKey btKey = new MockBridgingTableKey(deviceId, macAddress, vlanId);
+ MockBridgingTableValue btValue = new MockBridgingTableValue(popVlan, portNumber);
+
+ if (op.equals(Objective.Operation.ADD)) {
+ bridgingTable.put(btKey, btValue);
+ } else if (op.equals(Objective.Operation.REMOVE)) {
+ bridgingTable.remove(btKey, btValue);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java
new file mode 100644
index 0000000..e8c4701
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java
@@ -0,0 +1,45 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.host.HostServiceAdapter;
+
+import java.util.Set;
+
+/**
+ * Mock Host Service.
+ */
+public class MockHostService extends HostServiceAdapter {
+ private Set<Host> hosts;
+
+ MockHostService(Set<Host> hosts) {
+ this.hosts = ImmutableSet.copyOf(hosts);
+ }
+
+ @Override
+ public Set<Host> getHosts() {
+ return hosts;
+ }
+
+ @Override
+ public Host getHost(HostId hostId) {
+ return hosts.stream().filter(host -> hostId.equals(host.id())).findFirst().orElse(null);
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java
new file mode 100644
index 0000000..f8d04ca
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceServiceAdapter;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Mock Interface Service.
+ */
+public class MockInterfaceService extends InterfaceServiceAdapter {
+ private Set<Interface> interfaces;
+
+ MockInterfaceService(Set<Interface> interfaces) {
+ this.interfaces = ImmutableSet.copyOf(interfaces);
+ }
+
+ @Override
+ public Set<Interface> getInterfacesByPort(ConnectPoint cp) {
+ return interfaces.stream().filter(intf -> cp.equals(intf.connectPoint()))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<Interface> getInterfaces() {
+ return interfaces;
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java
new file mode 100644
index 0000000..2819c9d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onosproject.net.HostLocation;
+
+/**
+ * Mocks the LinkHandler in SR.
+ *
+ */
+public class MockLinkHandler extends LinkHandler {
+
+ MockLinkHandler(SegmentRoutingManager srManager) {
+ super(srManager, null);
+ }
+
+ @Override
+ void checkUplinksForDualHomedHosts(HostLocation loc) {
+ // currently does nothing - can be extended to be a useful mock when
+ // implementing unit tests for link handling
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.java
new file mode 100644
index 0000000..b0cdf43
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.java
@@ -0,0 +1,74 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.Lists;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Host;
+import org.onosproject.net.host.HostLocationProbingService;
+
+import java.util.List;
+import java.util.Objects;
+
+public class MockLocationProbingService implements HostLocationProbingService {
+ List<Probe> probes;
+
+ private class Probe {
+ private Host host;
+ private ConnectPoint connectPoint;
+ private ProbeMode probeMode;
+
+ Probe(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+ this.host = host;
+ this.connectPoint = connectPoint;
+ this.probeMode = probeMode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Probe)) {
+ return false;
+ }
+ Probe that = (Probe) o;
+ return (Objects.equals(this.host, that.host) &&
+ Objects.equals(this.connectPoint, that.connectPoint) &&
+ Objects.equals(this.probeMode, that.probeMode));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(host, connectPoint, probeMode);
+ }
+ }
+
+ MockLocationProbingService() {
+ probes = Lists.newArrayList();
+ }
+
+ boolean verifyProbe(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+ Probe probe = new Probe(host, connectPoint, probeMode);
+ return probes.contains(probe);
+ }
+
+ @Override
+ public void probeHostLocation(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+ probes.add(new Probe(host, connectPoint, probeMode));
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.java
new file mode 100644
index 0000000..5494e27
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.java
@@ -0,0 +1,40 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.mastership.MastershipServiceAdapter;
+import org.onosproject.net.DeviceId;
+
+import java.util.Set;
+
+/**
+ * Mock Mastership Service.
+ */
+public class MockMastershipService extends MastershipServiceAdapter {
+ // A set of devices of which we have mastership.
+ private Set<DeviceId> localDevices;
+
+ MockMastershipService(Set<DeviceId> localDevices) {
+ this.localDevices = ImmutableSet.copyOf(localDevices);
+ }
+
+ @Override
+ public boolean isLocalMaster(DeviceId deviceId) {
+ return localDevices.contains(deviceId);
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java
new file mode 100644
index 0000000..09f8da2
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java
@@ -0,0 +1,43 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.Sets;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+
+import java.util.Set;
+
+/**
+ * Mock Network Config Registry.
+ */
+class MockNetworkConfigRegistry extends NetworkConfigRegistryAdapter {
+ private Set<Config> configs = Sets.newHashSet();
+
+ public void applyConfig(Config config) {
+ configs.add(config);
+ }
+
+ @Override
+ public <S, C extends Config<S>> C getConfig(S subject, Class<C> configClass) {
+ Config c = configs.stream()
+ .filter(config -> subject.equals(config.subject()))
+ .filter(config -> configClass.equals(config.getClass()))
+ .findFirst().orElse(null);
+ return (C) c;
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java
new file mode 100644
index 0000000..7927636
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.google.common.collect.Sets;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteInfo;
+import org.onosproject.routeservice.RouteServiceAdapter;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Mock Route Service.
+ * We assume there is only one routing table named "default".
+ */
+public class MockRouteService extends RouteServiceAdapter {
+ private Map<MockRoutingTableKey, MockRoutingTableValue> routingTable;
+
+ MockRouteService(Map<MockRoutingTableKey, MockRoutingTableValue> routingTable) {
+ this.routingTable = routingTable;
+ }
+
+ @Override
+ public Collection<RouteInfo> getRoutes(RouteTableId id) {
+ return routingTable.entrySet().stream().map(e -> {
+ IpPrefix prefix = e.getKey().ipPrefix;
+ IpAddress nextHop = IpAddress.valueOf(0); // dummy
+ MacAddress mac = e.getValue().macAddress;
+ VlanId vlan = e.getValue().vlanId;
+ Route route = new Route(Route.Source.STATIC, prefix, nextHop);
+ ResolvedRoute rr = new ResolvedRoute(route, mac, vlan);
+
+ return new RouteInfo(prefix, rr, Sets.newHashSet(rr));
+ }).collect(Collectors.toSet());
+ }
+
+ @Override
+ public Collection<RouteTableId> getRouteTables() {
+ return Sets.newHashSet(new RouteTableId("default"));
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java
new file mode 100644
index 0000000..4eab7c0
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java
@@ -0,0 +1,54 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Map;
+
+/**
+ * Mock Routing Rule Populator.
+ */
+public class MockRoutingRulePopulator extends RoutingRulePopulator {
+ private Map<MockRoutingTableKey, MockRoutingTableValue> routingTable;
+
+ MockRoutingRulePopulator(SegmentRoutingManager srManager,
+ Map<MockRoutingTableKey, MockRoutingTableValue> routingTable) {
+ super(srManager);
+ this.routingTable = routingTable;
+ }
+
+ @Override
+ public void populateRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ MockRoutingTableKey rtKey = new MockRoutingTableKey(deviceId, prefix);
+ MockRoutingTableValue rtValue = new MockRoutingTableValue(outPort, hostMac, hostVlanId);
+ routingTable.put(rtKey, rtValue);
+ }
+
+ @Override
+ public void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+ MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+ MockRoutingTableKey rtKey = new MockRoutingTableKey(deviceId, prefix);
+ MockRoutingTableValue rtValue = new MockRoutingTableValue(outPort, hostMac, hostVlanId);
+ routingTable.remove(rtKey, rtValue);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java
new file mode 100644
index 0000000..4f09c2a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Mock Routing Table Key.
+ */
+class MockRoutingTableKey {
+ DeviceId deviceId;
+ IpPrefix ipPrefix;
+
+ MockRoutingTableKey(DeviceId deviceId, IpPrefix ipPrefix) {
+ this.deviceId = deviceId;
+ this.ipPrefix = ipPrefix;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MockRoutingTableKey)) {
+ return false;
+ }
+ final MockRoutingTableKey other = (MockRoutingTableKey) obj;
+ return Objects.equals(this.deviceId, other.deviceId) &&
+ Objects.equals(this.ipPrefix, other.ipPrefix);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceId, ipPrefix);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.java
new file mode 100644
index 0000000..5842d2d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.java
@@ -0,0 +1,58 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+/**
+ * Mock Routing Table Value.
+ */
+class MockRoutingTableValue {
+ PortNumber portNumber;
+ MacAddress macAddress;
+ VlanId vlanId;
+
+ MockRoutingTableValue(PortNumber portNumber, MacAddress macAddress, VlanId vlanId) {
+ this.portNumber = portNumber;
+ this.macAddress = macAddress;
+ this.vlanId = vlanId;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MockRoutingTableValue)) {
+ return false;
+ }
+ final MockRoutingTableValue other = (MockRoutingTableValue) obj;
+ return Objects.equals(this.portNumber, other.portNumber) &&
+ Objects.equals(this.macAddress, other.macAddress) &&
+ Objects.equals(this.vlanId, other.vlanId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(portNumber, macAddress, vlanId);
+ }
+}
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
new file mode 100644
index 0000000..ad098b4
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
@@ -0,0 +1,49 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Mock Segment Routing Manager.
+ */
+public class MockSegmentRoutingManager extends SegmentRoutingManager {
+ private Map<Integer, TrafficTreatment> nextTable;
+ private AtomicInteger atomicNextId = new AtomicInteger();
+
+ MockSegmentRoutingManager(Map<Integer, TrafficTreatment> nextTable) {
+ appId = new DefaultApplicationId(1, SegmentRoutingManager.APP_NAME);
+ this.nextTable = nextTable;
+ }
+
+ @Override
+ public int getPortNextObjectiveId(DeviceId deviceId, PortNumber portNum,
+ TrafficTreatment treatment,
+ TrafficSelector meta,
+ boolean createIfMissing) {
+ int nextId = atomicNextId.incrementAndGet();
+ nextTable.put(nextId, treatment);
+ return nextId;
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java
new file mode 100644
index 0000000..6ab4bad
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.PortAuthTracker.BlockState;
+import org.onosproject.segmentrouting.config.BlockedPortsConfig;
+import org.onosproject.segmentrouting.config.BlockedPortsConfigTest;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.net.ConnectPoint.deviceConnectPoint;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.AUTHENTICATED;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.BLOCKED;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.UNCHECKED;
+
+/**
+ * Unit Tests for {@link PortAuthTracker}.
+ */
+public class PortAuthTrackerTest {
+ private static final ApplicationId APP_ID = new DefaultApplicationId(1, "foo");
+ private static final String KEY = "blocked";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final String PATH_CFG = "/blocked-ports.json";
+ private static final String PATH_CFG_ALT = "/blocked-ports-alt.json";
+
+ private static final String DEV1 = "of:0000000000000001";
+ private static final String DEV3 = "of:0000000000000003";
+ private static final String DEV4 = "of:0000000000000004";
+
+ private BlockedPortsConfig cfg;
+ private AugmentedPortAuthTracker tracker;
+
+ private void print(String s) {
+ System.out.println(s);
+ }
+
+ private void print(Object o) {
+ print(o.toString());
+ }
+
+ private void print(String fmt, Object... params) {
+ print(String.format(fmt, params));
+ }
+
+ private void title(String s) {
+ print("=== %s ===", s);
+ }
+
+ private BlockedPortsConfig makeConfig(String path) throws IOException {
+ InputStream blockedPortsJson = BlockedPortsConfigTest.class
+ .getResourceAsStream(path);
+ JsonNode node = MAPPER.readTree(blockedPortsJson);
+ BlockedPortsConfig cfg = new BlockedPortsConfig();
+ cfg.init(APP_ID, KEY, node, MAPPER, null);
+ return cfg;
+ }
+
+ ConnectPoint cp(String devId, int port) {
+ return ConnectPoint.deviceConnectPoint(devId + "/" + port);
+ }
+
+ @Before
+ public void setUp() throws IOException {
+ cfg = makeConfig(PATH_CFG);
+ tracker = new AugmentedPortAuthTracker();
+ }
+
+ private void verifyPortState(String devId, int first, BlockState... states) {
+ DeviceId dev = deviceId(devId);
+ int last = first + states.length;
+ int pn = first;
+ int i = 0;
+ while (pn < last) {
+ PortNumber pnum = portNumber(pn);
+ BlockState actual = tracker.currentState(dev, pnum);
+ print("%s/%s [%s] --> %s", devId, pn, states[i], actual);
+ assertEquals("oops: " + devId + "/" + pn + "~" + actual,
+ states[i], actual);
+ pn++;
+ i++;
+ }
+ }
+
+ @Test
+ public void basic() {
+ title("basic");
+ print(tracker);
+ print(cfg);
+
+ assertEquals("wrong entry count", 0, tracker.entryCount());
+
+ // let's assume that the net config just got loaded..
+ tracker.configurePortBlocking(cfg);
+ assertEquals("wrong entry count", 13, tracker.entryCount());
+
+ verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+ verifyPortState(DEV1, 6, UNCHECKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+
+ verifyPortState(DEV3, 1, UNCHECKED, UNCHECKED, UNCHECKED);
+ verifyPortState(DEV3, 6, UNCHECKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+
+ verifyPortState(DEV4, 1, BLOCKED, UNCHECKED, UNCHECKED, UNCHECKED, BLOCKED);
+ }
+
+ @Test
+ public void logonLogoff() {
+ title("logonLogoff");
+
+ tracker.configurePortBlocking(cfg);
+ assertEquals("wrong entry count", 13, tracker.entryCount());
+ verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED);
+
+ ConnectPoint cp = deviceConnectPoint(DEV1 + "/2");
+ tracker.radiusAuthorize(cp);
+ print("");
+ verifyPortState(DEV1, 1, BLOCKED, AUTHENTICATED, BLOCKED);
+
+ tracker.radiusLogoff(cp);
+ print("");
+ verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED);
+ }
+
+ @Test
+ public void installedFlows() {
+ title("installed flows");
+
+ assertEquals(0, tracker.installed.size());
+ tracker.configurePortBlocking(cfg);
+ assertEquals(13, tracker.installed.size());
+
+ assertTrue(tracker.installed.contains(cp(DEV1, 1)));
+ assertTrue(tracker.installed.contains(cp(DEV3, 7)));
+ assertTrue(tracker.installed.contains(cp(DEV4, 5)));
+ }
+
+ @Test
+ public void flowsLogonLogoff() {
+ title("flows logon logoff");
+
+ tracker.configurePortBlocking(cfg);
+
+ // let's pick a connect point from the configuration
+ ConnectPoint cp = cp(DEV4, 5);
+
+ assertTrue(tracker.installed.contains(cp));
+ assertEquals(0, tracker.cleared.size());
+
+ tracker.resetMetrics();
+ tracker.radiusAuthorize(cp);
+ // verify we requested the blocking flow to be cleared
+ assertTrue(tracker.cleared.contains(cp));
+
+ tracker.resetMetrics();
+ assertEquals(0, tracker.installed.size());
+ tracker.radiusLogoff(cp);
+ // verify we requested the blocking flow to be reinstated
+ assertTrue(tracker.installed.contains(cp));
+ }
+
+ @Test
+ public void uncheckedPortIgnored() {
+ title("unchecked port ignored");
+
+ tracker.configurePortBlocking(cfg);
+ tracker.resetMetrics();
+
+ // let's pick a connect point NOT in the configuration
+ ConnectPoint cp = cp(DEV4, 2);
+ assertEquals(BlockState.UNCHECKED, tracker.currentState(cp));
+
+ assertEquals(0, tracker.installed.size());
+ assertEquals(0, tracker.cleared.size());
+ tracker.radiusAuthorize(cp);
+ assertEquals(0, tracker.installed.size());
+ assertEquals(0, tracker.cleared.size());
+ tracker.radiusLogoff(cp);
+ assertEquals(0, tracker.installed.size());
+ assertEquals(0, tracker.cleared.size());
+ }
+
+ @Test
+ public void reconfiguration() throws IOException {
+ title("reconfiguration");
+
+ /* see 'blocked-ports.json' and 'blocked-ports-alt.json'
+
+ cfg: "1": ["1-4", "7-9"],
+ "3": ["7-9"],
+ "4": ["1", "5", "9"]
+
+ alt: "1": ["1-9"],
+ "3": ["7"],
+ "4": ["1"]
+ */
+ tracker.configurePortBlocking(cfg);
+ // dev1: ports 5 and 6 are NOT configured in the original CFG
+ assertFalse(tracker.installed.contains(cp(DEV1, 5)));
+ assertFalse(tracker.installed.contains(cp(DEV1, 6)));
+
+ tracker.resetMetrics();
+ assertEquals(0, tracker.installed.size());
+ assertEquals(0, tracker.cleared.size());
+
+ BlockedPortsConfig alt = makeConfig(PATH_CFG_ALT);
+ tracker.configurePortBlocking(alt);
+
+ // dev1: ports 5 and 6 ARE configured in the alternate CFG
+ assertTrue(tracker.installed.contains(cp(DEV1, 5)));
+ assertTrue(tracker.installed.contains(cp(DEV1, 6)));
+
+ // also, check for the ports that were decommissioned
+ assertTrue(tracker.cleared.contains(cp(DEV3, 8)));
+ assertTrue(tracker.cleared.contains(cp(DEV3, 9)));
+ assertTrue(tracker.cleared.contains(cp(DEV4, 5)));
+ assertTrue(tracker.cleared.contains(cp(DEV4, 9)));
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java
new file mode 100644
index 0000000..23d1233
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Mode;
+
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for class {@link PwaasConfig}.
+ */
+public class PwaasConfigTest {
+
+ private static final String TUNNEL_ID_1 = "1";
+ private static final String TUNNEL_ID_2 = "20";
+ private static final String NOT_PRESENT_TUNNEL_ID = "2";
+ private static final ConnectPoint INGRESS_1 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+ private static final ConnectPoint INGRESS_2 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+ private static final ConnectPoint EGRESS_1 = ConnectPoint.deviceConnectPoint("of:0000000000000002/1");
+ private static final ConnectPoint EGRESS_2 = ConnectPoint.deviceConnectPoint("of:0000000000000002/1");
+ private static final VlanId INGRESS_INNER_TAG_1 = VlanId.vlanId("10");
+ private static final VlanId INGRESS_INNER_TAG_2 = VlanId.vlanId("100");
+ private static final VlanId INGRESS_OUTER_TAG_1 = VlanId.vlanId("20");
+ private static final VlanId INGRESS_OUTER_TAG_2 = VlanId.vlanId("200");
+ private static final VlanId EGRESS_INNER_TAG_1 = VlanId.vlanId("10");
+ private static final VlanId EGRESS_INNER_TAG_2 = VlanId.vlanId("100");
+ private static final VlanId EGRESS_OUTER_TAG_1 = VlanId.vlanId("21");
+ private static final VlanId EGRESS_OUTER_TAG_2 = VlanId.vlanId("210");
+ private static final String MODE_1 = "RAW";
+ private static final String MODE_2 = "RAW";
+ private static final VlanId SD_TAG_1 = VlanId.NONE;
+ private static final VlanId SD_TAG_2 = VlanId.NONE;
+ private static final MplsLabel PW_LABEL_1 = MplsLabel.mplsLabel("255");
+ private static final MplsLabel PW_LABEL_2 = MplsLabel.mplsLabel("1255");
+
+ /*
+ * Configuration below copied from host handler test.
+ */
+
+ // Host Mac, VLAN
+ private static final ProviderId PROVIDER_ID = ProviderId.NONE;
+ private static final MacAddress HOST_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+ private static final VlanId HOST_VLAN_UNTAGGED = VlanId.NONE;
+ private static final HostId HOST_ID_UNTAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_UNTAGGED);
+ private static final VlanId HOST_VLAN_TAGGED = VlanId.vlanId((short) 20);
+ private static final HostId HOST_ID_TAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_TAGGED);
+ // Host IP
+ private static final IpAddress HOST_IP11 = IpAddress.valueOf("10.0.1.1");
+ private static final IpAddress HOST_IP21 = IpAddress.valueOf("10.0.2.1");
+ private static final IpAddress HOST_IP12 = IpAddress.valueOf("10.0.1.2");
+ private static final IpAddress HOST_IP13 = IpAddress.valueOf("10.0.1.3");
+ private static final IpAddress HOST_IP14 = IpAddress.valueOf("10.0.1.4");
+ private static final IpAddress HOST_IP32 = IpAddress.valueOf("10.0.3.2");
+ // Device
+ private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+ private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+ private static final DeviceId DEV3 = DeviceId.deviceId("of:0000000000000003");
+ private static final DeviceId DEV4 = DeviceId.deviceId("of:0000000000000004");
+ private static final DeviceId DEV5 = DeviceId.deviceId("of:0000000000000005");
+ private static final DeviceId DEV6 = DeviceId.deviceId("of:0000000000000006");
+ // Port
+ private static final PortNumber P1 = PortNumber.portNumber(1);
+ private static final PortNumber P2 = PortNumber.portNumber(2);
+ private static final PortNumber P3 = PortNumber.portNumber(3);
+ private static final PortNumber P9 = PortNumber.portNumber(9);
+ // Connect Point
+ private static final ConnectPoint CP11 = new ConnectPoint(DEV1, P1);
+ private static final HostLocation HOST_LOC11 = new HostLocation(CP11, 0);
+ private static final ConnectPoint CP12 = new ConnectPoint(DEV1, P2);
+ private static final HostLocation HOST_LOC12 = new HostLocation(CP12, 0);
+ private static final ConnectPoint CP13 = new ConnectPoint(DEV1, P3);
+ private static final HostLocation HOST_LOC13 = new HostLocation(CP13, 0);
+ private static final ConnectPoint CP21 = new ConnectPoint(DEV2, P1);
+ private static final HostLocation HOST_LOC21 = new HostLocation(CP21, 0);
+ private static final ConnectPoint CP22 = new ConnectPoint(DEV2, P2);
+ private static final HostLocation HOST_LOC22 = new HostLocation(CP22, 0);
+ // Connect Point for dual-homed host failover
+ private static final ConnectPoint CP31 = new ConnectPoint(DEV3, P1);
+ private static final HostLocation HOST_LOC31 = new HostLocation(CP31, 0);
+ private static final ConnectPoint CP32 = new ConnectPoint(DEV3, P2);
+ private static final HostLocation HOST_LOC32 = new HostLocation(CP32, 0);
+ private static final ConnectPoint CP41 = new ConnectPoint(DEV4, P1);
+ private static final HostLocation HOST_LOC41 = new HostLocation(CP41, 0);
+ private static final ConnectPoint CP39 = new ConnectPoint(DEV3, P9);
+ private static final ConnectPoint CP49 = new ConnectPoint(DEV4, P9);
+ // Conenct Point for mastership test
+ private static final ConnectPoint CP51 = new ConnectPoint(DEV5, P1);
+ private static final HostLocation HOST_LOC51 = new HostLocation(CP51, 0);
+ private static final ConnectPoint CP61 = new ConnectPoint(DEV6, P1);
+ private static final HostLocation HOST_LOC61 = new HostLocation(CP61, 0);
+ // Interface VLAN
+ private static final VlanId INTF_VLAN_UNTAGGED = VlanId.vlanId((short) 10);
+ private static final Set<VlanId> INTF_VLAN_TAGGED = Sets.newHashSet(VlanId.vlanId((short) 20));
+ private static final VlanId INTF_VLAN_NATIVE = VlanId.vlanId((short) 30);
+ private static final Set<VlanId> INTF_VLAN_PAIR = Sets.newHashSet(VlanId.vlanId((short) 10),
+ VlanId.vlanId((short) 20), VlanId.vlanId((short) 30));
+ private static final VlanId INTF_VLAN_OTHER = VlanId.vlanId((short) 40);
+ // Interface subnet
+ private static final IpPrefix INTF_PREFIX1 = IpPrefix.valueOf("10.0.1.254/24");
+ private static final IpPrefix INTF_PREFIX2 = IpPrefix.valueOf("10.0.2.254/24");
+ private static final IpPrefix INTF_PREFIX3 = IpPrefix.valueOf("10.0.3.254/24");
+ private static final InterfaceIpAddress INTF_IP1 =
+ new InterfaceIpAddress(INTF_PREFIX1.address(), INTF_PREFIX1);
+ private static final InterfaceIpAddress INTF_IP2 =
+ new InterfaceIpAddress(INTF_PREFIX2.address(), INTF_PREFIX2);
+ private static final InterfaceIpAddress INTF_IP3 =
+ new InterfaceIpAddress(INTF_PREFIX3.address(), INTF_PREFIX3);
+ // Interfaces
+ private static final Interface INTF11 =
+ new Interface(null, CP11, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF12 =
+ new Interface(null, CP12, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF13 =
+ new Interface(null, CP13, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
+ null, INTF_VLAN_TAGGED, INTF_VLAN_NATIVE);
+ private static final Interface INTF21 =
+ new Interface(null, CP21, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF22 =
+ new Interface(null, CP22, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF31 =
+ new Interface(null, CP31, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF32 =
+ new Interface(null, CP32, Lists.newArrayList(INTF_IP3), MacAddress.NONE, null,
+ INTF_VLAN_OTHER, null, null);
+ private static final Interface INTF39 =
+ new Interface(null, CP39, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ null, INTF_VLAN_PAIR, null);
+ private static final Interface INTF41 =
+ new Interface(null, CP41, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ INTF_VLAN_UNTAGGED, null, null);
+ private static final Interface INTF49 =
+ new Interface(null, CP49, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+ null, INTF_VLAN_PAIR, null);
+ // Host
+ private static final Host HOST1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC,
+ HOST_VLAN_UNTAGGED,
+ Sets.newHashSet(HOST_LOC11, HOST_LOC21),
+ Sets.newHashSet(HOST_IP11),
+ false);
+
+ // A set of hosts
+ private static final Set<Host> HOSTS = Sets.newHashSet(HOST1);
+ // A set of devices of which we have mastership
+ private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(DEV1, DEV2, DEV3, DEV4);
+ // A set of interfaces
+ private static final Set<Interface> INTERFACES = Sets.newHashSet(INTF11, INTF12, INTF13, INTF21,
+ INTF22, INTF31, INTF32, INTF39, INTF41, INTF49);
+
+ private PwaasConfig config;
+ private PwaasConfig invalidConfigVlan;
+ private PwaasConfig invalidConfigMode;
+ private PwaasConfig invalidConfigLabel;
+ private PwaasConfig invalidConfigConflictingVlan;
+
+ @Before
+ public void setUp() throws Exception {
+ InputStream jsonStream = PwaasConfig.class
+ .getResourceAsStream("/pwaas.json");
+ InputStream jsonStreamInvalid1 = PwaasConfig.class
+ .getResourceAsStream("/pwaas-invalid-mode.json");
+ InputStream jsonStreamInvalid2 = PwaasConfig.class
+ .getResourceAsStream("/pwaas-invalid-pwlabel.json");
+ InputStream jsonStreamInvalid3 = PwaasConfig.class
+ .getResourceAsStream("/pwaas-invalid-vlan.json");
+ InputStream jsonStreamInvalid4 = PwaasConfig.class
+ .getResourceAsStream("/pwaas-conflicting-vlan.json");
+
+ String key = SegmentRoutingManager.APP_NAME;
+ ApplicationId subject = new TestApplicationId(key);
+ ObjectMapper mapper = new ObjectMapper();
+
+ JsonNode jsonNode = mapper.readTree(jsonStream);
+ JsonNode jsonNodeInvalid1 = mapper.readTree(jsonStreamInvalid1);
+ JsonNode jsonNodeInvalid2 = mapper.readTree(jsonStreamInvalid2);
+ JsonNode jsonNodeInvalid3 = mapper.readTree(jsonStreamInvalid3);
+ JsonNode jsonNodeInvalid4 = mapper.readTree(jsonStreamInvalid4);
+
+ ConfigApplyDelegate delegate = new MockDelegate();
+
+ DeviceService devService = new MockDeviceService();
+ InterfaceService infService = new MockInterfaceService(INTERFACES);
+
+ // create two devices and add them
+ DefaultAnnotations.Builder builderDev1 = DefaultAnnotations.builder();
+ DefaultAnnotations.Builder builderDev2 = DefaultAnnotations.builder();
+
+ Device dev1 = new MockDevice(DEV1, builderDev1.build());
+ Device dev2 = new MockDevice(DEV2, builderDev2.build());
+ ((MockDeviceService) devService).addDevice(dev1);
+ ((MockDeviceService) devService).addDevice(dev2);
+
+ config = new PwaasConfig(devService, infService);
+ invalidConfigVlan = new PwaasConfig(devService, infService);
+ invalidConfigMode = new PwaasConfig(devService, infService);
+ invalidConfigLabel = new PwaasConfig(devService, infService);
+ invalidConfigConflictingVlan = new PwaasConfig(devService, infService);
+
+ config.init(subject, key, jsonNode, mapper, delegate);
+ invalidConfigVlan.init(subject, key, jsonNodeInvalid1, mapper, delegate);
+ invalidConfigMode.init(subject, key, jsonNodeInvalid2, mapper, delegate);
+ invalidConfigLabel.init(subject, key, jsonNodeInvalid3, mapper, delegate);
+ invalidConfigConflictingVlan.init(subject, key, jsonNodeInvalid4, mapper, delegate);
+
+ config.deviceService = devService;
+ config.intfService = infService;
+
+ invalidConfigVlan.deviceService = devService;
+ invalidConfigVlan.intfService = infService;
+
+ invalidConfigLabel.deviceService = devService;
+ invalidConfigLabel.intfService = infService;
+
+ invalidConfigMode.deviceService = devService;
+ invalidConfigMode.intfService = infService;
+
+ invalidConfigConflictingVlan.deviceService = devService;
+ invalidConfigConflictingVlan.intfService = infService;
+ }
+
+ /**
+ * Tests config validity.
+ */
+ @Test
+ public void testIsValid() {
+ try {
+ assertTrue(config.isValid());
+ } catch (IllegalArgumentException e) {
+ assertTrue(false);
+ }
+ }
+
+ /**
+ * Tests config in-validity.
+ */
+ @Test
+ public void testValid1() {
+ assertFalse(invalidConfigVlan.isValid());
+ }
+
+ @Test
+ public void testValid2() {
+ assertFalse(invalidConfigMode.isValid());
+ }
+
+ @Test
+ public void testValid3() {
+ assertFalse(invalidConfigLabel.isValid());
+ }
+
+ @Test
+ public void testValid4() {
+ assertFalse(invalidConfigConflictingVlan.isValid());
+ }
+
+ /**
+ * Tests getPwIds.
+ */
+ @Test
+ public void testGetPwIds() {
+ Set<Long> pwIds = config.getPwIds();
+
+ assertThat(pwIds.size(), is(2));
+ assertTrue(pwIds.contains(Long.parseLong(TUNNEL_ID_1)));
+ assertTrue(pwIds.contains(Long.parseLong(TUNNEL_ID_2)));
+ assertFalse(pwIds.contains(Long.parseLong(NOT_PRESENT_TUNNEL_ID)));
+ }
+
+ /**
+ * Tests getPwDescription.
+ */
+ @Test
+ public void testGetPwDescription() {
+ L2TunnelDescription l2TunnelDescription = null;
+
+ L2Tunnel l2Tunnel = new DefaultL2Tunnel(
+ L2Mode.valueOf(MODE_1),
+ SD_TAG_1,
+ Long.parseLong(TUNNEL_ID_1),
+ PW_LABEL_1
+ );
+ L2TunnelPolicy l2TunnelPolicy = new DefaultL2TunnelPolicy(
+ Long.parseLong(TUNNEL_ID_1),
+ INGRESS_1,
+ INGRESS_INNER_TAG_1,
+ INGRESS_OUTER_TAG_1,
+ EGRESS_1,
+ EGRESS_INNER_TAG_1,
+ EGRESS_OUTER_TAG_1
+ );
+
+ l2TunnelDescription = config.getPwDescription(Long.parseLong(TUNNEL_ID_1));
+ assertThat(l2TunnelDescription.l2Tunnel().pwMode(), is(l2Tunnel.pwMode()));
+ assertThat(l2TunnelDescription.l2Tunnel().sdTag(), is(l2Tunnel.sdTag()));
+ assertThat(l2TunnelDescription.l2Tunnel().tunnelId(), is(l2Tunnel.tunnelId()));
+ assertThat(l2TunnelDescription.l2Tunnel().pwLabel(), is(l2Tunnel.pwLabel()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().tunnelId(), is(l2TunnelPolicy.tunnelId()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1InnerTag(), is(l2TunnelPolicy.cP1InnerTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1OuterTag(), is(l2TunnelPolicy.cP1OuterTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2InnerTag(), is(l2TunnelPolicy.cP2InnerTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1(), is(l2TunnelPolicy.cP1()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2(), is(l2TunnelPolicy.cP2()));
+
+ l2Tunnel = new DefaultL2Tunnel(
+ L2Mode.valueOf(MODE_2),
+ SD_TAG_2,
+ Long.parseLong(TUNNEL_ID_2),
+ PW_LABEL_2
+ );
+ l2TunnelPolicy = new DefaultL2TunnelPolicy(
+ Long.parseLong(TUNNEL_ID_2),
+ INGRESS_2,
+ INGRESS_INNER_TAG_2,
+ INGRESS_OUTER_TAG_2,
+ EGRESS_2,
+ EGRESS_INNER_TAG_2,
+ EGRESS_OUTER_TAG_2
+ );
+
+ l2TunnelDescription = config.getPwDescription(Long.parseLong(TUNNEL_ID_2));
+ assertThat(l2TunnelDescription.l2Tunnel().pwMode(), is(l2Tunnel.pwMode()));
+ assertThat(l2TunnelDescription.l2Tunnel().sdTag(), is(l2Tunnel.sdTag()));
+ assertThat(l2TunnelDescription.l2Tunnel().tunnelId(), is(l2Tunnel.tunnelId()));
+ assertThat(l2TunnelDescription.l2Tunnel().pwLabel(), is(l2Tunnel.pwLabel()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().tunnelId(), is(l2TunnelPolicy.tunnelId()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1InnerTag(), is(l2TunnelPolicy.cP1InnerTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1OuterTag(), is(l2TunnelPolicy.cP1OuterTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP1(), is(l2TunnelPolicy.cP1()));
+ assertThat(l2TunnelDescription.l2TunnelPolicy().cP2(), is(l2TunnelPolicy.cP2()));
+ }
+
+ private class MockDelegate implements ConfigApplyDelegate {
+ @Override
+ public void onApply(Config config) {
+ }
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
new file mode 100644
index 0000000..843ba6b
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.onosproject.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.easymock.EasyMock.reset;
+import static org.junit.Assert.*;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * Unit test for {@link RouteHandler}.
+ */
+public class RouteHandlerTest {
+ private SegmentRoutingManager srManager;
+ private RouteHandler routeHandler;
+ private HostService hostService;
+
+ // Mocked routing and bridging tables
+ private static final Map<MockBridgingTableKey, MockBridgingTableValue> BRIDGING_TABLE =
+ Maps.newConcurrentMap();
+ private static final Map<MockRoutingTableKey, MockRoutingTableValue> ROUTING_TABLE =
+ Maps.newConcurrentMap();
+ private static final Map<ConnectPoint, Set<IpPrefix>> SUBNET_TABLE = Maps.newConcurrentMap();
+ // Mocked Next Id
+ private static final Map<Integer, TrafficTreatment> NEXT_TABLE = Maps.newConcurrentMap();
+
+ private static final IpPrefix P1 = IpPrefix.valueOf("10.0.0.0/24");
+
+ // Single homed router 1
+ private static final IpAddress N1 = IpAddress.valueOf("10.0.1.254");
+ private static final MacAddress M1 = MacAddress.valueOf("00:00:00:00:00:01");
+ private static final VlanId V1 = VlanId.vlanId((short) 1);
+ private static final ConnectPoint CP1 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+ private static final Route R1 = new Route(Route.Source.STATIC, P1, N1);
+ private static final ResolvedRoute RR1 = new ResolvedRoute(R1, M1, V1);
+
+ // Single homed router 2
+ private static final IpAddress N2 = IpAddress.valueOf("10.0.2.254");
+ private static final MacAddress M2 = MacAddress.valueOf("00:00:00:00:00:02");
+ private static final VlanId V2 = VlanId.vlanId((short) 2);
+ private static final ConnectPoint CP2 = ConnectPoint.deviceConnectPoint("of:0000000000000002/2");
+ private static final Route R2 = new Route(Route.Source.STATIC, P1, N2);
+ private static final ResolvedRoute RR2 = new ResolvedRoute(R2, M2, V2);
+
+ // Dual homed router 1
+ private static final IpAddress N3 = IpAddress.valueOf("10.0.3.254");
+ private static final MacAddress M3 = MacAddress.valueOf("00:00:00:00:00:03");
+ private static final VlanId V3 = VlanId.vlanId((short) 3);
+ private static final Route R3 = new Route(Route.Source.STATIC, P1, N3);
+ private static final ResolvedRoute RR3 = new ResolvedRoute(R3, M3, V3);
+
+ // Hosts
+ private static final Host H1 = new DefaultHost(ProviderId.NONE, HostId.hostId(M1, V1), M1, V1,
+ Sets.newHashSet(new HostLocation(CP1, 0)), Sets.newHashSet(N1), false);
+ private static final Host H2 = new DefaultHost(ProviderId.NONE, HostId.hostId(M2, V2), M2, V2,
+ Sets.newHashSet(new HostLocation(CP2, 0)), Sets.newHashSet(N2), false);
+ private static final Host H3D = new DefaultHost(ProviderId.NONE, HostId.hostId(M3, V3), M3, V3,
+ Sets.newHashSet(new HostLocation(CP1, 0), new HostLocation(CP2, 0)), Sets.newHashSet(N3), false);
+ private static final Host H3S = new DefaultHost(ProviderId.NONE, HostId.hostId(M3, V3), M3, V3,
+ Sets.newHashSet(new HostLocation(CP1, 0)), Sets.newHashSet(N3), false);
+
+ // Pair Local Port
+ private static final PortNumber P9 = PortNumber.portNumber(9);
+
+ // A set of hosts
+ private static final Set<Host> HOSTS = Sets.newHashSet(H1, H2, H3D);
+ private static final Set<Host> HOSTS_ONE_FAIL = Sets.newHashSet(H1, H2, H3S);
+ private static final Set<Host> HOSTS_BOTH_FAIL = Sets.newHashSet(H1, H2);
+ // A set of devices of which we have mastership
+ private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(CP1.deviceId(), CP2.deviceId());
+ // A set of interfaces
+ private static final Set<Interface> INTERFACES = Sets.newHashSet();
+
+ @Before
+ public void setUp() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ ConfigApplyDelegate delegate = config -> { };
+
+ SegmentRoutingDeviceConfig dev1Config = new SegmentRoutingDeviceConfig();
+ JsonNode dev1Tree = mapper.createObjectNode();
+ dev1Config.init(CP1.deviceId(), "host-handler-test", dev1Tree, mapper, delegate);
+ dev1Config.setPairDeviceId(CP2.deviceId()).setPairLocalPort(P9);
+
+ SegmentRoutingDeviceConfig dev2Config = new SegmentRoutingDeviceConfig();
+ JsonNode dev2Tree = mapper.createObjectNode();
+ dev2Config.init(CP2.deviceId(), "host-handler-test", dev2Tree, mapper, delegate);
+ dev2Config.setPairDeviceId(CP1.deviceId()).setPairLocalPort(P9);
+
+ MockNetworkConfigRegistry mockNetworkConfigRegistry = new MockNetworkConfigRegistry();
+ mockNetworkConfigRegistry.applyConfig(dev1Config);
+ mockNetworkConfigRegistry.applyConfig(dev2Config);
+
+ // Initialize Segment Routing Manager
+ srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+ srManager.cfgService = new NetworkConfigRegistryAdapter();
+ srManager.deviceConfiguration = createMock(DeviceConfiguration.class);
+ srManager.flowObjectiveService = new MockFlowObjectiveService(BRIDGING_TABLE, NEXT_TABLE);
+ srManager.routingRulePopulator = new MockRoutingRulePopulator(srManager, ROUTING_TABLE);
+ srManager.defaultRoutingHandler = new MockDefaultRoutingHandler(srManager, SUBNET_TABLE);
+ srManager.interfaceService = new MockInterfaceService(INTERFACES);
+ srManager.mastershipService = new MockMastershipService(LOCAL_DEVICES);
+ hostService = new MockHostService(HOSTS);
+ srManager.hostService = hostService;
+ srManager.cfgService = mockNetworkConfigRegistry;
+ srManager.routeService = new MockRouteService(ROUTING_TABLE);
+
+ routeHandler = new RouteHandler(srManager) {
+ // routeEventCache is not necessary for unit tests
+ @Override
+ void enqueueRouteEvent(RouteEvent routeEvent) {
+ dequeueRouteEvent(routeEvent);
+ }
+ };
+
+ ROUTING_TABLE.clear();
+ BRIDGING_TABLE.clear();
+ SUBNET_TABLE.clear();
+ }
+
+ @Test
+ public void init() throws Exception {
+ MockRoutingTableKey rtk = new MockRoutingTableKey(CP1.deviceId(), P1);
+ MockRoutingTableValue rtv = new MockRoutingTableValue(CP1.port(), M1, V1);
+ ROUTING_TABLE.put(rtk, rtv);
+
+ routeHandler.init(CP1.deviceId());
+
+ assertEquals(1, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ assertEquals(M1, rtv1.macAddress);
+ assertEquals(V1, rtv1.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+
+ assertEquals(1, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+ }
+
+ @Test
+ public void processRouteAdded() throws Exception {
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.addSubnet(CP1, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR1, Sets.newHashSet(RR1));
+ routeHandler.processRouteAdded(re);
+
+ assertEquals(1, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ assertEquals(M1, rtv1.macAddress);
+ assertEquals(V1, rtv1.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+
+ assertEquals(1, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void processRouteUpdated() throws Exception {
+ processRouteAdded();
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.addSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_UPDATED, RR2, RR1, Sets.newHashSet(RR2),
+ Sets.newHashSet(RR1));
+ routeHandler.processRouteUpdated(re);
+
+ // Note: We shouldn't remove the old nexthop during the occasion of route update
+ // since the populate subnet will take care of it and point it to an ECMP group
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+ assertEquals(M2, rtv2.macAddress);
+ assertEquals(V2, rtv2.vlanId);
+ assertEquals(CP2.port(), rtv2.portNumber);
+
+ assertEquals(1, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void processRouteRemoved() throws Exception {
+ processRouteAdded();
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP1, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR1, Sets.newHashSet(RR1));
+ routeHandler.processRouteRemoved(re);
+
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, SUBNET_TABLE.size());
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testTwoSingleHomedAdded() throws Exception {
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.addSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.addSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR1, Sets.newHashSet(RR1, RR2));
+ routeHandler.processRouteAdded(re);
+
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+ assertEquals(M1, rtv1.macAddress);
+ assertEquals(M2, rtv2.macAddress);
+ assertEquals(V1, rtv1.vlanId);
+ assertEquals(V2, rtv2.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+ assertEquals(CP2.port(), rtv2.portNumber);
+
+ assertEquals(2, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+ assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testOneDualHomedAdded() throws Exception {
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.addSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.addSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR3, Sets.newHashSet(RR3));
+ routeHandler.processRouteAdded(re);
+
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+ assertEquals(M3, rtv1.macAddress);
+ assertEquals(M3, rtv2.macAddress);
+ assertEquals(V3, rtv1.vlanId);
+ assertEquals(V3, rtv2.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+ assertEquals(CP2.port(), rtv2.portNumber);
+
+ assertEquals(2, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+ assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testOneSingleHomedToTwoSingleHomed() throws Exception {
+ processRouteAdded();
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.addSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED, RR1, null,
+ Sets.newHashSet(RR1, RR2), Sets.newHashSet(RR1));
+ routeHandler.processAlternativeRoutesChanged(re);
+
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+ assertEquals(M1, rtv1.macAddress);
+ assertEquals(M2, rtv2.macAddress);
+ assertEquals(V1, rtv1.vlanId);
+ assertEquals(V2, rtv2.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+ assertEquals(CP2.port(), rtv2.portNumber);
+
+ assertEquals(2, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+ assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testTwoSingleHomedToOneSingleHomed() throws Exception {
+ testTwoSingleHomedAdded();
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED, RR1, null,
+ Sets.newHashSet(RR1), Sets.newHashSet(RR1, RR2));
+ routeHandler.processAlternativeRoutesChanged(re);
+
+ // Note: We shouldn't remove the old nexthop during the occasion of route update
+ // since the populate subnet will take care of it and point it to an ECMP group
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ assertEquals(M1, rtv1.macAddress);
+ assertEquals(V1, rtv1.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+
+ assertEquals(1, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ // TODO Add test cases for two single homed next hop at same location
+
+ @Test
+ public void testDualHomedSingleLocationFail() throws Exception {
+ testOneDualHomedAdded();
+
+ HostEvent he = new HostEvent(HostEvent.Type.HOST_MOVED, H3S, H3D);
+ routeHandler.processHostMovedEvent(he);
+
+ assertEquals(2, ROUTING_TABLE.size());
+ MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+ MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+ assertEquals(M3, rtv1.macAddress);
+ assertEquals(M3, rtv2.macAddress);
+ assertEquals(V3, rtv1.vlanId);
+ assertEquals(V3, rtv2.vlanId);
+ assertEquals(CP1.port(), rtv1.portNumber);
+ assertEquals(P9, rtv2.portNumber);
+
+ // ECMP route table hasn't changed
+ assertEquals(2, SUBNET_TABLE.size());
+ assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+ assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+ }
+
+ @Test
+ public void testDualHomedBothLocationFail() throws Exception {
+ testDualHomedSingleLocationFail();
+
+ hostService = new MockHostService(HOSTS_ONE_FAIL);
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.removeSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+ routeHandler.processRouteRemoved(re);
+
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, SUBNET_TABLE.size());
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testTwoSingleHomedRemoved() throws Exception {
+ testTwoSingleHomedAdded();
+
+ hostService = new MockHostService(HOSTS_BOTH_FAIL);
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.removeSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR1, Sets.newHashSet(RR1, RR2));
+ routeHandler.processRouteRemoved(re);
+
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, SUBNET_TABLE.size());
+
+ verify(srManager.deviceConfiguration);
+ }
+
+ @Test
+ public void testOneDualHomeRemoved() throws Exception {
+ testOneDualHomedAdded();
+
+ reset(srManager.deviceConfiguration);
+ srManager.deviceConfiguration.removeSubnet(CP1, P1);
+ expectLastCall().once();
+ srManager.deviceConfiguration.removeSubnet(CP2, P1);
+ expectLastCall().once();
+ replay(srManager.deviceConfiguration);
+
+ RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+ routeHandler.processRouteRemoved(re);
+
+ assertEquals(0, ROUTING_TABLE.size());
+ assertEquals(0, SUBNET_TABLE.size());
+
+ verify(srManager.deviceConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java
new file mode 100644
index 0000000..e457896
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link BlockedPortsConfig}.
+ */
+public class BlockedPortsConfigTest {
+
+ private static final ApplicationId APP_ID = new DefaultApplicationId(1, "foo");
+ private static final String KEY = "blocked";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static final String DEV1 = "of:0000000000000001";
+ private static final String DEV2 = "of:0000000000000002";
+ private static final String DEV3 = "of:0000000000000003";
+ private static final String DEV4 = "of:0000000000000004";
+ private static final String RANGE_14 = "1-4";
+ private static final String RANGE_79 = "7-9";
+ private static final String P1 = "1";
+ private static final String P5 = "5";
+ private static final String P9 = "9";
+
+ private BlockedPortsConfig cfg;
+ private BlockedPortsConfig.Range range;
+
+ private void print(String s) {
+ System.out.println(s);
+ }
+
+ private void print(Object o) {
+ print(o.toString());
+ }
+
+ @Before
+ public void setUp() throws IOException {
+ InputStream blockedPortsJson = BlockedPortsConfigTest.class
+ .getResourceAsStream("/blocked-ports.json");
+ JsonNode node = MAPPER.readTree(blockedPortsJson);
+ cfg = new BlockedPortsConfig();
+ cfg.init(APP_ID, KEY, node, MAPPER, null);
+ }
+
+ @Test
+ public void basic() {
+ cfg = new BlockedPortsConfig();
+ print(cfg);
+
+ assertEquals("non-empty devices list", 0, cfg.deviceIds().size());
+ assertEquals("non-empty port-ranges list", 0, cfg.portRanges("non-exist").size());
+ }
+
+
+ @Test
+ public void overIteratePort() {
+ Iterator<Long> iterator = cfg.portIterator(DEV3);
+ while (iterator.hasNext()) {
+ print(iterator.next());
+ }
+
+ try {
+ print(iterator.next());
+ fail("NoSuchElement exception NOT thrown");
+ } catch (NoSuchElementException e) {
+ print("<good> " + e);
+ }
+ }
+
+ @Test
+ public void overIterateRange() {
+ range = new BlockedPortsConfig.Range("4-6");
+
+ Iterator<Long> iterator = range.iterator();
+ while (iterator.hasNext()) {
+ print(iterator.next());
+ }
+
+ try {
+ print(iterator.next());
+ fail("NoSuchElement exception NOT thrown");
+ } catch (NoSuchElementException e) {
+ print("<good> " + e);
+ }
+ }
+
+
+ @Test
+ public void simple() {
+ List<String> devIds = cfg.deviceIds();
+ print(devIds);
+ assertEquals("wrong dev id count", 3, devIds.size());
+ assertEquals("missing dev 1", true, devIds.contains(DEV1));
+ assertEquals("dev 2??", false, devIds.contains(DEV2));
+ assertEquals("missing dev 3", true, devIds.contains(DEV3));
+
+ List<String> d1ranges = cfg.portRanges(DEV1);
+ print(d1ranges);
+ assertEquals("wrong d1 range count", 2, d1ranges.size());
+ assertEquals("missing 1-4", true, d1ranges.contains(RANGE_14));
+ assertEquals("missing 7-9", true, d1ranges.contains(RANGE_79));
+
+ List<String> d2ranges = cfg.portRanges(DEV2);
+ print(d2ranges);
+ assertEquals("wrong d2 range count", 0, d2ranges.size());
+
+ List<String> d3ranges = cfg.portRanges(DEV3);
+ print(d3ranges);
+ assertEquals("wrong d3 range count", 1, d3ranges.size());
+ assertEquals("range 1-4?", false, d3ranges.contains(RANGE_14));
+ assertEquals("missing 7-9", true, d3ranges.contains(RANGE_79));
+ }
+
+
+ private void verifyPorts(List<Long> ports, long... exp) {
+ assertEquals("Wrong port count", exp.length, ports.size());
+ for (long e : exp) {
+ assertEquals("missing port", true, ports.contains(e));
+ }
+ }
+
+ private void verifyPortIterator(String devid, long... exp) {
+ List<Long> ports = new ArrayList<>();
+ Iterator<Long> iter = cfg.portIterator(devid);
+ iter.forEachRemaining(ports::add);
+ print(ports);
+ verifyPorts(ports, exp);
+ }
+
+ @Test
+ public void rangeIterators() {
+ verifyPortIterator(DEV1, 1, 2, 3, 4, 7, 8, 9);
+ verifyPortIterator(DEV2);
+ verifyPortIterator(DEV3, 7, 8, 9);
+ }
+
+ @Test
+ public void singlePorts() {
+ List<String> devIds = cfg.deviceIds();
+ print(devIds);
+ assertEquals("wrong dev id count", 3, devIds.size());
+ assertEquals("missing dev 4", true, devIds.contains(DEV4));
+
+ List<String> d1ranges = cfg.portRanges(DEV4);
+ print(d1ranges);
+ assertEquals("wrong d4 range count", 3, d1ranges.size());
+ assertEquals("missing 1", true, d1ranges.contains(P1));
+ assertEquals("missing 5", true, d1ranges.contains(P5));
+ assertEquals("missing 9", true, d1ranges.contains(P9));
+
+ verifyPortIterator(DEV4, 1, 5, 9);
+ }
+
+
+ // test Range inner class
+
+ @Test
+ public void rangeBadFormat() {
+ try {
+ range = new BlockedPortsConfig.Range("not-a-range-format");
+ fail("no exception thrown");
+ } catch (IllegalArgumentException iar) {
+ print(iar);
+ assertEquals("wrong msg", "Bad Range Format not-a-range-format", iar.getMessage());
+ }
+ }
+
+ @Test
+ public void rangeBadHi() {
+ try {
+ range = new BlockedPortsConfig.Range("2-nine");
+ fail("no exception thrown");
+ } catch (IllegalArgumentException iar) {
+ print(iar);
+ assertEquals("wrong msg", "Bad Range Format 2-nine", iar.getMessage());
+ }
+ }
+
+ @Test
+ public void rangeHiLessThanLo() {
+ try {
+ range = new BlockedPortsConfig.Range("9-5");
+ fail("no exception thrown");
+ } catch (IllegalArgumentException iar) {
+ print(iar);
+ assertEquals("wrong msg", "Bad Range Format 9-5", iar.getMessage());
+ }
+ }
+
+ @Test
+ public void rangeNegative() {
+ try {
+ range = new BlockedPortsConfig.Range("-2-4");
+ fail("no exception thrown");
+ } catch (IllegalArgumentException iar) {
+ print(iar);
+ assertEquals("wrong msg", "Bad Range Format -2-4", iar.getMessage());
+ }
+ }
+
+ @Test
+ public void rangeGood() {
+ range = new BlockedPortsConfig.Range("100-104");
+ List<Long> values = new ArrayList<>();
+ range.iterator().forEachRemaining(values::add);
+ print(values);
+ verifyPorts(values, 100, 101, 102, 103, 104);
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java
new file mode 100644
index 0000000..72b1ef5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for class {@link SegmentRoutingAppConfig}.
+ */
+public class SegmentRoutingAppConfigTest {
+ private SegmentRoutingAppConfig config;
+ private SegmentRoutingAppConfig invalidConfig;
+ private SegmentRoutingAppConfig mplsEcmpConfig;
+
+ private static final MacAddress ROUTER_MAC_1 = MacAddress.valueOf("00:00:00:00:00:01");
+ private static final MacAddress ROUTER_MAC_2 = MacAddress.valueOf("00:00:00:00:00:02");
+ private static final MacAddress ROUTER_MAC_3 = MacAddress.valueOf("00:00:00:00:00:03");
+ private static final ConnectPoint PORT_1 = ConnectPoint.deviceConnectPoint("of:1/1");
+ private static final ConnectPoint PORT_2 = ConnectPoint.deviceConnectPoint("of:1/2");
+ private static final ConnectPoint PORT_3 = ConnectPoint.deviceConnectPoint("of:1/3");
+ private static final DeviceId VROUTER_ID_1 = DeviceId.deviceId("of:1");
+ private static final DeviceId VROUTER_ID_2 = DeviceId.deviceId("of:2");
+ private static final String PROVIDER_1 = "org.onosproject.provider.host";
+ private static final String PROVIDER_2 = "org.onosproject.netcfghost";
+ private static final String PROVIDER_3 = "org.onosproject.anotherprovider";
+
+ /**
+ * Initialize test related variables.
+ *
+ * @throws Exception
+ */
+ @Before
+ public void setUp() throws Exception {
+ InputStream jsonStream = SegmentRoutingAppConfigTest.class
+ .getResourceAsStream("/app.json");
+ InputStream invalidJsonStream = SegmentRoutingAppConfigTest.class
+ .getResourceAsStream("/app-invalid.json");
+ InputStream mplsEcmpJsonStream = SegmentRoutingAppConfigTest.class
+ .getResourceAsStream("/app-ecmp.json");
+
+ String key = SegmentRoutingManager.APP_NAME;
+ ApplicationId subject = new TestApplicationId(key);
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonStream);
+ JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+ JsonNode mplsEcmpJsonNode = mapper.readTree(mplsEcmpJsonStream);
+ ConfigApplyDelegate delegate = new MockDelegate();
+
+ config = new SegmentRoutingAppConfig();
+ config.init(subject, key, jsonNode, mapper, delegate);
+ invalidConfig = new SegmentRoutingAppConfig();
+ invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+ mplsEcmpConfig = new SegmentRoutingAppConfig();
+ mplsEcmpConfig.init(subject, key, mplsEcmpJsonNode, mapper, delegate);
+ }
+
+ /**
+ * Tests config validity.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testIsValid() throws Exception {
+ assertTrue(config.isValid());
+ assertFalse(invalidConfig.isValid());
+ assertTrue(mplsEcmpConfig.isValid());
+ }
+
+ /**
+ * Test MPLS-ECMP default getter. By-default
+ * MPLS-ECMPS is false.
+ */
+ @Test
+ public void testDefaultMplsEcmp() {
+ boolean mplsEcmp = config.mplsEcmp();
+ assertThat(mplsEcmp, is(false));
+ }
+
+ /**
+ * Test MPLS-ECMP getter.
+ */
+ @Test
+ public void testMplsEcmp() {
+ boolean mplsEcmp = mplsEcmpConfig.mplsEcmp();
+ assertThat(mplsEcmp, is(true));
+ }
+
+ /**
+ * Test MPLS-ECMP setter.
+ */
+ @Test
+ public void testSetMplsEcmp() {
+ /*
+ * In the config the value is not set.
+ */
+ boolean mplsEcmp = config.mplsEcmp();
+ assertThat(mplsEcmp, is(false));
+ config.setMplsEcmp(true);
+ mplsEcmp = config.mplsEcmp();
+ assertThat(mplsEcmp, is(true));
+ /*
+ * In the mplsEcmpConfig the value is true,
+ */
+ mplsEcmp = mplsEcmpConfig.mplsEcmp();
+ assertThat(mplsEcmp, is(true));
+ config.setMplsEcmp(false);
+ mplsEcmp = config.mplsEcmp();
+ assertThat(mplsEcmp, is(false));
+ }
+
+ /**
+ * Tests vRouterMacs getter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testVRouterMacs() throws Exception {
+ Set<MacAddress> vRouterMacs = config.vRouterMacs();
+ assertNotNull("vRouterMacs should not be null", vRouterMacs);
+ assertThat(vRouterMacs.size(), is(2));
+ assertTrue(vRouterMacs.contains(ROUTER_MAC_1));
+ assertTrue(vRouterMacs.contains(ROUTER_MAC_2));
+ }
+
+ /**
+ * Tests vRouterMacs setter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSetVRouterMacs() throws Exception {
+ ImmutableSet.Builder<MacAddress> builder = ImmutableSet.builder();
+ builder.add(ROUTER_MAC_3);
+ config.setVRouterMacs(builder.build());
+
+ Set<MacAddress> vRouterMacs = config.vRouterMacs();
+ assertThat(vRouterMacs.size(), is(1));
+ assertTrue(vRouterMacs.contains(ROUTER_MAC_3));
+ }
+
+ /**
+ * Tests suppressSubnet getter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSuppressSubnet() throws Exception {
+ Set<ConnectPoint> suppressSubnet = config.suppressSubnet();
+ assertNotNull("suppressSubnet should not be null", suppressSubnet);
+ assertThat(suppressSubnet.size(), is(2));
+ assertTrue(suppressSubnet.contains(PORT_1));
+ assertTrue(suppressSubnet.contains(PORT_2));
+ }
+
+ /**
+ * Tests suppressSubnet setter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSetSuppressSubnet() throws Exception {
+ ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+ builder.add(PORT_3);
+ config.setSuppressSubnet(builder.build());
+
+ Set<ConnectPoint> suppressSubnet = config.suppressSubnet();
+ assertNotNull("suppressSubnet should not be null", suppressSubnet);
+ assertThat(suppressSubnet.size(), is(1));
+ assertTrue(suppressSubnet.contains(PORT_3));
+ }
+
+ /**
+ * Tests suppressHostByPort getter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSuppressHostByPort() throws Exception {
+ Set<ConnectPoint> suppressHostByPort = config.suppressHostByPort();
+ assertNotNull("suppressHostByPort should not be null", suppressHostByPort);
+ assertThat(suppressHostByPort.size(), is(2));
+ assertTrue(suppressHostByPort.contains(PORT_1));
+ assertTrue(suppressHostByPort.contains(PORT_2));
+ }
+
+ /**
+ * Tests suppressHostByPort setter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSetSuppressHostByPort() throws Exception {
+ ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+ builder.add(PORT_3);
+ config.setSuppressHostByPort(builder.build());
+
+ Set<ConnectPoint> suppressHostByPort = config.suppressHostByPort();
+ assertNotNull("suppressHostByPort should not be null", suppressHostByPort);
+ assertThat(suppressHostByPort.size(), is(1));
+ assertTrue(suppressHostByPort.contains(PORT_3));
+ }
+
+ /**
+ * Tests suppressHostByProvider getter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSuppressHostByProvider() throws Exception {
+ Set<String> supprsuppressHostByProvider = config.suppressHostByProvider();
+ assertNotNull("suppressHostByProvider should not be null", supprsuppressHostByProvider);
+ assertThat(supprsuppressHostByProvider.size(), is(2));
+ assertTrue(supprsuppressHostByProvider.contains(PROVIDER_1));
+ assertTrue(supprsuppressHostByProvider.contains(PROVIDER_2));
+ }
+
+ /**
+ * Tests suppressHostByProvider setter.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSetHostLearning() throws Exception {
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ builder.add(PROVIDER_3);
+ config.setSuppressHostByProvider(builder.build());
+
+ Set<String> supprsuppressHostByProvider = config.suppressHostByProvider();
+ assertNotNull("suppressHostByProvider should not be null", supprsuppressHostByProvider);
+ assertThat(supprsuppressHostByProvider.size(), is(1));
+ assertTrue(supprsuppressHostByProvider.contains(PROVIDER_3));
+ }
+
+ private class MockDelegate implements ConfigApplyDelegate {
+ @Override
+ public void onApply(Config config) {
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java
new file mode 100644
index 0000000..ddf90bb
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for class {@link SegmentRoutingDeviceConfig}.
+ */
+public class SegmentRoutingDeviceConfigTest {
+ private SegmentRoutingDeviceConfig config;
+ private SegmentRoutingDeviceConfig ipv6Config;
+ private SegmentRoutingDeviceConfig pairConfig;
+ private SegmentRoutingDeviceConfig invalidConfig;
+ private Map<Integer, Set<Integer>> adjacencySids1;
+ private Map<Integer, Set<Integer>> adjacencySids2;
+ private static final DeviceId PAIR_DEVICE_ID = DeviceId.deviceId("of:123456789ABCDEF0");
+ private static final PortNumber PAIR_LOCAL_PORT = PortNumber.portNumber(10);
+
+ @Before
+ public void setUp() throws Exception {
+ InputStream jsonStream = SegmentRoutingDeviceConfigTest.class
+ .getResourceAsStream("/device.json");
+ InputStream ipv6JsonStream = SegmentRoutingDeviceConfigTest.class
+ .getResourceAsStream("/device-ipv6.json");
+ InputStream pairJsonStream = SegmentRoutingDeviceConfigTest.class
+ .getResourceAsStream("/device-pair.json");
+ InputStream invalidJsonStream = SegmentRoutingDeviceConfigTest.class
+ .getResourceAsStream("/device-invalid.json");
+
+ adjacencySids1 = new HashMap<>();
+ Set<Integer> ports1 = new HashSet<>();
+ ports1.add(2);
+ ports1.add(3);
+ adjacencySids1.put(100, ports1);
+ Set<Integer> ports2 = new HashSet<>();
+ ports2.add(4);
+ ports2.add(5);
+ adjacencySids1.put(200, ports2);
+
+ adjacencySids2 = new HashMap<>();
+ Set<Integer> ports3 = new HashSet<>();
+ ports3.add(6);
+ adjacencySids2.put(300, ports3);
+
+ DeviceId subject = DeviceId.deviceId("of:0000000000000001");
+ String key = "segmentrouting";
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonStream);
+ JsonNode ipv6JsonNode = mapper.readTree(ipv6JsonStream);
+ JsonNode pairJsonNode = mapper.readTree(pairJsonStream);
+ JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+ ConfigApplyDelegate delegate = new MockDelegate();
+
+ config = new SegmentRoutingDeviceConfig();
+ config.init(subject, key, jsonNode, mapper, delegate);
+
+ ipv6Config = new SegmentRoutingDeviceConfig();
+ ipv6Config.init(subject, key, ipv6JsonNode, mapper, delegate);
+
+ pairConfig = new SegmentRoutingDeviceConfig();
+ pairConfig.init(subject, key, pairJsonNode, mapper, delegate);
+
+ invalidConfig = new SegmentRoutingDeviceConfig();
+ invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+ }
+
+ @Test
+ public void testIsValid() {
+ assertTrue(config.isValid());
+ assertTrue(ipv6Config.isValid());
+ assertTrue(pairConfig.isValid());
+ assertFalse(invalidConfig.isValid());
+ }
+
+ @Test
+ public void testName() throws Exception {
+ assertTrue(config.name().isPresent());
+ assertThat(config.name().get(), is("Leaf-R1"));
+ }
+
+ @Test
+ public void testSetName() throws Exception {
+ config.setName("Spine-R1");
+ assertTrue(config.name().isPresent());
+ assertThat(config.name().get(), is("Spine-R1"));
+ }
+
+ @Test
+ public void testRouterIp() throws Exception {
+ assertThat(config.routerIpv4(), is(IpAddress.valueOf("10.0.1.254")));
+ assertThat(ipv6Config.routerIpv4(), is(IpAddress.valueOf("10.0.1.254")));
+ assertThat(ipv6Config.routerIpv6(), is(IpAddress.valueOf("2000::c0a8:0101")));
+ }
+
+ @Test
+ public void testSetRouterIp() throws Exception {
+ config.setRouterIpv4("10.0.2.254");
+ assertThat(config.routerIpv4(), is(IpAddress.valueOf("10.0.2.254")));
+ ipv6Config.setRouterIpv4("10.0.2.254");
+ assertThat(ipv6Config.routerIpv4(), is(IpAddress.valueOf("10.0.2.254")));
+ ipv6Config.setRouterIpv6("2000::c0a9:0101");
+ assertThat(ipv6Config.routerIpv6(), is(IpAddress.valueOf("2000::c0a9:0101")));
+ }
+
+ @Test
+ public void testRouterMac() throws Exception {
+ assertThat(config.routerMac(), is(MacAddress.valueOf("00:00:00:00:01:80")));
+ }
+
+ @Test
+ public void testSetRouterMac() throws Exception {
+ config.setRouterMac("00:00:00:00:02:80");
+ assertThat(config.routerMac(), is(MacAddress.valueOf("00:00:00:00:02:80")));
+ }
+
+ @Test
+ public void testNodeSid() throws Exception {
+ assertThat(config.nodeSidIPv4(), is(101));
+ assertThat(ipv6Config.nodeSidIPv4(), is(101));
+ assertThat(ipv6Config.nodeSidIPv6(), is(111));
+ }
+
+ @Test
+ public void testSetNodeSid() throws Exception {
+ config.setNodeSidIPv4(200);
+ assertThat(config.nodeSidIPv4(), is(200));
+ ipv6Config.setNodeSidIPv4(200);
+ assertThat(ipv6Config.nodeSidIPv4(), is(200));
+ ipv6Config.setNodeSidIPv6(201);
+ assertThat(ipv6Config.nodeSidIPv6(), is(201));
+ }
+
+ @Test
+ public void testIsEdgeRouter() throws Exception {
+ assertThat(config.isEdgeRouter(), is(true));
+ }
+
+ @Test
+ public void testSetIsEdgeRouter() throws Exception {
+ config.setIsEdgeRouter(false);
+ assertThat(config.isEdgeRouter(), is(false));
+ }
+
+ @Test
+ public void testAdjacencySids() throws Exception {
+ assertThat(config.adjacencySids(), is(adjacencySids1));
+ }
+
+ @Test
+ public void testSetAdjacencySids() throws Exception {
+ config.setAdjacencySids(adjacencySids2);
+ assertThat(config.adjacencySids(), is(adjacencySids2));
+ }
+
+ @Test
+ public void testPairDeviceId() throws Exception {
+ assertNull(config.pairDeviceId());
+ assertNull(ipv6Config.pairDeviceId());
+ assertThat(pairConfig.pairDeviceId(), is(PAIR_DEVICE_ID));
+ }
+
+ @Test
+ public void testSetPairDeviceId() throws Exception {
+ config.setPairDeviceId(PAIR_DEVICE_ID);
+ assertThat(config.pairDeviceId(), is(PAIR_DEVICE_ID));
+ }
+
+ @Test
+ public void testPairLocalPort() throws Exception {
+ assertNull(config.pairLocalPort());
+ assertNull(ipv6Config.pairLocalPort());
+ assertThat(pairConfig.pairLocalPort(), is(PAIR_LOCAL_PORT));
+ }
+
+ @Test
+ public void testSetPairLocalPort() throws Exception {
+ config.setPairLocalPort(PAIR_LOCAL_PORT);
+ assertThat(config.pairLocalPort(), is(PAIR_LOCAL_PORT));
+ }
+
+ private class MockDelegate implements ConfigApplyDelegate {
+ @Override
+ public void onApply(Config configFile) {
+ }
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java
new file mode 100644
index 0000000..62d2d86
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import java.io.InputStream;
+import java.util.Set;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for class {@link XConnectConfig}.
+ */
+public class XConnectConfigTest {
+ private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+ private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+ private static final VlanId VLAN10 = VlanId.vlanId((short) 10);
+ private static final VlanId VLAN20 = VlanId.vlanId((short) 20);
+ private static final PortNumber PORT3 = PortNumber.portNumber(3);
+ private static final PortNumber PORT4 = PortNumber.portNumber(4);
+ private static final PortNumber PORT5 = PortNumber.portNumber(5);
+ private static final XConnectStoreKey KEY1 = new XConnectStoreKey(DEV1, VLAN10);
+ private static final XConnectStoreKey KEY2 = new XConnectStoreKey(DEV2, VLAN10);
+ private static final XConnectStoreKey KEY3 = new XConnectStoreKey(DEV2, VLAN20);
+ private static final XConnectStoreKey KEY4 = new XConnectStoreKey(DEV2, VlanId.NONE);
+
+ private XConnectConfig config;
+ private XConnectConfig invalidConfig;
+
+ @Before
+ public void setUp() throws Exception {
+ InputStream jsonStream = SegmentRoutingAppConfigTest.class
+ .getResourceAsStream("/xconnect.json");
+ InputStream invalidJsonStream = SegmentRoutingAppConfigTest.class
+ .getResourceAsStream("/xconnect-invalid.json");
+
+ String key = SegmentRoutingManager.APP_NAME;
+ ApplicationId subject = new TestApplicationId(key);
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonStream);
+ JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+ ConfigApplyDelegate delegate = new XConnectConfigTest.MockDelegate();
+
+ config = new XConnectConfig();
+ config.init(subject, key, jsonNode, mapper, delegate);
+ invalidConfig = new XConnectConfig();
+ invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+ }
+
+ /**
+ * Tests config validity.
+ */
+ @Test
+ public void testIsValid() {
+ assertTrue(config.isValid());
+ assertFalse(invalidConfig.isValid());
+ }
+
+ /**
+ * Tests getXconnects.
+ */
+ @Test
+ public void testGetXconnects() {
+ Set<XConnectStoreKey> xconnects = config.getXconnects();
+ assertThat(xconnects.size(), is(3));
+ assertTrue(xconnects.contains(KEY1));
+ assertTrue(xconnects.contains(KEY2));
+ assertTrue(xconnects.contains(KEY3));
+ assertFalse(xconnects.contains(KEY4));
+ }
+
+ /**
+ * Tests getPorts.
+ */
+ @Test
+ public void testGetPorts() {
+ Set<PortNumber> ports;
+
+ ports = config.getPorts(KEY1);
+ assertThat(ports.size(), is(2));
+ assertTrue(ports.contains(PORT3));
+ assertTrue(ports.contains(PORT4));
+
+ ports = config.getPorts(KEY2);
+ assertThat(ports.size(), is(2));
+ assertTrue(ports.contains(PORT3));
+ assertTrue(ports.contains(PORT4));
+
+ ports = config.getPorts(KEY3);
+ assertThat(ports.size(), is(2));
+ assertTrue(ports.contains(PORT4));
+ assertTrue(ports.contains(PORT5));
+ }
+
+ private class MockDelegate implements ConfigApplyDelegate {
+ @Override
+ public void onApply(Config config) {
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java
new file mode 100644
index 0000000..f890852
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.onosproject.segmentrouting.grouphandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.net.DeviceId;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class DestinationSetTest {
+ DestinationSet ds1, ds2, ds3, ds4;
+ DeviceId d201, d202;
+ int el201, el202;
+
+ @Before
+ public void setUp() {
+ d201 = DeviceId.deviceId("of:0000000000000201");
+ d202 = DeviceId.deviceId("of:0000000000000202");
+ el201 = 201;
+ el202 = 202;
+ ds1 = new DestinationSet(false, d201);
+ ds2 = new DestinationSet(false, el201, d201);
+ ds3 = new DestinationSet(false, el201, d201, el202, d202);
+ ds4 = new DestinationSet(false,
+ DestinationSet.NO_EDGE_LABEL, d201,
+ DestinationSet.NO_EDGE_LABEL, d202);
+ }
+
+ @Test
+ public void testIsValid() {
+ assertTrue(!ds1.mplsSet());
+ assertTrue(ds1.getEdgeLabel(d201) == DestinationSet.NO_EDGE_LABEL);
+ assertTrue(ds1.getDestinationSwitches().size() == 1);
+
+ assertTrue(!ds2.mplsSet());
+ assertTrue(ds2.getEdgeLabel(d201) == el201);
+ assertTrue(ds2.getDestinationSwitches().size() == 1);
+
+ assertTrue(!ds3.mplsSet());
+ assertTrue(ds3.getEdgeLabel(d201) == el201);
+ assertTrue(ds3.getEdgeLabel(d202) == el202);
+ assertTrue(ds3.getDestinationSwitches().size() == 2);
+
+ assertTrue(!ds4.mplsSet());
+ assertTrue(ds4.getEdgeLabel(d201) == DestinationSet.NO_EDGE_LABEL);
+ assertTrue(ds4.getEdgeLabel(d202) == DestinationSet.NO_EDGE_LABEL);
+ assertTrue(ds4.getDestinationSwitches().size() == 2);
+
+ assertFalse(ds1.equals(ds2));
+ assertFalse(ds1.equals(ds4));
+ assertFalse(ds3.equals(ds4));
+ assertFalse(ds2.equals(ds3));
+ }
+
+
+ @Test
+ public void testOneDestinationWithoutLabel() {
+ DestinationSet testds = new DestinationSet(false, d201);
+ assertTrue(testds.equals(ds1)); // match
+
+ testds = new DestinationSet(true, d201);
+ assertFalse(testds.equals(ds1)); //wrong mplsSet
+
+ testds = new DestinationSet(false, d202);
+ assertFalse(testds.equals(ds1)); //wrong device
+
+ testds = new DestinationSet(false, el201, d201);
+ assertFalse(testds.equals(ds1)); // wrong label
+
+ testds = new DestinationSet(false, -1, d201, -1, d202);
+ assertFalse(testds.equals(ds1)); // 2-devs should not match
+ }
+
+
+
+ @Test
+ public void testOneDestinationWithLabel() {
+ DestinationSet testds = new DestinationSet(false, 203, d202);
+ assertFalse(testds.equals(ds2)); //wrong label
+
+ testds = new DestinationSet(true, 201, d201);
+ assertFalse(testds.equals(ds2)); //wrong mplsSet
+
+ testds = new DestinationSet(false, 201, d202);
+ assertFalse(testds.equals(ds2)); //wrong device
+
+ testds = new DestinationSet(false, 201, DeviceId.deviceId("of:0000000000000201"));
+ assertTrue(testds.equals(ds2)); // match
+
+ testds = new DestinationSet(false, d201);
+ assertFalse(testds.equals(ds2)); // wrong label
+
+ testds = new DestinationSet(false, el201, d201, el202, d202);
+ assertFalse(testds.equals(ds1)); // 2-devs should not match
+ }
+
+ @Test
+ public void testDestPairWithLabel() {
+ DestinationSet testds = new DestinationSet(false, el201, d201, el202, d202);
+ assertTrue(testds.equals(ds3)); // match same switches, same order
+ assertTrue(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el202, d202, el201, d201);
+ assertTrue(testds.equals(ds3)); // match same switches, order reversed
+ assertTrue(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el202, d202);
+ assertFalse(testds.equals(ds3)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el201, d201);
+ assertFalse(testds.equals(ds3)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el201, d201, 0, DeviceId.NONE);
+ assertFalse(testds.equals(ds3)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el201, d202, el201, d201);
+ assertFalse(testds.equals(ds3)); // wrong labels
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(true, el202, d202, el201, d201);
+ assertFalse(testds.equals(ds3)); // wrong mpls set
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false, el202, d202, el201, d202);
+ assertFalse(testds.equals(ds3)); // wrong device
+ assertFalse(testds.hashCode() == ds3.hashCode());
+
+ testds = new DestinationSet(false,
+ el202, DeviceId.deviceId("of:0000000000000205"),
+ el201, d201);
+ assertFalse(testds.equals(ds3)); // wrong device
+ assertFalse(testds.hashCode() == ds3.hashCode());
+ }
+
+ @Test
+ public void testDestPairWithoutLabel() {
+ DestinationSet testds = new DestinationSet(false, -1, d201, -1, d202);
+ assertTrue(testds.equals(ds4)); // match same switches, same order
+ assertTrue(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, -1, d202, -1, d201);
+ assertTrue(testds.equals(ds4)); // match same switches, order reversed
+ assertTrue(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, -1, d202);
+ assertFalse(testds.equals(ds4)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, -1, d201);
+ assertFalse(testds.equals(ds4)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, -1, d201, 0, DeviceId.NONE);
+ assertFalse(testds.equals(ds4)); // one less switch should not match
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, el201, d201, -1, d202);
+ assertFalse(testds.equals(ds4)); // wrong labels
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(true, -1, d202, -1, d201);
+ assertFalse(testds.equals(ds4)); // wrong mpls set
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false, -1, d202, -1, d202);
+ assertFalse(testds.equals(ds4)); // wrong device
+ assertFalse(testds.hashCode() == ds4.hashCode());
+
+ testds = new DestinationSet(false,
+ -1, DeviceId.deviceId("of:0000000000000205"),
+ -1, d201);
+ assertFalse(testds.equals(ds4)); // wrong device
+ assertFalse(testds.hashCode() == ds4.hashCode());
+ }
+
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app-ecmp.json b/apps/segmentrouting/app/src/test/resources/app-ecmp.json
new file mode 100644
index 0000000..e94b1c5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app-ecmp.json
@@ -0,0 +1,19 @@
+{
+ "vRouterMacs" : [
+ "00:00:00:00:00:01",
+ "00:00:00:00:00:02"
+ ],
+ "suppressSubnet" : [
+ "of:1/1",
+ "of:1/2"
+ ],
+ "suppressHostByPort" : [
+ "of:1/1",
+ "of:1/2"
+ ],
+ "suppressHostByProvider" : [
+ "org.onosproject.provider.host",
+ "org.onosproject.netcfghost"
+ ],
+ "MPLS-ECMP" : true
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app-invalid.json b/apps/segmentrouting/app/src/test/resources/app-invalid.json
new file mode 100644
index 0000000..01508f8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app-invalid.json
@@ -0,0 +1,15 @@
+{
+ "vRouterMacs" : [
+ "00:00:00:00:00:01",
+ "00:00:00:00:00:02"
+ ],
+ "suppressSubnet" : [
+ "of:1/1",
+ "of:1/2"
+ ],
+ "suppressHostByPort" : [
+ "of:1/1",
+ "wrongPort"
+ ],
+ "suppressHostByProvider" : []
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app.json b/apps/segmentrouting/app/src/test/resources/app.json
new file mode 100644
index 0000000..dab6384
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app.json
@@ -0,0 +1,18 @@
+{
+ "vRouterMacs" : [
+ "00:00:00:00:00:01",
+ "00:00:00:00:00:02"
+ ],
+ "suppressSubnet" : [
+ "of:1/1",
+ "of:1/2"
+ ],
+ "suppressHostByPort" : [
+ "of:1/1",
+ "of:1/2"
+ ],
+ "suppressHostByProvider" : [
+ "org.onosproject.provider.host",
+ "org.onosproject.netcfghost"
+ ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json b/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json
new file mode 100644
index 0000000..3d8749e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json
@@ -0,0 +1,5 @@
+{
+ "of:0000000000000001": ["1-9"],
+ "of:0000000000000003": ["7"],
+ "of:0000000000000004": ["1"]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/blocked-ports.json b/apps/segmentrouting/app/src/test/resources/blocked-ports.json
new file mode 100644
index 0000000..2543f3d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/blocked-ports.json
@@ -0,0 +1,5 @@
+{
+ "of:0000000000000001": ["1-4", "7-9"],
+ "of:0000000000000003": ["7-9"],
+ "of:0000000000000004": ["1", "5", "9"]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-invalid.json b/apps/segmentrouting/app/src/test/resources/device-invalid.json
new file mode 100644
index 0000000..dfcbdb8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-invalid.json
@@ -0,0 +1,12 @@
+{
+ "name" : "Leaf-R1",
+ "ipv4NodeSid" : 101,
+ "ipv4Loopback" : "10.0.1.254",
+ "routerMac" : "00:00:00:00:01:80",
+ "isEdgeRouter" : true,
+ "adjacencySids" : [
+ { "adjSid" : 100, "ports" : [2, 3] },
+ { "adjSid" : 200, "ports" : [4, 5] }
+ ],
+ "pairDeviceId" : "of:123456789ABCDEF0"
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-ipv6.json b/apps/segmentrouting/app/src/test/resources/device-ipv6.json
new file mode 100644
index 0000000..9832f8c
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-ipv6.json
@@ -0,0 +1,13 @@
+{
+ "name" : "Leaf-R1",
+ "ipv4NodeSid" : 101,
+ "ipv4Loopback" : "10.0.1.254",
+ "ipv6NodeSid" : 111,
+ "ipv6Loopback" : "2000::c0a8:0101",
+ "routerMac" : "00:00:00:00:01:80",
+ "isEdgeRouter" : true,
+ "adjacencySids" : [
+ { "adjSid" : 100, "ports" : [2, 3] },
+ { "adjSid" : 200, "ports" : [4, 5] }
+ ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-pair.json b/apps/segmentrouting/app/src/test/resources/device-pair.json
new file mode 100644
index 0000000..c699ff5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-pair.json
@@ -0,0 +1,13 @@
+{
+ "name" : "Leaf-R1",
+ "ipv4NodeSid" : 101,
+ "ipv4Loopback" : "10.0.1.254",
+ "routerMac" : "00:00:00:00:01:80",
+ "isEdgeRouter" : true,
+ "adjacencySids" : [
+ { "adjSid" : 100, "ports" : [2, 3] },
+ { "adjSid" : 200, "ports" : [4, 5] }
+ ],
+ "pairDeviceId" : "of:123456789ABCDEF0",
+ "pairLocalPort" : "10"
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device.json b/apps/segmentrouting/app/src/test/resources/device.json
new file mode 100644
index 0000000..83aec6e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device.json
@@ -0,0 +1,11 @@
+{
+ "name" : "Leaf-R1",
+ "ipv4NodeSid" : 101,
+ "ipv4Loopback" : "10.0.1.254",
+ "routerMac" : "00:00:00:00:01:80",
+ "isEdgeRouter" : true,
+ "adjacencySids" : [
+ { "adjSid" : 100, "ports" : [2, 3] },
+ { "adjSid" : 200, "ports" : [4, 5] }
+ ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json b/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json
new file mode 100644
index 0000000..fac162d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json
@@ -0,0 +1,24 @@
+{
+ "1": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/2",
+ "cP1InnerTag": "10",
+ "cP1OuterTag": "20",
+ "cP2InnerTag": "11",
+ "cP2OuterTag": "21",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "255"
+ },
+ "20": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/2",
+ "cP1InnerTag": "100",
+ "cP1OuterTag": "",
+ "cP2InnerTag": "21",
+ "cP2OuterTag": "",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "1255"
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json
new file mode 100644
index 0000000..af0a9d1
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json
@@ -0,0 +1,13 @@
+{
+ "1": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000011/1",
+ "cP1InnerTag": "10",
+ "cP1OuterTag": "20",
+ "cP2InnerTag": "11",
+ "cP2OuterTag": "",
+ "mode": "UNDEFINED_MODED",
+ "sdTag": "40",
+ "pwLabel": "255"
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json
new file mode 100644
index 0000000..66697f5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json
@@ -0,0 +1,13 @@
+{
+ "1": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000011/1",
+ "cP1InnerTag": "10",
+ "cP1OuterTag": "20",
+ "cP2InnerTag": "11",
+ "cP2OuterTag": "",
+ "mode": "1",
+ "sdTag": "40",
+ "pwLabel": "255.555551"
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json
new file mode 100644
index 0000000..b556e06
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json
@@ -0,0 +1,24 @@
+{
+ "1": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/2",
+ "cP1InnerTag": "10",
+ "cP1OuterTag": "20",
+ "cP2InnerTag": "11",
+ "cP2OuterTag": "21",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "255"
+ },
+ "20": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/2",
+ "cP1InnerTag": "100",
+ "cP1OuterTag": "200.55",
+ "cP2InnerTag": "1100",
+ "cP2OuterTag": "210",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "1255"
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas.json b/apps/segmentrouting/app/src/test/resources/pwaas.json
new file mode 100644
index 0000000..2a58b4e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas.json
@@ -0,0 +1,24 @@
+{
+ "1": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/1",
+ "cP1InnerTag": "10",
+ "cP1OuterTag": "20",
+ "cP2InnerTag": "10",
+ "cP2OuterTag": "21",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "255"
+ },
+ "20": {
+ "cP1": "of:0000000000000001/1",
+ "cP2": "of:0000000000000002/1",
+ "cP1InnerTag": "100",
+ "cP1OuterTag": "200",
+ "cP2InnerTag": "100",
+ "cP2OuterTag": "210",
+ "mode": "RAW",
+ "sdTag": "",
+ "pwLabel": "1255"
+ }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json b/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json
new file mode 100644
index 0000000..e468271
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json
@@ -0,0 +1,18 @@
+{
+ "of:0000000000000001": [
+ {
+ "vlan": 10,
+ "ports": [3, 4]
+ }
+ ],
+ "of:0000000000000002": [
+ {
+ "vlan": 10,
+ "ports": [3, 4]
+ },
+ {
+ "vlan": 20,
+ "ports": [4, 5, 6]
+ }
+ ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/xconnect.json b/apps/segmentrouting/app/src/test/resources/xconnect.json
new file mode 100644
index 0000000..ebd61b3
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/xconnect.json
@@ -0,0 +1,19 @@
+{
+ "of:0000000000000001": [
+ {
+ "vlan": 10,
+ "ports": [3, 4],
+ "name": "OLT1"
+ }
+ ],
+ "of:0000000000000002": [
+ {
+ "vlan": 10,
+ "ports": [3, 4]
+ },
+ {
+ "vlan": 20,
+ "ports": [4, 5]
+ }
+ ]
+}