├── .gitignore ├── README.md ├── ansible.cfg ├── bgp ├── bgp-config.j2 ├── config_bgp.yml ├── deploy.yml ├── deploy_bgp.yml └── verify_bgp.yml ├── create-data-model.yml ├── fabric.yml ├── group_vars └── all.yml ├── hosts ├── model └── nodes.j2 ├── nodes.yml ├── ospf ├── config_ospf.yml ├── deploy.yml ├── deploy_ospf.yml ├── ospf-config-from-fabric.j2 ├── ospf-config.j2 └── verify_ospf.yml ├── services-vpnv4.yml ├── tools ├── clean.yml ├── create_config_dir.yml ├── log_changes.yml └── log_changes_init.yml └── vpnv4 ├── config_vpnv4.yml ├── deploy.yml ├── deploy_vpnv4.yml ├── list-of-customers.json.j2 ├── vars_vpnv4.yml └── vpnv4-config.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | /configs 2 | /printouts 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy IGP+BGP routing and MPLS/VPN services in a multi-provider network 2 | 3 | This set of playbooks deploys routing protocol (OSPF/IBGP/EBGP) configurations and 4 | MPLS/VPN configurations on a set of Cisco IOS routers residing in one or more closely 5 | coupled autonomous systems. 6 | 7 | ## Overview 8 | 9 | The playbooks use a data model describing infrastructure (**fabric.yml**) and services (**services-vpnv4.yml**). The **create-data-model-yml** playbook transforms these data models (described in more details below) into per-node data models (stored in **nodes.yml**) that are easier to work with when generating device configurations. 10 | 11 | The playbooks in *bgp*, *ospf* and *vpnv4* directories read per-node data models from **nodes.yml**, create and deploy device configurations, and verify OSPF/BGP adjacencies. Invoke them from the main directory (otherwise the won't find the **nodes.yml**) with one or more of these tags: 12 | 13 | * **configs** - create the configuration files in *configs* directory 14 | * **deploy** - deploy the configuration files from the *configs* directory to the devices 15 | * **verify** - verify OSPF or BGP adjacencies 16 | 17 | The *hosts* file in this repository was used with [this VIRL topology](https://github.com/ipspace/NetOpsWorkshop/blob/master/topologies/VIRL/Inter-AS.virl). 18 | 19 | ## Data model 20 | 21 | The *infrastructure* data model in **fabric.yml** has these sections: 22 | 23 | * **services** - a list of services supported by the network. Elements of this list can be *IPv4*, *VPNv4* and *InterAS* (for inter-as VPNv4) 24 | * **nodes** - a list of nodes in the network. Every node has *name*, *mgmt* IP address (used to access the node) and router ID IP address (*rid*) configured on its loopback interface 25 | * **asn** - a dictionary of AS numbers. Each ASN has two elements: *members* is a list of device names belonging to the AS, *rr* is the list of route reflectors in that AS. 26 | * **fabric** - list of intra-AS links. Every link has *left* and *right* nodes, *left_ip* and *right_ip* IP addresses and *left_port* and *right_port* interfaces. *cost* is optional. OSPF is configured on intra-AS links. 27 | * **interas** - list of inter-AS links (similar to intra-AS links). EBGP is configured on inter-AS links. 28 | 29 | The *services-vpnv4* data model is a dictionary of customers with every customer having these elements: 30 | 31 | * **rd** - value used for MPLS/VPN RD and import/export RT (the data model and the playbooks support only simple non-overlapping VPNs) 32 | * **nodes** - a dictionary of PE-routers used in this service. The values of these elements is a dictionary of VRF interfaces with each interface having **ip** element (IP prefix configured on PE-router VRF interface) 33 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory=./hosts 3 | gathering=explicit 4 | retry_files_enabled=false 5 | transport=local 6 | -------------------------------------------------------------------------------- /bgp/bgp-config.j2: -------------------------------------------------------------------------------- 1 | {% macro bgp_af_neighbor(ip,svc) %} 2 | neighbor {{ip}} activate 3 | neighbor {{ip}} send-community {% if svc == "vpnv4" %}both{% endif %}{% endmacro %} 4 | 5 | {% set node = nodes[inventory_hostname] %} 6 | {% if node.as is defined %} 7 | {% if reset is defined %} 8 | no router bgp {{ node.as }} 9 | {% endif %} 10 | router bgp {{ node.as }} 11 | bgp router-id {{ node.rid }} 12 | ! 13 | {% for neighbor in node.ibgp %} 14 | neighbor {{ neighbor.ip }} remote-as {{ node.as }} 15 | neighbor {{ neighbor.ip }} update-source Loopback0 16 | neighbor {{ neighbor.ip }} description IBGP session to {{ neighbor.rid }} 17 | {% endfor %} 18 | {% for neighbor in node.ebgp|default([]) %} 19 | neighbor {{ neighbor.ip }} remote-as {{ neighbor.as }} 20 | {% if "InterAS" in services %} 21 | neighbor {{ neighbor.ip }} ebgp-multihop 2 22 | {% endif %} 23 | neighbor {{ neighbor.ip }} description EBGP session to {{ neighbor.rid }} 24 | {% endfor %} 25 | 26 | {% for svc in services|intersect(['IPv4','VPNv4'])|default(['IPv4']) %} 27 | {% set svc = svc.lower() %} 28 | address-family {{svc}} 29 | {% if svc == 'ipv4' %} 30 | redistribute connected route-map Redistribute32 31 | maximum-paths ibgp 32 32 | {% endif %} 33 | maximum-paths 32 34 | {% for neighbor in node.ibgp %} 35 | {{ bgp_af_neighbor(neighbor.ip,svc) }} 36 | {% if node.rr is defined and not (nodes[neighbor.rid].rr is defined) %} 37 | neighbor {{ neighbor.ip }} route-reflector-client 38 | {% endif %} 39 | {% if (svc == "ipv4") %} 40 | neighbor {{ neighbor.ip }} next-hop-self 41 | {% elif "InterAS" in services %} 42 | no neighbor {{ neighbor.ip }} next-hop-self 43 | {% endif %} 44 | {% if (svc == "ipv4") and ("InterAS" in services) %} 45 | neighbor {{ neighbor.ip }} send-label 46 | {% endif %} 47 | {% endfor %} 48 | {% for neighbor in node.ebgp|default([]) %} 49 | {{ bgp_af_neighbor(neighbor.ip,svc) }} 50 | {% if ("InterAS" in services) %} 51 | {% if svc == "ipv4" %} 52 | neighbor {{ neighbor.ip }} send-label 53 | {% endif %} 54 | {% if svc == "vpnv4" %} 55 | neighbor {{ neighbor.ip }} next-hop-unchanged 56 | {% endif %} 57 | {% endif %} 58 | {% endfor %} 59 | {% endfor %} 60 | ! 61 | route-map Redistribute32 permit 10 62 | match ip address prefix-list Subnet32 63 | ! 64 | no ip prefix-list Subnet32 65 | ip prefix-list Subnet32 permit {{ node.rid }}/32 66 | {% endif %} 67 | -------------------------------------------------------------------------------- /bgp/config_bgp.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy IBGP and EBGP routing in a WAN fabric 3 | # 4 | --- 5 | - name: Create configuration directory 6 | local_action: file path={{configs}} state=directory 7 | run_once: true 8 | check_mode: no 9 | changed_when: no 10 | tags: [ configs ] 11 | 12 | - name: Create configurations 13 | template: src=bgp-config.j2 dest={{configs}}/{{inventory_hostname}}.bgp.cfg 14 | check_mode: no 15 | changed_when: no 16 | tags: [ configs ] 17 | -------------------------------------------------------------------------------- /bgp/deploy.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy IBGP and EBGP routing in a WAN fabric 3 | # 4 | --- 5 | - name: Create and deploy BGP configurations 6 | hosts: all 7 | vars: 8 | configs: "{{inventory_dir}}/configs" 9 | tasks: 10 | - include_vars: "{{ item }}" 11 | with_first_found: 12 | - nodes.yml 13 | - "{{ inventory_dir }}/nodes.yml" 14 | tags: [ configs,verify,deploy ] 15 | - include: ../tools/clean.yml 16 | tags: [ clean ] 17 | - include: config_bgp.yml 18 | tags: [ configs ] 19 | - include: deploy_bgp.yml 20 | tags: [ deploy ] 21 | - include: verify_bgp.yml 22 | tags: [ verify ] 23 | -------------------------------------------------------------------------------- /bgp/deploy_bgp.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy IBGP and EBGP routing in a WAN fabric 3 | # 4 | --- 5 | - include: ../tools/log_changes_init.yml 6 | 7 | - name: Deploy configurations 8 | ios_config: 9 | provider: "{{ios_provider}}" 10 | src: "{{configs}}/{{inventory_hostname}}.bgp.cfg" 11 | register: changes 12 | 13 | - include: ../tools/log_changes.yml component=BGP 14 | 15 | - name: Refresh BGP routers 16 | ios_command: 17 | provider: "{{ios_provider}}" 18 | commands: 19 | - "clear ip bgp * all soft out" 20 | register: bgp_neighbors 21 | 22 | - set_fact: deploy=1 23 | -------------------------------------------------------------------------------- /bgp/verify_bgp.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy IBGP and EBGP routing in a WAN fabric 3 | # 4 | --- 5 | - name: Wait for BGP to start 6 | pause: seconds=60 prompt="Waiting 60 seconds for BGP to start" 7 | tags: [ verify ] 8 | when: deploy is defined 9 | 10 | - set_fact: 11 | sessions: "{{ nodes[inventory_hostname].ibgp | union(nodes[inventory_hostname].ebgp|default([])) }}" 12 | tags: [ verify ] 13 | 14 | - name: Collect BGP neighbors 15 | ios_command: 16 | provider: "{{ios_provider}}" 17 | commands: 18 | - "show ip bgp summary | include ^[0-9]+\\." 19 | register: bgp_neighbors 20 | tags: [ verify ] 21 | 22 | - name: Verify BGP neighbors are configured 23 | assert: 24 | that: "'{{ item.ip }}' in bgp_neighbors.stdout[0]" 25 | msg: "BGP neighbor {{ item.rid }}/{{item.ip}} is not configured" 26 | with_items: "{{ sessions }}" 27 | tags: [ verify ] 28 | 29 | - name: Verify BGP sessions are up and running 30 | assert: 31 | that: | 32 | {% set pattern = '^'~item.ip~'.*\s+(\d+)$' %} 33 | {{ bgp_neighbors.stdout[0] | regex_findall(pattern,'\1')|length > 0 }} 34 | msg: > 35 | {% set pattern = '^'~item.ip~'.*\s+(\D+)$' %} 36 | Session with BGP neighbor {{ item.rid }}/{{item.ip}} is not established 37 | state is {{ bgp_neighbors.stdout[0] | regex_findall(pattern,'\1') | join("") }} 38 | with_items: "{{ sessions }}" 39 | tags: [ verify ] 40 | -------------------------------------------------------------------------------- /create-data-model.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | gather_facts: no 4 | vars_files: [ fabric.yml ] 5 | tasks: 6 | - name: Create per-node data model from fabric data model 7 | template: src=model/nodes.j2 dest=./nodes.yml -------------------------------------------------------------------------------- /fabric.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Fabric definition 3 | # 4 | --- 5 | fabric: 6 | - {left: E1, left_ip: 10.0.0.21, left_port: GigabitEthernet0/2, 7 | right: E2, right_ip: 10.0.0.22, right_port: GigabitEthernet0/2, 8 | cost: 5 } 9 | - {left: E1, left_ip: 10.0.0.13, left_port: GigabitEthernet0/1, 10 | right: PE1, right_ip: 10.0.0.14, right_port: GigabitEthernet0/1, 11 | cost: 10 } 12 | - {left: E2, left_ip: 10.0.0.17, left_port: GigabitEthernet0/1, 13 | right: PE1, right_ip: 10.0.0.18, right_port: GigabitEthernet0/2, 14 | cost: 1 } 15 | - {left: E3, left_ip: 10.0.0.41, left_port: GigabitEthernet0/2, 16 | right: E4, right_ip: 10.0.0.42, right_port: GigabitEthernet0/2 } 17 | - {left: E3, left_ip: 10.0.0.29, left_port: GigabitEthernet0/3, 18 | right: PE2, right_ip: 10.0.0.30, right_port: GigabitEthernet0/1 } 19 | - {left: E4, left_ip: 10.0.0.37, left_port: GigabitEthernet0/3, 20 | right: PE2, right_ip: 10.0.0.38, right_port: GigabitEthernet0/2 } 21 | 22 | interas: 23 | - {left: E1, left_as: '64500', left_ip: 10.0.0.25, left_port: GigabitEthernet0/3, 24 | right: E3, right_as: '64501', right_ip: 10.0.0.26, right_port: GigabitEthernet0/1} 25 | - {left: E2, left_as: '64500', left_ip: 10.0.0.33, left_port: GigabitEthernet0/3, 26 | right: E4, right_as: '64501', right_ip: 10.0.0.34, right_port: GigabitEthernet0/1} 27 | 28 | asn: 29 | 64500: 30 | members: [ E1,E2,PE1 ] 31 | rr: [ E1,E2 ] 32 | 64501: 33 | members: [ E3,E4,PE2 ] 34 | rr: [ E3,E4 ] 35 | 36 | services: [ IPv4, VPNv4,InterAS ] 37 | 38 | nodes: 39 | - name: E1 40 | mgmt: 172.16.1.110 41 | rid: 192.168.0.2 42 | - name: E2 43 | mgmt: 172.16.1.111 44 | rid: 192.168.0.4 45 | - name: E3 46 | mgmt: 172.16.1.120 47 | rid: 192.168.0.5 48 | - name: E4 49 | mgmt: 172.16.1.121 50 | rid: 192.168.0.6 51 | - name: PE1 52 | mgmt: 172.16.1.112 53 | rid: 192.168.0.1 54 | - name: PE2 55 | mgmt: 172.16.1.122 56 | rid: 192.168.0.3 57 | -------------------------------------------------------------------------------- /group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ios_provider: 3 | username: "{{ansible_user}}" 4 | password: "{{ansible_ssh_pass}}" 5 | host: "{{ip|default(ansible_host)|default(inventory_hostname)}}" 6 | transport: cli 7 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | [AS64500] 2 | E1 ansible_host=172.16.1.110 3 | E2 ansible_host=172.16.1.111 4 | PE1 ansible_host=172.16.1.112 5 | 6 | [AS64501] 7 | E3 ansible_host=172.16.1.120 8 | E4 ansible_host=172.16.1.121 9 | PE2 ansible_host=172.16.1.122 10 | 11 | [all:vars] 12 | ansible_user=cisco 13 | #ansible_connection=local 14 | ansible_ssh_pass=cisco 15 | snmp_community=cisco 16 | os=ios 17 | -------------------------------------------------------------------------------- /model/nodes.j2: -------------------------------------------------------------------------------- 1 | # 2 | # Nodes in the network 3 | # 4 | {% macro internal_link(name,ip,cost) %} 5 | {{ name }}: { ip: {{ip}} {% if cost %}, cost: {{cost}}{% endif %} }{% endmacro %} 6 | {% macro ibgp_session(from,to,ip) %} 7 | {% if from != to %} 8 | - { rid: {{to}}, ip: "{{ip}}" }{% endif %}{% endmacro %} 9 | {% macro ebgp_session(from,to,as,ip,ecnt) %} 10 | {% if from != to %} 11 | - { rid: {{to}}, as: {{as}}, ip: "{{ip}}" }{% endif %}{% endmacro %} 12 | 13 | --- 14 | {% if services is defined %} 15 | services: {{services|to_json}} 16 | {% endif %} 17 | 18 | nodes: 19 | {% for node in nodes %} 20 | 21 | {{ node.name }}: 22 | mgmt: {{ node.mgmt }} 23 | rid: {{ node.rid }} 24 | {# 25 | Create IBGP information: 26 | * Find ASN that has current node name as one of its members 27 | * Create AS variable 28 | * Create a list of IBGP sessions 29 | 30 | IBGP sessions are established between RRs and all other 31 | nodes in the AS 32 | #} 33 | {% for k,v in asn|dictsort() %} 34 | {% if node.name in v.members %} 35 | as: {{ k }} 36 | {% if node.name in v.rr %} 37 | rr: yes 38 | {% endif %} 39 | ibgp: 40 | {% if node.name in v.rr %} 41 | {% for rid in v.members %} 42 | {% set neighbor = nodes|selectattr('name','equalto',rid)|first %} 43 | {{ibgp_session(node.name,rid,neighbor.rid)}} 44 | {% endfor %} 45 | {% else %} 46 | {% for rid in v.rr %} 47 | {% set neighbor = nodes|selectattr('name','equalto',rid)|first %} 48 | {{ibgp_session(node.name,rid,neighbor.rid)}} 49 | {% endfor %} 50 | {% endif %} 51 | {% endif %} 52 | {% endfor %} 53 | 54 | {% set ecnt = 0 %} 55 | {% for s in interas %} 56 | {% if s.left == node.name %} 57 | {% if ecnt == 0 %} 58 | ebgp: 59 | {% endif %} 60 | {{ebgp_session(node.name,s.right,s.right_as,s.right_ip,ecnt)}} 61 | {% set ecnt = ecnt + 1 %} 62 | {% endif %} 63 | {% if s.right == node.name %} 64 | {% if ecnt == 0 %} 65 | ebgp: 66 | {% endif %} 67 | {{ebgp_session(node.name,s.left,s.left_as,s.left_ip,ecnt)}} 68 | {% set ecnt = ecnt + 1 %} 69 | {% endif %} 70 | {% endfor %} 71 | internal: 72 | {% for link in fabric %} 73 | {% if link.left == node.name %} 74 | {{ internal_link(link.left_port,link.left_ip,link.cost|default('')) }} 75 | {% elif link.right == node.name %} 76 | {{ internal_link(link.right_port,link.right_ip, link.cost|default('')) }} 77 | {% endif %} 78 | {% endfor %} 79 | {% endfor %} 80 | -------------------------------------------------------------------------------- /nodes.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Nodes in the network 3 | # 4 | 5 | --- 6 | services: ["IPv4", "VPNv4", "InterAS"] 7 | 8 | nodes: 9 | 10 | E1: 11 | mgmt: 172.16.1.110 12 | rid: 192.168.0.2 13 | as: 64500 14 | rr: yes 15 | ibgp: 16 | 17 | - { rid: E2, ip: "192.168.0.4" } 18 | - { rid: PE1, ip: "192.168.0.1" } 19 | 20 | ebgp: 21 | - { rid: E3, as: 64501, ip: "10.0.0.26" } 22 | internal: 23 | GigabitEthernet0/2: { ip: 10.0.0.21 , cost: 5 } 24 | GigabitEthernet0/1: { ip: 10.0.0.13 , cost: 10 } 25 | 26 | E2: 27 | mgmt: 172.16.1.111 28 | rid: 192.168.0.4 29 | as: 64500 30 | rr: yes 31 | ibgp: 32 | - { rid: E1, ip: "192.168.0.2" } 33 | 34 | - { rid: PE1, ip: "192.168.0.1" } 35 | 36 | ebgp: 37 | - { rid: E4, as: 64501, ip: "10.0.0.34" } 38 | internal: 39 | GigabitEthernet0/2: { ip: 10.0.0.22 , cost: 5 } 40 | GigabitEthernet0/1: { ip: 10.0.0.17 , cost: 1 } 41 | 42 | E3: 43 | mgmt: 172.16.1.120 44 | rid: 192.168.0.5 45 | as: 64501 46 | rr: yes 47 | ibgp: 48 | 49 | - { rid: E4, ip: "192.168.0.6" } 50 | - { rid: PE2, ip: "192.168.0.3" } 51 | 52 | ebgp: 53 | - { rid: E1, as: 64500, ip: "10.0.0.25" } 54 | internal: 55 | GigabitEthernet0/2: { ip: 10.0.0.41 } 56 | GigabitEthernet0/3: { ip: 10.0.0.29 } 57 | 58 | E4: 59 | mgmt: 172.16.1.121 60 | rid: 192.168.0.6 61 | as: 64501 62 | rr: yes 63 | ibgp: 64 | - { rid: E3, ip: "192.168.0.5" } 65 | 66 | - { rid: PE2, ip: "192.168.0.3" } 67 | 68 | ebgp: 69 | - { rid: E2, as: 64500, ip: "10.0.0.33" } 70 | internal: 71 | GigabitEthernet0/2: { ip: 10.0.0.42 } 72 | GigabitEthernet0/3: { ip: 10.0.0.37 } 73 | 74 | PE1: 75 | mgmt: 172.16.1.112 76 | rid: 192.168.0.1 77 | as: 64500 78 | ibgp: 79 | - { rid: E1, ip: "192.168.0.2" } 80 | - { rid: E2, ip: "192.168.0.4" } 81 | 82 | internal: 83 | GigabitEthernet0/1: { ip: 10.0.0.14 , cost: 10 } 84 | GigabitEthernet0/2: { ip: 10.0.0.18 , cost: 1 } 85 | 86 | PE2: 87 | mgmt: 172.16.1.122 88 | rid: 192.168.0.3 89 | as: 64501 90 | ibgp: 91 | - { rid: E3, ip: "192.168.0.5" } 92 | - { rid: E4, ip: "192.168.0.6" } 93 | 94 | internal: 95 | GigabitEthernet0/1: { ip: 10.0.0.30 } 96 | GigabitEthernet0/2: { ip: 10.0.0.38 } 97 | -------------------------------------------------------------------------------- /ospf/config_ospf.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Create OSPF configuration 3 | # 4 | --- 5 | - name: Create configuration directory 6 | local_action: file path={{configs}} state=directory 7 | run_once: true 8 | check_mode: no 9 | changed_when: no 10 | 11 | - name: Create configurations 12 | template: src=ospf-config.j2 dest={{configs}}/{{inventory_hostname}}.ospf.cfg 13 | check_mode: no 14 | -------------------------------------------------------------------------------- /ospf/deploy.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy OSPF routing in a WAN fabric 3 | # 4 | --- 5 | - name: Create and deploy OSPF configurations 6 | hosts: all 7 | vars: 8 | configs: "{{inventory_dir}}/configs" 9 | tasks: 10 | - include_vars: "{{ item }}" 11 | with_first_found: 12 | - nodes.yml 13 | - "{{ inventory_dir }}/nodes.yml" 14 | tags: [ configs,verify ] 15 | - include: ../tools/clean.yml 16 | tags: [ clean ] 17 | - include: config_ospf.yml 18 | tags: [ configs ] 19 | - include: deploy_ospf.yml 20 | tags: [ deploy ] 21 | - include: verify_ospf.yml 22 | tags: [ verify ] 23 | -------------------------------------------------------------------------------- /ospf/deploy_ospf.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy OSPF routing in a WAN fabric 3 | # 4 | --- 5 | - include: ../tools/log_changes_init.yml 6 | 7 | - name: Deploy configurations 8 | ios_config: 9 | provider: "{{ios_provider}}" 10 | src: "{{configs}}/{{inventory_hostname}}.ospf.cfg" 11 | register: changes 12 | 13 | - include: ../tools/log_changes.yml component=OSPF 14 | -------------------------------------------------------------------------------- /ospf/ospf-config-from-fabric.j2: -------------------------------------------------------------------------------- 1 | {% macro internal_link(name) %} 2 | interface {{ name }} 3 | ip ospf 1 area 0 4 | {% endmacro %} 5 | 6 | {% set node = nodes|selectattr('name','equalto',inventory_hostname)|first %} 7 | 8 | default router ospf 1 9 | router ospf 1 10 | router-id {{ node.rid }} 11 | 12 | {% for link in fabric %} 13 | {% if inventory_hostname == link.left %} 14 | {{ internal_link(link.left_port) }} 15 | {% elif inventory_hostname == link.right %} 16 | {{ internal_link(link.right_port) }} 17 | {% endif %} 18 | {% endfor %} -------------------------------------------------------------------------------- /ospf/ospf-config.j2: -------------------------------------------------------------------------------- 1 | {% set node = nodes[inventory_hostname] %} 2 | {% if node.internal is defined %} 3 | router ospf 1 4 | router-id {{ node.rid }} 5 | ! 6 | interface Loopback0 7 | ip ospf 1 area 0 8 | ! 9 | {% for intf,data in node.internal|dictsort %} 10 | interface {{intf}} 11 | ip ospf 1 area 0 12 | ip ospf hello-interval 3 13 | ip ospf dead-interval 10 14 | ip ospf network point-to-multipoint 15 | {% if data.cost is defined %} 16 | ip ospf cost {{data.cost}} 17 | {% endif %} 18 | {% if "VPNv4" in services %} 19 | mpls ip 20 | {% endif %} 21 | ! 22 | {% endfor %} 23 | {% endif %} 24 | -------------------------------------------------------------------------------- /ospf/verify_ospf.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy OSPF routing in a WAN fabric 3 | # 4 | --- 5 | - name: Wait for OSPF to start 6 | pause: seconds=15 prompt="Waiting for OSPF to start" 7 | tags: [ verify ] 8 | 9 | - name: Collect OSPF neighbors 10 | ios_command: 11 | provider: "{{ios_provider}}" 12 | commands: 13 | - "show ip ospf neighbor | include ^[1-9]" 14 | register: ospf_neighbors 15 | tags: [ verify ] 16 | 17 | - name: Verify OSPF is running on all internal interfaces 18 | assert: 19 | that: "'{{ item.key }}' in ospf_neighbors.stdout[0]" 20 | msg: "OSPF session in interface {{item.key}} is missing" 21 | with_dict: "{{ nodes[inventory_hostname].internal }}" 22 | tags: [ verify ] 23 | -------------------------------------------------------------------------------- /services-vpnv4.yml: -------------------------------------------------------------------------------- 1 | # 2 | # VPNv4 service definition 3 | # 4 | # Data model 5 | # * Dictionary of customers 6 | # * Each customer has 7 | # - common parameters (RD, RT) 8 | # - dictionary of nodes 9 | # * Each node has dictionary of interfaces, each interface has key ip (prefix) 10 | 11 | C1: 12 | rd: "64500:1" 13 | nodes: 14 | PE1: 15 | Loopback1: { ip: 172.16.1.1/24 } 16 | PE2: 17 | Loopback1: { ip: 172.16.2.1/24 } -------------------------------------------------------------------------------- /tools/clean.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy IBGP and EBGP routing in a WAN fabric 3 | # 4 | --- 5 | - name: Clean configuration directory 6 | local_action: file path={{configs}} state=absent 7 | run_once: true 8 | check_mode: no 9 | changed_when: no 10 | tags: [ clean ] 11 | -------------------------------------------------------------------------------- /tools/create_config_dir.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Create configuration directory if it doesn't exist 3 | # 4 | --- 5 | - name: Create configuration directory 6 | local_action: file path={{configs}} state=directory 7 | run_once: true 8 | check_mode: no 9 | changed_when: no 10 | -------------------------------------------------------------------------------- /tools/log_changes.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Create files documenting changes made to individual devices 3 | # 4 | --- 5 | - name: Cleanup old changes file 6 | local_action: file path={{configs}}/changes/{{inventory_hostname}}.{{component}}.changes state=absent 7 | check_mode: no 8 | changed_when: no 9 | 10 | - name: Document changes 11 | copy: 12 | content: | 13 | ************************************* 14 | {{component}} changes on {{inventory_hostname}} 15 | ************************************* 16 | {% for line in changes.commands %} 17 | {{ line }} 18 | {% endfor %} 19 | dest: "{{configs}}/changes/{{inventory_hostname}}.{{component}}.changes" 20 | delegate_to: localhost 21 | check_mode: no 22 | when: changes.commands|default([])|length 23 | 24 | - assemble: src={{configs}}/changes dest={{configs}}/changes.txt 25 | check_mode: no 26 | changed_when: no 27 | delegate_to: localhost 28 | run_once: "{{allnodes|default(true)}}" 29 | when: true 30 | 31 | - name: Cleanup changes files 32 | local_action: file path={{configs}}/changes/{{inventory_hostname}}.{{component}}.changes state=absent 33 | check_mode: no 34 | changed_when: no 35 | -------------------------------------------------------------------------------- /tools/log_changes_init.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Create directory for change logging 3 | # 4 | --- 5 | - name: Create configuration changes directory 6 | local_action: file path={{configs}}/changes state=directory 7 | check_mode: no 8 | changed_when: no 9 | -------------------------------------------------------------------------------- /vpnv4/config_vpnv4.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy VPNv4 services from services-vpnv4 definition 3 | # 4 | --- 5 | - name: Create configurations 6 | template: src=vpnv4-config.j2 dest={{configs}}/{{inventory_hostname}}.vpnv4.cfg 7 | check_mode: no 8 | tags: [ configs ] 9 | -------------------------------------------------------------------------------- /vpnv4/deploy.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy VPNv4 services from services-vpnv4 definition 3 | # 4 | --- 5 | - name: Create and deploy VPNv4 configurations 6 | hosts: all 7 | vars: 8 | configs: "{{inventory_dir}}/configs" 9 | services: "{{inventory_dir}}/services-vpnv4.yml" 10 | allnodes: false 11 | tasks: 12 | - include: vars_vpnv4.yml 13 | tags: [ configs,deploy ] 14 | - include: ../tools/clean.yml 15 | tags: [ clean ] 16 | - include: ../tools/create_config_dir.yml 17 | - include: config_vpnv4.yml 18 | tags: [ configs ] 19 | when: customers|length > 0 20 | - include: deploy_vpnv4.yml 21 | tags: [ deploy ] 22 | when: customers|length > 0 23 | -------------------------------------------------------------------------------- /vpnv4/deploy_vpnv4.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Deploy VPNv4 services from configuration files 3 | # 4 | --- 5 | - include: ../tools/log_changes_init.yml 6 | 7 | - name: Deploy configurations 8 | ios_config: 9 | provider: "{{ios_provider}}" 10 | src: "{{configs}}/{{inventory_hostname}}.vpnv4.cfg" 11 | register: changes 12 | 13 | - include: ../tools/log_changes.yml component=VPNv4 14 | -------------------------------------------------------------------------------- /vpnv4/list-of-customers.json.j2: -------------------------------------------------------------------------------- 1 | {% set cnt = 0 %} 2 | [ 3 | {% for cust,vpn in vpnv4|dictsort %} 4 | {% if inventory_hostname in vpn.nodes.keys() %} 5 | {% if cnt > 0 %},"{% endif %}"{{ cust }}" 6 | {% set cnt = cnt + 1 %} 7 | {% endif %} 8 | {% endfor %} 9 | ] -------------------------------------------------------------------------------- /vpnv4/vars_vpnv4.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Read VPNv4 variables 3 | # 4 | --- 5 | - include_vars: 6 | file: "{{ services }}" 7 | name: vpnv4 8 | 9 | - include_vars: "{{ item }}" 10 | with_first_found: 11 | - nodes.yml 12 | - "{{ inventory_dir }}/nodes.yml" 13 | tags: [ configs,deploy ] 14 | 15 | - set_fact: customers="{{ lookup('template','list-of-customers.json.j2') }}" 16 | -------------------------------------------------------------------------------- /vpnv4/vpnv4-config.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Create VRF definitions 3 | #} 4 | {% for cust,vpn in vpnv4|dictsort %} 5 | {% for node,def in vpn.nodes|dictsort %} 6 | {% if inventory_hostname == node %} 7 | vrf definition {{ cust }} 8 | rd {{ vpn.rd }} 9 | route-target import {{ vpn.rt | default(vpn.rd) }} 10 | route-target export {{ vpn.rt | default(vpn.rd) }} 11 | address-family ipv4 12 | ! 13 | {% endif %} 14 | {% endfor %} 15 | {% endfor %} 16 | {# 17 | Create interfaces 18 | #} 19 | {% for cust,vpn in vpnv4|dictsort %} 20 | {% for node,def in vpn.nodes|dictsort %} 21 | {% if inventory_hostname == node %} 22 | {% for ifname,ifdef in def|dictsort %} 23 | interface {{ ifname }} 24 | vrf forwarding {{ cust }} 25 | ip address {{ifdef.ip | ipaddr('address')}} {{ifdef.ip | ipaddr('netmask')}} 26 | ! 27 | {% endfor %} 28 | {% endif %} 29 | {% endfor %} 30 | {% endfor %} 31 | {# 32 | Create VPN address families in BGP process 33 | #} 34 | router bgp {{ nodes[inventory_hostname].as }} 35 | {% for cust,vpn in vpnv4|dictsort %} 36 | {% for node,def in vpn.nodes|dictsort %} 37 | {% if inventory_hostname == node %} 38 | {% for ifname,ifdef in def|dictsort %} 39 | address-family ipv4 vrf {{ cust }} 40 | maximum-paths eibgp 4 41 | redistribute connected 42 | ! 43 | {% endfor %} 44 | {% endif %} 45 | {% endfor %} 46 | {% endfor %} 47 | --------------------------------------------------------------------------------