예제 #1
0
파일: session.py 프로젝트: D3f0/coreemu
class Session(object):

    # sessions that get automatically shutdown when the process
    # terminates normally
    __sessions = set()

    ''' CORE session manager.
    '''
    def __init__(self, sessionid = None, cfg = {}, server = None,
                 persistent = False, mkdir = True):
        if sessionid is None:
            # try to keep this short since it's used to construct
            # network interface names
            pid = os.getpid()
            sessionid = ((pid >> 16) ^
                         (pid & ((1 << 16) - 1)))
            sessionid ^= ((id(self) >> 16) ^ (id(self) & ((1 << 16) - 1)))
            sessionid &= 0xffff
        self.sessionid = sessionid
        self.sessiondir = os.path.join(tempfile.gettempdir(),
                                       "pycore.%s" % self.sessionid)
        if mkdir:
            os.mkdir(self.sessiondir)
        self.name = None
        self.filename = None
        self.thumbnail = None
        self.user = None
        self.node_count = None
        self._time = time.time()
        self.evq = EventLoop()
        # dict of objects: all nodes and nets
        self._objs = {}
        self._objslock = threading.Lock()
        # dict of configurable objects
        self._confobjs = {}
        self._confobjslock = threading.Lock()
        self._handlers = set()
        self._handlerslock = threading.Lock()
        self._state = None
        self._hooks = {}
        self._state_hooks = {}
        # dict of configuration items from /etc/core/core.conf config file
        self.cfg = cfg
        self.add_state_hook(coreapi.CORE_EVENT_RUNTIME_STATE,
                            self.runtime_state_hook)
        self.setstate(state=coreapi.CORE_EVENT_DEFINITION_STATE,
                      info=False, sendevent=False)
        self.server = server
        if not persistent:
            self.addsession(self)
        self.master = False
        self.broker = CoreBroker(session=self, verbose=True)
        self.location = CoreLocation(self)
        self.mobility = MobilityManager(self)
        self.services = CoreServices(self)
        self.emane = emane.Emane(self)
        self.xen = xenconfig.XenConfigManager(self)
        self.sdt = Sdt(self)
        # future parameters set by the GUI may go here
        self.options = SessionConfig(self)
        self.metadata = SessionMetaData(self)

    @classmethod
    def addsession(cls, session):
        cls.__sessions.add(session)

    @classmethod
    def delsession(cls, session):
        try:
            cls.__sessions.remove(session)
        except KeyError:
            pass

    @classmethod
    def atexit(cls):
        while cls.__sessions:
            s = cls.__sessions.pop()
            print >> sys.stderr, "WARNING: automatically shutting down " \
                "non-persistent session %s" % s.sessionid
            s.shutdown()

    def __del__(self):
        # note: there is no guarantee this will ever run
        self.shutdown()

    def shutdown(self):
        ''' Shut down all emulation objects and remove the session directory.
        '''
        if hasattr(self, 'emane'):
            self.emane.shutdown()
        if hasattr(self, 'broker'):
            self.broker.shutdown()
        if hasattr(self, 'sdt'):
            self.sdt.shutdown()
        self.delobjs()
        preserve = False
        if hasattr(self.options, 'preservedir'):
            if self.options.preservedir == '1':
                preserve = True
        if not preserve:
            shutil.rmtree(self.sessiondir, ignore_errors = True)
        if self.server:
            self.server.delsession(self)
        self.delsession(self)

    def isconnected(self):
        ''' Returns true if this session has a request handler.
        '''
        with self._handlerslock:
            if len(self._handlers) == 0:
                return False
            else:
                return True

    def connect(self, handler):
        ''' Set the request handler for this session, making it connected.
        '''
        # the master flag will only be set after a GUI has connected with the
        # handler, e.g. not during normal startup
        if handler.master is True:
            self.master = True
        with self._handlerslock:
            self._handlers.add(handler)

    def disconnect(self, handler):
        ''' Disconnect a request handler from this session. Shutdown this
            session if there is no running emulation.
        '''
        with self._handlerslock:
            try:
                self._handlers.remove(handler)
            except KeyError:
                raise ValueError, \
                    "Handler %s not associated with this session" % handler
            num_handlers = len(self._handlers)
        if num_handlers == 0:
            # shut down this session unless we are instantiating, running,
            # or collecting final data
            if self.getstate() < coreapi.CORE_EVENT_INSTANTIATION_STATE or \
                    self.getstate() > coreapi.CORE_EVENT_DATACOLLECT_STATE:
                self.shutdown()

    def broadcast(self, src, msg):
        ''' Send Node and Link CORE API messages to all handlers connected to this session.
        '''
        self._handlerslock.acquire()
        for handler in self._handlers:
            if handler == src:
                continue
            if isinstance(msg, coreapi.CoreNodeMessage) or \
                    isinstance(msg, coreapi.CoreLinkMessage):
                try:
                    handler.sendall(msg.rawmsg)
                except Exception, e:
                    self.warn("sendall() error: %s" % e)
        self._handlerslock.release()
