diff --git a/opennsa/backends/junoscsd.py b/opennsa/backends/junoscsd.py new file mode 100644 index 0000000000000000000000000000000000000000..26bbfbb88e89c7428e0d5a4006a1a4bb61ab5d4a --- /dev/null +++ b/opennsa/backends/junoscsd.py @@ -0,0 +1,328 @@ +""" +Backend for Juniper Junos SPACE CSD plugin. + +Author: Michal Hazlinsky <hazlinsky at cesnet.cz> + +Backend specific configuraton: + +space_user=USERNAME +space_password=SECRET_PASSWD +space_api_url=BASE_URL +csd_service_def=SERVICE_DEF_ID_TO_USE # from your CSD insatnce +csd_customer_id=CUSTOMER_ID_TO_USE # from your CSD instance +routers=ROUTER1_NAME@ROUTER1_ID + ROUTER2_NAME@ROUTER2_ID + +""" + +import base64 +import random +import time + +from twisted.python import log +from twisted.web.error import Error as WebError +from twisted.internet.ssl import ClientContextFactory + + +from opennsa import constants as cnt, config +from opennsa.backends.common import genericbackend +from opennsa.protocols.shared import httpclient + +from lxml import etree + +CSD_TIMEOUT = 60 # ncs typically spends 25-32 seconds creating/deleting a vpn, sometimes a bit more + +# NO_OUT_OF_SYNC_CHECK = 'no-out-of-sync-check' # put this as a query parameter to get ncs to bypass the check + +URI_CREATE_ORDER="api/space/nsas/eline-ptp/service-management/service-orders/" +URI_GET_SERVICES="api/space/nsas/eline-ptp/service-management/services" +URI_DELETE_SERVICE="api/space/nsas/eline-ptp/service-management/services/%(service_id)s" + +ORDER_PAYLOAD = """ +<Data xmlns="services.schema.networkapi.jmp.juniper.net"> +<ServiceResource> + <ServiceOrder> + <Common> + <Name>%(service_name)s</Name> + <Comments>%(description)s</Comments> + </Common> + <ServiceEndPointGroup> + <DeviceInfo> + <NA> + <DeviceName>%(router_a_name)s</DeviceName> + <DeviceID>%(router_a_id)s</DeviceID> + </NA> + </DeviceInfo> + <ServiceEndPoint> + <InterfaceName>%(interface_a)s</InterfaceName> + <ServiceEndpointConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="PTPElineLDPEndPointConfigParameterOrderType"> + <EndPointCategory>PTP</EndPointCategory> + <TrafficType>DOT1Q Transport single vlan</TrafficType> + <EthernetOption>dot1q</EthernetOption> + <UnitId>%(vlan_a)s</UnitId> + <VlanId>%(vlan_a)s</VlanId> + </ServiceEndpointConfiguration> + </ServiceEndPoint> + </ServiceEndPointGroup> + <ServiceEndPointGroup> + <DeviceInfo> + <NA> + <DeviceName>%(router_b_name)s</DeviceName> + <DeviceID>%(router_b_id)s</DeviceID> + </NA> + </DeviceInfo> + <ServiceEndPoint> + <InterfaceName>%(interface_b)s</InterfaceName> + <ServiceEndpointConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="PTPElineLDPEndPointConfigParameterOrderType"> + <EndPointCategory>PTP</EndPointCategory> + <TrafficType>DOT1Q Transport single vlan</TrafficType> + <EthernetOption>dot1q</EthernetOption> + <UnitId>%(vlan_a)s</UnitId> + <VlanId>%(vlan_b)s</VlanId> + </ServiceEndpointConfiguration> + </ServiceEndPoint> + </ServiceEndPointGroup> + <ServiceOrderParameter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="PTPConfigParameterOrderType"> + </ServiceOrderParameter> + <Reference> + <Customer key="%(cus_key)s"/> + <ServiceDefinition> + <ServiceDefinitionID key="%(service_def_id)s"/> + </ServiceDefinition> + </Reference> + </ServiceOrder> +</ServiceResource> + <CustomAction xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ServiceOrderCustomActionType"> + <Action>SaveAndDeployNow</Action> + </CustomAction> +</Data> +""" + + +LOG_SYSTEM = 'JUNOS.CSD' + +class JUNOSSPACERouter(object): + def __init__(self,router_name,router_id): + self.router_name = router_name + self.router_id = router_id + + def __repr__(self): + return self.__str__() + + def __str__(self): + return "Router name {} deviceId {} ".format(self.router_name,self.router_id) + + +class CSDTarget(object): + + def __init__(self, router, interface, vlan=None): + self.router = router + self.interface = interface + self.vlan = vlan + + def __str__(self): + if self.vlan: + return '<CSDTarget %s/%s#%i>' % (self.router, self.interface, self.vlan) + else: + return '<CSDTarget %s/%s>' % (self.router, self.interface) + + + +def createCSDPayload(connection_id, source_target, dest_target, service_def_id, cus_id, space_routers, csd_descriptions): + + intps = { + 'service_name' : connection_id, + 'router_a_name' : source_target.router, + 'router_a_id' : space_routers[source_target.router].router_id, + 'interface_a' : source_target.interface, + 'router_b_name' : dest_target.router, + 'router_b_id' : space_routers[dest_target.router].router_id, + 'interface_b' : dest_target.interface, + 'cus_key' : cus_id, + 'service_def_id' : service_def_id + } + + timestamp = str(time.time()).split(".", 1)[0] + intps['description'] = csd_descriptions + " " + source_target.router + "." + dest_target.router + "." + timestamp + " OpenNSA build" + intps['vlan_a'] = source_target.vlan + intps['vlan_b'] = dest_target.vlan + payload = ORDER_PAYLOAD % intps + log.msg("Payload created: \n {}".format(payload),debug=True,system=LOG_SYSTEM) + + return payload + + + +def _extractErrorMessage(failure): + # used to extract error messages from http requests + if isinstance(failure.value, WebError): + return failure.value.response + else: + return failure.getErrorMessage() + + +class WebClientContextFactory(ClientContextFactory): + def getContext(self): + return ClientContextFactory.getContext(self) + + +class CSDConnectionManager: + + def __init__(self, port_map, space_user, space_password, space_api_url, space_routers, csd_service_def, csd_customer_id, network_name, csd_descriptions): + self.network_name = network_name + self.port_map = port_map + self.space_user=space_user + self.space_password=space_password + self.space_api_url=space_api_url + self.space_routers = space_routers + self.csd_service_def = csd_service_def + self.csd_customer_id = csd_customer_id + self.csd_descriptions = csd_descriptions + + + def getResource(self, port, label): + assert label is None or label.type_ == cnt.ETHERNET_VLAN, 'Label must be None or VLAN' + val = "" if label is None else str(label.labelValue()) + return port + ':' + val # port contains router and port + + + def getTarget(self, port, label): + assert label is None or label.type_ == cnt.ETHERNET_VLAN, 'Label must be None or VLAN' + if label is not None and label.type_ == cnt.ETHERNET_VLAN: + vlan = int(label.labelValue()) + assert 1 <= vlan <= 4095, 'Invalid label value for vlan: %s' % label.labelValue() + else: + vlan = None + + ri = self.port_map[port] + router, interface = ri.split(':') + return CSDTarget(router, interface, vlan) + + + def createConnectionId(self, source_target, dest_target): + return 'ON-' + str(random.randint(100000,999999)) + + + def canSwapLabel(self, label_type): + return True + + + def _createAuthzHeader(self): + credentials = self.space_user + ":" + self.space_password + cr = base64.b64encode(credentials.encode("utf-8")) + cred = "Basic " + cr.decode("utf-8") + + return cred + + def _createHeaders(self): + headers = {} + #TODO> set propper data type -- done + headers["Content-Type"] = "application/vnd.net.juniper.space.service-management.service-order+xml;version=2;charset=UTF-8" + headers["Authorization"] = self._createAuthzHeader() + return headers + + def setupLink(self, connection_id, source_target, dest_target, bandwidth): + payload = createCSDPayload(connection_id, source_target, dest_target, self.csd_service_def, self.csd_customer_id, self.space_routers, self.csd_descriptions) + headers = self._createHeaders() + contextFactory = WebClientContextFactory() + + def linkUp(data): + log.msg('Link %s -> %s up' % (source_target, dest_target), system=LOG_SYSTEM) + log.msg('Response: \n %s ' % (data),debug=True, system=LOG_SYSTEM) + + def error(failure): + log.msg('Error bringing up link %s -> %s' % (source_target, dest_target), system=LOG_SYSTEM) + log.msg('Message: %s' % _extractErrorMessage(failure), system=LOG_SYSTEM) + return failure + + spaceurl=self.space_api_url + URI_CREATE_ORDER + d = httpclient.httpRequest(spaceurl, payload.encode(), headers, method=b'POST', timeout=CSD_TIMEOUT, ctx_factory=contextFactory) + d.addCallbacks(linkUp, error) + return d + + + def teardownLink(self, connection_id, source_target, dest_target, bandwidth): + headers = {} + headers["Accept"] = "*/*" + headers["Authorization"] = self._createAuthzHeader() + serviceID = None + contextFactory = WebClientContextFactory() + + def linkDown(data): + log.msg('Link %s -> %s down' % (source_target, dest_target), system=LOG_SYSTEM) + log.msg('Response: \n %s ' % (data),debug=True, system=LOG_SYSTEM) + + def error(failure): + log.msg('Error bringing down link %s -> %s' % (source_target, dest_target), system=LOG_SYSTEM) + log.msg('Message from Get Service ID: %s' % _extractErrorMessage(failure), system=LOG_SYSTEM) + return failure + + def doServiceDelete(data): + headers = {} + #headers["Content-Type"] = "application/vnd.net.juniper.space.service-management.service-order+xml;version=2;charset=UTF-8" + headers["Authorization"] = self._createAuthzHeader() + contextFactory = WebClientContextFactory() + serviceID = 0 + nsmap={'a': 'services.schema.networkapi.jmp.juniper.net'} + services = etree.fromstring(data).xpath("/a:Data/a:ServiceResource/a:Service", namespaces=nsmap) + for service in services : + if service.xpath("a:Common/a:Name", namespaces=nsmap)[0].text == connection_id: + serviceID = service.xpath("a:Common/a:Identity", namespaces=nsmap)[0].text + if serviceID is 0 : + raise Exception("Can't find service ID for connection %s " % connection_id) + log.msg('Link %s -> %s Call for DELETE. Service ID: %s' % (source_target, dest_target, serviceID), system=LOG_SYSTEM) + + spaceurl=self.space_api_url + URI_DELETE_SERVICE % {'service_id': serviceID} + d = httpclient.httpRequest(spaceurl, b'', headers, method=b'DELETE', timeout=CSD_TIMEOUT, ctx_factory=contextFactory) + return d + + def errorSerDel(failure): + log.msg('Error bringing down link %s -> %s' % (source_target, dest_target), system=LOG_SYSTEM) + log.msg('Message from Service delete: %s' % _extractErrorMessage(failure), system=LOG_SYSTEM) + return failure + + spaceurl=self.space_api_url + URI_GET_SERVICES + # spaceurl = self.space_api_url + "api/space/nsas/eline-ptp/service-management/services/32440380" + res = httpclient.httpRequest(spaceurl, b'', headers, method=b'GET', timeout=CSD_TIMEOUT, ctx_factory=contextFactory) + res.addCallbacks(doServiceDelete, error) + res.addCallbacks(linkDown, errorSerDel) + + + + #TODO: set up propper URL for delete application/vnd.net.juniper.space.service-management.service+xml + #d = httpclient.httpRequest(self.space_api_url, None, headers, method='DELETE', timeout=CSD_TIMEOUT) + #d.addCallbacks(linkDown, error) + return res + + +def JunosCSDBackend(network_name, nrm_ports, parent_requester, cfg): + + name = 'CSD %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 + space_user = cfg[config.SPACE_USER] + space_password = cfg[config.SPACE_PASSWORD] + space_api_url = cfg[config.SPACE_API_URL] + space_routers_config = cfg[config.SPACE_ROUTERS].split() + csd_service_def = cfg[config.CSD_SERVICE_DEF] + csd_customer_id = cfg[config.CSD_CUSTOMER_ID] + csd_descriptions = cfg.get(config.JUNOS_DESCRIPTIONS, "OpenNSA") + + + space_routers = dict() + log.msg("Loaded JunosCSD backend with routers:") + for g in space_routers_config: + r,n = g.split('@',1) + junosspace_router = JUNOSSPACERouter(r,n) + log.msg("%s" % (junosspace_router)) + space_routers[r] = junosspace_router + + # csd_services_url = str(cfg[config.NCS_SERVICES_URL]) # convert from unicode + # user = cfg[config.NCS_USER] + # password = cfg[config.NCS_PASSWORD] + + cm = CSDConnectionManager(port_map, space_user,space_password,space_api_url,space_routers, csd_service_def, csd_customer_id, network_name, csd_descriptions) + return genericbackend.GenericBackend(network_name, nrm_map, cm, parent_requester, name) +