diff --git a/build.gradle b/build.gradle index 670aad2..4e44e19 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ group 'ssbgp' -version '1.0' +version '1.1' buildscript { - ext.kotlin_version = '1.1.4-2' + ext.kotlin_version = '1.1.4-3' ext.dokka_version = '0.9.15' ext.junit_version = '1.0.0-M4' ext.junit5_version = '5.0.0-M4' diff --git a/src/main/kotlin/bgp/SSBGP.kt b/src/main/kotlin/bgp/SSBGP.kt index 69ed21a..ecec931 100644 --- a/src/main/kotlin/bgp/SSBGP.kt +++ b/src/main/kotlin/bgp/SSBGP.kt @@ -25,6 +25,8 @@ abstract class BaseSSBGP(mrai: Time = 0, routingTable: RoutingTable): return } + val prevSelectedRoute = routingTable.getSelectedRoute() + // Since a loop routing was detected, the new route via the sender node is surely invalid // Set the route via the sender as invalid @@ -33,7 +35,7 @@ abstract class BaseSSBGP(mrai: Time = 0, routingTable: RoutingTable): wasSelectedRouteUpdated = wasSelectedRouteUpdated || updated val alternativeRoute = routingTable.getSelectedRoute() - if (isRecurrent(node, route, alternativeRoute)) { + if (isRecurrent(node, route, alternativeRoute, prevSelectedRoute)) { disableNeighbor(sender) BGPNotifier.notifyDetect(DetectNotification(node, route, alternativeRoute, sender)) } @@ -43,7 +45,8 @@ abstract class BaseSSBGP(mrai: Time = 0, routingTable: RoutingTable): * Checks if the routing loop detected is recurrent. * Subclasses must implement this method to define the detection condition. */ - abstract fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean + protected abstract fun isRecurrent(node: Node, learnedRoute: BGPRoute, + alternativeRoute: BGPRoute, prevSelectedRoute: BGPRoute): Boolean /** * Enables the specified neighbor. @@ -81,7 +84,9 @@ abstract class BaseSSBGP(mrai: Time = 0, routingTable: RoutingTable): class SSBGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) : BaseSSBGP(mrai, routingTable) { - override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean { + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute, + prevSelectedRoute: BGPRoute): Boolean { + return learnedRoute.localPref > alternativeRoute.localPref } } @@ -93,8 +98,38 @@ class SSBGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable. class ISSBGP(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) : BaseSSBGP(mrai, routingTable) { - override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute): Boolean { + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute, + prevSelectedRoute: BGPRoute): Boolean { + return learnedRoute.localPref > alternativeRoute.localPref && alternativeRoute.asPath == learnedRoute.asPath.subPathBefore(node) } -} \ No newline at end of file +} + +/** + * SS-BGP version 2 Protocol: it uses a more generic detection condition than version 1. + */ +class SSBGP2(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) + : BaseSSBGP(mrai, routingTable) { + + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute, + prevSelectedRoute: BGPRoute): Boolean { + + return alternativeRoute.localPref < prevSelectedRoute.localPref + } +} + +/** + * ISS-BGP version 2 Protocol: it uses the detection condition of SS-BGP2 and also checks if the tail of looping + * path matches the path of the alternative route. + */ +class ISSBGP2(mrai: Time = 0, routingTable: RoutingTable = RoutingTable.empty(BGPRoute.invalid())) + : BaseSSBGP(mrai, routingTable) { + + override fun isRecurrent(node: Node, learnedRoute: BGPRoute, alternativeRoute: BGPRoute, + prevSelectedRoute: BGPRoute): Boolean { + + return alternativeRoute.localPref < prevSelectedRoute.localPref && + alternativeRoute.asPath == learnedRoute.asPath.subPathBefore(node) + } +} diff --git a/src/main/kotlin/core/routing/TopologyBuilder.kt b/src/main/kotlin/core/routing/TopologyBuilder.kt index 676e8af..eb8d0d8 100644 --- a/src/main/kotlin/core/routing/TopologyBuilder.kt +++ b/src/main/kotlin/core/routing/TopologyBuilder.kt @@ -28,14 +28,17 @@ class TopologyBuilder { * * @param id the ID to identify the new node * @param protocol the protocol deployed by the new node + * @return this builder * @throws ElementExistsException if a node with the specified ID was already added to the builder */ @Throws(ElementExistsException::class) - fun addNode(id: NodeID, protocol: Protocol) { + fun addNode(id: NodeID, protocol: Protocol): TopologyBuilder { if (nodes.putIfAbsent(id, Node(id, protocol)) != null) { throw ElementExistsException("Node with ID `$id` was added twice to the topology builder") } + + return this } /** @@ -47,11 +50,12 @@ class TopologyBuilder { * @param from the Id of the node at the tail of the link * @param to the protocol deployed by the new node * @param extender the protocol deployed by the new node + * @return this builder * @throws ElementExistsException if a node with the specified ID was already added to the builder * @throws ElementNotFoundException if builder is missing the node with ID [from] and/or [to] */ @Throws(ElementExistsException::class, ElementNotFoundException::class) - fun link(from: NodeID, to: NodeID, extender: Extender) { + fun link(from: NodeID, to: NodeID, extender: Extender): TopologyBuilder { val tail = nodes[from] ?: throw ElementNotFoundException("Node with ID `$from` was not yet added the builder") val head = nodes[to] ?: throw ElementNotFoundException("Node with ID `$to` was not yet added the builder") @@ -61,6 +65,8 @@ class TopologyBuilder { } head.addInNeighbor(tail, extender) + + return this } /** diff --git a/src/main/kotlin/io/ExtenderParseFunctions.kt b/src/main/kotlin/io/ExtenderParseFunctions.kt new file mode 100644 index 0000000..3598d7c --- /dev/null +++ b/src/main/kotlin/io/ExtenderParseFunctions.kt @@ -0,0 +1,47 @@ +package io + +import bgp.BGPRoute +import bgp.policies.interdomain.* +import core.routing.Extender + +/** + * Created on 31-08-2017 + * + * @author David Fialho + * + * This file contains functions to parse extenders from labels. + */ + +/** + * Parses an Interdomain Extender. The supported labels are: + * + * R+ - parsed as a PeerplusExtender + * C - parsed as a CustomerExtender + * R - parsed as a PeerExtender + * P - parsed as a ProviderExtender + * S - parsed as a SiblingExtender + * + * This function is NOT case sensitive! + * + * @param label the label of the extender + * @param lineNumber the number of the line in which the label was found (used for the parse exception message only) + * @return the extender parsed from the label + * @throws ParseException if the label is not recognized + */ +@Throws(ParseException::class) +fun parseInterdomainExtender(label: String, lineNumber: Int): Extender { + + return when (label.toLowerCase()) { + "r+" -> PeerplusExtender + "c" -> CustomerExtender + "r" -> PeerExtender + "p" -> ProviderExtender + "s" -> SiblingExtender + else -> throw ParseException("Extender label `$label` was not recognized: " + + "must be either R+, C, R, P, or S", lineNumber) + } +} + +fun parseInterdomainExtender(label: String): Extender { + return parseInterdomainExtender(label, lineNumber = 0) +} \ No newline at end of file diff --git a/src/main/kotlin/io/InterdomainTopologyReader.kt b/src/main/kotlin/io/InterdomainTopologyReader.kt index abd19f8..a46a539 100644 --- a/src/main/kotlin/io/InterdomainTopologyReader.kt +++ b/src/main/kotlin/io/InterdomainTopologyReader.kt @@ -1,12 +1,9 @@ package io -import bgp.BGP -import bgp.BGPRoute -import bgp.ISSBGP -import bgp.SSBGP -import bgp.policies.interdomain.* +import bgp.* import core.routing.* import io.TopologyParser.Handler +import utils.toNonNegativeInt import java.io.* /** @@ -14,7 +11,7 @@ import java.io.* * * @author David Fialho */ -class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Handler { +class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Handler { /** * Provides option to create a reader with a file object. @@ -54,14 +51,21 @@ class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Hand } val protocolLabel = values[0] - val mrai = parseNonNegativeInteger(values[1], currentLine) + val mrai = try { + values[1].toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("Failed to parse `${values[1]}`: must be a non-negative integer number", currentLine) + } val protocol = when (protocolLabel.toLowerCase()) { "bgp" -> BGP(mrai) "ssbgp" -> SSBGP(mrai) "issbgp" -> ISSBGP(mrai) - else -> throw ParseException("Protocol label `$protocolLabel` was not recognized: supported labels are BGP, " + - "SSBGP, and ISSBGP", currentLine) + "ssbgp2" -> SSBGP2(mrai) + "issbgp2" -> ISSBGP2(mrai) + else -> throw ParseException( + "Protocol label `$protocolLabel` was not recognized: supported labels are BGP, " + + "SSBGP, ISSBGP, SSBGP2, and ISSBGP2", currentLine) } try { @@ -86,7 +90,7 @@ class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Hand throw ParseException("Link is missing required values: extender label", currentLine) } - val extender = parseExtender(values[0], currentLine) + val extender = parseInterdomainExtender(values[0], currentLine) try { builder.link(tail, head, extender) @@ -98,36 +102,6 @@ class InterdomainTopologyReader(reader: Reader): TopologyReader, Closeable, Hand } } - @Throws(ParseException::class) - private fun parseNonNegativeInteger(value: String, currentLine: Int): Int { - - try { - val intValue = value.toInt() - if (intValue < 0) { - throw NumberFormatException() - } - - return intValue - - } catch (e: NumberFormatException) { - throw ParseException("Failed to parse value `$value`: must be a non-negative integer value", currentLine) - } - } - - @Throws(ParseException::class) - private fun parseExtender(label: String, currentLine: Int): Extender { - - return when (label) { - "r+" -> PeerplusExtender - "c" -> CustomerExtender - "r" -> PeerExtender - "p" -> ProviderExtender - "s" -> SiblingExtender - else -> throw ParseException("Extender label `$label` was not recognized: " + - "must be either R+, C, R, P, or S", currentLine) - } - } - /** * Closes the stream and releases any system resources associated with it. */ diff --git a/src/main/kotlin/io/StubParser.kt b/src/main/kotlin/io/StubParser.kt new file mode 100644 index 0000000..a5a47cf --- /dev/null +++ b/src/main/kotlin/io/StubParser.kt @@ -0,0 +1,116 @@ +package io + +import core.routing.NodeID +import utils.toNonNegativeInt +import java.io.* + +/** + * Created on 31-08-2017 + * + * @author David Fialho + */ +class StubParser(reader: Reader): Closeable { + + companion object { + + /** + * Creates a stub parser with a file reader to parse a file. + * + * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for + * some other reason cannot be opened for reading. + */ + @Throws(FileNotFoundException::class) + fun useFile(stubFile: File): StubParser { + return StubParser(FileReader(stubFile)) + } + + } + + /** + * Interface for an handler that is called when a new stub item is parsed. + */ + interface Handler { + + /** + * Invoked when a new stub item is found. + * + * @param id the ID of the stub + * @param inNeighbor the ID of the stub's in-neighbor + * @param label the label of the extender associated with the link between the neighbor and the stub + * @param currentLine line number where the stub link was parsed + */ + fun onStubLink(id: NodeID, inNeighbor: NodeID, label: String, currentLine: Int) + + } + + private val reader = BufferedReader(reader) + + /** + * Parses the stub file and notifies the handler once a new stub is parsed. + * + * @param handler the handler that will be notified of new stub items + * @throws IOException If an I/O error occurs + * @throws ParseException If a parse error occurs + */ + @Throws(IOException::class, ParseException::class) + fun parse(handler: Handler) { + + var line: String? = reader.readLine() ?: return + var currentLine = 1 + + while (line != null) { + + // Ignore blank lines + if (!line.isBlank()) + parseLine(line, handler, currentLine) + + line = reader.readLine() + currentLine++ + } + + } + + private fun parseLine(line: String, handler: Handler, currentLine: Int) { + + val values = line.split("|").map { it.trim() } + + if (values.size != 3) { + throw ParseException("A stub item requires 3 values, but ${values.size} were provided", currentLine) + } + + val stubID = try { + values[0].toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("Stub ID must be non-negative integer number: was `${values[0]}`", currentLine) + } + + val inNeighborID = try { + values[1].toNonNegativeInt() + } catch (e: NumberFormatException) { + throw ParseException("In-neighbor ID must be non-negative integer number: was `${values[1]}`", currentLine) + } + + val label = values[2] + if (label.isBlank()) throw ParseException("Stub item is missing label value", currentLine) + + handler.onStubLink(stubID, inNeighborID, label, currentLine) + } + + /** + * Resets the input stream. + * + * @throws IOException If the stream does not support reset(), or if some other I/O error occurs + */ + @Throws(IOException::class) + fun reset() { + if (!reader.markSupported()) + reader.reset() + } + + /** + * Closes the input stream. + */ + override fun close() { + reader.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/TopologyReader.kt b/src/main/kotlin/io/TopologyReader.kt index 65776da..259d84c 100644 --- a/src/main/kotlin/io/TopologyReader.kt +++ b/src/main/kotlin/io/TopologyReader.kt @@ -1,5 +1,6 @@ package io +import core.routing.Route import core.routing.Topology /** @@ -7,7 +8,7 @@ import core.routing.Topology * * @author David Fialho */ -interface TopologyReader { +interface TopologyReader { - fun read(): Topology<*> + fun read(): Topology } \ No newline at end of file diff --git a/src/main/kotlin/io/TopologyReaderHandler.kt b/src/main/kotlin/io/TopologyReaderHandler.kt index 2f12d35..c519720 100644 --- a/src/main/kotlin/io/TopologyReaderHandler.kt +++ b/src/main/kotlin/io/TopologyReaderHandler.kt @@ -1,5 +1,7 @@ package io +import bgp.BGPRoute +import core.routing.Route import core.routing.Topology import java.io.File import java.io.FileReader @@ -11,7 +13,7 @@ import java.io.Reader * * @author David Fialho */ -sealed class TopologyReaderHandler { +sealed class TopologyReaderHandler { /** * Reads the topology file associated with the handler on a new topology reader and then closes it down correctly @@ -22,14 +24,14 @@ sealed class TopologyReaderHandler { * @throws ParseException - if a topology object can not be created due to incorrect representation */ @Throws(IOException::class, ParseException::class) - abstract fun read(): Topology<*> + abstract fun read(): Topology } /** * Handler for InterdomainTopologyReader. */ -class InterdomainTopologyReaderHandler(private val reader: Reader): TopologyReaderHandler() { +class InterdomainTopologyReaderHandler(private val reader: Reader): TopologyReaderHandler() { constructor(topologyFile: File): this(FileReader(topologyFile)) @@ -41,7 +43,7 @@ class InterdomainTopologyReaderHandler(private val reader: Reader): TopologyRead * @throws ParseException - if a topology object can not be created due to incorrect representation */ @Throws(IOException::class, ParseException::class) - override fun read(): Topology<*> { + override fun read(): Topology { InterdomainTopologyReader(reader).use { return it.read() diff --git a/src/main/kotlin/simulation/RepetitionRunner.kt b/src/main/kotlin/simulation/RepetitionRunner.kt index adedb16..43570d4 100644 --- a/src/main/kotlin/simulation/RepetitionRunner.kt +++ b/src/main/kotlin/simulation/RepetitionRunner.kt @@ -2,6 +2,7 @@ package simulation import core.routing.Node import core.routing.NodeID +import core.routing.Route import core.routing.Topology import core.simulator.DelayGenerator import core.simulator.Engine @@ -14,12 +15,13 @@ import java.io.File * * @author David Fialho */ -class RepetitionRunner( +class RepetitionRunner( private val topologyFile: File, - private val topologyReader: TopologyReaderHandler, - private val destination: NodeID, + private val topologyReader: TopologyReaderHandler, + private val destinationID: NodeID, private val repetitions: Int, - private val messageDelayGenerator: DelayGenerator + private val messageDelayGenerator: DelayGenerator, + private val stubDB: StubDB? ): Runner { @@ -34,12 +36,12 @@ class RepetitionRunner( */ override fun run(execution: Execution, application: Application) { - val topology: Topology<*> = application.loadTopology(topologyFile, topologyReader) { + val topology: Topology = application.loadTopology(topologyFile, topologyReader) { topologyReader.read() } - val destination: Node<*> = application.findDestination(destination) { - topology[destination] + val destination: Node = application.findDestination(destinationID) { + topology[destinationID] ?: stubDB?.getStub(destinationID, topology) } Engine.messageDelayGenerator = messageDelayGenerator diff --git a/src/main/kotlin/simulation/StubDB.kt b/src/main/kotlin/simulation/StubDB.kt new file mode 100644 index 0000000..da63496 --- /dev/null +++ b/src/main/kotlin/simulation/StubDB.kt @@ -0,0 +1,87 @@ +package simulation + +import core.routing.* +import io.ParseException +import io.StubParser +import java.io.File +import java.util.* +import kotlin.collections.ArrayList + +/** + * Created on 31-08-2017 + * + * @author David Fialho + * + * @param stubProtocol the protocol to assign to the stub + * @property parseExtender function to convert a string label into an extender + */ +class StubDB( + val stubsFile: File, + private val stubProtocol: Protocol, + private val parseExtender: (String) -> Extender + +): StubParser.Handler { + + private var topology: Topology? = null + private var stubID: NodeID = -1 + + /** + * Stores the links of the stub that is to be obtained. + */ + private val stubLinks = ArrayList, Extender>>() + + /** + * Obtains the stub node with id [stubID]. + * + * @param stubID the ID of the stub to get + * @param topology the topology missing the stubs in this database + * @return the stub node initialized with its in-neighbors + * @throws ParseException if the input file defined a neighbor not included in the topology or if it includes a + * label that is not recognized. + */ + @Throws(ParseException::class) + fun getStub(stubID: NodeID, topology: Topology): Node? { + + StubParser.useFile(stubsFile).use { + this.stubID = stubID + this.topology = topology + it.parse(this) + } + + // Return empty optional if the stub was not found + if (stubLinks.isEmpty()) return null + + // Create stub node + val stub = Node(stubID, stubProtocol) + stubLinks.forEach { (neighbor, extender) -> stub.addInNeighbor(neighbor, extender) } + + // Stub links are no longer required + stubLinks.clear() + + return stub + } + + /** + * Invoked when a new stub item is found. + * + * @param id the ID of the stub + * @param inNeighbor the ID of the stub's in-neighbor + * @param label the label of the extender associated with the link between the neighbor and the stub + * @param currentLine line number where the stub link was parsed + * @throws ParseException if the neighbor was not included in the topology or if it includes a + * label that is not recognized. + */ + @Throws(ParseException::class) + override fun onStubLink(id: NodeID, inNeighbor: NodeID, label: String, currentLine: Int) { + + // Consider only the stub that we want to obtain + if (id != stubID) return + + val node = topology?.get(inNeighbor) ?: + throw ParseException("Neighbor ID `$inNeighbor` was not found in the topology", currentLine) + val extender = parseExtender(label) + + // Add neighbor and extender to list of stub links corresponding to the stub we want to get + stubLinks.add(Pair(node, extender)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/Application.kt b/src/main/kotlin/ui/Application.kt index 5eaa072..e28e71d 100644 --- a/src/main/kotlin/ui/Application.kt +++ b/src/main/kotlin/ui/Application.kt @@ -2,6 +2,7 @@ package ui import core.routing.Node import core.routing.NodeID +import core.routing.Route import core.routing.Topology import io.TopologyReaderHandler import java.io.File @@ -22,10 +23,10 @@ interface Application { * @param topologyReader the reader used to load the topology into memory * @param loadBlock the code block to load the topology. */ - fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, - loadBlock: () -> Topology<*>): Topology<*> + fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology): Topology - fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> + fun findDestination(destinationID: NodeID, block: () -> Node?): Node /** * Invoked while executing each execution. @@ -35,7 +36,7 @@ interface Application { * @param seed the seed of the message delay generator used for the execution * @param executeBlock the code block that performs one execution */ - fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) + fun execute(executionID: Int, destination: Node, seed: Long, executeBlock: () -> Unit) /** * Invoked during a run. diff --git a/src/main/kotlin/ui/DummyApplication.kt b/src/main/kotlin/ui/DummyApplication.kt index cb37eba..39ad735 100644 --- a/src/main/kotlin/ui/DummyApplication.kt +++ b/src/main/kotlin/ui/DummyApplication.kt @@ -2,6 +2,7 @@ package ui import core.routing.Node import core.routing.NodeID +import core.routing.Route import core.routing.Topology import io.TopologyReaderHandler import java.io.File @@ -15,12 +16,12 @@ object DummyApplication: Application { override fun launch(args: Array) = Unit - override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, - loadBlock: () -> Topology<*>): Topology<*> = loadBlock() + override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology): Topology = loadBlock() - override fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> = block()!! + override fun findDestination(destinationID: NodeID, block: () -> Node?): Node = block()!! - override fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) = Unit + override fun execute(executionID: Int, destination: Node, seed: Long, executeBlock: () -> Unit) = Unit override fun run(runBlock: () -> Unit) = Unit diff --git a/src/main/kotlin/ui/cli/CLIApplication.kt b/src/main/kotlin/ui/cli/CLIApplication.kt index baf0070..f1e9052 100644 --- a/src/main/kotlin/ui/cli/CLIApplication.kt +++ b/src/main/kotlin/ui/cli/CLIApplication.kt @@ -2,6 +2,7 @@ package ui.cli import core.routing.Node import core.routing.NodeID +import core.routing.Route import core.routing.Topology import io.ParseException import io.TopologyReaderHandler @@ -28,12 +29,14 @@ object CLIApplication: Application { runner.run(execution, this) } catch (e: InputArgumentsException) { - console.error("Input arguments are invalid.\n${e.message}.") - console.error("Cause: ${e.message ?: "No information available"}") + console.error("Input arguments are invalid.") + console.error("Cause: ${e.message ?: "No information available."}") + exitProcess(1) } catch (e: Exception){ console.error("Program was interrupted due to unexpected error.") - console.error("Cause: ${e.message ?: "No information available"}") + console.error("Cause: ${e.message ?: "No information available."}") + exitProcess(1) } } @@ -44,8 +47,8 @@ object CLIApplication: Application { * @param topologyReader the reader used to load the topology into memory * @param loadBlock the code block to load the topology. */ - override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, - loadBlock: () -> Topology<*>): Topology<*> { + override fun loadTopology(topologyFile: File, topologyReader: TopologyReaderHandler, + loadBlock: () -> Topology): Topology { try { console.info("Topology file: ${topologyFile.path}.") @@ -77,8 +80,21 @@ object CLIApplication: Application { * @param destinationID the destination ID * @param block the block of code to find the destination */ - override fun findDestination(destinationID: NodeID, block: () -> Node<*>?): Node<*> { - val destination: Node<*>? = block() + override fun findDestination(destinationID: NodeID, block: () -> Node?): Node { + + val destination= try { + block() + + } catch (exception: ParseException) { + console.error("Failed to parse stubs file.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(1) + + } catch (exception: IOException) { + console.error("Failed to read stubs file due to IO error.") + console.error("Cause: ${exception.message ?: "No information available"}") + exitProcess(2) + } if (destination == null) { console.error("Destination `$destinationID` was not found.") @@ -96,7 +112,7 @@ object CLIApplication: Application { * @param seed the seed of the message delay generator used for the execution * @param executeBlock the code block that performs one execution */ - override fun execute(executionID: Int, destination: Node<*>, seed: Long, executeBlock: () -> Unit) { + override fun execute(executionID: Int, destination: Node, seed: Long, executeBlock: () -> Unit) { console.info("Executing $executionID (destination=${destination.id} and seed=$seed)... ", inline = true) val (duration, _) = timer { @@ -112,8 +128,10 @@ object CLIApplication: Application { try { console.info("Running...") - runBlock() - console.info("Finished run") + val (duration, _) = timer { + runBlock() + } + console.info("Finished run in $duration in seconds") } catch (exception: IOException) { console.error("Failed to report results due to an IO error.") diff --git a/src/main/kotlin/ui/cli/InputArgumentsParser.kt b/src/main/kotlin/ui/cli/InputArgumentsParser.kt index 3fc6260..6ea47b9 100644 --- a/src/main/kotlin/ui/cli/InputArgumentsParser.kt +++ b/src/main/kotlin/ui/cli/InputArgumentsParser.kt @@ -1,12 +1,16 @@ package ui.cli +import bgp.BGP import core.simulator.RandomDelayGenerator import io.InterdomainTopologyReaderHandler +import io.parseInterdomainExtender import org.apache.commons.cli.CommandLine import org.apache.commons.cli.DefaultParser import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException import simulation.* import java.io.File +import java.util.* /** * Created on 30-08-2017 @@ -25,6 +29,7 @@ class InputArgumentsParser { private val MAX_DELAY = "maxdelay" private val THRESHOLD = "threshold" private val SEED = "seed" + private val STUBS = "stubs" private val options = Options() @@ -38,19 +43,27 @@ class InputArgumentsParser { options.addOption(MAX_DELAY, true, "maximum message delay (inclusive)") options.addOption("th", THRESHOLD, true, "threshold value") options.addOption(SEED, true, "first seed used for generate message delays") + options.addOption(STUBS, true, "path to stubs file") } @Throws(InputArgumentsException::class) fun parse(args: Array): Pair { - DefaultParser().parse(options, args).let { + val commandLine = try { + DefaultParser().parse(options, args) + } catch (e: ParseException) { + throw InputArgumentsException(e.message.toString()) + } + + commandLine.let { - val topologyFile = getFile(it, option = TOPOLOGY_FILE) + val topologyFile = getFile(it, option = TOPOLOGY_FILE).get() val destination = getNonNegativeInteger(it, option = DESTINATION) val repetitions = getPositiveInteger(it, option = REPETITIONS, default = 1) val reportDirectory = getDirectory(it, option = REPORT_DIRECTORY, default = File(System.getProperty("user.dir"))) val threshold = getPositiveInteger(it, option = THRESHOLD, default = 1_000_000) val seed = getLong(it, option = SEED, default = System.currentTimeMillis()) + val stubsFile = getFile(it, option = STUBS, default = Optional.empty()) val minDelay = getPositiveInteger(it, option = MIN_DELAY, default = 1) val maxDelay = getPositiveInteger(it, option = MAX_DELAY, default = 1) @@ -66,7 +79,20 @@ class InputArgumentsParser { val topologyReader = InterdomainTopologyReaderHandler(topologyFile) val messageDelayGenerator = RandomDelayGenerator.with(minDelay, maxDelay, seed) - val runner = RepetitionRunner(topologyFile, topologyReader, destination, repetitions, messageDelayGenerator) + val stubDB = if (stubsFile.isPresent) { + StubDB(stubsFile.get(), BGP(), ::parseInterdomainExtender) + } else { + null + } + + val runner = RepetitionRunner( + topologyFile, + topologyReader, + destination, + repetitions, + messageDelayGenerator, + stubDB + ) val execution = SimpleAdvertisementExecution(threshold).apply { dataCollectors.add(BasicDataCollector(reportFile)) } @@ -74,18 +100,16 @@ class InputArgumentsParser { return Pair(runner, execution) } } - @Throws(InputArgumentsException::class) - private fun getFile(commandLine: CommandLine, option: String, default: File? = null): File { + private fun getFile(commandLine: CommandLine, option: String, default: Optional? = null): Optional { verifyOption(commandLine, option, default) val value = commandLine.getOptionValue(option) - val file = if (value != null) File(value) else default!! // See note below - + val file = if (value != null) Optional.of(File(value)) else default!! // See note below // Note: the verifyOption method would throw exception if the option was ot defined and default was null - if (!file.isFile) { - throw InputArgumentsException("The file specified for `$option` does not exist: ${file.path}") + if (file.isPresent && !file.get().isFile) { + throw InputArgumentsException("The file specified for `$option` does not exist: ${file.get().path}") } return file diff --git a/src/main/kotlin/utils/HelperFunctions.kt b/src/main/kotlin/utils/HelperFunctions.kt new file mode 100644 index 0000000..ce5b6e6 --- /dev/null +++ b/src/main/kotlin/utils/HelperFunctions.kt @@ -0,0 +1,26 @@ +package utils + +/** + * Created on 31-08-2017 + * + * @author David Fialho + * + * This file contains a set of helper functions and extension function. + */ + +/** + * Parses a string as a non-negative integer number and returns the result + * + * @return the non-negative integer + * @throws NumberFormatException - if the string is not a valid representation of a number. + */ +@Throws(NumberFormatException::class) +fun String.toNonNegativeInt(): Int { + + val value = this.toInt() + if (value < 0) { + throw NumberFormatException() + } + + return value +} diff --git a/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt b/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt new file mode 100644 index 0000000..a0ed805 --- /dev/null +++ b/src/test/kotlin/bgp/SSBGP2WithShortestPathRoutingTests.kt @@ -0,0 +1,272 @@ +package bgp + +import core.routing.pathOf +import core.simulator.Engine +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.hamcrest.Matchers.nullValue +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import testing.* +import testing.bgp.pathOf + +/** + * Created on 01-09-2017 + * + * @author David Fialho + */ +object SSBGP2WithShortestPathRoutingTests: Spek({ + + given("topology with a single link from 2 to 1 with cost 10") { + + val topology = bgpTopology { + node { 1 deploying SSBGP2() } + node { 2 deploying SSBGP2() } + + link { 2 to 1 withCost 10 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.reset() + } + + val node1 = topology[1]!! + val node2 = topology[2]!! + val protocol1 = node1.protocol as SSBGP2 + val protocol2 = node2.protocol as SSBGP2 + + on("simulating with node 1 as the destination") { + + val terminated = Engine.simulate(topology, node1, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting self route") { + assertThat(protocol1.routingTable.getSelectedRoute(), Is(BGPRoute.self())) + } + + it("finishes with node 1 selecting route via himself") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), Is(node1)) + } + + it("finishes with node 2 selecting route with LOCAL-PREF=10 and AS-PATH=[1]") { + assertThat(protocol2.routingTable.getSelectedRoute(), Is(BGPRoute.with(10, pathOf(node1)))) + } + + it("finishes with node 2 selecting route via node 1") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), Is(node1)) + } + } + + on("simulating with node 2 as the destination") { + + val terminated = Engine.simulate(topology, node2, threshold = 1000) + + it("terminated") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting an invalid route") { + assertThat(protocol1.routingTable.getSelectedRoute(), Is(BGPRoute.invalid())) + } + + it("finishes with node 1 selecting null neighbor") { + assertThat(protocol1.routingTable.getSelectedNeighbor(), Is(nullValue())) + } + + it("finishes with node 2 selecting self route") { + assertThat(protocol2.routingTable.getSelectedRoute(), Is(BGPRoute.self())) + } + + it("finishes with node 2 selecting route via himself") { + assertThat(protocol2.routingTable.getSelectedNeighbor(), Is(node2)) + } + } + } + + given("topology with 4 where three form a cycle and all three have a link for node 0") { + + val topology = bgpTopology { + node { 0 deploying SSBGP2() } + node { 1 deploying SSBGP2() } + node { 2 deploying SSBGP2() } + node { 3 deploying SSBGP2() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost 1 } + link { 2 to 3 withCost -1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.reset() + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP2 } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 0 via node 0") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 disabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(false)) + } + + it("finishes with link from 2 to 3 disabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(false)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } + + given("topology with absorbent cycle") { + + val topology = bgpTopology { + node { 0 deploying SSBGP2() } + node { 1 deploying SSBGP2() } + node { 2 deploying SSBGP2() } + node { 3 deploying SSBGP2() } + + link { 1 to 0 withCost 0 } + link { 2 to 0 withCost 0 } + link { 3 to 0 withCost 0 } + link { 1 to 2 withCost -3 } + link { 2 to 3 withCost 1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.reset() + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP2 } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 3 via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 3, asPath = pathOf(0, 1, 3)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 enabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(true)) + } + + it("finishes with link from 2 to 3 enabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(true)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } + + given("topology with non-absorbent cycle, but only one node has an external route") { + + val topology = bgpTopology { + node { 0 deploying SSBGP2() } + node { 1 deploying SSBGP2() } + node { 2 deploying SSBGP2() } + node { 3 deploying SSBGP2() } + + link { 1 to 0 withCost 0 } + link { 1 to 2 withCost -1 } + link { 2 to 3 withCost 1 } + link { 3 to 1 withCost 2 } + } + + afterEachTest { + Engine.scheduler.reset() + topology.reset() + } + + val node = topology.nodes.sortedBy { it.id } + val protocol = node.map { it.protocol as SSBGP2 } + + on("simulating with node 0 as the destination") { + + val terminated = Engine.simulate(topology, node[0], threshold = 1000) + + it("terminates") { + assertThat(terminated, Is(true)) + } + + it("finishes with node 1 selecting route with cost 0 via node 0") { + assertThat(protocol[1].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 0, asPath = pathOf(0)))) + } + + it("finishes with node 2 selecting route with cost 3 via node 3") { + assertThat(protocol[2].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 3, asPath = pathOf(0, 1, 3)))) + } + + it("finishes with node 3 selecting route with cost 2 via node 1") { + assertThat(protocol[3].routingTable.getSelectedRoute(), + Is(BGPRoute.with(localPref = 2, asPath = pathOf(0, 1)))) + } + + it("finishes with link from 1 to 2 enabled") { + assertThat(protocol[1].routingTable.table.isEnabled(node[2]), Is(true)) + } + + it("finishes with link from 2 to 3 enabled") { + assertThat(protocol[2].routingTable.table.isEnabled(node[3]), Is(true)) + } + + it("finishes with link from 3 to 1 enabled") { + assertThat(protocol[3].routingTable.table.isEnabled(node[1]), Is(true)) + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/io/StubParserTest.kt b/src/test/kotlin/io/StubParserTest.kt new file mode 100644 index 0000000..6bb9c78 --- /dev/null +++ b/src/test/kotlin/io/StubParserTest.kt @@ -0,0 +1,152 @@ +package io + +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` as Is +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.on +import org.jetbrains.spek.api.dsl.it +import org.junit.jupiter.api.Assertions.assertThrows +import java.io.StringReader + +/** + * Created on 31-08-2017 + * + * @author David Fialho + */ +object StubParserTest: Spek({ + + given("an empty file") { + + val fileContent = "" + val handler: StubParser.Handler = mock() + + on("parsing the file") { + + it("does not throw anything") { + StubParser(StringReader(fileContent)).use { + it.parse(handler) + } + } + + it("does not parse any stub item") { + verify(handler, never()).onStubLink(any(), any(), any(), any()) + } + } + } + + given("file with single line `1 | 2 | C`") { + + val fileContent = "1 | 2 | C" + val handler: StubParser.Handler = mock() + + on("parsing the file") { + + StubParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed a single stub") { + verify(handler, times(1)).onStubLink(any(), any(), any(), any()) + } + + it("parsed in line 1 a stub with ID 1, neighbor 2, and extender with label `C`") { + verify(handler, times(1)).onStubLink(id = 1, inNeighbor = 2, label = "C", currentLine = 1) + } + } + } + + fun lines(vararg lines: String): String = lines.joinToString("\n") + + given("file with single lines `1 | 2 | C`, `1 | 3 | R`") { + + val fileContent = lines( + "1 | 2 | C", + "1 | 3 | R" + ) + val handler: StubParser.Handler = mock() + + on("parsing the file") { + + StubParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed two stubs") { + verify(handler, times(2)).onStubLink(any(), any(), any(), any()) + } + + it("parsed in line 1 a stub with ID 1, neighbor 2, and extender with label `C`") { + verify(handler, times(1)).onStubLink(id = 1, inNeighbor = 2, label = "C", currentLine = 1) + } + + it("parsed in line 2 a stub with ID 1, neighbor 3, and extender with label `R`") { + verify(handler, times(1)).onStubLink(id = 1, inNeighbor = 3, label = "R", currentLine = 2) + } + } + } + + given("file with single lines `1 | 2 | C`, ` `, `1 | 3 | R`") { + + val fileContent = lines( + "1 | 2 | C", + " ", + "1 | 3 | R" + ) + val handler: StubParser.Handler = mock() + + on("parsing the file") { + + StubParser(StringReader(fileContent)).use { + it.parse(handler) + } + + it("parsed two stubs") { + verify(handler, times(2)).onStubLink(any(), any(), any(), any()) + } + + it("parsed in line 1 a stub with ID 1, neighbor 2, and extender with label `C`") { + verify(handler, times(1)).onStubLink(id = 1, inNeighbor = 2, label = "C", currentLine = 1) + } + + it("parsed in line 3 a stub with ID 1, neighbor 3, and extender with label `R`") { + verify(handler, times(1)).onStubLink(id = 1, inNeighbor = 3, label = "R", currentLine = 3) + } + } + } + + val incorrectLines = listOf( + "1 | 2 ", + "1 | 2 |", + "a | 2 | C", + "1 | b | C", + " | | " + ) + + incorrectLines.forEach { line -> + + given("file with incorrect line `$line`") { + + val handler: StubParser.Handler = mock() + + on("parsing the file") { + + var exception: ParseException? = null + + it("throws a ParseException") { + StubParser(StringReader(line)).use { + exception = assertThrows(ParseException::class.java) { + it.parse(handler) + } + } + } + + it("indicates the error is in line 1") { + assertThat(exception?.lineNumber, Is(1)) + } + } + } + } + +}) \ No newline at end of file