예제 #2
0
class Session(object):
    """
    CORE session manager.
    """
    def __init__(self, session_id, config=None, mkdir=True):
        """
        Create a Session instance.

        :param int session_id: session id
        :param dict config: session configuration
        :param bool mkdir: flag to determine if a directory should be made
        """
        self.session_id = session_id

        # define and create session directory when desired
        self.session_dir = os.path.join(tempfile.gettempdir(),
                                        "pycore.%s" % self.session_id)
        if mkdir:
            os.mkdir(self.session_dir)

        self.name = None
        self.file_name = None
        self.thumbnail = None
        self.user = None
        self.event_loop = EventLoop()

        # dict of objects: all nodes and nets
        self.objects = {}
        self._objects_lock = threading.Lock()

        # TODO: should the default state be definition?
        self.state = EventTypes.NONE.value
        self._state_time = time.time()
        self._state_file = os.path.join(self.session_dir, "state")

        self._hooks = {}
        self._state_hooks = {}

        self.add_state_hook(state=EventTypes.RUNTIME_STATE.value,
                            hook=self.runtime_state_hook)

        self.master = False

        # handlers for broadcasting information
        self.event_handlers = []
        self.exception_handlers = []
        self.node_handlers = []
        self.link_handlers = []
        self.file_handlers = []
        self.config_handlers = []
        self.shutdown_handlers = []

        # session options/metadata
        self.options = SessionConfig()
        if not config:
            config = {}
        for key, value in config.iteritems():
            self.options.set_config(key, value)
        self.metadata = SessionMetaData()

        # initialize session feature helpers
        self.broker = CoreBroker(session=self)
        self.location = CoreLocation()
        self.mobility = MobilityManager(session=self)
        self.services = CoreServices(session=self)
        self.emane = EmaneManager(session=self)
        self.sdt = Sdt(session=self)

    def shutdown(self):
        """
        Shutdown all emulation objects and remove the session directory.
        """
        # shutdown/cleanup feature helpers
        self.emane.shutdown()
        self.broker.shutdown()
        self.sdt.shutdown()

        # delete all current objects
        self.delete_objects()

        # remove this sessions working directory
        preserve = self.options.get_config("preservedir") == "1"
        if not preserve:
            shutil.rmtree(self.session_dir, ignore_errors=True)

        # call session shutdown handlers
        for handler in self.shutdown_handlers:
            handler(self)

    def broadcast_event(self, event_data):
        """
        Handle event data that should be provided to event handler.

        :param core.data.EventData event_data: event data to send out
        :return: nothing
        """

        for handler in self.event_handlers:
            handler(event_data)

    def broadcast_exception(self, exception_data):
        """
        Handle exception data that should be provided to exception handlers.

        :param core.data.ExceptionData exception_data: exception data to send out
        :return: nothing
        """

        for handler in self.exception_handlers:
            handler(exception_data)

    def broadcast_node(self, node_data):
        """
        Handle node data that should be provided to node handlers.

        :param core.data.ExceptionData node_data: node data to send out
        :return: nothing
        """

        for handler in self.node_handlers:
            handler(node_data)

    def broadcast_file(self, file_data):
        """
        Handle file data that should be provided to file handlers.

        :param core.data.FileData file_data: file data to send out
        :return: nothing
        """

        for handler in self.file_handlers:
            handler(file_data)

    def broadcast_config(self, config_data):
        """
        Handle config data that should be provided to config handlers.

        :param core.data.ConfigData config_data: config data to send out
        :return: nothing
        """

        for handler in self.config_handlers:
            handler(config_data)

    def broadcast_link(self, link_data):
        """
        Handle link data that should be provided to link handlers.

        :param core.data.ExceptionData link_data: link data to send out
        :return: nothing
        """

        for handler in self.link_handlers:
            handler(link_data)

    def set_state(self, state, send_event=False):
        """
        Set the session's current state.

        :param core.enumerations.EventTypes state: state to set to
        :param send_event: if true, generate core API event messages
        :return: nothing
        """
        state_value = state.value
        state_name = state.name

        if self.state == state_value:
            logger.info("session(%s) is already in state: %s, skipping change",
                        self.session_id, state_name)
            return

        self.state = state_value
        self._state_time = time.time()
        logger.info("changing session(%s) to state %s", self.session_id,
                    state_name)

        self.write_state(state_value)
        self.run_hooks(state_value)
        self.run_state_hooks(state_value)

        if send_event:
            event_data = EventData(event_type=state_value,
                                   time="%s" % time.time())
            self.broadcast_event(event_data)

    def write_state(self, state):
        """
        Write the current state to a state file in the session dir.

        :param int state: state to write to file
        :return: nothing
        """
        try:
            state_file = open(self._state_file, "w")
            state_file.write("%d %s\n" % (state, coreapi.state_name(state)))
            state_file.close()
        except IOError:
            logger.exception("error writing state file: %s", state)

    def run_hooks(self, state):
        """
        Run hook scripts upon changing states. If hooks is not specified, run all hooks in the given state.

        :param int state: state to run hooks for
        :return: nothing
        """

        # check that state change hooks exist
        if state not in self._hooks:
            return

        # retrieve all state hooks
        hooks = self._hooks.get(state, [])

        # execute all state hooks
        if hooks:
            for hook in hooks:
                self.run_hook(hook)
        else:
            logger.info("no state hooks for %s", state)

    def set_hook(self, hook_type, file_name, source_name, data):
        """
        Store a hook from a received file message.

        :param str hook_type: hook type
        :param str file_name: file name for hook
        :param str source_name: source name
        :param data: hook data
        :return: nothing
        """
        logger.info("setting state hook: %s - %s from %s", hook_type,
                    file_name, source_name)

        _hook_id, state = hook_type.split(':')[:2]
        if not state.isdigit():
            logger.error("error setting hook having state '%s'", state)
            return

        state = int(state)
        hook = file_name, data

        # append hook to current state hooks
        state_hooks = self._hooks.setdefault(state, [])
        state_hooks.append(hook)

        # immediately run a hook if it is in the current state
        # (this allows hooks in the definition and configuration states)
        if self.state == state:
            logger.info("immediately running new state hook")
            self.run_hook(hook)

    def del_hooks(self):
        """
        Clear the hook scripts dict.
        """
        self._hooks.clear()

    def run_hook(self, hook):
        """
        Run a hook.

        :param tuple hook: hook to run
        :return: nothing
        """
        file_name, data = hook
        logger.info("running hook %s", file_name)

        # write data to hook file
        try:
            hook_file = open(os.path.join(self.session_dir, file_name), "w")
            hook_file.write(data)
            hook_file.close()
        except IOError:
            logger.exception("error writing hook '%s'", file_name)

        # setup hook stdout and stderr
        try:
            stdout = open(os.path.join(self.session_dir, file_name + ".log"),
                          "w")
            stderr = subprocess.STDOUT
        except IOError:
            logger.exception("error setting up hook stderr and stdout")
            stdout = None
            stderr = None

        # execute hook file
        try:
            args = ["/bin/sh", file_name]
            subprocess.check_call(args,
                                  stdout=stdout,
                                  stderr=stderr,
                                  close_fds=True,
                                  cwd=self.session_dir,
                                  env=self.get_environment())
        except (OSError, subprocess.CalledProcessError):
            logger.exception("error running hook: %s", file_name)

    def run_state_hooks(self, state):
        """
        Run state hooks.

        :param int state: state to run hooks for
        :return: nothing
        """
        for hook in self._state_hooks.get(state, []):
            try:
                hook(state)
            except:
                message = "exception occured when running %s state hook: %s" % (
                    coreapi.state_name(state), hook)
                logger.exception(message)
                self.exception(ExceptionLevels.ERROR,
                               "Session.run_state_hooks", None, message)

    def add_state_hook(self, state, hook):
        """
        Add a state hook.

        :param int state: state to add hook for
        :param func hook: hook callback for the state
        :return: nothing
        """
        hooks = self._state_hooks.setdefault(state, [])
        if hook in hooks:
            raise ValueError("attempting to add duplicate state hook")
        hooks.append(hook)

        if self.state == state:
            hook(state)

    def del_state_hook(self, state, hook):
        """
        Delete a state hook.

        :param int state: state to delete hook for
        :param func hook: hook to delete
        :return:
        """
        hooks = self._state_hooks.setdefault(state, [])
        hooks.remove(hook)

    def runtime_state_hook(self, state):
        """
        Runtime state hook check.

        :param int state: state to check
        :return: nothing
        """
        if state == EventTypes.RUNTIME_STATE.value:
            self.emane.poststartup()
            xml_file_version = self.options.get_config("xmlfilever")
            if xml_file_version in ("1.0", ):
                xml_file_name = os.path.join(self.session_dir,
                                             "session-deployed.xml")
                xml_writer = corexml.CoreXmlWriter(self)
                corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario)
                xml_writer.write(xml_file_name)

    def get_environment(self, state=True):
        """
        Get an environment suitable for a subprocess.Popen call.
        This is the current process environment with some session-specific
        variables.

        :param bool state: flag to determine if session state should be included
        :return:
        """
        env = os.environ.copy()
        env["SESSION"] = "%s" % self.session_id
        env["SESSION_SHORT"] = "%s" % self.short_session_id()
        env["SESSION_DIR"] = "%s" % self.session_dir
        env["SESSION_NAME"] = "%s" % self.name
        env["SESSION_FILENAME"] = "%s" % self.file_name
        env["SESSION_USER"] = "******" % self.user
        env["SESSION_NODE_COUNT"] = "%s" % self.get_node_count()

        if state:
            env["SESSION_STATE"] = "%s" % self.state

        # attempt to read and add environment config file
        environment_config_file = os.path.join(constants.CORE_CONF_DIR,
                                               "environment")
        try:
            if os.path.isfile(environment_config_file):
                utils.load_config(environment_config_file, env)
        except IOError:
            logger.warn("environment configuration file does not exist: %s",
                        environment_config_file)

        # attempt to read and add user environment file
        if self.user:
            environment_user_file = os.path.join("/home", self.user, ".core",
                                                 "environment")
            try:
                utils.load_config(environment_user_file, env)
            except IOError:
                logger.debug(
                    "user core environment settings file not present: %s",
                    environment_user_file)

        return env

    def set_thumbnail(self, thumb_file):
        """
        Set the thumbnail filename. Move files from /tmp to session dir.

        :param str thumb_file: tumbnail file to set for session
        :return: nothing
        """
        if not os.path.exists(thumb_file):
            logger.error("thumbnail file to set does not exist: %s",
                         thumb_file)
            self.thumbnail = None
            return

        destination_file = os.path.join(self.session_dir,
                                        os.path.basename(thumb_file))
        shutil.copy(thumb_file, destination_file)
        self.thumbnail = destination_file

    def set_user(self, user):
        """
        Set the username for this session. Update the permissions of the
        session dir to allow the user write access.

        :param str user: user to give write permissions to for the session directory
        :return: nothing
        """
        if user:
            try:
                uid = pwd.getpwnam(user).pw_uid
                gid = os.stat(self.session_dir).st_gid
                os.chown(self.session_dir, uid, gid)
            except IOError:
                logger.exception("failed to set permission on %s",
                                 self.session_dir)

        self.user = user

    def get_object_id(self):
        """
        Return a unique, new random object id.
        """
        object_id = None

        with self._objects_lock:
            while True:
                object_id = random.randint(1, 0xFFFF)
                if object_id not in self.objects:
                    break

        return object_id

    def add_object(self, cls, *clsargs, **clskwds):
        """
        Add an emulation object.

        :param class cls: object class to add
        :param list clsargs: list of arguments for the class to create
        :param dict clskwds: dictionary of arguments for the class to create
        :return: the created class instance
        """
        obj = cls(self, *clsargs, **clskwds)

        self._objects_lock.acquire()
        if obj.objid in self.objects:
            self._objects_lock.release()
            obj.shutdown()
            raise KeyError("duplicate object id %s for %s" % (obj.objid, obj))
        self.objects[obj.objid] = obj
        self._objects_lock.release()

        return obj

    def get_object(self, object_id):
        """
        Get an emulation object.

        :param int object_id: object id to retrieve
        :return: object for the given id
        :rtype: core.coreobj.PyCoreNode
        """
        if object_id not in self.objects:
            raise KeyError("unknown object id %s" % object_id)
        return self.objects[object_id]

    def get_object_by_name(self, name):
        """
        Get an emulation object using its name attribute.

        :param str name: name of object to retrieve
        :return: object for the name given
        """
        with self._objects_lock:
            for obj in self.objects.itervalues():
                if hasattr(obj, "name") and obj.name == name:
                    return obj
        raise KeyError("unknown object with name %s" % name)

    def delete_object(self, object_id):
        """
        Remove an emulation object.

        :param int object_id: object id to remove
        :return: nothing
        """
        with self._objects_lock:
            try:
                obj = self.objects.pop(object_id)
                obj.shutdown()
            except KeyError:
                logger.error(
                    "failed to remove object, object with id was not found: %s",
                    object_id)

    def delete_objects(self):
        """
        Clear the objects dictionary, and call shutdown for each object.
        """
        with self._objects_lock:
            while self.objects:
                _, obj = self.objects.popitem()
                obj.shutdown()

    def write_objects(self):
        """
        Write objects to a 'nodes' file in the session dir.
        The 'nodes' file lists: number, name, api-type, class-type
        """
        try:
            nodes_file = open(os.path.join(self.session_dir, "nodes"), "w")
            with self._objects_lock:
                for object_id in sorted(self.objects.keys()):
                    obj = self.objects[object_id]
                    nodes_file.write(
                        "%s %s %s %s\n" %
                        (object_id, obj.name, obj.apitype, type(obj)))
            nodes_file.close()
        except IOError:
            logger.exception("error writing nodes file")

    def dump_session(self):
        """
        Log information about the session in its current state.
        """
        logger.info("session id=%s name=%s state=%s", self.session_id,
                    self.name, self.state)
        logger.info("file=%s thumbnail=%s node_count=%s/%s", self.file_name,
                    self.thumbnail, self.get_node_count(), len(self.objects))

    def exception(self, level, source, object_id, text):
        """
        Generate and broadcast an exception event.

        :param str level: exception level
        :param str source: source name
        :param int object_id: object id
        :param str text: exception message
        :return: nothing
        """

        exception_data = ExceptionData(node=object_id,
                                       session=str(self.session_id),
                                       level=level,
                                       source=source,
                                       date=time.ctime(),
                                       text=text)

        self.broadcast_exception(exception_data)

    def instantiate(self):
        """
        We have entered the instantiation state, invoke startup methods
        of various managers and boot the nodes. Validate nodes and check
        for transition to the runtime state.
        """

        # write current objects out to session directory file
        self.write_objects()

        # controlnet may be needed by some EMANE models
        self.add_remove_control_interface(node=None, remove=False)

        # instantiate will be invoked again upon Emane configure
        if self.emane.startup() == self.emane.NOT_READY:
            return

        # start feature helpers
        self.broker.startup()
        self.mobility.startup()

        # boot the services on each node
        self.boot_nodes()

        # set broker local instantiation to complete
        self.broker.local_instantiation_complete()

        # notify listeners that instantiation is complete
        event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value)
        self.broadcast_event(event)

        # assume either all nodes have booted already, or there are some
        # nodes on slave servers that will be booted and those servers will
        # send a node status response message
        self.check_runtime()

    def get_node_count(self):
        """
        Returns the number of CoreNodes and CoreNets, except for those
        that are not considered in the GUI's node count.
        """

        with self._objects_lock:
            count = len([
                x for x in self.objects
                if not nodeutils.is_node(x, (NodeTypes.PEER_TO_PEER,
                                             NodeTypes.CONTROL_NET))
            ])

            # on Linux, GreTapBridges are auto-created, not part of GUI's node count
            count -= len([
                x for x in self.objects
                if nodeutils.is_node(x, NodeTypes.TAP_BRIDGE)
                and not nodeutils.is_node(x, NodeTypes.TUNNEL)
            ])

        return count

    def check_runtime(self):
        """
        Check if we have entered the runtime state, that all nodes have been
        started and the emulation is running. Start the event loop once we
        have entered runtime (time=0).
        """
        # this is called from instantiate() after receiving an event message
        # for the instantiation state, and from the broker when distributed
        # nodes have been started
        logger.info(
            "session(%s) checking if not in runtime state, current state: %s",
            self.session_id, coreapi.state_name(self.state))
        if self.state == EventTypes.RUNTIME_STATE.value:
            logger.info("valid runtime state found, returning")
            return

        # check to verify that all nodes and networks are running
        if not self.broker.instantiation_complete():
            return

        # start event loop and set to runtime
        self.event_loop.run()
        self.set_state(EventTypes.RUNTIME_STATE, send_event=True)

    def data_collect(self):
        """
        Tear down a running session. Stop the event loop and any running
        nodes, and perform clean-up.
        """
        # stop event loop
        self.event_loop.stop()

        # stop node services
        with self._objects_lock:
            for obj in self.objects.itervalues():
                # TODO: determine if checking for CoreNode alone is ok
                if isinstance(obj, nodes.PyCoreNode):
                    self.services.stop_services(obj)

        # shutdown emane
        self.emane.shutdown()

        # update control interface hosts
        self.update_control_interface_hosts(remove=True)

        # remove all four possible control networks. Does nothing if ctrlnet is not installed.
        self.add_remove_control_interface(node=None, net_index=0, remove=True)
        self.add_remove_control_interface(node=None, net_index=1, remove=True)
        self.add_remove_control_interface(node=None, net_index=2, remove=True)
        self.add_remove_control_interface(node=None, net_index=3, remove=True)

    def check_shutdown(self):
        """
        Check if we have entered the shutdown state, when no running nodes
        and links remain.
        """
        node_count = self.get_node_count()
        logger.info("session(%s) checking shutdown: %s nodes remaining",
                    self.session_id, node_count)

        shutdown = False
        if node_count == 0:
            shutdown = True
            self.set_state(EventTypes.SHUTDOWN_STATE)

        return shutdown

    def short_session_id(self):
        """
        Return a shorter version of the session ID, appropriate for
        interface names, where length may be limited.
        """
        ssid = (self.session_id >> 8) ^ (self.session_id & ((1 << 8) - 1))
        return "%x" % ssid

    def boot_nodes(self):
        """
        Invoke the boot() procedure for all nodes and send back node
        messages to the GUI for node messages that had the status
        request flag.
        """
        with self._objects_lock:
            pool = ThreadPool()
            results = []

            start = time.time()
            for obj in self.objects.itervalues():
                # TODO: PyCoreNode is not the type to check
                if isinstance(obj, nodes.PyCoreNode) and not nodeutils.is_node(
                        obj, NodeTypes.RJ45):
                    # add a control interface if configured
                    logger.info("booting node: %s", obj.name)
                    self.add_remove_control_interface(node=obj, remove=False)
                    result = pool.apply_async(self.services.boot_services,
                                              (obj, ))
                    results.append(result)

            pool.close()
            pool.join()
            for result in results:
                result.get()
            logger.debug("boot run time: %s", time.time() - start)

        self.update_control_interface_hosts()

    def get_control_net_prefixes(self):
        """
        Retrieve control net prefixes.

        :return: control net prefix list
        :rtype: list
        """
        p = self.options.get_config("controlnet")
        p0 = self.options.get_config("controlnet0")
        p1 = self.options.get_config("controlnet1")
        p2 = self.options.get_config("controlnet2")
        p3 = self.options.get_config("controlnet3")

        if not p0 and p:
            p0 = p

        return [p0, p1, p2, p3]

    def get_control_net_server_interfaces(self):
        """
        Retrieve control net server interfaces.

        :return: list of control net server interfaces
        :rtype: list
        """
        d0 = self.options.get_config("controlnetif0")
        if d0:
            logger.error(
                "controlnet0 cannot be assigned with a host interface")
        d1 = self.options.get_config("controlnetif1")
        d2 = self.options.get_config("controlnetif2")
        d3 = self.options.get_config("controlnetif3")
        return [None, d1, d2, d3]

    def get_control_net_index(self, dev):
        """
        Retrieve control net index.

        :param str dev: device to get control net index for
        :return: control net index, -1 otherwise
        :rtype: int
        """
        if dev[0:4] == "ctrl" and int(dev[4]) in [0, 1, 2, 3]:
            index = int(dev[4])
            if index == 0:
                return index
            if index < 4 and self.get_control_net_prefixes(
            )[index] is not None:
                return index
        return -1

    def get_control_net_object(self, net_index):
        # TODO: all nodes use an integer id and now this wants to use a string
        object_id = "ctrl%dnet" % net_index
        return self.get_object(object_id)

    def add_remove_control_net(self,
                               net_index,
                               remove=False,
                               conf_required=True):
        """
        Create a control network bridge as necessary.
        When the remove flag is True, remove the bridge that connects control
        interfaces. The conf_reqd flag, when False, causes a control network
        bridge to be added even if one has not been configured.

        :param int net_index: network index
        :param bool remove: flag to check if it should be removed
        :param bool conf_required: flag to check if conf is required
        :return: control net object
        :rtype: core.netns.nodes.CtrlNet
        """
        logger.debug(
            "add/remove control net: index(%s) remove(%s) conf_required(%s)",
            net_index, remove, conf_required)
        prefix_spec_list = self.get_control_net_prefixes()
        prefix_spec = prefix_spec_list[net_index]
        if not prefix_spec:
            if conf_required:
                # no controlnet needed
                return None
            else:
                control_net_class = nodeutils.get_node_class(
                    NodeTypes.CONTROL_NET)
                prefix_spec = control_net_class.DEFAULT_PREFIX_LIST[net_index]
        logger.debug("prefix spec: %s", prefix_spec)

        server_interface = self.get_control_net_server_interfaces()[net_index]

        # return any existing controlnet bridge
        try:
            control_net = self.get_control_net_object(net_index)

            if remove:
                self.delete_object(control_net.objid)
                return None

            return control_net
        except KeyError:
            if remove:
                return None

        # build a new controlnet bridge
        object_id = "ctrl%dnet" % net_index

        # use the updown script for control net 0 only.
        updown_script = None

        if net_index == 0:
            updown_script = self.options.get_config("controlnet_updown_script")
            if not updown_script:
                logger.warning("controlnet updown script not configured")

        prefixes = prefix_spec.split()
        if len(prefixes) > 1:
            # a list of per-host prefixes is provided
            assign_address = True
            if self.master:
                try:
                    # split first (master) entry into server and prefix
                    prefix = prefixes[0].split(":", 1)[1]
                except IndexError:
                    # no server name. possibly only one server
                    prefix = prefixes[0]
            else:
                # slave servers have their name and localhost in the serverlist
                servers = self.broker.getservernames()
                servers.remove("localhost")
                prefix = None

                for server_prefix in prefixes:
                    try:
                        # split each entry into server and prefix
                        server, p = server_prefix.split(":")
                    except ValueError:
                        server = ""
                        p = None

                    if server == servers[0]:
                        # the server name in the list matches this server
                        prefix = p
                        break

                if not prefix:
                    logger.error(
                        "Control network prefix not found for server '%s'" %
                        servers[0])
                    assign_address = False
                    try:
                        prefix = prefixes[0].split(':', 1)[1]
                    except IndexError:
                        prefix = prefixes[0]
        # len(prefixes) == 1
        else:
            # TODO: can we get the server name from the servers.conf or from the node assignments?
            # with one prefix, only master gets a ctrlnet address
            assign_address = self.master
            prefix = prefixes[0]

        control_net_class = nodeutils.get_node_class(NodeTypes.CONTROL_NET)
        control_net = self.add_object(cls=control_net_class,
                                      objid=object_id,
                                      prefix=prefix,
                                      assign_address=assign_address,
                                      updown_script=updown_script,
                                      serverintf=server_interface)

        # tunnels between controlnets will be built with Broker.addnettunnels()
        # TODO: potentially remove documentation saying object ids are ints
        # TODO: need to move broker code out of the session object
        self.broker.addnet(object_id)
        for server in self.broker.getservers():
            self.broker.addnodemap(server, object_id)

        return control_net

    def add_remove_control_interface(self,
                                     node,
                                     net_index=0,
                                     remove=False,
                                     conf_required=True):
        """
        Add a control interface to a node when a 'controlnet' prefix is
        listed in the config file or session options. Uses
        addremovectrlnet() to build or remove the control bridge.
        If conf_reqd is False, the control network may be built even
        when the user has not configured one (e.g. for EMANE.)

        :param core.netns.nodes.CoreNode node: node to add or remove control interface
        :param int net_index: network index
        :param bool remove: flag to check if it should be removed
        :param bool conf_required: flag to check if conf is required
        :return: nothing
        """
        control_net = self.add_remove_control_net(net_index, remove,
                                                  conf_required)
        if not control_net:
            return

        if not node:
            return

        # ctrl# already exists
        if node.netif(control_net.CTRLIF_IDX_BASE + net_index):
            return

        control_ip = node.objid

        try:
            addrlist = [
                "%s/%s" % (control_net.prefix.addr(control_ip),
                           control_net.prefix.prefixlen)
            ]
        except ValueError:
            msg = "Control interface not added to node %s. " % node.objid
            msg += "Invalid control network prefix (%s). " % control_net.prefix
            msg += "A longer prefix length may be required for this many nodes."
            logger.exception(msg)
            return

        interface1 = node.newnetif(net=control_net,
                                   ifindex=control_net.CTRLIF_IDX_BASE +
                                   net_index,
                                   ifname="ctrl%d" % net_index,
                                   hwaddr=MacAddress.random(),
                                   addrlist=addrlist)
        node.netif(interface1).control = True

    def update_control_interface_hosts(self, net_index=0, remove=False):
        """
        Add the IP addresses of control interfaces to the /etc/hosts file.

        :param int net_index: network index to update
        :param bool remove: flag to check if it should be removed
        :return: nothing
        """
        if not self.options.get_config_bool("update_etc_hosts", default=False):
            return

        try:
            control_net = self.get_control_net_object(net_index)
        except KeyError:
            logger.exception("error retrieving control net object")
            return

        header = "CORE session %s host entries" % self.session_id
        if remove:
            logger.info("Removing /etc/hosts file entries.")
            utils.file_demunge("/etc/hosts", header)
            return

        entries = []
        for interface in control_net.netifs():
            name = interface.node.name
            for address in interface.addrlist:
                entries.append("%s %s" % (address.split("/")[0], name))

        logger.info("Adding %d /etc/hosts file entries." % len(entries))

        utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n")

    def runtime(self):
        """
        Return the current time we have been in the runtime state, or zero
        if not in runtime.
        """
        if self.state == EventTypes.RUNTIME_STATE.value:
            return time.time() - self._state_time
        else:
            return 0.0

    def add_event(self, event_time, node=None, name=None, data=None):
        """
        Add an event to the event queue, with a start time relative to the
        start of the runtime state.

        :param event_time: event time
        :param core.netns.nodes.CoreNode node: node to add event for
        :param str name: name of event
        :param data: data for event
        :return: nothing
        """
        event_time = float(event_time)
        current_time = self.runtime()

        if current_time > 0.0:
            if time <= current_time:
                logger.warn(
                    "could not schedule past event for time %s (run time is now %s)",
                    time, current_time)
                return
            event_time = event_time - current_time

        self.event_loop.add_event(event_time,
                                  self.run_event,
                                  node=node,
                                  name=name,
                                  data=data)

        if not name:
            name = ""
        logger.info("scheduled event %s at time %s data=%s", name,
                    event_time + current_time, data)

    # TODO: if data is None, this blows up, but this ties into how event functions are ran, need to clean that up
    def run_event(self, node_id=None, name=None, data=None):
        """
        Run a scheduled event, executing commands in the data string.

        :param int node_id: node id to run event
        :param str name: event name
        :param str data: event data
        :return: nothing
        """
        now = self.runtime()
        if not name:
            name = ""

        logger.info("running event %s at time %s cmd=%s" % (name, now, data))
        if not node_id:
            utils.mute_detach(data)
        else:
            node = self.get_object(node_id)
            node.cmd(data, wait=False)
