# # CORE # Copyright (c)2011-2013 the Boeing Company. # See the LICENSE file included in this distribution. # # author: Jeff Ahrenholz # ''' Helpers for loading and saving XML files. savesessionxml(session, filename) is the main public interface here. ''' import os, pwd from xml.dom.minidom import parse, Document, Node from core import pycore from core.api import coreapi def addelementsfromlist(dom, parent, iterable, name, attr_name): ''' XML helper to iterate through a list and add items to parent using tags of the given name and the item value as an attribute named attr_name. Example: addelementsfromlist(dom, parent, ('a','b','c'), "letter", "value") ''' for item in iterable: element = dom.createElement(name) element.setAttribute(attr_name, item) parent.appendChild(element) def addtextelementsfromlist(dom, parent, iterable, name, attrs): ''' XML helper to iterate through a list and add items to parent using tags of the given name, attributes specified in the attrs tuple, and having the text of the item within the tags. ''' for item in iterable: element = dom.createElement(name) for k,v in attrs: element.setAttribute(k, v) parent.appendChild(element) txt = dom.createTextNode(item) element.appendChild(txt) def gettextelementstolist(parent): ''' XML helper to parse child text nodes from the given parent and return a list of (key, value) tuples. ''' r = [] for n in parent.childNodes: if n.nodeType != Node.ELEMENT_NODE: continue k = str(n.nodeName) v = '' # sometimes want None here? for c in n.childNodes: if c.nodeType != Node.TEXT_NODE: continue v = str(c.nodeValue) break r.append((k,v)) return r def addparamtoparent(dom, parent, name, value): ''' XML helper to add a tag to the parent element, when value is not None. ''' if value is None: return None p = dom.createElement("param") parent.appendChild(p) p.setAttribute("name", name) p.setAttribute("value", "%s" % value) return p def addtextparamtoparent(dom, parent, name, value): ''' XML helper to add a value tag to the parent element, when value is not None. ''' if value is None: return None p = dom.createElement("param") parent.appendChild(p) p.setAttribute("name", name) txt = dom.createTextNode(value) p.appendChild(txt) return p def getoneelement(dom, name): e = dom.getElementsByTagName(name) if len(e) == 0: return None return e[0] def gettextchild(dom): # this could be improved to skip XML comments child = dom.firstChild if child is not None and child.nodeType == Node.TEXT_NODE: return str(child.nodeValue) return None def getparamssetattrs(dom, param_names, target): ''' XML helper to get tags and set the attribute in the target object. String type is used. Target object attribute is unchanged if the XML attribute is not present. ''' params = dom.getElementsByTagName("param") for param in params: param_name = param.getAttribute("name") value = param.getAttribute("value") if value is None: continue # never reached? if param_name in param_names: setattr(target, param_name, str(value)) def xmltypetonodeclass(session, type): ''' Helper to convert from a type string to a class name in pycore.nodes.*. ''' if hasattr(pycore.nodes, type): return eval("pycore.nodes.%s" % type) else: return None class CoreDocumentParser(object): def __init__(self, session, filename): self.session = session self.verbose = self.session.getcfgitembool('verbose', False) self.filename = filename self.dom = parse(filename) #self.scenario = getoneelement(self.dom, "Scenario") self.np = getoneelement(self.dom, "NetworkPlan") if self.np is None: raise ValueError, "missing NetworkPlan!" self.mp = getoneelement(self.dom, "MotionPlan") self.sp = getoneelement(self.dom, "ServicePlan") self.meta = getoneelement(self.dom, "CoreMetaData") self.coords = self.getmotiondict(self.mp) # link parameters parsed in parsenets(), applied in parsenodes() self.linkparams = {} self.parsenets() self.parsenodes() self.parseservices() self.parsemeta() def warn(self, msg): if self.session: warnstr = "XML parsing '%s':" % (self.filename) self.session.warn("%s %s" % (warnstr, msg)) def getmotiondict(self, mp): ''' Parse a MotionPlan into a dict with node names for keys and coordinates for values. ''' if mp is None: return {} coords = {} for node in mp.getElementsByTagName("Node"): nodename = str(node.getAttribute("name")) if nodename == '': continue for m in node.getElementsByTagName("motion"): if m.getAttribute("type") != "stationary": continue point = m.getElementsByTagName("point") if len(point) == 0: continue txt = point[0].firstChild if txt is None: continue xyz = map(int, txt.nodeValue.split(',')) z = None x, y = xyz[0:2] if (len(xyz) == 3): z = xyz[2] coords[nodename] = (x, y, z) return coords @staticmethod def getcommonattributes(obj): ''' Helper to return tuple of attributes common to nodes and nets. ''' id = int(obj.getAttribute("id")) name = str(obj.getAttribute("name")) type = str(obj.getAttribute("type")) return(id, name, type) def parsenets(self): linkednets = [] for net in self.np.getElementsByTagName("NetworkDefinition"): id, name, type = self.getcommonattributes(net) nodecls = xmltypetonodeclass(self.session, type) if not nodecls: self.warn("skipping unknown network node '%s' type '%s'" % \ (name, type)) continue n = self.session.addobj(cls = nodecls, objid = id, name = name, start = False) if name in self.coords: x, y, z = self.coords[name] n.setposition(x, y, z) getparamssetattrs(net, ("icon", "canvas", "opaque"), n) if hasattr(n, "canvas") and n.canvas is not None: n.canvas = int(n.canvas) # links between two nets (e.g. switch-switch) for ifc in net.getElementsByTagName("interface"): netid = str(ifc.getAttribute("net")) linkednets.append((n, netid)) self.parsemodels(net, n) # link networks together now that they all have been parsed for (n, netid) in linkednets: try: n2 = n.session.objbyname(netid) except KeyError: n.warn("skipping net %s interface: unknown net %s" % \ (n.name, netid)) continue n.linknet(n2) def parsenodes(self): for node in self.np.getElementsByTagName("Node"): id, name, type = self.getcommonattributes(node) if type == "rj45": nodecls = pycore.nodes.RJ45Node else: nodecls = pycore.nodes.CoreNode n = self.session.addobj(cls = nodecls, objid = id, name = name, start = False) if name in self.coords: x, y, z = self.coords[name] n.setposition(x, y, z) n.type = type getparamssetattrs(node, ("icon", "canvas", "opaque"), n) if hasattr(n, "canvas") and n.canvas is not None: n.canvas = int(n.canvas) for ifc in node.getElementsByTagName("interface"): self.parseinterface(n, ifc) def parseinterface(self, n, ifc): ''' Parse a interface block such as:
00:00:00:aa:00:01
10.0.0.2/24
2001::2/64
''' name = str(ifc.getAttribute("name")) netid = str(ifc.getAttribute("net")) hwaddr = None addrlist = [] try: net = n.session.objbyname(netid) except KeyError: n.warn("skipping node %s interface %s: unknown net %s" % \ (n.name, name, netid)) return for addr in ifc.getElementsByTagName("address"): addrstr = gettextchild(addr) if addrstr is None: continue if addr.getAttribute("type") == "mac": hwaddr = addrstr else: addrlist.append(addrstr) i = n.newnetif(net, addrlist = addrlist, hwaddr = hwaddr, ifindex = None, ifname = name) for model in ifc.getElementsByTagName("model"): self.parsemodel(model, n, n.objid) key = (n.name, name) if key in self.linkparams: netif = n.netif(i) for (k, v) in self.linkparams[key]: netif.setparam(k, v) def parsemodels(self, dom, obj): ''' Mobility/wireless model config is stored in a ConfigurableManager's config dict. ''' nodenum = int(dom.getAttribute("id")) for model in dom.getElementsByTagName("model"): self.parsemodel(model, obj, nodenum) def parsemodel(self, model, obj, nodenum): ''' Mobility/wireless model config is stored in a ConfigurableManager's config dict. ''' name = model.getAttribute("name") if name == '': return type = model.getAttribute("type") # convert child text nodes into key=value pairs kvs = gettextelementstolist(model) mgr = self.session.mobility # TODO: the session.confobj() mechanism could be more generic; # it only allows registering Conf Message callbacks, but here # we want access to the ConfigurableManager, not the callback if name[:5] == "emane": mgr = self.session.emane elif name[:5] == "netem": mgr = None self.parsenetem(model, obj, kvs) elif name[:3] == "xen": mgr = self.session.xen # TODO: assign other config managers here if mgr: mgr.setconfig_keyvalues(nodenum, name, kvs) def parsenetem(self, model, obj, kvs): ''' Determine interface and invoke setparam() using the parsed (key, value) pairs. ''' ifname = model.getAttribute("netif") peer = model.getAttribute("peer") key = (peer, ifname) # nodes and interfaces do not exist yet, at this point of the parsing, # save (key, value) pairs for later try: #kvs = map(lambda(k, v): (int(v)), kvs) kvs = map(self.numericvalue, kvs) except ValueError: self.warn("error parsing link parameters for '%s' on '%s'" % \ (ifname, peer)) self.linkparams[key] = kvs @staticmethod def numericvalue(keyvalue): (key, value) = keyvalue if '.' in str(value): value = float(value) else: value = int(value) return (key, value) def parseservices(self): ''' After node objects exist, parse service customizations and add them to the nodes. ''' svclists = {} # parse services and store configs into session.services.configs for node in self.sp.getElementsByTagName("Node"): name = node.getAttribute("name") n = self.session.objbyname(name) if n is None: self.warn("skipping service config for unknown node '%s'" % \ name) continue for service in node.getElementsByTagName("Service"): svcname = service.getAttribute("name") if self.parseservice(service, n): if n.objid in svclists: svclists[n.objid] += "|" + svcname else: svclists[n.objid] = svcname # associate nodes with services for objid in sorted(svclists.keys()): n = self.session.obj(objid) self.session.services.addservicestonode(node=n, nodetype=n.type, services_str=svclists[objid], verbose=self.verbose) def parseservice(self, service, n): ''' Use session.services manager to store service customizations before they are added to a node. ''' name = service.getAttribute("name") svc = self.session.services.getservicebyname(name) if svc is None: return False values = [] startup_idx = service.getAttribute("startup_idx") if startup_idx is not None: values.append("startidx=%s" % startup_idx) startup_time = service.getAttribute("start_time") if startup_time is not None: values.append("starttime=%s" % startup_time) dirs = [] for dir in service.getElementsByTagName("Directory"): dirname = dir.getAttribute("name") dirs.append(dirname) if len(dirs): values.append("dirs=%s" % dirs) startup = [] shutdown = [] validate = [] for cmd in service.getElementsByTagName("Command"): type = cmd.getAttribute("type") cmdstr = gettextchild(cmd) if cmdstr is None: continue if type == "start": startup.append(cmdstr) elif type == "stop": shutdown.append(cmdstr) elif type == "validate": validate.append(cmdstr) if len(startup): values.append("cmdup=%s" % startup) if len(shutdown): values.append("cmddown=%s" % shutdown) if len(validate): values.append("cmdval=%s" % validate) files = [] for file in service.getElementsByTagName("File"): filename = file.getAttribute("name") files.append(filename) data = gettextchild(file) typestr = "service:%s:%s" % (name, filename) self.session.services.setservicefile(nodenum=n.objid, type=typestr, filename=filename, srcname=None, data=data) if len(files): values.append("files=%s" % files) if not bool(service.getAttribute("custom")): return True self.session.services.setcustomservice(n.objid, svc, values) return True def parsehooks(self, hooks): ''' Parse hook scripts from XML into session._hooks. ''' for hook in hooks.getElementsByTagName("Hook"): filename = hook.getAttribute("name") state = hook.getAttribute("state") data = gettextchild(hook) if data is None: data = "" # allow for empty file type = "hook:%s" % state self.session.sethook(type, filename=filename, srcname=None, data=data) def parsemeta(self): opt = getoneelement(self.meta, "SessionOptions") if opt: for param in opt.getElementsByTagName("param"): k = str(param.getAttribute("name")) v = str(param.getAttribute("value")) if v == '': v = gettextchild(param) # allow attribute/text for newlines setattr(self.session.options, k, v) hooks = getoneelement(self.meta, "Hooks") if hooks: self.parsehooks(hooks) meta = getoneelement(self.meta, "MetaData") if meta: for param in meta.getElementsByTagName("param"): k = str(param.getAttribute("name")) v = str(param.getAttribute("value")) if v == '': v = gettextchild(param) self.session.metadata.additem(k, v) class CoreDocumentWriter(Document): ''' Utility class for writing a CoreSession to XML. The init method builds an xml.dom.minidom.Document, and the writexml() method saves the XML file. ''' def __init__(self, session): ''' Create an empty Scenario XML Document, then populate it with objects from the given session. ''' Document.__init__(self) self.session = session self.scenario = self.createElement("Scenario") self.np = self.createElement("NetworkPlan") self.mp = self.createElement("MotionPlan") self.sp = self.createElement("ServicePlan") self.meta = self.createElement("CoreMetaData") self.appendChild(self.scenario) self.scenario.appendChild(self.np) self.scenario.appendChild(self.mp) self.scenario.appendChild(self.sp) self.scenario.appendChild(self.meta) self.populatefromsession() def populatefromsession(self): self.session.emane.setup() # not during runtime? self.addnets() self.addnodes() self.addmetadata() def writexml(self, filename): self.session.info("saving session XML file %s" % filename) f = open(filename, "w") Document.writexml(self, writer=f, indent="", addindent=" ", newl="\n", \ encoding="UTF-8") f.close() if self.session.user is not None: uid = pwd.getpwnam(self.session.user).pw_uid gid = os.stat(self.session.sessiondir).st_gid os.chown(filename, uid, gid) def addnets(self): ''' Add PyCoreNet objects as NetworkDefinition XML elements. ''' with self.session._objslock: for net in self.session.objs(): if not isinstance(net, pycore.nodes.PyCoreNet): continue self.addnet(net) def addnet(self, net): ''' Add one PyCoreNet object as a NetworkDefinition XML element. ''' n = self.createElement("NetworkDefinition") self.np.appendChild(n) n.setAttribute("name", net.name) # could use net.brname n.setAttribute("id", "%s" % net.objid) n.setAttribute("type", "%s" % net.__class__.__name__) self.addnetinterfaces(n, net) # key used with tunnel node if hasattr(net, 'grekey') and net.grekey is not None: n.setAttribute("key", "%s" % net.grekey) # link parameters for netif in net.netifs(sort=True): self.addnetem(n, netif) # wireless/mobility models modelconfigs = net.session.mobility.getmodels(net) modelconfigs += net.session.emane.getmodels(net) self.addmodels(n, modelconfigs) self.addposition(net) def addnetem(self, n, netif): ''' Similar to addmodels(); used for writing netem link effects parameters. TODO: Interface parameters should be moved to the model construct, then this separate method shouldn't be required. ''' if not hasattr(netif, "node") or netif.node is None: return params = netif.getparams() if len(params) == 0: return model = self.createElement("model") model.setAttribute("name", "netem") model.setAttribute("netif", netif.name) model.setAttribute("peer", netif.node.name) has_params = False for k, v in params: # default netem parameters are 0 or None if v is None or v == 0: continue if k == "has_netem" or k == "has_tbf": continue key = self.createElement(k) key.appendChild(self.createTextNode("%s" % v)) model.appendChild(key) has_params = True if has_params: n.appendChild(model) def addmodels(self, n, configs): ''' Add models from a list of model-class, config values tuples. ''' for (m, conf) in configs: model = self.createElement("model") n.appendChild(model) model.setAttribute("name", m._name) type = "wireless" if m._type == coreapi.CORE_TLV_REG_MOBILITY: type = "mobility" model.setAttribute("type", type) for i, k in enumerate(m.getnames()): key = self.createElement(k) value = conf[i] if value is None: value = "" key.appendChild(self.createTextNode("%s" % value)) model.appendChild(key) def addnodes(self): ''' Add PyCoreNode objects as node XML elements. ''' with self.session._objslock: for node in self.session.objs(): if not isinstance(node, pycore.nodes.PyCoreNode): continue self.addnode(node) def addnode(self, node): ''' Add a PyCoreNode object as node XML elements. ''' n = self.createElement("Node") self.np.appendChild(n) n.setAttribute("name", node.name) n.setAttribute("id", "%s" % node.nodeid()) if node.type: n.setAttribute("type", node.type) self.addinterfaces(n, node) self.addposition(node) addparamtoparent(self, n, "icon", node.icon) addparamtoparent(self, n, "canvas", node.canvas) self.addservices(node) def addinterfaces(self, n, node): ''' Add PyCoreNetIfs to node XML elements. ''' for ifc in node.netifs(sort=True): i = self.createElement("interface") n.appendChild(i) i.setAttribute("name", ifc.name) netmodel = None if ifc.net: i.setAttribute("net", ifc.net.name) if hasattr(ifc.net, "model"): netmodel = ifc.net.model if ifc.mtu and ifc.mtu != 1500: i.setAttribute("mtu", "%s" % ifc.mtu) # could use ifc.params, transport_type self.addaddresses(i, ifc) # per-interface models if netmodel and netmodel._name[:6] == "emane_": cfg = self.session.emane.getifcconfig(node.objid, netmodel._name, None, ifc) if cfg: self.addmodels(i, ((netmodel, cfg),) ) def addnetinterfaces(self, n, net): ''' Similar to addinterfaces(), but only adds interface elements to the supplied XML node that would not otherwise appear in the Node elements. These are any interfaces that link two switches/hubs together. ''' for ifc in net.netifs(sort=True): if not hasattr(ifc, "othernet") or not ifc.othernet: continue if net.objid == ifc.net.objid: continue i = self.createElement("interface") n.appendChild(i) i.setAttribute("name", ifc.name) if ifc.net: i.setAttribute("net", ifc.net.name) def addposition(self, node): ''' Add object coordinates as location XML element. ''' (x,y,z) = node.position.get() if x is None or y is None: return # mpn = self.createElement("Node") mpn.setAttribute("name", node.name) self.mp.appendChild(mpn) # motion = self.createElement("motion") motion.setAttribute("type", "stationary") mpn.appendChild(motion) # $X$,$Y$,$Z$ pt = self.createElement("point") motion.appendChild(pt) coordstxt = "%s,%s" % (x,y) if z: coordstxt += ",%s" % z coords = self.createTextNode(coordstxt) pt.appendChild(coords) def addservices(self, node): ''' Add services and their customizations to the ServicePlan. ''' if len(node.services) == 0: return defaults = self.session.services.getdefaultservices(node.type) if node.services == defaults: return spn = self.createElement("Node") spn.setAttribute("name", node.name) self.sp.appendChild(spn) for svc in node.services: s = self.createElement("Service") spn.appendChild(s) s.setAttribute("name", str(svc._name)) s.setAttribute("startup_idx", str(svc._startindex)) if svc._starttime != "": s.setAttribute("start_time", str(svc._starttime)) # only record service names if not a customized service if not svc._custom: continue s.setAttribute("custom", str(svc._custom)) addelementsfromlist(self, s, svc._dirs, "Directory", "name") for fn in svc._configs: if len(fn) == 0: continue f = self.createElement("File") f.setAttribute("name", fn) # all file names are added to determine when a file has been deleted s.appendChild(f) data = self.session.services.getservicefiledata(svc, fn) if data is None: # this includes only customized file contents and skips # the auto-generated files continue txt = self.createTextNode(data) f.appendChild(txt) addtextelementsfromlist(self, s, svc._startup, "Command", (("type","start"),)) addtextelementsfromlist(self, s, svc._shutdown, "Command", (("type","stop"),)) addtextelementsfromlist(self, s, svc._validate, "Command", (("type","validate"),)) def addaddresses(self, i, netif): ''' Add MAC and IP addresses to interface XML elements. ''' if netif.hwaddr: h = self.createElement("address") i.appendChild(h) h.setAttribute("type", "mac") htxt = self.createTextNode("%s" % netif.hwaddr) h.appendChild(htxt) for addr in netif.addrlist: a = self.createElement("address") i.appendChild(a) # a.setAttribute("type", ) atxt = self.createTextNode("%s" % addr) a.appendChild(atxt) def addhooks(self): ''' Add hook script XML elements to the metadata tag. ''' hooks = self.createElement("Hooks") for state in sorted(self.session._hooks.keys()): for (filename, data) in self.session._hooks[state]: hook = self.createElement("Hook") hook.setAttribute("name", filename) hook.setAttribute("state", str(state)) txt = self.createTextNode(data) hook.appendChild(txt) hooks.appendChild(hook) if hooks.hasChildNodes(): self.meta.appendChild(hooks) def addmetadata(self): ''' Add CORE-specific session meta-data XML elements. ''' # options options = self.createElement("SessionOptions") defaults = self.session.options.getdefaultvalues() for i, (k, v) in enumerate(self.session.options.getkeyvaluelist()): if str(v) != str(defaults[i]): addtextparamtoparent(self, options, k, v) #addparamtoparent(self, options, k, v) if options.hasChildNodes(): self.meta.appendChild(options) # hook scripts self.addhooks() # meta meta = self.createElement("MetaData") self.meta.appendChild(meta) for (k, v) in self.session.metadata.items(): addtextparamtoparent(self, meta, k, v) #addparamtoparent(self, meta, k, v) def opensessionxml(session, filename): ''' Import a session from the EmulationScript XML format. ''' doc = CoreDocumentParser(session, filename) def savesessionxml(session, filename): ''' Export a session to the EmulationScript XML format. ''' doc = CoreDocumentWriter(session) doc.writexml(filename)