Jonathan Hart

Add neighbor lifecycle management.

 * Made PIMNeighbor mostly immutable (apart from updatable timestamp)
 * Equals and hashCode for PIMNeighbor
 * Remove neighbor when we see a HELLO with holdTime==0
 * Periodic task to time out neighbors who haven't sent a HELLO in a while
 * Added a CLI command to view PIM neighbors

Change-Id: I59e52a847f7abcb8e9ac660c2cccace53e46867b
/*
* Copyright 2016 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.pim.cli;
import org.apache.karaf.shell.commands.Command;
import org.onlab.util.Tools;
import org.onosproject.cli.AbstractShellCommand;
import org.onosproject.pim.impl.PIMInterface;
import org.onosproject.pim.impl.PIMInterfaceService;
import org.onosproject.pim.impl.PIMNeighbor;
import java.util.Set;
/**
* Lists PIM neighbors.
*/
@Command(scope = "onos", name = "pim-neighbors",
description = "Lists the PIM neighbors")
public class PimNeighborsListCommand extends AbstractShellCommand {
private static final String INTF_FORMAT = "interface=%s, address=%s";
private static final String NEIGHBOR_FORMAT = " neighbor=%s, uptime=%s, holdtime=%s, drPriority=%s, genId=%s";
@Override
protected void execute() {
PIMInterfaceService interfaceService = get(PIMInterfaceService.class);
Set<PIMInterface> interfaces = interfaceService.getPimInterfaces();
for (PIMInterface intf : interfaces) {
print(INTF_FORMAT, intf.getInterface().name(), intf.getIpAddress());
for (PIMNeighbor neighbor : intf.getNeighbors()) {
// Filter out the PIM neighbor representing 'us'
if (!neighbor.ipAddress().equals(intf.getIpAddress())) {
print(NEIGHBOR_FORMAT, neighbor.ipAddress(),
Tools.timeAgo(neighbor.upTime()), neighbor.holdtime(),
neighbor.priority(), neighbor.generationId());
}
}
}
}
}
......@@ -32,10 +32,12 @@ import org.onosproject.net.packet.PacketService;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
......@@ -99,10 +101,7 @@ public final class PIMInterface {
generationId = new Random().nextInt();
// Create a PIM Neighbor to represent ourselves for DR election.
PIMNeighbor us = new PIMNeighbor(ourIp, mac);
// Priority and IP address are all we need to DR election.
us.setPriority(priority);
PIMNeighbor us = new PIMNeighbor(ourIp, mac, holdTime, 0, priority, generationId);
pimNeighbors.put(ourIp, us);
drIpaddress = ourIp;
......@@ -199,6 +198,32 @@ public final class PIMInterface {
}
/**
* Gets the neighbors seen on this interface.
*
* @return PIM neighbors
*/
public Collection<PIMNeighbor> getNeighbors() {
return pimNeighbors.values();
}
/**
* Checks whether any of our neighbors have expired, and cleans up their
* state if they have.
*/
public void checkNeighborTimeouts() {
Set<PIMNeighbor> expired = pimNeighbors.values().stream()
// Don't time ourselves out!
.filter(neighbor -> !neighbor.ipAddress().equals(getIpAddress()))
.filter(neighbor -> neighbor.isExpired())
.collect(Collectors.toSet());
for (PIMNeighbor neighbor : expired) {
log.info("Timing out neighbor {}", neighbor);
pimNeighbors.remove(neighbor.ipAddress(), neighbor);
}
}
/**
* Multicast a hello message out our interface. This hello message is sent
* periodically during the normal PIM Neighbor refresh time, as well as a
* result of a newly created interface.
......@@ -234,7 +259,7 @@ public final class PIMInterface {
* <li>We <em>may</em> have to create a new neighbor if one does not already exist</li>
* <li>We <em>may</em> need to re-elect a new DR if new information is received</li>
* <li>We <em>may</em> need to send an existing neighbor all joins if the genid changed</li>
* <li>We will refresh the neighbors timestamp</li>
* <li>We will refresh the neighbor's timestamp</li>
* </ul>
*
* @param ethPkt the Ethernet packet header
......@@ -259,7 +284,7 @@ public final class PIMInterface {
checkNotNull(dr);
IpAddress drip = drIpaddress;
int drpri = dr.getPriority();
int drpri = dr.priority();
// Assume we do not need to run a DR election
boolean reElectDr = false;
......@@ -269,18 +294,24 @@ public final class PIMInterface {
// Determine if we already have a PIMNeighbor
PIMNeighbor nbr = pimNeighbors.getOrDefault(srcip, null);
PIMNeighbor newNbr = PIMNeighbor.createPimNeighbor(srcip, nbrmac, hello.getOptions().values());
if (nbr == null) {
nbr = new PIMNeighbor(srcip, hello.getOptions());
checkNotNull(nbr);
} else {
Integer previousGenid = nbr.getGenid();
nbr.addOptions(hello.getOptions());
if (previousGenid != nbr.getGenid()) {
genidChanged = true;
pimNeighbors.putIfAbsent(srcip, newNbr);
nbr = newNbr;
} else if (!nbr.equals(newNbr)) {
if (newNbr.holdtime() == 0) {
// Neighbor has shut down. Remove them and clean up
pimNeighbors.remove(srcip, nbr);
return;
} else {
// Neighbor has changed one of their options.
pimNeighbors.put(srcip, newNbr);
nbr = newNbr;
}
}
// Refresh this neighbors timestamp
// Refresh this neighbor's timestamp
nbr.refreshTimestamp();
/*
......@@ -300,8 +331,8 @@ public final class PIMInterface {
// Run an election if we need to. Return the elected IP address.
private IpAddress election(PIMNeighbor nbr, IpAddress drIp, int drPriority) {
IpAddress nbrIp = nbr.getIpaddr();
if (nbr.getPriority() > drPriority) {
IpAddress nbrIp = nbr.ipAddress();
if (nbr.priority() > drPriority) {
return nbrIp;
}
......
......@@ -23,6 +23,7 @@ import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.onlab.util.SafeRecurringTask;
import org.onosproject.incubator.net.intf.Interface;
import org.onosproject.incubator.net.intf.InterfaceEvent;
import org.onosproject.incubator.net.intf.InterfaceListener;
......@@ -58,8 +59,10 @@ public class PIMInterfaceManager implements PIMInterfaceService {
private static final Class<PimInterfaceConfig> PIM_INTERFACE_CONFIG_CLASS = PimInterfaceConfig.class;
private static final String PIM_INTERFACE_CONFIG_KEY = "pimInterface";
// Create a Scheduled Executor service to send PIM hellos
private final ScheduledExecutorService helloScheduler =
private static final int DEFAULT_TIMEOUT_TASK_PERIOD_MS = 250;
// Create a Scheduled Executor service for recurring tasks
private final ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(1);
// Wait for a bout 3 seconds before sending the initial hello messages.
......@@ -69,6 +72,8 @@ public class PIMInterfaceManager implements PIMInterfaceService {
// Send PIM hello packets: 30 seconds.
private final long pimHelloPeriod = 30;
private final int timeoutTaskPeriod = DEFAULT_TIMEOUT_TASK_PERIOD_MS;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected PacketService packetService;
......@@ -113,18 +118,16 @@ public class PIMInterfaceManager implements PIMInterfaceService {
interfaceService.addListener(interfaceListener);
// Schedule the periodic hello sender.
helloScheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
for (PIMInterface pif : pimInterfaces.values()) {
pif.sendHello();
}
} catch (Exception e) {
log.warn("exception", e);
}
}
}, initialHelloDelay, pimHelloPeriod, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(
SafeRecurringTask.wrap(
() -> pimInterfaces.values().forEach(PIMInterface::sendHello)),
initialHelloDelay, pimHelloPeriod, TimeUnit.SECONDS);
// Schedule task to periodically time out expired neighbors
scheduledExecutorService.scheduleAtFixedRate(
SafeRecurringTask.wrap(
() -> pimInterfaces.values().forEach(PIMInterface::checkNeighborTimeouts)),
0, timeoutTaskPeriod, TimeUnit.MILLISECONDS);
log.info("Started");
}
......@@ -136,7 +139,7 @@ public class PIMInterfaceManager implements PIMInterfaceService {
networkConfig.unregisterConfigFactory(pimConfigFactory);
// Shutdown the periodic hello task.
helloScheduler.shutdown();
scheduledExecutorService.shutdown();
log.info("Stopped");
}
......
......@@ -15,216 +15,226 @@
*/
package org.onosproject.pim.impl;
import com.google.common.base.MoreObjects;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.pim.PIMHelloOption;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static org.slf4j.LoggerFactory.getLogger;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Represents a PIM neighbor.
*/
public class PIMNeighbor {
private final Logger log = getLogger(getClass());
// IP Address of this neighbor
private IpAddress ipAddr;
private final IpAddress ipAddr;
// MAC Address of the neighbor (Need for sending J/P)
private MacAddress macAddr;
private final MacAddress macAddr;
// Hello Options
// Our hello opt holdTime
private short holdTime;
private final short holdTime;
// Our hello opt prune delay
private int pruneDelay;
private final int pruneDelay;
// Neighbor priority
private int priority;
private final int priority;
// Our current genId
private int genId;
private final int genId;
private final long upTime;
// Our timestamp for this neighbor
private Date lastRefresh;
private long lastRefresh;
/**
* Construct a new PIM Neighbor.
* Class constructor.
*
* @param ipAddr the IP Address of our new neighbor
* @param opts option map
* @param ipAddress neighbor IP address
* @param macAddress neighbor MAC address
* @param holdTime hold time
* @param pruneDelay prune delay
* @param priority priority
* @param genId generation ID
*/
public PIMNeighbor(IpAddress ipAddr, Map<Short, PIMHelloOption> opts) {
this.ipAddr = ipAddr;
this.addOptions(opts);
public PIMNeighbor(IpAddress ipAddress, MacAddress macAddress,
short holdTime, int pruneDelay, int priority, int genId) {
this.ipAddr = checkNotNull(ipAddress);
this.macAddr = checkNotNull(macAddress);
this.holdTime = holdTime;
this.pruneDelay = pruneDelay;
this.priority = priority;
this.genId = genId;
this.upTime = System.currentTimeMillis();
}
/**
* Construct a new PIM neighbor.
* Gets the IP address of our neighbor.
*
* @param ipAddr the neighbors IP addr
* @param macAddr MAC address
* @return the IP address of our neighbor
*/
public PIMNeighbor(IpAddress ipAddr, MacAddress macAddr) {
this.ipAddr = ipAddr;
this.macAddr = macAddr;
public IpAddress ipAddress() {
return ipAddr;
}
/**
* Get the MAC address of this neighbor.
* Gets the MAC address of this neighbor.
*
* @return the mac address
*/
public MacAddress getMacaddr() {
public MacAddress macAddress() {
return macAddr;
}
/**
* Get the IP Address of our neighbor.
* Gets our neighbor's hold time.
*
* @return the IP address of our neighbor
* @return the hold time
*/
public IpAddress getIpaddr() {
return ipAddr;
public short holdtime() {
return holdTime;
}
/**
* Set the IP address of our neighbor.
* Gets our neighbor's prune delay.
*
* @param ipAddr our neighbors IP address
* @return our neighbor's prune delay
*/
public void setIpaddr(IpAddress ipAddr) {
this.ipAddr = ipAddr;
public int pruneDelay() {
return pruneDelay;
}
/**
* Get our neighbors holdTime.
* Gets our neighbor's priority.
*
* @return the holdTime
* @return our neighbor's priority
*/
public short getHoldtime() {
return holdTime;
public int priority() {
return priority;
}
/**
* Set our neighbors holdTime.
* Gets our neighbor's generation ID.
*
* @param holdTime the holdTime
* @return our neighbor's generation ID
*/
public void setHoldtime(short holdTime) {
this.holdTime = holdTime;
public int generationId() {
return genId;
}
/**
* Get our neighbors prune delay.
* Gets the last time we heard a HELLO from this neighbor.
*
* @return our neighbors prune delay
* @return last refresh time
*/
public int getPruneDelay() {
return pruneDelay;
public long lastRefresh() {
return lastRefresh;
}
/**
* Set our neighbors prune delay.
* Gets the time that we first learnt of this neighbor.
*
* @param pruneDelay the prune delay
* @return up time
*/
public void setPruneDelay(int pruneDelay) {
this.pruneDelay = pruneDelay;
public long upTime() {
return upTime;
}
/**
* Get our neighbors priority.
*
* @return our neighbors priority
* Refreshes this neighbor's last seen timestamp.
*/
public int getPriority() {
return priority;
public void refreshTimestamp() {
lastRefresh = System.currentTimeMillis();
}
/**
* Set our neighbors priority.
* Returns whether this neighbor is expired or not.
*
* @param priority our neighbors priority
* @return true if the neighbor is expired, otherwise false
*/
public void setPriority(int priority) {
this.priority = priority;
public boolean isExpired() {
return lastRefresh + TimeUnit.SECONDS.toMillis(holdTime)
< System.currentTimeMillis();
}
/**
* Get our neighbors Genid.
* Creates a PIM neighbor based on an IP, MAC, and collection of PIM HELLO
* options.
*
* @return our neighbor Genid
* @param ipAddress neighbor IP address
* @param macAddress neighbor MAC address
* @param opts options from the PIM HELLO packet
* @return new PIM neighbor
*/
public int getGenid() {
return genId;
}
public static PIMNeighbor createPimNeighbor(IpAddress ipAddress,
MacAddress macAddress,
Collection<PIMHelloOption> opts) {
int generationID = PIMHelloOption.DEFAULT_GENID;
short holdTime = PIMHelloOption.DEFAULT_HOLDTIME;
int priority = PIMHelloOption.DEFAULT_PRIORITY;
int pruneDelay = PIMHelloOption.DEFAULT_PRUNEDELAY;
for (PIMHelloOption opt : opts) {
short type = opt.getOptType();
ByteBuffer value = ByteBuffer.wrap(opt.getValue());
if (type == PIMHelloOption.OPT_GENID) {
generationID = value.getInt();
} else if (type == PIMHelloOption.OPT_HOLDTIME) {
holdTime = value.getShort();
} else if (type == PIMHelloOption.OPT_PRIORITY) {
priority = value.getInt();
} else if (type == PIMHelloOption.OPT_PRUNEDELAY) {
pruneDelay = value.getInt();
} else if (type == PIMHelloOption.OPT_ADDRLIST) {
// TODO: Will implement someday
}
}
/**
* Set our neighbors GenId.
*
* @param genId our neighbors GenId
*/
public void setGenid(int genId) {
this.genId = genId;
return new PIMNeighbor(ipAddress, macAddress, holdTime, pruneDelay, priority, generationID);
}
/**
* Add the options for this neighbor if needed.
*
* @param opts the options to be added/modified
* @return true if options changed, false if no option has changed
*/
public boolean addOptions(Map<Short, PIMHelloOption> opts) {
@Override
public boolean equals(Object other) {
if (!(other instanceof PIMNeighbor)) {
return false;
}
boolean changed = false;
PIMNeighbor that = (PIMNeighbor) other;
for (PIMHelloOption opt : opts.values()) {
Short otype = opt.getOptType();
ByteBuffer val = ByteBuffer.wrap(opt.getValue());
return this.ipAddr.equals(that.ipAddress()) &&
this.macAddr.equals(that.macAddress()) &&
this.genId == that.genId &&
this.holdTime == that.holdTime &&
this.priority == that.priority;
}
if (otype == PIMHelloOption.OPT_ADDRLIST) {
// TODO: Will implement someday
} else if (otype == PIMHelloOption.OPT_GENID) {
int newval = val.getInt();
if (newval != genId) {
genId = newval;
changed = true;
}
} else if (otype == PIMHelloOption.OPT_HOLDTIME) {
short newval = val.getShort();
if (newval != holdTime) {
holdTime = newval;
changed = true;
}
} else if (otype == PIMHelloOption.OPT_PRIORITY) {
int newval = val.getInt();
if (newval != priority) {
priority = newval;
changed = true;
}
} else if (otype == PIMHelloOption.OPT_PRUNEDELAY) {
int newval = val.getInt();
if (newval != pruneDelay) {
pruneDelay = newval;
changed = true;
}
} else {
log.warn("received unknown pim hello options" + otype);
}
}
return changed;
@Override
public int hashCode() {
return Objects.hash(ipAddr, macAddr, genId, holdTime, priority);
}
/**
* Refresh this neighbors timestamp.
*/
public void refreshTimestamp() {
lastRefresh = Calendar.getInstance().getTime();
@Override
public String toString() {
return MoreObjects.toStringHelper(getClass())
.add("ipAddress", ipAddr)
.add("macAddress", macAddr)
.add("generationId", genId)
.add("holdTime", holdTime)
.add("priority", priority)
.add("pruneDelay", pruneDelay)
.toString();
}
}
......
......@@ -19,6 +19,9 @@
<command>
<action class="org.onosproject.pim.cli.PimInterfacesListCommand"/>
</command>
<command>
<action class="org.onosproject.pim.cli.PimNeighborsListCommand"/>
</command>
</command-bundle>
</blueprint>
......
/*
* Copyright 2016 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onlab.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wrapper for a recurring task which catches all exceptions to prevent task
* being suppressed in a ScheduledExecutorService.
*/
public final class SafeRecurringTask implements Runnable {
private static final Logger log = LoggerFactory.getLogger(SafeRecurringTask.class);
private final Runnable runnable;
/**
* Constructor.
*
* @param runnable runnable to wrap
*/
private SafeRecurringTask(Runnable runnable) {
this.runnable = runnable;
}
@Override
public void run() {
if (Thread.currentThread().isInterrupted()) {
log.info("Task interrupted, quitting");
return;
}
try {
runnable.run();
} catch (Exception e) {
// Catch all exceptions to avoid task being suppressed
log.error("Exception thrown during task", e);
}
}
/**
* Wraps a runnable in a safe recurring task.
*
* @param runnable runnable to wrap
* @return safe recurring task
*/
public static SafeRecurringTask wrap(Runnable runnable) {
return new SafeRecurringTask(runnable);
}
}