예제 #3
0
파일: session.py 프로젝트: perhurt/core
class Session(object):

    # sessions that get automatically shutdown when the process
    # terminates normally
    __sessions = set()
    ''' CORE session manager.
    '''
    def __init__(self,
                 sessionid=None,
                 cfg={},
                 server=None,
                 persistent=False,
                 mkdir=True):
        if sessionid is None:
            # try to keep this short since it's used to construct
            # network interface names
            pid = os.getpid()
            sessionid = ((pid >> 16) ^ (pid & ((1 << 16) - 1)))
            sessionid ^= ((id(self) >> 16) ^ (id(self) & ((1 << 16) - 1)))
            sessionid &= 0xffff
        self.sessionid = sessionid
        self.sessiondir = os.path.join(tempfile.gettempdir(),
                                       "pycore.%s" % self.sessionid)
        if mkdir:
            os.mkdir(self.sessiondir)
        self.name = None
        self.filename = None
        self.thumbnail = None
        self.user = None
        self.node_count = None
        self._time = time.time()
        self.evq = EventLoop()
        # dict of objects: all nodes and nets
        self._objs = {}
        self._objslock = threading.Lock()
        # dict of configurable objects
        self._confobjs = {}
        self._confobjslock = threading.Lock()
        self._handlers = set()
        self._handlerslock = threading.Lock()
        self._state = None
        self._hooks = {}
        self._state_hooks = {}
        # dict of configuration items from /etc/core/core.conf config file
        self.cfg = cfg
        self.add_state_hook(coreapi.CORE_EVENT_RUNTIME_STATE,
                            self.runtime_state_hook)
        self.setstate(state=coreapi.CORE_EVENT_DEFINITION_STATE,
                      info=False,
                      sendevent=False)
        self.server = server
        if not persistent:
            self.addsession(self)
        self.master = False
        self.broker = CoreBroker(session=self, verbose=True)
        self.location = CoreLocation(self)
        self.mobility = MobilityManager(self)
        self.services = CoreServices(self)
        self.emane = emane.Emane(self)
        self.xen = xenconfig.XenConfigManager(self)
        self.sdt = Sdt(self)
        # future parameters set by the GUI may go here
        self.options = SessionConfig(self)
        self.metadata = SessionMetaData(self)

    @classmethod
    def addsession(cls, session):
        cls.__sessions.add(session)

    @classmethod
    def delsession(cls, session):
        try:
            cls.__sessions.remove(session)
        except KeyError:
            pass

    @classmethod
    def atexit(cls):
        while cls.__sessions:
            s = cls.__sessions.pop()
            print >> sys.stderr, "WARNING: automatically shutting down " \
                "non-persistent session %s" % s.sessionid
            s.shutdown()

    def shutdown(self):
        ''' Shut down all emulation objects and remove the session directory.
        '''
        if hasattr(self, 'emane'):
            self.emane.shutdown()
        if hasattr(self, 'broker'):
            self.broker.shutdown()
        if hasattr(self, 'sdt'):
            self.sdt.shutdown()
        self.delobjs()
        preserve = False
        if hasattr(self.options, 'preservedir'):
            if self.options.preservedir == '1':
                preserve = True
        if not preserve:
            shutil.rmtree(self.sessiondir, ignore_errors=True)
        if self.server:
            self.server.delsession(self)
        self.delsession(self)

    def isconnected(self):
        ''' Returns true if this session has a request handler.
        '''
        with self._handlerslock:
            if len(self._handlers) == 0:
                return False
            else:
                return True

    def connect(self, handler):
        ''' Set the request handler for this session, making it connected.
        '''
        # the master flag will only be set after a GUI has connected with the
        # handler, e.g. not during normal startup
        if handler.master is True:
            self.master = True
        with self._handlerslock:
            self._handlers.add(handler)

    def disconnect(self, handler):
        ''' Disconnect a request handler from this session. Shutdown this
            session if there is no running emulation.
        '''
        with self._handlerslock:
            try:
                self._handlers.remove(handler)
            except KeyError:
                raise ValueError, \
                    "Handler %s not associated with this session" % handler
            num_handlers = len(self._handlers)
        if num_handlers == 0:
            # shut down this session unless we are instantiating, running,
            # or collecting final data
            if self.getstate() < coreapi.CORE_EVENT_INSTANTIATION_STATE or \
                    self.getstate() > coreapi.CORE_EVENT_DATACOLLECT_STATE:
                self.shutdown()

    def broadcast(self, src, msg):
        ''' Send Node and Link CORE API messages to all handlers connected to this session.
        '''
        self._handlerslock.acquire()
        for handler in self._handlers:
            if handler == src:
                continue
            if isinstance(msg, coreapi.CoreNodeMessage) or \
                    isinstance(msg, coreapi.CoreLinkMessage):
                try:
                    handler.sendall(msg.rawmsg)
                except Exception, e:
                    self.warn("sendall() error: %s" % e)
        self._handlerslock.release()
