diff --git a/ansible/linkstate/down.yml b/ansible/linkstate/down.yml new file mode 100644 index 00000000000..8efbaf0e133 --- /dev/null +++ b/ansible/linkstate/down.yml @@ -0,0 +1,76 @@ +# ansible-playbook -i linkstate/testbed_inv.py -e target_host=vms3-t0-s6100 --vault-password-file=~/.password linkstate/down.yml + +- hosts: lab + gather_facts: no + tasks: + - name: prepare admin password + set_fact: ansible_ssh_user=root ansible_ssh_pass={{ lab_admin_pass }} + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Stop the script + shell: pkill -f "python fanout_listener.py" + when: '"python fanout_listener.py" in out.stdout' + - name: Remove the scripts + file: + dest: fanout_listener.py + state: absent + +- hosts: ptf_host + gather_facts: no + tasks: + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Stop the script + shell: pkill -f "python ptf_proxy.py" + when: '"python ptf_proxy.py" in out.stdout' + ignore_errors: yes # pkill does it work but returns error -15, which is not documented for pkill + - name: Remove the scripts + file: + dest: "{{ item }}" + state: absent + with_items: + - sonic_str_links.csv + - sonic_str_devices.csv + - sonic_lab_devices.csv + - sonic_lab_links.csv + - veos + - ptf_proxy.py + - topo.yaml + +- hosts: eos + gather_facts: no + tasks: + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Stop script vm_tcp_listener.py + shell: pkill -f "python vm_tcp_listener.py" + when: '"python vm_tcp_listener.py" in out.stdout' +# It requires double check because vm_state_changer.py exits as soon as vm_tcp_listener.py exited + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Stop script vm_state_changer.py + shell: pkill -f "python vm_state_changer.py" + when: '"python vm_state_changer.py" in out.stdout' + - name: Remove scripts + file: + dest: "{{ item }}" + state: absent + with_items: + - vm_state_changer.py + - vm_tcp_listener.py + - name: Check if the rule exists + command: ip netns exec ns-MGMT iptables -L -n + changed_when: False + register: iptables_out + - name: Deinstall iptable rule + shell: ip netns exec ns-MGMT iptables -D INPUT 1 + when: '"tcp dpt:9876" in iptables_out.stdout' + diff --git a/ansible/linkstate/scripts/fanout_listener.py b/ansible/linkstate/scripts/fanout_listener.py new file mode 100644 index 00000000000..ec8db6f2fc5 --- /dev/null +++ b/ansible/linkstate/scripts/fanout_listener.py @@ -0,0 +1,99 @@ +import EntityManager +import Tac +import socket +import pickle +import argparse +import datetime +from pprint import pprint + + +g_ptf_host = None +g_log_fp = None + + +def log(message, output_on_console=False): + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if output_on_console: + print "%s : %s" % (current_time, message) + global g_log_fp + if g_log_fp is not None: + g_log_fp.write("%s : %s\n" % (current_time, message)) + g_log_fp.flush() + + +class IntfMonitor(Tac.Notifiee): + notifierTypeName = "Interface::EthPhyIntfStatus" + def __init__(self, intfStatus): + self.state = {} + Tac.Notifiee.__init__(self, intfStatus) + self.send() + + @Tac.handler('linkStatus') + def handleLinkStatus(self): + self.send() + + def close(self): + Tac.Notifiee.close(self) + + def send(self): + if self.notifier_.intfId in self.state and self.state[self.notifier_.intfId] == self.notifier_.linkStatus: + return + + self.state[self.notifier_.intfId] = self.notifier_.linkStatus + conn = Conn() + data = {"intf": self.notifier_.intfId, "linkStatus": self.notifier_.linkStatus} + log("Event: intf %s changed its state %s" % (self.notifier_.intfId, self.notifier_.linkStatus)) + log("Send data %s" % str(data)) + conn.write(data) + data = conn.read() + log("Received reply: %s" % str(data)) + + +def setup_sw(): + em = EntityManager.Sysdb("ar") + mg = em.mountGroup() + intfStatusDir = mg.mount("interface/status/eth/phy/all", "Interface::AllEthPhyIntfStatusDir", "r") + mg.close(blocking=True) + + return Tac.collectionChangeReactor(intfStatusDir.intfStatus, IntfMonitor) + + +class Conn(object): + def __init__(self): + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.connect((g_ptf_host, 9877)) + + def __del__(self): + self.conn.close() + + def read(self): + fp = self.conn.makefile('rb', 1024) + data = pickle.load(fp) + fp.close() + return data + + def write(self, data): + fp = self.conn.makefile('wb', 1024) + pickle.dump(data, fp, pickle.HIGHEST_PROTOCOL) + fp.close() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("ptf_host", type=str, help="ip address of ptf host") + args = parser.parse_args() + global g_ptf_host + g_ptf_host = str(args.ptf_host) + + global g_log_fp + g_log_fp = open("/tmp/fanout_listener.log", "w") + + sw = setup_sw() + try: + Tac.runActivities() + except: + pass + +if __name__ == '__main__': + main() + diff --git a/ansible/linkstate/scripts/ptf_proxy.py b/ansible/linkstate/scripts/ptf_proxy.py new file mode 100644 index 00000000000..8f33373fb52 --- /dev/null +++ b/ansible/linkstate/scripts/ptf_proxy.py @@ -0,0 +1,199 @@ +import SocketServer +import pickle +import socket +import argparse +import yaml +import datetime +import os.path +from pprint import pprint + + +g_log_fp = None + + +def log(message, output_on_console=False): + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if output_on_console: + print "%s : %s" % (current_time, message) + global g_log_fp + if g_log_fp is not None: + g_log_fp.write("%s : %s\n" % (current_time, message)) + g_log_fp.flush() + + +class TCPHandler(SocketServer.StreamRequestHandler): + def handle(self): + data = pickle.load(self.rfile) + log("Received request: %s" % str(data)) + key = self.client_address[0], data['intf'] + if key in self.server.x_table: + value = self.server.x_table[key] + conn = Conn(value[0]) + data['intf'] = value[1] + log("Send data %s to %s" % (str(value[0]), str(data))) + conn.write(data) + data = conn.read() + log("Received reply %s" % str(data)) + else: + data = {'status': 'OK'} + data = {'status': 'OK'} + log("Send reply %s" % str(data)) + pickle.dump(data, self.wfile, pickle.HIGHEST_PROTOCOL) + + +class Conn(object): + def __init__(self, ip): + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.connect((ip, 9876)) + + def __del__(self): + self.conn.close() + + def read(self): + fp = self.conn.makefile('rb', 1024) + data = pickle.load(fp) + fp.close() + return data + + def write(self, data): + fp = self.conn.makefile('wb', 1024) + pickle.dump(data, fp, pickle.HIGHEST_PROTOCOL) + fp.close() + + +def parse_links(dut): + candidates = ['sonic_str_links.csv', 'sonic_lab_links.csv'] + # find what files exists before opening + target = None + for filename in candidates: + if os.path.exists(filename): + target = filename + break + with open(target) as fp: + all = fp.read() + rows = all.split("\n")[1:] + + devices = [] + dut_ports = [] + mapping = {} + + for r in rows: + if r == '': + continue + if dut not in r: + continue + values = r.split(',') + target_device = values[0] + target_port = values[1] + fanout_device = values[2] + fanout_port = values[3] + if target_device == dut: + devices.append(fanout_device) + mapping[(fanout_device, fanout_port)] = target_port + dut_ports.append(target_port) + + dut_ports = sorted(dut_ports, cmp=lambda x,y: cmp(int(x.replace('Ethernet', '')), int(y.replace('Ethernet', '')))) + + return devices, dut_ports, mapping + +def parse_devices(device_names): + ip_name = {} + candidates = ['sonic_str_devices.csv', 'sonic_lab_devices.csv'] + # find what files exists before opening + target = None + for filename in candidates: + if os.path.exists(filename): + target = filename + break + with open(target) as fp: + all = fp.read() + rows = all.split("\n") + for r in rows: + if r == '': + continue + values = r.split(',') + name = values[0] + if name not in device_names: + continue + ip_prefix = values[1] + ip_name[name] = ip_prefix.split('/')[0] + + return ip_name + +def parse_veos(vms): + mapping = {} + with open('veos') as fp: + all = fp.read() + rows = all.split('\n') + for r in rows: + if r == '': + continue + if not r.startswith('VM'): + continue + name, ansible_host = r.split(' ') + if name not in vms: + continue + address = ansible_host.split('=')[1] + mapping[name] = address + + return mapping + +def generate_vm_mappings(vms, base_vm, dut_ports, vm_2_ip): + base_vm_id = int(base_vm[2:]) + required_ports = {} + for vm_offset, ports in vms.items(): + vm = 'VM%04d' % (base_vm_id + vm_offset) + vm_ip = vm_2_ip[vm] + p = {dut_ports[port]: (vm_ip, 'Ethernet%d' % (offset + 1)) for offset, port in enumerate(ports)} + required_ports.update(p) + + return required_ports + +def generate_vm_port_mapping(vm_base): + with open('topo.yaml') as fp: + data = yaml.load(fp) + + base = int(vm_base.replace("VM", "")) + + vm_ports = {v['vm_offset']:v['vlans'] for v in data['topology']['VMs'].values()} + vm_list = ["VM%04d" % (base + p) for p in sorted(vm_ports.keys())] + + return vm_ports, vm_list + +def merge(fanout_mappings, fanout_name_2_ip, vm_mappings): + return {(fanout_name_2_ip[fanout_name], fanout_port) : vm_mappings[dut_port] for (fanout_name, fanout_port), dut_port in fanout_mappings.iteritems() if dut_port in vm_mappings} + +def generate_x_table(base_vm, dut): + devices, dut_ports, mapping = parse_links(dut) + fanout_name_2_ip = parse_devices(devices) + vm_ports, vm_list = generate_vm_port_mapping(base_vm) + vm_2_ip = parse_veos(vm_list) + vm_mappings = generate_vm_mappings(vm_ports, base_vm, dut_ports, vm_2_ip) + target = merge(mapping, fanout_name_2_ip, vm_mappings) + + return target + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("vm_base", type=str, help="vm_base parameter") + parser.add_argument("dut", type=str, help="dut parameter") + args = parser.parse_args() + base_vm = args.vm_base + dut = args.dut + + global g_log_fp + g_log_fp = open("/tmp/ptf_proxy.log", "w") + + x_table = generate_x_table(base_vm, dut) + + server = SocketServer.TCPServer(("0.0.0.0", 9877), TCPHandler) + server.request_queue_size = 64 + server.allow_reuse_address = True + server.x_table = x_table + server.serve_forever() + + return + +if __name__ == '__main__': + main() + diff --git a/ansible/linkstate/scripts/vm_state_changer.py b/ansible/linkstate/scripts/vm_state_changer.py new file mode 100644 index 00000000000..c16292113d4 --- /dev/null +++ b/ansible/linkstate/scripts/vm_state_changer.py @@ -0,0 +1,89 @@ +import PyClient +from pprint import pprint +import pickle +import os +import datetime +import errno + + +g_log_fp = None + + +def log(message, output_on_console=False): + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if output_on_console: + print "%s : %s" % (current_time, message) + global g_log_fp + if g_log_fp is not None: + g_log_fp.write("%s : %s\n" % (current_time, message)) + g_log_fp.flush() + + +class FIFOServer(object): + FIFOr = '/tmp/fifor' + FIFOw = '/tmp/fifow' + def __init__(self, intf_manager): + self.intf_manager = intf_manager + try: + if os.path.exists(self.FIFOr): + os.unlink(self.FIFOr) + if os.path.exists(self.FIFOw): + os.unlink(self.FIFOw) + os.mkfifo(self.FIFOr) + os.mkfifo(self.FIFOw) + except OSError as err: + if err.errno != errno.EEXIST: + raise + + self.fifow = open(self.FIFOw, "w") + self.fifor = open(self.FIFOr) + + def serve_forever(self): + while True: + data = pickle.load(self.fifor) + log("Received request %s" % str(data)) + self.intf_manager.linkChange(data['intf'], data['linkStatus']) + data = {'status': 'OK'} + log("Send reply %s" % str(data)) + pickle.dump(data, self.fifow, pickle.HIGHEST_PROTOCOL) + self.fifow.flush() + + +class IntfManager(object): + def __init__(self): + pc = PyClient.PyClient("ar", "Sysdb") + sysdb = pc.agentRoot() + allIntfStatus = sysdb["interface"]["status"]["all"] + self.intf_list = [intf for intf in sysdb["interface"]["status"]["eth"]["phy"]['all'] if 'Ethernet' in intf] + self.intfStatus = {intf : allIntfStatus.intfStatus[intf] for intf in self.intf_list} + + def linkChange(self, intf, state): + if intf in self.intf_list: + self.intfStatus[intf].linkStatus = state + else: + raise Exception("Interface %s doesn't exist" % intf) # FIXME: better just log it + + def linkUp(self, intf): + self.linkChange(intf, "linkUp") + + def linkDown(self, intf): + self.linkChange(intf, "linkDown") + + def get_interfaces(self): + return self.intf_list + + +def main(): + try: + global g_log_fp + g_log_fp = open("/tmp/vm_state_changer.log", "w") + + intf = IntfManager() + server = FIFOServer(intf) + server.serve_forever() + except: + pass + +if __name__ == '__main__': + main() + diff --git a/ansible/linkstate/scripts/vm_tcp_listener.py b/ansible/linkstate/scripts/vm_tcp_listener.py new file mode 100644 index 00000000000..44ae62bb6bc --- /dev/null +++ b/ansible/linkstate/scripts/vm_tcp_listener.py @@ -0,0 +1,59 @@ +from pprint import pprint +import pickle +import SocketServer +import datetime + + +g_log_fp = None + + +def log(message, output_on_console=False): + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if output_on_console: + print "%s : %s" % (current_time, message) + global g_log_fp + if g_log_fp is not None: + g_log_fp.write("%s : %s\n" % (current_time, message)) + g_log_fp.flush() + + +class TCPHandler(SocketServer.StreamRequestHandler): + def handle(self): + data = pickle.load(self.rfile) + log("Received and send request %s" % str(data)) + self.server.fifo_client.write(data) + data = self.server.fifo_client.read() + log("Received and send reply %s" % str(data)) + pickle.dump(data, self.wfile, pickle.HIGHEST_PROTOCOL) + + +class FIFOClient(object): + FIFOr = '/tmp/fifor' + FIFOw = '/tmp/fifow' + def __init__(self): + self.fifow = open(self.FIFOw) + self.fifor = open(self.FIFOr, 'w') + + def write(self, data): + pickle.dump(data, self.fifor, pickle.HIGHEST_PROTOCOL) + self.fifor.flush() + + def read(self): + return pickle.load(self.fifow) + + +def main(): + try: + global g_log_fp + g_log_fp = open("/tmp/vm_tcp_listener.log", "w") + + fifo = FIFOClient() + server = SocketServer.TCPServer(("0.0.0.0", 9876), TCPHandler) + server.fifo_client = fifo + server.serve_forever() + except: + pass + +if __name__ == '__main__': + main() + diff --git a/ansible/linkstate/testbed_inv.ini b/ansible/linkstate/testbed_inv.ini new file mode 100644 index 00000000000..bb06237b728 --- /dev/null +++ b/ansible/linkstate/testbed_inv.ini @@ -0,0 +1,6 @@ +[Global] +testbed_configuration = testbed.csv +vm_inventory = veos +lab_inventory = lab +lab_links = files/sonic_lab_links.csv + diff --git a/ansible/linkstate/testbed_inv.py b/ansible/linkstate/testbed_inv.py new file mode 100755 index 00000000000..a6909420200 --- /dev/null +++ b/ansible/linkstate/testbed_inv.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +import sys +import json +import yaml +import itertools +import ConfigParser +import os + +from pprint import pprint + +def read_config(): + config = ConfigParser.ConfigParser() + with open('linkstate/testbed_inv.ini') as fp: + config.readfp(fp) + testbed_topologies = config.get("Global", "testbed_configuration") + vm_inventory = config.get("Global", "vm_inventory") + lab_inventory = config.get("Global", "lab_inventory") + lab_links = config.get("Global", "lab_links") + return {"topos": testbed_topologies, + "inv": {"vm": vm_inventory, + "lab": lab_inventory}, + "links": lab_links} + +def parse_testbed_configuration(filename, target): + with open(filename) as fp: + for line in fp: + if line.startswith(target): + splitted_line = line.split(",") + ptf_name = splitted_line[1] + topo_name = splitted_line[2] + ptf_addr = splitted_line[4] + vm_start = splitted_line[6] + dut = splitted_line[7] + return ptf_name, topo_name, ptf_addr, vm_start, dut + +def parse_topology(topology_name, vm_start): + with open("vars/topo_%s.yml" % topology_name) as fp: + topo = yaml.load(fp) + vms = ["%s%04d" % (vm_start[0:2], int(vm_start[2:]) + v["vm_offset"]) for v in topo['topology']['VMs'].values()] + ports = list(itertools.chain(*(val['vlans'] for val in topo['topology']['VMs'].values()))) + return vms, ports + +def parse_links(links, dut, ports): + with open(links) as fp: + result = set(line.split(',')[2] for line in fp if line.startswith(dut)) + return list(result) + +def extract_hostvars(filename, host): + with open(filename) as fp: + for line in fp: + if line.startswith(host): + return {value.split('=')[0]:value.split('=')[1] for value in line.rstrip().split()[1:]} + +def get_hosts(host): + config = read_config() + ptf_name, topo_name, ptf_addr, vm_start, dut = parse_testbed_configuration(config["topos"], host) + vms, ports = parse_topology(topo_name, vm_start) + fanouts = parse_links(config["links"], dut, ports) + returned = {} + returned['ptf_host'] = [ptf_name] + returned['lab'] = fanouts + returned['str'] = fanouts + returned['eos'] = vms + + all_hosts = [ptf_name] + all_hosts.extend(vms) + all_hosts.extend(fanouts) + returned['all'] = all_hosts + + hostvars = {} + + hostvars[ptf_name] = extract_hostvars(config['inv']['lab'], ptf_name) + hostvars[ptf_name]['topo'] = topo_name + hostvars[ptf_name]['vm_base'] = vm_start + hostvars[ptf_name]['dut'] = dut + + for fanout in fanouts: + hostvars[fanout] = extract_hostvars(config['inv']['lab'], fanout) + hostvars[fanout]["ptf_host"] = ptf_addr.split('/')[0] + + for vm in vms: + hostvars[vm] = extract_hostvars(config['inv']['vm'], vm) + + returned['_meta'] = {} + returned['_meta']['hostvars'] = hostvars + + return returned + +def get_hostname(): + ppid = os.getppid() + with open('/proc/%d/cmdline' % ppid) as fp: + cmdline = fp.read() + for pair in cmdline.split('\0'): + if '=' in pair and pair.split('=')[0] == 'target_host': + return pair.split('=')[1] + + return None + +if __name__ == '__main__': + inventory = {} + + hostname = get_hostname() + if hostname is not None: + inventory = get_hosts(hostname) + +# with open('inventory_facts', 'w') as f: +# json.dump(inventory, f, indent=2) + + sys.stdout.write(json.dumps(inventory, indent=2)) diff --git a/ansible/linkstate/up.yml b/ansible/linkstate/up.yml new file mode 100644 index 00000000000..de8e18c1df2 --- /dev/null +++ b/ansible/linkstate/up.yml @@ -0,0 +1,73 @@ +# ansible-playbook -i linkstate/testbed_inv.py -e target_host=vms3-t0-s6100 --vault-password-file=~/.password linkstate/up.yml + +- hosts: eos + gather_facts: no + tasks: + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Copy scripts + copy: + src: "{{ item }}" + dest: /root + with_items: + - scripts/vm_state_changer.py + - scripts/vm_tcp_listener.py + - name: Check if the rule exists already + command: ip netns exec ns-MGMT iptables -L -n + changed_when: False + register: iptables_out + - name: Install iptable rule + shell: ip netns exec ns-MGMT iptables -I INPUT 1 -p tcp --dport 9876 -j ACCEPT + when: '"tcp dpt:9876" not in iptables_out.stdout' + - name: Run scripts vm_state_changer.py + shell: nohup python vm_state_changer.py > /tmp/vm_state_changer.console.log 2>&1 & + when: '"python vm_state_changer.py" not in out.stdout' + - name: Run scripts vm_tcp_listener.py + shell: nohup ip netns exec ns-MGMT python vm_tcp_listener.py > /tmp/vm_tcp_listener.console.log 2>&1 & + when: '"python vm_tcp_listener.py" not in out.stdout' + +- hosts: ptf_host + gather_facts: no + tasks: + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Copy scripts and source files + copy: + src: "{{ item }}" + dest: /root + with_items: + - ../files/sonic_str_links.csv + - ../files/sonic_str_devices.csv + - ../files/sonic_lab_links.csv + - ../files/sonic_lab_devices.csv + - ../veos + - scripts/ptf_proxy.py + - ../vars/topo_{{ topo }}.yml + ignore_errors: yes # either sonic_str_*.csv or sonic_lab_*.csv exists + - name: Rename topo to common filename + command: mv topo_{{ topo }}.yml topo.yaml + - name: Run the script + shell: nohup python ptf_proxy.py {{ vm_base }} {{ dut }} > /tmp/ptf_proxy.console.log 2>&1 & + when: '"python ptf_proxy.py" not in out.stdout' + +- hosts: lab + gather_facts: no + tasks: + - name: prepare admin password + set_fact: ansible_ssh_user=root ansible_ssh_pass={{ lab_admin_pass }} + - name: Check list of processes + command: ps ax + changed_when: False + register: out + - name: Copy script + copy: + src: scripts/fanout_listener.py + dest: /root + - name: Run the script + shell: nohup python fanout_listener.py {{ ptf_host }} > /tmp/fanout_listener.console.txt 2>&1 & + when: '"python fanout_listener.py" not in out.stdout' + diff --git a/ansible/roles/fanout/templates/arista_7260_deploy.j2 b/ansible/roles/fanout/templates/arista_7260_deploy.j2 index 233d80c756e..b5732739ad6 100644 --- a/ansible/roles/fanout/templates/arista_7260_deploy.j2 +++ b/ansible/roles/fanout/templates/arista_7260_deploy.j2 @@ -21,6 +21,7 @@ spanning-tree mode none no spanning-tree vlan {{ device_vlan_range | list | join(',') }} ! aaa authorization exec default local +aaa root secret 0 {{ lab_admin_pass }} ! no aaa root !