├── automation ├── host_vars │ └── .gitignore ├── collections │ └── .gitignore ├── requirements.yml ├── validation │ └── workshop │ │ ├── configs │ │ └── .gitignore │ │ └── data │ │ └── bf_facts │ │ └── .gitignore ├── roles │ ├── deploy │ │ ├── tasks │ │ │ └── main.yml │ │ ├── defaults │ │ │ └── main.yml │ │ └── templates │ │ │ └── template-config-all.j2 │ ├── validate │ │ ├── defaults │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── config.j2 │ └── monitoring │ │ └── tasks │ │ └── main.yml ├── playbooks │ ├── validate.yaml │ ├── intended.yaml │ └── ping.yaml ├── ansible.cfg └── inventory.yml ├── .gitignore ├── docs └── imgs │ ├── .gitkeep │ ├── stack.png │ ├── iac-broken.png │ ├── pipeline.png │ ├── gitlab-login.png │ ├── grafana-data.png │ ├── gitlab-pipeline.png │ ├── gitlab-project.png │ ├── grafana-empty.png │ ├── pipeline-failed.png │ ├── gitlab-project-info.png │ ├── gitlab-stage-detail.png │ ├── gitlab-create-project.png │ ├── gitlab-pipeline-finished.png │ ├── gitlab-pipeline-progress.png │ └── pipeline-failed-details.png ├── configs ├── Leaf-1.yml ├── Leaf-2.yml └── Spine-1.yml ├── gitlab-ci.yml ├── LICENSE └── README.md /automation/host_vars/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build/toopology/cEOS-Lab.tar.xz -------------------------------------------------------------------------------- /docs/imgs/.gitkeep: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /docs/imgs/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/stack.png -------------------------------------------------------------------------------- /automation/collections/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /docs/imgs/iac-broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/iac-broken.png -------------------------------------------------------------------------------- /docs/imgs/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/pipeline.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-login.png -------------------------------------------------------------------------------- /docs/imgs/grafana-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/grafana-data.png -------------------------------------------------------------------------------- /automation/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - lvrfrc87.git_acp 4 | - arista.eos 5 | 6 | roles: 7 | - batfish.base 8 | -------------------------------------------------------------------------------- /docs/imgs/gitlab-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-pipeline.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-project.png -------------------------------------------------------------------------------- /docs/imgs/grafana-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/grafana-empty.png -------------------------------------------------------------------------------- /docs/imgs/pipeline-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/pipeline-failed.png -------------------------------------------------------------------------------- /automation/validation/workshop/configs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /automation/validation/workshop/data/bf_facts/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /docs/imgs/gitlab-project-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-project-info.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-stage-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-stage-detail.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-create-project.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-pipeline-finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-pipeline-finished.png -------------------------------------------------------------------------------- /docs/imgs/gitlab-pipeline-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/gitlab-pipeline-progress.png -------------------------------------------------------------------------------- /docs/imgs/pipeline-failed-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vista-Technology/netops-quickstart/HEAD/docs/imgs/pipeline-failed-details.png -------------------------------------------------------------------------------- /automation/roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure device 3 | arista.eos.eos_config: 4 | src: template-config-all.j2 5 | replace: config 6 | match: none 7 | -------------------------------------------------------------------------------- /automation/roles/deploy/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | management: 3 | username: arista 4 | password: $6$H9hv1.QIXjv.Wb0I$ghbukD6MpBy.ONKkImqSv3tCA5Wz1FOeXgTbgw1QCRa8EWSiMRY5xrtzs57FvZXudZI/J3/NJwkBXLsFDG7gu0 -------------------------------------------------------------------------------- /automation/roles/validate/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | management: 3 | username: arista 4 | password: $6$H9hv1.QIXjv.Wb0I$ghbukD6MpBy.ONKkImqSv3tCA5Wz1FOeXgTbgw1QCRa8EWSiMRY5xrtzs57FvZXudZI/J3/NJwkBXLsFDG7gu0 5 | -------------------------------------------------------------------------------- /automation/playbooks/validate.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Validate configurations 3 | hosts: lab 4 | connection: local 5 | gather_facts: false 6 | 7 | roles: 8 | - batfish.base 9 | - validate 10 | -------------------------------------------------------------------------------- /automation/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hash_behaviour=merge 3 | inventory = inventory.yml 4 | roles_path=./roles 5 | collections_path=./collections 6 | deprecation_warnings=False 7 | command_warnings=False 8 | retry_files_enabled = False 9 | host_key_checking = False 10 | -------------------------------------------------------------------------------- /automation/playbooks/intended.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Apply intended configuration and set monitoring 3 | hosts: lab 4 | connection: network_cli 5 | become: yes 6 | become_method: enable 7 | gather_facts: false 8 | 9 | roles: 10 | - deploy 11 | - monitoring 12 | -------------------------------------------------------------------------------- /configs/Leaf-1.yml: -------------------------------------------------------------------------------- 1 | interfaces: 2 | - name: Ethernet1 3 | ipv4: 10.0.254.1/31 4 | routerid: 1.1.1.1/32 5 | bgp: 6 | asn: 65001 7 | neighbours: 8 | - ipv4: 10.0.254.0 9 | remote_asn: 65100 10 | vlan: 11 | id: 10 12 | svi: 192.168.10.1/24 13 | interface: Ethernet10 14 | -------------------------------------------------------------------------------- /configs/Leaf-2.yml: -------------------------------------------------------------------------------- 1 | interfaces: 2 | - name: Ethernet1 3 | ipv4: 10.0.254.3/31 4 | routerid: 1.1.1.2/32 5 | bgp: 6 | asn: 65002 7 | neighbours: 8 | - ipv4: 10.0.254.2 9 | remote_asn: 65100 10 | vlan: 11 | id: 20 12 | svi: 192.168.20.1/24 13 | interface: Ethernet10 14 | -------------------------------------------------------------------------------- /configs/Spine-1.yml: -------------------------------------------------------------------------------- 1 | interfaces: 2 | - name: Ethernet1 3 | ipv4: 10.0.254.0/31 4 | - name: Ethernet2 5 | ipv4: 10.0.254.2/31 6 | routerid: 1.1.1.100/32 7 | bgp: 8 | asn: 65100 9 | neighbours: 10 | - ipv4: 10.0.254.1 11 | remote_asn: 65001 12 | - ipv4: 10.0.254.3 13 | remote_asn: 65002 14 | -------------------------------------------------------------------------------- /automation/playbooks/ping.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test reachability from Hosts 3 | hosts: testing 4 | gather_facts: false 5 | 6 | tasks: 7 | - name: Ping destination 8 | shell: "ping -c 1 -w 2 {{ ip_to_ping }} " 9 | register: output 10 | 11 | - name: Print result 12 | debug: 13 | msg: "{{output.stdout_lines}}" 14 | 15 | -------------------------------------------------------------------------------- /automation/roles/monitoring/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Register Consul service 3 | community.general.consul: 4 | service_name: "Arista Monitoring" 5 | service_port: 8080 6 | service_id: "{{ inventory_hostname }}" 7 | service_address: "{{ inventory_hostname }}" 8 | host: "{{ consul_host }}" 9 | tags: 10 | - "_device={{ inventory_hostname }}" 11 | -------------------------------------------------------------------------------- /automation/inventory.yml: -------------------------------------------------------------------------------- 1 | lab: 2 | vars: 3 | ansible_connection: network_cli 4 | ansible_network_os: eos 5 | ansible_user: arista 6 | ansible_httpapi_pass: arista 7 | ansible_ssh_pass: arista 8 | ansible_password: arista 9 | ansible_become: true 10 | ansible_become_method: enable 11 | ansible_httpapi_use_ssl: true 12 | ansible_httpapi_validate_certs: false 13 | logging_remote_host: 14 | logging_remote_port: 51400 15 | consul_host: consul 16 | children: 17 | Spines: 18 | hosts: 19 | Spine-1 20 | Leafs: 21 | hosts: 22 | Leaf-1 23 | Leaf-2 24 | testing: 25 | vars: 26 | ansible_become: true 27 | ansible_user: alpine 28 | ansible_ssh_private_key_file: demo.key 29 | children: 30 | alpine: 31 | hosts: 32 | Host-1: 33 | ip_to_ping: 192.168.20.20 34 | Host-2: 35 | ip_to_ping: 192.168.10.10 36 | 37 | -------------------------------------------------------------------------------- /gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - validate 3 | - deploy 4 | - test 5 | 6 | batfish-validation: 7 | stage: validate 8 | script: 9 | - chmod 755 automation 10 | - cp configs/*.yml automation/host_vars/ 11 | - cd automation 12 | - ansible-galaxy install -r requirements.yml 13 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/validate.yaml 14 | 15 | deploy-configurations: 16 | stage: deploy 17 | script: 18 | - chmod 755 automation 19 | - cp configs/*.yml automation/host_vars/ 20 | - cd automation 21 | - ansible-galaxy install -r requirements.yml 22 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/intended.yaml 23 | 24 | ping-test: 25 | stage: test 26 | script: 27 | - chmod 755 automation 28 | - cp configs/*.yml automation/host_vars/ 29 | - cd automation 30 | - chmod 400 demo.key 31 | - ansible-galaxy install -r requirements.yml 32 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/ping.yaml 33 | -------------------------------------------------------------------------------- /automation/roles/validate/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Generate intended config to test 2 | template: 3 | src: config.j2 4 | dest: "validation/workshop/configs/{{inventory_hostname}}" 5 | 6 | - name: Setup connection to Batfish service 7 | bf_session: 8 | host: batfish 9 | name: local_batfish 10 | delegate_to: localhost 11 | run_once: true 12 | 13 | - name: Initialize the example network 14 | bf_init_snapshot: 15 | network: workshop 16 | snapshot: workshop 17 | snapshot_data: ../validation/workshop 18 | overwrite: true 19 | run_once: true 20 | delegate_to: localhost 21 | 22 | - name: Retrieve Batfish Facts 23 | bf_extract_facts: 24 | output_directory: ../validation/workshop/data/bf_facts 25 | register: bf_facts 26 | run_once: true 27 | delegate_to: localhost 28 | 29 | - name: Display neighbourships for all nodes 30 | debug: 31 | msg: " {{item.value.BGP.Neighbors}} " 32 | with_dict: "{{bf_facts.result.nodes}}" 33 | loop_control: 34 | label: "{{item.key}}.BGP.Neighbors" 35 | run_once: true 36 | delegate_to: localhost 37 | 38 | - name: Validate the configuration of network devices 39 | bf_assert: 40 | assertions: 41 | - type: assert_no_undefined_references 42 | name: Confirm that there are NO undefined references on any network device 43 | - type: assert_no_incompatible_bgp_sessions 44 | name: Confirm that all BGP peers are properly configured 45 | - type: assert_no_unestablished_bgp_sessions 46 | name: Confirm that all compatible BGP peers establish sessions 47 | run_once: true 48 | delegate_to: localhost 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Michael Kashin 4 | Copyright (c) 2021, Vista Technology 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /automation/roles/validate/templates/config.j2: -------------------------------------------------------------------------------- 1 | ! device: {{ inventory_hostname }} (cEOSLab, EOS-4.25.1F-20001546.4251F (engineering build)) 2 | ! 3 | hostname {{ inventory_hostname }} 4 | ! 5 | spanning-tree mode mstp 6 | ! 7 | no aaa root 8 | ! 9 | aaa authorization exec default local 10 | ! 11 | username {{ management.username }} privilege 15 secret sha512 {{ management.password }} 12 | ! 13 | management api http-commands 14 | no shutdown 15 | {% for intf in interfaces %} 16 | ! 17 | interface {{ intf.name }} 18 | no switchport 19 | ip address {{ intf.ipv4 }} 20 | {% endfor %} 21 | ! 22 | {% if vlan is defined %} 23 | vlan {{ vlan.id }} 24 | ! 25 | interface Vlan{{ vlan.id }} 26 | ip address {{ vlan.svi }} 27 | ! 28 | interface {{ vlan.interface }} 29 | switchport 30 | switchport mode access 31 | switchport access vlan {{ vlan.id }} 32 | {% endif %} 33 | {% if bgp is defined %} 34 | ! 35 | ip routing 36 | ! 37 | route-map RMAP-CONNECTED-BGP permit 1000 38 | ! 39 | interface Loopback0 40 | description ROUTER-ID 41 | ip address {{ routerid }} 42 | ! 43 | router bgp {{ bgp.asn }} 44 | {% set rid = routerid.split('/') %} 45 | router-id {{ rid[0] }} 46 | {% for neighbor in bgp.neighbours %} 47 | {% set peer_ip = neighbor.ipv4 | ipaddr('address') %} 48 | neighbor {{ peer_ip }} remote-as {{ neighbor.remote_asn }} 49 | neighbor {{ peer_ip }} send-community 50 | neighbor {{ peer_ip }} maximum-routes 12000 51 | {% endfor %} 52 | redistribute connected route-map RMAP-CONNECTED-BGP 53 | maximum-paths 2 54 | ! 55 | {% endif %} 56 | ! 57 | logging host {{ logging_remote_host }} {{ logging_remote_port }} protocol tcp 58 | logging format hostname fqdn 59 | ! 60 | end 61 | -------------------------------------------------------------------------------- /automation/roles/deploy/templates/template-config-all.j2: -------------------------------------------------------------------------------- 1 | ! 2 | hostname {{ inventory_hostname }} 3 | ! 4 | spanning-tree mode mstp 5 | ! 6 | no aaa root 7 | ! 8 | aaa authorization exec default local 9 | ! 10 | username {{ management.username }} privilege 15 secret sha512 {{ management.password }} 11 | ! 12 | ip access-list def2 13 | 9 permit tcp any any eq 8080 14 | 10 permit icmp any any 15 | 20 permit ip any any tracked 16 | 30 permit udp any any eq bfd ttl eq 255 17 | 40 permit udp any any eq bfd-echo ttl eq 254 18 | 50 permit udp any any eq multihop-bfd 19 | 60 permit udp any any eq micro-bfd 20 | 70 permit ospf any any 21 | 80 permit tcp any any eq ssh telnet www snmp bgp https msdp ldp netconf-ssh gnmi 22 | 90 permit udp any any eq bootps bootpc snmp rip ntp ldp 23 | 100 permit tcp any any eq mlag ttl eq 255 24 | 110 permit udp any any eq mlag ttl eq 255 25 | 120 permit vrrp any any 26 | 130 permit ahp any any 27 | 140 permit pim any any 28 | 150 permit igmp any any 29 | 160 permit tcp any any range 5900 5910 30 | 170 permit tcp any any range 50000 50100 31 | 180 permit udp any any range 51000 51100 32 | 190 permit tcp any any eq 3333 33 | 200 permit tcp any any eq nat ttl eq 255 34 | 210 permit tcp any eq bgp any 35 | 220 permit rsvp any any 36 | exit 37 | ! 38 | management api http-commands 39 | no shutdown 40 | exit 41 | {% for intf in interfaces %} 42 | ! 43 | interface {{ intf.name }} 44 | no switchport 45 | ip address {{ intf.ipv4 }} 46 | exit 47 | {% endfor %} 48 | ! 49 | {% if vlan is defined %} 50 | vlan {{ vlan.id }} 51 | ! 52 | interface Vlan{{ vlan.id }} 53 | ip address {{ vlan.svi }} 54 | exit 55 | ! 56 | interface {{ vlan.interface }} 57 | switchport 58 | switchport mode access 59 | switchport access vlan {{ vlan.id }} 60 | exit 61 | {% endif %} 62 | {% if bgp is defined %} 63 | ! 64 | ip routing 65 | ! 66 | route-map RMAP-CONNECTED-BGP permit 1000 67 | ! 68 | interface Loopback0 69 | description ROUTER-ID 70 | ip address {{ routerid }} 71 | exit 72 | ! 73 | router bgp {{ bgp.asn }} 74 | {% set rid = routerid.split('/') %} 75 | router-id {{ rid[0] }} 76 | {% for neighbor in bgp.neighbours %} 77 | {% set peer_ip = neighbor.ipv4 | ipaddr('address') %} 78 | neighbor {{ peer_ip }} remote-as {{ neighbor.remote_asn }} 79 | neighbor {{ peer_ip }} send-community 80 | neighbor {{ peer_ip }} maximum-routes 12000 81 | {% endfor %} 82 | redistribute connected route-map RMAP-CONNECTED-BGP 83 | maximum-paths 2 84 | exit 85 | ! 86 | {% endif %} 87 | ! 88 | system control-plane 89 | ip access-group def2 in 90 | exit 91 | ! 92 | daemon TerminAttr 93 | exec /usr/bin/TerminAttr -disableaaa 94 | no shutdown 95 | exit 96 | ! 97 | daemon ocprometheus 98 | exec /mnt/flash/ocprometheus -config /mnt/flash/ocprometheus.yml -addr localhost:6042 99 | no shutdown 100 | exit 101 | ! 102 | logging host {{ logging_remote_host }} {{ logging_remote_port }} protocol tcp 103 | logging format hostname fqdn 104 | ! 105 | end 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetOps Quickstart 2 | 3 | This is a DEMO project (thus, do not use in production environment) with the purpose of giving on overview on some example tools, methods and procedures focused on NetOps best practice. 4 | 5 | This work is based on and is inspired by the beautiful work of [networkop](https://github.com/networkop), in his repo [https://github.com/networkop/arista-network-ci](https://github.com/networkop/arista-network-ci) 6 | 7 | We've tried to put everything together in an all-in-one stack deployment, from code to network devices, passing passing through automation playbooks, validation and testing. 8 | 9 | It is important to say that this is not THE Solution (with capitol S), but a possible approach that can be helpful to understand the power of NetOps and give some initial spark to those who want to investigate this subject for the first time. 10 | 11 | We focus on the main goals of NetOps methodology, in a closed-loop telemetry/automation model: 12 | * **Infrascruture as Code** (aka IaC) 13 | * **Versioning** 14 | * **Automation** 15 | * **Monitoring** 16 | 17 | # Components 18 | 19 | Below an architecture on our DEMO environment, the tools we have chosen and the connections between them: 20 | 21 | | ![Stack](docs/imgs/stack.png) | 22 | |:--:| 23 | | *NetOps Stack* | 24 | 25 | > Note: All the tools in this architecture are deployed using Docker and Docker-compose 26 | 27 | ## Arista cEOS Topology 28 | 29 | We are testing our environment with a simple 3-nodes Spine/Leaf topology. We have chosen rely on [Arista](https://www.arista.com/en/) technology beacuse of the flexibility and semplicity they provide. In fact we are using containerized EOS images in conjunctions with [Docker-topo NG](https://github.com/Vista-Technology/docker-topo-ng), a software that allows you to replicate a network topology using docker technologies. 30 | 31 | We also added to Host nodes, one per Leaf, useful to test network connectivity if everything is ok. 32 | 33 | ## Gitalb CI/CD 34 | 35 | We are using [Gitlab](https://about.gitlab.com/) for 2 reasons: 36 | * **GIT** versioning 37 | * for IaC definitions of network devices 38 | * for automation playbooks 39 | * Embedded **DevOps** functionality 40 | * CI/CD Piepline triggers and definitions 41 | 42 | Our Gitlab project is composed by: 43 | * **automation** folder 44 | * contains al Ansible-related files and folders 45 | * **config** folder 46 | * contains the YAML definitions of the 3 devices 47 | * **gitlab-ci.yml** file 48 | * describe the CI/CD process that take place on every change 49 | 50 | The 3 network devices are described using YAML definitions, in order to respect **Infrastructure as Code** best practice, as follows: 51 | 52 | ```yaml 53 | ## Leaf-1 54 | 55 | interfaces: 56 | - name: Ethernet1 57 | ipv4: 10.0.254.1/31 58 | routerid: 1.1.1.1/32 59 | bgp: 60 | asn: 65001 61 | neighbours: 62 | - ipv4: 10.0.254.0 63 | remote_asn: 65100 64 | vlan: 65 | id: 10 66 | svi: 192.168.10.1/24 67 | interface: Ethernet10 68 | ``` 69 | 70 | ```yaml 71 | ## Leaf-2 72 | 73 | interfaces: 74 | - name: Ethernet1 75 | ipv4: 10.0.254.3/31 76 | routerid: 1.1.1.2/32 77 | bgp: 78 | asn: 65002 79 | neighbours: 80 | - ipv4: 10.0.254.2 81 | remote_asn: 65100 82 | vlan: 83 | id: 20 84 | svi: 192.168.20.1/24 85 | interface: Ethernet10 86 | ``` 87 | 88 | ```yaml 89 | ## Spine-1 90 | 91 | interfaces: 92 | - name: Ethernet1 93 | ipv4: 10.0.254.0/31 94 | - name: Ethernet2 95 | ipv4: 10.0.254.2/31 96 | routerid: 1.1.1.100/32 97 | bgp: 98 | asn: 65100 99 | neighbours: 100 | - ipv4: 10.0.254.1 101 | remote_asn: 65001 102 | - ipv4: 10.0.254.3 103 | remote_asn: 65002 104 | ``` 105 | 106 | ## Ansible 107 | We make use of 3 simple, demostrative, [Ansible](https://www.ansible.com/) playbooks to perform all the **automation** staff. 108 | 109 | > Note: Even the creating process of all the demo environment is built with Ansible 110 | 111 | Ansible automation is launched by the Gitlan pipeline. We have devided all the stuff in 3 topics: 112 | * Automatically launch **validation** processes 113 | * _playbooks/validate.yaml_ 114 | * Auotmatically deploy **configurations** on devices, and configure the monitoring stack 115 | * _playbooks/intended.yaml_ 116 | * Automatically **test** the final environment 117 | * _playbooks/ping.yaml_ 118 | 119 | Inside the _automation_ folder of this project you can find all files related to Ansible automation, regarding the NetOps process. 120 | We use some collections and roles to perform the tasks (as you can see in the _requirements.yaml_ file): 121 | * Collections: 122 | * lvrfrc87.git_acp 123 | * arista.eos 124 | * Roles: 125 | * batfish.base 126 | 127 | As you can see, the _host_vars_ folder is empty. It will be filled with **IaC** configuration declarations by the Gitlab **pipeline**. 128 | 129 | The arista.eos collection is the key to perform configuration deployment on the devices 130 | 131 | ```yaml 132 | --- 133 | - name: Configure device 134 | arista.eos.eos_config: 135 | src: template-config-all.j2 136 | replace: config 137 | match: none 138 | ``` 139 | We use a jinja2 template, filled with IaC variables, to get the final configuration 140 | 141 | ```python 142 | ! 143 | hostname {{ inventory_hostname }} 144 | ! 145 | spanning-tree mode mstp 146 | ! 147 | no aaa root 148 | ! 149 | aaa authorization exec default local 150 | ! 151 | username {{ management.username }} privilege 15 secret sha512 {{ management.password }} 152 | ! 153 | ip access-list def2 154 | 9 permit tcp any any eq 8080 155 | 10 permit icmp any any 156 | 20 permit ip any any tracked 157 | 30 permit udp any any eq bfd ttl eq 255 158 | 40 permit udp any any eq bfd-echo ttl eq 254 159 | 50 permit udp any any eq multihop-bfd 160 | 60 permit udp any any eq micro-bfd 161 | 70 permit ospf any any 162 | 80 permit tcp any any eq ssh telnet www snmp bgp https msdp ldp netconf-ssh gnmi 163 | 90 permit udp any any eq bootps bootpc snmp rip ntp ldp 164 | 100 permit tcp any any eq mlag ttl eq 255 165 | 110 permit udp any any eq mlag ttl eq 255 166 | 120 permit vrrp any any 167 | 130 permit ahp any any 168 | 140 permit pim any any 169 | 150 permit igmp any any 170 | 160 permit tcp any any range 5900 5910 171 | 170 permit tcp any any range 50000 50100 172 | 180 permit udp any any range 51000 51100 173 | 190 permit tcp any any eq 3333 174 | 200 permit tcp any any eq nat ttl eq 255 175 | 210 permit tcp any eq bgp any 176 | 220 permit rsvp any any 177 | exit 178 | ! 179 | management api http-commands 180 | no shutdown 181 | exit 182 | {% for intf in interfaces %} 183 | ! 184 | interface {{ intf.name }} 185 | no switchport 186 | ip address {{ intf.ipv4 }} 187 | exit 188 | {% endfor %} 189 | ! 190 | {% if vlan is defined %} 191 | vlan {{ vlan.id }} 192 | ! 193 | interface Vlan{{ vlan.id }} 194 | ip address {{ vlan.svi }} 195 | exit 196 | ! 197 | interface {{ vlan.interface }} 198 | switchport 199 | switchport mode access 200 | switchport access vlan {{ vlan.id }} 201 | exit 202 | {% endif %} 203 | {% if bgp is defined %} 204 | ! 205 | ip routing 206 | ! 207 | route-map RMAP-CONNECTED-BGP permit 1000 208 | ! 209 | interface Loopback0 210 | description ROUTER-ID 211 | ip address {{ routerid }} 212 | exit 213 | ! 214 | router bgp {{ bgp.asn }} 215 | {% set rid = routerid.split('/') %} 216 | router-id {{ rid[0] }} 217 | {% for neighbor in bgp.neighbours %} 218 | {% set peer_ip = neighbor.ipv4 | ipaddr('address') %} 219 | neighbor {{ peer_ip }} remote-as {{ neighbor.remote_asn }} 220 | neighbor {{ peer_ip }} send-community 221 | neighbor {{ peer_ip }} maximum-routes 12000 222 | {% endfor %} 223 | redistribute connected route-map RMAP-CONNECTED-BGP 224 | maximum-paths 2 225 | exit 226 | ! 227 | {% endif %} 228 | ! 229 | system control-plane 230 | ip access-group def2 in 231 | exit 232 | ! 233 | daemon TerminAttr 234 | exec /usr/bin/TerminAttr -disableaaa 235 | no shutdown 236 | exit 237 | ! 238 | daemon ocprometheus 239 | exec /mnt/flash/ocprometheus -config /mnt/flash/ocprometheus.yml -addr localhost:6042 240 | no shutdown 241 | exit 242 | ! 243 | logging host {{ logging_remote_host }} {{ logging_remote_port }} protocol tcp 244 | logging format hostname fqdn 245 | ! 246 | end 247 | 248 | ``` 249 | 250 | ## Batfish 251 | [Batfish](https://www.batfish.org/) is an open source **network configuration analysis** tool. 252 | 253 | We have deployed a Batfish server inside our stack and we run some simple demostrative **validation** against it. As described before, we run this validations using Ansible automation. 254 | 255 | For example, we want to validate BGP neighbourship 256 | ```yaml 257 | - name: Generate intended config to test 258 | template: 259 | src: config.j2 260 | dest: "validation/workshop/configs/{{inventory_hostname}}" 261 | 262 | - name: Setup connection to Batfish service 263 | bf_session: 264 | host: localhost 265 | name: local_batfish 266 | delegate_to: localhost 267 | run_once: true 268 | 269 | - name: Initialize the example network 270 | bf_init_snapshot: 271 | network: workshop 272 | snapshot: workshop 273 | snapshot_data: ../validation/workshop 274 | overwrite: true 275 | run_once: true 276 | delegate_to: localhost 277 | 278 | - name: Retrieve Batfish Facts 279 | bf_extract_facts: 280 | output_directory: ../validation/workshop/data/bf_facts 281 | register: bf_facts 282 | run_once: true 283 | delegate_to: localhost 284 | 285 | - name: Display neighbourships for all nodes 286 | debug: 287 | msg: " {{item.value.BGP.Neighbors}} " 288 | with_dict: "{{bf_facts.result.nodes}}" 289 | loop_control: 290 | label: "{{item.key}}.BGP.Neighbors" 291 | run_once: true 292 | delegate_to: localhost 293 | 294 | - name: Validate the configuration of network devices 295 | bf_assert: 296 | assertions: 297 | - type: assert_no_undefined_references 298 | name: Confirm that there are NO undefined references on any network device 299 | - type: assert_no_incompatible_bgp_sessions 300 | name: Confirm that all BGP peers are properly configured 301 | - type: assert_no_unestablished_bgp_sessions 302 | name: Confirm that all compatible BGP peers establish sessions 303 | run_once: true 304 | delegate_to: localhost 305 | ``` 306 | 307 | ## Consul 308 | We have introduced [Consul]() simply to simulate a **service discovery** component inside our **automation** stack. 309 | 310 | This kind of objects are very usefull when we have to deal with automation tasks. In this way, within the _intended.yaml_ playbook, we can *register* a device to be monitored by the monitoring tools without specify any further configuration inside Prometheus 311 | 312 | ```yaml 313 | --- 314 | - name: Register Consul service 315 | community.general.consul: 316 | service_name: "Arista Monitoring" 317 | service_port: "{{ metrics_port }}" 318 | service_id: "{{ inventory_hostname }}" 319 | service_address: "{{ ansible_host }}" 320 | host: "{{ consul_host }}" 321 | tags: 322 | - "_device={{ inventory_hostname }}" 323 | ``` 324 | 325 | Prometheus itself has a service discovery plugin to be able to get hosts from registered device inside Consul. 326 | 327 | ## Prometheus, Loki and Grafana 328 | Finally, as a complete Monitoring/Telemetry/Log aggregation stack we have chosen: 329 | * [Prometheus](https://prometheus.io/) 330 | * scrapes metrics information of the Arista devices, both directly (thanks to OCPrometheus daemon inside EOS) and via custom eAPI exporter (just for demonstration sake, it is absolutely not necessary to run both mechanisms) 331 | * [Loki](https://grafana.com/oss/loki/) 332 | * scrapes devices logs, through the help of [Promtail](https://grafana.com/docs/loki/latest/clients/promtail/) (make log available for scraping) component and [Syslog-NG](https://www.syslog-ng.com/) ( first catches and parses logs) 333 | * [Grafana](https://grafana.com/oss/grafana/) 334 | * provides visualization dashboards 335 | 336 | # CI/CD Process 337 | 338 | | ![CI/CD Pipeline](docs/imgs/pipeline.png) | 339 | |:--:| 340 | | *CI/CD Pipeline detail* | 341 | We use the default CI/CD mechanism inside Gitlab to perform the pipeline process. 342 | 343 | Therefore we provide a simple _.gitlab-ci.yml_ file that describes all the stages 344 | ```yaml 345 | stages: 346 | - validate 347 | - deploy 348 | - test 349 | 350 | batfish-validation: 351 | stage: validate 352 | script: 353 | - chmod 755 automation 354 | - cp configs/*.yml automation/host_vars/ 355 | - cd automation 356 | - ansible-galaxy install -r requirements.yml 357 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/validate.yaml 358 | 359 | deploy-configurations: 360 | stage: deploy 361 | script: 362 | - chmod 755 automation 363 | - cp configs/*.yml automation/host_vars/ 364 | - cd automation 365 | - ansible-galaxy install -r requirements.yml 366 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/intended.yaml 367 | 368 | ping-test: 369 | stage: test 370 | script: 371 | - chmod 755 automation 372 | - cp configs/*.yml automation/host_vars/ 373 | - cd automation 374 | - chmod 400 demo.key 375 | - ansible-galaxy install -r requirements.yml 376 | - ansible-playbook -e 'ansible_python_interpreter=/usr/bin/python3' playbooks/ping.yaml 377 | ``` 378 | We have build a custom docker image that the runner can use to perform tasks inside stages. It containes all the softwares necessary. 379 | 380 | # Pre-requisite 381 | You can run all the demo environment inside a single Linux-based host. 382 | 383 | We recommend to use a not-too-old linux distribution, with at least 4 CPUs and 16GB RAM. 384 | 385 | This softwares has to be present before launch the installation: 386 | * python3 387 | * pip3 388 | * Python virtualenv 389 | * docker 390 | 391 | # How to install 392 | First clone this repository on your server and chanage directory inside the root. 393 | 394 | Then create the virtualenv and install all the python requirements (this action could takes long to finish) 395 | 396 | ```console 397 | foo@bar:~$ cd netops-quickstart 398 | foo@bar:~$ virtualenv .venv 399 | foo@bar:~$ source .venv/bin/activate 400 | (.venv)foo@bar:~$ cd build 401 | (.venv)foo@bar:~$ pip install -r requirements.txt 402 | ``` 403 | 404 | Download from Arista portal the latest cEOS images and put it inside the topology folder, with the name _cEOS-Lab.tar.xz_ 405 | 406 | ```console 407 | (.venv)foo@bar:~$ ls -al topology 408 | 409 | total 391776 410 | drwxrwxr-x 4 ubuntu ubuntu 4096 May 7 15:17 . 411 | drwxrwxr-x 8 ubuntu ubuntu 4096 May 7 14:28 .. 412 | drwxrwxr-x 2 ubuntu ubuntu 4096 May 7 15:15 alpine-host 413 | -rw-r--r-- 1 ubuntu ubuntu 401152996 May 7 15:13 cEOS-Lab.tar.xz 414 | drwxrwxr-x 2 ubuntu ubuntu 4096 May 7 15:17 configs 415 | -rw-rw-r-- 1 ubuntu ubuntu 638 May 7 14:28 topology.yaml 416 | ``` 417 | 418 | Now within the virtual environment you can launch Ansible automation that builds all the infrastructure (it will takes approximately 7/8 minutes) 419 | 420 | ```console 421 | (.venv)foo@bar:~$ ansible-playbook build.yml 422 | 423 | 424 | PLAY [lab] ************************************************************************************************************************************************************************* 425 | 426 | TASK [build : Generate SSH key pair for Hosts in topology] ************************************************************************************************************************* 427 | changed: [Spine-1] 428 | 429 | TASK [build : Build Host docker image] ********************************************************************************************************************************************* 430 | changed: [Spine-1] 431 | 432 | TASK [build : Import & Build cEOS image] ******************************************************************************************************************************************* 433 | changed: [Spine-1] 434 | 435 | TASK [build : Generate Arista configuration from templates] ************************************************************************************************************************ 436 | changed: [Spine-1] 437 | changed: [Leaf-1] 438 | changed: [Leaf-2] 439 | 440 | TASK [build : Start Arista topology] *********************************************************************************************************************************************** 441 | changed: [Spine-1] 442 | 443 | TASK [build : Pause for 30 seconds to topology creation] *************************************************************************************************************************** 444 | Pausing for 30 seconds 445 | (ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) 446 | ok: [Spine-1] 447 | 448 | TASK [build : Copy OCPrometheus binary and configuration] ************************************************************************************************************************** 449 | changed: [Spine-1] 450 | changed: [Leaf-1] 451 | changed: [Leaf-2] 452 | 453 | TASK [build : Pause for 90 seconds to topology up & running] *********************************************************************************************************************** 454 | Pausing for 90 seconds 455 | (ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) 456 | ok: [Spine-1] 457 | 458 | TASK [build : Build Network CI/CD docker image for Gitlab Runner] ****************************************************************************************************************** 459 | changed: [Spine-1] 460 | 461 | TASK [build : Start all stack architecture] **************************************************************************************************************************************** 462 | changed: [Spine-1] 463 | 464 | TASK [build : Pause for 3 minutes, waiting for the stack comes up] ***************************************************************************************************************** 465 | Pausing for 180 seconds 466 | (ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) 467 | ok: [Spine-1] 468 | 469 | TASK [build : Register Gitlab Runner] ********************************************************************************************************************************************** 470 | changed: [Spine-1] 471 | 472 | PLAY RECAP ************************************************************************************************************************************************************************* 473 | Leaf-1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 474 | Leaf-2 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 475 | Spine-1 : ok=12 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 476 | 477 | ``` 478 | 479 | At the end of the playbook you will have this situation 480 | 481 | ```console 482 | (.venv)foo@bar:~$ docker ps 483 | 484 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 485 | 605125504e40 grafana/loki:latest "/usr/bin/loki -conf…" 5 minutes ago Up 5 minutes 0.0.0.0:3100->3100/tcp, :::3100->3100/tcp loki 486 | 54d06e721082 prom/prometheus:latest "/bin/prometheus --w…" 5 minutes ago Up 5 minutes 0.0.0.0:9090->9090/tcp, :::9090->9090/tcp prometheus 487 | 49a918ab0d36 arista-eapi-exporter "python -u ./main.py" 5 minutes ago Up 5 minutes 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp arista-eapi-exporter 488 | 01c192a0744b gitlab/gitlab-ce:latest "/assets/wrapper" 5 minutes ago Up 5 minutes (healthy) 80/tcp, 443/tcp, 0.0.0.0:8822->22/tcp, :::8822->22/tcp, 0.0.0.0:8888->9080/tcp, :::8888->9080/tcp gitlab-lab 489 | 1785c34d6fec gitlab/gitlab-runner:latest "/usr/bin/dumb-init …" 5 minutes ago Up 5 minutes runner-lab 490 | 9af5d52d0f5f grafana/grafana:latest "/run.sh" 5 minutes ago Up 5 minutes 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp grafana 491 | acc1d7780a5d batfish/allinone "./wrapper.sh" 5 minutes ago Up 5 minutes 8888/tcp, 0.0.0.0:9996-9998->9996-9998/tcp, :::9996-9998->9996-9998/tcp batfish 492 | f81482213ebb bitnami/consul:1-debian-10 "/opt/bitnami/script…" 5 minutes ago Up 5 minutes 0.0.0.0:8300-8301->8300-8301/tcp, :::8300-8301->8300-8301/tcp, 0.0.0.0:8500->8500/tcp, 0.0.0.0:8301->8301/udp, :::8500->8500/tcp, :::8301->8301/udp, 0.0.0.0:8600->8600/tcp, :::8600->8600/tcp, 0.0.0.0:8600->8600/udp, :::8600->8600/udp consul 493 | f9690138a56e balabit/syslog-ng "/usr/sbin/syslog-ng…" 5 minutes ago Up 5 minutes (healthy) 601/tcp, 514/udp, 6514/tcp, 0.0.0.0:51400->514/tcp, :::51400->514/tcp syslog-ng 494 | 6d81eb4cf9b4 grafana/promtail:latest "/usr/bin/promtail -…" 5 minutes ago Up 5 minutes 0.0.0.0:9080->9080/tcp, :::9080->9080/tcp, 0.0.0.0:15140->1514/tcp, :::15140->1514/tcp promtail 495 | 07d00af9fd15 alpine-host:latest "/home/alpine/entryp…" 12 minutes ago Up 12 minutes 0.0.0.0:2001->22/tcp, :::2001->22/tcp, 0.0.0.0:8001->443/tcp, :::8001->443/tcp, 0.0.0.0:8881->8080/tcp, :::8881->8080/tcp lab_Host-2 496 | 73f42929556d alpine-host:latest "/home/alpine/entryp…" 12 minutes ago Up 12 minutes 0.0.0.0:2000->22/tcp, :::2000->22/tcp, 0.0.0.0:8000->443/tcp, :::8000->443/tcp, 0.0.0.0:8880->8080/tcp, :::8880->8080/tcp lab_Host-1 497 | b5b11322bbde ceos:latest "/sbin/init systemd.…" 12 minutes ago Up 12 minutes 0.0.0.0:2004->22/tcp, :::2004->22/tcp, 0.0.0.0:8004->443/tcp, :::8004->443/tcp, 0.0.0.0:8884->8080/tcp, :::8884->8080/tcp lab_Spine-1 498 | 1823eb54a25f ceos:latest "/sbin/init systemd.…" 12 minutes ago Up 12 minutes 0.0.0.0:2003->22/tcp, :::2003->22/tcp, 0.0.0.0:8003->443/tcp, :::8003->443/tcp, 0.0.0.0:8883->8080/tcp, :::8883->8080/tcp lab_Leaf-2 499 | 03646ccf2cf6 ceos:latest "/sbin/init systemd.…" 12 minutes ago Up 12 minutes 0.0.0.0:2002->22/tcp, :::2002->22/tcp, 0.0.0.0:8002->443/tcp, :::8002->443/tcp, 0.0.0.0:8882->8080/tcp, :::8882->8080/tcp lab_Leaf-1 500 | ``` 501 | 502 | # Test your NetOps process 503 | 504 | ## What we have done so far... 505 | 506 | Now that the whole stack is up&running you can easily check if you can access this endpoints: 507 | * **Gitlab server** 508 | * http://\:8888 509 | * user: _root_ 510 | * password: _NetOpsVista21!_ 511 | * **Grafana** 512 | * http://\:3000 513 | * user: _admin_ 514 | * password: _NetOps_ 515 | * **Prometheus UI** 516 | * http://\:9090 517 | * **Consul UI** 518 | * http://\:8500 519 | 520 | You can also access the 2 fake Hosts attached to Leafs, and check that ping between them is not working right now 521 | 522 | ```console 523 | (.venv)foo@bar:~$ docker exec -it lab_Host-1 /bin/sh 524 | 525 | / $ sudo ping 192.168.20.20 526 | PING 192.168.20.20 (192.168.20.20): 56 data bytes 527 | ^C 528 | --- 192.168.20.20 ping statistics --- 529 | 13 packets transmitted, 0 packets received, 100% packet loss 530 | / $ 531 | ``` 532 | So, that means we still have to configure a lot of stuff on Arista devices... (no BGP configuration means no neighbourship means no network reachability between Hosts) 533 | 534 | Also, accessing Grafana dashboard, you will see blank data 535 | 536 | | ![Grafana-empty](docs/imgs/grafana-empty.png) | 537 | |:--:| 538 | | *Grafana empty dashboard* | 539 | 540 | Let's start by creating our GIT project and prepare everything to deploy correct configurations on devices. 541 | 542 | ## Let's do some magic! 543 | 544 | | ![Gitlab-login](docs/imgs/gitlab-login.png) | 545 | |:--:| 546 | | *Login to Gitlab server* | 547 | 548 | | ![Gitlab-create-project](docs/imgs/gitlab-create-project.png) | 549 | |:--:| 550 | | *Create a new project* | 551 | 552 | | ![Gitlab-project-info](docs/imgs/gitlab-project-info.png) | 553 | |:--:| 554 | | *Fill project info* | 555 | 556 | | ![Gitlab-project](docs/imgs/gitlab-project.png) | 557 | |:--:| 558 | | *TADAAA!* | 559 | 560 | Now you can clone your brand new repository and start adding stuff to you project. 561 | 562 | ```console 563 | foo@bar:~$ cd 564 | foo@bar:~$ git clone http://\:8888/root/netops.git 565 | 566 | Cloning into 'netops'... 567 | Username for 'http://54.38.137.252:8888': root 568 | Password for 'http://root@54.38.137.252:8888': 569 | remote: Enumerating objects: 3, done. 570 | remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 3 571 | Unpacking objects: 100% (3/3), 224 bytes | 224.00 KiB/s, done. 572 | 573 | foo@bar:~$ cd netops 574 | foo@bar:~$ ls -al 575 | 576 | total 16 577 | drwxrwxr-x 3 ubuntu ubuntu 4096 May 11 13:09 . 578 | drwxr-xr-x 9 ubuntu ubuntu 4096 May 11 13:09 .. 579 | drwxrwxr-x 8 ubuntu ubuntu 4096 May 11 13:09 .git 580 | -rw-rw-r-- 1 ubuntu ubuntu 36 May 11 13:09 README.md 581 | ``` 582 | 583 | Now you can copy some files and folder from this demo repo to the new repo, as follows 584 | 585 | ```console 586 | foo@bar:~$ cd 587 | foo@bar:~$ cp netops-quickstart/gitlab-ci.yml netops/.gitlab-ci.yml 588 | foo@bar:~$ cp -r netops-quickstart/{configs,automation} netops/ 589 | foo@bar:~$ cd netops 590 | foo@bar:~$ ls -al 591 | 592 | total 28 593 | drwxrwxr-x 5 ubuntu ubuntu 4096 May 11 13:15 . 594 | drwxr-xr-x 9 ubuntu ubuntu 4096 May 11 13:09 .. 595 | drwxrwxr-x 8 ubuntu ubuntu 4096 May 11 13:09 .git 596 | -rw-rw-r-- 1 ubuntu ubuntu 996 May 11 13:15 .gitlab-ci.yml 597 | -rw-rw-r-- 1 ubuntu ubuntu 36 May 11 13:09 README.md 598 | drwxrwxr-x 7 ubuntu ubuntu 4096 May 11 13:15 automation 599 | drwxrwxr-x 2 ubuntu ubuntu 4096 May 11 13:15 configs 600 | ``` 601 | 602 | One little small change: you need to insert your demo server IP address inside the inventory 603 | 604 | ```yaml 605 | lab: 606 | vars: 607 | ansible_connection: network_cli 608 | ansible_network_os: eos 609 | ansible_user: arista 610 | ansible_httpapi_pass: arista 611 | ansible_ssh_pass: arista 612 | ansible_password: arista 613 | ansible_become: true 614 | ansible_become_method: enable 615 | ansible_httpapi_use_ssl: true 616 | ansible_httpapi_validate_certs: false 617 | logging_remote_host: #INSERT YOUR IP HERE! 618 | logging_remote_port: 51400 619 | consul_host: consul 620 | children: 621 | Spines: 622 | hosts: 623 | Spine-1 624 | Leafs: 625 | hosts: 626 | Leaf-1 627 | Leaf-2 628 | testing: 629 | vars: 630 | ansible_become: true 631 | ansible_user: alpine 632 | ansible_ssh_private_key_file: demo.key 633 | children: 634 | alpine: 635 | hosts: 636 | Host-1: 637 | ip_to_ping: 192.168.20.20 638 | Host-2: 639 | ip_to_ping: 192.168.10.10 640 | ``` 641 | 642 | Now it's time to add, commit and push! 643 | 644 | ```console 645 | foo@bar:~$ git config user.email "admin@example.com" 646 | foo@bar:~$ git config user.name "Administrator" 647 | foo@bar:~$ git add . 648 | foo@bar:~$ git status 649 | 650 | On branch master 651 | Your branch is up to date with 'origin/master'. 652 | 653 | Changes to be committed: 654 | (use "git restore --staged ..." to unstage) 655 | new file: .gitlab-ci.yml 656 | new file: automation/.gitignore 657 | new file: automation/ansible.cfg 658 | new file: automation/collections/.gitignore 659 | new file: automation/host_vars/.gitignore 660 | new file: automation/inventory.yml 661 | new file: automation/playbooks/intended.yaml 662 | new file: automation/playbooks/ping.yaml 663 | new file: automation/playbooks/validate.yaml 664 | new file: automation/requirements.yml 665 | new file: automation/roles/deploy/defaults/main.yml 666 | new file: automation/roles/deploy/tasks/main.yml 667 | new file: automation/roles/deploy/templates/template-config-all.j2 668 | new file: automation/roles/monitoring/tasks/main.yml 669 | new file: automation/roles/validate/defaults/main.yml 670 | new file: automation/roles/validate/tasks/main.yml 671 | new file: automation/roles/validate/templates/config.j2 672 | new file: automation/validation/workshop/configs/.gitignore 673 | new file: automation/validation/workshop/data/bf_facts/.gitignore 674 | new file: configs/Leaf-1.yml 675 | new file: configs/Leaf-2.yml 676 | new file: configs/Spine-1.yml 677 | 678 | foo@bar:~$ git commit -m "First commit" 679 | foo@bar:~$ git push 680 | 681 | Username for 'http://54.38.137.252:8888': root 682 | Password for 'http://root@54.38.137.252:8888': 683 | Enumerating objects: 42, done. 684 | Counting objects: 100% (42/42), done. 685 | Delta compression using up to 8 threads 686 | Compressing objects: 100% (28/28), done. 687 | Writing objects: 100% (41/41), 5.68 KiB | 969.00 KiB/s, done. 688 | Total 41 (delta 3), reused 0 (delta 0) 689 | To http://54.38.137.252:8888/root/netops.git 690 | aabc023..c155808 master -> master 691 | ``` 692 | 693 | After the push, he CI/CD pipeline will take place 694 | 695 | | ![Gitlab-pipeline](docs/imgs/gitlab-pipeline.png) | 696 | |:--:| 697 | | *Gitlab CI/CD Pipeline started* | 698 | 699 | | ![Gitlab-pipeline-progress](docs/imgs/gitlab-pipeline-progress.png) | 700 | |:--:| 701 | | *Gitlab CI/CD Pipeline stages, in progress* | 702 | 703 | | ![Gitlab-pipeline-finished](docs/imgs/gitlab-pipeline-finished.png) | 704 | |:--:| 705 | | *Gitlab CI/CD Pipeline stages, finished successfully* | 706 | 707 | We can see the detail of the last stage, where Host-1 and Host-2 can ping each others 708 | 709 | | ![Gitlab-pipeline-stage-output](docs/imgs/gitlab-stage-detail.png) | 710 | |:--:| 711 | | *Gitlab CI/CD Pipeline stages output* | 712 | 713 | We can also check from docker cli that now the 2 Hosts ping each others 714 | 715 | ```console 716 | (.venv)foo@bar:~$ docker exec -it lab_Host-1 /bin/sh 717 | 718 | / $ sudo ping 192.168.20.20 719 | PING 192.168.20.20 (192.168.20.20): 56 data bytes 720 | 64 bytes from 192.168.20.20: seq=0 ttl=61 time=10.994 ms 721 | 64 bytes from 192.168.20.20: seq=1 ttl=61 time=9.132 ms 722 | 64 bytes from 192.168.20.20: seq=2 ttl=61 time=8.530 ms 723 | 64 bytes from 192.168.20.20: seq=3 ttl=61 time=7.020 ms 724 | 64 bytes from 192.168.20.20: seq=4 ttl=61 time=7.871 ms 725 | 64 bytes from 192.168.20.20: seq=5 ttl=61 time=6.509 ms 726 | ^C 727 | --- 192.168.20.20 ping statistics --- 728 | 6 packets transmitted, 6 packets received, 0% packet loss 729 | round-trip min/avg/max = 6.509/8.342/10.994 ms 730 | ``` 731 | 732 | Also Grafana dashboard now show some data and logs 733 | 734 | | ![Grafana](docs/imgs/grafana-data.png) | 735 | |:--:| 736 | | *Grafana* | 737 | 738 | ## It's time to break everything... 739 | 740 | Now, let's try to voluntarily insert wrong data on IaC side. For example we could change an IP address on Leaf-2 741 | 742 | | ![IaC](docs/imgs/iac-broken.png) | 743 | |:--:| 744 | | *Insert a wrong IP (10.0.154.2 on the BGP configuration of Leaf-2)* | 745 | 746 | The excpected behaviour is to fail the first validation step of the CI?CD pipeline. 747 | 748 | And in fact here it is 749 | 750 | | ![CI/CD Validation failed](docs/imgs/pipeline-failed.png) | 751 | |:--:| 752 | | *Failed on Validation stage* | 753 | 754 | | ![CI/CD Validation failed](docs/imgs/pipeline-failed-details.png) | 755 | |:--:| 756 | | *Failed on Validation stage, details* | 757 | 758 | So it's working! Validation stage prevent something broken to arrive to production environment. 759 | 760 | # Clean up the demo stack 761 | 762 | If you're tired of the playground you can simply clean all up by running an Ansible playbook 763 | 764 | ```console 765 | (.venv)foo@bar:~$ cd netops-quickstart/build 766 | (.venv)foo@bar:~$ ansible-playbook clean_up.yml 767 | 768 | PLAY [lab] *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** 769 | 770 | TASK [Destroy Arista topology] *************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** 771 | changed: [Spine-1] 772 | 773 | TASK [Destroy the stack] ********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************* 774 | changed: [Spine-1] 775 | 776 | TASK [Prune docker stack] ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** 777 | changed: [Spine-1] 778 | 779 | PLAY RECAP *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** 780 | Spine-1 : ok=3 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 781 | 782 | ``` 783 | --------------------------------------------------------------------------------