예제 #4
0
파일: session.py 프로젝트: Benocs/core
class Session(object):

    # sessions that get automatically shutdown when the process
    # terminates normally
    __sessions = set()
    verbose = False

    ''' CORE session manager.
    '''
    def __init__(self, sessionid = None, cfg = {}, server = None,
                 persistent = False, mkdir = True):
        if sessionid is None:
            # try to keep this short since it's used to construct
            # network interface names
            pid = os.getpid()
            sessionid = ((pid >> 16) ^
                         (pid & ((1 << 16) - 1)))
            sessionid ^= ((id(self) >> 16) ^ (id(self) & ((1 << 16) - 1)))
            sessionid &= 0xffff
        self.sessionid = sessionid
        self.sessiondir = os.path.join(tempfile.gettempdir(),
                                       "pycore.%s" % self.sessionid)
        if mkdir:
            os.mkdir(self.sessiondir)
        self.name = None
        self.filename = None
        self.thumbnail = None
        self.user = None
        self.node_count = None
        self._time = time.time()
        self.evq = EventLoop()
        # dict of objects: all nodes and nets
        self._objs = {}
        self._objslock = threading.Lock()
        # dict of configurable objects
        self._confobjs = {}
        self._confobjslock = threading.Lock()
        self._handlers = set()
        self._handlerslock = threading.Lock()
        self._hooks = {}
        self.setstate(state=coreapi.CORE_EVENT_DEFINITION_STATE,
                      info=False, sendevent=False)
        # dict of configuration items from /etc/core/core.conf config file
        self.cfg = cfg
        self.server = server
        if not persistent:
            self.addsession(self)
        self.master = False
        self.broker = CoreBroker(session=self, verbose=self.verbose)
        self.location = CoreLocation(self)
        self.mobility = MobilityManager(self)
        self.netidmanager = NetIDSubnetMapManager(self)
        self.services = CoreServices(self)
        self.emane = emane.Emane(self)
        self.xen = xenconfig.XenConfigManager(self)
        self.sdt = Sdt(self)
        # future parameters set by the GUI may go here
        self.options = SessionConfig(self)
        self.metadata = SessionMetaData(self)

    @classmethod
    def addsession(cls, session):
        cls.__sessions.add(session)

    @classmethod
    def delsession(cls, session):
        try:
            cls.__sessions.remove(session)
        except KeyError:
            pass

    @classmethod
    def atexit(cls):
        while cls.__sessions:
            s = cls.__sessions.pop()
            if cls.verbose:
                print(("WARNING: automatically shutting down " \
                    "non-persistent session %s" % s.sessionid), file = sys.stderr)
            s.shutdown()

    def __del__(self):
        # note: there is no guarantee this will ever run
        self.shutdown()

    def shutdown(self):
        ''' Shut down all emulation objects and remove the session directory.
        '''
        self.emane.shutdown()
        self.broker.shutdown()
        self.sdt.shutdown()
        self.delobjs()
        self.netidmanager.clear()
        preserve = False
        if hasattr(self.options, 'preservedir'):
            if self.options.preservedir == '1':
                preserve = True
        if not preserve:
            shutil.rmtree(self.sessiondir, ignore_errors = True)
        if self.server:
            self.server.delsession(self)
        self.delsession(self)

    def isconnected(self):
        ''' Returns true if this session has a request handler.
        '''
        with self._handlerslock:
            if len(self._handlers) == 0:
                return False
            else:
                return True

    def connect(self, handler):
        ''' Set the request handler for this session, making it connected.
        '''
        # the master flag will only be set after a GUI has connected with the
        # handler, e.g. not during normal startup
        if handler.master is True:
            self.master = True
        with self._handlerslock:
            self._handlers.add(handler)

    def disconnect(self, handler):
        ''' Disconnect a request handler from this session. Shutdown this
            session if there is no running emulation.
        '''
        with self._handlerslock:
            try:
                self._handlers.remove(handler)
            except KeyError:
                raise ValueError("Handler %s not associated with this session" % handler)
            num_handlers = len(self._handlers)
        if num_handlers == 0:
            # shut down this session unless we are instantiating, running,
            # or collecting final data
            if self.getstate() < coreapi.CORE_EVENT_INSTANTIATION_STATE or \
                    self.getstate() > coreapi.CORE_EVENT_DATACOLLECT_STATE:
                self.shutdown()

    def broadcast(self, src, msg):
        ''' Send Node and Link CORE API messages to all handlers connected to this session.
        '''
        self._handlerslock.acquire()
        for handler in self._handlers:
            if handler == src:
                continue
            if isinstance(msg, coreapi.CoreNodeMessage) or \
                    isinstance(msg, coreapi.CoreLinkMessage):
                try:
                    handler.sendall(msg.rawmsg)
                except Exception as e:
                    self.warn("sendall() error: %s" % e)
        self._handlerslock.release()

    def broadcastraw(self, src, data):
        ''' Broadcast raw data to all handlers except src.
        '''
        self._handlerslock.acquire()
        for handler in self._handlers:
            if handler == src:
                continue
            try:
                handler.sendall(data)
            except Exception as e:
                self.warn("sendall() error: %s" % e)
        self._handlerslock.release()
        
    def gethandler(self):
        ''' Get one of the connected handlers, preferrably the master.
        '''
        with self._handlerslock:
            if len(self._handlers) == 0:
                return None
            for handler in self._handlers:
                if handler.master:
                    return handler
            for handler in self._handlers:
                return handler

    def setstate(self, state, info = False, sendevent = False, 
                 returnevent = False):
        ''' Set the session state. When info is true, log the state change
            event using the session handler's info method. When sendevent is
            true, generate a CORE API Event Message and send to the connected
            entity.
        '''
        self._time = time.time()
        self._state = state
        replies = []

        if self.isconnected() and info:
            statename = coreapi.state_name(state)
            with self._handlerslock:
                for handler in self._handlers:
                    handler.info("SESSION %s STATE %d: %s at %s" % \
                                (self.sessionid, state, statename,
                                 time.ctime()))
        self.writestate(state)
        self.runhook(state)
        if self.isconnected() and sendevent:
            tlvdata = b""
            tlvdata += coreapi.CoreEventTlv.pack(coreapi.CORE_TLV_EVENT_TYPE,
                                                 state)
            msg = coreapi.CoreEventMessage.pack(0, tlvdata)
            # send Event Message to connected handlers (e.g. GUI)
            try:
                if returnevent:
                    replies.append(msg)
                else:
                    self.broadcastraw(None, msg)
            except Exception as e:
                self.warn("Error sending Event Message: %s" % e)
            # also inform slave servers
            tmp = self.broker.handlerawmsg(msg)
        return replies


    def getstate(self):
        ''' Retrieve the current state of the session.
        '''
        return self._state
        
    def writestate(self, state):
        ''' Write the current state to a state file in the session dir.
        '''
        try:
            f = open(os.path.join(self.sessiondir, "state"), "w")
            f.write("%d %s\n" % (state, coreapi.state_name(state)))
            f.close()
        except Exception as e:
            self.warn("Error writing state file: %s" % e)

    def runhook(self, state, hooks=None):
        ''' Run hook scripts upon changing states.
        If hooks is not specified, run all hooks in the given state.
        '''
        if state not in self._hooks:
            return
        if hooks is None:
            hooks = self._hooks[state]
        for (filename, data) in hooks:
            try:
                f = open(os.path.join(self.sessiondir, filename), "w")
                f.write(data)
                f.close()
            except Exception as e:
                self.warn("Error writing hook '%s': %s" % (filename, e))
            self.info("Running hook %s for state %s" % (filename, state))
            try:
                check_call(["/bin/sh", filename], cwd=self.sessiondir,
                           env=self.getenviron())
            except Exception as e:
                self.warn("Error running hook '%s' for state %s: %s" % 
                          (filename, state, e))
            
    def sethook(self, type, filename, srcname, data):
        ''' Store a hook from a received File Message.
        '''
        if srcname is not None:
            raise NotImplementedError
        (hookid, state) = type.split(':')[:2]
        if not state.isdigit():
            self.warn("Error setting hook having state '%s'" % state)
            return
        state = int(state)
        hook = (filename, data)
        if state not in self._hooks:
            self._hooks[state] = [hook,]
        else:
            self._hooks[state] += hook
        # immediately run a hook if it is in the current state
        # (this allows hooks in the definition and configuration states)
        if self.getstate() == state:
            self.runhook(state, hooks = [hook,])
    
    def delhooks(self):
        ''' Clear the hook scripts dict.
        '''
        self._hooks = {}
        
    def getenviron(self, state=True):
        ''' Get an environment suitable for a subprocess.Popen call.
            This is the current process environment with some session-specific
            variables.
        '''
        env = os.environ.copy()
        env['SESSION'] = "%s" % self.sessionid
        env['SESSION_DIR'] = "%s" % self.sessiondir
        env['SESSION_NAME'] = "%s" % self.name
        env['SESSION_FILENAME'] = "%s" % self.filename
        env['SESSION_USER'] = "******" % self.user
        env['SESSION_NODE_COUNT'] = "%s" % self.node_count
        if state:
            env['SESSION_STATE'] = "%s" % self.getstate()
        try:
            readfileintodict(os.path.join(CORE_CONF_DIR, "environment"), env)
        except IOError:
            pass
        if self.user:
            try:
                readfileintodict(os.path.join('/home', self.user, ".core",
                                 "environment"), env)
            except IOError:
                pass
        return env

    def setthumbnail(self, thumbfile):
        ''' Set the thumbnail filename. Move files from /tmp to session dir.
        '''
        if not os.path.exists(thumbfile):
            self.thumbnail = None
            return
        dstfile = os.path.join(self.sessiondir, os.path.basename(thumbfile))
        shutil.move(thumbfile, dstfile)
        #print "thumbnail: %s -> %s" % (thumbfile, dstfile)
        self.thumbnail = dstfile

    def setuser(self, user):
        ''' Set the username for this session. Update the permissions of the
            session dir to allow the user write access.
        '''
        if user is not None:
            try:
                uid = pwd.getpwnam(user).pw_uid
                gid = os.stat(self.sessiondir).st_gid
                os.chown(self.sessiondir, uid, gid)
            except Exception as e:
                self.warn("Failed to set permission on %s: %s" % (self.sessiondir, e)) 
        self.user = user

    def objs(self):
        ''' Return iterator over the emulation object dictionary.
        '''
        return iter(list(self._objs.values()))
        
    def getobjid(self):
        ''' Return a unique, random object id.
        '''
        self._objslock.acquire()
        while True:
            id = random.randint(1, 0xFFFF)
            if id not in self._objs:
                break
        self._objslock.release()
        return id

    def addobj(self, cls, *clsargs, **clskwds):
        ''' Add an emulation object.
        '''
        obj = cls(self, *clsargs, **clskwds)
        self._objslock.acquire()
        if obj.objid in self._objs:
            self._objslock.release()
            obj.shutdown()
            raise KeyError("non-unique object id %s for %s" % (obj.objid, obj))
        self._objs[obj.objid] = obj
        self._objslock.release()
        return obj

    def obj(self, objid):
        ''' Get an emulation object.
        '''
        if objid not in self._objs:
            raise KeyError("unknown object id %s" % (objid))
        return self._objs[objid]
        
    def objbyname(self, name):
        ''' Get an emulation object using its name attribute.
        '''
        with self._objslock:
            for obj in self.objs():
                if hasattr(obj, "name") and obj.name == name:
                    return obj
        raise KeyError("unknown object with name %s" % (name))

    def delobj(self, objid):
        ''' Remove an emulation object.
        '''
        self._objslock.acquire()
        try:
            o = self._objs.pop(objid)
        except KeyError:
            o = None
        self._objslock.release()
        if o:
            o.shutdown()
            del o
        gc.collect()
