Source code for geni.rspec.vts

# Copyright (c) 2014-2017  Barnstormer Softworks, Ltd.
# Copyright (c) 2020  University of Houston Networking Lab

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import

import base64
import functools
import decimal

import ipaddress
from lxml import etree as ET
import six

import geni.rspec
import geni.namespaces as GNS
from geni.rspec.pg import Resource


[docs]class Namespaces(object): VTS = GNS.Namespace("vts", "http://geni.bssoftworks.com/rspec/ext/vts/request/1") SDN = GNS.Namespace("sdn", "http://geni.bssoftworks.com/rspec/ext/sdn/request/1")
[docs]class BadImageTypeError(Exception): def __init__ (self, rtype): self.rtype = rtype def __str__ (self): return "Supplied image must be of type %s" % (self.rtype)
################################################ # Base Request - Must be at top for EXTENSIONS # ################################################
[docs]class Request(geni.rspec.RSpec): EXTENSIONS = [] def __init__ (self): super(Request, self).__init__("request") self._resources = [] self.topo_name = None self.addNamespace(GNS.REQUEST, None) self.addNamespace(Namespaces.VTS) self.addNamespace(Namespaces.SDN) self._ext_children = [] for name,ext in Request.EXTENSIONS: self._wrapext(name,ext) def _wrapext (self, name, klass): @functools.wraps(klass.__init__) def wrap(*args, **kw): instance = klass(*args, **kw) self._ext_children.append(instance) return instance setattr(self, name, wrap)
[docs] def addResource (self, rsrc): for ns in rsrc.namespaces: self.addNamespace(ns) self._resources.append(rsrc)
[docs] def writeXML (self, path): f = open(path, "w+") f.write(self.toXMLString(True)) f.close()
[docs] def toXMLString (self, pretty_print = False, ucode = False): rspec = self.getDOM() if self.topo_name: ci = ET.SubElement(rspec, "{%s}client-info" % (Namespaces.VTS)) ci.attrib["topo-name"] = str(self.topo_name) for resource in self._resources: resource._write(rspec) for obj in self._ext_children: obj._write(rspec) if ucode: buf = ET.tostring(rspec, pretty_print = pretty_print, encoding="unicode") else: buf = ET.tostring(rspec, pretty_print = pretty_print) return buf
@property def resources(self): return self._resources + self._ext_children
###################### # Internal Functions # ###################### def _am_encrypt (gv, plaintext): # This should be turned into an Encryptor object that you can init once and carry around from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding pubkey = serialization.load_pem_public_key(gv["request.pubkey"], backend=default_backend()) if gv["request.hash"] == "sha1": hfunc = hashes.SHA1 elif gv["request.hash"] == "sha256": hfunc = hashes.SHA256 elif gv["request.hash"] == "sha384": hfunc = hashes.SHA384 elif gv["request.hash"] == "sha512": hfunc = hashes.SHA512 return base64.b64encode(pubkey.encrypt(plaintext, padding.OAEP(padding.MGF1(hfunc()), hfunc(), None))) ################### # Utility Objects # ###################
[docs]class DelayInfo(object): def __init__ (self, time = None, jitter = None, correlation = None, distribution = None): self.time = time self.jitter = jitter self.correlation = correlation self.distribution = distribution def __json__ (self): d = {"type" : "egress-delay"} if self.time: d["time"] = self.time if self.jitter: d["jitter"] = self.jitter if self.correlation: d["correlation"] = self.correlation if self.distribution: d["distribution"] = self.distribution return d def _write (self, element): d = ET.SubElement(element, "{%s}egress-delay" % (Namespaces.VTS.name)) if self.time: d.attrib["time"] = str(self.time) if self.jitter: d.attrib["jitter"] = str(self.jitter) if self.correlation: d.attrib["correlation"] = str(self.correlation) if self.distribution: d.attrib["distribution"] = self.distribution return d
[docs]class LossInfo(object): def __init__ (self, percent): self.percent = percent def __json__ (self): return {"type" : "egress-loss", "percent" : "%d" % (self.percent)} def _write (self, element): d = ET.SubElement(element, "{%s}egress-loss" % (Namespaces.VTS)) d.attrib["percent"] = "%d" % (self.percent) return d
[docs]class ReorderInfo(object): def __init__ (self, percent, correlation, gap = None): self.percent = percent self.correlation = correlation self.gap = gap def _write (self, element): d = ET.SubElement(element, "{%s}egress-reorder" % (Namespaces.VTS)) d.attrib["percent"] = str(self.percent) d.attrib["correlation"] = str(self.correlation) if self.gap: d.attrib["gap"] = str(self.gap) return d
################### # Datapath Images # ###################
[docs]class Image(object): def __init__ (self, name): self.name = name self._features = [] self._image_attrs = []
[docs] def setImageAttribute (self, name, val): self._image_attrs.append((name, val))
def _write (self, element): i = ET.SubElement(element, "{%s}image" % (Namespaces.VTS.name)) i.attrib["name"] = self.name for feature in self._features: feature._write(i) for (name,val) in self._image_attrs: ae = ET.SubElement(i, "{%s}image-attribute" % (Namespaces.VTS)) ae.attrib["name"] = name ae.attrib["value"] = str(val) return i
[docs]class SimpleDHCPImage(Image): def __init__ (self, subnet = None): super(SimpleDHCPImage, self).__init__("uh.simple-dhcpd") self.subnet = subnet def _write (self, element): e = super(SimpleDHCPImage, self)._write(element) if self.subnet: subnet = ET.SubElement(e, "{%s}image-attribute" % (Namespaces.VTS)) subnet.attrib["name"] = "subnet" subnet.attrib["value"] = str(self.subnet) return e
[docs]class DatapathImage(Image): pass
[docs]class OVSImage(DatapathImage): def __init__ (self, name): super(OVSImage, self).__init__(name) @property def sflow (self): return None @sflow.setter def sflow (self, val): if isinstance(val, SFlow): self._features.append(val) # TODO: Throw exception @property def netflow (self): return None @netflow.setter def netflow (self, val): if isinstance(val, NetFlow): self._features.append(val) # TODO: Throw exception
[docs] def setMirror (self, port): self._features.append(MirrorPort(port))
[docs]class OVSOpenFlowImage(OVSImage): def __init__ (self, controller, ofver = "1.0", dpid = None): super(OVSOpenFlowImage, self).__init__("bss:ovs-201-of") self.dpid = dpid self.controller = controller self.ofver = ofver def _write (self, element): i = super(OVSOpenFlowImage, self)._write(element) c = ET.SubElement(i, "{%s}controller" % (Namespaces.SDN.name)) c.attrib["url"] = self.controller v = ET.SubElement(i, "{%s}openflow-version" % (Namespaces.VTS.name)) v.attrib["value"] = self.ofver if self.dpid: d = ET.SubElement(i, "{%s}openflow-dpid" % (Namespaces.VTS.name)) d.attrib["value"] = str(self.dpid) return i
[docs]class UnknownSTPModeError(Exception): def __init__ (self, val): self._val = val def __str__ (self): return "Unknown STP Mode (%d)" % (self._val)
[docs]class IllegalModeForParamError(Exception): def __init__ (self, param): self.param = param def __str__ (self): return "The parameter '%s' is not configurable in the current STP mode" % (self.param)
[docs]class OVSL2STP(object): STP = 1 RSTP = 2 def __init__ (self): self._mode = OVSL2STP.STP self._rstp_params = {} self._stp_params = {} @property def mode (self): return self._mode @mode.setter def mode (self, val): if val != OVSL2STP.STP and val != OVSL2STP.RSTP: raise UnknownSTPModeError(val) self._mode = val @property def type (self): if self._mode == OVSL2STP.STP: return "stp" elif self._mode == OVSL2STP.RSTP: return "rstp" @property def priority (self): try: return self._stp_params["priority"] except KeyError: return None @priority.setter def priority (self, val): self._stp_params["priority"] = val self._rstp_params["priority"] = val @property def max_age (self): try: return self._stp_params["max-age"] except KeyError: return None @max_age.setter def max_age (self, val): self._stp_params["max-age"] = val self._rstp_params["max-age"] = val @property def hello_time (self): if self._mode != OVSL2STP.STP: raise IllegalModeForParamError("hello-time") try: return self._stp_params["hello-time"] except KeyError: return None @hello_time.setter def hello_time (self, val): if self._mode != OVSL2STP.STP: raise IllegalModeForParamError("hello-time") self._stp_params["hello-time"] = val @property def forward_delay (self): try: return self._stp_params["forward-delay"] except KeyError: return None @forward_delay.setter def forward_delay (self, val): self._stp_params["forward-delay"] = val self._rstp_params["forward-delay"] = val @property def address (self): try: return self._stp_params["system-id"] except KeyError: return None @address.setter def address (self, val): self._stp_params["system-id"] = val self._rstp_params["address"] = val @property def ageing_time (self): if self._mode != OVSL2STP.RSTP: raise IllegalModeForParamError("ageing-time") try: return self._rstp_params["ageing-time"] except KeyError: return None @ageing_time.setter def ageing_time (self, val): if self._mode != OVSL2STP.RSTP: raise IllegalModeForParamError("ageing-time") self._rstp_params["ageing-time"] = val @property def xmit_hold_count (self): if self._mode != OVSL2STP.RSTP: raise IllegalModeForParamError("xmit-hold-count") try: return self._rstp_params["xmit-hold-count"] except KeyError: return None @xmit_hold_count.setter def xmit_hold_count (self, val): if self._mode != OVSL2STP.RSTP: raise IllegalModeForParamError("xmit-hold-count") self._rstp_params["xmit-hold-count"] = val def _write (self, element): se = ET.SubElement(element, "{%s}stp" % (Namespaces.VTS)) if self._mode == OVSL2STP.STP: se.attrib["type"] = "stp" for k,v in self._stp_params.items(): pe = ET.SubElement(se, "{%s}%s" % (Namespaces.VTS, k)) pe.attrib["value"] = str(v) elif self._mode == OVSL2STP.RSTP: se.attrib["type"] = "rstp" for k,v in self._rstp_params.items(): pe = ET.SubElement(se, "{%s}%s" % (Namespaces.VTS, k)) pe.attrib["value"] = str(v) elif self._mode == -1: se.attrib["type"] = "disabled" return element def _as_jsonable (self): obj = {"type" : self.type} if self._mode == OVSL2STP.STP: for k,v in self._stp_params.items(): obj[k] = v elif self._mode == OVSL2STP.RSTP: for k,v in self._rstp_params.items(): obj[k] = v return obj
OVSL2STP.system_id = OVSL2STP.address
[docs]class OVSL2Image(OVSImage): def __init__ (self): super(OVSL2Image, self).__init__("bss:ovs-201") self.stp = OVSL2STP() self.mac_table_size = None self.mac_age = None def _write (self, element): i = super(OVSL2Image, self)._write(element) self.stp._write(i) mpe = ET.SubElement(i, "{%s}mac-table-params" % (Namespaces.VTS)) if self.mac_table_size: mse = ET.SubElement(mpe, "{%s}max-size" % (Namespaces.VTS)) mse.attrib["value"] = str(self.mac_table_size) if self.mac_age: mae = ET.SubElement(mpe, "{%s}max-age" % (Namespaces.VTS)) mae.attrib["value"] = str(self.mac_age) return i
################## # Image Features # ##################
[docs]class SFlow(object): def __init__ (self, collector_ip): self.collector_ip = collector_ip self.collector_port = 6343 self.header_bytes = 128 self.sampling_n = 64 self.polling_secs = 5 def _write (self, element): s = ET.SubElement(element, "{%s}sflow" % (Namespaces.VTS.name)) s.attrib["collector"] = "%s:%d" % (self.collector_ip, self.collector_port) s.attrib["header-bytes"] = str(self.header_bytes) s.attrib["sampling-n"] = str(self.sampling_n) s.attrib["polling-secs"] = str(self.polling_secs) return s
[docs]class NetFlow(object): def __init__ (self, collector_ip): self.collector_ip = collector_ip self.collector_port = 6343 self.timeout = 20 def _write (self, element): s = ET.SubElement(element, "{%s}netflow" % (Namespaces.VTS)) s.attrib["collector"] = "%s:%d" % (self.collector_ip, self.collector_port) s.attrib["timeout"] = str(self.timeout) return s
[docs]class MirrorPort(object): def __init__ (self, port): self.target = port.client_id def _write (self, element): s = ET.SubElement(element, "{%s}mirror" % (Namespaces.VTS)) s.attrib["target"] = self.target return s
################## # Graph Elements # ##################
[docs]class SSLVPNFunction(Resource): def __init__ (self, client_id): super(SSLVPNFunction, self).__init__() self.client_id = client_id self.protocol = None def _write (self, element): d = ET.SubElement(element, "{%s}function" % (Namespaces.VTS.name)) d.attrib["client_id"] = self.client_id d.attrib["type"] = "sslvpn" return d
Request.EXTENSIONS.append(("SSLVPNFunction", SSLVPNFunction)) Request.EXTENSIONS.append(("L2SSLVPNServer", SSLVPNFunction))
[docs]class L2SSLVPNClient(Resource): def __init__ (self, client_id): super(L2SSLVPNClient, self).__init__() self.client_id = client_id self.protocol = "udp" self.remote_ip = None self.remote_port = None self.note = None self.key = None def _write (self, element): d = ET.SubElement(element, "{%s}function" % (Namespaces.VTS)) d.attrib["type"] = "sslvpn-client" d.attrib["client_id"] = self.client_id d.attrib["remote-ip"] = str(self.remote_ip) d.attrib["remote-port"] = str(self.remote_port) d.attrib["note"] = str(self.note) d.text = str(self.key) return d
Request.EXTENSIONS.append(("L2SSLVPNClient", L2SSLVPNClient))
[docs]class Datapath(Resource): def __init__ (self, image, client_id): super(Datapath, self).__init__() if not isinstance(image, DatapathImage): raise BadImageTypeError(str(DatapathImage)) self.image = image self.ports = [] self.client_id = client_id @property def name (self): return self.client_id @name.setter def name (self, val): self.client_id = val
[docs] def attachPort (self, port): if port.client_id is None: if port.name is None: port.client_id = "%s:%d" % (self.name, len(self.ports)) else: port.client_id = "%s:%s" % (self.name, port.name) self.ports.append(port) return port
[docs] def connectCrossSliver (self, other_dp): port = InternalCircuit(None, None, None, None) self.attachPort(port) port.target = other_dp.client_id return port
def _write (self, element): d = ET.SubElement(element, "{%s}datapath" % (Namespaces.VTS.name)) d.attrib["client_id"] = self.name self.image._write(d) for port in self.ports: port._write(d) return d
Request.EXTENSIONS.append(("Datapath", Datapath))
[docs]class Container(Resource): EXTENSIONS = [] def __init__ (self, image, name): super(Container, self).__init__() self.image = image self.ports =[] self.name = name self.ram = None self.routes = [] for name,ext in Container.EXTENSIONS: self._wrapext(name, ext)
[docs] def attachPort (self, port): if port.name is None: port.client_id = "%s:%d" % (self.name, len(self.ports)) else: port.client_id = "%s:%s" % (self.name, port.name) self.ports.append(port) return port
[docs] def addIPRoute (self, network, gateway): self.routes.append((ipaddress.IPv4Network(six.ensure_text(network)), ipaddress.IPv4Address(six.ensure_text(gateway))))
[docs] def connectCrossSliver (self, other_dp): port = InternalCircuit(None, None, None, None) self.attachPort(port) port.target = other_dp.client_id return port
def _write (self, element): d = ET.SubElement(element, "{%s}container" % (Namespaces.VTS.name)) d.attrib["client_id"] = self.name if self.ram: d.attrib["ram"] = str(self.ram) self.image._write(d) for port in self.ports: port._write(d) for net,gw in self.routes: re = ET.SubElement(d, "{%s}route" % (Namespaces.VTS)) re.attrib["network"] = str(net) re.attrib["gateway"] = str(gw) super(Container, self)._write(d) return d
Request.EXTENSIONS.append(("Container", Container))
[docs]class Port(object): def __init__ (self, name = None): self.client_id = None self.name = name def _write (self, element): p = ET.SubElement(element, "{%s}port" % (Namespaces.VTS.name)) p.attrib["client_id"] = self.client_id return p
[docs]class PGCircuit(Port): def __init__ (self, name = None, delay_info = None): super(PGCircuit, self).__init__(name) self.delay_info = delay_info def _write (self, element): p = super(PGCircuit, self)._write(element) p.attrib["type"] = "pg-local" if self.delay_info: self.delay_info._write(p) return p
LocalCircuit = PGCircuit
[docs]class VFCircuit(Port): def __init__ (self, target): super(VFCircuit, self).__init__() self.target = target def _write (self, element): p = super(VFCircuit, self)._write(element) p.attrib["type"] = "vf-port" t = ET.SubElement(p, "{%s}target" % (Namespaces.VTS.name)) t.attrib["remote-clientid"] = self.target return p
[docs]class InternalCircuit(Port): def __init__ (self, target, vlan = None, delay_info = None, loss_info = None): super(InternalCircuit, self).__init__() self.vlan = vlan self.target = target self.delay_info = delay_info self.loss_info = loss_info self.reorder_info = None def _write (self, element): p = super(InternalCircuit, self)._write(element) p.attrib["type"] = "internal" if self.vlan: p.attrib["vlan-id"] = str(self.vlan) if self.delay_info: self.delay_info._write(p) if self.loss_info: self.loss_info._write(p) if self.reorder_info: self.reorder_info._write(p) t = ET.SubElement(p, "{%s}target" % (Namespaces.VTS.name)) t.attrib["remote-clientid"] = self.target return p
[docs]class ContainerPort(InternalCircuit): def __init__ (self, target, vlan = None, delay_info = None, loss_info = None): super(ContainerPort, self).__init__(target, vlan, delay_info, loss_info) self._v4addresses = [] def _write (self, element): p = super(ContainerPort, self)._write(element) for addr in self._v4addresses: ae = ET.SubElement(p, "{%s}ipv4-address" % (Namespaces.VTS)) ae.attrib["value"] = str(addr) return p
[docs] def addIPv4Address (self, value): self._v4addresses.append(ipaddress.IPv4Interface(six.ensure_text(value)))
[docs]class GRECircuit(Port): def __init__ (self, circuit_plane, endpoint): super(GRECircuit, self).__init__() self.circuit_plane = circuit_plane self.endpoint = endpoint def _write (self, element): p = super(GRECircuit, self)._write(element) p.attrib["type"] = "gre" p.attrib["circuit-plane"] = self.circuit_plane p.attrib["endpoint"] = self.endpoint return p
###################### # Element Extensions # ######################
[docs]class Mount(Resource): def __init__ (self, type, name, mount_path): self.type = type self.name = name self.mount_path = mount_path self.attrs = {} def _write (self, element): melem = ET.SubElement(element, "{%s}mount" % (Namespaces.VTS)) melem.attrib["type"] = self.type melem.attrib["name"] = self.name melem.attrib["path"] = self.mount_path for k,v in self.attrs.items(): melem.attrib[k] = str(v) return melem
Container.EXTENSIONS.append(("Mount", Mount))
[docs]class HgMount(Mount): """ Clone a public mercurial repo on a host Args: name (str): a reference name given on the mounting AM, must be unique within a sliver source (str): the URL to the source of repository mount_path (str): the path where the repository would be mounted in the host filesystem branch (str): the branch of the repository to be cloned on host (if any) """ def __init__ (self, name, source, mount_path, branch = "default"): super(HgMount, self).__init__("hg", name, mount_path) self.attrs["source"] = source self.attrs["branch"] = branch
Container.EXTENSIONS.append(("HgMount", HgMount))
[docs]class SecureHgMount(Mount): def __init__ (self, getversion_output, name, source, mount_path, branch = "default"): super(SecureHgMount, self).__init__("hg-secure", name, mount_path) self._source = source self.attrs["source"] = _am_encrypt(getversion_output, source) self.attrs["branch"] = branch
[docs] def rebind (self, getversion_output): self.attrs["source"] = _am_encrypt(getversion_output, self._source)
Container.EXTENSIONS.append(("SecureHgMount", SecureHgMount))
[docs]class DropboxMount(Mount): def __init__ (self, name, mount_path): super(DropboxMount, self).__init__("dropbox", name, mount_path)
Container.EXTENSIONS.append(("DropboxMount", DropboxMount)) ############# # Utilities # #############
[docs]def connectInternalCircuit (dp1, dp2, delay_info = None, loss_info = None): dp1v = None dp2v = None if isinstance(dp1, tuple): dp1v = dp1[1] dp1 = dp1[0] if isinstance(dp2, tuple): dp2v = dp2[1] dp2 = dp2[0] if isinstance(dp1, Container): sp = ContainerPort(None, dp1v, delay_info, loss_info) elif isinstance(dp1, Datapath): sp = InternalCircuit(None, dp1v, delay_info, loss_info) if isinstance(dp2, Container): dp = ContainerPort(None, dp2v, delay_info, loss_info) elif isinstance(dp2, Datapath): dp = InternalCircuit(None, dp2v, delay_info, loss_info) dp1.attachPort(sp) dp2.attachPort(dp) sp.target = dp.client_id dp.target = sp.client_id return (sp, dp)