diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | emulab_support.py | 336 | ||||
-rwxr-xr-x | main.py | 6 | ||||
-rwxr-xr-x | rhumba.py | 198 |
4 files changed, 526 insertions, 17 deletions
@@ -87,3 +87,6 @@ ENV/ # Rope project settings .ropeproject + +# emacs temporary files +*~
\ No newline at end of file diff --git a/emulab_support.py b/emulab_support.py new file mode 100644 index 0000000..6c31cd6 --- /dev/null +++ b/emulab_support.py @@ -0,0 +1,336 @@ +# +# Emulab support for Rhumba +# +# Sander Vrijders <sander.vrijders@intec.ugent.be> +# Wouter Tavernier <wouter.tavernier@intec.ugent.be> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +import socket +import paramiko +import time +import os +import re +from ast import literal_eval +import configparser + +import warnings +warnings.filterwarnings("ignore") + +tag = "emulab-support" + +def log_debug(message): + print(tag + "(DBG): " + message) + +def log_error(message): + print(tag + "(ERR): " + message) + +def get_ssh_client(): + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + return ssh_client + +def ops_server(testbed): + ''' + Return server name of the ops-server (is testbed specific) + + @param testbed: testbed info + + @return: server name of the ops-server + ''' + return 'ops.' + testbed.url + +def full_name(testbed, node_name): + ''' + Return server name of a node + + @param node_name: name of the node + @param testbed: testbed info + + @return: server name of the node + ''' + return node_name + '.' + testbed.exp_name + '.' + \ + testbed.proj_name + '.' + testbed.url + +def execute_command(testbed, hostname, command, time_out = 3): + ''' + Remote execution of a list of shell command on hostname. By + default this function will exit (timeout) after 3 seconds. + + @param testbed: testbed info + @param hostname: host name or ip address of the node + @param command: *nix shell command + @param time_out: time_out value in seconds, error will be generated if + no result received in given number of seconds, the value None can + be used when no timeout is needed + + @return: stdout resulting from the command + ''' + ssh_client = get_ssh_client() + + try: + ssh_client.connect(hostname, 22, + testbed.username, testbed.password, + look_for_keys = True, timeout = time_out) + stdin, stdout, stderr = ssh_client.exec_command(command) + err = str(stderr.read()).strip('b\'\"\\n') + if err != "": + log_error(err) + output = str(stdout.read()).strip('b\'\"\\n') + ssh_client.close() + + return output + + except Exception as e: + log_error(str(e)) + return + +def copy_file_to_testbed(testbed, hostname, text, file_name): + ''' + Write a string to a given remote file. + Overwrite the complete file if it already exists! + + @param testbed: testbed info + @param hostname: host name or ip address of the node + @param text: string to be written in file + @param file_name: file name (including full path) on the host + ''' + ssh_client = get_ssh_client() + + try: + ssh_client.connect(hostname, 22, + testbed.username, + testbed.password, + look_for_keys=True) + + cmd = "touch " + file_name + \ + "; chmod a+rwx " + file_name + + stdin, stdout, stderr = ssh_client.exec_command(cmd) + err = str(stderr.read()).strip('b\'\"\\n') + if err != "": + log_error(err) + + sftp_client = ssh_client.open_sftp() + remote_file = sftp_client.open(file_name, 'w') + + remote_file.write(text) + remote_file.close() + + except Exception as e: + log_error(str(e)) + +def get_experiment_list(testbed, project_name = None): + ''' + Get list of made emulab experiments accessible with your credentials + + @param testbed: testbed info + @param project_name: optional filter on project + + @return: list of created experiments (strings) + ''' + cmd = '/usr/testbed/bin/sslxmlrpc_client.py -m experiment getlist' + out = execute_command(testbed, ops_server(testbed), cmd) + + try: + if project_name != None: + return literal_eval(out)[project_name][project_name] + else: + return literal_eval(out) + except: + return { project_name: { project_name: [] }} + +def swap_exp_in(testbed): + ''' + Swaps experiment in + + @param testbed: testbed info + ''' + cmd = '/usr/testbed/bin/sslxmlrpc_client.py swapexp proj=' + \ + testbed.proj_name + \ + ' exp=' + \ + testbed.exp_name + \ + ' direction=in' + + output = execute_command(testbed, ops_server(testbed), cmd) + + return output + +def create_experiment(testbed, nodes, links): + ''' + Creates an emulab experiment + + @param testbed: testbed info + @param nodes: holds the nodes in the experiment + @param links: holds the links in the experiment + ''' + proj_name = testbed.proj_name + exp_name = testbed.exp_name + + exp_list = get_experiment_list(testbed) + + try: + if exp_name in exp_list[proj_name][proj_name]: + log_debug("Experiment already exists.") + return + except: + log_debug("First experiment to be created for that project.") + + ns = generate_ns_script(testbed, nodes, links) + dest_file_name = '/users/'+ testbed.username + \ + '/temp_ns_file.%s.ns' % os.getpid() + copy_file_to_testbed(testbed, ops_server(testbed), ns, dest_file_name) + + cmd = '/usr/testbed/bin/sslxmlrpc_client.py startexp ' + \ + 'batch=false wait=true proj="' + proj_name + \ + '" exp="' + exp_name + '" noswapin=true ' + \ + 'nsfilepath="' + dest_file_name + '"' + + execute_command(testbed, ops_server(testbed), cmd, time_out = None) + execute_command(testbed, ops_server(testbed),'rm ' + dest_file_name) + log_debug("New experiment succesfully created.") + +def generate_ns_script(testbed, nodes, p2plinks): + ''' + Generate ns script based on network graph. + Enables to customize default node image. + + @param nodes: holds the nodes in the experiment + @param links: holds the links in the experiment + @param testbed: testbed info + + @return: ns2 script for Emulab experiment + ''' + + ns2_script = "# ns script generated by Rhumba\n" + ns2_script += "set ns [new Simulator]\n" + ns2_script += "source tb_compat.tcl\n" + + for node in nodes: + ns2_script += "set " + node.name + " [$ns node]\n" + ns2_script += "tb-set-node-os $" + node.name + " " + \ + testbed.image + "\n" + + for link in p2plinks: + ns2_script += "set " + link.name + \ + " [$ns duplex-link $" + \ + link.node_a.name + " $" + \ + link.node_b.name + " 1000Mb 0ms DropTail]\n" + + ns2_script += "$ns run\n" + + return ns2_script + +def wait_until_nodes_up(testbed): + ''' + Checks if nodes are up + + @param testbed: testbed info + ''' + log_debug("Waiting until all nodes are up") + + cmd = '/usr/testbed/bin/script_wrapper.py expinfo -e' + \ + testbed.proj_name + \ + ',' + \ + testbed.exp_name + \ + ' -a | grep State | cut -f2,2 -d " "' + + res = execute_command(testbed, ops_server(testbed), cmd) + active = False + if res == "active": + active = True + while active != True: + res = execute_command(testbed, ops_server(testbed), cmd) + if res == "active": + active = True + log_debug("Still waiting") + time.sleep(5) + +def complete_experiment_graph(testbed, nodes, p2plinks): + ''' + Gets the interface (ethx) to link mapping + + @param testbed: testbed info + @param nodes: holds the nodes in the experiment + @param links: holds the links in the experiment + ''' + + node_full_name = full_name(testbed, nodes[0].name) + cmd = 'cat /var/emulab/boot/topomap' + topomap = execute_command(testbed, node_full_name, cmd) + # Almost as ugly as yo momma + index = topomap.rfind("# lans") + topo_array = topomap[:index].split('\\n')[1:-1] + # Array contains things like 'r2b1,link7:10.1.6.3 link6:10.1.5.3' + for item in topo_array: + item_array = re.split(',? ?', item) + node_name = item_array[0] + for item2 in item_array[1:]: + item2 = item2.split(':') + link_name = item2[0] + link_ip = item2[1] + for link in p2plinks: + if link.name == link_name: + if link.node_a.name == node_name: + link.int_a.ip = link_ip + elif link.node_b.name == node_name: + link.int_b.ip = link_ip + + for node in nodes: + cmd = 'cat /var/emulab/boot/ifmap' + node_full_name = full_name(testbed, node.name) + output = execute_command(testbed, node_full_name, cmd) + output = re.split('\\\\n', output) + for item in output: + item = item.split() + for link in p2plinks: + if link.node_a.name == node.name and \ + link.int_a.ip == item[1]: + link.int_a.name = item[0] + elif link.node_b.name == node.name and \ + link.int_b.ip == item[1]: + link.int_b.name = item[0] + +def setup_vlan(testbed, node_name, vlan_id, int_name): + ''' + Gets the interface (ethx) to link mapping + + @param testbed: testbed info + @param node_name: the node to create the VLAN on + @param vlan_id: the VLAN id + @param int_name: the name of the interface + ''' + log_debug("Setting up VLAN on node " + node_name) + + node_full_name = full_name(node_name, testbed) + cmd = "sudo ip link add link " + \ + str(int_name) + \ + " name " + str(int_name) + \ + "." + str(vlan_id) + \ + " type vlan id " + str(vlan_id) + execute_command(testbed, node_full_name, cmd) + cmd = "sudo ifconfig " + \ + str(int_name) + "." + \ + str(vlan_id) + " up" + execute_command(node_full_name, cmd, testbed) + cmd = "sudo ethtool -K " + \ + str(int_name) + " rxvlan off" + execute_command(node_full_name, cmd, testbed) + cmd = "sudo ethtool -K " + \ + str(int_name) + " txvlan off" + execute_command(node_full_name, cmd, testbed) @@ -19,7 +19,11 @@ b = Node("b", difs = [e1, n1], dif_registrations = {n1 : [e1]}) -exp = IRATIExperiment("paperino", +tb = EmulabTestbed(username = "sander", + exp_name = "test001", + url = "wall2.ilabt.iminds.be") + +exp = IRATIExperiment("paperino", tb, nodes = [a, b]) print(exp) @@ -1,26 +1,176 @@ # # A library to manage ARCFIRE experiments # +# Sander Vrijders <sander.vrijders@intec.ugent.be> +# Vincenzo Maffione <v.maffione@nextworks.it> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +import emulab_support as es +import abc + +# Represents generic testbed info +# +# @username [string] user name +# @password [string] password +# @proj_name [string] project name +# @exp_name [string] experiment name +# +class Testbed: + def __init__(self, username = "", password = "", + proj_name = "", exp_name = ""): + self.username = username + self.password = password + self.proj_name = proj_name + self.exp_name = exp_name + + @abc.abstractmethod + def create_experiment(self, nodes, links): + return + + @abc.abstractmethod + def swap_exp_in(self): + return + + @abc.abstractmethod + def wait_until_nodes_up(self): + return + + @abc.abstractmethod + def complete_experiment_graph(self, nodes, links): + return + + @abc.abstractmethod + def execute_command(self, hostname, command, time_out = 3): + return + + @abc.abstractmethod + def copy_file_to_testbed(self, hostname, text, file_name): + return + +# Represents an emulab testbed info +# +# @url [string] URL of the testbed +# @image [string] specific image to use +# +class EmulabTestbed: + def __init__(self, username = "", password = "", + proj_name = "ARCFIRE", exp_name = "", + url = "wall1.ilabt.iminds.be", + image = "UBUNTU14-64-STD"): + Testbed.__init__(self, username, password, + proj_name, exp_name) + self.url = url + self.image = image + + def create_experiment(self, nodes, links): + es.create_experiment(self, nodes, links) + + def swap_exp_in(self): + es.swap_exp_in(self) + + def wait_until_nodes_up(self): + es.wait_until_nodes_up(self) + + def complete_experiment_graph(self, nodes, links): + es.complete_experiment_graph(self, nodes, links) + + def execute_command(self, hostname, command, time_out = 3): + es.execute_command(self, hostname, command, time_out = 3) + + def copy_file_to_testbed(self, hostname, text, file_name): + es.copy_file_to_testbed(self, hostname, text, file_name) + +# Represents an interface on a node +# +# @name [string] interface name +# @ip [int] IP address of that interface +# +class Interface: + def __init__(self, name = "", ip = ""): + self.name = name + self.ip = ip + +# Represents a link in the physical graph +# +# @name [string] Link name +# +class Link: + def __init__(self, name): + self.name = name + +# Represents a point-to-point link in the physical graph +# +# @name [string] DIF name +# +class P2PLink(Link): + def __init__(self, name, node_a, node_b, + int_a = Interface(), + int_b = Interface()): + Link.__init__(self, name) + self.node_a = node_a + self.node_b = node_b + self.int_a = int_a + self.int_b = int_b + +def get_links(nodes): + difs = set() + links = list() + for node in nodes: + for dif in node.difs: + if type(dif) is ShimEthDIF: + difs.add(dif) + + for dif in difs: + # Point-to-point link + if len(dif.members) == 2: + node_a = dif.members[0] + node_b = dif.members[1] + link = P2PLink(node_a.name + "-" + node_b.name, + node_a, node_b) + links.append(link) + + return links # Base class for DIFs # # @name [string] DIF name # class DIF: - def __init__(self, name): + def __init__(self, name, members = list()): self.name = name + self.members = members def __repr__(self): s = "DIF %s" % self.name return s + def add_member(self, node): + self.members.append(node) + + def del_member(self, node): + self.members.remove(node) + # Shim over Ethernet # # @link_speed [int] Speed of the Ethernet network, in Mbps # class ShimEthDIF(DIF): - def __init__(self, name, link_speed = 0): - DIF.__init__(self, name) + def __init__(self, name, members = list(), link_speed = 0): + DIF.__init__(self, name, members) self.link_speed = int(link_speed) if self.link_speed < 0: raise ValueError("link_speed must be a non-negative number") @@ -30,8 +180,8 @@ class ShimEthDIF(DIF): # @policies [dict] Policies of the normal DIF # class NormalDIF(DIF): - def __init__(self, name, policies = dict()): - DIF.__init__(self, name) + def __init__(self, name, members = list(), policies = dict()): + DIF.__init__(self, name, members) self.policies = policies def add_policy(self, comp, pol): @@ -54,12 +204,14 @@ class NormalDIF(DIF): # @bindings: Binding of names on the processing system # class Node: - def __init__(self, name, difs = set(), + def __init__(self, name, difs = list(), dif_registrations = dict(), registrations = dict(), bindings = dict()): self.name = name self.difs = difs + for dif in difs: + dif.add_member(self) self.dif_registrations = dif_registrations self.registrations = registrations self.bindings = bindings @@ -90,6 +242,14 @@ class Node: s += " ]\n" return s + def add_dif(self, dif): + self.difs.append(dif) + dif.add_member(self) + + def del_dif(self, dif): + self.difs.remove(dif) + dif.del_member(self) + def add_dif_registration(self, dif_a, dif_b): self.dif_registrations[dif_a].append(dif_b) @@ -114,9 +274,10 @@ class Node: # @nodes: Nodes in the experiment # class Experiment: - def __init__(self, name, nodes = set()): + def __init__(self, name, testbed, nodes = list()): self.name = name self.nodes = nodes + self.testbed = testbed def __repr__(self): s = "%s:" % self.name @@ -132,35 +293,40 @@ class Experiment: self.nodes.remove(node) def run(self): - print("[Experiment %s] start" % self.name) - print("[Experiment %s] end" % self.name) - + self.links = get_links(self.nodes) + self.testbed.create_experiment(self.nodes, self.links) + self.testbed.swap_exp_in() + self.testbed.wait_until_nodes_up() + self.testbed.complete_experiment_graph(self.nodes, self.links) # An experiment over the IRATI implementation class IRATIExperiment(Experiment): - def __init__(self, name, nodes = set()): - Experiment.__init__(self, name, nodes) + def __init__(self, name, testbed, nodes = list()): + Experiment.__init__(self, name, testbed, nodes) def run(self): print("[IRATI experiment %s] start" % self.name) + Experiment.run(self) print("[IRATI experiment %s] end" % self.name) # An experiment over the RLITE implementation class RLITEExperiment(Experiment): - def __init__(self, name, nodes = set()): - Experiment.__init__(self, name, nodes) + def __init__(self, name, testbed, nodes = list()): + Experiment.__init__(self, name, testbed, nodes) def run(self): print("[RLITE experiment %s] start" % self.name) + Experiment.run(self) print("[RLITE experiment %s] end" % self.name) # An experiment over the Ouroboros implementation class OuroborosExperiment(Experiment): - def __init__(self, name, nodes = set()): - Experiment.__init__(self, name, nodes) + def __init__(self, name, testbed, nodes = list()): + Experiment.__init__(self, name, testbed, nodes) def run(self): print("[Ouroboros experiment %s] start" % self.name) + Experiment.run(self) print("[Ouroboros experiment %s] end" % self.name) |