#
# CORE
# Copyright (c) 2015 the Boeing Company.
# See the LICENSE file included in this distribution.
#

import sys
import random
from core.netns import nodes
from core import constants
from core.misc.ipaddr import MacAddr
from xml.dom.minidom import parse
from xmlutils import *

class CoreDocumentParser1(object):

    layer2_device_types = 'hub', 'switch'
    layer3_device_types = 'host', 'router'
    device_types = layer2_device_types + layer3_device_types

    # TODO: support CORE interface classes:
    #   RJ45Node
    #   TunnelNode

    def __init__(self, session, filename, options):
        self.session = session
        self.verbose = self.session.getcfgitembool('verbose', False)
        self.filename = filename
        if 'dom' in options:
            # this prevents parsing twice when detecting file versions
            self.dom = options['dom']
        else:
            self.dom = parse(filename)
        self.start = options['start']
        self.nodecls = options['nodecls']
        self.scenario = self.get_scenario(self.dom)
        self.location_refgeo_set = False
        self.location_refxyz_set = False
        # saved link parameters saved when parsing networks and applied later
        self.link_params = {}
        # map from id-string to objid, for files having node names but
        # not node numbers
        self.objidmap = {}
        self.objids = set()
        self.default_services = {}
        if self.scenario:
            self.parse_scenario()

    def info(self, msg):
        s = 'XML parsing \'%s\': %s' % (self.filename, msg)
        if self.session:
            self.session.info(s)
        else:
            sys.stdout.write(s + '\n')

    def warn(self, msg):
        s = 'WARNING XML parsing \'%s\': %s' % (self.filename, msg)
        if self.session:
            self.session.warn(s)
        else:
            sys.stderr.write(s + '\n')

    @staticmethod
    def get_scenario(dom):
        scenario = getFirstChildByTagName(dom, 'scenario')
        if not scenario:
            raise ValueError, 'no scenario element found'
        version = scenario.getAttribute('version')
        if version and version != '1.0':
            raise ValueError, \
                'unsupported scenario version found: \'%s\'' % version
        return scenario

    def parse_scenario(self):
        self.parse_default_services()
        self.parse_session_config()
        self.parse_network_plan()

    def assign_id(self, idstr, idval):
        if idstr in self.objidmap:
            assert self.objidmap[idstr] == idval and idval in self.objids
            return
        self.objidmap[idstr] = idval
        self.objids.add(idval)

    def rand_id(self):
        while True:
            x = random.randint(0, 0xffff)
            if x not in self.objids:
                return x

    def get_id(self, idstr):
        '''\
        Get a, possibly new, object id (node number) corresponding to
        the given XML string id.
        '''
        if not idstr:
            idn = self.rand_id()
            self.objids.add(idn)
            return idn
        elif idstr in self.objidmap:
            return self.objidmap[idstr]
        else:
            try:
                idn = int(idstr)
            except ValueError:
                idn = self.rand_id()
            self.assign_id(idstr, idn)
            return idn

    def get_common_attributes(self, node):
        '''\
        Return id, name attributes for the given XML element.  These
        attributes are common to nodes and networks.
        '''
        idstr = node.getAttribute('id')
        # use an explicit set COREID if it exists
        coreid = self.find_core_id(node)
        if coreid:
            idn = int(coreid)
            if idstr:
                self.assign_id(idstr, idn)
        else:
            idn = self.get_id(idstr)
        # TODO: consider supporting unicode; for now convert to an
        # ascii string
        namestr = str(node.getAttribute('name'))
        return idn, namestr

    def iter_network_member_devices(self, element):
        # element can be a network or a channel
        for interface in iterChildrenWithAttribute(element, 'member',
                                                   'type', 'interface'):
            if_id = getChildTextTrim(interface)
            assert if_id        # XXX for testing
            if not if_id:
                continue
            device, if_name = self.find_device_with_interface(if_id)
            assert device, 'no device for if_id: %s' % if_id # XXX for testing
            if device:
                yield device, if_name

    def network_class(self, network, network_type):
        '''\
        Return the corresponding CORE network class for the given
        network/network_type.
        '''
        if network_type == 'ethernet':
            return nodes.PtpNet
        elif network_type == 'satcom':
            return nodes.PtpNet
        elif network_type == 'wireless':
            channel = getFirstChildByTagName(network, 'channel')
            if channel:
                # use an explicit CORE type if it exists
                coretype = getFirstChildTextTrimWithAttribute(channel, 'type',
                                                              'domain', 'CORE')
                if coretype:
                    if coretype == 'basic_range':
                        return nodes.WlanNode
                    elif coretype.startswith('emane'):
                        return nodes.EmaneNode
                    else:
                        self.warn('unknown network type: \'%s\'' % coretype)
                        return xmltypetonodeclass(self.session, coretype)
            return nodes.WlanNode
        self.warn('unknown network type: \'%s\'' % network_type)
        return None

    def create_core_object(self, objcls, objid, objname, element, node_type):
        obj = self.session.addobj(cls = objcls, objid = objid,
                                  name = objname, start = self.start)
        if self.verbose:
            self.info('added object objid=%s name=%s cls=%s' % \
                          (objid, objname, objcls))
        self.set_object_position(obj, element)
        self.set_object_presentation(obj, element, node_type)
        return obj

    def get_core_object(self, idstr):
        if idstr and idstr in self.objidmap:
            objid = self.objidmap[idstr]
            return self.session.obj(objid)
        return None

    def parse_network_plan(self):
        # parse the scenario in the following order:
        #   1. layer-2 devices
        #   2. other networks (ptp/wlan)
        #   3. layer-3 devices
        self.parse_layer2_devices()
        self.parse_networks()
        self.parse_layer3_devices()

    def set_ethernet_link_parameters(self, channel, link_params,
                                     mobility_model_name, mobility_params):
        # save link parameters for later use, indexed by the tuple
        # (device_id, interface_name)
        for dev, if_name in self.iter_network_member_devices(channel):
            if self.device_type(dev) in self.device_types:
                dev_id = dev.getAttribute('id')
                key = (dev_id, if_name)
                self.link_params[key] = link_params
        if mobility_model_name or mobility_params:
            raise NotImplementedError

    def set_wireless_link_parameters(self, channel, link_params,
                                     mobility_model_name, mobility_params):
        network = self.find_channel_network(channel)
        network_id = network.getAttribute('id')
        if network_id in self.objidmap:
            nodenum = self.objidmap[network_id]
        else:
            self.warn('unknown network: %s' % network.toxml('utf-8'))
            assert False        # XXX for testing
            return
        model_name = getFirstChildTextTrimWithAttribute(channel, 'type',
                                                        'domain', 'CORE')
        if not model_name:
            model_name = 'basic_range'
        if model_name == 'basic_range':
            mgr = self.session.mobility
        elif model_name.startswith('emane'):
            mgr = self.session.emane
        elif model_name.startswith('xen'):
            mgr = self.session.xen
        else:
            # TODO: any other config managers?
            raise NotImplementedError
        mgr.setconfig_keyvalues(nodenum, model_name, link_params.items())
        if mobility_model_name and mobility_params:
            mgr.setconfig_keyvalues(nodenum, mobility_model_name,
                                    mobility_params.items())

    def link_layer2_devices(self, device1, ifname1, device2, ifname2):
        '''\
        Link two layer-2 devices together.
        '''
        devid1 = device1.getAttribute('id')
        dev1 = self.get_core_object(devid1)
        devid2 = device2.getAttribute('id')
        dev2 = self.get_core_object(devid2)
        assert dev1 and dev2    # XXX for testing
        if dev1 and dev2:
            # TODO: review this
            if isinstance(dev2, nodes.RJ45Node):
                # RJ45 nodes have different linknet()
                netif = dev2.linknet(dev1)
            else:
                netif = dev1.linknet(dev2)
            self.set_wired_link_parameters(dev1, netif, devid1, ifname1)

    @classmethod
    def parse_xml_value(cls, valtext):
        if not valtext:
            return None
        try:
            if not valtext.translate(None, '0123456789'):
                val = int(valtext)
            else:
                val = float(valtext)
        except ValueError:
            val = str(valtext)
        return val

    @classmethod
    def parse_parameter_children(cls, parent):
        params = {}
        for parameter in iterChildrenWithName(parent, 'parameter'):
            param_name = parameter.getAttribute('name')
            assert param_name   # XXX for testing
            if not param_name:
                continue
            # TODO: consider supporting unicode; for now convert
            # to an ascii string
            param_name = str(param_name)
            param_val = cls.parse_xml_value(getChildTextTrim(parameter))
            # TODO: check if the name already exists?
            if param_name and param_val:
                params[param_name] = param_val
        return params

    def parse_network_channel(self, channel):
        element = self.search_for_element(channel, 'type',
                                          lambda x: not x.hasAttributes())
        channel_type = getChildTextTrim(element)
        link_params = self.parse_parameter_children(channel)

        mobility = getFirstChildByTagName(channel, 'CORE:mobility')
        if mobility:
            mobility_model_name = \
                getFirstChildTextTrimByTagName(mobility, 'type')
            mobility_params = self.parse_parameter_children(mobility)
        else:
            mobility_model_name = None
            mobility_params = None
        if channel_type == 'wireless':
            self.set_wireless_link_parameters(channel, link_params,
                                              mobility_model_name,
                                              mobility_params)
        elif channel_type == 'ethernet':
            # TODO: maybe this can be done in the loop below to avoid
            # iterating through channel members multiple times
            self.set_ethernet_link_parameters(channel, link_params,
                                              mobility_model_name,
                                              mobility_params)
        else:
            raise NotImplementedError
        layer2_device = []
        for dev, if_name in self.iter_network_member_devices(channel):
            if self.device_type(dev) in self.layer2_device_types:
                layer2_device.append((dev, if_name))
        assert len(layer2_device) <= 2
        if len(layer2_device) == 2:
            self.link_layer2_devices(layer2_device[0][0], layer2_device[0][1],
                                     layer2_device[1][0], layer2_device[1][1])

    def parse_network(self, network):
        '''\
        Each network element should have an 'id' and 'name' attribute
        and include the following child elements:

            type	(one)
            member	(zero or more with type="interface" or type="channel")
            channel	(zero or more)
        '''
        layer2_members = set()
        layer3_members = 0
        for dev, if_name in self.iter_network_member_devices(network):
            if not dev:
                continue
            devtype = self.device_type(dev)
            if devtype in self.layer2_device_types:
                layer2_members.add(dev)
            elif devtype in self.layer3_device_types:
                layer3_members += 1
            else:
                raise NotImplementedError
        if len(layer2_members) == 0:
            net_type = getFirstChildTextTrimByTagName(network, 'type')
            if not net_type:
                msg = 'no network type found for network: \'%s\'' % \
                    network.toxml('utf-8')
                self.warn(msg)
                assert False    # XXX for testing
                return
            net_cls = self.network_class(network, net_type)
            objid, net_name = self.get_common_attributes(network)
            if self.verbose:
                self.info('parsing network: %s %s' % (net_name, objid))
            if objid in self.session._objs:
                return
            n = self.create_core_object(net_cls, objid, net_name,
                                        network, None)
        # handle channel parameters
        for channel in iterChildrenWithName(network, 'channel'):
            self.parse_network_channel(channel)

    def parse_networks(self):
        '''\
        Parse all 'network' elements.
        '''
        for network in iterDescendantsWithName(self.scenario, 'network'):
            self.parse_network(network)

    def parse_addresses(self, interface):
        mac = []
        ipv4 = []
        ipv6= []
        hostname = []
        for address in iterChildrenWithName(interface, 'address'):
            addr_type = address.getAttribute('type')
            if not addr_type:
                msg = 'no type attribute found for address ' \
                    'in interface: \'%s\'' % interface.toxml('utf-8')
                self.warn(msg)
                assert False    # XXX for testing
                continue
            addr_text = getChildTextTrim(address)
            if not addr_text:
                msg = 'no text found for address ' \
                    'in interface: \'%s\'' % interface.toxml('utf-8')
                self.warn(msg)
                assert False    # XXX for testing
                continue
            if addr_type == 'mac':
                mac.append(addr_text)
            elif addr_type == 'IPv4':
                ipv4.append(addr_text)
            elif addr_type == 'IPv6':
                ipv6.append(addr_text)
            elif addr_type == 'hostname':
                hostname.append(addr_text)
            else:
                msg = 'skipping unknown address type \'%s\' in ' \
                    'interface: \'%s\'' % (addr_type, interface.toxml('utf-8'))
                self.warn(msg)
                assert False    # XXX for testing
                continue
        return mac, ipv4, ipv6, hostname

    def parse_interface(self, node, device_id, interface):
        '''\
        Each interface can have multiple 'address' elements.
        '''
        if_name = interface.getAttribute('name')
        network = self.find_interface_network_object(interface)
        if not network:
            msg = 'skipping node \'%s\' interface \'%s\': ' \
                'unknown network' % (node.name, if_name)
            self.warn(msg)
            assert False    # XXX for testing
            return
        mac, ipv4, ipv6, hostname = self.parse_addresses(interface)
        if mac:
            hwaddr = MacAddr.fromstring(mac[0])
        else:
            hwaddr = None
        ifindex = node.newnetif(network, addrlist = ipv4 + ipv6,
                                hwaddr = hwaddr, ifindex = None,
                                ifname = if_name)
        # TODO: 'hostname' addresses are unused
        if self.verbose:
            msg = 'node \'%s\' interface \'%s\' connected ' \
                'to network \'%s\'' % (node.name, if_name, network.name)
            self.info(msg)
        # set link parameters for wired links
        if isinstance(network,
                      (nodes.HubNode, nodes.PtpNet, nodes.SwitchNode)):
            netif = node.netif(ifindex)
            self.set_wired_link_parameters(network, netif, device_id)

    def set_wired_link_parameters(self, network, netif,
                                  device_id, netif_name = None):
        if netif_name is None:
            netif_name = netif.name
        key = (device_id, netif_name)
        if key in self.link_params:
            link_params = self.link_params[key]
            if self.start:
                bw = link_params.get('bw')
                delay = link_params.get('delay')
                loss = link_params.get('loss')
                duplicate = link_params.get('duplicate')
                jitter = link_params.get('jitter')
                network.linkconfig(netif, bw = bw, delay = delay, loss = loss,
                                   duplicate = duplicate, jitter = jitter)
            else:
                for k, v in link_params.iteritems():
                    netif.setparam(k, v)

    @staticmethod
    def search_for_element(node, tagName, match = None):
        '''\
        Search the given node and all ancestors for an element named
        tagName that satisfies the given matching function.
        '''
        while True:
            for child in iterChildren(node, Node.ELEMENT_NODE):
                if child.tagName == tagName and \
                        (match is None or match(child)):
                    return child
            node = node.parentNode
            if not node:
                break
        return None

    @classmethod
    def find_core_id(cls, node):
        def match(x):
            domain = x.getAttribute('domain')
            return domain == 'COREID'
        alias = cls.search_for_element(node, 'alias', match)
        if alias:
            return getChildTextTrim(alias)
        return None

    @classmethod
    def find_point(cls, node):
        return cls.search_for_element(node, 'point')

    @staticmethod
    def find_channel_network(channel):
        p = channel.parentNode
        if p and p.tagName == 'network':
            return p
        return None

    def find_interface_network_object(self, interface):
        network_id = getFirstChildTextTrimWithAttribute(interface, 'member',
                                                        'type', 'network')
        if not network_id:
            # support legacy notation: <interface net="netid" ...
            network_id = interface.getAttribute('net')
        obj = self.get_core_object(network_id)
        if obj:
            # the network_id should exist for ptp or wlan/emane networks
            return obj
        # the network should correspond to a layer-2 device if the
        # network_id does not exist
        channel_id = getFirstChildTextTrimWithAttribute(interface, 'member',
                                                        'type', 'channel')
        if not network_id or not channel_id:
            return None
        network = getFirstChildWithAttribute(self.scenario, 'network',
                                             'id', network_id)
        if not network:
            return None
        channel = getFirstChildWithAttribute(network, 'channel',
                                             'id', channel_id)
        if not channel:
            return None
        device = None
        for dev, if_name in self.iter_network_member_devices(channel):
            if self.device_type(dev) in self.layer2_device_types:
                assert not device # XXX
                device = dev
        if device:
            obj = self.get_core_object(device.getAttribute('id'))
            if obj:
                return obj
        return None

    def set_object_position_pixel(self, obj, point):
        x = float(point.getAttribute('x'))
        y = float(point.getAttribute('y'))
        z = point.getAttribute('z')
        if z:
            z = float(z)
        else:
            z = 0.0
        # TODO: zMode is unused
        # z_mode = point.getAttribute('zMode'))
        if x < 0.0:
            self.warn('limiting negative x position of \'%s\' to zero: %s' %
                      (obj.name, x))
            x = 0.0
        if y < 0.0:
            self.warn('limiting negative y position of \'%s\' to zero: %s' %
                      (obj.name, y))
            y = 0.0
        obj.setposition(x, y, z)

    def set_object_position_gps(self, obj, point):
        lat = float(point.getAttribute('lat'))
        lon = float(point.getAttribute('lon'))
        zalt = point.getAttribute('z')
        if zalt:
            zalt = float(zalt)
        else:
            zalt = 0.0
        # TODO: zMode is unused
        # z_mode = point.getAttribute('zMode'))
        if not self.location_refgeo_set:
            # for x,y,z conversion, we need a reasonable refpt; this
            # picks the first coordinates as the origin
            self.session.location.setrefgeo(lat, lon, zalt)
            self.location_refgeo_set = True
        x, y, z = self.session.location.getxyz(lat, lon, zalt)
        if x < 0.0:
            self.warn('limiting negative x position of \'%s\' to zero: %s' %
                      (obj.name, x))
            x = 0.0
        if y < 0.0:
            self.warn('limiting negative y position of \'%s\' to zero: %s' %
                      (obj.name, y))
            y = 0.0
        obj.setposition(x, y, z)

    def set_object_position_cartesian(self, obj, point):
        # TODO: review this
        xm = float(point.getAttribute('x'))
        ym = float(point.getAttribute('y'))
        zm = point.getAttribute('z')
        if zm:
            zm = float(zm)
        else:
            zm = 0.0
        # TODO: zMode is unused
        # z_mode = point.getAttribute('zMode'))
        if not self.location_refxyz_set:
            self.session.location.refxyz = xm, ym, zm
            self.location_refxyz_set = True
        # need to convert meters to pixels
        x = self.session.location.m2px(xm) + self.session.location.refxyz[0]
        y = self.session.location.m2px(ym) + self.session.location.refxyz[1]
        z = self.session.location.m2px(zm) + self.session.location.refxyz[2]
        if x < 0.0:
            self.warn('limiting negative x position of \'%s\' to zero: %s' %
                      (obj.name, x))
            x = 0.0
        if y < 0.0:
            self.warn('limiting negative y position of \'%s\' to zero: %s' %
                      (obj.name, y))
            y = 0.0
        obj.setposition(x, y, z)

    def set_object_position(self, obj, element):
        '''\
        Set the x,y,x position of obj from the point associated with
        the given element.
        '''
        point = self.find_point(element)
        if not point:
            return False
        point_type = point.getAttribute('type')
        if not point_type:
            msg = 'no type attribute found for point: \'%s\'' % \
                point.toxml('utf-8')
            self.warn(msg)
            assert False    # XXX for testing
            return False
        elif point_type == 'pixel':
            self.set_object_position_pixel(obj, point)
        elif point_type == 'gps':
            self.set_object_position_gps(obj, point)
        elif point_type == 'cart':
            self.set_object_position_cartesian(obj, point)
        else:
            self.warn("skipping unknown point type: '%s'" % point_type)
            assert False    # XXX for testing
            return False
        if self.verbose:
            msg = 'set position of %s from point element: \'%s\'' % \
                (obj.name, point.toxml('utf-8'))
            self.info(msg)
        return True

    def parse_device_service(self, service, node):
        name = service.getAttribute('name')
        session_service = self.session.services.getservicebyname(name)
        if not session_service:
            assert False        # XXX for testing
            return None
        values = []
        startup_idx = service.getAttribute('startup_idx')
        if startup_idx:
            values.append('startidx=%s' % startup_idx)
        startup_time = service.getAttribute('start_time')
        if startup_time:
            values.append('starttime=%s' % startup_time)
        dirs = []
        for directory in iterChildrenWithName(service, 'directory'):
            dirname = directory.getAttribute('name')
            dirs.append(str(dirname))
        if dirs:
            values.append("dirs=%s" % dirs)
        startup = []
        shutdown = []
        validate = []
        for command in iterChildrenWithName(service, 'command'):
            command_type = command.getAttribute('type')
            command_text = getChildTextTrim(command)
            if not command_text:
                continue
            if command_type == 'start':
                startup.append(str(command_text))
            elif command_type == 'stop':
                shutdown.append(str(command_text))
            elif command_type == 'validate':
                validate.append(str(command_text))
        if startup:
            values.append('cmdup=%s' % startup)
        if shutdown:
            values.append('cmddown=%s' % shutdown)
        if validate:
            values.append('cmdval=%s' % validate)
        filenames = []
        files = []
        for f in iterChildrenWithName(service, 'file'):
            filename = f.getAttribute('name')
            if not filename:
                continue;
            filenames.append(filename)
            data = getChildTextTrim(f)
            if data:
                data = str(data)
            else:
                data = None
            typestr = 'service:%s:%s' % (name, filename)
            files.append((typestr, filename, data))
        if filenames:
            values.append('files=%s' % filenames)
        custom = service.getAttribute('custom')
        if custom and custom.lower() == 'true':
            self.session.services.setcustomservice(node.objid,
                                                   session_service, values)
        # NOTE: if a custom service is used, setservicefile() must be
        # called after the custom service exists
        for typestr, filename, data in files:
            self.session.services.setservicefile(nodenum = node.objid,
                                                 type = typestr,
                                                 filename = filename,
                                                 srcname = None,
                                                 data = data)
        return str(name)

    def parse_device_services(self, services, node):
        '''\
        Use session.services manager to store service customizations
        before they are added to a node.
        '''
        service_names = []
        for service in iterChildrenWithName(services, 'service'):
            name = self.parse_device_service(service, node)
            if name:
                service_names.append(name)
        return '|'.join(service_names)

    def add_device_services(self, node, device, node_type):
        '''\
        Add services to the given node.
        '''
        services = getFirstChildByTagName(device, 'CORE:services')
        if services:
            services_str = self.parse_device_services(services, node)
            if self.verbose:
                self.info('services for node \'%s\': %s' % \
                              (node.name, services_str))
        elif node_type in self.default_services:
            services_str = None # default services will be added
        else:
            return
        self.session.services.addservicestonode(node = node,
                                                nodetype = node_type,
                                                services_str = services_str,
                                                verbose = self.verbose)

    def set_object_presentation(self, obj, element, node_type):
        # defaults from the CORE GUI
        default_icons = {
            'router': 'router.gif',
            'host': 'host.gif',
            'PC': 'pc.gif',
            'mdr': 'mdr.gif',
            # 'prouter': 'router_green.gif',
            # 'xen': 'xen.gif'
            }
        icon_set = False
        for child in iterChildrenWithName(element, 'CORE:presentation'):
            canvas = child.getAttribute('canvas')
            if canvas:
                obj.canvas = int(canvas)
            icon = child.getAttribute('icon')
            if icon:
                icon = str(icon).replace("$CORE_DATA_DIR",
                                         constants.CORE_DATA_DIR)
                obj.icon = icon
                icon_set = True
        if not icon_set and node_type in default_icons:
            obj.icon = default_icons[node_type]

    def device_type(self, device):
        if device.tagName in self.device_types:
            return device.tagName
        return None

    def core_node_type(self, device):
        # use an explicit CORE type if it exists
        coretype = getFirstChildTextTrimWithAttribute(device, 'type',
                                                      'domain', 'CORE')
        if coretype:
            return coretype
        return self.device_type(device)

    def find_device_with_interface(self, interface_id):
        # TODO: suport generic 'device' elements
        for device in iterDescendantsWithName(self.scenario,
                                              self.device_types):
            interface = getFirstChildWithAttribute(device, 'interface',
                                                   'id', interface_id)
            if interface:
                if_name = interface.getAttribute('name')
                return device, if_name
        return None, None

    def parse_layer2_device(self, device):
        objid, device_name = self.get_common_attributes(device)
        if self.verbose:
            self.info('parsing layer-2 device: %s %s' % (device_name, objid))
        try:
            return self.session.obj(objid)
        except KeyError:
            pass
        device_type = self.device_type(device)
        if device_type == 'hub':
            device_class = nodes.HubNode
        elif device_type == 'switch':
            device_class = nodes.SwitchNode
        else:
            self.warn('unknown layer-2 device type: \'%s\'' % device_type)
            assert False        # XXX for testing
            return None
        n = self.create_core_object(device_class, objid, device_name,
                                    device, None)
        return n

    def parse_layer3_device(self, device):
        objid, device_name = self.get_common_attributes(device)
        if self.verbose:
            self.info('parsing layer-3 device: %s %s' % (device_name, objid))
        try:
            return self.session.obj(objid)
        except KeyError:
            pass
        device_cls = self.nodecls
        core_node_type = self.core_node_type(device)
        n = self.create_core_object(device_cls, objid, device_name,
                                    device, core_node_type)
        n.type = core_node_type
        self.add_device_services(n, device, core_node_type)
        for interface in iterChildrenWithName(device, 'interface'):
            self.parse_interface(n, device.getAttribute('id'), interface)
        return n

    def parse_layer2_devices(self):
        '''\
        Parse all layer-2 device elements.  A device can be: 'switch',
        'hub'.
        '''
        # TODO: suport generic 'device' elements
        for device in iterDescendantsWithName(self.scenario,
                                              self.layer2_device_types):
            self.parse_layer2_device(device)

    def parse_layer3_devices(self):
        '''\
        Parse all layer-3 device elements.  A device can be: 'host',
        'router'.
        '''
        # TODO: suport generic 'device' elements
        for device in iterDescendantsWithName(self.scenario,
                                              self.layer3_device_types):
            self.parse_layer3_device(device)

    def parse_session_origin(self, session_config):
        '''\
        Parse the first origin tag and set the CoreLocation reference
        point appropriately.
        '''
        # defaults from the CORE GUI
        self.session.location.setrefgeo(47.5791667, -122.132322, 2.0)
        self.session.location.refscale = 150.0
        origin = getFirstChildByTagName(session_config, 'origin')
        if not origin:
            return
        lat = origin.getAttribute('lat')
        lon = origin.getAttribute('lon')
        alt = origin.getAttribute('alt')
        if lat and lon and alt:
            self.session.location.setrefgeo(float(lat), float(lon), float(alt))
            self.location_refgeo_set = True
        scale100 = origin.getAttribute("scale100")
        if scale100:
            self.session.location.refscale = float(scale100)
        point = getFirstChildTextTrimByTagName(origin, 'point')
        if point:
            xyz = point.split(',')
            if len(xyz) == 2:
                xyz.append('0.0')
            if len(xyz) == 3:
                self.session.location.refxyz = \
                    (float(xyz[0]), float(xyz[1]), float(xyz[2]))
                self.location_refxyz_set = True

    def parse_session_options(self, session_config):
        options = getFirstChildByTagName(session_config, 'options')
        if not options:
            return
        params = self.parse_parameter_children(options)
        for name, value in params.iteritems():
            if name and value:
                setattr(self.session.options, str(name), str(value))

    def parse_session_hooks(self, session_config):
        '''\
        Parse hook scripts.
        '''
        hooks = getFirstChildByTagName(session_config, 'hooks')
        if not hooks:
            return
        for hook in iterChildrenWithName(hooks, 'hook'):
            filename = hook.getAttribute('name')
            state = hook.getAttribute('state')
            data = getChildTextTrim(hook)
            if data is None:
                data = ''       # allow for empty file
            hook_type = "hook:%s" % state
            self.session.sethook(hook_type, filename = str(filename),
                                 srcname = None, data = str(data))

    def parse_session_metadata(self, session_config):
        metadata = getFirstChildByTagName(session_config, 'metadata')
        if not metadata:
            return
        params = self.parse_parameter_children(metadata)
        for name, value in params.iteritems():
            if name and value:
                self.session.metadata.additem(str(name), str(value))

    def parse_session_config(self):
        session_config = \
            getFirstChildByTagName(self.scenario, 'CORE:sessionconfig')
        if not session_config:
            return
        self.parse_session_origin(session_config)
        self.parse_session_options(session_config)
        self.parse_session_hooks(session_config)
        self.parse_session_metadata(session_config)

    def parse_default_services(self):
        # defaults from the CORE GUI
        self.default_services = {
            'router': ['zebra', 'OSPFv2', 'OSPFv3', 'vtysh', 'IPForward'],
            'host': ['DefaultRoute', 'SSH'],
            'PC': ['DefaultRoute',],
            'mdr': ['zebra', 'OSPFv3MDR', 'vtysh', 'IPForward'],
            # 'prouter': ['zebra', 'OSPFv2', 'OSPFv3', 'vtysh', 'IPForward'],
            # 'xen': ['zebra', 'OSPFv2', 'OSPFv3', 'vtysh', 'IPForward'],
            }
        default_services = \
            getFirstChildByTagName(self.scenario, 'CORE:defaultservices')
        if not default_services:
            return
        for device in iterChildrenWithName(default_services, 'device'):
            device_type = device.getAttribute('type')
            if not device_type:
                self.warn('parse_default_services: no type attribute ' \
                              'found for device')
                continue
            services = []
            for service in iterChildrenWithName(device, 'service'):
                name = service.getAttribute('name')
                if name:
                    services.append(str(name))
            self.default_services[device_type] = services
        # store default services for the session
        for t, s in self.default_services.iteritems():
            self.session.services.defaultservices[t] = s
            if self.verbose:
                self.info('default services for node type \'%s\' ' \
                              'set to: %s' % (t, s))