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()
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)
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()
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
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)