├── README.md ├── acme-large ├── 0_built_topo.py ├── 1_monitor.py ├── 2_test.py ├── 3_destroy_topo.py ├── README.md ├── config │ ├── Backup-CORE-SW.txt │ ├── Backup-DMVPN.txt │ ├── Backup-MPLS.txt │ ├── Backup-WAN.txt │ ├── Cloud-1.txt │ ├── Cloud-2.txt │ ├── DMVPN-ONLY.txt │ ├── FW.txt │ ├── MPLS-DMVPN.txt │ ├── MPLS-ONLY.txt │ ├── Primary-CORE-SW.txt │ ├── Primary-DMVPN.txt │ ├── Primary-MPLS.txt │ └── Primary-WAN.txt ├── network │ ├── __init__.py │ ├── acme-large.jpg │ ├── ospf-bgp.txt │ ├── tests │ │ ├── ping_flows.txt │ │ └── traffic_flows.txt │ ├── topology.py │ └── unetlab.yml ├── requirements.txt ├── tmp │ └── .gitignore └── tools │ ├── __init__.py │ ├── conf_analyzer.py │ ├── decorators.py │ ├── dns.py │ ├── endpoint.py │ ├── file_io.py │ ├── globals.py │ ├── ping.py │ ├── testflows.py │ ├── traceroute.py │ └── unetlab.py ├── acme-small ├── 0_built_topo.py ├── 1_monitor.py ├── 2_test.py ├── 3_destroy_topo.py ├── README.md ├── config │ ├── R1.txt │ ├── R2.txt │ ├── R3.txt │ └── R4.txt ├── network │ ├── __init__.py │ ├── acme-small.jpg │ ├── tests │ │ ├── ping_flows.txt │ │ └── traffic_flows.txt │ ├── topology.py │ └── unetlab.yml ├── requirements.txt ├── tmp │ └── .gitignore └── tools │ ├── __init__.py │ ├── conf_analyzer.py │ ├── decorators.py │ ├── dns.py │ ├── endpoint.py │ ├── file_io.py │ ├── globals.py │ ├── ping.py │ ├── testflows.py │ ├── traceroute.py │ └── unetlab.py └── skeleton ├── 0_built_topo.py ├── 1_monitor.py ├── 2_test.py ├── 3_destroy_topo.py ├── README.md ├── network ├── __init__.py ├── testing │ ├── ping_flows.txt │ └── traffic_flows.txt ├── topology.py └── unetlab.yml ├── requirements.txt └── tools ├── __init__.py ├── conf_analyzer.py ├── decorators.py ├── dns.py ├── endpoint.py ├── file_io.py ├── globals.py ├── ping.py ├── testflows.py ├── traceroute.py └── unetlab.py /README.md: -------------------------------------------------------------------------------- 1 | # NETWORK-CI 2 | 3 | ## Proof of Concept of CI/CD methodology applied to traditional non-SDN network topologies 4 | 5 | This repository contains a set of tools that can be used to automate network testing during design and upgrade stages. 6 | This is what these tools can do for you: 7 | 8 | * Build full network topology inside a network emulation environment 9 | * Apply configuration to all built nodes 10 | * Verify real-time connectivity between nodes while making manual configuration changes 11 | * Verify traffic flows against pre-defined rules 12 | * Shutdown and delete the network topology 13 | 14 | These tools can be combined and used by CI/CD automation servers like Jenkins as I showed in my [blog](http://networkop.github.io/blog/2016/02/19/network-ci-intro/). 15 | 16 | ## Examples 17 | 18 | To show how these tools can be used, I've created the following sample networks ranging in size from 4 to 14 nodes. 19 | 20 | * [acme-small](/acme-small) - simple 4-node topology 21 | * [acme-large](/acme-large) - 14-node enterprise data centre 22 | 23 | ## Documentation 24 | 25 | For detailed instructions on how to use these tool proceed to [skeleton](/skeleton/) 26 | 27 | -------------------------------------------------------------------------------- /acme-large/0_built_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | from tools.globals import * 4 | import tools.file_io as file_io 5 | from tools.unetlab import UNetLab 6 | from tools.conf_analyzer import ConfAnalyzer 7 | from network import topology 8 | 9 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 10 | 11 | 12 | def main(): 13 | try: 14 | print("*** CONNECTING TO UNL") 15 | UNL.create_lab() 16 | print("*** BUILDING TOPOLOGY") 17 | UNL.build_topo(topology) 18 | print("*** NORMALIZING CONFIGURATION FILES") 19 | conf_files = ConfAnalyzer() 20 | conf_files.normalize(file_io.read_yaml(INTF_CONV_FILE)) 21 | print("*** EXTRACTING IP") 22 | conf_files.extract_ip() 23 | print("*** STARTING ALL NODES") 24 | UNL.start() 25 | print("*** CONFIGURING NODES") 26 | UNL.configure_nodes(TMP_DIR) 27 | print("*** ALL NODES CONFIGURED") 28 | except Exception: 29 | UNL.destroy() 30 | raise 31 | return 0 32 | 33 | if __name__ == '__main__': 34 | sys.exit(main()) 35 | 36 | -------------------------------------------------------------------------------- /acme-large/1_monitor.py: -------------------------------------------------------------------------------- 1 | from tools.ping import Ping 2 | import tools.file_io as file_io 3 | from tools.unetlab import UNetLab 4 | from tools.globals import * 5 | import sys 6 | import threading 7 | 8 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 9 | PING_FLOWS = file_io.read_txt('{}/ping_flows.txt'.format(TEST_DIR)) 10 | RUN = [True] 11 | 12 | 13 | def key_press(): 14 | global RUN 15 | RUN[0] = raw_input() 16 | 17 | 18 | def main(): 19 | lab = UNL.get_lab() 20 | ping = Ping(PING_FLOWS, lab) 21 | thread = threading.Thread(target=key_press) 22 | thread.start() 23 | print('Starting pings. Press "Enter" to stop') 24 | ping.run(RUN) 25 | print('\rStopped'), 26 | 27 | if __name__ == '__main__': 28 | sys.exit(main()) 29 | -------------------------------------------------------------------------------- /acme-large/2_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import time 4 | from tools.unetlab import UNetLab 5 | from tools.testflows import TestFlows 6 | from tools.traceroute import Traceroute 7 | import tools.file_io as file_io 8 | from tools.globals import * 9 | 10 | 11 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 12 | TEST_FLOWS = TestFlows(file_io.read_txt('{}/traffic_flows.txt'.format(TEST_DIR))) 13 | INTF_CONV = file_io.read_yaml(INTF_CONV_FILE) 14 | RECONVERGENCE_TIMER = 15 15 | 16 | 17 | def conf_shut_intf(intf): 18 | return conf_run_intf(intf, 'shutdown') 19 | 20 | 21 | def conf_unshut_intf(intf): 22 | return conf_run_intf(intf, 'no shutdown') 23 | 24 | 25 | def conf_run_intf(intf, command): 26 | return '\r\n'.join(['enable', 'conf t', 'interface {}'.format(intf), command, 'end']) 27 | 28 | 29 | def run_tests(tests): 30 | failed = False 31 | for seq, fail_condition in sorted(tests.keys()): 32 | print 33 | print("*** TESTING SCENARIO {}".format(seq)) 34 | for fail_point in fail_condition: 35 | if not fail_point.dev == 'None': 36 | fail_node = fail_point.get_node() 37 | try: 38 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 39 | except KeyError: 40 | lab_intf = fail_point.intf 41 | fail_node.configure(conf_shut_intf(lab_intf)) 42 | print("*** FAILURE CONDITION CREATED: {}".format(fail_point)) 43 | # wait for protocols to converge 44 | time.sleep(RECONVERGENCE_TIMER) 45 | for (from_point, to_point), flow_data in tests[(seq, fail_condition)].iteritems(): 46 | flow = flow_data['parsed'] 47 | print("*** TESTING FLOW FROM {} TO {}".format(from_point, to_point)) 48 | from_node = from_point.get_node() 49 | trace_command = 'traceroute {} {} numeric'.format(to_point.ip, 'source ' + from_point.ip) 50 | enable = 'enable\rconf t\rno logging console\rend\r' 51 | trace_result = from_node.configure(enable + trace_command) 52 | traceroute = Traceroute(trace_result) 53 | traceroute.parse() 54 | traceroute.resolve() 55 | rc = traceroute.verify(flow) 56 | if rc > 0: 57 | failed = True 58 | print('!!! FAILED FLOW FROM {} TO {}'.format(from_point, to_point)) 59 | print('!!! EXPECTED PATH: {}, ACTUAL PATH: {}' 60 | .format('->'.join(flow_data['text'].split(', ')), traceroute.as_string([from_point.dev]))) 61 | else: 62 | print ('*** SUCCESSFUL FLOW') 63 | for fail_point in fail_condition: 64 | if not fail_point.dev == 'None': 65 | fail_node = fail_point.get_node() 66 | try: 67 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 68 | except KeyError: 69 | lab_intf = fail_point.intf 70 | fail_node.configure(conf_unshut_intf(lab_intf)) 71 | print("*** FAILURE CONDITION RESTORED: {}".format(fail_point)) 72 | return failed 73 | 74 | 75 | def main(): 76 | try: 77 | lab = UNL.get_lab() 78 | print("*** CONNECTED TO UNL") 79 | flows = TEST_FLOWS.parse(lab) 80 | failed = run_tests(flows) 81 | print("*** TESTS COMPLETED") 82 | except: 83 | raise 84 | return 1 if failed else 0 85 | 86 | 87 | if __name__ == '__main__': 88 | sys.exit(main()) 89 | -------------------------------------------------------------------------------- /acme-large/3_destroy_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import tools.file_io as file_io 4 | from tools.unetlab import UNetLab 5 | from tools.globals import * 6 | 7 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 8 | 9 | 10 | def main(): 11 | try: 12 | UNL.destroy() 13 | print("*** LAB DESTROYED") 14 | for f in os.listdir(TMP_DIR): 15 | if not f.startswith('.'): 16 | file_path = os.path.join(TMP_DIR, f) 17 | if os.path.isfile(file_path): 18 | os.unlink(file_path) 19 | print("*** TMP DIRECTORY CLEANED UP") 20 | except: 21 | print ('*** Emulation Failed') 22 | raise 23 | return 0 24 | 25 | if __name__ == '__main__': 26 | sys.exit(main()) 27 | 28 | -------------------------------------------------------------------------------- /acme-large/README.md: -------------------------------------------------------------------------------- 1 | # ACME-LARGE 2 | 3 | A large 14-node topology. 4 | 5 | ![Alt text](./network/acme-large.jpg?raw=true "4-node topology") 6 | 7 | ## Prerequisites 8 | 9 | * UNetLab server reachable from local machine 10 | * L2 and L3 IOU images under `/opt/unetlab/addons/iol/bin` renamed to 'L2-LATEST.bin' and 'L3-LATEST.bin' 11 | 12 | ## Install dependencies 13 | 14 | ```bash 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | ## Environment setup 19 | 20 | * Change `./network/tests/traffic_flows.txt` file to match the expected traffic paths 21 | * Change `./network/tests/ping_flows.txt` file to match the destinations that need to be monitored 22 | * Change `./network/unetlab.yml` to match your UNetLab server environment 23 | 24 | ## Workflow 25 | 26 | 1. Build and configure topology 27 | ```bash 28 | ./0_build_topo.py 29 | ``` 30 | 31 | After this step you should be able to find the lab up and running on UNetLab server. 32 | 33 | 2. Verify real-time connectivity while making configuration changes. Example of changes required 34 | to convert from OSPF to BGP can be found in `./network/ospf-bgp.txt` file 35 | 36 | ```bash 37 | ./1_monitor.py 38 | ``` 39 | 40 | Only failed pings will be displayed. 41 | 42 | 3. Verify test scenarios 43 | 44 | ```bash 45 | ./2_test.py 46 | ``` 47 | 48 | If any of the scenarios have failed, examine the output, adjust configuration as needed and re-run the tests. 49 | 50 | 4. Shutdown and delete the lab 51 | 52 | ```bash 53 | ./3_destroy_topo.py 54 | ``` 55 | 56 | ## Caveats 57 | 58 | * Designed only for IPv4 on Cisco IOS devices 59 | * Assuming only 15 seconds for protocol reconvergence when creating failure conditions 60 | -------------------------------------------------------------------------------- /acme-large/config/Backup-CORE-SW.txt: -------------------------------------------------------------------------------- 1 | 2 | hostname Standby-CORE-SW 3 | ! 4 | spanning-tree mode rapid-pvst 5 | spanning-tree portfast bpduguard default 6 | ! 7 | ! 8 | ! 9 | ! 10 | vlan internal allocation policy ascending 11 | ! 12 | vlan 210 13 | name OSPF-WAN 14 | ! 15 | vlan 220 16 | name OSPF-DMVPN 17 | ! 18 | vlan 30 19 | name OSPF-CORE 20 | ! 21 | vlan 40 22 | name FW 23 | ! 24 | interface Loopback0 25 | ip address 10.1.3.2 255.255.255.255 26 | ! 27 | interface GigabitEthernet0/1 28 | description STANDBY-WAN (Gi0/3) 29 | switchport trunk encapsulation dot1q 30 | switchport mode trunk 31 | ! 32 | interface GigabitEthernet0/2 33 | description PRIMARY-DMVPN (G0/3) 34 | switchport access vlan 220 35 | switchport mode access 36 | spanning-tree portfast 37 | ! 38 | interface GigabitEthernet0/3 39 | description FW 40 | switchport trunk encapsulation dot1q 41 | switchport mode trunk 42 | ! 43 | interface GigabitEthernet0/4 44 | no switchport 45 | description Cloud-2 46 | ip address 10.1.4.25 255.255.255.252 47 | ip ospf cost 1000 48 | ip ospf network point-to-point 49 | ip ospf hello-interval 5 50 | ip ospf dead-interval 15 51 | ! 52 | interface GigabitEthernet0/5 53 | description Cloud-1 54 | no switchport 55 | ip address 10.1.4.9 255.255.255.252 56 | ip ospf cost 1000 57 | ip ospf network point-to-point 58 | ip ospf hello-interval 5 59 | ip ospf dead-interval 15 60 | ! 61 | interface GigabitEthernet0/6 62 | switchport trunk allowed vlan 30,40 63 | switchport trunk encapsulation dot1q 64 | switchport mode trunk 65 | ! 66 | interface Vlan210 67 | ip address 10.1.1.14 255.255.255.252 68 | ip ospf network point-to-point 69 | ip ospf hello-interval 5 70 | ip ospf dead-interval 15 71 | ! 72 | interface Vlan220 73 | ip address 10.1.1.18 255.255.255.252 74 | ip ospf network point-to-point 75 | ip ospf hello-interval 5 76 | ip ospf dead-interval 15 77 | ! 78 | interface Vlan30 79 | ip address 10.1.1.10 255.255.255.252 80 | ip ospf network point-to-point 81 | ip ospf hello-interval 5 82 | ip ospf dead-interval 15 83 | ! 84 | interface Vlan40 85 | description FW 86 | ip address 10.1.1.35 255.255.255.224 87 | no ip redirects 88 | no ip proxy-arp 89 | ip ospf cost 1000 90 | standby version 2 91 | standby 20 ip 10.1.1.33 92 | standby 20 preempt 93 | ! 94 | router ospf 1 95 | network 0.0.0.0 255.255.255.255 area 0 96 | passive-interface default 97 | no passive-interface GigabitEthernet0/4 98 | no passive-interface GigabitEthernet0/5 99 | no passive-interface Vlan210 100 | no passive-interface Vlan220 101 | no passive-interface Vlan30 102 | ! 103 | router bgp 65529 104 | template peer-policy RR-CLIENT 105 | route-reflector-client 106 | send-community both 107 | default-originate 108 | exit-peer-policy 109 | ! 110 | template peer-session iBGP 111 | remote-as 65529 112 | timers 5 15 113 | update-source Loopback0 114 | exit-peer-session 115 | ! 116 | bgp cluster-id 10.1.3.2 117 | bgp log-neighbor-changes 118 | neighbor 10.1.2.2 remote-as 65529 119 | neighbor 10.1.2.2 inherit peer-session iBGP 120 | neighbor 10.1.2.2 inherit peer-policy RR-CLIENT 121 | neighbor 10.1.3.1 remote-as 65529 122 | neighbor 10.1.3.1 inherit peer-session iBGP 123 | neighbor 10.1.3.1 inherit peer-policy RR-CLIENT 124 | neighbor 10.1.3.7 remote-as 65529 125 | neighbor 10.1.3.7 inherit peer-session iBGP 126 | neighbor 10.1.3.7 inherit peer-policy RR-CLIENT 127 | neighbor 10.4.0.1 remote-as 65529 128 | neighbor 10.4.0.1 inherit peer-session iBGP 129 | neighbor 10.4.0.1 inherit peer-policy RR-CLIENT 130 | neighbor 10.5.0.1 remote-as 65529 131 | neighbor 10.5.0.1 inherit peer-session iBGP 132 | neighbor 10.5.0.1 inherit peer-policy RR-CLIENT 133 | ! 134 | ip route 0.0.0.0 0.0.0.0 10.1.1.62 135 | ! 136 | !workaround the IOU bug 137 | no ip cef 138 | ! -------------------------------------------------------------------------------- /acme-large/config/Backup-DMVPN.txt: -------------------------------------------------------------------------------- 1 | 2 | hostname Standby-DMVPN 3 | ! 4 | interface Loopback0 5 | ip address 10.1.3.7 255.255.255.255 6 | ! 7 | interface GigabitEthernet0/1 8 | ip address 10.1.12.1 255.255.255.0 9 | ! 10 | ! 11 | interface GigabitEthernet0/3 12 | ip address 10.1.1.17 255.255.255.252 13 | ip ospf network point-to-point 14 | ip ospf hello-interval 5 15 | ip ospf dead-interval 15 16 | ! 17 | ! 18 | router eigrp 2 19 | network 10.1.12.0 0.0.0.255 20 | default-metric 1 1 1 1 1 21 | redistribute bgp 65529 route-map RM-BGP-EIGRP 22 | passive-interface default 23 | no passive-interface GigabitEthernet0/1 24 | ! 25 | router ospf 1 26 | network 0.0.0.0 255.255.255.255 area 0 27 | passive-interface default 28 | no passive-interface GigabitEthernet0/3 29 | ! 30 | router bgp 65529 31 | bgp log-neighbor-changes 32 | neighbor 10.1.3.2 remote-as 65529 33 | neighbor 10.1.3.2 update-source Loopback0 34 | neighbor 10.1.3.2 send-community both 35 | redistribute eigrp 2 route-map RM-EIGRP-BGP 36 | bgp redistribute-internal 37 | ! 38 | ip community-list standard ICL-DC-DMVPN permit 65529:2 39 | route-map RM-EIGRP-BGP deny 10 40 | match tag 65529 41 | route-map RM-EIGRP-BGP permit 1000 42 | set community 65529:2 43 | route-map RM-BGP-EIGRP deny 10 44 | match community ICL-DC-DMVPN 45 | route-map RM-BGP-EIGRP permit 1000 46 | set tag 65529 47 | ! -------------------------------------------------------------------------------- /acme-large/config/Backup-MPLS.txt: -------------------------------------------------------------------------------- 1 | hostname Backup-MPLS 2 | ! 3 | ! 4 | ! 5 | ! 6 | ! 7 | interface GigabitEthernet0/0 8 | ip address 172.16.0.69 255.255.255.252 9 | ! 10 | interface GigabitEthernet0/1 11 | ip address 172.16.0.73 255.255.255.252 12 | ! 13 | interface GigabitEthernet0/2 14 | ip address 172.16.0.77 255.255.255.252 15 | ! 16 | ! 17 | router bgp 65530 18 | bgp log-neighbor-changes 19 | timers bgp 2 6 20 | neighbor 172.16.0.70 remote-as 65529 21 | neighbor 172.16.0.74 remote-as 65531 22 | neighbor 172.16.0.78 remote-as 65532 23 | ! 24 | ip forward-protocol nd 25 | ! 26 | ! 27 | no ip http server 28 | no ip http secure-server 29 | ! 30 | ! 31 | ! 32 | ! 33 | control-plane 34 | ! 35 | ! 36 | ! 37 | ! 38 | ! 39 | ! 40 | ! 41 | ! 42 | line con 0 43 | logging synchronous 44 | line aux 0 45 | line vty 0 4 46 | login 47 | transport input none 48 | ! 49 | -------------------------------------------------------------------------------- /acme-large/config/Backup-WAN.txt: -------------------------------------------------------------------------------- 1 | 2 | hostname Standby-WAN 3 | ! 4 | ! 5 | interface Loopback0 6 | ip address 10.1.3.1 255.255.255.255 7 | ! 8 | interface GigabitEthernet0/0 9 | description MPLS-WAN 10 | ip address 172.16.0.70 255.255.255.252 11 | ! 12 | interface GigabitEthernet0/3 13 | description Standby-CORE-SW (G0/1) 14 | ! 15 | interface GigabitEthernet0/3.210 16 | encapsulation dot1Q 210 17 | ip address 10.1.1.13 255.255.255.252 18 | ip ospf network point-to-point 19 | ip ospf hello-interval 5 20 | ip ospf dead-interval 15 21 | ! 22 | ! 23 | router ospf 1 24 | networ 0.0.0.0 255.255.255.255 area 0 25 | passive-interface default 26 | no passive-interface GigabitEthernet0/3.210 27 | ! 28 | router bgp 65529 29 | neighbor 172.16.0.69 remote-as 65530 30 | neighbor 172.16.0.69 route-map AS-PREPEND out 31 | neighbor 172.16.0.69 route-map RM-SECOND-IN in 32 | neighbor 10.1.3.2 remote-as 65529 33 | neighbor 10.1.3.2 update-source Loopback0 34 | neighbor 10.1.3.2 send-community both 35 | ! 36 | route-map AS-PREPEND permit 10 37 | set as-path prepend 65529 65529 38 | ! 39 | route-map RM-SECOND-IN permit 10 40 | set local-preference 150 41 | -------------------------------------------------------------------------------- /acme-large/config/Cloud-1.txt: -------------------------------------------------------------------------------- 1 | Current configuration : 1273 bytes 2 | ! 3 | ! Last configuration change at 03:17:13 UTC Fri Feb 12 2016 4 | ! 5 | version 15.4 6 | service timestamps debug datetime msec 7 | service timestamps log datetime msec 8 | no service password-encryption 9 | ! 10 | hostname Cloud-1 11 | ! 12 | boot-start-marker 13 | boot-end-marker 14 | ! 15 | ! 16 | ! 17 | no aaa new-model 18 | mmi polling-interval 60 19 | no mmi auto-configure 20 | no mmi pvc 21 | mmi snmp-timeout 180 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | ! 30 | 31 | 32 | ! 33 | ! 34 | ! 35 | ! 36 | no ip domain lookup 37 | ip cef 38 | no ipv6 cef 39 | ! 40 | multilink bundle-name authenticated 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | ! 48 | ! 49 | ! 50 | redundancy 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | ! 62 | ! 63 | ! 64 | ! 65 | ! 66 | interface Loopback0 67 | ip address 10.4.0.1 255.255.0.0 68 | ! 69 | interface Ethernet0/0 70 | ip address 10.1.4.2 255.255.255.252 71 | ip ospf network point-to-point 72 | ip ospf hello-interval 5 73 | ip ospf dead-interval 15 74 | ! 75 | interface Ethernet0/1 76 | ip address 10.1.4.10 255.255.255.252 77 | ip ospf cost 1000 78 | ip ospf network point-to-point 79 | ip ospf hello-interval 5 80 | ip ospf dead-interval 15 81 | ! 82 | interface Ethernet0/2 83 | no ip address 84 | shutdown 85 | ! 86 | interface Ethernet0/3 87 | no ip address 88 | shutdown 89 | ! 90 | interface Ethernet1/0 91 | no ip address 92 | shutdown 93 | ! 94 | interface Ethernet1/1 95 | no ip address 96 | shutdown 97 | ! 98 | interface Ethernet1/2 99 | no ip address 100 | shutdown 101 | ! 102 | interface Ethernet1/3 103 | no ip address 104 | shutdown 105 | ! 106 | router ospf 1 107 | network 0.0.0.0 255.255.255.255 area 0 108 | ! 109 | router bgp 65529 110 | bgp log-neighbor-changes 111 | neighbor 10.1.2.2 remote-as 65529 112 | neighbor 10.1.2.2 update-source Loopback0 113 | neighbor 10.1.2.2 send-community both 114 | neighbor 10.1.3.2 remote-as 65529 115 | neighbor 10.1.3.2 update-source Loopback0 116 | neighbor 10.1.3.2 send-community both 117 | network 10.4.0.0 mask 255.255.0.0 118 | ! 119 | ip forward-protocol nd 120 | ! 121 | ! 122 | no ip http server 123 | no ip http secure-server 124 | ! 125 | ! 126 | ! 127 | ! 128 | control-plane 129 | ! 130 | ! 131 | ! 132 | ! 133 | ! 134 | ! 135 | ! 136 | ! 137 | line con 0 138 | logging synchronous 139 | line aux 0 140 | line vty 0 4 141 | login 142 | transport input none 143 | ! 144 | -------------------------------------------------------------------------------- /acme-large/config/Cloud-2.txt: -------------------------------------------------------------------------------- 1 | ! 2 | ! Last configuration change at 09:07:12 UTC Fri Feb 12 2016 3 | ! 4 | version 15.4 5 | service timestamps debug datetime msec 6 | service timestamps log datetime msec 7 | no service password-encryption 8 | ! 9 | hostname CLOUD-2 10 | ! 11 | boot-start-marker 12 | boot-end-marker 13 | ! 14 | ! 15 | ! 16 | no aaa new-model 17 | mmi polling-interval 60 18 | no mmi auto-configure 19 | no mmi pvc 20 | mmi snmp-timeout 180 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | 30 | 31 | ! 32 | ! 33 | ! 34 | ! 35 | no ip domain lookup 36 | ip cef 37 | no ipv6 cef 38 | ! 39 | multilink bundle-name authenticated 40 | ! 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | ! 48 | ! 49 | redundancy 50 | ! 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | ! 62 | ! 63 | ! 64 | ! 65 | interface Loopback0 66 | ip address 10.5.0.1 255.255.0.0 67 | ! 68 | interface Ethernet0/0 69 | ip address 10.1.4.18 255.255.255.252 70 | ip ospf network point-to-point 71 | ip ospf hello-interval 5 72 | ip ospf dead-interval 15 73 | ! 74 | interface Ethernet0/1 75 | ip address 10.1.4.26 255.255.255.252 76 | ip ospf cost 1000 77 | ip ospf network point-to-point 78 | ip ospf hello-interval 5 79 | ip ospf dead-interval 15 80 | ! 81 | interface Ethernet0/2 82 | no ip address 83 | shutdown 84 | ! 85 | interface Ethernet0/3 86 | no ip address 87 | shutdown 88 | ! 89 | interface Ethernet1/0 90 | no ip address 91 | shutdown 92 | ! 93 | interface Ethernet1/1 94 | no ip address 95 | shutdown 96 | ! 97 | interface Ethernet1/2 98 | no ip address 99 | shutdown 100 | ! 101 | interface Ethernet1/3 102 | no ip address 103 | shutdown 104 | ! 105 | router ospf 1 106 | network 0.0.0.0 255.255.255.255 area 0 107 | ! 108 | router bgp 65529 109 | bgp log-neighbor-changes 110 | network 10.5.0.0 mask 255.255.0.0 111 | neighbor 10.1.2.2 remote-as 65529 112 | neighbor 10.1.2.2 update-source Loopback0 113 | neighbor 10.1.2.2 send-community both 114 | neighbor 10.1.3.2 remote-as 65529 115 | neighbor 10.1.3.2 update-source Loopback0 116 | neighbor 10.1.3.2 send-community both 117 | ! 118 | ip forward-protocol nd 119 | ! 120 | ! 121 | no ip http server 122 | no ip http secure-server 123 | ! 124 | ! 125 | ! 126 | ! 127 | ! 128 | control-plane 129 | ! 130 | ! 131 | ! 132 | ! 133 | ! 134 | ! 135 | ! 136 | ! 137 | line con 0 138 | logging synchronous 139 | line aux 0 140 | line vty 0 4 141 | login 142 | transport input none 143 | ! 144 | -------------------------------------------------------------------------------- /acme-large/config/DMVPN-ONLY.txt: -------------------------------------------------------------------------------- 1 | ! 2 | ! Last configuration change at 03:30:42 UTC Fri Feb 12 2016 3 | ! 4 | version 15.4 5 | service timestamps debug datetime msec 6 | service timestamps log datetime msec 7 | no service password-encryption 8 | ! 9 | hostname DMVPN-ONLY 10 | ! 11 | boot-start-marker 12 | boot-end-marker 13 | ! 14 | ! 15 | ! 16 | no aaa new-model 17 | mmi polling-interval 60 18 | no mmi auto-configure 19 | no mmi pvc 20 | mmi snmp-timeout 180 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | ! 30 | ! 31 | ! 32 | ! 33 | no ip domain lookup 34 | ip cef 35 | no ipv6 cef 36 | ! 37 | multilink bundle-name authenticated 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | redundancy 48 | ! 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | ! 62 | ! 63 | interface Loopback0 64 | ip address 10.130.0.1 255.255.255.255 65 | ! 66 | interface Ethernet0/0 67 | ip address 10.1.11.11 255.255.255.0 68 | ip summary-address eigrp 2 10.130.0.0 255.255.0.0 69 | ! 70 | interface Ethernet0/1 71 | ip address 10.1.12.11 255.255.255.0 72 | ip summary-address eigrp 2 10.130.0.0 255.255.0.0 73 | delay 1000 74 | ! 75 | interface Ethernet0/2 76 | no ip address 77 | shutdown 78 | ! 79 | interface Ethernet0/3 80 | no ip address 81 | shutdown 82 | ! 83 | interface Ethernet1/0 84 | no ip address 85 | shutdown 86 | ! 87 | interface Ethernet1/1 88 | no ip address 89 | shutdown 90 | ! 91 | interface Ethernet1/2 92 | no ip address 93 | shutdown 94 | ! 95 | interface Ethernet1/3 96 | no ip address 97 | shutdown 98 | ! 99 | ! 100 | router eigrp 2 101 | distribute-list prefix PL-SUMMARY out 102 | network 0.0.0.0 103 | passive-interface default 104 | no passive-interface Ethernet0/0 105 | no passive-interface Ethernet0/1 106 | ! 107 | ip forward-protocol nd 108 | ip prefix-list PL-SUMMARY seq 5 permit 10.130.0.0/16 109 | ! 110 | ! 111 | no ip http server 112 | no ip http secure-server 113 | ! 114 | ! 115 | ! 116 | ! 117 | control-plane 118 | ! 119 | ! 120 | ! 121 | ! 122 | ! 123 | ! 124 | ! 125 | ! 126 | line con 0 127 | logging synchronous 128 | line aux 0 129 | line vty 0 4 130 | login 131 | transport input none 132 | ! 133 | -------------------------------------------------------------------------------- /acme-large/config/FW.txt: -------------------------------------------------------------------------------- 1 | ! 2 | ! Last configuration change at 06:05:56 UTC Fri Feb 12 2016 3 | ! 4 | version 15.4 5 | service timestamps debug datetime msec 6 | service timestamps log datetime msec 7 | no service password-encryption 8 | ! 9 | hostname FW 10 | ! 11 | boot-start-marker 12 | boot-end-marker 13 | ! 14 | ! 15 | ! 16 | no aaa new-model 17 | mmi polling-interval 60 18 | no mmi auto-configure 19 | no mmi pvc 20 | mmi snmp-timeout 180 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | ! 28 | ! 29 | 30 | 31 | ! 32 | ! 33 | ! 34 | ! 35 | no ip domain lookup 36 | ip cef 37 | no ipv6 cef 38 | ! 39 | multilink bundle-name authenticated 40 | ! 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | ! 48 | ! 49 | redundancy 50 | ! 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | bridge irb 62 | ! 63 | ! 64 | ! 65 | ! 66 | interface Loopback0 67 | ip address 8.8.8.8 255.255.255.255 68 | ! 69 | interface Loopback1 70 | no ip address 71 | ! 72 | interface Loopback2 73 | no ip address 74 | ! 75 | interface Loopback100 76 | no ip address 77 | ! 78 | interface Ethernet0/0 79 | no ip address 80 | ! 81 | interface Ethernet0/0.40 82 | encapsulation dot1Q 40 83 | bridge-group 1 84 | ! 85 | interface Ethernet0/1 86 | no ip address 87 | ! 88 | interface Ethernet0/1.40 89 | encapsulation dot1Q 40 90 | bridge-group 1 91 | ! 92 | interface Ethernet0/2 93 | no ip address 94 | shutdown 95 | ! 96 | interface Ethernet0/3 97 | no ip address 98 | shutdown 99 | ! 100 | interface Ethernet1/0 101 | no ip address 102 | shutdown 103 | ! 104 | interface Ethernet1/1 105 | no ip address 106 | shutdown 107 | ! 108 | interface Ethernet1/2 109 | no ip address 110 | shutdown 111 | ! 112 | interface Ethernet1/3 113 | no ip address 114 | shutdown 115 | ! 116 | interface BVI1 117 | ip address 10.1.1.62 255.255.255.224 118 | ! 119 | ip forward-protocol nd 120 | ! 121 | ! 122 | no ip http server 123 | no ip http secure-server 124 | ip route 0.0.0.0 0.0.0.0 10.1.1.33 125 | ! 126 | ! 127 | ! 128 | ! 129 | control-plane 130 | ! 131 | bridge 1 protocol ieee 132 | bridge 1 route ip 133 | ! 134 | ! 135 | ! 136 | ! 137 | ! 138 | ! 139 | ! 140 | line con 0 141 | logging synchronous 142 | line aux 0 143 | line vty 0 4 144 | login 145 | transport input none 146 | ! 147 | ! 148 | -------------------------------------------------------------------------------- /acme-large/config/MPLS-DMVPN.txt: -------------------------------------------------------------------------------- 1 | ! 2 | version 15.4 3 | service timestamps debug datetime msec 4 | service timestamps log datetime msec 5 | no service password-encryption 6 | ! 7 | hostname MPLS-DMVPN 8 | ! 9 | boot-start-marker 10 | boot-end-marker 11 | ! 12 | ! 13 | ! 14 | no aaa new-model 15 | mmi polling-interval 60 16 | no mmi auto-configure 17 | no mmi pvc 18 | mmi snmp-timeout 180 19 | ! 20 | ! 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | 28 | 29 | ! 30 | ! 31 | ! 32 | ! 33 | no ip domain lookup 34 | ip cef 35 | no ipv6 cef 36 | ! 37 | multilink bundle-name authenticated 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | redundancy 48 | ! 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | ! 62 | ! 63 | interface Loopback0 64 | ip address 10.25.0.1 255.255.255.255 65 | ! 66 | interface Ethernet0/0 67 | ip address 172.16.0.18 255.255.255.252 68 | ! 69 | interface Ethernet0/1 70 | ip address 172.16.0.78 255.255.255.252 71 | ! 72 | interface Ethernet0/2 73 | ip address 10.1.11.12 255.255.255.0 74 | ip summary-address eigrp 2 10.25.0.0 255.255.0.0 75 | ! 76 | interface Ethernet0/3 77 | ip address 10.1.12.12 255.255.255.0 78 | ip summary-address eigrp 2 10.25.0.0 255.255.0.0 79 | delay 1000 80 | ! 81 | interface Ethernet1/0 82 | no ip address 83 | shutdown 84 | ! 85 | interface Ethernet1/1 86 | no ip address 87 | shutdown 88 | ! 89 | interface Ethernet1/2 90 | no ip address 91 | shutdown 92 | ! 93 | interface Ethernet1/3 94 | no ip address 95 | shutdown 96 | ! 97 | ! 98 | router eigrp 2 99 | distribute-list prefix PL-SUMMARY out 100 | network 10.1.11.12 0.0.0.0 101 | network 10.1.12.12 0.0.0.0 102 | network 10.25.0.1 0.0.0.0 103 | ! 104 | route-map AS-PREPEND permit 10 105 | set as-path prepend 65531 65531 65531 106 | ! 107 | router bgp 65532 108 | bgp log-neighbor-changes 109 | network 10.25.0.1 mask 255.255.255.255 110 | aggregate-address 10.25.0.0 255.255.0.0 summary-only 111 | neighbor 172.16.0.17 remote-as 65530 112 | neighbor 172.16.0.77 remote-as 65530 113 | neighbor 172.16.0.77 route-map AS-PREPEND out 114 | ! 115 | ip forward-protocol nd 116 | ip prefix-list PL-SUMMARY seq 5 permit 10.25.0.0/16 117 | ! 118 | ! 119 | no ip http server 120 | no ip http secure-server 121 | ! 122 | ! 123 | ! 124 | ! 125 | control-plane 126 | ! 127 | ! 128 | ! 129 | ! 130 | ! 131 | ! 132 | ! 133 | ! 134 | line con 0 135 | logging synchronous 136 | line aux 0 137 | line vty 0 4 138 | login 139 | transport input none 140 | ! 141 | -------------------------------------------------------------------------------- /acme-large/config/MPLS-ONLY.txt: -------------------------------------------------------------------------------- 1 | ! 2 | version 15.4 3 | service timestamps debug datetime msec 4 | service timestamps log datetime msec 5 | no service password-encryption 6 | ! 7 | hostname MPLS-ONLY 8 | ! 9 | boot-start-marker 10 | boot-end-marker 11 | ! 12 | ! 13 | ! 14 | no aaa new-model 15 | mmi polling-interval 60 16 | no mmi auto-configure 17 | no mmi pvc 18 | mmi snmp-timeout 180 19 | ! 20 | ! 21 | ! 22 | ! 23 | ! 24 | ! 25 | ! 26 | ! 27 | 28 | 29 | ! 30 | ! 31 | ! 32 | ! 33 | no ip domain lookup 34 | ip cef 35 | no ipv6 cef 36 | ! 37 | multilink bundle-name authenticated 38 | ! 39 | ! 40 | ! 41 | ! 42 | ! 43 | ! 44 | ! 45 | ! 46 | ! 47 | redundancy 48 | ! 49 | ! 50 | ! 51 | ! 52 | ! 53 | ! 54 | ! 55 | ! 56 | ! 57 | ! 58 | ! 59 | ! 60 | ! 61 | ! 62 | ! 63 | interface Loopback0 64 | ip address 10.199.0.1 255.255.255.255 65 | ! 66 | interface Ethernet0/0 67 | ip address 172.16.0.14 255.255.255.252 68 | ! 69 | interface Ethernet0/1 70 | ip address 172.16.0.74 255.255.255.252 71 | ! 72 | interface Ethernet0/2 73 | no ip address 74 | shutdown 75 | ! 76 | interface Ethernet0/3 77 | no ip address 78 | shutdown 79 | ! 80 | interface Ethernet1/0 81 | no ip address 82 | shutdown 83 | ! 84 | interface Ethernet1/1 85 | no ip address 86 | shutdown 87 | ! 88 | interface Ethernet1/2 89 | no ip address 90 | shutdown 91 | ! 92 | interface Ethernet1/3 93 | no ip address 94 | shutdown 95 | ! 96 | route-map AS-PREPEND permit 10 97 | set as-path prepend 65531 65531 65531 98 | ! 99 | router bgp 65531 100 | bgp log-neighbor-changes 101 | network 10.199.0.1 mask 255.255.255.255 102 | aggregate-address 10.199.0.0 255.255.192.0 summary-only 103 | neighbor 172.16.0.13 remote-as 65530 104 | neighbor 172.16.0.73 remote-as 65530 105 | neighbor 172.16.0.73 route-map AS-PREPEND out 106 | ! 107 | ip forward-protocol nd 108 | ! 109 | ! 110 | no ip http server 111 | no ip http secure-server 112 | ! 113 | ! 114 | ! 115 | ! 116 | control-plane 117 | ! 118 | ! 119 | ! 120 | ! 121 | ! 122 | ! 123 | ! 124 | ! 125 | line con 0 126 | logging synchronous 127 | line aux 0 128 | line vty 0 4 129 | login 130 | transport input none 131 | ! 132 | -------------------------------------------------------------------------------- /acme-large/config/Primary-CORE-SW.txt: -------------------------------------------------------------------------------- 1 | ! 2 | hostname Primary-CORE-SW 3 | ! 4 | ! 5 | spanning-tree mode rapid-pvst 6 | spanning-tree portfast bpduguard default 7 | ! 8 | vlan internal allocation policy ascending 9 | ! 10 | vlan 110 11 | name OSPF-WAN 12 | ! 13 | vlan 120 14 | name OSPF-DMVPN 15 | ! 16 | vlan 30 17 | name OSPF-CORE 18 | ! 19 | vlan 40 20 | name FW 21 | ! 22 | interface Loopback0 23 | ip address 10.1.2.2 255.255.255.255 24 | ! 25 | interface GigabitEthernet0/1 26 | description PRIMARY-WAN (Gi0/3) 27 | switchport trunk encapsulation dot1q 28 | switchport mode trunk 29 | spanning-tree portfast trunk 30 | ! 31 | interface GigabitEthernet0/2 32 | description PRIMARY-DMVPN (G0/3) 33 | switchport access vlan 120 34 | switchport mode access 35 | spanning-tree portfast 36 | ! 37 | interface GigabitEthernet0/3 38 | description FW 39 | switchport trunk encapsulation dot1q 40 | switchport mode trunk 41 | ! 42 | interface GigabitEthernet0/4 43 | description Cloud-1 44 | no switchport 45 | ip address 10.1.4.1 255.255.255.252 46 | ip ospf network point-to-point 47 | ip ospf hello-interval 5 48 | ip ospf dead-interval 15 49 | ! 50 | interface GigabitEthernet0/5 51 | description Cloud-2 52 | no switchport 53 | ip address 10.1.4.17 255.255.255.252 54 | ip ospf network point-to-point 55 | ip ospf hello-interval 5 56 | ip ospf dead-interval 15 57 | ! 58 | interface GigabitEthernet0/6 59 | switchport trunk allowed vlan 30,40 60 | switchport trunk encapsulation dot1q 61 | switchport mode trunk 62 | ! 63 | ! 64 | interface Vlan110 65 | ip address 10.1.1.2 255.255.255.252 66 | no ip redirects 67 | no ip proxy-arp 68 | ip ospf network point-to-point 69 | ip ospf hello-interval 5 70 | ip ospf dead-interval 15 71 | ! 72 | interface Vlan120 73 | ip address 10.1.1.6 255.255.255.252 74 | no ip redirects 75 | no ip proxy-arp 76 | ip ospf network point-to-point 77 | ip ospf hello-interval 5 78 | ip ospf dead-interval 15 79 | ! 80 | interface Vlan30 81 | ip address 10.1.1.9 255.255.255.252 82 | no ip redirects 83 | no ip proxy-arp 84 | ip ospf network point-to-point 85 | ip ospf hello-interval 5 86 | ip ospf dead-interval 15 87 | ! 88 | interface Vlan40 89 | description FW 90 | ip address 10.1.1.34 255.255.255.224 91 | no ip redirects 92 | no ip proxy-arp 93 | standby version 2 94 | standby 20 ip 10.1.1.33 95 | standby 20 priority 110 96 | standby 20 preempt 97 | ! 98 | router ospf 1 99 | network 0.0.0.0 255.255.255.255 area 0 100 | passive-interface default 101 | no passive-interface GigabitEthernet0/4 102 | no passive-interface GigabitEthernet0/5 103 | no passive-interface Vlan110 104 | no passive-interface Vlan120 105 | no passive-interface Vlan30 106 | ! 107 | ip route 0.0.0.0 0.0.0.0 10.1.1.62 108 | ! 109 | router bgp 65529 110 | template peer-policy RR-CLIENT 111 | route-reflector-client 112 | send-community both 113 | default-originate 114 | exit-peer-policy 115 | ! 116 | template peer-session iBGP 117 | remote-as 65529 118 | timers 5 15 119 | update-source Loopback0 120 | exit-peer-session 121 | ! 122 | bgp cluster-id 10.1.2.2 123 | bgp log-neighbor-changes 124 | neighbor 10.1.2.1 remote-as 65529 125 | neighbor 10.1.2.1 inherit peer-session iBGP 126 | neighbor 10.1.2.1 inherit peer-policy RR-CLIENT 127 | neighbor 10.1.2.7 remote-as 65529 128 | neighbor 10.1.2.7 inherit peer-session iBGP 129 | neighbor 10.1.2.7 inherit peer-policy RR-CLIENT 130 | neighbor 10.1.3.2 remote-as 65529 131 | neighbor 10.1.3.2 inherit peer-session iBGP 132 | neighbor 10.1.3.2 inherit peer-policy RR-CLIENT 133 | neighbor 10.4.0.1 remote-as 65529 134 | neighbor 10.4.0.1 inherit peer-session iBGP 135 | neighbor 10.4.0.1 inherit peer-policy RR-CLIENT 136 | neighbor 10.5.0.1 remote-as 65529 137 | neighbor 10.5.0.1 inherit peer-session iBGP 138 | neighbor 10.5.0.1 inherit peer-policy RR-CLIENT 139 | ! 140 | !workaround the IOU bug 141 | no ip cef 142 | ! -------------------------------------------------------------------------------- /acme-large/config/Primary-DMVPN.txt: -------------------------------------------------------------------------------- 1 | hostname Primary-DMVPN 2 | ! 3 | ! 4 | ! 5 | interface Loopback0 6 | ip address 10.1.2.7 255.255.255.255 7 | ! 8 | interface GigabitEthernet0/1 9 | description ** DMVPN ** 10 | ip address 10.1.11.1 255.255.255.0 11 | ! 12 | ! 13 | interface GigabitEthernet0/3 14 | ip address 10.1.1.5 255.255.255.252 15 | ip ospf network point-to-point 16 | ip ospf hello-interval 5 17 | ip ospf dead-interval 15 18 | ! 19 | ! 20 | router eigrp 2 21 | network 10.1.11.0 0.0.0.255 22 | default-metric 1 1 1 1 1 23 | redistribute bgp 65529 route-map RM-BGP-EIGRP 24 | passive-interface default 25 | no passive-interface GigabitEthernet0/1 26 | ! 27 | ! 28 | router ospf 1 29 | network 0.0.0.0 255.255.255.255 area 0 30 | passive-interface default 31 | no passive-interface GigabitEthernet0/3 32 | ! 33 | router bgp 65529 34 | neighbor 10.1.2.2 remote-as 65529 35 | neighbor 10.1.2.2 update-source Loopback0 36 | neighbor 10.1.2.2 send-community both 37 | redistribute eigrp 2 route-map RM-EIGRP-BGP 38 | bgp redistribute-internal 39 | ! 40 | ip community-list standard ICL-DC-DMVPN permit 65529:2 41 | route-map RM-EIGRP-BGP deny 10 42 | match tag 65529 43 | route-map RM-EIGRP-BGP permit 1000 44 | set community 65529:2 45 | set local-preference 125 46 | route-map RM-BGP-EIGRP deny 10 47 | match community ICL-DC-DMVPN 48 | route-map RM-BGP-EIGRP permit 1000 49 | set tag 65529 -------------------------------------------------------------------------------- /acme-large/config/Primary-MPLS.txt: -------------------------------------------------------------------------------- 1 | 2 | ! 3 | hostname Primary-MPLS 4 | ! 5 | ! 6 | interface GigabitEthernet0/0 7 | ip address 172.16.0.10 255.255.255.252 8 | ! 9 | interface GigabitEthernet0/1 10 | ip address 172.16.0.13 255.255.255.252 11 | ! 12 | interface GigabitEthernet0/2 13 | ip address 172.16.0.17 255.255.255.252 14 | ! 15 | ! 16 | router bgp 65530 17 | bgp log-neighbor-changes 18 | timers bgp 2 6 19 | neighbor 172.16.0.9 remote-as 65529 20 | neighbor 172.16.0.14 remote-as 65531 21 | neighbor 172.16.0.18 remote-as 65532 22 | ! 23 | -------------------------------------------------------------------------------- /acme-large/config/Primary-WAN.txt: -------------------------------------------------------------------------------- 1 | 2 | hostname PRIMARY-WAN 3 | ! 4 | interface Loopback0 5 | ip address 10.1.2.1 255.255.255.255 6 | ! 7 | interface GigabitEthernet0/0 8 | description MPLS-WAN 9 | ip address 172.16.0.9 255.255.255.252 10 | ! 11 | interface GigabitEthernet0/3 12 | description Primary-CORE-SW (G0/1) 13 | ! 14 | interface GigabitEthernet0/3.110 15 | encapsulation dot1Q 110 16 | ip address 10.1.1.1 255.255.255.252 17 | ip ospf network point-to-point 18 | ip ospf hello-interval 5 19 | ip ospf dead-interval 15 20 | ! 21 | ! 22 | router ospf 1 23 | networ 0.0.0.0 255.255.255.255 area 0 24 | passive-interface default 25 | no passive-interface GigabitEthernet0/3.110 26 | ! 27 | router bgp 65529 28 | neighbor 172.16.0.10 remote-as 65530 29 | neighbor 172.16.0.10 route-map RM-FIRST-IN in 30 | neighbor 10.1.2.2 remote-as 65529 31 | neighbor 10.1.2.2 update-source Loopback0 32 | neighbor 10.1.2.2 send-community both 33 | ! 34 | route-map RM-FIRST-IN permit 10 35 | set local-preference 200 -------------------------------------------------------------------------------- /acme-large/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-large/network/__init__.py -------------------------------------------------------------------------------- /acme-large/network/acme-large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-large/network/acme-large.jpg -------------------------------------------------------------------------------- /acme-large/network/ospf-bgp.txt: -------------------------------------------------------------------------------- 1 | ==================================================== 2 | BGP PEERINGS 3 | ==================================================== 4 | 5 | --- PRIMARY-WAN, PRIMARY-DMVPN ------ 6 | router bgp 65529 7 | neighbor 10.1.2.2 remote-as 65529 8 | neighbor 10.1.2.2 update-source Loopback0 9 | neighbor 10.1.2.2 send-community both 10 | 11 | 12 | --- CLOUD-1, CLOUD-2 ------- 13 | router bgp 65529 14 | bgp log-neighbor-changes 15 | neighbor 10.1.2.2 remote-as 65529 16 | neighbor 10.1.2.2 update-source Loopback0 17 | neighbor 10.1.2.2 send-community both 18 | neighbor 10.1.3.2 remote-as 65529 19 | neighbor 10.1.3.2 update-source Loopback0 20 | neighbor 10.1.3.2 send-community both 21 | 22 | 23 | --- Backup-WAN, Backup-DMVPN ----------- 24 | router bgp 65529 25 | bgp log-neighbor-changes 26 | neighbor 10.1.3.2 remote-as 65529 27 | neighbor 10.1.3.2 update-source Loopback0 28 | neighbor 10.1.3.2 send-community both 29 | 30 | 31 | --- PRIMARY-CORE ------- 32 | router bgp 65529 33 | template peer-policy RR-CLIENT 34 | route-reflector-client 35 | send-community both 36 | exit-peer-policy 37 | ! 38 | template peer-session iBGP 39 | remote-as 65529 40 | timers 5 15 41 | update-source Loopback0 42 | exit-peer-session 43 | ! 44 | bgp cluster-id 10.1.2.2 45 | bgp log-neighbor-changes 46 | neighbor 10.1.2.1 remote-as 65529 47 | neighbor 10.1.2.1 inherit peer-session iBGP 48 | neighbor 10.1.2.1 inherit peer-policy RR-CLIENT 49 | neighbor 10.1.2.7 remote-as 65529 50 | neighbor 10.1.2.7 inherit peer-session iBGP 51 | neighbor 10.1.2.7 inherit peer-policy RR-CLIENT 52 | neighbor 10.1.3.2 remote-as 65529 53 | neighbor 10.1.3.2 inherit peer-session iBGP 54 | neighbor 10.1.3.2 inherit peer-policy RR-CLIENT 55 | neighbor 10.4.0.1 remote-as 65529 56 | neighbor 10.4.0.1 inherit peer-session iBGP 57 | neighbor 10.4.0.1 inherit peer-policy RR-CLIENT 58 | neighbor 10.5.0.1 remote-as 65529 59 | neighbor 10.5.0.1 inherit peer-session iBGP 60 | neighbor 10.5.0.1 inherit peer-policy RR-CLIENT 61 | 62 | 63 | ---- Backup-CORE -------- 64 | router bgp 65529 65 | template peer-policy RR-CLIENT 66 | route-reflector-client 67 | send-community both 68 | exit-peer-policy 69 | ! 70 | template peer-session iBGP 71 | remote-as 65529 72 | timers 5 15 73 | update-source Loopback0 74 | exit-peer-session 75 | ! 76 | bgp cluster-id 10.1.3.2 77 | bgp log-neighbor-changes 78 | neighbor 10.1.2.2 remote-as 65529 79 | neighbor 10.1.2.2 inherit peer-session iBGP 80 | neighbor 10.1.2.2 inherit peer-policy RR-CLIENT 81 | neighbor 10.1.3.1 remote-as 65529 82 | neighbor 10.1.3.1 inherit peer-session iBGP 83 | neighbor 10.1.3.1 inherit peer-policy RR-CLIENT 84 | neighbor 10.1.3.7 remote-as 65529 85 | neighbor 10.1.3.7 inherit peer-session iBGP 86 | neighbor 10.1.3.7 inherit peer-policy RR-CLIENT 87 | neighbor 10.4.0.1 remote-as 65529 88 | neighbor 10.4.0.1 inherit peer-session iBGP 89 | neighbor 10.4.0.1 inherit peer-policy RR-CLIENT 90 | neighbor 10.5.0.1 remote-as 65529 91 | neighbor 10.5.0.1 inherit peer-session iBGP 92 | neighbor 10.5.0.1 inherit peer-policy RR-CLIENT 93 | 94 | 95 | ==================================================== 96 | ROUTE INJECTION 97 | ==================================================== 98 | --- PRIMARY-WAN, BACKUP-WAN ------ 99 | router bgp 65529 100 | no network 0.0.0.0 101 | no redistribute ospf 1 102 | router ospf 1 103 | no redistribute bgp 65529 subnets 104 | 105 | --- PRIMARY-DMVPN, BACKUP-DMVPN ------ 106 | ip community-list standard ICL-DC-DMVPN permit 65529:2 107 | route-map RM-EIGRP-BGP deny 10 108 | match tag 65529 109 | route-map RM-EIGRP-BGP permit 1000 110 | set community 65529:2 111 | route-map RM-BGP-EIGRP deny 10 112 | match community ICL-DC-DMVPN 113 | route-map RM-BGP-EIGRP permit 1000 114 | set tag 65529 115 | router ospf 1 116 | no redistribute eigrp 2 117 | router eigrp 2 118 | no redistribute ospf 1 119 | redistribute bgp 65529 route-map RM-BGP-EIGRP 120 | router bgp 65529 121 | redistribute eigrp 2 route-map RM-EIGRP-BGP 122 | bgp redistribute-internal 123 | 124 | --- CLOUD-1 -------- 125 | router bgp 65529 126 | network 10.4.0.0 mask 255.255.0.0 127 | 128 | --- CLOUD-2 -------- 129 | router bgp 65529 130 | network 10.5.0.0 mask 255.255.0.0 131 | 132 | --- PRIMARY-CORE, BACKUP-CORE --------- 133 | router bgp 65529 134 | template peer-policy RR-CLIENT 135 | default-originate 136 | router ospf 1 137 | no default-information originate 138 | 139 | ==================================================== 140 | TRAFFIC ENGINEERING POLICIES 141 | ==================================================== 142 | 143 | --- PRIMARY-WAN ------- 144 | route-map RM-FIRST-IN permit 10 145 | set local-preference 200 146 | router bgp 65529 147 | neighbor 172.16.0.10 route-map RM-FIRST-IN in 148 | 149 | --- BACKUP-WAN ------- 150 | route-map RM-SECOND-IN permit 10 151 | set local-preference 150 152 | router bgp 65529 153 | neighbor 172.16.0.69 route-map RM-SECOND-IN in 154 | 155 | --- PRIMARY-DMVPN ------- 156 | route-map RM-EIGRP-BGP permit 1000 157 | set local-preference 125 158 | 159 | -------------------------------------------------------------------------------- /acme-large/network/tests/ping_flows.txt: -------------------------------------------------------------------------------- 1 | From FW to MPLS-ONLY 2 | From FW to MPLS-DMVPN 3 | From FW to Cloud-1 4 | From FW to Cloud-2 5 | From FW to DMVPN-ONLY -------------------------------------------------------------------------------- /acme-large/network/tests/traffic_flows.txt: -------------------------------------------------------------------------------- 1 | ## Normal operations 2 | 1 Failed None 3 | From FW to MPLS-DMVPN via Primary-WAN, Primary-MPLS 4 | From FW to DMVPN-ONLY via Primary-CORE-SW, Primary-DMVPN 5 | From FW to MPLS-ONLY via Primary-WAN, Primary-MPLS 6 | From Cloud-1 to FW Loopback0 via Primary-CORE-SW 7 | From Cloud-2 to MPLS-DMVPN via Primary-WAN, Primary-MPLS 8 | 9 | # Failed link between to MPLS in Primary DC 10 | 2 Failed Primary-WAN Gig0/0, Primary-MPLS Gig0/0 11 | From FW to MPLS-DMVPN via Backup-WAN, Backup-MPLS 12 | From FW to DMVPN-ONLY via Primary-CORE-SW, Primary-DMVPN 13 | From FW to MPLS-ONLY via Backup-WAN, Backup-MPLS 14 | From Cloud-1 to FW Loopback0 via Primary-CORE-SW 15 | From Cloud-1 to MPLS-DMVPN via Backup-WAN, Backup-MPLS 16 | 17 | # Failed Primary Core Switch 18 | 3 Failed Primary-CORE-SW G0/1, Primary-CORE-SW G0/2, Primary-CORE-SW G0/3, Primary-CORE-SW G0/4, Primary-CORE-SW G0/5, Primary-CORE-SW G0/6 19 | From FW to MPLS-DMVPN via not Primary-CORE-SW, Backup-WAN, Backup-MPLS 20 | From FW to DMVPN-ONLY via not Primary-CORE-SW, Backup-DMVPN 21 | From FW to MPLS-ONLY via not Primary-CORE-SW, Backup-WAN, Backup-MPLS 22 | From Cloud-1 to FW Loopback0 via not Primary-CORE-SW 23 | From Cloud-1 to MPLS-DMVPN via Backup-CORE-SW, Backup-WAN, Backup-MPLS 24 | 25 | 26 | -------------------------------------------------------------------------------- /acme-large/network/topology.py: -------------------------------------------------------------------------------- 1 | real = ((('Primary-WAN', 'GigabitEthernet0/3'), ('Primary-CORE-SW', 'GigabitEthernet0/1')), 2 | (('Primary-CORE-SW', 'GigabitEthernet0/2'), ('Primary-DMVPN', 'GigabitEthernet0/3')), 3 | (('Primary-CORE-SW', 'GigabitEthernet0/6'), ('Backup-CORE-SW', 'GigabitEthernet0/6')), 4 | (('Backup-WAN', 'GigabitEthernet0/3'), ('Backup-CORE-SW', 'GigabitEthernet0/1')), 5 | (('Backup-CORE-SW', 'GigabitEthernet0/2'), ('Backup-DMVPN', 'GigabitEthernet0/3')), 6 | (('Primary-MPLS', 'GigabitEthernet0/0'), ('Primary-WAN', 'GigabitEthernet0/0')), 7 | (('Primary-MPLS', 'GigabitEthernet0/1'), ('MPLS-ONLY', 'Ethernet0/0')), 8 | (('Primary-MPLS', 'GigabitEthernet0/2'), ('MPLS-DMVPN', 'Ethernet0/0')), 9 | (('Backup-MPLS', 'GigabitEthernet0/0'), ('Backup-WAN', 'GigabitEthernet0/0')), 10 | (('Backup-MPLS', 'GigabitEthernet0/1'), ('MPLS-ONLY', 'Ethernet0/1')), 11 | (('Backup-MPLS', 'GigabitEthernet0/2'), ('MPLS-DMVPN', 'Ethernet0/1')), 12 | (('Primary-DMVPN', 'GigabitEthernet0/1'), ('MPLS-DMVPN', 'Ethernet0/2')), 13 | (('Primary-DMVPN', 'GigabitEthernet0/1'), ('DMVPN-ONLY', 'Ethernet0/0')), 14 | (('Backup-DMVPN', 'GigabitEthernet0/1'), ('MPLS-DMVPN', 'Ethernet0/3')), 15 | (('Backup-DMVPN', 'GigabitEthernet0/1'), ('DMVPN-ONLY', 'Ethernet0/1')), 16 | (('Cloud-1', 'Ethernet0/0'), ('Primary-CORE-SW', 'GigabitEthernet0/4')), 17 | (('Cloud-1', 'Ethernet0/1'), ('Backup-CORE-SW', 'GigabitEthernet0/5')), 18 | (('Cloud-2', 'Ethernet0/0'), ('Primary-CORE-SW', 'GigabitEthernet0/5')), 19 | (('Cloud-2', 'Ethernet0/1'), ('Backup-CORE-SW', 'GigabitEthernet0/4')), 20 | (('FW', 'Ethernet0/0'), ('Primary-CORE-SW', 'GigabitEthernet0/3')), 21 | (('FW', 'Ethernet0/1'), ('Backup-CORE-SW', 'GigabitEthernet0/3'))) 22 | -------------------------------------------------------------------------------- /acme-large/network/unetlab.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ip: 192.168.91.139 3 | user: admin 4 | pwd: unl 5 | lab_name: acme-large 6 | -------------------------------------------------------------------------------- /acme-large/requirements.txt: -------------------------------------------------------------------------------- 1 | restunlclient 2 | pyyaml 3 | -------------------------------------------------------------------------------- /acme-large/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /acme-large/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-large/tools/__init__.py -------------------------------------------------------------------------------- /acme-large/tools/conf_analyzer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import file_io 3 | from globals import * 4 | 5 | 6 | class ConfAnalyzer(object): 7 | 8 | @staticmethod 9 | def convert_intf(text, intf_conv): 10 | new_text = [] 11 | for line in text.splitlines(): 12 | for old_intf, new_intf in intf_conv.iteritems(): 13 | if re.match(r'^.*{}({})?$'.format(old_intf, INTF_DOT1Q_REGEX), line): 14 | line = line.replace(old_intf, new_intf) 15 | if line.startswith('interface '): 16 | line += '\r no shut' 17 | new_text.append(line) 18 | return '\r'.join(new_text) 19 | 20 | @staticmethod 21 | def cleanup(text): 22 | new_text = [] 23 | ignore_lines = False 24 | for line in text.splitlines(): 25 | if any([line.startswith(ignore) for ignore in IGNORE_CONFIG]): 26 | ignore_lines = True 27 | elif '!' in line: 28 | new_text.append(line) 29 | ignore_lines = False 30 | elif not ignore_lines: 31 | new_text.append(line) 32 | return '\r'.join(new_text) 33 | 34 | def normalize(self, intf_conv): 35 | for f in os.listdir(CONF_DIR): 36 | if f.endswith('.txt'): 37 | file_name = os.path.splitext(f)[0] 38 | file_text = file_io.read_txt(CONF_DIR + '/' + f) 39 | converted_intf = self.convert_intf(file_text, intf_conv.get(file_name, {})) 40 | updated_text = self.cleanup(converted_intf) 41 | new_filename = os.path.join(TMP_DIR, file_name + '.txt') 42 | file_io.write_txt(new_filename, updated_text) 43 | return 44 | 45 | @staticmethod 46 | def get_ips(text): 47 | result = {} 48 | ip_list, intf_name = [], None 49 | for line in text.splitlines(): 50 | if re.search(r'^interface (.*)$', line): 51 | intf_name = re.search(r'^interface (.*)$', line).group(1) 52 | elif intf_name and re.search(r'{}'.format(IP_REGEX), line): 53 | ip = re.search(r'{}'.format(IP_REGEX), line).group(1) 54 | ip_list.append(ip) 55 | elif intf_name and re.search(r'shut', line): 56 | ip_list = [] 57 | elif intf_name and re.search(r'!', line): 58 | if ip_list: 59 | result[intf_name] = ip_list 60 | ip_list, intf_name = [], None 61 | return result 62 | 63 | def extract_ip(self): 64 | result = {} 65 | cwd = os.path.join(TMP_DIR, CONF_DIR) 66 | for f in os.listdir(cwd): 67 | if f.endswith('.txt'): 68 | dev_name = os.path.splitext(f)[0] 69 | file_text = file_io.read_txt(cwd + '/' + f) 70 | ips = self.get_ips(file_text) 71 | result[dev_name] = ips 72 | file_io.write_yaml(TMP_DIR + '/' + 'ip.yml', result) 73 | return result 74 | 75 | 76 | def main(): 77 | pass 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /acme-large/tools/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | 4 | 5 | def timer(func): 6 | def timed(*args, **kwargs): 7 | start = time.time() 8 | result = func(*args, **kwargs) 9 | stop = time.time() 10 | total_time = stop-start 11 | print('\rTotal time: %d minutes, %d seconds' % (total_time/60, total_time % 60)) 12 | return result 13 | return timed 14 | 15 | 16 | def progress(function): 17 | import threading 18 | 19 | def inner(*args, **kwargs): 20 | run = True 21 | # if running in Jenkins do not decorate 22 | if os.environ.get('UNL_IP'): 23 | return function(*args, **kwargs) 24 | 25 | def print_progress(index=0): 26 | while run: 27 | print('\r' + 'Progress' + '.' * index), 28 | index = 0 if index > 2 else index + 1 29 | time.sleep(0.5) 30 | 31 | process_print = threading.Thread(target=print_progress) 32 | process_print.start() 33 | try: 34 | return function(*args, **kwargs) 35 | finally: 36 | run = False 37 | return inner 38 | 39 | 40 | @timer 41 | @progress 42 | def main(): 43 | for x in xrange(3): 44 | time.sleep(1) 45 | print('Done') 46 | return 42 47 | 48 | if __name__ == '__main__': 49 | print("Computing, please wait") 50 | main() 51 | 52 | -------------------------------------------------------------------------------- /acme-large/tools/dns.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | 4 | 5 | class DNS(object): 6 | 7 | @staticmethod 8 | def reverse_dict(original): 9 | result = {} 10 | for device, intf_ips in original.iteritems(): 11 | for intf, ips in intf_ips.iteritems(): 12 | for ip in ips: 13 | result[ip] = (device, intf) 14 | return result 15 | 16 | def __init__(self, host_to_ips): 17 | self.ip_re = re.compile(IP_REGEX) 18 | self.host_to_ips = host_to_ips 19 | self.ips_to_host = self.reverse_dict(self.host_to_ips) 20 | 21 | def is_ip(self, text): 22 | return self.ip_re.search(text) 23 | 24 | def get(self, text, intf='Loopback0'): 25 | if self.is_ip(text): 26 | return self.ips_to_host.get(text, (text, '')) 27 | else: 28 | return self.host_to_ips.get(text, {}).get(intf, ['127.0.0.1'])[0] 29 | -------------------------------------------------------------------------------- /acme-large/tools/endpoint.py: -------------------------------------------------------------------------------- 1 | from restunl.device import IOL 2 | from globals import * 3 | from dns import DNS 4 | import file_io 5 | import re 6 | 7 | 8 | DEFAULT_INTF = 'Loopback0' 9 | DNS_RESOLVER = DNS(file_io.read_yaml('{}/ip.yml'.format(TMP_DIR))) 10 | 11 | 12 | class IncorrectEndpointFormat(Exception): 13 | pass 14 | 15 | 16 | class Endpoint(object): 17 | 18 | def __init__(self, text): 19 | self.dev, self.intf, self.ip = '', '', '' 20 | self.node = None 21 | if DNS_RESOLVER.is_ip(text): 22 | self.dev, self.intf = DNS_RESOLVER.get(text) 23 | self.ip = text 24 | else: 25 | self.dev, self.intf = self._parse(text) 26 | self.ip = DNS_RESOLVER.get(self.dev, self.intf) 27 | 28 | @staticmethod 29 | def _parse(text): 30 | separators = ',|\s' 31 | seq = [word.strip() for word in re.split(separators, text)] 32 | if len(seq) > 1: 33 | return seq[0], Endpoint.expand(seq[1]) 34 | else: 35 | return seq[0], DEFAULT_INTF 36 | 37 | @staticmethod 38 | def expand(intf): 39 | intf_re = re.match('([a-zA-Z]+)(\d+(/\d+)*)', intf) 40 | try: 41 | intf_name = intf_re.group(1) 42 | intf_number = intf_re.group(2) 43 | except IndexError: 44 | raise IncorrectEndpointFormat('Could not split interface into name and number: {}'.format(intf)) 45 | if intf_name.startswith('G'): 46 | intf_name = 'GigabitEthernet' 47 | elif intf_name.startswith('T'): 48 | intf_name = 'TenGigabitEthernet' 49 | elif intf_name.startswith('E'): 50 | intf_name = 'Ethernet' 51 | elif intf_name.startswith('L'): 52 | intf_name = 'Loopback' 53 | elif intf_name.startswith('V'): 54 | intf_name = 'Vlan' 55 | else: 56 | raise IncorrectEndpointFormat('Could not expand interface name: {}'.format(intf)) 57 | return intf_name + intf_number 58 | 59 | def __str__(self): 60 | return self.dev + '(' + self.intf + ')' 61 | 62 | def get_node(self): 63 | return self.node 64 | 65 | 66 | class ActiveEndpoint(Endpoint): 67 | 68 | def __init__(self, text, lab): 69 | super(ActiveEndpoint, self).__init__(text) 70 | if not self.dev == 'None': 71 | self.node = self._set_node(lab) 72 | else: 73 | self.node = None 74 | 75 | def _set_node(self, lab): 76 | return lab.get_node(IOL(self.dev)) 77 | 78 | -------------------------------------------------------------------------------- /acme-large/tools/file_io.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def read_txt(filename): 5 | try: 6 | with open(filename) as stream: 7 | return ''.join(stream.readlines()) 8 | except IOError: 9 | return '' 10 | 11 | 12 | def read_yaml(filename): 13 | try: 14 | with open(filename, 'r') as stream: 15 | yml = yaml.load(stream) 16 | return yml 17 | except IOError: 18 | return {} 19 | 20 | 21 | def write_txt(filename, text): 22 | with open(filename, 'w+') as f: 23 | f.write(text) 24 | return None 25 | 26 | 27 | def write_yaml(filename, data): 28 | write_txt(filename, yaml.dump(data, default_flow_style=False)) 29 | return None 30 | 31 | -------------------------------------------------------------------------------- /acme-large/tools/globals.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | L3_IMAGE = 'L3-LATEST.bin' 4 | L2_IMAGE = 'L2-LATEST.bin' 5 | NET = 'network' 6 | TEST = 'tests' 7 | CONF = 'config' 8 | TMP = 'tmp' 9 | NET_DIR = os.path.abspath(NET) 10 | CONF_DIR = os.path.abspath(CONF) 11 | TMP_DIR = os.path.abspath(TMP) 12 | TEST_DIR = os.path.abspath(os.path.join(NET, TEST)) 13 | INTF_CONV_FILE = os.path.join(TMP_DIR, 'intf_conv' + '.yml') 14 | IP_REGEX = '([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' 15 | INTF_DOT1Q_REGEX = '\.[0-9]{1,3}' 16 | IGNORE_CONFIG = ['class-map', 'policy-map', 'service', 'aaa', 'flow', 'ip nbar', 'logging', 17 | 'snmp', 'banner', 'ntp', 'event', 'ip wccp', 'boot', 'archive', 'privilege', 'tacacs', 18 | 'ip domain', 'ip ftp', 'ip http', 'ip sla', 'track', 'version', '! '] 19 | 20 | 21 | def main(): 22 | print NET_DIR 23 | print CONF_DIR 24 | print TMP_DIR 25 | print TEST_DIR 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /acme-large/tools/ping.py: -------------------------------------------------------------------------------- 1 | import re 2 | from endpoint import ActiveEndpoint 3 | 4 | 5 | class Ping(object): 6 | 7 | def print_results(self, result=''): 8 | intro = '\rFailed connectivity : ' 9 | result = ', '.join(from_node + ' -> ' + ', '.join(to_nodes) 10 | for from_node, to_nodes in self.failed.iteritems() if to_nodes) 11 | print intro + result, 12 | 13 | def __init__(self, scenarios, lab): 14 | self.failed = dict() 15 | self.lab = lab 16 | self.scenarios = self._parse_scenarios(scenarios, self.lab) 17 | 18 | def run(self, run): 19 | enable = 'enable\r' 20 | while all(run): 21 | for from_point in self.scenarios: 22 | command = enable 23 | for to_point in self.scenarios[from_point]: 24 | command += 'ping {} {} repeat 2'.format(to_point.ip, 'source ' + from_point.ip) 25 | self._do_ping(command, from_point, to_point) 26 | self.print_results() 27 | 28 | def _do_ping(self, command, from_point, to_point): 29 | from_node = from_point.get_node() 30 | result = from_node.configure(command) 31 | percentage = self._parse_result(result) 32 | if not int(percentage) > 0: 33 | self.failed.setdefault(str(from_point), set([])).add(str(to_point)) 34 | else: 35 | self.failed.setdefault(str(from_point), set([])).discard(str(to_point)) 36 | 37 | 38 | @staticmethod 39 | def _parse_result(output): 40 | for line in output.splitlines(): 41 | match_percent = re.match(r'Success rate is (\d+) percent', line) 42 | if match_percent: 43 | return match_percent.group(1) 44 | return '100' 45 | 46 | @staticmethod 47 | def _parse_scenarios(scenarios, lab): 48 | result = {} 49 | for line in scenarios.splitlines(): 50 | try: 51 | _, flow = line.split('From ') 52 | from_, to_ = flow.split(' to ') 53 | from_point = ActiveEndpoint(from_, lab) 54 | to_point = ActiveEndpoint(to_, lab) 55 | result.setdefault(from_point, []).append(to_point) 56 | except: 57 | print('*** Failed to parse scenario {}'.format(line)) 58 | raise 59 | return result 60 | 61 | 62 | def main(): 63 | pass 64 | 65 | if __name__ == '__main__': 66 | main() 67 | 68 | 69 | -------------------------------------------------------------------------------- /acme-large/tools/testflows.py: -------------------------------------------------------------------------------- 1 | from endpoint import ActiveEndpoint 2 | 3 | 4 | class TestFlows(object): 5 | 6 | def __init__(self, flows): 7 | self.flows = flows 8 | self.parsed_flows = dict() 9 | 10 | @staticmethod 11 | def split_strip(text, separator): 12 | return [word.strip() for word in text.split(separator)] 13 | 14 | def parse(self, lab): 15 | flows = [line for line in self.flows.split('\n') if line and not line.strip().startswith('#')] 16 | for line in flows: 17 | if 'Failed' in line and line[0].isdigit(): 18 | scenario = dict() 19 | seq, actions = self.split_strip(line, 'Failed') 20 | parsed_action = [ActiveEndpoint(word, lab) for word in self.split_strip(actions, ',')] 21 | flow_key = (seq, tuple(parsed_action)) 22 | self.parsed_flows[flow_key] = scenario 23 | elif all([token in line for token in ['From', 'to', 'via']]): 24 | _, rule = self.split_strip(line, 'From') 25 | from_device, to_via = self.split_strip(rule, 'to') 26 | from_point = ActiveEndpoint(from_device, lab) 27 | to_device, flow = self.split_strip(to_via, 'via') 28 | to_point = ActiveEndpoint(to_device, lab) 29 | flow_and = self.split_strip(flow, ',') 30 | flow_and_or = [['OR', self.split_strip(word, 'or')] if 'or' in word else word for word in flow_and] 31 | flow_and_or_not = [word.split() if 'not' in word else word for word in flow_and_or] 32 | flow_final = [['AND', word] if type(word) is str else word for word in flow_and_or_not] 33 | scenario_key = (from_point, to_point) 34 | scenario[scenario_key] = {'text': flow, 'parsed': flow_final} 35 | else: 36 | raise Exception('Incorrect traffic flow syntax in {}'.format(line)) 37 | return self.parsed_flows 38 | 39 | def print_flow(self, number, from_device, to_device): 40 | print('From: {}, to: {}, via: {}'.format(from_device, to_device, 41 | self.parsed_flows[number][from_device][to_device])) 42 | 43 | 44 | def main(): 45 | pass 46 | 47 | if __name__ == '__main__': 48 | main() 49 | 50 | -------------------------------------------------------------------------------- /acme-large/tools/traceroute.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | from endpoint import Endpoint 4 | 5 | 6 | class Traceroute(object): 7 | def __init__(self, output): 8 | self.trace = output 9 | self.parsed_result = dict() 10 | self.result_list = [] 11 | self.trace_re = re.compile(r'(\d+)?\s+({ip}|\*)+'.format(ip=IP_REGEX)) 12 | 13 | def parse(self): 14 | self.trace = [line for line in self.trace.split('\n') if line] 15 | seq, ip = None, None 16 | for line in self.trace: 17 | try: 18 | trace_match = self.trace_re.search(line) 19 | if trace_match: 20 | ip = trace_match.group(3) 21 | if trace_match.group(1): 22 | seq = trace_match.group(1) 23 | if seq and ip: 24 | self.parsed_result.setdefault(int(seq), []).append(Endpoint(ip)) 25 | except AttributeError: 26 | print('Cannot parse traceroute output {}'.format(line)) 27 | raise 28 | return self.parsed_result 29 | 30 | def resolve(self): 31 | for key in sorted(self.parsed_result.keys()): 32 | self.result_list.append([hop.dev for hop in self.parsed_result[key]]) 33 | return self.result_list 34 | 35 | def as_string(self, from_node=[]): 36 | return '->'.join('|'.join([dev for dev in set(el)]) for el in [from_node] + self.result_list) 37 | 38 | def verify(self, flow): 39 | index = 0 40 | for step in self.result_list: 41 | if index < len(flow): 42 | index += self.bool_compare(step, flow[index]) 43 | rc = 1 if len(flow[index:]) > 0 else 0 44 | return rc 45 | 46 | @staticmethod 47 | def bool_compare(hops, rule): 48 | op, device = rule 49 | # print("COMPARING TRACE {} with RULE {}".format(hops, rule)) 50 | if op.upper() == 'NOT': 51 | return 1 if device not in hops else 0 52 | elif op.upper() == 'OR': 53 | return 1 if all([hop in device for hop in hops]) else 0 54 | else: 55 | return 1 if all([hop in device for hop in set(hops)]) else 0 56 | return 0 57 | 58 | 59 | def main(): 60 | line1 = ''' 61 | Type escape sequence to abort. 62 | Tracing the route to 10.0.0.3 63 | VRF info: (vrf in name/id, vrf out name/id) 64 | 1 * * 65 | 12.12.12.2 1 msec 66 | 2 23.23.23.3 0 msec 67 | 34.34.34.3 1 msec * 68 | ''' 69 | line2 = ''' 70 | Type escape sequence to abort. 71 | Tracing the route to 10.0.0.3 72 | VRF info: (vrf in name/id, vrf out name/id) 73 | 1 14.14.14.4 0 msec 74 | 12.12.12.2 1 msec 75 | 14.14.14.4 0 msec 76 | 2 23.23.23.3 0 msec 77 | 34.34.34.3 1 msec * 78 | ''' 79 | line3 = ''' 80 | Type escape sequence to abort. 81 | Tracing the route to 10.0.0.3 82 | VRF info: (vrf in name/id, vrf out name/id) 83 | 1 * * * 84 | 2 23.23.23.3 0 msec 85 | 34.34.34.3 1 msec * 86 | ''' 87 | print Traceroute(line1).parse() 88 | print Traceroute(line2).parse() 89 | print Traceroute(line3).parse() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() -------------------------------------------------------------------------------- /acme-large/tools/unetlab.py: -------------------------------------------------------------------------------- 1 | from restunl.unetlab import UnlServer 2 | from restunl.device import Router, Switch 3 | from globals import * 4 | import file_io 5 | import decorators 6 | import os 7 | 8 | 9 | class UNetLab(object): 10 | 11 | def __init__(self, ip='', user='', pwd='', lab_name=''): 12 | self.ip, self.user, self.pwd, self.lab_name = ip, user, pwd, lab_name 13 | if os.environ.get('UNL_IP'): 14 | self.ip = os.environ.get('UNL_IP') 15 | self.unl = UnlServer(self.ip) 16 | self.unl.login(self.user, self.pwd) 17 | self.lab = None 18 | self.nodes = dict() 19 | 20 | def create_lab(self): 21 | self.lab = self.unl.create_lab(self.lab_name) 22 | self.lab.cleanup() 23 | 24 | def get_lab(self): 25 | return self.unl.get_lab(self.lab_name) 26 | 27 | def build_topo(self, topology): 28 | real_topo = topology.real 29 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 30 | for ((a_name, a_intf), (b_name, b_intf)) in real_topo: 31 | a_device = Switch(a_name, L2_IMAGE) if 'sw' in a_name.lower() else Router(a_name, L3_IMAGE) 32 | b_device = Switch(b_name, L2_IMAGE) if 'sw' in b_name.lower() else Router(b_name, L3_IMAGE) 33 | if a_name not in self.nodes: 34 | self.nodes[a_name] = self.lab.create_node(a_device) 35 | # print("*** NODE {} CREATED".format(a_name)) 36 | if b_name not in self.nodes: 37 | self.nodes[b_name] = self.lab.create_node(b_device) 38 | # print("*** NODE {} CREATED".format(b_name)) 39 | node_a = self.nodes[a_name] 40 | node_b = self.nodes[b_name] 41 | if intf_conv.get(a_name, {}).get(a_intf, None): 42 | a_intf_lab = intf_conv[a_name][a_intf] 43 | else: 44 | a_intf_lab = node_a.get_next_intf() 45 | if intf_conv.get(b_name, {}).get(b_intf, None): 46 | b_intf_lab = intf_conv[b_name][b_intf] 47 | else: 48 | b_intf_lab = node_b.get_next_intf() 49 | intf_conv.setdefault(a_name, {})[a_intf] = a_intf_lab 50 | intf_conv.setdefault(b_name, {})[b_intf] = b_intf_lab 51 | node_a.connect_node(a_intf_lab, node_b, b_intf_lab) 52 | # print("*** NODES {} and {} ARE CONNECTED".format(a_name, b_name)) 53 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 54 | return None 55 | 56 | def ext_connect(self, topo): 57 | ext_topo = topo.ext_net 58 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 59 | for (node_name, node_intf), pnet in ext_topo.iteritems(): 60 | ext_net = self.lab.create_net('cloud', net_type=pnet) 61 | the_node = self.nodes[node_name] 62 | node_intf_lab = the_node.get_next_intf() 63 | the_node.connect_interface(node_intf_lab, ext_net) 64 | intf_conv.setdefault(node_name, {})[node_intf] = node_intf_lab 65 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 66 | return None 67 | 68 | @decorators.timer 69 | @decorators.progress 70 | def configure_nodes(self, path): 71 | import threading 72 | processes = [] 73 | for node_name in self.nodes: 74 | conf = 'no\renable\r configure terminal\r no ip domain-lookup\r' 75 | conf += file_io.read_txt('{0}/{1}.txt'.format(path, node_name)) 76 | conf += '\rend\r write\r' 77 | process = threading.Thread(target=self.nodes[node_name].configure, args=(conf,)) 78 | # self.nodes[node_name].configure(conf) 79 | process.start() 80 | processes.append(process) 81 | # print("*** NODE {} CONFIGURED".format(node_name)) 82 | [p.join() for p in processes] 83 | return None 84 | 85 | def start(self): 86 | return self.lab.start_all_nodes() 87 | 88 | @decorators.timer 89 | @decorators.progress 90 | def destroy(self): 91 | self.lab = self.get_lab() 92 | self.lab.cleanup() 93 | self.unl.delete_lab(self.lab_name) 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /acme-small/0_built_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | from tools.globals import * 4 | import tools.file_io as file_io 5 | from tools.unetlab import UNetLab 6 | from tools.conf_analyzer import ConfAnalyzer 7 | from network import topology 8 | 9 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 10 | 11 | 12 | def main(): 13 | try: 14 | print("*** CONNECTING TO UNL") 15 | UNL.create_lab() 16 | print("*** BUILDING TOPOLOGY") 17 | UNL.build_topo(topology) 18 | print("*** NORMALIZING CONFIGURATION FILES") 19 | conf_files = ConfAnalyzer() 20 | conf_files.normalize(file_io.read_yaml(INTF_CONV_FILE)) 21 | print("*** EXTRACTING IP") 22 | conf_files.extract_ip() 23 | print("*** STARTING ALL NODES") 24 | UNL.start() 25 | print("*** CONFIGURING NODES") 26 | UNL.configure_nodes(TMP_DIR) 27 | print("*** ALL NODES CONFIGURED") 28 | except Exception: 29 | UNL.destroy() 30 | raise 31 | return 0 32 | 33 | if __name__ == '__main__': 34 | sys.exit(main()) 35 | 36 | -------------------------------------------------------------------------------- /acme-small/1_monitor.py: -------------------------------------------------------------------------------- 1 | from tools.ping import Ping 2 | import tools.file_io as file_io 3 | from tools.unetlab import UNetLab 4 | from tools.globals import * 5 | import sys 6 | import threading 7 | 8 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 9 | PING_FLOWS = file_io.read_txt('{}/ping_flows.txt'.format(TEST_DIR)) 10 | RUN = [True] 11 | 12 | 13 | def key_press(): 14 | global RUN 15 | RUN[0] = raw_input() 16 | 17 | 18 | def main(): 19 | lab = UNL.get_lab() 20 | ping = Ping(PING_FLOWS, lab) 21 | thread = threading.Thread(target=key_press) 22 | thread.start() 23 | print('Starting pings. Press "Enter" to stop') 24 | ping.run(RUN) 25 | print('\rStopped'), 26 | 27 | if __name__ == '__main__': 28 | sys.exit(main()) 29 | -------------------------------------------------------------------------------- /acme-small/2_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import time 4 | from tools.unetlab import UNetLab 5 | from tools.testflows import TestFlows 6 | from tools.traceroute import Traceroute 7 | import tools.file_io as file_io 8 | from tools.globals import * 9 | 10 | 11 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 12 | TEST_FLOWS = TestFlows(file_io.read_txt('{}/traffic_flows.txt'.format(TEST_DIR))) 13 | INTF_CONV = file_io.read_yaml(INTF_CONV_FILE) 14 | RECONVERGENCE_TIMER = 15 15 | 16 | 17 | def conf_shut_intf(intf): 18 | return conf_run_intf(intf, 'shutdown') 19 | 20 | 21 | def conf_unshut_intf(intf): 22 | return conf_run_intf(intf, 'no shutdown') 23 | 24 | 25 | def conf_run_intf(intf, command): 26 | return '\r\n'.join(['enable', 'conf t', 'interface {}'.format(intf), command, 'end']) 27 | 28 | 29 | def run_tests(tests): 30 | failed = False 31 | for seq, fail_condition in sorted(tests.keys()): 32 | timer = RECONVERGENCE_TIMER 33 | print 34 | print("*** TESTING SCENARIO {}".format(seq)) 35 | for fail_point in fail_condition: 36 | if not fail_point.dev == 'None': 37 | fail_node = fail_point.get_node() 38 | try: 39 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 40 | except KeyError as e: 41 | e.message = 'Could not find interface in conversion table: {}'.format(fail_node) 42 | raise 43 | fail_node.configure(conf_shut_intf(lab_intf)) 44 | print("*** FAILURE CONDITION CREATED: {}".format(fail_point)) 45 | else: 46 | timer = 0 47 | # wait for protocols to converge 48 | time.sleep(timer) 49 | for (from_point, to_point), flow_data in tests[(seq, fail_condition)].iteritems(): 50 | flow = flow_data['parsed'] 51 | print("*** TESTING FLOW FROM {} TO {}".format(from_point, to_point)) 52 | from_node = from_point.get_node() 53 | trace_command = 'traceroute {} {} numeric'.format(to_point.ip, 'source ' + from_point.ip) 54 | enable = 'enable\rconf t\rno logging console\rend\r' 55 | trace_result = from_node.configure(enable + trace_command) 56 | traceroute = Traceroute(trace_result) 57 | traceroute.parse() 58 | traceroute.resolve() 59 | rc = traceroute.verify(flow) 60 | if rc > 0: 61 | failed = True 62 | print('!!! FAILED FLOW FROM {} TO {}'.format(from_point, to_point)) 63 | print('!!! EXPECTED PATH: {}, ACTUAL PATH: {}' 64 | .format('->'.join(flow_data['text'].split(', ')), traceroute.as_string([from_point.dev]))) 65 | else: 66 | print ('*** SUCCESSFUL FLOW') 67 | for fail_point in fail_condition: 68 | if not fail_point.dev == 'None': 69 | fail_node = fail_point.get_node() 70 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 71 | fail_node.configure(conf_unshut_intf(lab_intf)) 72 | print("*** FAILURE CONDITION RESTORED: {}".format(fail_point)) 73 | return failed 74 | 75 | 76 | def main(): 77 | try: 78 | lab = UNL.get_lab() 79 | print("*** CONNECTED TO UNL") 80 | flows = TEST_FLOWS.parse(lab) 81 | failed = run_tests(flows) 82 | print("*** TESTS COMPLETED") 83 | except: 84 | raise 85 | return 1 if failed else 0 86 | 87 | 88 | if __name__ == '__main__': 89 | sys.exit(main()) 90 | -------------------------------------------------------------------------------- /acme-small/3_destroy_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import tools.file_io as file_io 4 | from tools.unetlab import UNetLab 5 | from tools.globals import * 6 | 7 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 8 | 9 | 10 | def main(): 11 | try: 12 | UNL.destroy() 13 | print("*** LAB DESTROYED") 14 | for f in os.listdir(TMP_DIR): 15 | if not f.startswith('.'): 16 | file_path = os.path.join(TMP_DIR, f) 17 | if os.path.isfile(file_path): 18 | os.unlink(file_path) 19 | print("*** TMP DIRECTORY CLEANED UP") 20 | except: 21 | print ('*** Emulation Failed') 22 | raise 23 | return 0 24 | 25 | if __name__ == '__main__': 26 | sys.exit(main()) 27 | 28 | -------------------------------------------------------------------------------- /acme-small/README.md: -------------------------------------------------------------------------------- 1 | # ACME-SMALL 2 | 3 | A simple 4-node topology. 4 | 5 | ![Alt text](./network/acme-small.jpg?raw=true "4-node topology") 6 | 7 | ## Prerequisites 8 | 9 | * UNetLab server reachable from local machine 10 | * L2 and L3 IOU images under `/opt/unetlab/addons/iol/bin` renamed to 'L2-LATEST.bin' and 'L3-LATEST.bin' 11 | 12 | ## Install dependencies 13 | 14 | ```bash 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | ## Environment setup 19 | 20 | * Change `./network/tests/traffic_flows.txt` file to match the expected traffic paths 21 | * Change `./network/tests/ping_flows.txt` file to match the destinations that need to be monitored 22 | * Change `./network/unetlab.yml` to match your UNetLab server environment 23 | 24 | ## Workflow 25 | 26 | 1. Build and configure topology 27 | ```bash 28 | ./0_build_topo.py 29 | ``` 30 | 31 | After this step you should be able to find the lab up and running on UNetLab server. 32 | 33 | 2. Verify real-time connectivity while making configuration changes 34 | 35 | ```bash 36 | ./1_monitor.py 37 | ``` 38 | 39 | Only failed pings will be displayed. 40 | 41 | 3. Verify test scenarios 42 | 43 | ```bash 44 | ./2_test.py 45 | ``` 46 | 47 | If any of the scenarios have failed, examine the output, adjust configuration as needed and re-run the tests. 48 | 49 | 4. Shutdown and delete the lab 50 | 51 | ```bash 52 | ./3_destroy_topo.py 53 | ``` 54 | 55 | ## Caveats 56 | 57 | * Designed only for IPv4 on Cisco IOS devices 58 | * Assuming only 15 seconds for protocol reconvergence when creating failure conditions 59 | -------------------------------------------------------------------------------- /acme-small/config/R1.txt: -------------------------------------------------------------------------------- 1 | ! hostname R1 ! crypto key generate rsa modulus 1024 label ssh ! username cisco privilege 15 secret cisco ! interface GigabitEthernet1/1 no shut ip address 12.12.12.1 255.255.255.0 ! interface GigabitEthernet0/0 no shut ip address 14.14.14.1 255.255.255.0 ! interface Loopback0 ip address 10.0.0.1 255.255.255.255 ! ip access-list standard OFFSET permit 34.34.34.0 0.0.0.255 ! router eigrp 100 network 0.0.0.0 offset-list OFFSET in 1000000 GigabitEthernet0/0 ! interface GigabitEthernet0 no shut ip address 192.168.91.250 255.255.255.0 ! line vty 0 4 login local transport input ssh ! -------------------------------------------------------------------------------- /acme-small/config/R2.txt: -------------------------------------------------------------------------------- 1 | ! hostname R2 ! crypto key generate rsa modulus 1024 label ssh ! username cisco privilege 15 secret cisco ! interface GigabitEthernet0/0 no shut ip address 12.12.12.2 255.255.255.0 ! interface GigabitEthernet0/1 no shut ip address 23.23.23.2 255.255.255.0 ! interface Loopback0 ip address 10.0.0.2 255.255.255.255 ! router eigrp 100 network 0.0.0.0 ! ! line vty 0 4 login local transport input ssh ! -------------------------------------------------------------------------------- /acme-small/config/R3.txt: -------------------------------------------------------------------------------- 1 | ! hostname R3 ! crypto key generate rsa modulus 1024 label ssh ! username cisco privilege 15 secret cisco ! interface GigabitEthernet0/0 no shut ip address 23.23.23.3 255.255.255.0 ! interface GigabitEthernet0/1 no shut ip address 34.34.34.3 255.255.255.0 ! interface Loopback0 ip address 10.0.0.3 255.255.255.255 ! router eigrp 100 network 0.0.0.0 ! ! line vty 0 4 login local transport input ssh ! -------------------------------------------------------------------------------- /acme-small/config/R4.txt: -------------------------------------------------------------------------------- 1 | ! hostname R4 ! crypto key generate rsa modulus 1024 label ssh ! username cisco privilege 15 secret cisco ! interface GigabitEthernet1/0 no shut ip address 14.14.14.4 255.255.255.0 ! interface GigabitEthernet1/1 no shut ip address 34.34.34.4 255.255.255.0 ! interface Loopback0 ip address 10.0.0.4 255.255.255.255 ! router eigrp 100 network 0.0.0.0 ! ! line vty 0 4 login local transport input ssh ! -------------------------------------------------------------------------------- /acme-small/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-small/network/__init__.py -------------------------------------------------------------------------------- /acme-small/network/acme-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-small/network/acme-small.jpg -------------------------------------------------------------------------------- /acme-small/network/tests/ping_flows.txt: -------------------------------------------------------------------------------- 1 | From R3,G0/1 to R1,Loopback0 2 | From R2 to R4 3 | From R2 to R3 4 | From R2 to R1 5 | From R2 to R3,G0/1 -------------------------------------------------------------------------------- /acme-small/network/tests/traffic_flows.txt: -------------------------------------------------------------------------------- 1 | # Ignore comments 2 | ## Normal operations 3 | 1 Failed None 4 | From R1 to R3 via R2 or R4, R3 5 | From R2 to R3 via R3 6 | From R2 to R4 via R1 or R3 7 | # new traffic engineering requirement 8 | From R1 to R3,G0/1 via not R4, R3 9 | 10 | # Failed link between R1 and R2 11 | 2 Failed R1 Gig1/1, R2 Gig0/0 12 | From R1 to R2 via not R2, R3 13 | From R2 to R4 via not R1, R4 14 | 15 | 16 | -------------------------------------------------------------------------------- /acme-small/network/topology.py: -------------------------------------------------------------------------------- 1 | real = ((('R1', 'GigabitEthernet1/1'), ('R2', 'GigabitEthernet0/0')), 2 | (('R2', 'GigabitEthernet0/1'), ('R3', 'GigabitEthernet0/0')), 3 | (('R3', 'GigabitEthernet0/1'), ('R4', 'GigabitEthernet1/1')), 4 | (('R4', 'GigabitEthernet1/0'), ('R1', 'GigabitEthernet0/0'))) 5 | 6 | 7 | # ext_net = {('R1', 'GigabitEthernet0'): 'pnet0'} 8 | -------------------------------------------------------------------------------- /acme-small/network/unetlab.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ip: 192.168.91.139 3 | user: admin 4 | pwd: unl 5 | lab_name: network-ci-acme-small 6 | -------------------------------------------------------------------------------- /acme-small/requirements.txt: -------------------------------------------------------------------------------- 1 | restunlclient 2 | pyyaml 3 | -------------------------------------------------------------------------------- /acme-small/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /acme-small/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/acme-small/tools/__init__.py -------------------------------------------------------------------------------- /acme-small/tools/conf_analyzer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import file_io 3 | from globals import * 4 | 5 | 6 | class ConfAnalyzer(object): 7 | 8 | @staticmethod 9 | def convert_intf(text, intf_conv): 10 | new_text = [] 11 | for line in text.splitlines(): 12 | for old_intf, new_intf in intf_conv.iteritems(): 13 | if re.match(r'^.*{}({})?$'.format(old_intf, INTF_DOT1Q_REGEX), line): 14 | line = line.replace(old_intf, new_intf) 15 | if line.startswith('interface '): 16 | line += '\r no shut' 17 | new_text.append(line) 18 | return '\r'.join(new_text) 19 | 20 | @staticmethod 21 | def cleanup(text): 22 | new_text = [] 23 | ignore_lines = False 24 | for line in text.splitlines(): 25 | if any([line.startswith(ignore) for ignore in IGNORE_CONFIG]): 26 | ignore_lines = True 27 | elif '!' in line: 28 | new_text.append(line) 29 | ignore_lines = False 30 | elif not ignore_lines: 31 | new_text.append(line) 32 | return '\r'.join(new_text) 33 | 34 | def normalize(self, intf_conv): 35 | for f in os.listdir(CONF_DIR): 36 | if f.endswith('.txt'): 37 | file_name = os.path.splitext(f)[0] 38 | file_text = file_io.read_txt(CONF_DIR + '/' + f) 39 | converted_intf = self.convert_intf(file_text, intf_conv.get(file_name, {})) 40 | updated_text = self.cleanup(converted_intf) 41 | new_filename = os.path.join(TMP_DIR, file_name + '.txt') 42 | file_io.write_txt(new_filename, updated_text) 43 | return 44 | 45 | @staticmethod 46 | def get_ips(text): 47 | result = {} 48 | ip, intf_name = None, None 49 | for line in text.splitlines(): 50 | if re.search(r'^interface (.*)$', line): 51 | intf_name = re.search(r'^interface (.*)$', line).group(1) 52 | elif intf_name and re.search(r'{}'.format(IP_REGEX), line): 53 | ip = re.search(r'{}'.format(IP_REGEX), line).group(1) 54 | elif intf_name and re.search(r'shut', line): 55 | ip = None 56 | elif intf_name and re.search(r'!', line): 57 | if ip: 58 | result.setdefault(intf_name, []).append(ip) 59 | ip, intf_name = None, None 60 | return result 61 | 62 | def extract_ip(self): 63 | result = {} 64 | cwd = os.path.join(TMP_DIR, CONF_DIR) 65 | for f in os.listdir(cwd): 66 | if f.endswith('.txt'): 67 | dev_name = os.path.splitext(f)[0] 68 | file_text = file_io.read_txt(cwd + '/' + f) 69 | ips = self.get_ips(file_text) 70 | result[dev_name] = ips 71 | file_io.write_yaml(TMP_DIR + '/' + 'ip.yml', result) 72 | return result 73 | 74 | 75 | def main(): 76 | pass 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /acme-small/tools/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | 4 | 5 | def timer(func): 6 | def timed(*args, **kwargs): 7 | start = time.time() 8 | result = func(*args, **kwargs) 9 | stop = time.time() 10 | total_time = stop-start 11 | print('\rTotal time: %d minutes, %d seconds' % (total_time/60, total_time % 60)) 12 | return result 13 | return timed 14 | 15 | 16 | def progress(function): 17 | import threading 18 | 19 | def inner(*args, **kwargs): 20 | run = True 21 | # if running in Jenkins do not decorate 22 | if os.environ.get('UNL_IP'): 23 | return function(*args, **kwargs) 24 | 25 | def print_progress(index=0): 26 | while run: 27 | print('\r' + 'Progress' + '.' * index), 28 | index = 0 if index > 2 else index + 1 29 | time.sleep(0.5) 30 | 31 | process_print = threading.Thread(target=print_progress) 32 | process_print.start() 33 | try: 34 | return function(*args, **kwargs) 35 | finally: 36 | run = False 37 | return inner 38 | 39 | 40 | @timer 41 | @progress 42 | def main(): 43 | for x in xrange(3): 44 | time.sleep(1) 45 | print('Done') 46 | return 42 47 | 48 | if __name__ == '__main__': 49 | print("Computing, please wait") 50 | main() 51 | 52 | -------------------------------------------------------------------------------- /acme-small/tools/dns.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | 4 | 5 | class DNS(object): 6 | 7 | @staticmethod 8 | def reverse_dict(original): 9 | result = {} 10 | for device, intf_ips in original.iteritems(): 11 | for intf, ips in intf_ips.iteritems(): 12 | for ip in ips: 13 | result[ip] = (device, intf) 14 | return result 15 | 16 | def __init__(self, host_to_ips): 17 | self.ip_re = re.compile(IP_REGEX) 18 | self.host_to_ips = host_to_ips 19 | self.ips_to_host = self.reverse_dict(self.host_to_ips) 20 | 21 | def is_ip(self, text): 22 | return self.ip_re.search(text) 23 | 24 | def get(self, text, intf='Loopback0'): 25 | if self.is_ip(text): 26 | return self.ips_to_host.get(text, (text, '')) 27 | else: 28 | return self.host_to_ips.get(text, {}).get(intf, ['127.0.0.1'])[0] 29 | -------------------------------------------------------------------------------- /acme-small/tools/endpoint.py: -------------------------------------------------------------------------------- 1 | from restunl.device import IOL 2 | from globals import * 3 | from dns import DNS 4 | import file_io 5 | import re 6 | 7 | 8 | DEFAULT_INTF = 'Loopback0' 9 | DNS_RESOLVER = DNS(file_io.read_yaml('{}/ip.yml'.format(TMP_DIR))) 10 | 11 | 12 | class IncorrectEndpointFormat(Exception): 13 | pass 14 | 15 | 16 | class Endpoint(object): 17 | 18 | def __init__(self, text): 19 | self.dev, self.intf, self.ip = '', '', '' 20 | self.node = None 21 | if DNS_RESOLVER.is_ip(text): 22 | self.dev, self.intf = DNS_RESOLVER.get(text) 23 | self.ip = text 24 | else: 25 | self.dev, self.intf = self._parse(text) 26 | self.ip = DNS_RESOLVER.get(self.dev, self.intf) 27 | 28 | @staticmethod 29 | def _parse(text): 30 | separators = ',|\s' 31 | seq = [word.strip() for word in re.split(separators, text)] 32 | if len(seq) > 1: 33 | return seq[0], Endpoint.expand(seq[1]) 34 | else: 35 | return seq[0], DEFAULT_INTF 36 | 37 | @staticmethod 38 | def expand(intf): 39 | intf_re = re.match('(\w+)(\d+(/\d+)*)', intf) 40 | try: 41 | intf_name = intf_re.group(1) 42 | intf_number = intf_re.group(2) 43 | except IndexError: 44 | raise IncorrectEndpointFormat('Could not split interface into name and number: {}'.format(intf)) 45 | if intf_name.startswith('G'): 46 | intf_name = 'GigabitEthernet' 47 | elif intf_name.startswith('T'): 48 | intf_name = 'TenGigabitEthernet' 49 | elif intf_name.startswith('E'): 50 | intf_name = 'Ethernet' 51 | elif intf_name.startswith('L'): 52 | intf_name = 'Loopback' 53 | else: 54 | raise IncorrectEndpointFormat('Could not expand interface name: {}'.format(intf)) 55 | return intf_name + intf_number 56 | 57 | def __str__(self): 58 | return self.dev + '(' + self.intf + ')' 59 | 60 | def get_node(self): 61 | return self.node 62 | 63 | 64 | class ActiveEndpoint(Endpoint): 65 | 66 | def __init__(self, text, lab): 67 | super(ActiveEndpoint, self).__init__(text) 68 | if not self.dev == 'None': 69 | self.node = self._set_node(lab) 70 | else: 71 | self.node = None 72 | 73 | def _set_node(self, lab): 74 | return lab.get_node(IOL(self.dev)) 75 | 76 | -------------------------------------------------------------------------------- /acme-small/tools/file_io.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def read_txt(filename): 5 | try: 6 | with open(filename) as stream: 7 | return ''.join(stream.readlines()) 8 | except IOError: 9 | return '' 10 | 11 | 12 | def read_yaml(filename): 13 | try: 14 | with open(filename, 'r') as stream: 15 | yml = yaml.load(stream) 16 | return yml 17 | except IOError: 18 | return {} 19 | 20 | 21 | def write_txt(filename, text): 22 | with open(filename, 'w+') as f: 23 | f.write(text) 24 | return None 25 | 26 | 27 | def write_yaml(filename, data): 28 | write_txt(filename, yaml.dump(data, default_flow_style=False)) 29 | return None 30 | 31 | -------------------------------------------------------------------------------- /acme-small/tools/globals.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | L3_IMAGE = 'L3-LATEST.bin' 4 | L2_IMAGE = 'L2-LATEST.bin' 5 | NET = 'network' 6 | TEST = 'tests' 7 | CONF = 'config' 8 | TMP = 'tmp' 9 | NET_DIR = os.path.abspath(NET) 10 | CONF_DIR = os.path.abspath(CONF) 11 | TMP_DIR = os.path.abspath(TMP) 12 | TEST_DIR = os.path.abspath(os.path.join(NET, TEST)) 13 | INTF_CONV_FILE = os.path.join(TMP_DIR, 'intf_conv' + '.yml') 14 | IP_REGEX = '([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' 15 | INTF_DOT1Q_REGEX = '\.[0-9]{1,3}' 16 | IGNORE_CONFIG = ['class-map', 'policy-map', 'service', 'aaa', 'flow', 'ip nbar', 'logging', 17 | 'snmp', 'banner', 'ntp', 'event', 'ip wccp', 'boot', 'archive', 'privilege', 'tacacs', 18 | 'ip domain', 'ip ftp', 'ip http', 'ip sla', 'track', 'version', '! '] 19 | 20 | 21 | def main(): 22 | print NET_DIR 23 | print CONF_DIR 24 | print TMP_DIR 25 | print TEST_DIR 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /acme-small/tools/ping.py: -------------------------------------------------------------------------------- 1 | import re 2 | from endpoint import ActiveEndpoint 3 | import threading 4 | import time 5 | 6 | 7 | class Ping(object): 8 | 9 | def print_results(self, result=''): 10 | intro = '\rFailed connectivity : ' 11 | result = ', '.join(from_node + ' -> ' + ', '.join(to_nodes) 12 | for from_node, to_nodes in self.failed.iteritems() if to_nodes) 13 | print intro + result, 14 | 15 | def __init__(self, scenarios, lab): 16 | self.failed = dict() 17 | self.lab = lab 18 | self.scenarios = self._parse_scenarios(scenarios, self.lab) 19 | 20 | def run(self, run): 21 | enable = 'enable\r' 22 | processes = [] 23 | while all(run): 24 | for from_point in self.scenarios: 25 | command = enable 26 | for to_point in self.scenarios[from_point]: 27 | command += 'ping {} {} repeat 2'.format(to_point.ip, 'source ' + from_point.ip) 28 | process = threading.Thread(target=self._do_ping, args=(command, from_point, to_point)) 29 | process.start() 30 | processes.append(process) 31 | time.sleep(0.3) 32 | [p.join() for p in processes] 33 | self.print_results() 34 | 35 | def _do_ping(self, command, from_point, to_point): 36 | from_node = from_point.get_node() 37 | result = from_node.configure(command) 38 | percentage = self._parse_result(result) 39 | if not int(percentage) > 0: 40 | self.failed.setdefault(str(from_point), set([])).add(str(to_point)) 41 | else: 42 | self.failed.setdefault(str(from_point), set([])).discard(str(to_point)) 43 | 44 | 45 | @staticmethod 46 | def _parse_result(output): 47 | for line in output.splitlines(): 48 | match_percent = re.match(r'Success rate is (\d+) percent', line) 49 | if match_percent: 50 | return match_percent.group(1) 51 | return '100' 52 | 53 | @staticmethod 54 | def _parse_scenarios(scenarios, lab): 55 | result = {} 56 | for line in scenarios.splitlines(): 57 | try: 58 | _, flow = line.split('From ') 59 | from_, to_ = flow.split(' to ') 60 | from_point = ActiveEndpoint(from_, lab) 61 | to_point = ActiveEndpoint(to_, lab) 62 | result.setdefault(from_point, []).append(to_point) 63 | except: 64 | print('*** Failed to parse scenario {}'.format(line)) 65 | raise 66 | return result 67 | 68 | 69 | def main(): 70 | pass 71 | 72 | if __name__ == '__main__': 73 | main() 74 | -------------------------------------------------------------------------------- /acme-small/tools/testflows.py: -------------------------------------------------------------------------------- 1 | from endpoint import ActiveEndpoint 2 | 3 | 4 | class TestFlows(object): 5 | 6 | def __init__(self, flows): 7 | self.flows = flows 8 | self.parsed_flows = dict() 9 | 10 | @staticmethod 11 | def split_strip(text, separator): 12 | return [word.strip() for word in text.split(separator)] 13 | 14 | def parse(self, lab): 15 | flows = [line for line in self.flows.split('\n') if line and not line.strip().startswith('#')] 16 | for line in flows: 17 | if 'Failed' in line and line[0].isdigit(): 18 | scenario = dict() 19 | seq, actions = self.split_strip(line, 'Failed') 20 | parsed_action = [ActiveEndpoint(word, lab) for word in self.split_strip(actions, ',')] 21 | flow_key = (seq, tuple(parsed_action)) 22 | self.parsed_flows[flow_key] = scenario 23 | elif all([token in line for token in ['From', 'to', 'via']]): 24 | _, rule = self.split_strip(line, 'From') 25 | from_device, to_via = self.split_strip(rule, 'to') 26 | from_point = ActiveEndpoint(from_device, lab) 27 | to_device, flow = self.split_strip(to_via, 'via') 28 | to_point = ActiveEndpoint(to_device, lab) 29 | flow_and = self.split_strip(flow, ',') 30 | flow_and_or = [['OR', self.split_strip(word, 'or')] if 'or' in word else word for word in flow_and] 31 | flow_and_or_not = [word.split() if 'not' in word else word for word in flow_and_or] 32 | flow_final = [['AND', word] if type(word) is str else word for word in flow_and_or_not] 33 | scenario_key = (from_point, to_point) 34 | scenario[scenario_key] = {'text': flow, 'parsed': flow_final} 35 | else: 36 | raise Exception('Incorrect traffic flow syntax in {}'.format(line)) 37 | return self.parsed_flows 38 | 39 | def print_flow(self, number, from_device, to_device): 40 | print('From: {}, to: {}, via: {}'.format(from_device, to_device, 41 | self.parsed_flows[number][from_device][to_device])) 42 | 43 | 44 | def main(): 45 | pass 46 | 47 | if __name__ == '__main__': 48 | main() 49 | 50 | -------------------------------------------------------------------------------- /acme-small/tools/traceroute.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | from endpoint import Endpoint 4 | 5 | 6 | class Traceroute(object): 7 | def __init__(self, output): 8 | self.trace = output 9 | self.parsed_result = dict() 10 | self.result_list = [] 11 | self.trace_re = re.compile(r'(\d+)?\s+({ip}|\*)+'.format(ip=IP_REGEX)) 12 | 13 | def parse(self): 14 | self.trace = [line for line in self.trace.split('\n') if line] 15 | seq, ip = None, None 16 | for line in self.trace: 17 | try: 18 | trace_match = self.trace_re.search(line) 19 | if trace_match: 20 | ip = trace_match.group(3) 21 | if trace_match.group(1): 22 | seq = trace_match.group(1) 23 | if seq and ip: 24 | self.parsed_result.setdefault(int(seq), []).append(Endpoint(ip)) 25 | except AttributeError: 26 | print('Cannot parse traceroute output {}'.format(line)) 27 | raise 28 | return self.parsed_result 29 | 30 | def resolve(self): 31 | for key in sorted(self.parsed_result.keys()): 32 | self.result_list.append([hop.dev for hop in self.parsed_result[key]]) 33 | return self.result_list 34 | 35 | def as_string(self, from_node=[]): 36 | return '->'.join('|'.join([dev for dev in set(el)]) for el in [from_node] + self.result_list) 37 | 38 | def verify(self, flow): 39 | index = 0 40 | for step in self.result_list: 41 | if index < len(flow): 42 | index += self.bool_compare(step, flow[index]) 43 | rc = 1 if len(flow[index:]) > 0 else 0 44 | return rc 45 | 46 | @staticmethod 47 | def bool_compare(hops, rule): 48 | op, device = rule 49 | # print("COMPARING TRACE {} with RULE {}".format(hops, rule)) 50 | if op.upper() == 'NOT': 51 | return 1 if device not in hops else 0 52 | elif op.upper() == 'OR': 53 | return 1 if all([hop in device for hop in hops]) else 0 54 | else: 55 | return 1 if all([hop in device for hop in set(hops)]) else 0 56 | return 0 57 | 58 | 59 | def main(): 60 | line1 = ''' 61 | Type escape sequence to abort. 62 | Tracing the route to 10.0.0.3 63 | VRF info: (vrf in name/id, vrf out name/id) 64 | 1 * * 65 | 12.12.12.2 1 msec 66 | 2 23.23.23.3 0 msec 67 | 34.34.34.3 1 msec * 68 | ''' 69 | line2 = ''' 70 | Type escape sequence to abort. 71 | Tracing the route to 10.0.0.3 72 | VRF info: (vrf in name/id, vrf out name/id) 73 | 1 14.14.14.4 0 msec 74 | 12.12.12.2 1 msec 75 | 14.14.14.4 0 msec 76 | 2 23.23.23.3 0 msec 77 | 34.34.34.3 1 msec * 78 | ''' 79 | line3 = ''' 80 | Type escape sequence to abort. 81 | Tracing the route to 10.0.0.3 82 | VRF info: (vrf in name/id, vrf out name/id) 83 | 1 * * * 84 | 2 23.23.23.3 0 msec 85 | 34.34.34.3 1 msec * 86 | ''' 87 | print Traceroute(line1).parse() 88 | print Traceroute(line2).parse() 89 | print Traceroute(line3).parse() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() -------------------------------------------------------------------------------- /acme-small/tools/unetlab.py: -------------------------------------------------------------------------------- 1 | from restunl.unetlab import UnlServer 2 | from restunl.device import Router, Switch 3 | from globals import * 4 | import file_io 5 | import decorators 6 | import os 7 | 8 | 9 | class UNetLab(object): 10 | 11 | def __init__(self, ip='', user='', pwd='', lab_name=''): 12 | self.ip, self.user, self.pwd, self.lab_name = ip, user, pwd, lab_name 13 | if os.environ.get('UNL_IP'): 14 | self.ip = os.environ.get('UNL_IP') 15 | self.unl = UnlServer(self.ip) 16 | self.unl.login(self.user, self.pwd) 17 | self.lab = None 18 | self.nodes = dict() 19 | 20 | def create_lab(self): 21 | self.lab = self.unl.create_lab(self.lab_name) 22 | self.lab.cleanup() 23 | 24 | def get_lab(self): 25 | return self.unl.get_lab(self.lab_name) 26 | 27 | def build_topo(self, topology): 28 | real_topo = topology.real 29 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 30 | for ((a_name, a_intf), (b_name, b_intf)) in real_topo: 31 | a_device = Switch(a_name, L2_IMAGE) if 'sw' in a_name.lower() else Router(a_name, L3_IMAGE) 32 | b_device = Switch(b_name, L2_IMAGE) if 'sw' in b_name.lower() else Router(b_name, L3_IMAGE) 33 | if a_name not in self.nodes: 34 | self.nodes[a_name] = self.lab.create_node(a_device) 35 | # print("*** NODE {} CREATED".format(a_name)) 36 | if b_name not in self.nodes: 37 | self.nodes[b_name] = self.lab.create_node(b_device) 38 | # print("*** NODE {} CREATED".format(b_name)) 39 | node_a = self.nodes[a_name] 40 | node_b = self.nodes[b_name] 41 | a_intf_lab = node_a.get_next_intf() 42 | b_intf_lab = node_b.get_next_intf() 43 | intf_conv.setdefault(a_name, {})[a_intf] = a_intf_lab 44 | intf_conv.setdefault(b_name, {})[b_intf] = b_intf_lab 45 | node_a.connect_node(a_intf_lab, node_b, b_intf_lab) 46 | # print("*** NODES {} and {} ARE CONNECTED".format(a_name, b_name)) 47 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 48 | return None 49 | 50 | def ext_connect(self, topo): 51 | ext_topo = topo.ext_net 52 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 53 | for (node_name, node_intf), pnet in ext_topo.iteritems(): 54 | ext_net = self.lab.create_net('cloud', net_type=pnet) 55 | the_node = self.nodes[node_name] 56 | node_intf_lab = the_node.get_next_intf() 57 | the_node.connect_interface(node_intf_lab, ext_net) 58 | intf_conv.setdefault(node_name, {})[node_intf] = node_intf_lab 59 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 60 | return None 61 | 62 | @decorators.timer 63 | @decorators.progress 64 | def configure_nodes(self, path): 65 | import threading 66 | processes = [] 67 | for node_name in self.nodes: 68 | conf = 'no\renable\r configure terminal\r no ip domain-lookup\r' 69 | conf += file_io.read_txt('{0}/{1}.txt'.format(path, node_name)) 70 | conf += '\rend\r write\r' 71 | process = threading.Thread(target=self.nodes[node_name].configure, args=(conf,)) 72 | # self.nodes[node_name].configure(conf) 73 | process.start() 74 | processes.append(process) 75 | # print("*** NODE {} CONFIGURED".format(node_name)) 76 | [p.join() for p in processes] 77 | return None 78 | 79 | def start(self): 80 | return self.lab.start_all_nodes() 81 | 82 | @decorators.timer 83 | @decorators.progress 84 | def destroy(self): 85 | self.lab = self.get_lab() 86 | self.lab.cleanup() 87 | self.unl.delete_lab(self.lab_name) 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /skeleton/0_built_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | from tools.globals import * 4 | import tools.file_io as file_io 5 | from tools.unetlab import UNetLab 6 | from tools.conf_analyzer import ConfAnalyzer 7 | from network import topology 8 | 9 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 10 | 11 | 12 | def main(): 13 | try: 14 | print("*** CONNECTING TO UNL") 15 | UNL.create_lab() 16 | print("*** BUILDING TOPOLOGY") 17 | UNL.build_topo(topology) 18 | print("*** NORMALIZING CONFIGURATION FILES") 19 | conf_files = ConfAnalyzer() 20 | conf_files.normalize(file_io.read_yaml(INTF_CONV_FILE)) 21 | print("*** EXTRACTING IP") 22 | conf_files.extract_ip() 23 | print("*** STARTING ALL NODES") 24 | UNL.start() 25 | print("*** CONFIGURING NODES") 26 | UNL.configure_nodes(TMP_DIR) 27 | print("*** ALL NODES CONFIGURED") 28 | except Exception: 29 | UNL.destroy() 30 | raise 31 | return 0 32 | 33 | if __name__ == '__main__': 34 | sys.exit(main()) 35 | 36 | -------------------------------------------------------------------------------- /skeleton/1_monitor.py: -------------------------------------------------------------------------------- 1 | from tools.ping import Ping 2 | import tools.file_io as file_io 3 | from tools.unetlab import UNetLab 4 | from tools.globals import * 5 | import sys 6 | import threading 7 | 8 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 9 | PING_FLOWS = file_io.read_txt('{}/ping_flows.txt'.format(TEST_DIR)) 10 | RUN = [True] 11 | 12 | 13 | def key_press(): 14 | global RUN 15 | RUN[0] = raw_input() 16 | 17 | 18 | def main(): 19 | lab = UNL.get_lab() 20 | ping = Ping(PING_FLOWS, lab) 21 | thread = threading.Thread(target=key_press) 22 | thread.start() 23 | print('Starting pings. Press "Enter" to stop') 24 | ping.run(RUN) 25 | print('\rStopped'), 26 | 27 | if __name__ == '__main__': 28 | sys.exit(main()) 29 | -------------------------------------------------------------------------------- /skeleton/2_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import time 4 | from tools.unetlab import UNetLab 5 | from tools.testflows import TestFlows 6 | from tools.traceroute import Traceroute 7 | import tools.file_io as file_io 8 | from tools.globals import * 9 | 10 | 11 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 12 | TEST_FLOWS = TestFlows(file_io.read_txt('{}/traffic_flows.txt'.format(TEST_DIR))) 13 | INTF_CONV = file_io.read_yaml(INTF_CONV_FILE) 14 | RECONVERGENCE_TIMER = 15 15 | 16 | 17 | def conf_shut_intf(intf): 18 | return conf_run_intf(intf, 'shutdown') 19 | 20 | 21 | def conf_unshut_intf(intf): 22 | return conf_run_intf(intf, 'no shutdown') 23 | 24 | 25 | def conf_run_intf(intf, command): 26 | return '\r\n'.join(['enable', 'conf t', 'interface {}'.format(intf), command, 'end']) 27 | 28 | 29 | def run_tests(tests): 30 | failed = False 31 | for seq, fail_condition in sorted(tests.keys()): 32 | timer = RECONVERGENCE_TIMER 33 | print 34 | print("*** TESTING SCENARIO {}".format(seq)) 35 | for fail_point in fail_condition: 36 | if not fail_point.dev == 'None': 37 | fail_node = fail_point.get_node() 38 | try: 39 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 40 | except KeyError: 41 | lab_intf = fail_point.intf 42 | fail_node.configure(conf_shut_intf(lab_intf)) 43 | print("*** FAILURE CONDITION CREATED: {}".format(fail_point)) 44 | else: 45 | timer = 0 46 | # wait for protocols to converge 47 | time.sleep(timer) 48 | for (from_point, to_point), flow_data in tests[(seq, fail_condition)].iteritems(): 49 | flow = flow_data['parsed'] 50 | print("*** TESTING FLOW FROM {} TO {}".format(from_point, to_point)) 51 | from_node = from_point.get_node() 52 | trace_command = 'traceroute {} {} numeric'.format(to_point.ip, 'source ' + from_point.ip) 53 | enable = 'enable\rconf t\rno logging console\rend\r' 54 | trace_result = from_node.configure(enable + trace_command) 55 | traceroute = Traceroute(trace_result) 56 | traceroute.parse() 57 | traceroute.resolve() 58 | rc = traceroute.verify(flow) 59 | if rc > 0: 60 | failed = True 61 | print('!!! FAILED FLOW FROM {} TO {}'.format(from_point, to_point)) 62 | print('!!! EXPECTED PATH: {}, ACTUAL PATH: {}' 63 | .format('->'.join(flow_data['text'].split(', ')), traceroute.as_string([from_point.dev]))) 64 | else: 65 | print ('*** SUCCESSFUL FLOW') 66 | for fail_point in fail_condition: 67 | if not fail_point.dev == 'None': 68 | fail_node = fail_point.get_node() 69 | try: 70 | lab_intf = INTF_CONV[fail_point.dev][fail_point.intf] 71 | except KeyError: 72 | lab_intf = fail_point.intf 73 | fail_node.configure(conf_unshut_intf(lab_intf)) 74 | print("*** FAILURE CONDITION RESTORED: {}".format(fail_point)) 75 | return failed 76 | 77 | 78 | def main(): 79 | try: 80 | lab = UNL.get_lab() 81 | print("*** CONNECTED TO UNL") 82 | flows = TEST_FLOWS.parse(lab) 83 | failed = run_tests(flows) 84 | print("*** TESTS COMPLETED") 85 | except: 86 | raise 87 | return 1 if failed else 0 88 | 89 | 90 | if __name__ == '__main__': 91 | sys.exit(main()) 92 | -------------------------------------------------------------------------------- /skeleton/3_destroy_topo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import tools.file_io as file_io 4 | from tools.unetlab import UNetLab 5 | from tools.globals import * 6 | 7 | UNL = UNetLab(**file_io.read_yaml('{}/unetlab.yml'.format(NET_DIR))) 8 | 9 | 10 | def main(): 11 | try: 12 | UNL.destroy() 13 | print("*** LAB DESTROYED") 14 | for f in os.listdir(TMP_DIR): 15 | if not f.startswith('.'): 16 | file_path = os.path.join(TMP_DIR, f) 17 | if os.path.isfile(file_path): 18 | os.unlink(file_path) 19 | print("*** TMP DIRECTORY CLEANED UP") 20 | except: 21 | print ('*** Emulation Failed') 22 | raise 23 | return 0 24 | 25 | if __name__ == '__main__': 26 | sys.exit(main()) 27 | 28 | -------------------------------------------------------------------------------- /skeleton/README.md: -------------------------------------------------------------------------------- 1 | # SKELETON 2 | 3 | Input information: 4 | 5 | * topology in `topology.py` 6 | * unetlab in `unetlab.yml` 7 | * test scenarios in `traffic_flows.txt` 8 | 9 | To Run: 10 | 11 | 1. Create topology 12 | 13 | ``` 14 | ./1_build_topo.py 15 | ``` 16 | 17 | 2. Verify test scenarios 18 | 19 | ``` 20 | ./2_test.py 21 | ``` 22 | 23 | 3. Cleanup the lab 24 | 25 | ``` 26 | ./3_destroy_topo.py 27 | ``` 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /skeleton/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/skeleton/network/__init__.py -------------------------------------------------------------------------------- /skeleton/network/testing/ping_flows.txt: -------------------------------------------------------------------------------- 1 | From R1, Loopback0 to R3, Ethernet0/0 2 | From R2 to R4 -------------------------------------------------------------------------------- /skeleton/network/testing/traffic_flows.txt: -------------------------------------------------------------------------------- 1 | # Ignore comments 2 | ## Normal operations 3 | 1 Failed None 4 | From R1 to R3 via R2 or R4, R3 5 | From R2 to R3 via R3 6 | From R2 to R4 via R1 or R3 7 | 8 | # Failed link between R1 and R2 9 | 2 Failed R1 Ethernet0/0 10 | From R1 to R2 via not R2, R3 11 | From R2 to R4 via not R1, R4 12 | 13 | 14 | -------------------------------------------------------------------------------- /skeleton/network/topology.py: -------------------------------------------------------------------------------- 1 | real = {('R1', 'Ethernet0/0'): ('R2', 'Ethernet0/0'), 2 | ('R2', 'Ethernet0/1'): ('R3', 'Ethernet0/0'), 3 | ('R3', 'Ethernet0/1'): ('R4', 'Ethernet0/1'), 4 | ('R4', 'Ethernet0/0'): ('R1', 'Ethernet0/1')} 5 | 6 | ext_net = {('R1', 'Ethernet0/3'): 'pnet1'} 7 | -------------------------------------------------------------------------------- /skeleton/network/unetlab.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ip: 192.168.247.20 3 | user: admin 4 | pwd: unl 5 | lab_name: network-ci-acme-small 6 | -------------------------------------------------------------------------------- /skeleton/requirements.txt: -------------------------------------------------------------------------------- 1 | restunlclient 2 | pyyaml 3 | -------------------------------------------------------------------------------- /skeleton/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networkop/network-ci/8a0c5d7c636133ba4b101d926474fbfe7a6869fa/skeleton/tools/__init__.py -------------------------------------------------------------------------------- /skeleton/tools/conf_analyzer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import file_io 3 | from globals import * 4 | 5 | 6 | class ConfAnalyzer(object): 7 | 8 | @staticmethod 9 | def convert_intf(text, intf_conv): 10 | new_text = [] 11 | for line in text.splitlines(): 12 | for old_intf, new_intf in intf_conv.iteritems(): 13 | if re.match(r'^.*{}({})?$'.format(old_intf, INTF_DOT1Q_REGEX), line): 14 | line = line.replace(old_intf, new_intf) 15 | if line.startswith('interface '): 16 | line += '\r no shut' 17 | new_text.append(line) 18 | return '\r'.join(new_text) 19 | 20 | @staticmethod 21 | def cleanup(text): 22 | new_text = [] 23 | ignore_lines = False 24 | for line in text.splitlines(): 25 | if any([line.startswith(ignore) for ignore in IGNORE_CONFIG]): 26 | ignore_lines = True 27 | elif '!' in line: 28 | new_text.append(line) 29 | ignore_lines = False 30 | elif not ignore_lines: 31 | new_text.append(line) 32 | return '\r'.join(new_text) 33 | 34 | def normalize(self, intf_conv): 35 | for f in os.listdir(CONF_DIR): 36 | if f.endswith('.txt'): 37 | file_name = os.path.splitext(f)[0] 38 | file_text = file_io.read_txt(CONF_DIR + '/' + f) 39 | converted_intf = self.convert_intf(file_text, intf_conv.get(file_name, {})) 40 | updated_text = self.cleanup(converted_intf) 41 | new_filename = os.path.join(TMP_DIR, file_name + '.txt') 42 | file_io.write_txt(new_filename, updated_text) 43 | return 44 | 45 | @staticmethod 46 | def get_ips(text): 47 | result = {} 48 | ip_list, intf_name = [], None 49 | for line in text.splitlines(): 50 | if re.search(r'^interface (.*)$', line): 51 | intf_name = re.search(r'^interface (.*)$', line).group(1) 52 | elif intf_name and re.search(r'{}'.format(IP_REGEX), line): 53 | ip = re.search(r'{}'.format(IP_REGEX), line).group(1) 54 | ip_list.append(ip) 55 | elif intf_name and re.search(r'shut', line): 56 | ip_list = [] 57 | elif intf_name and re.search(r'!', line): 58 | if ip_list: 59 | result[intf_name] = ip_list 60 | ip_list, intf_name = [], None 61 | return result 62 | 63 | def extract_ip(self): 64 | result = {} 65 | cwd = os.path.join(TMP_DIR, CONF_DIR) 66 | for f in os.listdir(cwd): 67 | if f.endswith('.txt'): 68 | dev_name = os.path.splitext(f)[0] 69 | file_text = file_io.read_txt(cwd + '/' + f) 70 | ips = self.get_ips(file_text) 71 | result[dev_name] = ips 72 | file_io.write_yaml(TMP_DIR + '/' + 'ip.yml', result) 73 | return result 74 | 75 | 76 | def main(): 77 | pass 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /skeleton/tools/decorators.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | 4 | 5 | def timer(func): 6 | def timed(*args, **kwargs): 7 | start = time.time() 8 | result = func(*args, **kwargs) 9 | stop = time.time() 10 | total_time = stop-start 11 | print('\rTotal time: %d minutes, %d seconds' % (total_time/60, total_time % 60)) 12 | return result 13 | return timed 14 | 15 | 16 | def progress(function): 17 | import threading 18 | 19 | def inner(*args, **kwargs): 20 | run = True 21 | # if running in Jenkins do not decorate 22 | if os.environ.get('UNL_IP'): 23 | return function(*args, **kwargs) 24 | 25 | def print_progress(index=0): 26 | while run: 27 | print('\r' + 'Progress' + '.' * index), 28 | index = 0 if index > 2 else index + 1 29 | time.sleep(0.5) 30 | 31 | process_print = threading.Thread(target=print_progress) 32 | process_print.start() 33 | try: 34 | return function(*args, **kwargs) 35 | finally: 36 | run = False 37 | return inner 38 | 39 | 40 | @timer 41 | @progress 42 | def main(): 43 | for x in xrange(3): 44 | time.sleep(1) 45 | print('Done') 46 | return 42 47 | 48 | if __name__ == '__main__': 49 | print("Computing, please wait") 50 | main() 51 | 52 | -------------------------------------------------------------------------------- /skeleton/tools/dns.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | 4 | 5 | class DNS(object): 6 | 7 | @staticmethod 8 | def reverse_dict(original): 9 | result = {} 10 | for device, intf_ips in original.iteritems(): 11 | for intf, ips in intf_ips.iteritems(): 12 | for ip in ips: 13 | result[ip] = (device, intf) 14 | return result 15 | 16 | def __init__(self, host_to_ips): 17 | self.ip_re = re.compile(IP_REGEX) 18 | self.host_to_ips = host_to_ips 19 | self.ips_to_host = self.reverse_dict(self.host_to_ips) 20 | 21 | def is_ip(self, text): 22 | return self.ip_re.search(text) 23 | 24 | def get(self, text, intf='Loopback0'): 25 | if self.is_ip(text): 26 | return self.ips_to_host.get(text, (text, '')) 27 | else: 28 | return self.host_to_ips.get(text, {}).get(intf, ['127.0.0.1'])[0] 29 | -------------------------------------------------------------------------------- /skeleton/tools/endpoint.py: -------------------------------------------------------------------------------- 1 | from restunl.device import IOL 2 | from globals import * 3 | from dns import DNS 4 | import file_io 5 | import re 6 | 7 | 8 | DEFAULT_INTF = 'Loopback0' 9 | DNS_RESOLVER = DNS(file_io.read_yaml('{}/ip.yml'.format(TMP_DIR))) 10 | 11 | 12 | class IncorrectEndpointFormat(Exception): 13 | pass 14 | 15 | 16 | class Endpoint(object): 17 | 18 | def __init__(self, text): 19 | self.dev, self.intf, self.ip = '', '', '' 20 | self.node = None 21 | if DNS_RESOLVER.is_ip(text): 22 | self.dev, self.intf = DNS_RESOLVER.get(text) 23 | self.ip = text 24 | else: 25 | self.dev, self.intf = self._parse(text) 26 | self.ip = DNS_RESOLVER.get(self.dev, self.intf) 27 | 28 | @staticmethod 29 | def _parse(text): 30 | separators = ',|\s' 31 | seq = [word.strip() for word in re.split(separators, text)] 32 | if len(seq) > 1: 33 | return seq[0], Endpoint.expand(seq[1]) 34 | else: 35 | return seq[0], DEFAULT_INTF 36 | 37 | @staticmethod 38 | def expand(intf): 39 | intf_re = re.match('([a-zA-Z]+)(\d+(/\d+)*)', intf) 40 | try: 41 | intf_name = intf_re.group(1) 42 | intf_number = intf_re.group(2) 43 | except IndexError: 44 | raise IncorrectEndpointFormat('Could not split interface into name and number: {}'.format(intf)) 45 | if intf_name.startswith('G'): 46 | intf_name = 'GigabitEthernet' 47 | elif intf_name.startswith('T'): 48 | intf_name = 'TenGigabitEthernet' 49 | elif intf_name.startswith('E'): 50 | intf_name = 'Ethernet' 51 | elif intf_name.startswith('L'): 52 | intf_name = 'Loopback' 53 | elif intf_name.startswith('V'): 54 | intf_name = 'Vlan' 55 | else: 56 | raise IncorrectEndpointFormat('Could not expand interface name: {}'.format(intf)) 57 | return intf_name + intf_number 58 | 59 | def __str__(self): 60 | return self.dev + '(' + self.intf + ')' 61 | 62 | def get_node(self): 63 | return self.node 64 | 65 | 66 | class ActiveEndpoint(Endpoint): 67 | 68 | def __init__(self, text, lab): 69 | super(ActiveEndpoint, self).__init__(text) 70 | if not self.dev == 'None': 71 | self.node = self._set_node(lab) 72 | else: 73 | self.node = None 74 | 75 | def _set_node(self, lab): 76 | return lab.get_node(IOL(self.dev)) 77 | 78 | -------------------------------------------------------------------------------- /skeleton/tools/file_io.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | def read_txt(filename): 5 | try: 6 | with open(filename) as stream: 7 | return ''.join(stream.readlines()) 8 | except IOError: 9 | return '' 10 | 11 | 12 | def read_yaml(filename): 13 | try: 14 | with open(filename, 'r') as stream: 15 | yml = yaml.load(stream) 16 | return yml 17 | except IOError: 18 | return {} 19 | 20 | 21 | def write_txt(filename, text): 22 | with open(filename, 'w+') as f: 23 | f.write(text) 24 | return None 25 | 26 | 27 | def write_yaml(filename, data): 28 | write_txt(filename, yaml.dump(data, default_flow_style=False)) 29 | return None 30 | 31 | -------------------------------------------------------------------------------- /skeleton/tools/globals.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | L3_IMAGE = 'L3-LATEST.bin' 4 | L2_IMAGE = 'L2-LATEST.bin' 5 | NET = 'network' 6 | TEST = 'tests' 7 | CONF = 'config' 8 | TMP = 'tmp' 9 | NET_DIR = os.path.abspath(NET) 10 | CONF_DIR = os.path.abspath(CONF) 11 | TMP_DIR = os.path.abspath(TMP) 12 | TEST_DIR = os.path.abspath(os.path.join(NET, TEST)) 13 | INTF_CONV_FILE = os.path.join(TMP_DIR, 'intf_conv' + '.yml') 14 | IP_REGEX = '([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' 15 | INTF_DOT1Q_REGEX = '\.[0-9]{1,3}' 16 | IGNORE_CONFIG = ['class-map', 'policy-map', 'service', 'aaa', 'flow', 'ip nbar', 'logging', 17 | 'snmp', 'banner', 'ntp', 'event', 'ip wccp', 'boot', 'archive', 'privilege', 'tacacs', 18 | 'ip domain', 'ip ftp', 'ip http', 'ip sla', 'track', 'version', '! '] 19 | 20 | 21 | def main(): 22 | print NET_DIR 23 | print CONF_DIR 24 | print TMP_DIR 25 | print TEST_DIR 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /skeleton/tools/ping.py: -------------------------------------------------------------------------------- 1 | import re 2 | from endpoint import ActiveEndpoint 3 | import threading 4 | import time 5 | 6 | 7 | class Ping(object): 8 | 9 | def print_results(self, result=''): 10 | intro = '\rFailed connectivity : ' 11 | result = ', '.join(from_node + ' -> ' + ', '.join(to_nodes) 12 | for from_node, to_nodes in self.failed.iteritems() if to_nodes) 13 | print intro + result, 14 | 15 | def __init__(self, scenarios, lab): 16 | self.failed = dict() 17 | self.lab = lab 18 | self.scenarios = self._parse_scenarios(scenarios, self.lab) 19 | 20 | def run(self, run): 21 | enable = 'enable\r' 22 | processes = [] 23 | while all(run): 24 | for from_point in self.scenarios: 25 | command = enable 26 | for to_point in self.scenarios[from_point]: 27 | command += 'ping {} {} repeat 2'.format(to_point.ip, 'source ' + from_point.ip) 28 | process = threading.Thread(target=self._do_ping, args=(command, from_point, to_point)) 29 | process.start() 30 | processes.append(process) 31 | time.sleep(0.3) 32 | [p.join() for p in processes] 33 | self.print_results() 34 | 35 | def _do_ping(self, command, from_point, to_point): 36 | from_node = from_point.get_node() 37 | result = from_node.configure(command) 38 | percentage = self._parse_result(result) 39 | if not int(percentage) > 0: 40 | self.failed.setdefault(str(from_point), set([])).add(str(to_point)) 41 | else: 42 | self.failed.setdefault(str(from_point), set([])).discard(str(to_point)) 43 | 44 | 45 | @staticmethod 46 | def _parse_result(output): 47 | for line in output.splitlines(): 48 | match_percent = re.match(r'Success rate is (\d+) percent', line) 49 | if match_percent: 50 | return match_percent.group(1) 51 | return '100' 52 | 53 | @staticmethod 54 | def _parse_scenarios(scenarios, lab): 55 | result = {} 56 | for line in scenarios.splitlines(): 57 | try: 58 | _, flow = line.split('From ') 59 | from_, to_ = flow.split(' to ') 60 | from_point = ActiveEndpoint(from_, lab) 61 | to_point = ActiveEndpoint(to_, lab) 62 | result.setdefault(from_point, []).append(to_point) 63 | except: 64 | print('*** Failed to parse scenario {}'.format(line)) 65 | raise 66 | return result 67 | 68 | 69 | def main(): 70 | pass 71 | 72 | if __name__ == '__main__': 73 | main() 74 | 75 | 76 | -------------------------------------------------------------------------------- /skeleton/tools/testflows.py: -------------------------------------------------------------------------------- 1 | from endpoint import ActiveEndpoint 2 | 3 | 4 | class TestFlows(object): 5 | 6 | def __init__(self, flows): 7 | self.flows = flows 8 | self.parsed_flows = dict() 9 | 10 | @staticmethod 11 | def split_strip(text, separator): 12 | return [word.strip() for word in text.split(separator)] 13 | 14 | def parse(self, lab): 15 | flows = [line for line in self.flows.split('\n') if line and not line.strip().startswith('#')] 16 | for line in flows: 17 | if 'Failed' in line and line[0].isdigit(): 18 | scenario = dict() 19 | seq, actions = self.split_strip(line, 'Failed') 20 | parsed_action = [ActiveEndpoint(word, lab) for word in self.split_strip(actions, ',')] 21 | flow_key = (seq, tuple(parsed_action)) 22 | self.parsed_flows[flow_key] = scenario 23 | elif all([token in line for token in ['From', 'to', 'via']]): 24 | _, rule = self.split_strip(line, 'From') 25 | from_device, to_via = self.split_strip(rule, 'to') 26 | from_point = ActiveEndpoint(from_device, lab) 27 | to_device, flow = self.split_strip(to_via, 'via') 28 | to_point = ActiveEndpoint(to_device, lab) 29 | flow_and = self.split_strip(flow, ',') 30 | flow_and_or = [['OR', self.split_strip(word, 'or')] if 'or' in word else word for word in flow_and] 31 | flow_and_or_not = [word.split() if 'not' in word else word for word in flow_and_or] 32 | flow_final = [['AND', word] if type(word) is str else word for word in flow_and_or_not] 33 | scenario_key = (from_point, to_point) 34 | scenario[scenario_key] = {'text': flow, 'parsed': flow_final} 35 | else: 36 | raise Exception('Incorrect traffic flow syntax in {}'.format(line)) 37 | return self.parsed_flows 38 | 39 | def print_flow(self, number, from_device, to_device): 40 | print('From: {}, to: {}, via: {}'.format(from_device, to_device, 41 | self.parsed_flows[number][from_device][to_device])) 42 | 43 | 44 | def main(): 45 | pass 46 | 47 | if __name__ == '__main__': 48 | main() 49 | 50 | -------------------------------------------------------------------------------- /skeleton/tools/traceroute.py: -------------------------------------------------------------------------------- 1 | import re 2 | from globals import * 3 | from endpoint import Endpoint 4 | 5 | 6 | class Traceroute(object): 7 | def __init__(self, output): 8 | self.trace = output 9 | self.parsed_result = dict() 10 | self.result_list = [] 11 | self.trace_re = re.compile(r'(\d+)?\s+({ip}|\*)+'.format(ip=IP_REGEX)) 12 | 13 | def parse(self): 14 | self.trace = [line for line in self.trace.split('\n') if line] 15 | seq, ip = None, None 16 | for line in self.trace: 17 | try: 18 | trace_match = self.trace_re.search(line) 19 | if trace_match: 20 | ip = trace_match.group(3) 21 | if trace_match.group(1): 22 | seq = trace_match.group(1) 23 | if seq and ip: 24 | self.parsed_result.setdefault(int(seq), []).append(Endpoint(ip)) 25 | except AttributeError: 26 | print('Cannot parse traceroute output {}'.format(line)) 27 | raise 28 | return self.parsed_result 29 | 30 | def resolve(self): 31 | for key in sorted(self.parsed_result.keys()): 32 | self.result_list.append([hop.dev for hop in self.parsed_result[key]]) 33 | return self.result_list 34 | 35 | def as_string(self, from_node=[]): 36 | return '->'.join('|'.join([dev for dev in set(el)]) for el in [from_node] + self.result_list) 37 | 38 | def verify(self, flow): 39 | index = 0 40 | for step in self.result_list: 41 | if index < len(flow): 42 | index += self.bool_compare(step, flow[index]) 43 | rc = 1 if len(flow[index:]) > 0 else 0 44 | return rc 45 | 46 | @staticmethod 47 | def bool_compare(hops, rule): 48 | op, device = rule 49 | # print("COMPARING TRACE {} with RULE {}".format(hops, rule)) 50 | if op.upper() == 'NOT': 51 | return 1 if device not in hops else 0 52 | elif op.upper() == 'OR': 53 | return 1 if all([hop in device for hop in hops]) else 0 54 | else: 55 | return 1 if all([hop in device for hop in set(hops)]) else 0 56 | return 0 57 | 58 | 59 | def main(): 60 | line1 = ''' 61 | Type escape sequence to abort. 62 | Tracing the route to 10.0.0.3 63 | VRF info: (vrf in name/id, vrf out name/id) 64 | 1 * * 65 | 12.12.12.2 1 msec 66 | 2 23.23.23.3 0 msec 67 | 34.34.34.3 1 msec * 68 | ''' 69 | line2 = ''' 70 | Type escape sequence to abort. 71 | Tracing the route to 10.0.0.3 72 | VRF info: (vrf in name/id, vrf out name/id) 73 | 1 14.14.14.4 0 msec 74 | 12.12.12.2 1 msec 75 | 14.14.14.4 0 msec 76 | 2 23.23.23.3 0 msec 77 | 34.34.34.3 1 msec * 78 | ''' 79 | line3 = ''' 80 | Type escape sequence to abort. 81 | Tracing the route to 10.0.0.3 82 | VRF info: (vrf in name/id, vrf out name/id) 83 | 1 * * * 84 | 2 23.23.23.3 0 msec 85 | 34.34.34.3 1 msec * 86 | ''' 87 | print Traceroute(line1).parse() 88 | print Traceroute(line2).parse() 89 | print Traceroute(line3).parse() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() -------------------------------------------------------------------------------- /skeleton/tools/unetlab.py: -------------------------------------------------------------------------------- 1 | from restunl.unetlab import UnlServer 2 | from restunl.device import Router, Switch 3 | from globals import * 4 | import file_io 5 | import decorators 6 | import os 7 | 8 | 9 | class UNetLab(object): 10 | 11 | def __init__(self, ip='', user='', pwd='', lab_name=''): 12 | self.ip, self.user, self.pwd, self.lab_name = ip, user, pwd, lab_name 13 | if os.environ.get('UNL_IP'): 14 | self.ip = os.environ.get('UNL_IP') 15 | self.unl = UnlServer(self.ip) 16 | self.unl.login(self.user, self.pwd) 17 | self.lab = None 18 | self.nodes = dict() 19 | 20 | def create_lab(self): 21 | self.lab = self.unl.create_lab(self.lab_name) 22 | self.lab.cleanup() 23 | 24 | def get_lab(self): 25 | return self.unl.get_lab(self.lab_name) 26 | 27 | def build_topo(self, topology): 28 | real_topo = topology.real 29 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 30 | for ((a_name, a_intf), (b_name, b_intf)) in real_topo: 31 | a_device = Switch(a_name, L2_IMAGE) if 'sw' in a_name.lower() else Router(a_name, L3_IMAGE) 32 | b_device = Switch(b_name, L2_IMAGE) if 'sw' in b_name.lower() else Router(b_name, L3_IMAGE) 33 | if a_name not in self.nodes: 34 | self.nodes[a_name] = self.lab.create_node(a_device) 35 | # print("*** NODE {} CREATED".format(a_name)) 36 | if b_name not in self.nodes: 37 | self.nodes[b_name] = self.lab.create_node(b_device) 38 | # print("*** NODE {} CREATED".format(b_name)) 39 | node_a = self.nodes[a_name] 40 | node_b = self.nodes[b_name] 41 | if intf_conv.get(a_name, {}).get(a_intf, None): 42 | a_intf_lab = intf_conv[a_name][a_intf] 43 | else: 44 | a_intf_lab = node_a.get_next_intf() 45 | if intf_conv.get(b_name, {}).get(b_intf, None): 46 | b_intf_lab = intf_conv[b_name][b_intf] 47 | else: 48 | b_intf_lab = node_b.get_next_intf() 49 | intf_conv.setdefault(a_name, {})[a_intf] = a_intf_lab 50 | intf_conv.setdefault(b_name, {})[b_intf] = b_intf_lab 51 | node_a.connect_node(a_intf_lab, node_b, b_intf_lab) 52 | # print("*** NODES {} and {} ARE CONNECTED".format(a_name, b_name)) 53 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 54 | return None 55 | 56 | def ext_connect(self, topo): 57 | ext_topo = topo.ext_net 58 | intf_conv = file_io.read_yaml(INTF_CONV_FILE) 59 | for (node_name, node_intf), pnet in ext_topo.iteritems(): 60 | ext_net = self.lab.create_net('cloud', net_type=pnet) 61 | the_node = self.nodes[node_name] 62 | node_intf_lab = the_node.get_next_intf() 63 | the_node.connect_interface(node_intf_lab, ext_net) 64 | intf_conv.setdefault(node_name, {})[node_intf] = node_intf_lab 65 | file_io.write_yaml(INTF_CONV_FILE, intf_conv) 66 | return None 67 | 68 | @decorators.timer 69 | @decorators.progress 70 | def configure_nodes(self, path): 71 | import threading 72 | processes = [] 73 | for node_name in self.nodes: 74 | conf = 'no\renable\r configure terminal\r no ip domain-lookup\r' 75 | conf += file_io.read_txt('{0}/{1}.txt'.format(path, node_name)) 76 | conf += '\rend\r write\r' 77 | process = threading.Thread(target=self.nodes[node_name].configure, args=(conf,)) 78 | # self.nodes[node_name].configure(conf) 79 | process.start() 80 | processes.append(process) 81 | # print("*** NODE {} CONFIGURED".format(node_name)) 82 | [p.join() for p in processes] 83 | return None 84 | 85 | def start(self): 86 | return self.lab.start_all_nodes() 87 | 88 | @decorators.timer 89 | @decorators.progress 90 | def destroy(self): 91 | self.lab = self.get_lab() 92 | self.lab.cleanup() 93 | self.unl.delete_lab(self.lab_name) 94 | 95 | 96 | 97 | 98 | 99 | 100 | --------------------------------------------------------------------------------