diff --git a/datafiles/schema.sql b/datafiles/schema.sql
index c8cca318b137f6fe18e61717e911ffafb9701073..9ce1f579d38dbf728dca8a10b1b3203d2a998d9c 100644
--- a/datafiles/schema.sql
+++ b/datafiles/schema.sql
@@ -101,3 +101,15 @@ CREATE TABLE generic_backend_connections (
allocated boolean NOT NULL -- indicated if the resources are actually allocated
);
+
+-- Force this to only have a single row
+-- generate new id with:
+-- there needs to be a conflict check to see if the backend has a row (and insert corrosonding start value)
+-- INSERT INTO backend_connection_id (connection_id) VALUES (190000) ON CONFLICT DO NOTHING;
+-- Generate new id with:
+-- UPDATE backend_connection_id SET connection_id = connection_id + 1 RETURNING connection_id;
+CREATE TABLE backend_connection_id (
+ id integer PRIMARY KEY NOT NULL DEFAULT(1) CHECK (id = 1),
+ connection_id serial NOT NULL
+);
+
diff --git a/opennsa/backends/junipervpls.py b/opennsa/backends/junipervpls.py
new file mode 100644
index 0000000000000000000000000000000000000000..44ad96783afebe7906257ba414c9a5297012da97
--- /dev/null
+++ b/opennsa/backends/junipervpls.py
@@ -0,0 +1,430 @@
+"""
+OpenNSA Juniper/JunOS VPLS backend.
+
+Intended to match Canaries usage.
+
+Requires a JunOS device with VPLS support (duh)
+
+Author: Henrik Thostrup Jensen <htj@nordu.net>
+Copyright: NORDUnet (2017)
+
+"""
+
+# Configuration conventions / environment, and snippets
+
+# We assign each service a unique Circuit-ID and this CID, or parts of it,
+# are used for provisioning
+#
+# Circuit-ID follows this naming/syntax <Unique_ID> - <Organizational_ID>
+# - CANARIE_site_short<1..9> - CANARIE_site_short<1..9> - Service_Name
+# for example, 13903CS01-NORDUNet-AMST1-NYCN1
+#
+# This circuit ID is used in "description" of sub-interface/logical unit
+# number for example, description "13903CS01-NORDUNet-AMST1-NYCN1
+# [with some extra information in square brackets .... L2VPN circuit
+# Amsterdam to New York for XYZ]";
+
+# This circuit ID is also used as the name for the routing instance
+# for example,
+# routing-instances {
+# 13903CS01-NORDUNet-AMST1-NYCN1 {
+# .....
+#
+# route-distinguisher and vrf-target for the routing instance are also
+# composed using the information that derives from <Unique_ID>
+# The two characters (in this example CS) are removed and seven digest
+# used for the second, after ":" part. The first part is our AS number.
+#
+# for unique_id "13903CS01", for example
+# route-distinguisher 6509:1390301;
+# vrf-target target:6509:1390301;
+#
+# the sites for the routing instance are directly derived from Circuit-ID,
+# site AMST1 {
+# site-identifier 1;
+# .....
+# site NYCN1 {
+# site-identifier 2;
+# ....
+#
+# logical unit number normally matches a vlan ID (or a first VLAN number in a list)
+#
+# interfaces {
+# et-x/y/z {
+# description "ANA-300 link to Amsterdam";
+# flexible-vlan-tagging;
+# encapsulation flexible-ethernet-services;
+# .....
+# unit 1000 {
+# description "13903CS01-NORDUNet-AMST1-NYCN1 [L2VPN circuit Amsterdam to New York for NORDUNet]";
+# encapsulation vlan-vpls;
+# vlan-id-list 1000;
+# family vpls;
+# }
+#
+# et-x/y/w {
+# description "CANARIE/ANA-300 link to New York City";
+# flexible-vlan-tagging;
+# encapsulation flexible-ethernet-services;
+# .....
+# unit 1000 {
+# description "13903CS01-NORDUNet-AMST1-NYCN1 [L2VPN circuit Amsterdam to New York for NORDUNet]";
+# encapsulation vlan-vpls;
+# vlan-id-list 1000;
+# family vpls;
+# }
+# }
+#
+#
+# routing-instances {
+# 13903CS01-NORDUNet-AMST1-NYCN1 {
+# instance-type vpls;
+# interface et-x/y/z.1000;
+# interface et-x/y/w.1000;
+# route-distinguisher 6509:1390301;
+# vrf-target target:6509:1390301;
+# protocols {
+# vpls {
+# site-range 2;
+# no-tunnel-services;
+# site AMST1 {
+# site-identifier 1;
+# interface et-x/y/z.1000;
+# }
+# site NYCN1 {
+# site-identifier 2;
+# interface et-x/y/w.1000;
+# }
+# }
+# }
+# }
+#
+
+
+#import random
+
+from twisted.python import log
+from twisted.internet import defer
+
+from opennsa import constants as cnt, config, database
+from opennsa.backends.common import genericbackend, ssh
+
+
+LOG_SYSTEM = 'JuniperVPLS'
+
+
+# JunOS commands, static
+CONFIGURE = 'configure'
+COMMIT = 'commit'
+
+# JunOS commands, parameterized
+
+# Interface unit configuration
+#SET_UNIT = 'set interfaces %(interface) unit %(unit)'
+#SET_UNIT_DESCRIPTION = 'set interfaces %(interface) unit %(unit) description %(description)'
+#SET_UNIT_ENCAPSULATION = 'set interfaces %(interface) unit %(unit) encapsulation vlan-vpls'
+#SET_UNIT_VLAN = 'set interfaces %(interface) unit %(unit) vlan-id-list %(vlan)'
+#SET_UNIT_FAMILY = 'set interfaces %(interface) unit %(unit) family vpls'
+
+SET_UNIT = 'set interfaces %(interface) unit %(unit) description %(description) encapsulation vlan-vpls vlan-id %(vlan) family vpls'
+
+# Routing instance configuration
+#SET_RI = 'set routing-instance %(instance)'
+SET_RI_INSTANCE_TYPE = 'set routing-instances %(instance) instance-type vpls'
+SET_RI_INTERFACE = 'set routing-instances %(instance) interface %(interface)'
+SET_RI_ROUTE_DISTINGUISHER = 'set routing-instances %(instance) route-distinguisher %(route-distinguisher)'
+SET_RI_VRF_TARGET = 'set routing-instances %(instance) vrf-target %(vrf-target)'
+SET_RI_PROTOCOLS = 'set routing-instances %(instance) protocols vpls site-range 2 no-tunnel-services'
+SET_RI_VPLS_SITE = 'set routing-instances %(instance) protocols vpls site %(site) site-identifier %(site-id) interface %(interface)'
+#SET_RI_VPLS_SITE2 = 'set routing-instances %(instance) protocols vpls site %(site) site-identifier 2 interface %(interface)'
+
+# Delete statements
+DELETE_UNIT = 'delete interfaces %(interface) unit $(unit)'
+DELETE_ROUTING_INSTANCE = 'delete routing-instance %(instance)'
+
+
+
+def createSetupCommands(source_port, dest_port, vlan, instance_id, description, route_distinguiser, vrf_target):
+
+ commands = [
+ SET_UNIT % {'interface': source_port, 'unit': vlan, 'description': description, 'vlan': vlan},
+ SET_UNIT % {'interface': dest_port, 'unit': vlan, 'description': description, 'vlan': vlan},
+
+ SET_RI_INSTANCE_TYPE % {'instance': instance_id },
+ SET_RI_INTERFACE % {'instance': instance_id, 'interface': source_port + '.' + vlan },
+ SET_RI_INTERFACE % {'instance': instance_id, 'interface': dest_port + '.' + vlan },
+ SET_RI_ROUTE_DISTINGUISHER % {'instance': instance_id, 'route-distinguisher': route_distinguiser },
+ SET_RI_VRF_TARGET % {'instance': instance_id, 'vrf-target': vrf_target },
+ SET_RI_PROTOCOLS % {'instance': instance_id },
+ SET_RI_VPLS_SITE % {'instance': instance_id, 'site': 'SITE1', 'site-id': 1 },
+ SET_RI_VPLS_SITE % {'instance': instance_id, 'site': 'SITE2', 'site-id': 2 }
+ ]
+
+ return commands
+
+
+def createDeleteCommands(source_port, dest_port, vlan, instance_id):
+
+ commands = [
+ DELETE_UNIT % {'interface': source_port, 'unit': vlan },
+ DELETE_UNIT % {'interface': dest_port, 'unit': vlan },
+ DELETE_ROUTING_INSTANCE % {'instance' : instance_id }
+ ]
+
+ return commands
+
+
+# ---
+
+
+class SSHChannel(ssh.SSHChannel):
+
+ name = 'session'
+
+ def __init__(self, conn):
+ ssh.SSHChannel.__init__(self, conn=conn)
+
+ self.line = ''
+
+ self.wait_defer = None
+ self.wait_line = None
+
+
+ @defer.inlineCallbacks
+ def sendCommands(self, commands):
+ LT = '\r' # line termination
+
+ try:
+ yield self.conn.sendRequest(self, 'shell', '', wantReply=1)
+ d = self.waitForLine('>')
+ self.write(CONFIGURE + LT)
+ yield d
+
+ log.msg('Entered configure mode', debug=True, system=LOG_SYSTEM)
+
+ for cmd in commands:
+ log.msg('CMD> %s' % cmd, system=LOG_SYSTEM)
+ d = self.waitForLine('[edit]')
+ self.write(cmd + LT)
+ yield d
+
+ # commit commands, check for 'commit complete' as success
+ # not quite sure how to handle failure here
+
+ d = self.waitForLine('commit complete')
+ self.write(COMMIT + LT)
+ yield d
+
+ except Exception, e:
+ log.msg('Error sending commands: %s' % str(e))
+ raise e
+
+ log.msg('Commands successfully committed', debug=True, system=LOG_SYSTEM)
+ self.sendEOF()
+ self.closeIt()
+
+
+ def waitForLine(self, line):
+ self.wait_line = line
+ self.wait_defer = defer.Deferred()
+ return self.wait_defer
+
+
+ def matchLine(self, line):
+ if self.wait_line and self.wait_defer:
+ if self.wait_line in line.strip():
+ d = self.wait_defer
+ self.wait_line = None
+ self.wait_defer = None
+ d.callback(self)
+ else:
+ pass
+
+
+ def dataReceived(self, data):
+ if len(data) == 0:
+ pass
+ else:
+ self.line += data
+ if '\n' in data:
+ lines = [ line.strip() for line in self.line.split('\n') if line.strip() ]
+ self.line = ''
+ for l in lines:
+ self.matchLine(l)
+
+
+
+class JuniperVPLSCommandSender:
+
+
+ def __init__(self, host, port, ssh_host_fingerprint, user, ssh_public_key_path, ssh_private_key_path):
+
+ self.ssh_connection_creator = \
+ ssh.SSHConnectionCreator(host, port, [ ssh_host_fingerprint ], user, ssh_public_key_path, ssh_private_key_path)
+
+ self.ssh_connection = None # cached connection
+
+
+ def _getSSHChannel(self):
+
+ def setSSHConnectionCache(ssh_connection):
+ log.msg('SSH Connection created and cached', system=LOG_SYSTEM)
+ self.ssh_connection = ssh_connection
+ return ssh_connection
+
+ def gotSSHConnection(ssh_connection):
+ channel = SSHChannel(conn = ssh_connection)
+ ssh_connection.openChannel(channel)
+ return channel.channel_open
+
+ if self.ssh_connection and not self.ssh_connection.transport.factory.stopped:
+ log.msg('Reusing SSH connection', debug=True, system=LOG_SYSTEM)
+ return gotSSHConnection(self.ssh_connection)
+ else:
+ # since creating a new connection should be uncommon, we log it
+ # this makes it possible to see if something fucks up and creates connections continuously
+ log.msg('Creating new SSH connection', system=LOG_SYSTEM)
+ d = self.ssh_connection_creator.getSSHConnection()
+ d.addCallback(setSSHConnectionCache)
+ d.addCallback(gotSSHConnection)
+ return d
+
+
+ def _sendCommands(self, commands):
+
+ def gotChannel(channel):
+ d = channel.sendCommands(commands)
+ return d
+
+ d = self._getSSHChannel()
+ d.addCallback(gotChannel)
+ return d
+
+
+ #def setupLink(self, source_port, source_vlan, dest_port, dest_vlan):
+ def setupLink(self, source_port, dest_port, vlan, instance_id, as_number):
+
+ # createSetupCommands(source_port, dest_port, vlan, instance_id, description, route_distinguiser, vrf_target)
+
+ description = instance_id + '[ X-connect created by OpenNSA ]'
+ unique_id = instance_id[:5] + instance_id[7:9]
+ route_distinguisher = as_number + ':' + unique_id
+ vrf_target = 'target:' + as_number + ':' + unique_id
+
+ commands = createSetupCommands(source_port, dest_port, vlan, instance_id, description, route_distinguisher, vrf_target)
+
+ return self._sendCommands(commands)
+
+
+ def teardownLink(self, source_port, dest_port, vlan, instance_id):
+
+ # createDeleteCommands(source_port, dest_port, vlan, instance_id)
+
+ commands = createDeleteCommands(source_port, dest_port, vlan, instance_id)
+
+ return self._sendCommands(commands)
+
+
+# --------
+
+
+class JunosUnitTarget(object):
+
+ def __init__(self, port, vlan):
+ self.port = port
+ self.vlan = vlan
+
+ def __str__(self):
+ return '<JunosUnitTarget %s.%i>' % (self.port, self.vlan)
+
+
+
+class JuniperVPLSConnectionManager:
+
+
+ def __init__(self, port_map, host, port, host_fingerprint, user, ssh_public_key, ssh_private_key, as_number):
+
+ self.port_map = port_map
+ self.command_sender = JuniperVPLSCommandSender(host, port, host_fingerprint, user, ssh_public_key, ssh_private_key)
+ self.as_number = as_number
+
+
+ def getResource(self, port, label):
+ assert label is not None and label.type_ == cnt.ETHERNET_VLAN, 'Label must be vlan'
+ device_port = self.port_map[port]
+ if device_port is None:
+ raise ValueError('Invalid port specified: %s' % device_port)
+ return port + '.' + label.labelValue()
+
+
+ def getTarget(self, port, label):
+ assert label is not None and label.type_ == cnt.ETHERNET_VLAN, 'Label must be vlan'
+ device_port = self.port_map[port]
+ if device_port is None:
+ raise ValueError('Invalid port specified: %s' % device_port)
+
+ vlan = int(label.labelValue())
+ assert 1 <= vlan <= 4095, 'Invalid label value for vlan: %s' % label.labelValues()
+
+ return JunosUnitTarget(self.port_map[port], vlan)
+
+
+ def createConnectionId(self, source_target, dest_target):
+ # This needs to be fixed!
+ unique_id = database.getBackendConnectionId()
+ if unique_id is None:
+ raise ValueError("Could not generate an connection id from the database, most likely serviceid_start isn't set")
+
+ # not quite done here...
+ connection_id = unique_id[:5] + 'CS' + unique_id[5:] + '-ANA'
+ print 'generated id', connection_id
+ return connection_id
+
+
+ def canSwapLabel(self, label_type):
+ # Not right now at least, maybe in the future
+ return False
+
+
+ def setupLink(self, connection_id, source_target, dest_target, bandwidth):
+
+ def linkUp(_):
+ log.msg('Link %s -> %s setup done' % (source_target, dest_target), system=LOG_SYSTEM)
+
+ assert source_target.vlan == dest_target.vlan, 'Source and destination vlan must match'
+
+ d = self.command_sender.setupLink(source_target.port, dest_target.port, dest_target.vlan, connection_id, self.as_number)
+ d.addCallback(linkUp)
+ return d
+
+
+ def teardownLink(self, connection_id, source_target, dest_target, bandwidth):
+
+ def linkDown(_):
+ log.msg('Link %s -> %s teardown done' % (source_target, dest_target), system=LOG_SYSTEM)
+
+ assert source_target.vlan == dest_target.vlan, 'Source and destination vlan must match'
+
+ d = self.command_sender.teardownLink(source_target.port, source_target.vlan, dest_target.port, dest_target.vlan)
+ d.addCallback(linkDown)
+ return d
+
+
+
+def JuniperVPLSBackend(network_name, nrm_ports, parent_requester, cfg):
+
+ name = 'JuniperVPLS %s' % network_name
+ nrm_map = dict( [ (p.name, p) for p in nrm_ports ] ) # for the generic backend
+ port_map = dict( [ (p.name, p.interface) for p in nrm_ports ] ) # for the nrm backend
+
+ # extract config items
+ host = cfg[config.JUNIPER_HOST]
+ port = cfg.get(config.JUNIPER_PORT, 22)
+ host_fingerprint = cfg[config.JUNIPER_HOST_FINGERPRINT]
+ user = cfg[config.JUNIPER_USER]
+ ssh_public_key = cfg[config.JUNIPER_SSH_PUBLIC_KEY]
+ ssh_private_key = cfg[config.JUNIPER_SSH_PRIVATE_KEY]
+ as_number = cfg[config.AS_NUMBER]
+
+ cm = JuniperVPLSConnectionManager(port_map, host, port, host_fingerprint, user, ssh_public_key, ssh_private_key, as_number)
+ return genericbackend.GenericBackend(network_name, nrm_map, cm, parent_requester, name)
diff --git a/opennsa/config.py b/opennsa/config.py
index 85e4b615063a1ddd1a5a95f8a0c162c87a83752b..b558ee616ee2bd1fd3835b6eb61e5915337f3665 100644
--- a/opennsa/config.py
+++ b/opennsa/config.py
@@ -27,6 +27,7 @@ DEFAULT_CERTIFICATE_DIR = '/etc/ssl/certs' # This will work on most mordern linu
BLOCK_SERVICE = 'service'
BLOCK_DUD = 'dud'
BLOCK_JUNIPER_EX = 'juniperex'
+BLOCK_JUNIPER_VPLS = 'junipervpls'
BLOCK_JUNOS = 'junos'
BLOCK_FORCE10 = 'force10'
BLOCK_BROCADE = 'brocade'
@@ -46,6 +47,7 @@ NRM_MAP_FILE = 'nrmmap'
PEERS = 'peers'
POLICY = 'policy'
PLUGIN = 'plugin'
+SERVICE_ID_START = 'serviceid_start'
# database
DATABASE = 'database' # mandatory
@@ -59,7 +61,7 @@ CERTIFICATE_DIR = 'certdir' # mandatory (but dir can be empty)
VERIFY_CERT = 'verify'
ALLOWED_HOSTS = 'allowedhosts' # comma seperated list
-# generic ssh stuff, don't use directly
+# generic stuff
_SSH_HOST = 'host'
_SSH_PORT = 'port'
_SSH_HOST_FINGERPRINT = 'fingerprint'
@@ -68,6 +70,8 @@ _SSH_PASSWORD = 'password'
_SSH_PUBLIC_KEY = 'publickey'
_SSH_PRIVATE_KEY = 'privatekey'
+AS_NUMBER = 'asnumber'
+
# juniper block - same for ex/qxf backend and mx backend
JUNIPER_HOST = _SSH_HOST
JUNIPER_PORT = _SSH_PORT
@@ -236,6 +240,11 @@ def readVerifyConfig(cfg):
except ConfigParser.NoOptionError:
vc[DATABASE_PASSWORD] = None
+ try:
+ vc[SERVICE_ID_START] = cfg.get(BLOCK_SERVICE, SERVICE_ID_START)
+ except ConfigParser.NoOptionError:
+ vc[SERVICE_ID_START] = None
+
# we always extract certdir and verify as we need that for performing https requests
try:
certdir = cfg.get(BLOCK_SERVICE, CERTIFICATE_DIR)
@@ -291,7 +300,7 @@ def readVerifyConfig(cfg):
if name in backends:
raise ConfigurationError('Can only have one backend named "%s"' % name)
- if backend_type in (BLOCK_DUD, BLOCK_JUNIPER_EX, BLOCK_JUNOS, BLOCK_FORCE10, BLOCK_BROCADE,
+ if backend_type in (BLOCK_DUD, BLOCK_JUNIPER_EX, BLOCK_JUNIPER_VPLS, BLOCK_JUNOS, BLOCK_FORCE10, BLOCK_BROCADE,
BLOCK_DELL, BLOCK_NCSVPN, BLOCK_PICA8OVS, BLOCK_OESS, 'asyncfail'):
backend_conf = dict( cfg.items(section) )
backend_conf['_backend_type'] = backend_type
diff --git a/opennsa/database.py b/opennsa/database.py
index f637d117721a43f2d62eb05e75cbb30e7b81c6cf..284a097d3469e7584930fc4b01b148826f8e4cf6 100644
--- a/opennsa/database.py
+++ b/opennsa/database.py
@@ -59,7 +59,7 @@ def castDatetime(value, cur):
# setup
-def setupDatabase(database, user, password=None):
+def setupDatabase(database, user, password=None, connection_id_start=None):
# hack on, use psycopg2 connection to register postgres label -> nsa label adaptation
import psycopg2
@@ -74,6 +74,9 @@ def setupDatabase(database, user, password=None):
DT = psycopg2.extensions.new_type((timestamptz_oid,), "timestamptz", castDatetime)
psycopg2.extensions.register_type(DT)
+ if connection_id_start:
+ cur.execute("INSERT INTO backend_connection_id (connection_id) VALUES (%s) ON CONFLICT DO NOTHING;", connection_id_start)
+
conn.close()
Registry.DBPOOL = adbapi.ConnectionPool('psycopg2', user=user, password=password, database=database)
@@ -81,6 +84,8 @@ def setupDatabase(database, user, password=None):
+
+
# ORM Objects
class ServiceConnection(DBObject):
@@ -91,5 +96,36 @@ class SubConnection(DBObject):
BELONGSTO = ['ServiceConnection']
+class STPAuthz(DBObject):
+ TABLENAME = 'stp_authz'
+
+
+# Not really needed
+class BackendConnectionID(DBObject):
+ TABLENAME = 'backend_connection_id'
+
+#@defer.inlineCallbacks
+def getBackendConnectionId():
+
+# rows = yield BackendConnectionID.find()
+# if len(rows) == 0:
+# defer.returnValue(0)
+# else:
+# connection_id = rows[0].connection_id
+# rows[0].connection_id += 1
+# rows[0].save()
+# defer.returnValue(connection_id)
+
+ def gotResult(rows):
+ print 'rows', rows
+ if len(rows) == 0:
+ return None
+ else:
+ return rows[0][0]
+
+ return Registry.DBPOOL.runQuery('UPDATE backend_connection_id SET connection_id = connection_id + 1 RETURNING connection_id;').addCallback(gotResult)
+
+
+
Registry.register(ServiceConnection, SubConnection)
diff --git a/opennsa/setup.py b/opennsa/setup.py
index b2347468810413dee1da036967d138a481435730..3c63019224a1935a92ffd77fdcedf7b0d835e6e3 100644
--- a/opennsa/setup.py
+++ b/opennsa/setup.py
@@ -41,6 +41,10 @@ def setupBackend(backend_cfg, network_name, nrm_ports, parent_requester):
from opennsa.backends import juniperex
BackendConstructer = juniperex.JuniperEXBackend
+ elif backend_type == config.BLOCK_JUNIPER_VPLS:
+ from opennsa.backends import junipervpls
+ BackendConstructer = junipervpls.JuniperVPLSBackend
+
elif backend_type == config.BLOCK_BROCADE:
from opennsa.backends import brocade
BackendConstructer = brocade.BrocadeBackend
@@ -152,7 +156,7 @@ class OpenNSAService(twistedservice.MultiService):
vc[config.HOST] = socket.getfqdn()
# database
- database.setupDatabase(vc[config.DATABASE], vc[config.DATABASE_USER], vc[config.DATABASE_PASSWORD])
+ database.setupDatabase(vc[config.DATABASE], vc[config.DATABASE_USER], vc[config.DATABASE_PASSWORD], vc[config.SERVICE_ID_START])
service_endpoints = []