#         print "gc count:", gc.get_count()
#         for o in gc.get_objects():
#             if isinstance(o, PyCoreObj):
#                 print "XXX XXX XXX PyCoreObj:", o
#                 for r in gc.get_referrers(o):
#                     print "XXX XXX XXX referrer:", gc.get_referrers(o)

    def delobjs(self):
        ''' Clear the _objs dictionary, and call each obj.shutdown() routine.
        '''
        self._objslock.acquire()
        while self._objs:
            k, o = self._objs.popitem()
            o.shutdown()
        self._objslock.release()
        
    def writeobjs(self):
        ''' Write objects to a 'nodes' file in the session dir.
            The 'nodes' file lists:
                number, name, api-type, class-type
        '''
        try:
            f = open(os.path.join(self.sessiondir, "nodes"), "w")
            with self._objslock:
                for objid in sorted(self._objs.keys()):
                    o = self._objs[objid]
                    f.write("%s %s %s %s\n" % (objid, o.name, o.apitype, type(o)))
            f.close()
        except Exception as e:
            self.warn("Error writing nodes file: %s" % e)

    def addconfobj(self, objname, type, callback):
        ''' Objects can register configuration objects that are included in
            the Register Message and may be configured via the Configure
            Message. The callback is invoked when receiving a Configure Message.
        '''
        if type not in coreapi.reg_tlvs:
            raise Exception("invalid configuration object type")
        self._confobjslock.acquire()
        self._confobjs[objname] = (type, callback)
        self._confobjslock.release()

    def confobj(self, objname, session, msg):
        ''' Invoke the callback for an object upon receipt of a Configure
            Message for that object. A no-op if the object doesn't exist.
        '''
        replies = []
        self._confobjslock.acquire()
        if objname == "all":
            for objname in self._confobjs:
                (type, callback) = self._confobjs[objname]
                reply = callback(session, msg)
                if reply is not None:
                    replies.append(reply)
            self._confobjslock.release()
            return replies
        if objname in self._confobjs:
            (type, callback) = self._confobjs[objname]
            self._confobjslock.release()
            reply = callback(session, msg)
            if reply is not None:
                replies.append(reply)
            return replies
        else:
            self.info("session object doesn't own model '%s', ignoring" % \
                      objname)
        self._confobjslock.release()
        return replies

    def confobjs_to_tlvs(self):
        ''' Turn the configuration objects into a list of Register Message TLVs.
        '''
        tlvdata = b""
        self._confobjslock.acquire()
        for objname in self._confobjs:
            (type, callback) = self._confobjs[objname]
            # type must be in coreapi.reg_tlvs
            tlvdata += coreapi.CoreRegTlv.pack(type, objname)
        self._confobjslock.release()
        return tlvdata

    def info(self, msg):
        ''' Utility method for writing output to stdout.
        '''
        if hasattr(self.options, 'clientlogfile'):
            fname = self.options.clientlogfile
            with open(fname, 'a') as logfile:
               print(msg, file = logfile, flush = True)
        else:
            print(msg, file = sys.stdout, flush = True)
            sys.stdout.flush()

    def warn(self, msg):
        ''' Utility method for writing output to stderr.
        '''
        if hasattr(self.options, 'clientlogfile'):
            fname = self.options.clientlogfile
            with open(fname, 'a') as logfile:
               print(msg, file = logfile, flush = True)
        else:
            print(msg, file = sys.stderr, flush = True)
            sys.stderr.flush()

    def dumpsession(self):
        ''' Debug print this session.
        '''
        self.info("session id=%s name=%s state=%s connected=%s" % \
                  (self.sessionid, self.name, self._state, self.isconnected()))
        num = len(self._objs)
        self.info("        file=%s thumb=%s nc=%s/%s" % \
                  (self.filename, self.thumbnail, self.node_count, num))
                  
    def exception(self, level, source, objid, text):
        ''' Generate an Exception Message 
        '''
        vals = (objid, str(self.sessionid), level, source, time.ctime(), text)
        types = ("NODE", "SESSION", "LEVEL", "SOURCE", "DATE", "TEXT")
        tlvdata = b''
        for (t,v) in zip(types, vals):
            if v is not None:                
                tlvdata += coreapi.CoreExceptionTlv.pack(
                                    eval("coreapi.CORE_TLV_EXCP_%s" % t), v)
        msg = coreapi.CoreExceptionMessage.pack(0, tlvdata)
        self.warn("exception: %s (%s) %s" % (source, objid, text))
        # send Exception Message to connected handlers (e.g. GUI)
        self.broadcastraw(None, msg)

    def getcfgitem(self, cfgname):
        ''' Return an entry from the configuration dictionary that comes from
            command-line arguments and/or the core.conf config file.
        '''
        if cfgname not in self.cfg:
            return None
        else:
            return self.cfg[cfgname]

    def getcfgitembool(self, cfgname, defaultifnone = None):
        ''' Return a boolean entry from the configuration dictionary, may
            return None if undefined.
        '''
        item = self.getcfgitem(cfgname)
        if item is None:
            return defaultifnone
        return bool(item.lower() == "true")

    def getcfgitemint(self, cfgname, defaultifnone = None):
        ''' Return an integer entry from the configuration dictionary, may
            return None if undefined.
        '''
        item = self.getcfgitem(cfgname)
        if item is None:
            return defaultifnone
        return int(item)

    def instantiate(self, handler=None):
        ''' We have entered the instantiation state, invoke startup methods
            of various managers and boot the nodes. Validate nodes and check
            for transition to the runtime state.
        '''
        self.writeobjs()
        # controlnet may be needed by some EMANE models
        self.addremovectrlif(node=None, remove=False)
        if self.emane.startup() == self.emane.NOT_READY:
            return # instantiate() will be invoked again upon Emane.configure()
        self.broker.startup()
        self.mobility.startup()
        # boot the services on each node
        self.bootnodes(handler)
        # allow time for processes to start
        time.sleep(0.125)
        self.validatenodes()
        self.emane.poststartup()
        # assume either all nodes have booted already, or there are some
        # nodes on slave servers that will be booted and those servers will
        # send a node status response message
        self.checkruntime()

    def getnodecount(self):
        ''' Returns the number of CoreNodes and CoreNets, except for those
        that are not considered in the GUI's node count.
        '''

        with self._objslock:
            count = len([x for x in self.objs() if not isinstance(x, (nodes.PtpNet, nodes.CtrlNet))])
            # on Linux, GreTapBridges are auto-created, not part of
            # GUI's node count
            if 'GreTapBridge' in globals():
                count -= len([x for x in self.objs() if isinstance(x, GreTapBridge) and not \
                                    isinstance(x, nodes.TunnelNode)])
        return count

    def checkruntime(self):
        ''' Check if we have entered the runtime state, that all nodes have been
            started and the emulation is running. Start the event loop once we
            have entered runtime (time=0).
        '''
        # this is called from instantiate() after receiving an event message
        # for the instantiation state, and from the broker when distributed
        # nodes have been started
        if self.node_count is None:
            return
        if self.getstate() == coreapi.CORE_EVENT_RUNTIME_STATE:
            return
        session_node_count = int(self.node_count)
        nc = self.getnodecount()
        # count booted nodes not emulated on this server
        # TODO: let slave server determine RUNTIME and wait for Event Message
        # broker.getbootocunt() counts all CoreNodes from status reponse 
        #  messages, plus any remote WLANs; remote EMANE, hub, switch, etc.
        #  are already counted in self._objs
        nc += self.broker.getbootcount()
        self.info("Checking for runtime with %d of %d session nodes" % \
                    (nc, session_node_count))
        if nc < session_node_count:
            return # do not have information on all nodes yet
        # information on all nodes has been received and they have been started
        # enter the runtime state
        # TODO: more sophisticated checks to verify that all nodes and networks
        #       are running
        state = coreapi.CORE_EVENT_RUNTIME_STATE
        self.evq.run()
        self.setstate(state, info=True, sendevent=True)

    def datacollect(self):
        ''' Tear down a running session. Stop the event loop and any running
            nodes, and perform clean-up.
        '''
        self.evq.stop()
        with self._objslock:
            for obj in self.objs():
                if isinstance(obj, nodes.PyCoreNode):
                    self.services.stopnodeservices(obj)
        self.emane.shutdown()
        self.updatectrlifhosts(remove=True)
        self.addremovectrlif(node=None, remove=True)
        # self.checkshutdown() is currently invoked from node delete handler
    
    def checkshutdown(self):
        ''' Check if we have entered the shutdown state, when no running nodes
            and links remain.
        '''
        with self._objslock:
            nc = len(self._objs)
        # TODO: this doesn't consider slave server node counts
        # wait for slave servers to enter SHUTDOWN state, then master session
        # can enter SHUTDOWN
        replies = ()
        if nc == 0:
            replies = self.setstate(state=coreapi.CORE_EVENT_SHUTDOWN_STATE, 
                                    info=True, sendevent=True, returnevent=True)
            self.sdt.shutdown()
        return replies

    def setmaster(self, handler):
        ''' Look for the specified handler and set our master flag
            appropriately. Returns True if we are connected to the given
            handler.
        '''
        with self._handlerslock:
            for h in self._handlers:
                if h != handler:
                    continue
                self.master = h.master
                return True
        return False
    
    def shortsessionid(self):
        ''' Return a shorter version of the session ID, appropriate for
            interface names, where length may be limited.
        '''
        return (self.sessionid >> 8) ^ (self.sessionid & ((1 << 8) - 1))
        
    def bootnodes(self, handler):
        ''' Invoke the boot() procedure for all nodes and send back node 
            messages to the GUI for node messages that had the status
            request flag.
        '''
        #self.addremovectrlif(node=None, remove=False)
        with self._objslock:
            for n in self.objs():
                if not isinstance(n, nodes.PyCoreNode):
                    continue
                if isinstance(n, nodes.RJ45Node):
                    continue
                # add a control interface if configured
                self.addremovectrlif(node=n, remove=False)
                n.boot()
                nodenum = n.objid
                if handler is None:
                    continue
                if nodenum in handler.nodestatusreq:
                    tlvdata = b""
                    tlvdata += coreapi.CoreNodeTlv.pack(coreapi.CORE_TLV_NODE_NUMBER,
                                                        nodenum)
                    tlvdata += coreapi.CoreNodeTlv.pack(coreapi.CORE_TLV_NODE_EMUID,
                                                        n.objid)
                    reply = coreapi.CoreNodeMessage.pack(coreapi.CORE_API_ADD_FLAG \
                                                       | coreapi.CORE_API_LOC_FLAG,
                                                         tlvdata)
                    try:
                        handler.request.sendall(reply)
                    except Exception as e:
                        self.warn("sendall() error: %s" % e)
                    del handler.nodestatusreq[nodenum]
        self.updatectrlifhosts()
    
    
    def validatenodes(self):
        with self._objslock:
            for n in self.objs():
                # TODO: this can be extended to validate everything
                # such as vnoded process, bridges, etc.
                if not isinstance(n, nodes.PyCoreNode):
                    continue
                if isinstance(n, nodes.RJ45Node):
                    continue
                n.validate()
                
    def addremovectrlnet(self, remove=False):
        ''' Create a control network bridge as necessary. 
        When the remove flag is True, remove the bridge that connects control
        interfaces.
        '''
        prefix = None
        try:
            if self.cfg['controlnet']:
                prefix = self.cfg['controlnet']
        except KeyError:
            pass
        if hasattr(self.options, 'controlnet'):
            prefix = self.options.controlnet
        if not prefix:
            return None # no controlnet needed
            
        # return any existing controlnet bridge
        id = "ctrlnet"
        try:
            ctrlnet = self.obj(id)
            if remove:
                self.delobj(ctrlnet.objid)
                return None
            return ctrlnet
        except KeyError:
            if remove:
                return None

        # build a new controlnet bridge
        updown_script = None
        try:
            if self.cfg['controlnet_updown_script']:
                updown_script = self.cfg['controlnet_updown_script']
        except KeyError:
            pass
            
        prefixes = prefix.split()
        if len(prefixes) > 1:
            assign_address = True
            if self.master:
                try:
                    prefix = prefixes[0].split(':', 1)[1]
                except IndexError:
                    prefix = prefixes[0] # possibly only one server
            else:
                # slave servers have their name and localhost in the serverlist
                servers = self.broker.getserverlist()
                servers.remove('localhost')
                prefix = None
                for server_prefix in prefixes:
                    server, p = server_prefix.split(':')
                    if server == servers[0]:
                        prefix = p
                        break
                if not prefix:
                    msg = "Control network prefix not found for server '%s'" % \
                            servers[0]
                    self.exception(coreapi.CORE_EXCP_LEVEL_ERROR,
                                   "Session.addremovectrlnet()", None, msg)
                    prefix = prefixes[0].split(':', 1)[1]
                    assign_address = False
        else:
            # with one prefix, only master gets a ctrlnet address
            assign_address = self.master
        ctrlnet = self.addobj(cls=nodes.CtrlNet, objid=id, prefix=prefix,
                              assign_address=assign_address,
                              updown_script=updown_script)
        # tunnels between controlnets will be built with Broker.addnettunnels()
        self.broker.addnet(id)
        for server in self.broker.getserverlist():
            self.broker.addnodemap(server, id)
        return ctrlnet

    def addremovectrlif(self, node, remove=False):
        ''' Add a control interface to a node when a 'controlnet' prefix is
            listed in the config file or session options. Uses
            addremovectrlnet() to build or remove the control bridge.
        '''
        ctrlnet = self.addremovectrlnet(remove)
        if ctrlnet is None:
            return
        if node is None:
            return
        ctrlip = node.objid
        try:
            addrlist = ["%s/%s" % (ctrlnet.prefix.addr(ctrlip),
                                   ctrlnet.prefix.prefixlen)]
        except ValueError:
            msg = "Control interface not added to node %s. " % node.objid
            msg += "Invalid control network prefix (%s). " % ctrlnet.prefix
            msg += "A longer prefix length may be required for this many nodes."
            node.exception(coreapi.CORE_EXCP_LEVEL_ERROR,
                           "Session.addremovectrlif()", msg)
            return
        ifi = node.newnetif(net = ctrlnet, ifindex = ctrlnet.CTRLIF_IDX_BASE,
                            ifname = "ctrl0", hwaddr = MacAddr.random(),
                            addrlist = addrlist)
        node.netif(ifi).control = True

    def updatectrlifhosts(self, remove=False):
        ''' Add the IP addresses of control interfaces to the /etc/hosts file.
        '''
        if not self.getcfgitembool('update_etc_hosts', False):
            return
        id = "ctrlnet"
        try:
            ctrlnet = self.obj(id)
        except KeyError:
            return
        header = "CORE session %s host entries" % self.sessionid
        if remove:
            if self.getcfgitembool('verbose', False):
                self.info("Removing /etc/hosts file entries.")
            filedemunge('/etc/hosts', header)
            return
        entries = []
        for ifc in ctrlnet.netifs():
            name = ifc.node.name
            for addr in ifc.addrlist:
                entries.append("%s %s" % (addr.split('/')[0], ifc.node.name))
        if self.getcfgitembool('verbose', False):
            self.info("Adding %d /etc/hosts file entries." % len(entries))
        filemunge('/etc/hosts', header, '\n'.join(entries) + '\n')

    def runtime(self):
        ''' Return the current time we have been in the runtime state, or zero
            if not in runtime.
        '''
        if self.getstate() == coreapi.CORE_EVENT_RUNTIME_STATE:
            return time.time() - self._time
        else:
            return 0.0
        
    def addevent(self, etime, node=None, name=None, data=None):
        ''' Add an event to the event queue, with a start time relative to the
            start of the runtime state.
        '''
        etime = float(etime)
        runtime = self.runtime()
        if runtime > 0.0:
            if time <= runtime:
                self.warn("Could not schedule past event for time %s " \
                          "(run time is now %s)" % (time, runtime))
                return
            etime = etime - runtime
        func = self.runevent
        self.evq.add_event(etime, func, node=node, name=name, data=data)
        if name is None:
            name = ""
        self.info("scheduled event %s at time %s data=%s" % \
                  (name, etime + runtime, data))

    def runevent(self, node=None, name=None, data=None):
        ''' Run a scheduled event, executing commands in the data string.
        '''
        now = self.runtime()
        if name is None:
            name = ""
        self.info("running event %s at time %s cmd=%s" % (name, now, data))
        if node is None:
            mutedetach(shlex.split(data))
        else:
            n = self.obj(node)
            n.cmd(shlex.split(data), wait=False)
            
    def sendobjs(self):
        ''' Return API messages that describe the current session.
        '''
        replies = []
        nn = 0
        ni = 0
        # send NetIDSubnetMap
        msgs = self.netidmanager.toconfmsgs(flags=0, nodenum=-1,
                typeflags=coreapi.CONF_TYPE_FLAGS_UPDATE)
        replies.extend(msgs)

        # send node messages for node and network objects
        with self._objslock:
            for obj in self.objs():
                created_nodemsg = False
                msg = obj.tonodemsg(flags = coreapi.CORE_API_ADD_FLAG)
                if msg is not None:
                    created_nodemsg = True
                    replies.append(msg)
                    nn += 1
                # send interface messages from interface objects
                # if obj has tonodemsg(), the it's a node and thus contains
                # interfaces. we will now iterate over those interface and push
                # one API message each.
                if created_nodemsg:
                    for ifindex, interface in list(obj._netif.items()):
                        msg = interface.tointerfacemsg(flags = coreapi.CORE_API_ADD_FLAG)
                        if msg is not None:
                            replies.append(msg)
                            ni += 1


        nl = 0
        # send link messages from net objects
        with self._objslock:
            for obj in self.objs():
                linkmsgs = obj.tolinkmsgs(flags = coreapi.CORE_API_ADD_FLAG)
                for msg in linkmsgs:
                    replies.append(msg)
                    nl += 1

        # send model info
        configs = self.mobility.getallconfigs()
        configs += self.emane.getallconfigs()
        for (nodenum, cls, values) in configs:
            #cls = self.mobility._modelclsmap[conftype]
            msg = cls.toconfmsg(flags=0, nodenum=nodenum,
                                typeflags=coreapi.CONF_TYPE_FLAGS_UPDATE,
                                values=values)
            replies.append(msg)
        # service customizations
        svc_configs = self.services.getallconfigs()
        for (nodenum, svc) in svc_configs:
            opaque = "service:%s" % svc._name
            tlvdata = b""
            tlvdata += coreapi.CoreConfTlv.pack(coreapi.CORE_TLV_CONF_NODE,
                                                nodenum)
            tlvdata += coreapi.CoreConfTlv.pack(coreapi.CORE_TLV_CONF_OPAQUE,
                                                opaque)
            tmp = coreapi.CoreConfMessage(flags=0, hdr=b"", data=tlvdata)
            replies.append(self.services.configure_request(tmp))
            for (filename, data) in self.services.getallfiles(svc):
                flags = coreapi.CORE_API_ADD_FLAG
                tlvdata = coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_NODE,
                                                   nodenum)
                tlvdata += coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_NAME,
                                                    str(filename))
                tlvdata += coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_TYPE,
                                                    opaque)
                tlvdata += coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_DATA,
                                                    str(data))
                replies.append(coreapi.CoreFileMessage.pack(flags, tlvdata))

        # TODO: send location info
        # replies.append(self.location.toconfmsg())
        # send hook scripts
        for state in sorted(self._hooks.keys()):
            for (filename, data) in self._hooks[state]:
                flags = coreapi.CORE_API_ADD_FLAG
                tlvdata = coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_NAME,
                                                    str(filename))
                tlvdata += coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_TYPE,
                                                    "hook:%s" % state)
                tlvdata += coreapi.CoreFileTlv.pack(coreapi.CORE_TLV_FILE_DATA,
                                                    str(data))
                replies.append(coreapi.CoreFileMessage.pack(flags, tlvdata))

        # send meta data
        tmp = coreapi.CoreConfMessage(flags=0, hdr=b"", data=b"")
        opts = self.options.configure_request(tmp,
                                    typeflags = coreapi.CONF_TYPE_FLAGS_UPDATE)
        if opts:
            replies.append(opts)
        meta = self.metadata.configure_request(tmp,
                                    typeflags = coreapi.CONF_TYPE_FLAGS_UPDATE)
        if meta:
            replies.append(meta)
        
        self.info("informing GUI about %d nodes, %d interfaces and %d links" % (nn, ni, nl))
        return replies
