-
Henrik Thostrup Jensen authoredHenrik Thostrup Jensen authored
nml.py 15.85 KiB
"""
OpenNSA NML topology model.
Author: Henrik Thostrup Jensen <htj@nordu.net>
Copyright: NORDUnet (2011-2013)
"""
import itertools
import datetime
from twisted.python import log
from opennsa import constants as cnt, nsa, error
LOG_SYSTEM = 'opennsa.topology'
INGRESS = 'ingress'
EGRESS = 'egress'
class Port(object):
def __init__(self, id_, name, label, remote_port=None):
assert not id_.startswith('urn:'), 'URNs are not used in core OpenNSA NML (id: %s)' % id_
assert ':' not in name, 'Invalid port name %s, must not contain ":"' % name
if label is not None:
assert type(label) is nsa.Label, 'label must be nsa.Label or None, not type(%s)' % str(type(label))
self.id_ = id_ # The URN of the port
self.name = name # String ; Base name, no network name or uri prefix
self._label = label # nsa.Label ; can be None
self.remote_port = remote_port # String
def canMatchLabel(self, label):
return nsa.Label.canMatch(self, label)
def isBidirectional(self):
return False
def label(self):
return self._label
def hasRemote(self):
return self.remote_port != None
def __repr__(self):
return '<Port %s (%s) # %s -> %s>' % (self.id_, self.name, self._label, self.remote_port)
class InternalPort(Port):
"""
Same as Port, but also has a bandwidth, so the pathfinder can probe for bandwidth.
"""
def __init__(self, id_, name, bandwidth, label, remote_port=None):
super(InternalPort, self).__init__(id_, name, label, remote_port)
self.bandwidth = bandwidth
def canProvideBandwidth(self, desired_bandwidth):
return desired_bandwidth <= self.bandwidth
def __repr__(self):
return '<InternalPort %s (%s) # %s : %i -> %s>' % (self.id_, self.name, self._label, self.bandwidth, self.remote_port)
class BidirectionalPort(object):
def __init__(self, id_, name, inbound_port, outbound_port, remote_port=None):
assert type(id_) is str, 'Port id must be a string'
assert type(name) is str, 'Port name must be a string'
assert isinstance(inbound_port, Port), 'Inbound port must be a <Port>'
assert isinstance(outbound_port, Port), 'Outbound port must be a <Port>'
if inbound_port.label() is None and outbound_port.label() is None:
pass # ok
else:
assert inbound_port.label().type_ == outbound_port.label().type_, 'Port labels must match each other'
assert not id_.startswith('urn:'), 'URNs are not used in core OpenNSA NML (id: %s)' % id_
self.id_ = id_
self.name = name
self.inbound_port = inbound_port
self.outbound_port = outbound_port
self.remote_port = remote_port # hack on!
def isBidirectional(self):
return True
def label(self):
if self.inbound_port.label() and self.outbound_port.label():
return self.inbound_port.label().intersect(self.outbound_port.label())
else:
return None
def canMatchLabel(self, label):
return self.inbound_port.canMatchLabel(label) and self.outbound_port.canMatchLabel(label)
def hasRemote(self):
return self.inbound_port.hasRemote() and self.outbound_port.hasRemote()
def canProvideBandwidth(self, desired_bandwidth):
return self.inbound_port.canProvideBandwidth(desired_bandwidth) and self.outbound_port.canProvideBandwidth(desired_bandwidth)
def __repr__(self):
return '<BidirectionalPort %s (%s) : %s/%s>' % (self.id_, self.name, self.inbound_port.name, self.outbound_port.name)
class Network(object):
def __init__(self, id_, name, inbound_ports, outbound_ports, bidirectional_ports, version=None):
assert type(id_) is str, 'Network id must be a string'
assert type(name) is str, 'Network name must be a string'
assert type(inbound_ports) is list, 'Inbound ports must be a list'
assert type(outbound_ports) is list, 'Outbound network ports must be a list'
assert type(bidirectional_ports) is list, 'Bidirectional network ports must be a list'
assert not id_.startswith('urn:'), 'URNs are not used in core OpenNSA NML (id: %s)' % id_
# we should perhaps check that no ports has the same name
self.id_ = id_ # String ; the urn of the network topology
self.name = name # String ; just base name, no prefix or URI stuff
self.inbound_ports = inbound_ports or []
self.outbound_ports = outbound_ports or []
self.bidirectional_ports = bidirectional_ports or []
self.version = version or datetime.datetime.utcnow().replace(microsecond=0)
def getPort(self, port_id):
for port in itertools.chain(self.inbound_ports, self.outbound_ports, self.bidirectional_ports):
if port.id_ == port_id:
return port
# better error message
ports = [ p.id_ for p in list(itertools.chain(self.inbound_ports, self.outbound_ports, self.bidirectional_ports)) ]
raise error.STPUnavailableError('No port named %s for network %s (ports: %s)' %(port_id, self.id_, str(ports)))
def findPorts(self, bidirectionality, label=None, exclude=None):
matching_ports = []
for port in itertools.chain(self.inbound_ports, self.outbound_ports, self.bidirectional_ports):
if port.isBidirectional() == bidirectionality and (label is None or port.canMatchLabel(label)):
if exclude and port.id_ == exclude:
continue
matching_ports.append(port)
return matching_ports
def canSwapLabel(self, label_type):
return False # not really clear how nml expresses this yet
class Topology(object):
def __init__(self):
self.networks = {} # network_name -> ( Network, nsa.NetworkServiceAgent)
def addNetwork(self, network, managing_nsa):
assert type(network) is Network
assert type(managing_nsa) is nsa.NetworkServiceAgent
if network.id_ in self.networks:
raise error.TopologyError('Entry for network with id %s already exists' % network.id_)
self.networks[network.id_] = (network, managing_nsa)
def updateNetwork(self, network, managing_nsa):
# update an existing network entry
existing_entry = self.networks.pop(network.id_, None) # note - we may get none here (for new network)
try:
self.addNetwork(network, managing_nsa)
except error.TopologyError as e:
log.msg('Error updating network entry for %s. Reason: %s' % (network.id_, str(e)))
if existing_entry:
self.networks[network.id_] = existing_entry # restore old entry
raise e
def getNetwork(self, network_id):
try:
return self.networks[network_id][0]
except KeyError:
raise error.TopologyError('No network with id %s' % (network_id))
def getNetworkPort(self, port_id):
for network_id, (network,_) in self.networks.items():
try:
port = network.getPort(port_id)
return network_id, port
except error.TopologyError:
continue
else:
raise error.TopologyError('Cannot find port with id %s in topology' % port_id)
def getNSA(self, network_id):
try:
return self.networks[network_id][1]
except KeyError as e:
raise error.TopologyError('No NSA for network with id %s (%s)' % (network_id, str(e)))
def findDemarcationPort(self, port):
# finds - if it exists - the demarcation port of a bidirectional port - have to go through unidirectional model
assert isinstance(port, BidirectionalPort), 'Specified port for demarcation find is not bidirectional'
if not port.hasRemote():
return None
try:
remote_network_in, remote_port_in = self.getNetworkPort(port.outbound_port.remote_port)
remote_network_out, remote_port_out = self.getNetworkPort(port.inbound_port.remote_port)
if remote_network_in != remote_network_out:
log.msg('Bidirectional port %s leads to multiple networks. Topology screwup?' % port.id_, system=LOG_SYSTEM)
return None
except error.TopologyError as e:
log.msg('Error looking up demarcation port for %s. Message: %s' % (port.id_, str(e)), system=LOG_SYSTEM)
return None
remote_network = self.getNetwork(remote_network_in)
for rp in remote_network.findPorts(True):
if isinstance(rp, BidirectionalPort) and rp.inbound_port.id_ == remote_port_in.id_ and rp.outbound_port.id_ == remote_port_out.id_:
return remote_network.id_, rp.id_
return None
def findPaths(self, source_stp, dest_stp, bandwidth, exclude_networks=None):
source_port = self.getNetwork(source_stp.network).getPort(source_stp.port)
dest_port = self.getNetwork(dest_stp.network).getPort(dest_stp.port)
if source_port.isBidirectional() or dest_port.isBidirectional():
# at least one of the stps are bidirectional
if not source_port.isBidirectional():
raise error.TopologyError('Cannot connect bidirectional source with unidirectional destination')
if not dest_port.isBidirectional():
raise error.TopologyError('Cannot connect bidirectional destination with unidirectional source')
else:
# both ports are unidirectional
if not (source_port.orientation, dest_port.orientation) in ( (INGRESS, EGRESS), (EGRESS, INGRESS) ):
raise error.TopologyError('Cannot connect STPs of same unidirectional direction (%s -> %s)' % (source_port.orientation, dest_port.orientation))
# these are only really interesting for the initial call, afterwards they just prune
if not source_port.canMatchLabel(source_stp.label):
raise error.TopologyError('Source port %s (label %s) cannot match label for source STP (%s)' % (source_port.id_, source_port.label(), source_stp.label))
if not dest_port.canMatchLabel(dest_stp.label):
raise error.TopologyError('Desitination port %s (label %s) cannot match label for destination STP %s' % (dest_port.id_, dest_port.label(), dest_stp.label))
# if not source_port.canProvideBandwidth(bandwidth):
# raise error.BandwidthUnavailableError('Source port cannot provide enough bandwidth (%i)' % bandwidth)
# if not dest_port.canProvideBandwidth(bandwidth):
# raise error.BandwidthUnavailableError('Destination port cannot provide enough bandwidth (%i)' % bandwidth)
return self._findPathsRecurse(source_stp, dest_stp, bandwidth)
def _findPathsRecurse(self, source_stp, dest_stp, bandwidth, exclude_networks=None):
source_network = self.getNetwork(source_stp.network)
dest_network = self.getNetwork(dest_stp.network)
source_port = source_network.getPort(source_stp.port)
dest_port = dest_network.getPort(dest_stp.port)
if not (source_port.canMatchLabel(source_stp.label) or dest_port.canMatchLabel(dest_stp.label)):
return []
# if not (source_port.canProvideBandwidth(bandwidth) and dest_port.canProvideBandwidth(bandwidth)):
# return []
if source_port.isBidirectional() and dest_port.isBidirectional():
# bidirectional path finding, easy case first
if source_stp.network == dest_stp.network:
# while it is possible to cross other network in order to connect to intra-network STPs
# it is not something we really want to do in the real world, so we don't
try:
if source_network.canSwapLabel(source_stp.label.type_):
source_label = source_port.label().intersect(source_stp.label)
dest_label = dest_port.label().intersect(dest_stp.label)
else:
source_label = source_port.label().intersect(dest_port.label()).intersect(source_stp.label).intersect(dest_stp.label)
dest_label = source_label
link = nsa.Link(source_stp.network, source_stp.port, dest_stp.port, source_label, dest_label)
return [ [ link ] ]
except nsa.EmptyLabelSet:
return [] # no path
else:
# ok, time for real pathfinding
link_ports = source_network.findPorts(True, source_stp.label, source_stp.port)
link_ports = [ port for port in link_ports if port.hasRemote() ] # filter out termination ports
links = []
for lp in link_ports:
demarcation = self.findDemarcationPort(lp)
if demarcation is None:
continue
d_network_id, d_port_id = demarcation
if exclude_networks is not None and demarcation[0] in exclude_networks:
continue # don't do loops in path finding
demarcation_label = lp.label() if source_network.canSwapLabel(source_stp.label.type_) else source_stp.label.intersect(lp.label())
demarcation_stp = nsa.STP(demarcation[0], demarcation[1], demarcation_label)
sub_exclude_networks = [ source_network.id_ ] + (exclude_networks or [])
sub_links = self._findPathsRecurse(demarcation_stp, dest_stp, bandwidth, sub_exclude_networks)
# if we didn't find any sub paths, just continue
if not sub_links:
continue
for sl in sub_links:
# --
if source_network.canSwapLabel(source_stp.label.type_):
source_label = source_port.label().intersect(source_stp.label)
dest_label = lp.label().intersect(sl[0].src_stp.label)
else:
source_label = source_port.label().intersect(source_stp.label).intersect(lp.label()).intersect(sl[0].src_stp.label)
dest_label = source_label
first_link = nsa.Link(source_stp.network, source_stp.port, lp.id_, source_label, dest_label)
path = [ first_link ] + sl
links.append(path)
return sorted(links, key=len) # sort by length, shortest first
else:
raise error.TopologyError('Unidirectional path-finding not implemented yet')
def createNMLNetwork(nrm_ports, network_name, network_readable_name):
# create an nml network (topology) from a list of nrm ports
inbound_ports = []
outbound_ports = []
bidirectional_ports = []
for port in nrm_ports:
assert port.port_type == cnt.ETHERNET, 'Sorry can only do ethernet ports for now'
inbound_port_name = port.name + '-in'
outbound_port_name = port.name + '-out'
port_id = network_name + ':' + port.name
inbound_port_id = network_name + ':' + inbound_port_name
outbound_port_id = network_name + ':' + outbound_port_name
inbound_port = InternalPort(inbound_port_id, inbound_port_name, port.bandwidth, port.label, port.remote_out)
outbound_port = InternalPort(outbound_port_id, outbound_port_name, port.bandwidth, port.label, port.remote_in)
bidirectional_port = BidirectionalPort(port_id, port.name, inbound_port, outbound_port, port.remote_port)
inbound_ports.append(inbound_port)
outbound_ports.append(outbound_port)
bidirectional_ports.append(bidirectional_port)
return Network(network_name, network_readable_name, inbound_ports, outbound_ports, bidirectional_ports)