From 19d5a189b1db5ff8bd4a2f8eddad9d2e3f056cd5 Mon Sep 17 00:00:00 2001 From: Marco Capitani Date: Wed, 9 May 2018 12:06:57 +0200 Subject: storyboard: fix Event calling get_e_id always This fixes the behaviour of the Event class which would always call get_e_id and fail if the action is a pure function or not the method of a SBEntity. Now it will simply output a warning if the event is written (because it won't be readable as-is) On top of that, all DIFs are now scriptable, and some small fixes and cleanups. --- rumba/elements/topology.py | 4 ++ rumba/storyboard.py | 173 +++++++++++++++++++++++++++++++-------------- 2 files changed, 124 insertions(+), 53 deletions(-) diff --git a/rumba/elements/topology.py b/rumba/elements/topology.py index 3054782..c47d541 100644 --- a/rumba/elements/topology.py +++ b/rumba/elements/topology.py @@ -35,6 +35,10 @@ class DIF(object): """ Base class for DIFs. """ + + def get_e_id(self): + return "DIF." + self.name + def __init__(self, name, members=None): """ :param name: Name of the DIF. diff --git a/rumba/storyboard.py b/rumba/storyboard.py index 922b200..6b56a92 100644 --- a/rumba/storyboard.py +++ b/rumba/storyboard.py @@ -235,8 +235,8 @@ class ClientProcess(_SBEntity): kill_cmd = self.shutdown.replace('', str(self.pid)) self.node.execute_command(kill_cmd) except ssh_support.SSHException: - logger.warn('Could not kill client %s on node %s.', - self.ap_id, self.node.name) + logger.warning('Could not kill client %s on node %s.', + self.ap_id, self.node.name) else: logger.debug( 'Client %s on node %s has terminated.', @@ -421,8 +421,8 @@ class Server(_SBEntity): try: self.pids[node] = node.execute_command(cmd_2, as_root=self.as_root) except ssh_support.SSHException: - logger.warn('Could not start server %s on node %s.', - self.id, node.name) + logger.warning('Could not start server %s on node %s.', + self.id, node.name) def stop(self): """Stops this server""" @@ -434,8 +434,8 @@ class Server(_SBEntity): try: node.execute_command("kill %s" % pid) except ssh_support.SSHException: - logger.warn('Could not kill server %s on node %s.', - self.id, node.name) + logger.warning('Could not kill server %s on node %s.', + self.id, node.name) # Base class for ARCFIRE storyboards @@ -485,6 +485,7 @@ class StoryBoard(_SBEntity): self.commands_list = {} self.node_map = {} self.shims = {} + self.difs = {} self._build_nodes_lists() # The following must be last, because it needs the info from # _build_nodes_list @@ -502,6 +503,7 @@ class StoryBoard(_SBEntity): for node in self.experiment.nodes: self.node_map[node.name] = node for dif in self.experiment.dif_ordering: + self.difs[dif.name] = dif if isinstance(dif, model.ShimEthDIF): self.shims[dif.name] = dif @@ -585,7 +587,7 @@ class StoryBoard(_SBEntity): kwargs=None, c_time=None, trigger=None, - ev_id=None,): + ev_id=None): """ Calls a Python function with the specified arguments as soon as the specified triggers are satisfied. @@ -1034,7 +1036,7 @@ class StoryBoard(_SBEntity): for dif in node.difs: if not isinstance(dif, model.ShimEthDIF): continue - action = functools.partial(node.schedule_link_state, dif, state) + action = functools.partial(node.set_link_state, dif, state) self._script.add_event(Event(action, ev_time=t)) def schedule_node_up(self, t, node): @@ -1166,8 +1168,13 @@ class StoryBoard(_SBEntity): node.has_tcpdump = True # Create random string - pcap_file = node.name + '_' + dif.name + '_' + \ - str(uuid.uuid4())[0:4] + ".pcap" + pcap_file = ( + node.name + + '_' + + dif.name + + '_' + + str(uuid.uuid4())[0:4] + ".pcap" + ) tcpd_client = Client(ap="tcpdump", options="-i %s -w %s" \ % (ipcp.ifname, pcap_file))\ @@ -1216,17 +1223,17 @@ class Event(object): self.trigger = trigger.id if trigger is not None else None self.exception = None self.done = False - self._repr = "%(prefix)s &%(label)s | %(entity)s %(method)s" % { - 'prefix': self._prefix_repr(), - 'label': self.id, - 'entity': '$' + self.action.func.__self__.get_e_id(), - 'method': self._action_repr() - } + self._repr = None def _action_repr(self): if isinstance(self.action, functools.partial): name = self.action.func.__name__ - args = ' '.join(self._action_arg_repr(x) for x in self.action.args) + arg_list = [self._action_arg_repr(x) for x in self.action.args] + arg_list += [ + k + '=' + self._action_arg_repr(v) + for k, v in self.action.keywords.items() + ] + args = ' '.join(arg_list) else: name = self.action.__name__ args = '' @@ -1307,7 +1314,39 @@ class Event(object): def __str__(self): return self.id + def __recover_e_name(self): + if isinstance(self.action, functools.partial): + func = self.action.func + else: + func = self.action + + # Recover entity name if possible + entity = getattr(func, '__self__', None) + if entity is None: # Not a method, skip + e_name = None + else: + try: + e_name = entity.get_e_id() # SB entity + except AttributeError: + # last ditch effort + e_name = getattr(entity, 'name', None) + if e_name is not None: + # Prepend token used in .rsb + e_name = '$' + e_name + return e_name + def __repr__(self): + if self._repr is None: + e_name = self.__recover_e_name() + if e_name is None: + logger.warning("Event %s has no valid entity name, " + "cannot serialize correctly." % (self.id,)) + self._repr = "%(prefix)s &%(label)s | %(entity)s %(method)s" % { + 'prefix': self._prefix_repr(), + 'label': self.id, + 'entity': e_name, + 'method': self._action_repr() + } return self._repr @@ -1354,7 +1393,8 @@ class _Script(object): 'Server': self._servers, 'Client': self._clients, 'ClientProcess': self._processes, - 'ShimEthDIF': self._shims + 'ShimEthDIF': self._shims, + 'DIF': self._storyboard.difs } def add_process(self, process): @@ -1428,45 +1468,72 @@ class _Script(object): raise ValueError('No method called "%s" for entity %s.' % (method_n, entity.get_e_id())) args_l = action_l[1:] - args = self._parse_action_arguments(args_l) - return functools.partial(method, *args) - # TODO maybe some argument checking? + args, kwargs = self._parse_action_arguments(args_l) + return functools.partial(method, *args, **kwargs) + # TODO maybe some introspection for argument checking? - def _parse_action_arguments(self, args_l): - args = [] - quotes_part = None - while len(args_l) > 0: + @staticmethod + def _pop_str_arg(args_l): + part = None + must_close = False + while not must_close: arg_s = args_l.pop(0) - if quotes_part is not None: + if part is not None: # We're inside a string if arg_s.endswith("'"): - arg_s = arg_s[:-1] # Strip final ' - quotes_part += ' ' + arg_s - args.append(quotes_part) - quotes_part = None - elif arg_s.startswith("'"): - arg_s = arg_s[1:] # Strip initial ' + arg_s = arg_s[:-1] # Strip final ' char + must_close = True + part += ' ' + arg_s + elif arg_s.startswith("'"): # Starting a string + arg_s = arg_s[1:] # Strip initial ' char if arg_s.endswith("'"): - arg_s = arg_s[:-1] # Strip final ' - args.append(arg_s) - else: - quotes_part = arg_s - else: # Not a string - if arg_s.startswith('$'): - args.append(self._resolve_entity(arg_s[1:])) - continue - try: # No string, no entity, it must be a number - args.append(int(arg_s)) - continue - except ValueError: - pass # Try to parse it as float - try: - args.append(float(arg_s)) - except ValueError: - raise ValueError('Syntax error: %s is not a valid value. ' - 'If it is supposed to be a string, ' - 'enclose it in single quotes.' - % (arg_s,)) - return args + arg_s = arg_s[:-1] # Strip final ' char + must_close = True + part = arg_s + else: # Should not happen + raise ValueError('Non string arg %s parsed as string' + % (arg_s,)) + return part + + def _pop_non_str_arg(self, args_l): + arg_s = args_l.pop(0) + if arg_s.startswith('$'): + return self._resolve_entity(arg_s[1:]) + if '=' in arg_s: # kw arg + split = arg_s.split('=') + k = split[0] + v = ''.join(split[1:]) # e.g. v = equals containing str + args_l.insert(0, v) # treat the value as one common arg + parsed_v = self._pop_one_arg(args_l) + return k, parsed_v + try: # No string, no entity, no kw, it must be a number + return int(arg_s) + except ValueError: + pass # No int, maybe float? + try: + return float(arg_s) + except ValueError: # Out of options + raise ValueError('Syntax error: %s is not a valid value. ' + 'If it is supposed to be a string, ' + 'enclose it in single quotes.' + % (arg_s,)) + + def _pop_one_arg(self, args_l): + if args_l[0].startswith("'"): + return self._pop_str_arg(args_l) + else: + return self._pop_non_str_arg(args_l) + + def _parse_action_arguments(self, args_l): + args = [] + kwargs = {} + while len(args_l) > 0: + next_arg = self._pop_one_arg(args_l) + if isinstance(next_arg, tuple): + k, v = next_arg + kwargs[k] = v + else: + args.append(next_arg) + return args, kwargs def _parse_prefix(self, prefix): prefix = prefix.strip().split('&') -- cgit v1.2.3