예제 #5
0
파일: session.py 프로젝트: gsomlo/core
class Session(object):
    """
    CORE session manager.
    """

    def __init__(self, session_id, config=None, mkdir=True):
        """
        Create a Session instance.

        :param int session_id: session id
        :param dict config: session configuration
        :param bool mkdir: flag to determine if a directory should be made
        """
        self.session_id = session_id

        # define and create session directory when desired
        self.session_dir = os.path.join(tempfile.gettempdir(), "pycore.%s" % self.session_id)
        if mkdir:
            os.mkdir(self.session_dir)

        self.name = None
        self.file_name = None
        self.thumbnail = None
        self.user = None
        self.event_loop = EventLoop()

        # dict of objects: all nodes and nets
        self.objects = {}
        self._objects_lock = threading.Lock()

        # TODO: should the default state be definition?
        self.state = EventTypes.NONE.value
        self._state_time = time.time()
        self._state_file = os.path.join(self.session_dir, "state")

        self._hooks = {}
        self._state_hooks = {}

        self.add_state_hook(state=EventTypes.RUNTIME_STATE.value, hook=self.runtime_state_hook)

        self.master = False

        # handlers for broadcasting information
        self.event_handlers = []
        self.exception_handlers = []
        self.node_handlers = []
        self.link_handlers = []
        self.file_handlers = []
        self.config_handlers = []
        self.shutdown_handlers = []

        # session options/metadata
        self.options = SessionConfig()
        if not config:
            config = {}
        for key, value in config.iteritems():
            self.options.set_config(key, value)
        self.metadata = SessionMetaData()

        # initialize session feature helpers
        self.broker = CoreBroker(session=self)
        self.location = CoreLocation()
        self.mobility = MobilityManager(session=self)
        self.services = CoreServices(session=self)
        self.emane = EmaneManager(session=self)
        self.sdt = Sdt(session=self)

    def shutdown(self):
        """
        Shutdown all emulation objects and remove the session directory.
        """
        # shutdown/cleanup feature helpers
        self.emane.shutdown()
        self.broker.shutdown()
        self.sdt.shutdown()

        # delete all current objects
        self.delete_objects()

        # remove this sessions working directory
        preserve = self.options.get_config("preservedir") == "1"
        if not preserve:
            shutil.rmtree(self.session_dir, ignore_errors=True)

        # call session shutdown handlers
        for handler in self.shutdown_handlers:
            handler(self)

    def broadcast_event(self, event_data):
        """
        Handle event data that should be provided to event handler.

        :param core.data.EventData event_data: event data to send out
        :return: nothing
        """

        for handler in self.event_handlers:
            handler(event_data)

    def broadcast_exception(self, exception_data):
        """
        Handle exception data that should be provided to exception handlers.

        :param core.data.ExceptionData exception_data: exception data to send out
        :return: nothing
        """

        for handler in self.exception_handlers:
            handler(exception_data)

    def broadcast_node(self, node_data):
        """
        Handle node data that should be provided to node handlers.

        :param core.data.ExceptionData node_data: node data to send out
        :return: nothing
        """

        for handler in self.node_handlers:
            handler(node_data)

    def broadcast_file(self, file_data):
        """
        Handle file data that should be provided to file handlers.

        :param core.data.FileData file_data: file data to send out
        :return: nothing
        """

        for handler in self.file_handlers:
            handler(file_data)

    def broadcast_config(self, config_data):
        """
        Handle config data that should be provided to config handlers.

        :param core.data.ConfigData config_data: config data to send out
        :return: nothing
        """

        for handler in self.config_handlers:
            handler(config_data)

    def broadcast_link(self, link_data):
        """
        Handle link data that should be provided to link handlers.

        :param core.data.ExceptionData link_data: link data to send out
        :return: nothing
        """

        for handler in self.link_handlers:
            handler(link_data)

    def set_state(self, state, send_event=False):
        """
        Set the session's current state.

        :param core.enumerations.EventTypes state: state to set to
        :param send_event: if true, generate core API event messages
        :return: nothing
        """
        state_value = state.value
        state_name = state.name

        if self.state == state_value:
            logger.info("session(%s) is already in state: %s, skipping change", self.session_id, state_name)
            return

        self.state = state_value
        self._state_time = time.time()
        logger.info("changing session(%s) to state %s", self.session_id, state_name)

        self.write_state(state_value)
        self.run_hooks(state_value)
        self.run_state_hooks(state_value)

        if send_event:
            event_data = EventData(event_type=state_value, time="%s" % time.time())
            self.broadcast_event(event_data)

    def write_state(self, state):
        """
        Write the current state to a state file in the session dir.

        :param int state: state to write to file
        :return: nothing
        """
        try:
            state_file = open(self._state_file, "w")
            state_file.write("%d %s\n" % (state, coreapi.state_name(state)))
            state_file.close()
        except IOError:
            logger.exception("error writing state file: %s", state)

    def run_hooks(self, state):
        """
        Run hook scripts upon changing states. If hooks is not specified, run all hooks in the given state.

        :param int state: state to run hooks for
        :return: nothing
        """

        # check that state change hooks exist
        if state not in self._hooks:
            return

        # retrieve all state hooks
        hooks = self._hooks.get(state, [])

        # execute all state hooks
        for hook in hooks:
            self.run_hook(hook)
        else:
            logger.info("no state hooks for %s", state)

    def set_hook(self, hook_type, file_name, source_name, data):
        """
        Store a hook from a received file message.

        :param str hook_type: hook type
        :param str file_name: file name for hook
        :param str source_name: source name
        :param data: hook data
        :return: nothing
        """
        logger.info("setting state hook: %s - %s from %s", hook_type, file_name, source_name)

        hook_id, state = hook_type.split(':')[:2]
        if not state.isdigit():
            logger.error("error setting hook having state '%s'", state)
            return

        state = int(state)
        hook = file_name, data

        # append hook to current state hooks
        state_hooks = self._hooks.setdefault(state, [])
        state_hooks.append(hook)

        # immediately run a hook if it is in the current state
        # (this allows hooks in the definition and configuration states)
        if self.state == state:
            logger.info("immediately running new state hook")
            self.run_hook(hook)

    def del_hooks(self):
        """
        Clear the hook scripts dict.
        """
        self._hooks.clear()

    def run_hook(self, hook):
        """
        Run a hook.

        :param tuple hook: hook to run
        :return: nothing
        """
        file_name, data = hook
        logger.info("running hook %s", file_name)

        # write data to hook file
        try:
            hook_file = open(os.path.join(self.session_dir, file_name), "w")
            hook_file.write(data)
            hook_file.close()
        except IOError:
            logger.exception("error writing hook '%s'", file_name)

        # setup hook stdout and stderr
        try:
            stdout = open(os.path.join(self.session_dir, file_name + ".log"), "w")
            stderr = subprocess.STDOUT
        except IOError:
            logger.exception("error setting up hook stderr and stdout")
            stdout = None
            stderr = None

        # execute hook file
        try:
            args = ["/bin/sh", file_name]
            subprocess.check_call(args, stdout=stdout, stderr=stderr,
                                  close_fds=True, cwd=self.session_dir, env=self.get_environment())
        except (OSError, subprocess.CalledProcessError):
            logger.exception("error running hook: %s", file_name)

    def run_state_hooks(self, state):
        """
        Run state hooks.

        :param int state: state to run hooks for
        :return: nothing
        """
        for hook in self._state_hooks.get(state, []):
            try:
                hook(state)
            except:
                message = "exception occured when running %s state hook: %s" % (coreapi.state_name(state), hook)
                logger.exception(message)
                self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", None, message)

    def add_state_hook(self, state, hook):
        """
        Add a state hook.

        :param int state: state to add hook for
        :param func hook: hook callback for the state
        :return: nothing
        """
        hooks = self._state_hooks.setdefault(state, [])
        assert hook not in hooks
        hooks.append(hook)

        if self.state == state:
            hook(state)

    def del_state_hook(self, state, hook):
        """
        Delete a state hook.

        :param int state: state to delete hook for
        :param func hook: hook to delete
        :return:
        """
        hooks = self._state_hooks.setdefault(state, [])
        hooks.remove(hook)

    def runtime_state_hook(self, state):
        """
        Runtime state hook check.

        :param int state: state to check
        :return: nothing
        """
        if state == EventTypes.RUNTIME_STATE.value:
            self.emane.poststartup()
            xml_file_version = self.options.get_config("xmlfilever")
            if xml_file_version in ("1.0",):
                xml_file_name = os.path.join(self.session_dir, "session-deployed.xml")
                xml_writer = corexml.CoreXmlWriter(self)
                corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario)
                xml_writer.write(xml_file_name)

    def get_environment(self, state=True):
        """
        Get an environment suitable for a subprocess.Popen call.
        This is the current process environment with some session-specific
        variables.

        :param bool state: flag to determine if session state should be included
        :return:
        """
        env = os.environ.copy()
        env["SESSION"] = "%s" % self.session_id
        env["SESSION_SHORT"] = "%s" % self.short_session_id()
        env["SESSION_DIR"] = "%s" % self.session_dir
        env["SESSION_NAME"] = "%s" % self.name
        env["SESSION_FILENAME"] = "%s" % self.file_name
        env["SESSION_USER"] = "******" % self.user
        env["SESSION_NODE_COUNT"] = "%s" % self.get_node_count()

        if state:
            env["SESSION_STATE"] = "%s" % self.state

        # attempt to read and add environment config file
        environment_config_file = os.path.join(constants.CORE_CONF_DIR, "environment")
        try:
            if os.path.isfile(environment_config_file):
                utils.load_config(environment_config_file, env)
        except IOError:
            logger.warn("environment configuration file does not exist: %s", environment_config_file)

        # attempt to read and add user environment file
        if self.user:
            environment_user_file = os.path.join("/home", self.user, ".core", "environment")
            try:
                utils.load_config(environment_user_file, env)
            except IOError:
                logger.debug("user core environment settings file not present: %s", environment_user_file)

        return env

    def set_thumbnail(self, thumb_file):
        """
        Set the thumbnail filename. Move files from /tmp to session dir.

        :param str thumb_file: tumbnail file to set for session
        :return: nothing
        """
        if not os.path.exists(thumb_file):
            logger.error("thumbnail file to set does not exist: %s", thumb_file)
            self.thumbnail = None
            return

        destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file))
        shutil.copy(thumb_file, destination_file)
        self.thumbnail = destination_file

    def set_user(self, user):
        """
        Set the username for this session. Update the permissions of the
        session dir to allow the user write access.

        :param str user: user to give write permissions to for the session directory
        :return: nothing
        """
        if user:
            try:
                uid = pwd.getpwnam(user).pw_uid
                gid = os.stat(self.session_dir).st_gid
                os.chown(self.session_dir, uid, gid)
            except IOError:
                logger.exception("failed to set permission on %s", self.session_dir)

        self.user = user

    def get_object_id(self):
        """
        Return a unique, new random object id.
        """
        object_id = None

        with self._objects_lock:
            while True:
                object_id = random.randint(1, 0xFFFF)
                if object_id not in self.objects:
                    break

        return object_id

    def add_object(self, cls, *clsargs, **clskwds):
        """
        Add an emulation object.

        :param class cls: object class to add
        :param list clsargs: list of arguments for the class to create
        :param dict clskwds: dictionary of arguments for the class to create
        :return: the created class instance
        """
        obj = cls(self, *clsargs, **clskwds)

        self._objects_lock.acquire()
        if obj.objid in self.objects:
            self._objects_lock.release()
            obj.shutdown()
            raise KeyError("duplicate object id %s for %s" % (obj.objid, obj))
        self.objects[obj.objid] = obj
        self._objects_lock.release()

        return obj

    def get_object(self, object_id):
        """
        Get an emulation object.

        :param int object_id: object id to retrieve
        :return: object for the given id
        :rtype: core.coreobj.PyCoreNode
        """
        if object_id not in self.objects:
            raise KeyError("unknown object id %s" % object_id)
        return self.objects[object_id]

    def get_object_by_name(self, name):
        """
        Get an emulation object using its name attribute.

        :param str name: name of object to retrieve
        :return: object for the name given
        """
        with self._objects_lock:
            for obj in self.objects.itervalues():
                if hasattr(obj, "name") and obj.name == name:
                    return obj
        raise KeyError("unknown object with name %s" % name)

    def delete_object(self, object_id):
        """
        Remove an emulation object.

        :param int object_id: object id to remove
        :return: nothing
        """
        with self._objects_lock:
            try:
                obj = self.objects.pop(object_id)
                obj.shutdown()
            except KeyError:
                logger.error("failed to remove object, object with id was not found: %s", object_id)

    def delete_objects(self):
        """
        Clear the objects dictionary, and call shutdown for each object.
        """
        with self._objects_lock:
            while self.objects:
                _, obj = self.objects.popitem()
                obj.shutdown()

    def write_objects(self):
        """
        Write objects to a 'nodes' file in the session dir.
        The 'nodes' file lists: number, name, api-type, class-type
        """
        try:
            nodes_file = open(os.path.join(self.session_dir, "nodes"), "w")
            with self._objects_lock:
                for object_id in sorted(self.objects.keys()):
                    obj = self.objects[object_id]
                    nodes_file.write("%s %s %s %s\n" % (object_id, obj.name, obj.apitype, type(obj)))
            nodes_file.close()
        except IOError:
            logger.exception("error writing nodes file")

    def dump_session(self):
        """
        Log information about the session in its current state.
        """
        logger.info("session id=%s name=%s state=%s", self.session_id, self.name, self.state)
        logger.info("file=%s thumbnail=%s node_count=%s/%s",
                    self.file_name, self.thumbnail, self.get_node_count(), len(self.objects))

    def exception(self, level, source, object_id, text):
        """
        Generate and broadcast an exception event.

        :param str level: exception level
        :param str source: source name
        :param int object_id: object id
        :param str text: exception message
        :return: nothing
        """

        exception_data = ExceptionData(
            node=object_id,
            session=str(self.session_id),
            level=level,
            source=source,
            date=time.ctime(),
            text=text
        )

        self.broadcast_exception(exception_data)

    def instantiate(self):
        """
        We have entered the instantiation state, invoke startup methods
        of various managers and boot the nodes. Validate nodes and check
        for transition to the runtime state.
        """

        # write current objects out to session directory file
        self.write_objects()

        # controlnet may be needed by some EMANE models
        self.add_remove_control_interface(node=None, remove=False)

        # instantiate will be invoked again upon Emane configure
        if self.emane.startup() == self.emane.NOT_READY:
            return

        # start feature helpers
        self.broker.startup()
        self.mobility.startup()

        # boot the services on each node
        self.boot_nodes()

        # set broker local instantiation to complete
        self.broker.local_instantiation_complete()

        # notify listeners that instantiation is complete
        event = EventData(event_type=EventTypes.INSTANTIATION_COMPLETE.value)
        self.broadcast_event(event)

        # assume either all nodes have booted already, or there are some
        # nodes on slave servers that will be booted and those servers will
        # send a node status response message
        self.check_runtime()

    def get_node_count(self):
        """
        Returns the number of CoreNodes and CoreNets, except for those
        that are not considered in the GUI's node count.
        """

        with self._objects_lock:
            count = len(filter(lambda x: not nodeutils.is_node(x, (NodeTypes.PEER_TO_PEER, NodeTypes.CONTROL_NET)),
                               self.objects))

            # on Linux, GreTapBridges are auto-created, not part of GUI's node count
            count -= len(filter(
                lambda (x): nodeutils.is_node(x, NodeTypes.TAP_BRIDGE) and not nodeutils.is_node(x, NodeTypes.TUNNEL),
                self.objects))

        return count

    def check_runtime(self):
        """
        Check if we have entered the runtime state, that all nodes have been
        started and the emulation is running. Start the event loop once we
        have entered runtime (time=0).
        """
        # this is called from instantiate() after receiving an event message
        # for the instantiation state, and from the broker when distributed
        # nodes have been started
        logger.info("session(%s) checking if not in runtime state, current state: %s", self.session_id,
                    coreapi.state_name(self.state))
        if self.state == EventTypes.RUNTIME_STATE.value:
            logger.info("valid runtime state found, returning")
            return

        # check to verify that all nodes and networks are running
        if not self.broker.instantiation_complete():
            return

        # start event loop and set to runtime
        self.event_loop.run()
        self.set_state(EventTypes.RUNTIME_STATE, send_event=True)

    def data_collect(self):
        """
        Tear down a running session. Stop the event loop and any running
        nodes, and perform clean-up.
        """
        # stop event loop
        self.event_loop.stop()

        # stop node services
        with self._objects_lock:
            for obj in self.objects.itervalues():
                # TODO: determine if checking for CoreNode alone is ok
                if isinstance(obj, nodes.PyCoreNode):
                    self.services.stop_services(obj)

        # shutdown emane
        self.emane.shutdown()

        # update control interface hosts
        self.update_control_interface_hosts(remove=True)

        # remove all four possible control networks. Does nothing if ctrlnet is not installed.
        self.add_remove_control_interface(node=None, net_index=0, remove=True)
        self.add_remove_control_interface(node=None, net_index=1, remove=True)
        self.add_remove_control_interface(node=None, net_index=2, remove=True)
        self.add_remove_control_interface(node=None, net_index=3, remove=True)

    def check_shutdown(self):
        """
        Check if we have entered the shutdown state, when no running nodes
        and links remain.
        """
        node_count = self.get_node_count()
        logger.info("session(%s) checking shutdown: %s nodes remaining", self.session_id, node_count)

        shutdown = False
        if node_count == 0:
            shutdown = True
            self.set_state(EventTypes.SHUTDOWN_STATE)

        return shutdown

    def short_session_id(self):
        """
        Return a shorter version of the session ID, appropriate for
        interface names, where length may be limited.
        """
        ssid = (self.session_id >> 8) ^ (self.session_id & ((1 << 8) - 1))
        return "%x" % ssid

    def boot_nodes(self):
        """
        Invoke the boot() procedure for all nodes and send back node
        messages to the GUI for node messages that had the status
        request flag.
        """
        with self._objects_lock:
            pool = ThreadPool()
            results = []

            start = time.time()
            for obj in self.objects.itervalues():
                # TODO: PyCoreNode is not the type to check
                if isinstance(obj, nodes.PyCoreNode) and not nodeutils.is_node(obj, NodeTypes.RJ45):
                    # add a control interface if configured
                    logger.info("booting node: %s", obj.name)
                    self.add_remove_control_interface(node=obj, remove=False)
                    result = pool.apply_async(self.services.boot_services, (obj,))
                    results.append(result)

            pool.close()
            pool.join()
            for result in results:
                result.get()
            logger.debug("boot run time: %s", time.time() - start)

        self.update_control_interface_hosts()

    def get_control_net_prefixes(self):
        """
        Retrieve control net prefixes.

        :return: control net prefix list
        :rtype: list
        """
        p = self.options.get_config("controlnet")
        p0 = self.options.get_config("controlnet0")
        p1 = self.options.get_config("controlnet1")
        p2 = self.options.get_config("controlnet2")
        p3 = self.options.get_config("controlnet3")

        if not p0 and p:
            p0 = p

        return [p0, p1, p2, p3]

    def get_control_net_server_interfaces(self):
        """
        Retrieve control net server interfaces.

        :return: list of control net server interfaces
        :rtype: list
        """
        d0 = self.options.get_config("controlnetif0")
        if d0:
            logger.error("controlnet0 cannot be assigned with a host interface")
        d1 = self.options.get_config("controlnetif1")
        d2 = self.options.get_config("controlnetif2")
        d3 = self.options.get_config("controlnetif3")
        return [None, d1, d2, d3]

    def get_control_net_index(self, dev):
        """
        Retrieve control net index.

        :param str dev: device to get control net index for
        :return: control net index, -1 otherwise
        :rtype: int
        """
        if dev[0:4] == "ctrl" and int(dev[4]) in [0, 1, 2, 3]:
            index = int(dev[4])
            if index == 0:
                return index
            if index < 4 and self.get_control_net_prefixes()[index] is not None:
                return index
        return -1

    def get_control_net_object(self, net_index):
        # TODO: all nodes use an integer id and now this wants to use a string
        object_id = "ctrl%dnet" % net_index
        return self.get_object(object_id)

    def add_remove_control_net(self, net_index, remove=False, conf_required=True):
        """
        Create a control network bridge as necessary.
        When the remove flag is True, remove the bridge that connects control
        interfaces. The conf_reqd flag, when False, causes a control network
        bridge to be added even if one has not been configured.

        :param int net_index: network index
        :param bool remove: flag to check if it should be removed
        :param bool conf_required: flag to check if conf is required
        :return: control net object
        :rtype: core.netns.nodes.CtrlNet
        """
        logger.debug("add/remove control net: index(%s) remove(%s) conf_required(%s)", net_index, remove, conf_required)
        prefix_spec_list = self.get_control_net_prefixes()
        prefix_spec = prefix_spec_list[net_index]
        if not prefix_spec:
            if conf_required:
                # no controlnet needed
                return None
            else:
                control_net_class = nodeutils.get_node_class(NodeTypes.CONTROL_NET)
                prefix_spec = control_net_class.DEFAULT_PREFIX_LIST[net_index]
        logger.debug("prefix spec: %s", prefix_spec)

        server_interface = self.get_control_net_server_interfaces()[net_index]

        # return any existing controlnet bridge
        try:
            control_net = self.get_control_net_object(net_index)

            if remove:
                self.delete_object(control_net.objid)
                return None

            return control_net
        except KeyError:
            if remove:
                return None

        # build a new controlnet bridge
        object_id = "ctrl%dnet" % net_index

        # use the updown script for control net 0 only.
        updown_script = None

        if net_index == 0:
            updown_script = self.options.get_config("controlnet_updown_script")
            if not updown_script:
                logger.warning("controlnet updown script not configured")

        prefixes = prefix_spec.split()
        if len(prefixes) > 1:
            # a list of per-host prefixes is provided
            assign_address = True
            if self.master:
                try:
                    # split first (master) entry into server and prefix
                    prefix = prefixes[0].split(":", 1)[1]
                except IndexError:
                    # no server name. possibly only one server
                    prefix = prefixes[0]
            else:
                # slave servers have their name and localhost in the serverlist
                servers = self.broker.getservernames()
                servers.remove("localhost")
                prefix = None

                for server_prefix in prefixes:
                    try:
                        # split each entry into server and prefix
                        server, p = server_prefix.split(":")
                    except ValueError:
                        server = ""
                        p = None

                    if server == servers[0]:
                        # the server name in the list matches this server
                        prefix = p
                        break

                if not prefix:
                    logger.error("Control network prefix not found for server '%s'" % servers[0])
                    assign_address = False
                    try:
                        prefix = prefixes[0].split(':', 1)[1]
                    except IndexError:
                        prefix = prefixes[0]
        # len(prefixes) == 1
        else:
            # TODO: can we get the server name from the servers.conf or from the node assignments?
            # with one prefix, only master gets a ctrlnet address
            assign_address = self.master
            prefix = prefixes[0]

        control_net_class = nodeutils.get_node_class(NodeTypes.CONTROL_NET)
        control_net = self.add_object(cls=control_net_class, objid=object_id, prefix=prefix,
                                      assign_address=assign_address,
                                      updown_script=updown_script, serverintf=server_interface)

        # tunnels between controlnets will be built with Broker.addnettunnels()
        # TODO: potentially remove documentation saying object ids are ints
        # TODO: need to move broker code out of the session object
        self.broker.addnet(object_id)
        for server in self.broker.getservers():
            self.broker.addnodemap(server, object_id)

        return control_net

    def add_remove_control_interface(self, node, net_index=0, remove=False, conf_required=True):
        """
        Add a control interface to a node when a 'controlnet' prefix is
        listed in the config file or session options. Uses
        addremovectrlnet() to build or remove the control bridge.
        If conf_reqd is False, the control network may be built even
        when the user has not configured one (e.g. for EMANE.)

        :param core.netns.nodes.CoreNode node: node to add or remove control interface
        :param int net_index: network index
        :param bool remove: flag to check if it should be removed
        :param bool conf_required: flag to check if conf is required
        :return: nothing
        """
        control_net = self.add_remove_control_net(net_index, remove, conf_required)
        if not control_net:
            return

        if not node:
            return

        # ctrl# already exists
        if node.netif(control_net.CTRLIF_IDX_BASE + net_index):
            return

        control_ip = node.objid

        try:
            addrlist = ["%s/%s" % (control_net.prefix.addr(control_ip), control_net.prefix.prefixlen)]
        except ValueError:
            msg = "Control interface not added to node %s. " % node.objid
            msg += "Invalid control network prefix (%s). " % control_net.prefix
            msg += "A longer prefix length may be required for this many nodes."
            logger.exception(msg)
            return

        interface1 = node.newnetif(net=control_net,
                                   ifindex=control_net.CTRLIF_IDX_BASE + net_index,
                                   ifname="ctrl%d" % net_index, hwaddr=MacAddress.random(),
                                   addrlist=addrlist)
        node.netif(interface1).control = True

    def update_control_interface_hosts(self, net_index=0, remove=False):
        """
        Add the IP addresses of control interfaces to the /etc/hosts file.

        :param int net_index: network index to update
        :param bool remove: flag to check if it should be removed
        :return: nothing
        """
        if not self.options.get_config_bool("update_etc_hosts", default=False):
            return

        try:
            control_net = self.get_control_net_object(net_index)
        except KeyError:
            logger.exception("error retrieving control net object")
            return

        header = "CORE session %s host entries" % self.session_id
        if remove:
            logger.info("Removing /etc/hosts file entries.")
            utils.file_demunge("/etc/hosts", header)
            return

        entries = []
        for interface in control_net.netifs():
            name = interface.node.name
            for address in interface.addrlist:
                entries.append("%s %s" % (address.split("/")[0], name))

        logger.info("Adding %d /etc/hosts file entries." % len(entries))

        utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n")

    def runtime(self):
        """
        Return the current time we have been in the runtime state, or zero
        if not in runtime.
        """
        if self.state == EventTypes.RUNTIME_STATE.value:
            return time.time() - self._state_time
        else:
            return 0.0

    def add_event(self, event_time, node=None, name=None, data=None):
        """
        Add an event to the event queue, with a start time relative to the
        start of the runtime state.

        :param event_time: event time
        :param core.netns.nodes.CoreNode node: node to add event for
        :param str name: name of event
        :param data: data for event
        :return: nothing
        """
        event_time = float(event_time)
        current_time = self.runtime()

        if current_time > 0.0:
            if time <= current_time:
                logger.warn("could not schedule past event for time %s (run time is now %s)", time, current_time)
                return
            event_time = event_time - current_time

        self.event_loop.add_event(event_time, self.run_event, node=node, name=name, data=data)

        if not name:
            name = ""
        logger.info("scheduled event %s at time %s data=%s", name, event_time + current_time, data)

    # TODO: if data is None, this blows up, but this ties into how event functions are ran, need to clean that up
    def run_event(self, node_id=None, name=None, data=None):
        """
        Run a scheduled event, executing commands in the data string.

        :param int node_id: node id to run event
        :param str name: event name
        :param str data: event data
        :return: nothing
        """
        now = self.runtime()
        if not name:
            name = ""

        logger.info("running event %s at time %s cmd=%s" % (name, now, data))
        if not node_id:
            utils.mute_detach(data)
        else:
            node = self.get_object(node_id)
            node.cmd(data, wait=False)