├── tutorial-6-abstractions ├── group_vars │ ├── my_demo_site.yml │ └── all.yml ├── roles │ ├── base │ │ ├── templates │ │ │ ├── eos │ │ │ │ └── simple.j2 │ │ │ └── junos │ │ │ │ └── simple.j2 │ │ └── tasks │ │ │ └── main.yml │ └── ipfabric │ │ ├── tasks │ │ └── main.yml │ │ └── templates │ │ ├── junos │ │ ├── ipfabric.j2 │ │ └── bgp.j2 │ │ └── eos │ │ ├── ipfabric.j2 │ │ └── bgp.j2 ├── ansible.cfg ├── data │ ├── acme │ │ ├── attributes.yml │ │ ├── devices.yml │ │ └── services.yml │ └── evil │ │ ├── attributes.yml │ │ ├── devices.yml │ │ └── services.yml ├── tasks │ ├── __init__.py │ ├── helpers.py │ ├── tasks_db.py │ ├── tasks_site.py │ └── tasks_service.py ├── README.md ├── playbook_configure.yml ├── hosts.py ├── playbook_verify.yml ├── create_all_the_data.py ├── library │ ├── napalm_get_facts.py │ └── napalm_install_config.py └── callback_plugins │ └── selective.py ├── tutorial-3-abstract-vendor-configuration ├── host_vars │ ├── rtr00.yml │ └── rtr01.yml ├── templates │ ├── eos │ │ └── simple.j2 │ └── junos │ │ └── simple.j2 ├── group_vars │ └── all.yml ├── ansible.cfg ├── hosts ├── README.md ├── playbook_facts.yml ├── playbook_configure.yml ├── library │ ├── napalm_get_facts.py │ └── napalm_install_config.py └── callback_plugins │ └── selective.py ├── tutorial-5-data-driven-configuration_with_backend ├── group_vars │ ├── my_demo_site.yml │ └── all.yml ├── roles │ ├── base │ │ ├── templates │ │ │ ├── eos │ │ │ │ └── simple.j2 │ │ │ └── junos │ │ │ │ └── simple.j2 │ │ └── tasks │ │ │ └── main.yml │ └── ipfabric │ │ ├── tasks │ │ └── main.yml │ │ └── templates │ │ ├── junos │ │ ├── ipfabric.j2 │ │ └── bgp.j2 │ │ └── eos │ │ ├── ipfabric.j2 │ │ └── bgp.j2 ├── ansible.cfg ├── README.md ├── playbook_configure.yml ├── hosts.py ├── playbook_verify.yml ├── create_data.sh ├── library │ ├── napalm_get_facts.py │ └── napalm_install_config.py └── callback_plugins │ └── selective.py ├── tutorial-4-data-driven-configuration ├── roles │ ├── base │ │ ├── templates │ │ │ ├── eos │ │ │ │ └── simple.j2 │ │ │ └── junos │ │ │ │ └── simple.j2 │ │ └── tasks │ │ │ └── main.yml │ └── ipfabric │ │ ├── tasks │ │ └── main.yml │ │ └── templates │ │ ├── eos │ │ └── ipfabric.j2 │ │ └── junos │ │ └── ipfabric.j2 ├── group_vars │ └── all.yml ├── ansible.cfg ├── hosts ├── README.md ├── host_vars │ ├── rtr00.yml │ └── rtr01.yml ├── playbook_verify.yml ├── playbook_configure.yml ├── library │ ├── napalm_get_facts.py │ └── napalm_install_config.py └── callback_plugins │ └── selective.py ├── labs ├── lab1 │ ├── diagram.graffle │ │ ├── data.plist │ │ ├── image1.tiff │ │ ├── image2.tiff │ │ ├── image8.tiff │ │ ├── image9.tiff │ │ ├── image10.tiff │ │ ├── image12.tiff │ │ ├── image14.tiff │ │ └── image15.tiff │ ├── hosts.yaml │ └── Vagrantfile └── nsot │ └── Vagrantfile ├── setup.cfg ├── requirements.txt ├── tutorial-0-building-env ├── content │ ├── python.png │ ├── vagrant_add.png │ ├── support_arista.png │ └── tutorial_0_building_env_demo_preview.gif └── README.md ├── README.md ├── .gitignore ├── tutorial-1-hello-world └── README.md ├── tutorial-2-abstract-vendor-interfaces └── README.md └── LICENSE /tutorial-6-abstractions/group_vars/my_demo_site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | domain: acme.com 3 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/host_vars/rtr00.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: thehostnama 3 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/host_vars/rtr01.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: another-host 3 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/group_vars/my_demo_site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | domain: acme.com 3 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_python_interpreter: "/usr/bin/env python" 3 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/templates/eos/simple.j2: -------------------------------------------------------------------------------- 1 | hostname {{ hostname }} 2 | ip domain-name {{ domain }} 3 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/base/templates/eos/simple.j2: -------------------------------------------------------------------------------- 1 | hostname {{ inventory_hostname }} 2 | ip domain-name {{ domain }} 3 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/base/templates/eos/simple.j2: -------------------------------------------------------------------------------- 1 | hostname {{ hostname }} 2 | ip domain-name {{ domain }} 3 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_python_interpreter: "/usr/bin/env python" 3 | -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/data.plist -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image1.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image1.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image2.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image2.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image8.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image8.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image9.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image9.tiff -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pylama] 2 | linters = mccabe,pep8,pyflakes,pydocstyle 3 | ignore = D203 4 | 5 | [pylama:pep8] 6 | max_line_length = 100 7 | -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image10.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image10.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image12.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image12.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image14.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image14.tiff -------------------------------------------------------------------------------- /labs/lab1/diagram.graffle/image15.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/labs/lab1/diagram.graffle/image15.tiff -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML 2 | ansible==2.1.1.0 3 | junos-eznc 4 | napalm 5 | pyeapi 6 | pylama 7 | pynsot 8 | invoke 9 | yamllint 10 | -------------------------------------------------------------------------------- /tutorial-0-building-env/content/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/tutorial-0-building-env/content/python.png -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/templates/junos/simple.j2: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {{ hostname}}; 3 | domain-name {{ domain }}; 4 | } 5 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_python_interpreter: "/usr/bin/env python" 3 | 4 | domain: acme.com 5 | 6 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_python_interpreter: "/usr/bin/env python" 3 | 4 | domain: acme.com 5 | 6 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/base/templates/junos/simple.j2: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {{ inventory_hostname}}; 3 | domain-name {{ domain }}; 4 | } 5 | -------------------------------------------------------------------------------- /tutorial-0-building-env/content/vagrant_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/tutorial-0-building-env/content/vagrant_add.png -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/base/templates/junos/simple.j2: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {{ hostname}}; 3 | domain-name {{ domain }}; 4 | } 5 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/base/templates/eos/simple.j2: -------------------------------------------------------------------------------- 1 | hostname {{ inventory_hostname }} 2 | ip domain-name {{ domain }} 3 | -------------------------------------------------------------------------------- /tutorial-0-building-env/content/support_arista.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/tutorial-0-building-env/content/support_arista.png -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/base/templates/junos/simple.j2: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {{ inventory_hostname}}; 3 | domain-name {{ domain }}; 4 | } 5 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hostfile = hosts.py 3 | retry_files_enabled = False 4 | callback_plugins = callback_plugins 5 | stdout_callback = selective 6 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hostfile = hosts 3 | retry_files_enabled = False 4 | callback_plugins = callback_plugins 5 | stdout_callback = selective 6 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hostfile = hosts 3 | retry_files_enabled = False 4 | callback_plugins = callback_plugins 5 | stdout_callback = selective 6 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/hosts: -------------------------------------------------------------------------------- 1 | [all] 2 | rtr00 os=eos host=127.0.0.1 user=vagrant password=vagrant port=12443 3 | rtr01 os=junos host=127.0.0.1 user=vagrant password="" port=12203 4 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/hosts: -------------------------------------------------------------------------------- 1 | [all] 2 | rtr00 os=eos host=127.0.0.1 user=vagrant password=vagrant port=12443 3 | rtr01 os=junos host=127.0.0.1 user=vagrant password="" port=12203 4 | -------------------------------------------------------------------------------- /tutorial-0-building-env/content/tutorial_0_building_env_demo_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dravetech/network-tutorials/HEAD/tutorial-0-building-env/content/tutorial_0_building_env_demo_preview.gif -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hostfile = hosts.py 3 | retry_files_enabled = False 4 | callback_plugins = callback_plugins 5 | stdout_callback = selective 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # network-tutorials 2 | 3 | This repo contains a few tutorials about network automation. For instructions, please, check the slides on the following link: https://www.dravetech.com/presos/network_automation_tutorial.html 4 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Some base configuration 3 | template: 4 | src: "{{ os }}/simple.j2" 5 | dest: "{{ host_dir }}/simple.conf" 6 | changed_when: False 7 | always_run: true 8 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/ipfabric/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure IP fabric 3 | template: 4 | src: "{{ os }}/ipfabric.j2" 5 | dest: "{{ host_dir }}/ipfabric.conf" 6 | changed_when: False 7 | always_run: true 8 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Some base configuration 3 | template: 4 | src: "{{ os }}/simple.j2" 5 | dest: "{{ host_dir }}/simple.conf" 6 | changed_when: False 7 | always_run: yes 8 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/ipfabric/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure IP fabric 3 | template: 4 | src: "{{ os }}/ipfabric.j2" 5 | dest: "{{ host_dir }}/ipfabric.conf" 6 | changed_when: False 7 | always_run: yes 8 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Some base configuration 3 | template: 4 | src: "{{ os }}/simple.j2" 5 | dest: "{{ host_dir }}/simple.conf" 6 | changed_when: False 7 | always_run: yes 8 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/ipfabric/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure IP fabric 3 | template: 4 | src: "{{ os }}/ipfabric.j2" 5 | dest: "{{ host_dir }}/ipfabric.conf" 6 | changed_when: False 7 | always_run: yes 8 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-4-data-driven-configuration 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/8 4 | 5 | ## Doing small changes 6 | 7 | ansible-playbook playbook_configure.yml -C 8 | 9 | ansible-playbook playbook_configure.yml 10 | 11 | ## Getting information 12 | 13 | ansible-playbook playbook_verify.yml 14 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-3-abstract-vendor 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/7 4 | 5 | ## Getting information 6 | 7 | ansible-playbook playbook_facts.yml 8 | 9 | 10 | ## Doing small changes 11 | 12 | ansible-playbook playbook_configure.yml -C 13 | 14 | 15 | ansible-playbook playbook_configure.yml 16 | -------------------------------------------------------------------------------- /labs/lab1/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: veos 3 | box: vEOS-lab-4.16.6M 4 | forwarded_ports: 5 | - name: https 6 | guest: 443 7 | host: 12443 8 | links: 9 | - name: link1 10 | - name: link2 11 | - name: junos 12 | box: juniper/ffp-12.1X47-D20.7-packetmode 13 | forwarded_ports: 14 | - name: ssh 15 | guest: 22 16 | host: 12203 17 | links: 18 | - name: link1 19 | - name: link2 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/acme/attributes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | attributes: 3 | Device: 4 | os: {} 5 | host: {} 6 | user: {} 7 | password: 8 | constraints: 9 | allow_empty: true 10 | port: {} 11 | asn: {} 12 | router_id: {} 13 | domain: {} 14 | Network: 15 | type: {} 16 | service: {} 17 | Interface: 18 | link_type: {} 19 | connects_to_device: {} 20 | connects_to_iface: {} 21 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/evil/attributes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | attributes: 3 | Device: 4 | os: {} 5 | host: {} 6 | user: {} 7 | password: 8 | constraints: 9 | allow_empty: true 10 | port: {} 11 | asn: {} 12 | router_id: {} 13 | domain: {} 14 | Network: 15 | type: {} 16 | service: {} 17 | Interface: 18 | link_type: {} 19 | connects_to_device: {} 20 | connects_to_iface: {} 21 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tasks for tutorial-6-abstractions.""" 3 | 4 | import invoke 5 | 6 | from . import tasks_db, tasks_site, tasks_service 7 | 8 | ns = invoke.Collection() 9 | ns.add_collection(invoke.Collection.from_module(tasks_db), 'db') 10 | ns.add_collection(invoke.Collection.from_module(tasks_site), 'site') 11 | ns.add_collection(invoke.Collection.from_module(tasks_service), 'serivce') 12 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/acme/devices.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | rtr00: 4 | os: eos 5 | host: 127.0.0.1 6 | domain: acme.com 7 | user: vagrant 8 | password: vagrant 9 | port: '12443' 10 | asn: '65001' 11 | router_id: 10.1.1.1 12 | rtr01: 13 | os: junos 14 | host: 127.0.0.1 15 | domain: acme.com 16 | user: vagrant 17 | password: '' 18 | port: '12203' 19 | asn: '65002' 20 | router_id: 10.1.1.2 21 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/host_vars/rtr00.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: rtr00 3 | 4 | asn: 65000 5 | router_id: "1.1.1.100" 6 | 7 | interfaces: 8 | - name: "lo0" 9 | ip_address: "2001:db8:b33f::100/128" 10 | - name: "et1" 11 | ip_address: "2001:db8:caf3:1::/127" 12 | - name: "et2" 13 | ip_address: "2001:db8:caf3:2::/127" 14 | peers: 15 | - ip: "2001:db8:caf3:1::1" 16 | asn: 65001 17 | - ip: "2001:db8:caf3:2::1" 18 | asn: 65001 19 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/evil/devices.yml: -------------------------------------------------------------------------------- 1 | --- 2 | devices: 3 | evil00: 4 | os: eos 5 | host: 127.0.0.1 6 | domain: evilcorp.com 7 | user: vagrant 8 | password: vagrant 9 | port: '12443' 10 | asn: '65666' 11 | router_id: 10.6.66.1 12 | evil01: 13 | os: junos 14 | host: 127.0.0.1 15 | domain: evil.com 16 | user: vagrant 17 | password: '' 18 | port: '12203' 19 | asn: '65666' 20 | router_id: 10.6.66.2 21 | 22 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/host_vars/rtr01.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hostname: rtr01 3 | 4 | asn: 65001 5 | router_id: "1.1.1.101" 6 | 7 | interfaces: 8 | - name: "lo0" 9 | ip_address: "2001:db8:b33f::101/128" 10 | - name: "ge-0/0/1" 11 | ip_address: "2001:db8:caf3:1::1/127" 12 | - name: "ge-0/0/2" 13 | ip_address: "2001:db8:caf3:2::1/127" 14 | peers: 15 | - ip: "2001:db8:caf3:1::" 16 | asn: 65000 17 | - ip: "2001:db8:caf3:2::" 18 | asn: 65000 19 | 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/acme/services.yml: -------------------------------------------------------------------------------- 1 | --- 2 | loopbacks: 3 | network_ranges: 4 | loopbacks: 2001:db8:feed::/48 5 | definition: {} 6 | 7 | ipfabric: 8 | network_ranges: 9 | fabric_links: 2001:db8:cafe::/48 10 | definition: 11 | links: 12 | - left_device: rtr00 13 | left_iface: et1 14 | right_device: rtr01 15 | right_iface: ge-0/0/1 16 | - left_device: rtr00 17 | left_iface: et2 18 | right_device: rtr01 19 | right_iface: ge-0/0/2 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/data/evil/services.yml: -------------------------------------------------------------------------------- 1 | --- 2 | loopbacks: 3 | network_ranges: 4 | loopbacks: 2001:db8:dead::/48 5 | definition: {} 6 | 7 | ipfabric: 8 | network_ranges: 9 | fabric_links: 2001:db8:c0ff::/48 10 | definition: 11 | links: 12 | - left_device: evil00 13 | left_iface: et1 14 | right_device: evil01 15 | right_iface: ge-0/0/1 16 | - left_device: evil00 17 | left_iface: et2 18 | right_device: evil01 19 | right_iface: ge-0/0/2 20 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/playbook_facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get facts 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | 8 | tasks: 9 | - name: get facts from device 10 | napalm_get_facts: 11 | hostname: "{{ host }}" 12 | username: "{{ user }}" 13 | dev_os: "{{ os }}" 14 | password: "{{ password }}" 15 | optional_args: 16 | port: "{{ port }}" 17 | filter: ['facts'] 18 | register: napalm_facts 19 | - name: Facts 20 | debug: 21 | msg: "{{ napalm_facts.ansible_facts.facts|to_nice_json }}" 22 | tags: [print_action] 23 | 24 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/ipfabric/templates/junos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | {% for interface, interface_data in interfaces.items() %} 2 | 3 | interfaces { 4 | replace: 5 | {{ interface }} { 6 | description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }}; 7 | unit 0 { 8 | family inet6 { 9 | {% for address in interface_data.ipv6 %} 10 | address {{ address if interface_data.attributes.link_type != "loopback" else "{}/128".format(address.split('/')[0]) }}; 11 | {% endfor %} 12 | } 13 | } 14 | } 15 | } 16 | 17 | {% endfor %} 18 | 19 | {% include 'bgp.j2' %} 20 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/ipfabric/templates/junos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | {% for interface, interface_data in interfaces.items() %} 2 | 3 | interfaces { 4 | replace: 5 | {{ interface }} { 6 | description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }}; 7 | unit 0 { 8 | family inet6 { 9 | {% for address in interface_data.ipv6 %} 10 | address {{ address if interface_data.attributes.link_type != "loopback" else "{}/128".format(address.split('/')[0]) }}; 11 | {% endfor %} 12 | } 13 | } 14 | } 15 | } 16 | 17 | {% endfor %} 18 | 19 | {% include 'bgp.j2' %} 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/ipfabric/templates/eos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | ipv6 unicast-routing 2 | 3 | {% for interface, interface_data in interfaces.items() %} 4 | 5 | default interface {{ interface }} 6 | interface {{ interface }} 7 | description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }} 8 | {{ 'no switchport' if not interface_data.attributes.link_type == "loopbacks" else "" }} 9 | 10 | {% for address in interface_data.ipv6 %} 11 | ipv6 address {{ address if interface_data.attributes.link_type != "loopback" else "{}/128".format(address.split('/')[0]) }} 12 | {% endfor %} 13 | 14 | {% endfor %} 15 | 16 | {% include 'bgp.j2' %} 17 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/ipfabric/templates/eos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | ipv6 unicast-routing 2 | 3 | {% for interface, interface_data in interfaces.items() %} 4 | 5 | default interface {{ interface }} 6 | interface {{ interface }} 7 | description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }} 8 | {{ 'no switchport' if not interface_data.attributes.link_type == "loopback" else "" }} 9 | 10 | {% for address in interface_data.ipv6 %} 11 | ipv6 address {{ address if interface_data.attributes.link_type != "loopback" else "{}/128".format(address.split('/')[0]) }} 12 | {% endfor %} 13 | 14 | {% endfor %} 15 | 16 | {% include 'bgp.j2' %} 17 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/ipfabric/templates/eos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | ipv6 unicast-routing 2 | 3 | {% for interface in interfaces %} 4 | 5 | default interface {{ interface.name }} 6 | interface {{ interface.name }} 7 | {{ 'no switchport' if not interface.name.startswith("lo") else "" }} 8 | ipv6 address {{ interface.ip_address }} 9 | 10 | {% endfor %} 11 | 12 | route-map EXPORT-LO0 permit 10 13 | match interface Loopback0 14 | 15 | no router bgp 16 | router bgp {{ asn }} 17 | router-id {{ router_id }} 18 | redistribute connected route-map EXPORT-LO0 19 | 20 | {% for peer in peers %} 21 | neighbor {{ peer.ip }} remote-as {{ peer.asn }} 22 | address-family ipv6 23 | neighbor {{ peer.ip }} activate 24 | 25 | {% endfor %} 26 | -------------------------------------------------------------------------------- /labs/nsot/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/trusty64" 6 | 7 | config.vm.network "forwarded_port", guest: 80, host: 8080 8 | config.vm.network "forwarded_port", guest: 8990, host: 8990 9 | 10 | config.vm.provision "shell", inline: <<-SHELL 11 | export DEBIAN_FRONTEND=noninteractive 12 | sudo apt-get -y update 13 | sudo apt-get -y install build-essential python-dev libffi-dev libssl-dev 14 | sudo apt-get -y install python-pip 15 | sudo pip install nsot==1.1.1 16 | sudo pip install mrproxy 17 | mkdir /home/vagrant/.nsot 18 | nsot-server init /home/vagrant/.nsot/nsot.conf.py 19 | chown -R vagrant:vagrant /home/vagrant/.nsot 20 | sed -i "s/localhost/0.0.0.0/" /home/vagrant/.nsot/nsot.conf.py 21 | nsot-server --config=/home/vagrant/.nsot/nsot.conf.py collectstatic --noinput 22 | SHELL 23 | 24 | end 25 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/ipfabric/templates/eos/bgp.j2: -------------------------------------------------------------------------------- 1 | route-map EXPORT-LO0 permit 10 2 | match interface Loopback0 3 | 4 | no router bgp 5 | router bgp {{ asn }} 6 | router-id {{ router_id }} 7 | redistribute connected route-map EXPORT-LO0 8 | 9 | {% for interface, interface_data in interfaces.items() if interface_data.attributes.link_type == "fabric" %} 10 | {% set peer =interface_data.attributes.connects_to_device %} 11 | {% set peer_iface =interface_data.attributes.connects_to_iface %} 12 | {% set peer_ip = hostvars[peer]['interfaces'][peer_iface]['ipv6'][0].split('/')[0] %} 13 | {% set peer_asn = hostvars[peer]['asn'] %} 14 | neighbor {{ peer_ip }} remote-as {{ peer_asn }} 15 | neighbor {{ peer_ip }} description {{ peer }}:{{ peer_iface }} 16 | address-family ipv6 17 | neighbor {{ peer_ip }} activate 18 | {% endfor %} 19 | 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/ipfabric/templates/eos/bgp.j2: -------------------------------------------------------------------------------- 1 | route-map EXPORT-LO0 permit 10 2 | match interface Loopback0 3 | 4 | no router bgp 5 | router bgp {{ asn }} 6 | router-id {{ router_id }} 7 | redistribute connected route-map EXPORT-LO0 8 | 9 | {% for interface, interface_data in interfaces.items() if interface_data.attributes.link_type == "fabric_links" %} 10 | {% set peer_ip = hostvars[interface_data.attributes.connects_to_device]['interfaces'][interface_data.attributes.connects_to_iface]['ipv6'][0].split('/')[0] %} 11 | {% set peer_asn = hostvars[interface_data.attributes.connects_to_device]['asn'] %} 12 | 13 | neighbor {{ peer_ip }} remote-as {{ peer_asn }} 14 | neighbor {{ peer_ip }} description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }} 15 | address-family ipv6 16 | neighbor {{ peer_ip }} activate 17 | 18 | {% endfor %} 19 | 20 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-6-abstractions 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/10 4 | 5 | ## Deploy site ACME 6 | 7 | 8 | inv site.create -n acme -d "Acme Corp." 9 | inv site.add_atribbutes -s acme -f data/acme/attributes.yml 10 | inv site.add_devices -s acme -f data/acme/devices.yml 11 | inv serivce.loopbacks -s acme -f data/acme/services.yml 12 | inv serivce.ipfabric -s acme -f data/acme/services.yml 13 | 14 | inv site.deploy -s acme 15 | inv site.deploy -s acme --commit 16 | inv site.verify -s acme 17 | 18 | ### Deploy site EVIL 19 | 20 | inv site.create -n evil -d "Evil Corp." 21 | inv site.add_atribbutes -s evil -f data/evil/attributes.yml 22 | inv site.add_devices -s evil -f data/evil/devices.yml 23 | inv serivce.loopbacks -s evil -f data/evil/services.yml 24 | inv serivce.ipfabric -s evil -f data/evil/services.yml 25 | 26 | inv site.deploy -s evil 27 | inv site.deploy -s evil --commit 28 | inv site.verify -s evil 29 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/roles/ipfabric/templates/junos/ipfabric.j2: -------------------------------------------------------------------------------- 1 | {% for interface in interfaces %} 2 | 3 | interfaces { 4 | replace: 5 | {{ interface.name }} { 6 | unit 0 { 7 | family inet6 { 8 | address {{ interface.ip_address }}; 9 | } 10 | } 11 | } 12 | } 13 | 14 | {% endfor %} 15 | 16 | routing-options { 17 | router-id {{ router_id }}; 18 | autonomous-system {{ asn }}; 19 | } 20 | 21 | policy-options { 22 | policy-statement EXPORT_LO0 { 23 | from interface lo0.0; 24 | then accept; 25 | } 26 | policy-statement PERMIT_ALL { 27 | from protocol bgp; 28 | then accept; 29 | } 30 | } 31 | 32 | protocols { 33 | replace: 34 | bgp { 35 | import PERMIT_ALL; 36 | export [ EXPORT_LO0 PERMIT_ALL ]; 37 | } 38 | } 39 | 40 | {% for peer in peers %} 41 | protocols { 42 | bgp { 43 | group peers { 44 | neighbor {{ peer.ip }} { 45 | peer-as {{ peer.asn }}; 46 | } 47 | } 48 | } 49 | } 50 | {% endfor %} 51 | 52 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/playbook_verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get facts 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | 8 | tasks: 9 | - name: get facts from device 10 | napalm_get_facts: 11 | hostname: "{{ host }}" 12 | username: "{{ user }}" 13 | dev_os: "{{ os }}" 14 | password: "{{ password }}" 15 | optional_args: 16 | port: "{{ port }}" 17 | filter: ['bgp_neighbors'] 18 | register: napalm_facts 19 | - name: Check BGP sessions are healthy 20 | assert: 21 | that: 22 | - item.value.is_up 23 | msg: "{{ item.key }} is down" 24 | with_dict: "{{ napalm_facts.ansible_facts.bgp_neighbors.global.peers }}" 25 | tags: [print_action] 26 | - name: Check BGP sessions are receiving prefixes 27 | assert: 28 | that: 29 | - item.value.address_family.ipv6.received_prefixes > 0 30 | msg: "{{ item.key }} is not receiving any prefixes" 31 | with_dict: "{{ napalm_facts.ansible_facts.bgp_neighbors.global.peers }}" 32 | tags: [print_action] 33 | 34 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/roles/ipfabric/templates/junos/bgp.j2: -------------------------------------------------------------------------------- 1 | routing-options { 2 | router-id {{ router_id }}; 3 | autonomous-system {{ asn }}; 4 | } 5 | 6 | policy-options { 7 | policy-statement EXPORT_LO0 { 8 | from interface lo0.0; 9 | then accept; 10 | } 11 | policy-statement PERMIT_ALL { 12 | from protocol bgp; 13 | then accept; 14 | } 15 | } 16 | 17 | protocols { 18 | replace: 19 | bgp { 20 | import PERMIT_ALL; 21 | export [ EXPORT_LO0 PERMIT_ALL ]; 22 | } 23 | } 24 | 25 | {% for interface, interface_data in interfaces.items() if interface_data.attributes.link_type == "fabric" %} 26 | {% set peer = interface_data.attributes.connects_to_device %} 27 | {% set peer_iface = interface_data.attributes.connects_to_iface %} 28 | {% set peer_ip = hostvars[peer]['interfaces'][peer_iface]['ipv6'][0].split('/')[0] %} 29 | {% set peer_asn = hostvars[peer]['asn'] %} 30 | 31 | protocols { 32 | bgp { 33 | group peers { 34 | neighbor {{ peer_ip }} { 35 | description {{ peer }}:{{ peer_iface }}; 36 | peer-as {{ peer_asn }}; 37 | } 38 | } 39 | } 40 | } 41 | 42 | {% endfor %} 43 | 44 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/roles/ipfabric/templates/junos/bgp.j2: -------------------------------------------------------------------------------- 1 | routing-options { 2 | router-id {{ router_id }}; 3 | autonomous-system {{ asn }}; 4 | } 5 | 6 | policy-options { 7 | policy-statement EXPORT_LO0 { 8 | from interface lo0.0; 9 | then accept; 10 | } 11 | policy-statement PERMIT_ALL { 12 | from protocol bgp; 13 | then accept; 14 | } 15 | } 16 | 17 | protocols { 18 | replace: 19 | bgp { 20 | import PERMIT_ALL; 21 | export [ EXPORT_LO0 PERMIT_ALL ]; 22 | } 23 | } 24 | 25 | {% for interface, interface_data in interfaces.items() if interface_data.attributes.link_type == "fabric_links" %} 26 | {% set peer_ip = hostvars[interface_data.attributes.connects_to_device]['interfaces'][interface_data.attributes.connects_to_iface]['ipv6'][0].split('/')[0] %} 27 | {% set peer_asn = hostvars[interface_data.attributes.connects_to_device]['asn'] %} 28 | 29 | protocols { 30 | bgp { 31 | group peers { 32 | neighbor {{ peer_ip }} { 33 | description {{ interface_data.attributes.connects_to_device }}:{{ interface_data.attributes.connects_to_iface }}; 34 | peer-as {{ peer_asn }}; 35 | } 36 | } 37 | } 38 | } 39 | 40 | {% endfor %} 41 | 42 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-5-data-driven-configuration_with_backend 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/9 4 | 5 | ## Backend 6 | 7 | ### Start NSOT 8 | 9 | cd labs/nsot 10 | vagrant up 11 | vagrant ssh 12 | nsot-server start 13 | (input email/password) 14 | 15 | Go to http://localhost:8990/users/1 and get "Secret key". 16 | 17 | ### Configure NSOT 18 | 19 | # nsot sites list 20 | /Users/dbarroso/.pynsotrc not found; would you like to create it? [Y/n]: 21 | Please enter url: http://localhost:8990/api 22 | Please choose auth_method [auth_token, auth_header]: auth_token 23 | Please enter secret_key: bVg27yn1i9MkmRa14fJFAtsfMRZjuYtG1H5X4ZVaCgU= 24 | Please enter email: asd@asd.com 25 | Please enter default_site (optional): 26 | Please enter api_version (optional): 27 | No site found matching args: offset=None, limit=None, id=None, name=None! 28 | 29 | ### Adding data to the backend 30 | 31 | ./create_data.sh 32 | 33 | ## Doing small changes 34 | 35 | ansible-playbook playbook_configure.yml -C 36 | 37 | ansible-playbook playbook_configure.yml 38 | 39 | 40 | ## Getting information 41 | 42 | ansible-playbook playbook_verify.yml 43 | 44 | Note: Highlight differences between previous tutorial and this one 45 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/tasks/helpers.py: -------------------------------------------------------------------------------- 1 | """Some helpers for the rest of the tasks.""" 2 | 3 | import logging 4 | import sys 5 | 6 | 7 | def get_logger(debug, configure=True): 8 | """Configure the logging facility.""" 9 | logger = logging.getLogger("tutorial-6-abstractions") 10 | 11 | if not configure: 12 | return logger 13 | 14 | if debug: 15 | logger.setLevel(logging.DEBUG) 16 | else: 17 | logger.setLevel(logging.INFO) 18 | 19 | ch = logging.StreamHandler(sys.stdout) 20 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 21 | ch.setFormatter(formatter) 22 | logger.addHandler(ch) 23 | return logger 24 | 25 | 26 | def get_site_id(client, name): 27 | """Return id of a site.""" 28 | result = client.sites.get(name=name) 29 | logger = get_logger(False, False) 30 | 31 | if result: 32 | return result[0]['id'] 33 | else: 34 | logger.error("Site {} doesn't exist.".format(name)) 35 | sys.exit(-1) 36 | 37 | 38 | def get_host_id(client, site_id, name): 39 | """Return id of a site.""" 40 | result = client.sites(site_id).devices.get(hostname=name) 41 | logger = get_logger(False, False) 42 | 43 | if result: 44 | return result[0]['id'] 45 | else: 46 | logger.error("Host {} doesn't exist.".format(name)) 47 | sys.exit(-1) 48 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/playbook_configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Simple configuration 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | conf_dir: "{{ playbook_dir }}/.compiled/" 8 | host_dir: "{{ conf_dir}}/{{ inventory_hostname}}" 9 | 10 | pre_tasks: 11 | - name: Make sure there are no remains from a previous run 12 | file: 13 | path: "{{ host_dir }}" 14 | state: absent 15 | changed_when: False 16 | always_run: yes 17 | - name: Create folder to store configurations and diffs for/from the devices 18 | file: 19 | path: "{{ host_dir }}" 20 | state: directory 21 | changed_when: False 22 | always_run: yes 23 | 24 | tasks: 25 | - name: A simple template with some configuration 26 | template: 27 | src: "{{ os }}/simple.j2" 28 | dest: "{{ host_dir }}/simple.conf" 29 | changed_when: False 30 | always_run: yes 31 | - name: Load configuration into the device 32 | napalm_install_config: 33 | hostname: "{{ host }}" 34 | username: "{{ user }}" 35 | dev_os: "{{ os }}" 36 | password: "{{ password }}" 37 | optional_args: 38 | port: "{{ port }}" 39 | config_file: "{{ host_dir }}/simple.conf" 40 | replace_config: false 41 | get_diffs: True 42 | diff_file: "{{ host_dir }}/diff" 43 | tags: [print_action] 44 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/tasks/tasks_db.py: -------------------------------------------------------------------------------- 1 | """Tasks to manipulate the db.""" 2 | 3 | from invoke import task 4 | from pynsot.client import get_api_client 5 | 6 | from pynsot.vendor.slumber.exceptions import HttpClientError 7 | 8 | import helpers 9 | 10 | import sys 11 | 12 | 13 | @task 14 | def reset(ctx, debug=False): 15 | """Resets the database. All sites and their related data are wiped out.""" 16 | global logger 17 | logger = helpers.get_logger(debug) 18 | 19 | client = get_api_client() 20 | 21 | for site in client.sites.get(): 22 | logger.info("Deleting site: {}".format(site)) 23 | _reset_resources(client, site['id']) 24 | _reset_resources(client, site['id']) 25 | _reset_resources(client, site['id']) 26 | try: 27 | client.sites(site['id']).delete() 28 | except HttpClientError as e: 29 | logger.error(e) 30 | logger.error(e.response.json()) 31 | sys.exit(-1) 32 | 33 | 34 | def _reset_resources(client, site_id): 35 | resources = ['interfaces', 'networks', 'devices', 'attributes'] 36 | for r in resources: 37 | path = getattr(client.sites(site_id), r) 38 | for element in path.get(): 39 | logger.debug("Deleteing {}: {}".format(r, element)) 40 | try: 41 | path(element['id']).delete() 42 | except HttpClientError as e: 43 | logger.debug(e) 44 | logger.debug(e.response.json()) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .DS_Store 92 | *.swp 93 | 94 | .vagrant 95 | .compiled 96 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/playbook_configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Simple configuration 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | conf_dir: "{{ playbook_dir }}/.compiled/" 8 | 9 | pre_tasks: 10 | - name: Set some global facts 11 | set_fact: 12 | host_dir: "{{ conf_dir}}/{{ inventory_hostname}}" 13 | changed_when: False 14 | - name: Make sure there are no remains from a previous run 15 | file: 16 | path: "{{ host_dir }}" 17 | state: absent 18 | changed_when: False 19 | always_run: true 20 | - name: Create folder to store configurations and diffs for/from the devices 21 | file: 22 | path: "{{ host_dir }}" 23 | state: directory 24 | changed_when: False 25 | always_run: true 26 | 27 | - name: Basic Configuration 28 | hosts: all 29 | connection: local 30 | roles: 31 | - base 32 | 33 | - name: Fabric Configuration 34 | hosts: all 35 | connection: local 36 | roles: 37 | - ipfabric 38 | 39 | post_tasks: 40 | - name: Assemble all the configuration bits 41 | assemble: 42 | src: "{{ host_dir }}/" 43 | dest: "{{ host_dir }}/assembled.conf" 44 | changed_when: False 45 | always_run: true 46 | - name: Load configuration into the device 47 | napalm_install_config: 48 | hostname: "{{ host }}" 49 | username: "{{ user }}" 50 | dev_os: "{{ os }}" 51 | password: "{{ password }}" 52 | optional_args: 53 | port: "{{ port }}" 54 | config_file: "{{ host_dir }}/assembled.conf" 55 | replace_config: false 56 | get_diffs: True 57 | diff_file: "{{ host_dir }}/diff" 58 | tags: [print_action] 59 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/playbook_configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Simple configuration 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | conf_dir: "{{ playbook_dir }}/.compiled/" 8 | 9 | pre_tasks: 10 | - name: Set some global facts 11 | set_fact: 12 | host_dir: "{{ conf_dir}}/{{ inventory_hostname}}" 13 | changed_when: False 14 | - name: Make sure there are no remains from a previous run 15 | file: 16 | path: "{{ host_dir }}" 17 | state: absent 18 | changed_when: False 19 | always_run: yes 20 | - name: Create folder to store configurations and diffs for/from the devices 21 | file: 22 | path: "{{ host_dir }}" 23 | state: directory 24 | changed_when: False 25 | always_run: yes 26 | 27 | - name: Basic Configuration 28 | hosts: all 29 | connection: local 30 | roles: 31 | - base 32 | 33 | - name: Fabric Configuration 34 | hosts: all 35 | connection: local 36 | roles: 37 | - ipfabric 38 | 39 | post_tasks: 40 | - name: Assemble all the configuration bits 41 | assemble: 42 | src: "{{ host_dir }}/" 43 | dest: "{{ host_dir }}/assembled.conf" 44 | always_run: yes 45 | changed_when: False 46 | - name: Load configuration into the device 47 | napalm_install_config: 48 | hostname: "{{ host }}" 49 | username: "{{ user }}" 50 | dev_os: "{{ os }}" 51 | password: "{{ password }}" 52 | optional_args: 53 | port: "{{ port }}" 54 | config_file: "{{ host_dir }}/assembled.conf" 55 | replace_config: false 56 | get_diffs: True 57 | diff_file: "{{ host_dir }}/diff" 58 | tags: [print_action] 59 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/playbook_configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Simple configuration 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | conf_dir: "{{ playbook_dir }}/.compiled/" 8 | 9 | pre_tasks: 10 | - name: Set some global facts 11 | set_fact: 12 | host_dir: "{{ conf_dir}}/{{ inventory_hostname}}" 13 | changed_when: False 14 | always_run: yes 15 | - name: Make sure there are no remains from a previous run 16 | file: 17 | path: "{{ host_dir }}" 18 | state: absent 19 | changed_when: False 20 | always_run: yes 21 | - name: Create folder to store configurations and diffs for/from the devices 22 | file: 23 | path: "{{ host_dir }}" 24 | state: directory 25 | changed_when: False 26 | always_run: yes 27 | 28 | - name: Basic Configuration 29 | hosts: all 30 | connection: local 31 | roles: 32 | - base 33 | 34 | - name: Fabric Configuration 35 | hosts: all 36 | connection: local 37 | roles: 38 | - ipfabric 39 | 40 | post_tasks: 41 | - name: Assemble all the configuration bits 42 | assemble: 43 | src: "{{ host_dir }}/" 44 | dest: "{{ host_dir }}/assembled.conf" 45 | always_run: yes 46 | changed_when: False 47 | - name: Load configuration into the device 48 | napalm_install_config: 49 | hostname: "{{ host }}" 50 | username: "{{ user }}" 51 | dev_os: "{{ os }}" 52 | password: "{{ password }}" 53 | optional_args: 54 | port: "{{ port }}" 55 | config_file: "{{ host_dir }}/assembled.conf" 56 | replace_config: false 57 | get_diffs: True 58 | diff_file: "{{ host_dir }}/diff" 59 | tags: [print_action] 60 | -------------------------------------------------------------------------------- /tutorial-1-hello-world/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-1-hello-world 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/5 4 | 5 | ## Getting information 6 | 7 | ### JunOS 8 | 9 | from jnpr.junos import Device 10 | import pprint 11 | pp = pprint.PrettyPrinter(indent=4) 12 | 13 | junos = Device(host='127.0.0.1', user='vagrant', port=12203) 14 | junos.open() 15 | junos_facts = junos.facts 16 | pp.pprint(junos_facts) 17 | junos.close() 18 | 19 | ### EOS 20 | 21 | import pyeapi 22 | import pprint 23 | pp = pprint.PrettyPrinter(indent=4) 24 | connection = pyeapi.client.connect(transport='https', host='127.0.0.1', username='vagrant', password='vagrant', port=12443,) 25 | eos = pyeapi.client.Node(connection) 26 | eos_facts = eos.run_commands(['show version']) 27 | pp.pprint(eos_facts) 28 | 29 | ## Doing small changes 30 | 31 | ### JunOS 32 | 33 | from jnpr.junos import Device 34 | from jnpr.junos.utils.config import Config 35 | 36 | junos = Device(host='127.0.0.1', user='vagrant', port=12203) 37 | junos.open() 38 | 39 | print(junos.facts['hostname']) 40 | junos.bind(cu=Config) 41 | junos.cu.lock() 42 | junos.cu.load("system {host-name new-hostname;}", format="text", merge=True) 43 | junos.cu.commit() 44 | junos.cu.unlock() 45 | junos.facts_refresh() 46 | print(junos.facts['hostname']) 47 | junos.close() 48 | 49 | ### EOS 50 | 51 | import pyeapi 52 | connection = pyeapi.client.connect( 53 | transport='https', 54 | host='127.0.0.1', 55 | username='vagrant', 56 | password='vagrant', 57 | port=12443, 58 | ) 59 | eos = pyeapi.client.Node(connection) 60 | print(eos.run_commands(['show hostname'])[0]['hostname']) 61 | eos.run_commands(['configure', 'hostname a-new-hostname']) 62 | print(eos.run_commands(['show hostname'])[0]['hostname']) 63 | -------------------------------------------------------------------------------- /labs/lab1/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Specify minimum Vagrant version and Vagrant API version 5 | Vagrant.require_version ">= 1.6.0" 6 | VAGRANTFILE_API_VERSION = "2" 7 | 8 | # Plugins we require 9 | required_plugins = %w(vagrant-junos vagrant-vbguest vagrant-host-shell) 10 | 11 | ##### START Helper functions 12 | def install_ssh_key() 13 | puts "Adding ssh key to the ssh agent" 14 | puts "ssh-add #{Vagrant.source_root}/keys/vagrant" 15 | system "ssh-add #{Vagrant.source_root}/keys/vagrant" 16 | end 17 | 18 | def install_plugins(required_plugins) 19 | plugins_to_install = required_plugins.select { |plugin| not Vagrant.has_plugin? plugin } 20 | if not plugins_to_install.empty? 21 | puts "Installing plugins: #{plugins_to_install.join(' ')}" 22 | if system "vagrant plugin install #{plugins_to_install.join(' ')}" 23 | exec "vagrant #{ARGV.join(' ')}" 24 | else 25 | abort "Installation of one or more plugins has failed. Aborting." 26 | end 27 | end 28 | end 29 | ##### END Helper functions 30 | 31 | # Install ssh key 32 | install_ssh_key 33 | 34 | # Check certain plugins are installed 35 | install_plugins required_plugins 36 | 37 | # Require YAML module 38 | require 'yaml' 39 | 40 | # Read YAML file with box details 41 | hosts = YAML.load_file('hosts.yaml') 42 | 43 | # Lab definition begins here 44 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 45 | config.vbguest.auto_update = false 46 | 47 | # Iterate through entries in YAML file 48 | hosts.each do |host| 49 | config.vm.define host["name"] do |srv| 50 | srv.vm.box = host["box"] 51 | 52 | host["forwarded_ports"].each do |port| 53 | srv.vm.network :forwarded_port, guest: port["guest"], host: port["host"], id: port["name"] 54 | end 55 | 56 | host["links"].each do |link| 57 | srv.vm.network "private_network", virtualbox__intnet: link["name"], ip: "169.254.1.11", auto_config: false 58 | end 59 | 60 | end 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /tutorial-2-abstract-vendor-interfaces/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-2-abstract-vendor-interfaces 2 | 3 | https://www.dravetech.com/presos/network_automation_tutorial.html#/6 4 | 5 | ## Getting information 6 | 7 | from napalm_base import get_network_driver 8 | import pprint 9 | pp = pprint.PrettyPrinter(indent=4) 10 | 11 | junos_driver = get_network_driver('junos') 12 | eos_driver = get_network_driver('eos') 13 | 14 | junos_configuration = { 15 | 'hostname': '127.0.0.1', 16 | 'username': 'vagrant', 17 | 'password': '', 18 | 'optional_args': {'port': 12203} 19 | } 20 | 21 | eos_configuration = { 22 | 'hostname': '127.0.0.1', 23 | 'username': 'vagrant', 24 | 'password': 'vagrant', 25 | 'optional_args': {'port': 12443} 26 | } 27 | 28 | with junos_driver(**junos_configuration) as junos: 29 | pp.pprint(junos.get_facts()) 30 | 31 | with eos_driver(**eos_configuration) as eos: 32 | pp.pprint(eos.get_facts()) 33 | 34 | 35 | 36 | 37 | ## Doing small changes 38 | 39 | from napalm_base import get_network_driver 40 | junos_driver = get_network_driver('junos') 41 | eos_driver = get_network_driver('eos') 42 | 43 | junos_configuration = { 44 | 'hostname': '127.0.0.1', 45 | 'username': 'vagrant', 46 | 'password': '', 47 | 'optional_args': {'port': 12203} 48 | } 49 | 50 | eos_configuration = { 51 | 'hostname': '127.0.0.1', 52 | 'username': 'vagrant', 53 | 'password': 'vagrant', 54 | 'optional_args': {'port': 12443} 55 | } 56 | 57 | def change_configuration(device, configuration): 58 | device.load_merge_candidate(config=configuration) 59 | print(device.compare_config()) 60 | device.commit_config() 61 | 62 | 63 | with junos_driver(**junos_configuration) as junos: 64 | change_configuration(junos, "system {host-name yet-another-hostname;}") 65 | 66 | 67 | with eos_driver(**eos_configuration) as eos: 68 | change_configuration(eos, 'hostname yet-another-hostname') 69 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | nsot docstring. 4 | """ 5 | import json 6 | import netaddr 7 | 8 | 9 | from pynsot.client import get_api_client 10 | 11 | 12 | def _build_resource_map(resource_list, alias='name'): 13 | return {r['id']: r[alias] for r in resource_list} 14 | 15 | 16 | def _process_devices(devices, sites_map, result): 17 | for device in devices: 18 | result[sites_map[device['site_id']]]['hosts'].append(device['hostname']) 19 | result['_meta']['hostvars'][device['hostname']] = device['attributes'] 20 | result['_meta']['hostvars'][device['hostname']]['interfaces'] = {} 21 | 22 | 23 | def _resolve_cidr(address, networks): 24 | for network in networks: 25 | net = netaddr.IPNetwork(network) 26 | cidr = '{}/{}'.format(address, net.prefixlen) 27 | if address in net: 28 | return cidr 29 | 30 | 31 | def _process_interfaces(interfaces, device_map, result): 32 | for interface in interfaces: 33 | ipv4 = [] 34 | ipv6 = [] 35 | for address in interface['addresses']: 36 | addr = netaddr.IPAddress(address.split('/')[0]) 37 | cidr = _resolve_cidr(addr, interface['networks']) 38 | if addr.version == 4: 39 | ipv4.append(cidr) 40 | else: 41 | ipv6.append(cidr) 42 | interface['ipv4'] = ipv4 43 | interface['ipv6'] = ipv6 44 | result['_meta']['hostvars'][device_map[interface['device']]]['interfaces'][interface['name']] = interface # noqa 45 | 46 | 47 | def main(): 48 | """Main process.""" 49 | api = get_api_client() 50 | sites = api.sites.get() 51 | sites_map = _build_resource_map(sites) 52 | 53 | devices = [] 54 | for site in sites: 55 | devices += api.sites(site['id']).devices.get() 56 | devices_map = _build_resource_map(devices, 'hostname') 57 | 58 | interfaces = [] 59 | for site in sites: 60 | interfaces += api.sites(site['id']).interfaces.get() 61 | 62 | result = { 63 | "_meta": { 64 | "hostvars": {} 65 | } 66 | } 67 | 68 | for _, site in sites_map.items(): 69 | result[site] = { 70 | "hosts": [], 71 | "children": [], 72 | "vars": {} 73 | } 74 | _process_devices(devices, sites_map, result) 75 | _process_interfaces(interfaces, devices_map, result) 76 | 77 | return result 78 | 79 | 80 | if __name__ == "__main__": 81 | print json.dumps(main()) 82 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/hosts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | nsot docstring. 4 | """ 5 | import json 6 | import netaddr 7 | 8 | 9 | from pynsot.client import get_api_client 10 | 11 | 12 | def _build_resource_map(resource_list, alias='name'): 13 | return {r['id']: r[alias] for r in resource_list} 14 | 15 | 16 | def _process_devices(devices, sites_map, result): 17 | for device in devices: 18 | result[sites_map[device['site_id']]]['hosts'].append(device['hostname']) 19 | result['_meta']['hostvars'][device['hostname']] = device['attributes'] 20 | result['_meta']['hostvars'][device['hostname']]['interfaces'] = {} 21 | 22 | 23 | def _resolve_cidr(address, networks): 24 | for network in networks: 25 | net = netaddr.IPNetwork(network) 26 | cidr = '{}/{}'.format(address, net.prefixlen) 27 | if address in net: 28 | return cidr 29 | 30 | 31 | def _process_interfaces(interfaces, device_map, result): 32 | for interface in interfaces: 33 | ipv4 = [] 34 | ipv6 = [] 35 | for address in interface['addresses']: 36 | addr = netaddr.IPAddress(address.split('/')[0]) 37 | cidr = _resolve_cidr(addr, interface['networks']) 38 | if addr.version == 4: 39 | ipv4.append(cidr) 40 | else: 41 | ipv6.append(cidr) 42 | interface['ipv4'] = ipv4 43 | interface['ipv6'] = ipv6 44 | result['_meta']['hostvars'][device_map[interface['device']]]['interfaces'][interface['name']] = interface # noqa 45 | 46 | 47 | def main(): 48 | """Main process.""" 49 | api = get_api_client() 50 | sites = api.sites.get() 51 | sites_map = _build_resource_map(sites) 52 | 53 | devices = [] 54 | for site in sites: 55 | devices += api.sites(site['id']).devices.get() 56 | devices_map = _build_resource_map(devices, 'hostname') 57 | 58 | interfaces = [] 59 | for site in sites: 60 | interfaces += api.sites(site['id']).interfaces.get() 61 | 62 | result = { 63 | "_meta": { 64 | "hostvars": {} 65 | } 66 | } 67 | 68 | for _, site in sites_map.items(): 69 | result[site] = { 70 | "hosts": [], 71 | "children": [], 72 | "vars": {} 73 | } 74 | _process_devices(devices, sites_map, result) 75 | _process_interfaces(interfaces, devices_map, result) 76 | 77 | return result 78 | 79 | 80 | if __name__ == "__main__": 81 | print json.dumps(main()) 82 | -------------------------------------------------------------------------------- /tutorial-0-building-env/README.md: -------------------------------------------------------------------------------- 1 | # tutorial-0-building-env 2 | 3 | ## Requirements 4 | 5 | 1. [git](https://git-scm.com/downloads) 6 | 1. [python 2.7](https://www.python.org/downloads/) 7 | 1. [VirtualBox](https://www.virtualbox.org/wiki/Downloads) 8 | 1. [Vagrant](https://www.vagrantup.com/docs/installation/) 9 | 1. [virtualenvwrapper](http://virtualenvwrapper.readthedocs.io/en/latest/install.html) (optional but recommended) 10 | 11 | ## Git 12 | 13 | Installing git is very straightforward, just follow the instructions for your operating system. 14 | 15 | ## Python 16 | 17 | To install python follow the instructions for your operating system. To verify python is installed correctly open a terminal, execute the command ``python`` and then execute ``print("hello world")``. 18 | 19 | ![hello world](content/python.png) 20 | 21 | ## vagrant 22 | 23 | To install Vagrant install first VirtualBox and then Vagrant. In both cases follow the instructions for your operating system. 24 | 25 | ### vagrant boxes 26 | 27 | Now we are going to need a few boxes, these boxes will help us provision clean labs every time we need them. To support the following tutorials we will use a box containining vEOS and another one containing JunOS. The main reason for this is because they are the only ones that provide them without needing to pay or contact some sales representative. 28 | 29 | First of all, you will have to go to arista [download](http://www.arista.com/en/support/software-download) page (you can register for free) and download the file ``vEOS-lab-4.16.6M-virtualbox.box`` under the vEOS section. 30 | 31 | ![eos box](content/support_arista.png) 32 | 33 | Then you will have to execute the following commands: 34 | 35 | vagrant box add --name vEOS-lab-4.16.6M Downloads/vEOS-lab-4.16.6M-virtualbox.box 36 | vagrant box add juniper/ffp-12.1X47-D20.7-packetmode 37 | 38 | ![vagrant add](content/vagrant_add.png) 39 | 40 | ## Testing the setup 41 | 42 | Now that everything is in place let's clone the repository where the tutorial lives and try to start the test lab. 43 | 44 | git clone https://github.com/dravetech/network-tutorials.git 45 | cd network-tutorials/labs/lab1 46 | vagrant up 47 | 48 | The last command will initialize the lab. To see the description of the lab check the contents of the file ``labs/lab1/Vagrantfile``. You should be able to connect to the vEOS and the JunOS box with the commands ``vagrant ssh eos`` and ``vagrant ssh junos`` respectively. 49 | 50 | When you are done with the lab you can destroy it to free up resources with ``vagrant destroy``. 51 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/playbook_verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get facts 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | 8 | tasks: 9 | - name: get facts from device 10 | napalm_get_facts: 11 | hostname: "{{ host }}" 12 | username: "{{ user }}" 13 | dev_os: "{{ os }}" 14 | password: "{{ password }}" 15 | optional_args: 16 | port: "{{ port }}" 17 | filter: ['bgp_neighbors'] 18 | register: napalm_facts 19 | - name: Check all BGP sessions are configured 20 | assert: 21 | that: 22 | - hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] in napalm_facts.ansible_facts.bgp_neighbors.global.peers 23 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is missing" 24 | with_dict: "{{ interfaces }}" 25 | when: "{{ item.value.attributes.link_type == 'fabric_links' }}" 26 | tags: [print_action] 27 | - name: Check BGP sessions are healthy 28 | assert: 29 | that: 30 | - napalm_facts.ansible_facts.bgp_neighbors.global.peers[hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0]].is_up 31 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is configured but down" 32 | with_dict: "{{ interfaces }}" 33 | when: "{{ item.value.attributes.link_type == 'fabric_links' }}" 34 | tags: [print_action] 35 | - name: Chack BGP sessions are receiving prefixes 36 | assert: 37 | that: 38 | - napalm_facts.ansible_facts.bgp_neighbors.global.peers[hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0]].address_family.ipv6.received_prefixes > 0 39 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is not receiving any prefixes" 40 | with_dict: "{{ interfaces }}" 41 | when: "{{ item.value.attributes.link_type == 'fabric_links' }}" 42 | tags: [print_action] 43 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/playbook_verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get facts 3 | hosts: all 4 | connection: local 5 | gather_facts: no 6 | vars: 7 | 8 | tasks: 9 | - name: get facts from device 10 | napalm_get_facts: 11 | hostname: "{{ host }}" 12 | username: "{{ user }}" 13 | dev_os: "{{ os }}" 14 | password: "{{ password }}" 15 | optional_args: 16 | port: "{{ port }}" 17 | filter: ['bgp_neighbors'] 18 | register: napalm_facts 19 | - name: Check all BGP sessions are configured 20 | assert: 21 | that: 22 | - hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] in napalm_facts.ansible_facts.bgp_neighbors.global.peers 23 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is missing" 24 | with_dict: "{{ interfaces }}" 25 | when: "{{ item.value.attributes.link_type == 'fabric' }}" 26 | tags: [print_action] 27 | - name: Check BGP sessions are healthy 28 | assert: 29 | that: 30 | - napalm_facts.ansible_facts.bgp_neighbors.global.peers[hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0]].is_up 31 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is configured but down" 32 | with_dict: "{{ interfaces }}" 33 | when: "{{ item.value.attributes.link_type == 'fabric' }}" 34 | tags: [print_action] 35 | - name: Chack BGP sessions are receiving prefixes 36 | assert: 37 | that: 38 | - napalm_facts.ansible_facts.bgp_neighbors.global.peers[hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0]].address_family.ipv6.received_prefixes > 0 39 | msg: "{{ '{}:{}'.format(item.value.attributes.connects_to_iface, item.value.attributes.connects_to_device) }} ({{ hostvars[item.value.attributes.connects_to_device]['interfaces'][item.value.attributes.connects_to_iface]['ipv6'][0].split('/')[0] }}) is not receiving any prefixes" 40 | with_dict: "{{ interfaces }}" 41 | when: "{{ item.value.attributes.link_type == 'fabric' }}" 42 | tags: [print_action] 43 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/create_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x #echo on 3 | 4 | nsot sites add --name my_demo_site 5 | 6 | nsot attributes add --site-id 1 --resource-name device --name os 7 | nsot attributes add --site-id 1 --resource-name device --name host 8 | nsot attributes add --site-id 1 --resource-name device --name user 9 | nsot attributes add --site-id 1 --resource-name device --name password --allow-empty 10 | nsot attributes add --site-id 1 --resource-name device --name port 11 | 12 | nsot devices add --site-id 1 --hostname rtr00 13 | nsot devices update --site-id 1 --id 1 -a os=eos -a host=127.0.0.1 -a user=vagrant -a password=vagrant -a port=12443 14 | nsot devices add --site-id 1 --hostname rtr01 15 | nsot devices update --site-id 1 --id 2 -a os=junos -a host=127.0.0.1 -a user=vagrant -a password="" -a port=12203 16 | 17 | 18 | nsot attributes add --site-id 1 --resource-name device --name asn 19 | nsot attributes add --site-id 1 --resource-name device --name router_id 20 | 21 | nsot devices update --site-id 1 --id 1 -a asn=65001 -a router_id=10.1.1.1 22 | nsot devices update --site-id 1 --id 2 -a asn=65002 -a router_id=10.1.1.2 23 | 24 | nsot attributes add --site-id 1 --resource-name network --name type 25 | nsot networks add --site-id 1 --cidr 2001:db8:b33f::/64 -a type=loopbacks 26 | 27 | nsot attributes add --site-id 1 --resource-name interface --name link_type 28 | nsot attributes add --site-id 1 --resource-name interface --name connects_to_device 29 | nsot attributes add --site-id 1 --resource-name interface --name connects_to_iface 30 | 31 | nsot interfaces add --site-id 1 --device 1 --name lo0 --addresses 2001:db8:b33f::100/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 32 | nsot interfaces add --site-id 1 --device 2 --name lo0 --addresses 2001:db8:b33f::101/128 -a link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 33 | 34 | nsot networks add --site-id 1 --cidr 2001:db8:caf3::/64 -a type=ptp 35 | nsot networks add --site-id 1 --cidr 2001:db8:caf3::/127 -a type=ptp 36 | nsot networks add --site-id 1 --cidr 2001:db8:caf3::2/127 -a type=ptp 37 | 38 | nsot interfaces add --site-id 1 --device 1 --name et1 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/1 -c 2001:db8:caf3:: 39 | nsot interfaces add --site-id 1 --device 1 --name et2 -a link_type=fabric -a connects_to_device=rtr01 -a connects_to_iface=ge-0/0/2 -c 2001:db8:caf3::2 40 | 41 | nsot interfaces add --site-id 1 --device 2 --name ge-0/0/1 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et1 -c 2001:db8:caf3::1 42 | nsot interfaces add --site-id 1 --device 2 --name ge-0/0/2 -a link_type=fabric -a connects_to_device=rtr00 -a connects_to_iface=et2 -c 2001:db8:caf3::3 43 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/tasks/tasks_site.py: -------------------------------------------------------------------------------- 1 | """Tasks to manipulate sites""" 2 | 3 | import invoke 4 | 5 | from pynsot.client import get_api_client 6 | 7 | from pynsot.vendor.slumber.exceptions import HttpClientError 8 | 9 | import helpers 10 | 11 | import yaml 12 | 13 | import sys 14 | 15 | 16 | @invoke.task 17 | def create(ctx, name, description="", debug=False): 18 | """Create a site.""" 19 | global logger 20 | logger = helpers.get_logger(debug) 21 | 22 | client = get_api_client() 23 | site = client.sites.get(name=name) 24 | 25 | site_data = { 26 | 'name': name, 27 | 'description': description, 28 | } 29 | 30 | if not site: 31 | site = client.sites.post(site_data) 32 | else: 33 | site = client.sites(site[0]['id']).put(site_data) 34 | 35 | logger.info(site) 36 | return site 37 | 38 | 39 | @invoke.task 40 | def add_atribbutes(ctx, site, filename="data/attributes.yml", debug=False): 41 | """Add attributes to a site.""" 42 | global logger 43 | logger = helpers.get_logger(debug) 44 | 45 | client = get_api_client() 46 | with open(filename, 'r') as f: 47 | attributes = yaml.load(f.read())['attributes'] 48 | 49 | site_id = helpers.get_site_id(client, site) 50 | 51 | result = {} 52 | for resource, attrs in attributes.items(): 53 | for attr_name, attr_options in attrs.items(): 54 | attribute = client.sites(site_id).attributes.get(resource=resource, name=attr_name) 55 | 56 | options = { 57 | 'name': attr_name, 58 | 'resource_name': resource, 59 | } 60 | options.update(attr_options) 61 | try: 62 | if not attribute: 63 | logger.info("Creating attribute: {}".format(options)) 64 | attribute = client.sites(site_id).attributes.post(options) 65 | else: 66 | logger.info("Updating attribute: {}".format(attribute)) 67 | attribute = client.sites(site_id).attributes(attribute[0]['id']).put(options) 68 | except HttpClientError as e: 69 | logger.error(e) 70 | logger.error(e.response.json()) 71 | sys.exit(-1) 72 | 73 | result[attr_name] = attribute 74 | 75 | return result 76 | 77 | 78 | @invoke.task 79 | def add_devices(ctx, site, filename="data/devices.yml", debug=False): 80 | """Add devices to a site.""" 81 | global logger 82 | logger = helpers.get_logger(debug) 83 | 84 | client = get_api_client() 85 | with open(filename, 'r') as f: 86 | devices = yaml.load(f.read())['devices'] 87 | 88 | site_id = helpers.get_site_id(client, site) 89 | 90 | result = {} 91 | for host, data in devices.items(): 92 | device = client.sites(site_id).devices.get(hostname=host) 93 | data = { 94 | 'attributes': data, 95 | 'hostname': host 96 | } 97 | if not device: 98 | logger.info("Creating device: {}".format(data)) 99 | device = client.sites(site_id).devices.post(data) 100 | else: 101 | logger.info("Updating device: {}".format(device)) 102 | device = client.sites(site_id).devices(device[0]['id']).put(data) 103 | result[host] = device 104 | 105 | return result 106 | 107 | 108 | @invoke.task 109 | def deploy(ctx, site, commit=False): 110 | """Deploy a site.""" 111 | check_mode = "-C" if not commit else "" 112 | cli = "ansible-playbook playbook_configure.yml -l {} {}".format(site, 113 | check_mode) 114 | invoke.run(cli) 115 | 116 | 117 | @invoke.task 118 | def verify(ctx, site): 119 | """Verify site is healthy.""" 120 | cli = "ansible-playbook playbook_verify.yml -l {}".format(site) 121 | invoke.run(cli) 122 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/create_all_the_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | nsot docstring. 4 | """ 5 | 6 | import logging 7 | import sys 8 | 9 | from pynsot.client import get_api_client 10 | from pynsot.models import Network 11 | 12 | import yaml 13 | 14 | 15 | logger = logging.getLogger("site_creator") 16 | 17 | 18 | def _configure_logging(logger, debug): 19 | if debug: 20 | logger.setLevel(logging.DEBUG) 21 | else: 22 | logger.setLevel(logging.INFO) 23 | 24 | ch = logging.StreamHandler(sys.stdout) 25 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 26 | ch.setFormatter(formatter) 27 | logger.addHandler(ch) 28 | return logger 29 | 30 | 31 | def create_site(site_data): 32 | """Create site.""" 33 | site = api.sites.get(name=site_data['name']) 34 | 35 | if not site: 36 | site = api.sites.post(site_data) 37 | else: 38 | site = api.sites(site[0]['id']).put(site_data) 39 | logger.debug("site: {}".format(site)) 40 | return site 41 | 42 | 43 | def create_attributes(attributes, site): 44 | """Create attributes.""" 45 | result = {} 46 | for resource, attrs in attributes.items(): 47 | for attr_name, attr_options in attrs.items(): 48 | attribute = api.attributes.get(site=site['id'], resource=resource, name=attr_name) 49 | 50 | options = { 51 | 'name': attr_name, 52 | 'resource_name': resource, 53 | } 54 | options.update(attr_options) 55 | if not attribute: 56 | attribute = api.sites(site['id']).attributes.post(options) 57 | else: 58 | attribute = api.sites(site['id']).attributes(attribute[0]['id']).put(options) 59 | result[attr_name] = attribute 60 | logger.debug("attribute: {}".format(attribute)) 61 | return result 62 | 63 | 64 | def _create_networks(networks, service, site): 65 | result = {} 66 | 67 | for net_type, prefix in networks.items(): 68 | network = api.networks.get(site=site['id'], cidr=prefix, service=service, type=net_type) 69 | data = { 70 | 'attributes': { 71 | 'type': net_type, 72 | 'service': service 73 | }, 74 | 'cidr': prefix 75 | } 76 | if not network: 77 | network = api.sites(site['id']).networks.post(data) 78 | else: 79 | network = api.sites(site['id']).networks(network[0]['id']).put(data) 80 | logger.debug("network: {}".format(network)) 81 | result[net_type] = network 82 | return result 83 | 84 | 85 | def service_fabric_links(data, networks): 86 | logger.info(data) 87 | logger.info(networks) 88 | 89 | 90 | def service_loopbacks(devices, network): 91 | logger.info(devices) 92 | logger.info(network) 93 | net = Network(client=api, site_id=network['site_id'], 94 | cidr='{}/{}'.format(network['network_address'], network['prefix_length'])) 95 | logger.info(dir(net)) 96 | logger.info(net.next()) 97 | 98 | 99 | 100 | def create_service(name, data, site, devices): 101 | result = {'networks': _create_networks(data['network_ranges'], name, site), 'services': {}} 102 | logger.info(result) 103 | 104 | # if name == 'fabric_links': 105 | # result['services']['fabric_links'] = service_fabric_links(data, result['networks']) 106 | if name == 'loopbacks': 107 | result['services']['loopbacks'] = service_loopbacks(devices, 108 | result['networks']['loopbacks']) 109 | 110 | return result 111 | 112 | 113 | def create_devices(devices, site): 114 | """Create devices.""" 115 | result = {} 116 | 117 | for host, data in devices.items(): 118 | device = api.devices.get(site=site['id'], hostname=host) 119 | data = { 120 | 'attributes': data, 121 | 'hostname': host 122 | } 123 | if not device: 124 | device = api.sites(site['id']).devices.post(data) 125 | else: 126 | device = api.sites(site['id']).devices(device[0]['id']).put(data) 127 | result[host] = device 128 | logger.debug("device: {}".format(device)) 129 | return result 130 | 131 | if __name__ == "__main__": 132 | with open('service.yaml', 'r') as f: 133 | data = yaml.load(f.read()) 134 | 135 | _configure_logging(logger, debug=False) 136 | 137 | global api 138 | api = get_api_client() 139 | 140 | site = create_site(data['site']) 141 | attributes = create_attributes(data['attributes'], site) 142 | devices = create_devices(data['devices'], site) 143 | 144 | services = {} 145 | for svc, svc_data in data['services'].items(): 146 | services[svc] = create_service(svc, svc_data, site, devices) 147 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/tasks/tasks_service.py: -------------------------------------------------------------------------------- 1 | """Tasks to manipulate sites""" 2 | 3 | from invoke import task 4 | from pynsot.client import get_api_client 5 | 6 | from pynsot.vendor.slumber.exceptions import HttpClientError 7 | 8 | import helpers 9 | 10 | import yaml 11 | 12 | import sys 13 | 14 | 15 | def _create_networks(client, networks, service, site_id): 16 | result = {} 17 | 18 | for net_type, prefix in networks.items(): 19 | network = client.sites(site_id).networks.get(cidr=prefix, service=service, type=net_type) 20 | data = { 21 | 'attributes': { 22 | 'type': net_type, 23 | 'service': service 24 | }, 25 | 'cidr': prefix 26 | } 27 | if not network: 28 | logger.info("Creating network: {}".format(data)) 29 | network = client.sites(site_id).networks.post(data) 30 | else: 31 | logger.info("Updating network: {}".format(network)) 32 | network = client.sites(site_id).networks(network[0]['id']).put(data) 33 | result[net_type] = network 34 | return result 35 | 36 | 37 | def _create_subnet(client, site_id, supernet, prefix_length): 38 | network = client.sites(site_id).networks(supernet['id']).next_network.get( 39 | prefix_length=prefix_length) 40 | print(supernet) 41 | print(network) 42 | data = { 43 | 'attributes': { 44 | 'type': supernet['attributes']['type'], 45 | 'service': supernet['attributes']['service'], 46 | }, 47 | 'cidr': network[0] 48 | } 49 | logger.info("Creating network: {}".format(data)) 50 | network = client.sites(site_id).networks.post(data) 51 | return network 52 | 53 | 54 | def _create_interface(client, site_id, device_id, iface_name, link_type, 55 | connects_to_device, connects_to_iface, network=None): 56 | """ 57 | nsot interfaces add --site-id 1 --device 1 --name lo0 --addresses 2001:db8:b33f::100/128 -a 58 | link_type=loopback -a connects_to_device=loopback -a connects_to_iface=lo0 59 | """ 60 | iface = [i for i in client.sites(site_id).devices(device_id).interfaces.get() 61 | if i['name'] == iface_name] 62 | 63 | if not iface: 64 | assigned = client.sites(site_id).networks(network['id']).next_address.get() \ 65 | if network else [] 66 | else: 67 | if iface[0]['addresses']: 68 | assigned = iface[0]['addresses'] 69 | else: 70 | assigned = client.sites(site_id).networks(network['id']).next_address.get() \ 71 | if network else [] 72 | print(assigned) 73 | 74 | data = { 75 | 'name': iface_name, 76 | 'device': device_id, 77 | 'addresses': assigned, 78 | 'attributes': { 79 | 'link_type': link_type, 80 | 'connects_to_device': connects_to_device, 81 | 'connects_to_iface': connects_to_iface, 82 | } 83 | } 84 | try: 85 | if not iface: 86 | logger.info("Creating iface: {}".format(data)) 87 | iface = client.sites(site_id).interfaces.post(data) 88 | else: 89 | logger.info("Updating iface: {}".format(iface)) 90 | iface = client.sites(site_id).interfaces(iface[0]['id']).put(data) 91 | 92 | except HttpClientError as e: 93 | logger.error(e) 94 | logger.error(e.response.json()) 95 | sys.exit(-1) 96 | logger.info(iface) 97 | return iface 98 | 99 | 100 | @task 101 | def loopbacks(ctx, site, filename="data/services.yml", debug=False): 102 | """Read service definition for loopbacks service and add data to the backend.""" 103 | global logger 104 | logger = helpers.get_logger(debug) 105 | 106 | client = get_api_client() 107 | with open(filename, 'r') as f: 108 | service = yaml.load(f.read())['loopbacks'] 109 | logger.debug(service) 110 | 111 | site_id = helpers.get_site_id(client, site) 112 | 113 | network = _create_networks(client, service['network_ranges'], 'loopback', 114 | site_id)['loopbacks'] 115 | 116 | ifaces = [] 117 | for device in client.sites(site_id).devices.get(): 118 | ifaces.append(_create_interface(client, site_id, device['id'], 'lo0', 'loopbacks', 119 | 'loopback', 'loopback', network)) 120 | return ifaces 121 | 122 | 123 | @task 124 | def ipfabric(ctx, site, filename="data/services.yml", debug=False): 125 | """Read service definition for ipfabric service and add data to the backend.""" 126 | global logger 127 | logger = helpers.get_logger(debug) 128 | 129 | client = get_api_client() 130 | with open(filename, 'r') as f: 131 | service = yaml.load(f.read())['ipfabric'] 132 | logger.debug(service) 133 | 134 | site_id = helpers.get_site_id(client, site) 135 | 136 | supernet = _create_networks(client, service['network_ranges'], 'ipfabric', 137 | site_id)['fabric_links'] 138 | 139 | for link in service['definition']['links']: 140 | network = None 141 | logger.debug("Processing {}".format(link)) 142 | left_id = helpers.get_host_id(client, site_id, link['left_device']) 143 | right_id = helpers.get_host_id(client, site_id, link['right_device']) 144 | logger.debug("Found hosts {} and {}".format(left_id, right_id)) 145 | 146 | left_interface = _create_interface(client, site_id, left_id, link['left_iface'], 147 | 'fabric_links', link['right_device'], 148 | link['right_iface']) 149 | right_interface = _create_interface(client, site_id, right_id, link['right_iface'], 150 | 'fabric_links', link['left_device'], 151 | link['left_iface']) 152 | if not left_interface['networks']: 153 | network = _create_subnet(client, site_id, supernet, 127) 154 | left_interface = _create_interface(client, site_id, left_id, link['left_iface'], 155 | 'fabric_links', link['right_device'], 156 | link['right_iface'], network) 157 | if not right_interface['networks']: 158 | network = client.sites(site_id).networks(left_interface['networks'][0]).get() 159 | right_interface = _create_interface(client, site_id, right_id, link['right_iface'], 160 | 'fabric_links', link['left_device'], 161 | link['left_iface'], network) 162 | 163 | return network 164 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/library/napalm_get_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | 7 | This file is part of Ansible 8 | 9 | Ansible is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | Ansible is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with Ansible. If not, see . 21 | """ 22 | 23 | DOCUMENTATION = ''' 24 | --- 25 | module: napalm_get_facts 26 | author: "Elisa Jasinska (@fooelisa)" 27 | version_added: "2.1" 28 | short_description: "Gathers facts from a network device via napalm" 29 | description: 30 | - "Gathers facts from a network device via the Python module napalm" 31 | requirements: 32 | - napalm 33 | options: 34 | hostname: 35 | description: 36 | - IP or FQDN of the device you want to connect to 37 | required: True 38 | username: 39 | description: 40 | - Username 41 | required: True 42 | password: 43 | description: 44 | - Password 45 | required: True 46 | dev_os: 47 | description: 48 | - OS of the device 49 | required: True 50 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 51 | timeout: 52 | description: 53 | - Time in seconds to wait for the device to respond 54 | required: False 55 | default: 60 56 | optional_args: 57 | description: 58 | - Dictionary of additional arguments passed to underlying driver 59 | required: False 60 | default: None 61 | filter: 62 | description: 63 | - A list of facts to retreive from a device and provided though C(ansible_facts) 64 | The following facts are available- 65 | facts, environment, interfaces, interfaces_counter, bgp_config, bgp_neighbors, 66 | bgp_neighbors_detail, lldp_neighbors, lldp_neighbors_detail 67 | Note- not all getters are implemented on all supported devcie types 68 | required: False 69 | default: ['facts'] 70 | ''' 71 | 72 | EXAMPLES = ''' 73 | - name: get facts from device 74 | napalm_get_facts: 75 | hostname={{ inventory_hostname }} 76 | username={{ user }} 77 | dev_os={{ os }} 78 | password={{ passwd }} 79 | filter=['facts'] 80 | register: result 81 | 82 | - name: print data 83 | debug: var=result 84 | ''' 85 | 86 | RETURN = ''' 87 | changed: 88 | description: "whether the command has been executed on the device" 89 | returned: always 90 | type: bool 91 | sample: True 92 | ansible_facts: 93 | description: "Facts gathered on the device provided via C(ansible_facts)" 94 | returned: certain keys are returned depending on filter 95 | type: dict 96 | ''' 97 | 98 | try: 99 | from napalm_base import get_network_driver 100 | except ImportError: 101 | napalm_found = False 102 | else: 103 | napalm_found = True 104 | 105 | def main(): 106 | module = AnsibleModule( 107 | argument_spec=dict( 108 | hostname=dict(type='str', required=True), 109 | username=dict(type='str', required=True), 110 | password=dict(type='str', required=True, no_log=True), 111 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 112 | timeout=dict(type='int', required=False, default=60), 113 | optional_args=dict(type='dict', required=False, default=None), 114 | filter=dict(type='list', required=False, default=['facts']), 115 | 116 | ), 117 | supports_check_mode=True 118 | ) 119 | 120 | if not napalm_found: 121 | module.fail_json(msg="the python module napalm is required") 122 | 123 | hostname = module.params['hostname'] 124 | username = module.params['username'] 125 | dev_os = module.params['dev_os'] 126 | password = module.params['password'] 127 | timeout = module.params['timeout'] 128 | filter_list = module.params['filter'] 129 | 130 | if module.params['optional_args'] is None: 131 | optional_args = {} 132 | else: 133 | optional_args = module.params['optional_args'] 134 | 135 | # open device connection 136 | try: 137 | network_driver = get_network_driver(dev_os) 138 | device = network_driver(hostname=hostname, 139 | username=username, 140 | password=password, 141 | timeout=timeout, 142 | optional_args=optional_args) 143 | device.open() 144 | except Exception, e: 145 | module.fail_json(msg="cannot connect to device: " + str(e)) 146 | 147 | # retreive data from device 148 | facts = {} 149 | try: 150 | for filter in filter_list: 151 | if filter == 'facts': 152 | result = device.get_facts() 153 | facts['facts'] = result 154 | elif filter == 'interfaces': 155 | result = device.get_interfaces() 156 | facts['interfaces'] = result 157 | elif filter == 'interfaces_ip': 158 | result = device.get_interfaces_ip() 159 | facts['interfaces_ip'] = result 160 | elif filter == 'interfaces_counter': 161 | result = device.get_interfaces_counter() 162 | facts['interfaces_counter'] = result 163 | elif filter == 'bgp_config': 164 | result = device.get_bgp_config() 165 | facts['bgp_config'] = result 166 | elif filter == 'bgp_neighbors': 167 | result = device.get_bgp_neighbors() 168 | facts['bgp_neighbors'] = result 169 | elif filter == 'bgp_neighbors_detail': 170 | result = device.get_bgp_neighbors_detail() 171 | facts['bgp_neighbors_detail'] = result 172 | elif filter == 'environment': 173 | result = device.get_environment() 174 | facts['environment'] = result 175 | elif filter == 'lldp_neighbors': 176 | result = device.get_lldp_neighbors() 177 | facts['lldp_neighbors'] = result 178 | elif filter == 'lldp_neighbors_detail': 179 | result = device.get_lldp_neighbors_detail() 180 | facts['lldp_neighbors_detail'] = result 181 | else: 182 | module.fail_json(msg="filter not recognized: " + filter) 183 | except Exception, e: 184 | module.fail_json(msg="cannot retrieve device data: " + str(e)) 185 | 186 | # close device connection 187 | try: 188 | device.close() 189 | except Exception, e: 190 | module.fail_json(msg="cannot close device connection: " + str(e)) 191 | 192 | module.exit_json(ansible_facts=facts) 193 | 194 | # standard ansible module imports 195 | from ansible.module_utils.basic import * 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/library/napalm_get_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | 7 | This file is part of Ansible 8 | 9 | Ansible is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | Ansible is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with Ansible. If not, see . 21 | """ 22 | 23 | DOCUMENTATION = ''' 24 | --- 25 | module: napalm_get_facts 26 | author: "Elisa Jasinska (@fooelisa)" 27 | version_added: "2.1" 28 | short_description: "Gathers facts from a network device via napalm" 29 | description: 30 | - "Gathers facts from a network device via the Python module napalm" 31 | requirements: 32 | - napalm 33 | options: 34 | hostname: 35 | description: 36 | - IP or FQDN of the device you want to connect to 37 | required: True 38 | username: 39 | description: 40 | - Username 41 | required: True 42 | password: 43 | description: 44 | - Password 45 | required: True 46 | dev_os: 47 | description: 48 | - OS of the device 49 | required: True 50 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 51 | timeout: 52 | description: 53 | - Time in seconds to wait for the device to respond 54 | required: False 55 | default: 60 56 | optional_args: 57 | description: 58 | - Dictionary of additional arguments passed to underlying driver 59 | required: False 60 | default: None 61 | filter: 62 | description: 63 | - A list of facts to retreive from a device and provided though C(ansible_facts) 64 | The following facts are available- 65 | facts, environment, interfaces, interfaces_counter, bgp_config, bgp_neighbors, 66 | bgp_neighbors_detail, lldp_neighbors, lldp_neighbors_detail 67 | Note- not all getters are implemented on all supported devcie types 68 | required: False 69 | default: ['facts'] 70 | ''' 71 | 72 | EXAMPLES = ''' 73 | - name: get facts from device 74 | napalm_get_facts: 75 | hostname={{ inventory_hostname }} 76 | username={{ user }} 77 | dev_os={{ os }} 78 | password={{ passwd }} 79 | filter=['facts'] 80 | register: result 81 | 82 | - name: print data 83 | debug: var=result 84 | ''' 85 | 86 | RETURN = ''' 87 | changed: 88 | description: "whether the command has been executed on the device" 89 | returned: always 90 | type: bool 91 | sample: True 92 | ansible_facts: 93 | description: "Facts gathered on the device provided via C(ansible_facts)" 94 | returned: certain keys are returned depending on filter 95 | type: dict 96 | ''' 97 | 98 | try: 99 | from napalm_base import get_network_driver 100 | except ImportError: 101 | napalm_found = False 102 | else: 103 | napalm_found = True 104 | 105 | def main(): 106 | module = AnsibleModule( 107 | argument_spec=dict( 108 | hostname=dict(type='str', required=True), 109 | username=dict(type='str', required=True), 110 | password=dict(type='str', required=True, no_log=True), 111 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 112 | timeout=dict(type='int', required=False, default=60), 113 | optional_args=dict(type='dict', required=False, default=None), 114 | filter=dict(type='list', required=False, default=['facts']), 115 | 116 | ), 117 | supports_check_mode=True 118 | ) 119 | 120 | if not napalm_found: 121 | module.fail_json(msg="the python module napalm is required") 122 | 123 | hostname = module.params['hostname'] 124 | username = module.params['username'] 125 | dev_os = module.params['dev_os'] 126 | password = module.params['password'] 127 | timeout = module.params['timeout'] 128 | filter_list = module.params['filter'] 129 | 130 | if module.params['optional_args'] is None: 131 | optional_args = {} 132 | else: 133 | optional_args = module.params['optional_args'] 134 | 135 | # open device connection 136 | try: 137 | network_driver = get_network_driver(dev_os) 138 | device = network_driver(hostname=hostname, 139 | username=username, 140 | password=password, 141 | timeout=timeout, 142 | optional_args=optional_args) 143 | device.open() 144 | except Exception, e: 145 | module.fail_json(msg="cannot connect to device: " + str(e)) 146 | 147 | # retreive data from device 148 | facts = {} 149 | try: 150 | for filter in filter_list: 151 | if filter == 'facts': 152 | result = device.get_facts() 153 | facts['facts'] = result 154 | elif filter == 'interfaces': 155 | result = device.get_interfaces() 156 | facts['interfaces'] = result 157 | elif filter == 'interfaces_ip': 158 | result = device.get_interfaces_ip() 159 | facts['interfaces_ip'] = result 160 | elif filter == 'interfaces_counter': 161 | result = device.get_interfaces_counter() 162 | facts['interfaces_counter'] = result 163 | elif filter == 'bgp_config': 164 | result = device.get_bgp_config() 165 | facts['bgp_config'] = result 166 | elif filter == 'bgp_neighbors': 167 | result = device.get_bgp_neighbors() 168 | facts['bgp_neighbors'] = result 169 | elif filter == 'bgp_neighbors_detail': 170 | result = device.get_bgp_neighbors_detail() 171 | facts['bgp_neighbors_detail'] = result 172 | elif filter == 'environment': 173 | result = device.get_environment() 174 | facts['environment'] = result 175 | elif filter == 'lldp_neighbors': 176 | result = device.get_lldp_neighbors() 177 | facts['lldp_neighbors'] = result 178 | elif filter == 'lldp_neighbors_detail': 179 | result = device.get_lldp_neighbors_detail() 180 | facts['lldp_neighbors_detail'] = result 181 | else: 182 | module.fail_json(msg="filter not recognized: " + filter) 183 | except Exception, e: 184 | module.fail_json(msg="cannot retrieve device data: " + str(e)) 185 | 186 | # close device connection 187 | try: 188 | device.close() 189 | except Exception, e: 190 | module.fail_json(msg="cannot close device connection: " + str(e)) 191 | 192 | module.exit_json(ansible_facts=facts) 193 | 194 | # standard ansible module imports 195 | from ansible.module_utils.basic import * 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/library/napalm_get_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | 7 | This file is part of Ansible 8 | 9 | Ansible is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | Ansible is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with Ansible. If not, see . 21 | """ 22 | 23 | DOCUMENTATION = ''' 24 | --- 25 | module: napalm_get_facts 26 | author: "Elisa Jasinska (@fooelisa)" 27 | version_added: "2.1" 28 | short_description: "Gathers facts from a network device via napalm" 29 | description: 30 | - "Gathers facts from a network device via the Python module napalm" 31 | requirements: 32 | - napalm 33 | options: 34 | hostname: 35 | description: 36 | - IP or FQDN of the device you want to connect to 37 | required: True 38 | username: 39 | description: 40 | - Username 41 | required: True 42 | password: 43 | description: 44 | - Password 45 | required: True 46 | dev_os: 47 | description: 48 | - OS of the device 49 | required: True 50 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 51 | timeout: 52 | description: 53 | - Time in seconds to wait for the device to respond 54 | required: False 55 | default: 60 56 | optional_args: 57 | description: 58 | - Dictionary of additional arguments passed to underlying driver 59 | required: False 60 | default: None 61 | filter: 62 | description: 63 | - A list of facts to retreive from a device and provided though C(ansible_facts) 64 | The following facts are available- 65 | facts, environment, interfaces, interfaces_counter, bgp_config, bgp_neighbors, 66 | bgp_neighbors_detail, lldp_neighbors, lldp_neighbors_detail 67 | Note- not all getters are implemented on all supported devcie types 68 | required: False 69 | default: ['facts'] 70 | ''' 71 | 72 | EXAMPLES = ''' 73 | - name: get facts from device 74 | napalm_get_facts: 75 | hostname={{ inventory_hostname }} 76 | username={{ user }} 77 | dev_os={{ os }} 78 | password={{ passwd }} 79 | filter=['facts'] 80 | register: result 81 | 82 | - name: print data 83 | debug: var=result 84 | ''' 85 | 86 | RETURN = ''' 87 | changed: 88 | description: "whether the command has been executed on the device" 89 | returned: always 90 | type: bool 91 | sample: True 92 | ansible_facts: 93 | description: "Facts gathered on the device provided via C(ansible_facts)" 94 | returned: certain keys are returned depending on filter 95 | type: dict 96 | ''' 97 | 98 | try: 99 | from napalm_base import get_network_driver 100 | except ImportError: 101 | napalm_found = False 102 | else: 103 | napalm_found = True 104 | 105 | def main(): 106 | module = AnsibleModule( 107 | argument_spec=dict( 108 | hostname=dict(type='str', required=True), 109 | username=dict(type='str', required=True), 110 | password=dict(type='str', required=True, no_log=True), 111 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 112 | timeout=dict(type='int', required=False, default=60), 113 | optional_args=dict(type='dict', required=False, default=None), 114 | filter=dict(type='list', required=False, default=['facts']), 115 | 116 | ), 117 | supports_check_mode=True 118 | ) 119 | 120 | if not napalm_found: 121 | module.fail_json(msg="the python module napalm is required") 122 | 123 | hostname = module.params['hostname'] 124 | username = module.params['username'] 125 | dev_os = module.params['dev_os'] 126 | password = module.params['password'] 127 | timeout = module.params['timeout'] 128 | filter_list = module.params['filter'] 129 | 130 | if module.params['optional_args'] is None: 131 | optional_args = {} 132 | else: 133 | optional_args = module.params['optional_args'] 134 | 135 | # open device connection 136 | try: 137 | network_driver = get_network_driver(dev_os) 138 | device = network_driver(hostname=hostname, 139 | username=username, 140 | password=password, 141 | timeout=timeout, 142 | optional_args=optional_args) 143 | device.open() 144 | except Exception, e: 145 | module.fail_json(msg="cannot connect to device: " + str(e)) 146 | 147 | # retreive data from device 148 | facts = {} 149 | try: 150 | for filter in filter_list: 151 | if filter == 'facts': 152 | result = device.get_facts() 153 | facts['facts'] = result 154 | elif filter == 'interfaces': 155 | result = device.get_interfaces() 156 | facts['interfaces'] = result 157 | elif filter == 'interfaces_ip': 158 | result = device.get_interfaces_ip() 159 | facts['interfaces_ip'] = result 160 | elif filter == 'interfaces_counter': 161 | result = device.get_interfaces_counter() 162 | facts['interfaces_counter'] = result 163 | elif filter == 'bgp_config': 164 | result = device.get_bgp_config() 165 | facts['bgp_config'] = result 166 | elif filter == 'bgp_neighbors': 167 | result = device.get_bgp_neighbors() 168 | facts['bgp_neighbors'] = result 169 | elif filter == 'bgp_neighbors_detail': 170 | result = device.get_bgp_neighbors_detail() 171 | facts['bgp_neighbors_detail'] = result 172 | elif filter == 'environment': 173 | result = device.get_environment() 174 | facts['environment'] = result 175 | elif filter == 'lldp_neighbors': 176 | result = device.get_lldp_neighbors() 177 | facts['lldp_neighbors'] = result 178 | elif filter == 'lldp_neighbors_detail': 179 | result = device.get_lldp_neighbors_detail() 180 | facts['lldp_neighbors_detail'] = result 181 | else: 182 | module.fail_json(msg="filter not recognized: " + filter) 183 | except Exception, e: 184 | module.fail_json(msg="cannot retrieve device data: " + str(e)) 185 | 186 | # close device connection 187 | try: 188 | device.close() 189 | except Exception, e: 190 | module.fail_json(msg="cannot close device connection: " + str(e)) 191 | 192 | module.exit_json(ansible_facts=facts) 193 | 194 | # standard ansible module imports 195 | from ansible.module_utils.basic import * 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/library/napalm_get_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | 7 | This file is part of Ansible 8 | 9 | Ansible is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | Ansible is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with Ansible. If not, see . 21 | """ 22 | 23 | DOCUMENTATION = ''' 24 | --- 25 | module: napalm_get_facts 26 | author: "Elisa Jasinska (@fooelisa)" 27 | version_added: "2.1" 28 | short_description: "Gathers facts from a network device via napalm" 29 | description: 30 | - "Gathers facts from a network device via the Python module napalm" 31 | requirements: 32 | - napalm 33 | options: 34 | hostname: 35 | description: 36 | - IP or FQDN of the device you want to connect to 37 | required: True 38 | username: 39 | description: 40 | - Username 41 | required: True 42 | password: 43 | description: 44 | - Password 45 | required: True 46 | dev_os: 47 | description: 48 | - OS of the device 49 | required: True 50 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 51 | timeout: 52 | description: 53 | - Time in seconds to wait for the device to respond 54 | required: False 55 | default: 60 56 | optional_args: 57 | description: 58 | - Dictionary of additional arguments passed to underlying driver 59 | required: False 60 | default: None 61 | filter: 62 | description: 63 | - A list of facts to retreive from a device and provided though C(ansible_facts) 64 | The following facts are available- 65 | facts, environment, interfaces, interfaces_counter, bgp_config, bgp_neighbors, 66 | bgp_neighbors_detail, lldp_neighbors, lldp_neighbors_detail 67 | Note- not all getters are implemented on all supported devcie types 68 | required: False 69 | default: ['facts'] 70 | ''' 71 | 72 | EXAMPLES = ''' 73 | - name: get facts from device 74 | napalm_get_facts: 75 | hostname={{ inventory_hostname }} 76 | username={{ user }} 77 | dev_os={{ os }} 78 | password={{ passwd }} 79 | filter=['facts'] 80 | register: result 81 | 82 | - name: print data 83 | debug: var=result 84 | ''' 85 | 86 | RETURN = ''' 87 | changed: 88 | description: "whether the command has been executed on the device" 89 | returned: always 90 | type: bool 91 | sample: True 92 | ansible_facts: 93 | description: "Facts gathered on the device provided via C(ansible_facts)" 94 | returned: certain keys are returned depending on filter 95 | type: dict 96 | ''' 97 | 98 | try: 99 | from napalm_base import get_network_driver 100 | except ImportError: 101 | napalm_found = False 102 | else: 103 | napalm_found = True 104 | 105 | def main(): 106 | module = AnsibleModule( 107 | argument_spec=dict( 108 | hostname=dict(type='str', required=True), 109 | username=dict(type='str', required=True), 110 | password=dict(type='str', required=True, no_log=True), 111 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 112 | timeout=dict(type='int', required=False, default=60), 113 | optional_args=dict(type='dict', required=False, default=None), 114 | filter=dict(type='list', required=False, default=['facts']), 115 | 116 | ), 117 | supports_check_mode=True 118 | ) 119 | 120 | if not napalm_found: 121 | module.fail_json(msg="the python module napalm is required") 122 | 123 | hostname = module.params['hostname'] 124 | username = module.params['username'] 125 | dev_os = module.params['dev_os'] 126 | password = module.params['password'] 127 | timeout = module.params['timeout'] 128 | filter_list = module.params['filter'] 129 | 130 | if module.params['optional_args'] is None: 131 | optional_args = {} 132 | else: 133 | optional_args = module.params['optional_args'] 134 | 135 | # open device connection 136 | try: 137 | network_driver = get_network_driver(dev_os) 138 | device = network_driver(hostname=hostname, 139 | username=username, 140 | password=password, 141 | timeout=timeout, 142 | optional_args=optional_args) 143 | device.open() 144 | except Exception, e: 145 | module.fail_json(msg="cannot connect to device: " + str(e)) 146 | 147 | # retreive data from device 148 | facts = {} 149 | try: 150 | for filter in filter_list: 151 | if filter == 'facts': 152 | result = device.get_facts() 153 | facts['facts'] = result 154 | elif filter == 'interfaces': 155 | result = device.get_interfaces() 156 | facts['interfaces'] = result 157 | elif filter == 'interfaces_ip': 158 | result = device.get_interfaces_ip() 159 | facts['interfaces_ip'] = result 160 | elif filter == 'interfaces_counter': 161 | result = device.get_interfaces_counter() 162 | facts['interfaces_counter'] = result 163 | elif filter == 'bgp_config': 164 | result = device.get_bgp_config() 165 | facts['bgp_config'] = result 166 | elif filter == 'bgp_neighbors': 167 | result = device.get_bgp_neighbors() 168 | facts['bgp_neighbors'] = result 169 | elif filter == 'bgp_neighbors_detail': 170 | result = device.get_bgp_neighbors_detail() 171 | facts['bgp_neighbors_detail'] = result 172 | elif filter == 'environment': 173 | result = device.get_environment() 174 | facts['environment'] = result 175 | elif filter == 'lldp_neighbors': 176 | result = device.get_lldp_neighbors() 177 | facts['lldp_neighbors'] = result 178 | elif filter == 'lldp_neighbors_detail': 179 | result = device.get_lldp_neighbors_detail() 180 | facts['lldp_neighbors_detail'] = result 181 | else: 182 | module.fail_json(msg="filter not recognized: " + filter) 183 | except Exception, e: 184 | module.fail_json(msg="cannot retrieve device data: " + str(e)) 185 | 186 | # close device connection 187 | try: 188 | device.close() 189 | except Exception, e: 190 | module.fail_json(msg="cannot close device connection: " + str(e)) 191 | 192 | module.exit_json(ansible_facts=facts) 193 | 194 | # standard ansible module imports 195 | from ansible.module_utils.basic import * 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/library/napalm_install_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | Original prototype by David Barroso 7 | 8 | This file is part of Ansible 9 | 10 | Ansible is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | Ansible is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with Ansible. If not, see . 22 | """ 23 | 24 | DOCUMENTATION = ''' 25 | --- 26 | module: napalm_install_config 27 | author: "Elisa Jasinska (@fooelisa)" 28 | version_added: "2.1" 29 | short_description: "Installs the configuration taken from a file on a device supported by NAPALM" 30 | description: 31 | - "This library will take the configuration from a file and load it into a device running any OS supported by napalm. 32 | The old configuration will be replaced or merged with the new one." 33 | requirements: 34 | - napalm 35 | options: 36 | hostname: 37 | description: 38 | - IP or FQDN of the device you want to connect to 39 | required: True 40 | username: 41 | description: 42 | - Username 43 | required: True 44 | password: 45 | description: 46 | - Password 47 | required: True 48 | dev_os: 49 | description: 50 | - OS of the device 51 | required: True 52 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 53 | timeout: 54 | description: 55 | - Time in seconds to wait for the device to respond 56 | required: False 57 | default: 60 58 | optional_args: 59 | description: 60 | - Dictionary of additional arguments passed to underlying driver 61 | required: False 62 | default: None 63 | config_file: 64 | description: 65 | - Path to the file to load the configuration from 66 | required: True 67 | replace_config: 68 | description: 69 | - If set to True, the entire configuration on the device will be replaced during the commit. 70 | If set to False, we will merge the new config with the existing one. Default- False 71 | choices: [true,false] 72 | default: False 73 | required: False 74 | diff_file: 75 | description: 76 | - A path to the file where we store the "diff" between the running configuration and the new 77 | configuration. If not set the diff between configurations will not be saved. 78 | default: None 79 | required: False 80 | get_diffs: 81 | description: 82 | - Set to False to not have any diffs generated. Useful if platform does not support commands 83 | being used to generate diffs. Note- By default diffs are generated even if the diff_file 84 | param is not set. 85 | choices: [true,false] 86 | default: True 87 | required: False 88 | ''' 89 | 90 | EXAMPLES = ''' 91 | - assemble: 92 | src=../compiled/{{ inventory_hostname }}/ 93 | dest=../compiled/{{ inventory_hostname }}/running.conf 94 | 95 | - napalm_install_config: 96 | hostname={{ inventory_hostname }} 97 | username={{ user }} 98 | dev_os={{ os }} 99 | password={{ passwd }} 100 | config_file=../compiled/{{ inventory_hostname }}/running.conf 101 | replace_config={{ replace_config }} 102 | get_diffs=True 103 | diff_file=../compiled/{{ inventory_hostname }}/diff 104 | ''' 105 | 106 | RETURN = ''' 107 | changed: 108 | description: whether the config on the device was changed 109 | returned: always 110 | type: bool 111 | sample: True 112 | msg: 113 | description: diff of the change 114 | returned: always 115 | type: string 116 | sample: "[edit system]\n- host-name lab-testing;\n+ host-name lab;" 117 | ''' 118 | 119 | try: 120 | from napalm_base import get_network_driver 121 | except ImportError: 122 | napalm_found = False 123 | else: 124 | napalm_found = True 125 | 126 | def save_to_file(content, filename): 127 | f = open(filename, 'w') 128 | try: 129 | f.write(content) 130 | finally: 131 | f.close() 132 | 133 | def main(): 134 | module = AnsibleModule( 135 | argument_spec=dict( 136 | hostname=dict(type='str', required=True), 137 | username=dict(type='str', required=True), 138 | password=dict(type='str', required=True, no_log=True), 139 | timeout=dict(type='int', required=False, default=60), 140 | optional_args=dict(required=False, type='dict', default=None), 141 | config_file=dict(type='str', required=True), 142 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 143 | replace_config=dict(type='bool', required=False, default=False), 144 | diff_file=dict(type='str', required=False, default=None), 145 | get_diffs=dict(type='bool', required=False, default=True) 146 | ), 147 | supports_check_mode=True 148 | ) 149 | 150 | if not napalm_found: 151 | module.fail_json(msg="the python module napalm is required") 152 | 153 | hostname = module.params['hostname'] 154 | username = module.params['username'] 155 | dev_os = module.params['dev_os'] 156 | password = module.params['password'] 157 | timeout = module.params['timeout'] 158 | config_file = module.params['config_file'] 159 | replace_config = module.params['replace_config'] 160 | diff_file = module.params['diff_file'] 161 | get_diffs = module.params['get_diffs'] 162 | 163 | if module.params['optional_args'] is None: 164 | optional_args = {} 165 | else: 166 | optional_args = module.params['optional_args'] 167 | 168 | try: 169 | network_driver = get_network_driver(dev_os) 170 | device = network_driver(hostname=hostname, 171 | username=username, 172 | password=password, 173 | timeout=timeout, 174 | optional_args=optional_args) 175 | device.open() 176 | except Exception, e: 177 | module.fail_json(msg="cannot connect to device: " + str(e)) 178 | 179 | try: 180 | if replace_config: 181 | device.load_replace_candidate(filename=config_file) 182 | else: 183 | device.load_merge_candidate(filename=config_file) 184 | except Exception, e: 185 | module.fail_json(msg="cannot load config: " + str(e)) 186 | 187 | try: 188 | if get_diffs: 189 | diff = device.compare_config().encode('utf-8') 190 | changed = len(diff) > 0 191 | else: 192 | changed = True 193 | diff = None 194 | if diff_file is not None and get_diffs: 195 | save_to_file(diff, diff_file) 196 | except Exception, e: 197 | module.fail_json(msg="cannot diff config: " + str(e)) 198 | 199 | try: 200 | if module.check_mode: 201 | device.discard_config() 202 | else: 203 | if changed: 204 | device.commit_config() 205 | except Exception, e: 206 | module.fail_json(msg="cannot install config: " + str(e)) 207 | 208 | try: 209 | device.close() 210 | except Exception, e: 211 | module.fail_json(msg="cannot close device connection: " + str(e)) 212 | 213 | module.exit_json(changed=changed, msg=diff) 214 | 215 | # standard ansible module imports 216 | from ansible.module_utils.basic import * 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/library/napalm_install_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | Original prototype by David Barroso 7 | 8 | This file is part of Ansible 9 | 10 | Ansible is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | Ansible is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with Ansible. If not, see . 22 | """ 23 | 24 | DOCUMENTATION = ''' 25 | --- 26 | module: napalm_install_config 27 | author: "Elisa Jasinska (@fooelisa)" 28 | version_added: "2.1" 29 | short_description: "Installs the configuration taken from a file on a device supported by NAPALM" 30 | description: 31 | - "This library will take the configuration from a file and load it into a device running any OS supported by napalm. 32 | The old configuration will be replaced or merged with the new one." 33 | requirements: 34 | - napalm 35 | options: 36 | hostname: 37 | description: 38 | - IP or FQDN of the device you want to connect to 39 | required: True 40 | username: 41 | description: 42 | - Username 43 | required: True 44 | password: 45 | description: 46 | - Password 47 | required: True 48 | dev_os: 49 | description: 50 | - OS of the device 51 | required: True 52 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 53 | timeout: 54 | description: 55 | - Time in seconds to wait for the device to respond 56 | required: False 57 | default: 60 58 | optional_args: 59 | description: 60 | - Dictionary of additional arguments passed to underlying driver 61 | required: False 62 | default: None 63 | config_file: 64 | description: 65 | - Path to the file to load the configuration from 66 | required: True 67 | replace_config: 68 | description: 69 | - If set to True, the entire configuration on the device will be replaced during the commit. 70 | If set to False, we will merge the new config with the existing one. Default- False 71 | choices: [true,false] 72 | default: False 73 | required: False 74 | diff_file: 75 | description: 76 | - A path to the file where we store the "diff" between the running configuration and the new 77 | configuration. If not set the diff between configurations will not be saved. 78 | default: None 79 | required: False 80 | get_diffs: 81 | description: 82 | - Set to False to not have any diffs generated. Useful if platform does not support commands 83 | being used to generate diffs. Note- By default diffs are generated even if the diff_file 84 | param is not set. 85 | choices: [true,false] 86 | default: True 87 | required: False 88 | ''' 89 | 90 | EXAMPLES = ''' 91 | - assemble: 92 | src=../compiled/{{ inventory_hostname }}/ 93 | dest=../compiled/{{ inventory_hostname }}/running.conf 94 | 95 | - napalm_install_config: 96 | hostname={{ inventory_hostname }} 97 | username={{ user }} 98 | dev_os={{ os }} 99 | password={{ passwd }} 100 | config_file=../compiled/{{ inventory_hostname }}/running.conf 101 | replace_config={{ replace_config }} 102 | get_diffs=True 103 | diff_file=../compiled/{{ inventory_hostname }}/diff 104 | ''' 105 | 106 | RETURN = ''' 107 | changed: 108 | description: whether the config on the device was changed 109 | returned: always 110 | type: bool 111 | sample: True 112 | msg: 113 | description: diff of the change 114 | returned: always 115 | type: string 116 | sample: "[edit system]\n- host-name lab-testing;\n+ host-name lab;" 117 | ''' 118 | 119 | try: 120 | from napalm_base import get_network_driver 121 | except ImportError: 122 | napalm_found = False 123 | else: 124 | napalm_found = True 125 | 126 | def save_to_file(content, filename): 127 | f = open(filename, 'w') 128 | try: 129 | f.write(content) 130 | finally: 131 | f.close() 132 | 133 | def main(): 134 | module = AnsibleModule( 135 | argument_spec=dict( 136 | hostname=dict(type='str', required=True), 137 | username=dict(type='str', required=True), 138 | password=dict(type='str', required=True, no_log=True), 139 | timeout=dict(type='int', required=False, default=60), 140 | optional_args=dict(required=False, type='dict', default=None), 141 | config_file=dict(type='str', required=True), 142 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 143 | replace_config=dict(type='bool', required=False, default=False), 144 | diff_file=dict(type='str', required=False, default=None), 145 | get_diffs=dict(type='bool', required=False, default=True) 146 | ), 147 | supports_check_mode=True 148 | ) 149 | 150 | if not napalm_found: 151 | module.fail_json(msg="the python module napalm is required") 152 | 153 | hostname = module.params['hostname'] 154 | username = module.params['username'] 155 | dev_os = module.params['dev_os'] 156 | password = module.params['password'] 157 | timeout = module.params['timeout'] 158 | config_file = module.params['config_file'] 159 | replace_config = module.params['replace_config'] 160 | diff_file = module.params['diff_file'] 161 | get_diffs = module.params['get_diffs'] 162 | 163 | if module.params['optional_args'] is None: 164 | optional_args = {} 165 | else: 166 | optional_args = module.params['optional_args'] 167 | 168 | try: 169 | network_driver = get_network_driver(dev_os) 170 | device = network_driver(hostname=hostname, 171 | username=username, 172 | password=password, 173 | timeout=timeout, 174 | optional_args=optional_args) 175 | device.open() 176 | except Exception, e: 177 | module.fail_json(msg="cannot connect to device: " + str(e)) 178 | 179 | try: 180 | if replace_config: 181 | device.load_replace_candidate(filename=config_file) 182 | else: 183 | device.load_merge_candidate(filename=config_file) 184 | except Exception, e: 185 | module.fail_json(msg="cannot load config: " + str(e)) 186 | 187 | try: 188 | if get_diffs: 189 | diff = device.compare_config().encode('utf-8') 190 | changed = len(diff) > 0 191 | else: 192 | changed = True 193 | diff = None 194 | if diff_file is not None and get_diffs: 195 | save_to_file(diff, diff_file) 196 | except Exception, e: 197 | module.fail_json(msg="cannot diff config: " + str(e)) 198 | 199 | try: 200 | if module.check_mode: 201 | device.discard_config() 202 | else: 203 | if changed: 204 | device.commit_config() 205 | except Exception, e: 206 | module.fail_json(msg="cannot install config: " + str(e)) 207 | 208 | try: 209 | device.close() 210 | except Exception, e: 211 | module.fail_json(msg="cannot close device connection: " + str(e)) 212 | 213 | module.exit_json(changed=changed, msg=diff) 214 | 215 | # standard ansible module imports 216 | from ansible.module_utils.basic import * 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/library/napalm_install_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | Original prototype by David Barroso 7 | 8 | This file is part of Ansible 9 | 10 | Ansible is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | Ansible is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with Ansible. If not, see . 22 | """ 23 | 24 | DOCUMENTATION = ''' 25 | --- 26 | module: napalm_install_config 27 | author: "Elisa Jasinska (@fooelisa)" 28 | version_added: "2.1" 29 | short_description: "Installs the configuration taken from a file on a device supported by NAPALM" 30 | description: 31 | - "This library will take the configuration from a file and load it into a device running any OS supported by napalm. 32 | The old configuration will be replaced or merged with the new one." 33 | requirements: 34 | - napalm 35 | options: 36 | hostname: 37 | description: 38 | - IP or FQDN of the device you want to connect to 39 | required: True 40 | username: 41 | description: 42 | - Username 43 | required: True 44 | password: 45 | description: 46 | - Password 47 | required: True 48 | dev_os: 49 | description: 50 | - OS of the device 51 | required: True 52 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 53 | timeout: 54 | description: 55 | - Time in seconds to wait for the device to respond 56 | required: False 57 | default: 60 58 | optional_args: 59 | description: 60 | - Dictionary of additional arguments passed to underlying driver 61 | required: False 62 | default: None 63 | config_file: 64 | description: 65 | - Path to the file to load the configuration from 66 | required: True 67 | replace_config: 68 | description: 69 | - If set to True, the entire configuration on the device will be replaced during the commit. 70 | If set to False, we will merge the new config with the existing one. Default- False 71 | choices: [true,false] 72 | default: False 73 | required: False 74 | diff_file: 75 | description: 76 | - A path to the file where we store the "diff" between the running configuration and the new 77 | configuration. If not set the diff between configurations will not be saved. 78 | default: None 79 | required: False 80 | get_diffs: 81 | description: 82 | - Set to False to not have any diffs generated. Useful if platform does not support commands 83 | being used to generate diffs. Note- By default diffs are generated even if the diff_file 84 | param is not set. 85 | choices: [true,false] 86 | default: True 87 | required: False 88 | ''' 89 | 90 | EXAMPLES = ''' 91 | - assemble: 92 | src=../compiled/{{ inventory_hostname }}/ 93 | dest=../compiled/{{ inventory_hostname }}/running.conf 94 | 95 | - napalm_install_config: 96 | hostname={{ inventory_hostname }} 97 | username={{ user }} 98 | dev_os={{ os }} 99 | password={{ passwd }} 100 | config_file=../compiled/{{ inventory_hostname }}/running.conf 101 | replace_config={{ replace_config }} 102 | get_diffs=True 103 | diff_file=../compiled/{{ inventory_hostname }}/diff 104 | ''' 105 | 106 | RETURN = ''' 107 | changed: 108 | description: whether the config on the device was changed 109 | returned: always 110 | type: bool 111 | sample: True 112 | msg: 113 | description: diff of the change 114 | returned: always 115 | type: string 116 | sample: "[edit system]\n- host-name lab-testing;\n+ host-name lab;" 117 | ''' 118 | 119 | try: 120 | from napalm_base import get_network_driver 121 | except ImportError: 122 | napalm_found = False 123 | else: 124 | napalm_found = True 125 | 126 | def save_to_file(content, filename): 127 | f = open(filename, 'w') 128 | try: 129 | f.write(content) 130 | finally: 131 | f.close() 132 | 133 | def main(): 134 | module = AnsibleModule( 135 | argument_spec=dict( 136 | hostname=dict(type='str', required=True), 137 | username=dict(type='str', required=True), 138 | password=dict(type='str', required=True, no_log=True), 139 | timeout=dict(type='int', required=False, default=60), 140 | optional_args=dict(required=False, type='dict', default=None), 141 | config_file=dict(type='str', required=True), 142 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 143 | replace_config=dict(type='bool', required=False, default=False), 144 | diff_file=dict(type='str', required=False, default=None), 145 | get_diffs=dict(type='bool', required=False, default=True) 146 | ), 147 | supports_check_mode=True 148 | ) 149 | 150 | if not napalm_found: 151 | module.fail_json(msg="the python module napalm is required") 152 | 153 | hostname = module.params['hostname'] 154 | username = module.params['username'] 155 | dev_os = module.params['dev_os'] 156 | password = module.params['password'] 157 | timeout = module.params['timeout'] 158 | config_file = module.params['config_file'] 159 | replace_config = module.params['replace_config'] 160 | diff_file = module.params['diff_file'] 161 | get_diffs = module.params['get_diffs'] 162 | 163 | if module.params['optional_args'] is None: 164 | optional_args = {} 165 | else: 166 | optional_args = module.params['optional_args'] 167 | 168 | try: 169 | network_driver = get_network_driver(dev_os) 170 | device = network_driver(hostname=hostname, 171 | username=username, 172 | password=password, 173 | timeout=timeout, 174 | optional_args=optional_args) 175 | device.open() 176 | except Exception, e: 177 | module.fail_json(msg="cannot connect to device: " + str(e)) 178 | 179 | try: 180 | if replace_config: 181 | device.load_replace_candidate(filename=config_file) 182 | else: 183 | device.load_merge_candidate(filename=config_file) 184 | except Exception, e: 185 | module.fail_json(msg="cannot load config: " + str(e)) 186 | 187 | try: 188 | if get_diffs: 189 | diff = device.compare_config().encode('utf-8') 190 | changed = len(diff) > 0 191 | else: 192 | changed = True 193 | diff = None 194 | if diff_file is not None and get_diffs: 195 | save_to_file(diff, diff_file) 196 | except Exception, e: 197 | module.fail_json(msg="cannot diff config: " + str(e)) 198 | 199 | try: 200 | if module.check_mode: 201 | device.discard_config() 202 | else: 203 | if changed: 204 | device.commit_config() 205 | except Exception, e: 206 | module.fail_json(msg="cannot install config: " + str(e)) 207 | 208 | try: 209 | device.close() 210 | except Exception, e: 211 | module.fail_json(msg="cannot close device connection: " + str(e)) 212 | 213 | module.exit_json(changed=changed, msg=diff) 214 | 215 | # standard ansible module imports 216 | from ansible.module_utils.basic import * 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/library/napalm_install_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | (c) 2016 Elisa Jasinska 6 | Original prototype by David Barroso 7 | 8 | This file is part of Ansible 9 | 10 | Ansible is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | Ansible is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with Ansible. If not, see . 22 | """ 23 | 24 | DOCUMENTATION = ''' 25 | --- 26 | module: napalm_install_config 27 | author: "Elisa Jasinska (@fooelisa)" 28 | version_added: "2.1" 29 | short_description: "Installs the configuration taken from a file on a device supported by NAPALM" 30 | description: 31 | - "This library will take the configuration from a file and load it into a device running any OS supported by napalm. 32 | The old configuration will be replaced or merged with the new one." 33 | requirements: 34 | - napalm 35 | options: 36 | hostname: 37 | description: 38 | - IP or FQDN of the device you want to connect to 39 | required: True 40 | username: 41 | description: 42 | - Username 43 | required: True 44 | password: 45 | description: 46 | - Password 47 | required: True 48 | dev_os: 49 | description: 50 | - OS of the device 51 | required: True 52 | choices: ['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos'] 53 | timeout: 54 | description: 55 | - Time in seconds to wait for the device to respond 56 | required: False 57 | default: 60 58 | optional_args: 59 | description: 60 | - Dictionary of additional arguments passed to underlying driver 61 | required: False 62 | default: None 63 | config_file: 64 | description: 65 | - Path to the file to load the configuration from 66 | required: True 67 | replace_config: 68 | description: 69 | - If set to True, the entire configuration on the device will be replaced during the commit. 70 | If set to False, we will merge the new config with the existing one. Default- False 71 | choices: [true,false] 72 | default: False 73 | required: False 74 | diff_file: 75 | description: 76 | - A path to the file where we store the "diff" between the running configuration and the new 77 | configuration. If not set the diff between configurations will not be saved. 78 | default: None 79 | required: False 80 | get_diffs: 81 | description: 82 | - Set to False to not have any diffs generated. Useful if platform does not support commands 83 | being used to generate diffs. Note- By default diffs are generated even if the diff_file 84 | param is not set. 85 | choices: [true,false] 86 | default: True 87 | required: False 88 | ''' 89 | 90 | EXAMPLES = ''' 91 | - assemble: 92 | src=../compiled/{{ inventory_hostname }}/ 93 | dest=../compiled/{{ inventory_hostname }}/running.conf 94 | 95 | - napalm_install_config: 96 | hostname={{ inventory_hostname }} 97 | username={{ user }} 98 | dev_os={{ os }} 99 | password={{ passwd }} 100 | config_file=../compiled/{{ inventory_hostname }}/running.conf 101 | replace_config={{ replace_config }} 102 | get_diffs=True 103 | diff_file=../compiled/{{ inventory_hostname }}/diff 104 | ''' 105 | 106 | RETURN = ''' 107 | changed: 108 | description: whether the config on the device was changed 109 | returned: always 110 | type: bool 111 | sample: True 112 | msg: 113 | description: diff of the change 114 | returned: always 115 | type: string 116 | sample: "[edit system]\n- host-name lab-testing;\n+ host-name lab;" 117 | ''' 118 | 119 | try: 120 | from napalm_base import get_network_driver 121 | except ImportError: 122 | napalm_found = False 123 | else: 124 | napalm_found = True 125 | 126 | def save_to_file(content, filename): 127 | f = open(filename, 'w') 128 | try: 129 | f.write(content) 130 | finally: 131 | f.close() 132 | 133 | def main(): 134 | module = AnsibleModule( 135 | argument_spec=dict( 136 | hostname=dict(type='str', required=True), 137 | username=dict(type='str', required=True), 138 | password=dict(type='str', required=True, no_log=True), 139 | timeout=dict(type='int', required=False, default=60), 140 | optional_args=dict(required=False, type='dict', default=None), 141 | config_file=dict(type='str', required=True), 142 | dev_os=dict(type='str', required=True, choices=['eos', 'junos', 'iosxr', 'fortios', 'ibm', 'ios', 'nxos', 'panos']), 143 | replace_config=dict(type='bool', required=False, default=False), 144 | diff_file=dict(type='str', required=False, default=None), 145 | get_diffs=dict(type='bool', required=False, default=True) 146 | ), 147 | supports_check_mode=True 148 | ) 149 | 150 | if not napalm_found: 151 | module.fail_json(msg="the python module napalm is required") 152 | 153 | hostname = module.params['hostname'] 154 | username = module.params['username'] 155 | dev_os = module.params['dev_os'] 156 | password = module.params['password'] 157 | timeout = module.params['timeout'] 158 | config_file = module.params['config_file'] 159 | replace_config = module.params['replace_config'] 160 | diff_file = module.params['diff_file'] 161 | get_diffs = module.params['get_diffs'] 162 | 163 | if module.params['optional_args'] is None: 164 | optional_args = {} 165 | else: 166 | optional_args = module.params['optional_args'] 167 | 168 | try: 169 | network_driver = get_network_driver(dev_os) 170 | device = network_driver(hostname=hostname, 171 | username=username, 172 | password=password, 173 | timeout=timeout, 174 | optional_args=optional_args) 175 | device.open() 176 | except Exception, e: 177 | module.fail_json(msg="cannot connect to device: " + str(e)) 178 | 179 | try: 180 | if replace_config: 181 | device.load_replace_candidate(filename=config_file) 182 | else: 183 | device.load_merge_candidate(filename=config_file) 184 | except Exception, e: 185 | module.fail_json(msg="cannot load config: " + str(e)) 186 | 187 | try: 188 | if get_diffs: 189 | diff = device.compare_config().encode('utf-8') 190 | changed = len(diff) > 0 191 | else: 192 | changed = True 193 | diff = None 194 | if diff_file is not None and get_diffs: 195 | save_to_file(diff, diff_file) 196 | except Exception, e: 197 | module.fail_json(msg="cannot diff config: " + str(e)) 198 | 199 | try: 200 | if module.check_mode: 201 | device.discard_config() 202 | else: 203 | if changed: 204 | device.commit_config() 205 | except Exception, e: 206 | module.fail_json(msg="cannot install config: " + str(e)) 207 | 208 | try: 209 | device.close() 210 | except Exception, e: 211 | module.fail_json(msg="cannot close device connection: " + str(e)) 212 | 213 | module.exit_json(changed=changed, msg=diff) 214 | 215 | # standard ansible module imports 216 | from ansible.module_utils.basic import * 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /tutorial-6-abstractions/callback_plugins/selective.py: -------------------------------------------------------------------------------- 1 | # (c) Fastly, inc 2016 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | 18 | """ 19 | selective.py callback plugin. 20 | 21 | This callback only prints tasks that have been tagged with `print_action` or that have failed. 22 | Tasks that are not printed are placed with a '.'. 23 | 24 | For example: 25 | 26 | - debug: msg="This will not be printed" 27 | - debug: msg="But this will" 28 | tags: [print_action]" 29 | 30 | This allows operators to focus on the tasks that provide value only. 31 | 32 | If you increase verbosity all tasks are printed. 33 | """ 34 | 35 | from __future__ import (absolute_import, division, print_function) 36 | 37 | import difflib 38 | import os 39 | 40 | from ansible.plugins.callback import CallbackBase 41 | 42 | __metaclass__ = type 43 | 44 | 45 | COLORS = { 46 | 'normal': '\033[0m', 47 | 'ok': '\033[92m', 48 | 'bold': '\033[1m', 49 | 'not_so_bold': '\033[1m\033[34m', 50 | 'changed': '\033[93m', 51 | 'failed': '\033[91m', 52 | 'endc': '\033[0m', 53 | 'skipped': '\033[96m', 54 | } 55 | 56 | DONT_COLORIZE = os.getenv('ANSIBLY_DONT_COLORIZE', default=False) 57 | 58 | 59 | def dict_diff(prv, nxt): 60 | """Return a dict of keys that differ with another config object.""" 61 | keys = set(prv.keys() + nxt.keys()) 62 | result = {} 63 | for k in keys: 64 | if prv.get(k) != nxt.get(k): 65 | result[k] = (prv.get(k), nxt.get(k)) 66 | return result 67 | 68 | 69 | def colorize(msg, color): 70 | """Given a string add necessary codes to format the string.""" 71 | if DONT_COLORIZE: 72 | return msg 73 | else: 74 | return '{}{}{}'.format(COLORS[color], msg, COLORS['endc']) 75 | 76 | 77 | class CallbackModule(CallbackBase): 78 | """selective.py callback plugin.""" 79 | 80 | CALLBACK_VERSION = 2.0 81 | CALLBACK_TYPE = 'stdout' 82 | CALLBACK_NAME = 'selective' 83 | 84 | def __init__(self, display=None): 85 | """selective.py callback plugin.""" 86 | super(CallbackModule, self).__init__(display) 87 | self.last_skipped = False 88 | self.last_task_name = None 89 | self.printed_last_task = False 90 | 91 | def _print_task(self, task_name=None): 92 | if task_name is None: 93 | task_name = self.last_task_name 94 | 95 | if not self.printed_last_task: 96 | self.printed_last_task = True 97 | line_length = 120 98 | if self.last_skipped: 99 | print() 100 | msg = colorize("# {} {}".format(task_name, 101 | '*' * (line_length - len(task_name))), 'bold') 102 | print(msg) 103 | 104 | def _indent_text(self, text, indent_level): 105 | lines = text.splitlines() 106 | result_lines = [] 107 | for l in lines: 108 | result_lines.append("{}{}".format(' '*indent_level, l)) 109 | return '\n'.join(result_lines) 110 | 111 | def _print_diff(self, diff, indent_level): 112 | if isinstance(diff, dict): 113 | try: 114 | diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(), 115 | diff['after'].splitlines(), 116 | fromfile=diff.get('before_header', 117 | 'new_file'), 118 | tofile=diff['after_header'])) 119 | except AttributeError: 120 | diff = dict_diff(diff['before'], diff['after']) 121 | if diff: 122 | diff = colorize(diff, 'changed') 123 | print(self._indent_text(diff, indent_level+4)) 124 | 125 | def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr): 126 | if is_host: 127 | indent_level = 0 128 | name = colorize(host_or_item.name, 'not_so_bold') 129 | else: 130 | indent_level = 4 131 | name = colorize(unicode(host_or_item['key']), 'bold') 132 | 133 | if error: 134 | color = 'failed' 135 | change_string = colorize('FAILED!!!', color) 136 | else: 137 | color = 'changed' if changed else 'ok' 138 | change_string = colorize("changed={}".format(changed), color) 139 | 140 | msg = colorize(msg, color) 141 | 142 | line_length = 120 143 | spaces = ' ' * (40-len(name)-indent_level) 144 | line = "{} * {}{}- {}".format(' ' * indent_level, name, spaces, change_string) 145 | 146 | if len(msg) < 50: 147 | line += ' -- {}'.format(msg) 148 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 149 | else: 150 | print("{} {}".format(line, '-' * (line_length - len(line)))) 151 | print(self._indent_text(msg, indent_level+4)) 152 | 153 | if diff is not None: 154 | self._print_diff(diff, indent_level) 155 | if stdout is not None: 156 | stdout = colorize(stdout, 'failed') 157 | print(self._indent_text(stdout, indent_level+4)) 158 | if stderr is not None: 159 | stderr = colorize(stderr, 'failed') 160 | print(self._indent_text(stderr, indent_level+4)) 161 | 162 | def v2_playbook_on_play_start(self, play): 163 | """Run on start of the play.""" 164 | pass 165 | 166 | def v2_playbook_on_task_start(self, task, **kwargs): 167 | """Run when a task starts.""" 168 | self.last_task_name = task.get_name() 169 | self.printed_last_task = False 170 | 171 | def v2_runner_on_ok(self, result, **kwargs): 172 | """Run when a task finishes correctly.""" 173 | failed = 'failed' in result._result 174 | unreachable = 'unreachable' in result._result 175 | 176 | if 'print_action' in result._task.tags or failed or unreachable or \ 177 | self._display.verbosity > 1: 178 | self._print_task() 179 | self.last_skipped = False 180 | msg = unicode(result._result.get('msg', '')) or\ 181 | unicode(result._result.get('reason', '')) 182 | self._print_host_or_item(result._host, 183 | result._result.get('changed', False), 184 | msg, 185 | result._result.get('diff', None), 186 | is_host=True, 187 | error=failed or unreachable, 188 | stdout=result._result.get('module_stdout', None), 189 | stderr=result._result.get('module_stderr', None), 190 | ) 191 | if 'results' in result._result: 192 | for r in result._result['results']: 193 | failed = 'failed' in r 194 | self._print_host_or_item(r['item'], 195 | r.get('changed', False), 196 | unicode(r.get('msg', '')), 197 | r.get('diff', None), 198 | is_host=False, 199 | error=failed, 200 | stdout=r.get('module_stdout', None), 201 | stderr=r.get('module_stderr', None), 202 | ) 203 | else: 204 | self.last_skipped = True 205 | print('.', end="") 206 | 207 | def v2_playbook_on_stats(self, stats): 208 | """Display info about playbook statistics.""" 209 | print() 210 | self.printed_last_task = False 211 | self._print_task('STATS') 212 | 213 | hosts = sorted(stats.processed.keys()) 214 | for host in hosts: 215 | s = stats.summarize(host) 216 | 217 | if s['failures'] or s['unreachable']: 218 | color = 'failed' 219 | elif s['changed']: 220 | color = 'changed' 221 | else: 222 | color = 'ok' 223 | 224 | msg = '{} : ok={}\tchanged={}\tfailed={}\tunreachable={}'.format( 225 | host, s['ok'], s['changed'], s['failures'], s['unreachable']) 226 | print(colorize(msg, color)) 227 | 228 | def v2_runner_on_skipped(self, result, **kwargs): 229 | """Run when a task is skipped.""" 230 | if self._display.verbosity > 1: 231 | self._print_task() 232 | self.last_skipped = False 233 | 234 | line_length = 120 235 | spaces = ' ' * (31-len(result._host.name)-4) 236 | 237 | line = " * {}{}- {}".format(colorize(result._host.name, 'not_so_bold'), 238 | spaces, 239 | colorize("skipped", 'skipped'),) 240 | 241 | reason = result._result.get('skipped_reason', '') or \ 242 | result._result.get('skip_reason', '') 243 | if len(reason) < 50: 244 | line += ' -- {}'.format(reason) 245 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 246 | else: 247 | print("{} {}".format(line, '-' * (line_length - len(line)))) 248 | print(self._indent_text(reason, 8)) 249 | print(reason) 250 | 251 | v2_playbook_on_handler_task_start = v2_playbook_on_task_start 252 | v2_runner_on_failed = v2_runner_on_ok 253 | v2_runner_on_unreachable = v2_runner_on_ok 254 | -------------------------------------------------------------------------------- /tutorial-3-abstract-vendor-configuration/callback_plugins/selective.py: -------------------------------------------------------------------------------- 1 | # (c) Fastly, inc 2016 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | 18 | """ 19 | selective.py callback plugin. 20 | 21 | This callback only prints tasks that have been tagged with `print_action` or that have failed. 22 | Tasks that are not printed are placed with a '.'. 23 | 24 | For example: 25 | 26 | - debug: msg="This will not be printed" 27 | - debug: msg="But this will" 28 | tags: [print_action]" 29 | 30 | This allows operators to focus on the tasks that provide value only. 31 | 32 | If you increase verbosity all tasks are printed. 33 | """ 34 | 35 | from __future__ import (absolute_import, division, print_function) 36 | 37 | import difflib 38 | import os 39 | 40 | from ansible.plugins.callback import CallbackBase 41 | 42 | __metaclass__ = type 43 | 44 | 45 | COLORS = { 46 | 'normal': '\033[0m', 47 | 'ok': '\033[92m', 48 | 'bold': '\033[1m', 49 | 'not_so_bold': '\033[1m\033[34m', 50 | 'changed': '\033[93m', 51 | 'failed': '\033[91m', 52 | 'endc': '\033[0m', 53 | 'skipped': '\033[96m', 54 | } 55 | 56 | DONT_COLORIZE = os.getenv('ANSIBLY_DONT_COLORIZE', default=False) 57 | 58 | 59 | def dict_diff(prv, nxt): 60 | """Return a dict of keys that differ with another config object.""" 61 | keys = set(prv.keys() + nxt.keys()) 62 | result = {} 63 | for k in keys: 64 | if prv.get(k) != nxt.get(k): 65 | result[k] = (prv.get(k), nxt.get(k)) 66 | return result 67 | 68 | 69 | def colorize(msg, color): 70 | """Given a string add necessary codes to format the string.""" 71 | if DONT_COLORIZE: 72 | return msg 73 | else: 74 | return '{}{}{}'.format(COLORS[color], msg, COLORS['endc']) 75 | 76 | 77 | class CallbackModule(CallbackBase): 78 | """selective.py callback plugin.""" 79 | 80 | CALLBACK_VERSION = 2.0 81 | CALLBACK_TYPE = 'stdout' 82 | CALLBACK_NAME = 'selective' 83 | 84 | def __init__(self, display=None): 85 | """selective.py callback plugin.""" 86 | super(CallbackModule, self).__init__(display) 87 | self.last_skipped = False 88 | self.last_task_name = None 89 | self.printed_last_task = False 90 | 91 | def _print_task(self, task_name=None): 92 | if task_name is None: 93 | task_name = self.last_task_name 94 | 95 | if not self.printed_last_task: 96 | self.printed_last_task = True 97 | line_length = 120 98 | if self.last_skipped: 99 | print() 100 | msg = colorize("# {} {}".format(task_name, 101 | '*' * (line_length - len(task_name))), 'bold') 102 | print(msg) 103 | 104 | def _indent_text(self, text, indent_level): 105 | lines = text.splitlines() 106 | result_lines = [] 107 | for l in lines: 108 | result_lines.append("{}{}".format(' '*indent_level, l)) 109 | return '\n'.join(result_lines) 110 | 111 | def _print_diff(self, diff, indent_level): 112 | if isinstance(diff, dict): 113 | try: 114 | diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(), 115 | diff['after'].splitlines(), 116 | fromfile=diff.get('before_header', 117 | 'new_file'), 118 | tofile=diff['after_header'])) 119 | except AttributeError: 120 | diff = dict_diff(diff['before'], diff['after']) 121 | if diff: 122 | diff = colorize(diff, 'changed') 123 | print(self._indent_text(diff, indent_level+4)) 124 | 125 | def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr): 126 | if is_host: 127 | indent_level = 0 128 | name = colorize(host_or_item.name, 'not_so_bold') 129 | else: 130 | indent_level = 4 131 | name = colorize(unicode(host_or_item), 'bold') 132 | 133 | if error: 134 | color = 'failed' 135 | change_string = colorize('FAILED!!!', color) 136 | else: 137 | color = 'changed' if changed else 'ok' 138 | change_string = colorize("changed={}".format(changed), color) 139 | 140 | msg = colorize(msg, color) 141 | 142 | line_length = 120 143 | spaces = ' ' * (40-len(name)-indent_level) 144 | line = "{} * {}{}- {}".format(' ' * indent_level, name, spaces, change_string) 145 | 146 | if len(msg) < 50: 147 | line += ' -- {}'.format(msg) 148 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 149 | else: 150 | print("{} {}".format(line, '-' * (line_length - len(line)))) 151 | print(self._indent_text(msg, indent_level+4)) 152 | 153 | if diff is not None: 154 | self._print_diff(diff, indent_level) 155 | if stdout is not None: 156 | stdout = colorize(stdout, 'failed') 157 | print(self._indent_text(stdout, indent_level+4)) 158 | if stderr is not None: 159 | stderr = colorize(stderr, 'failed') 160 | print(self._indent_text(stderr, indent_level+4)) 161 | 162 | def v2_playbook_on_play_start(self, play): 163 | """Run on start of the play.""" 164 | pass 165 | 166 | def v2_playbook_on_task_start(self, task, **kwargs): 167 | """Run when a task starts.""" 168 | self.last_task_name = task.get_name() 169 | self.printed_last_task = False 170 | 171 | def v2_runner_on_ok(self, result, **kwargs): 172 | """Run when a task finishes correctly.""" 173 | failed = 'failed' in result._result 174 | unreachable = 'unreachable' in result._result 175 | 176 | if 'print_action' in result._task.tags or failed or unreachable or \ 177 | self._display.verbosity > 1: 178 | self._print_task() 179 | self.last_skipped = False 180 | msg = unicode(result._result.get('msg', '')) or\ 181 | unicode(result._result.get('reason', '')) 182 | self._print_host_or_item(result._host, 183 | result._result.get('changed', False), 184 | msg, 185 | result._result.get('diff', None), 186 | is_host=True, 187 | error=failed or unreachable, 188 | stdout=result._result.get('module_stdout', None), 189 | stderr=result._result.get('module_stderr', None), 190 | ) 191 | if 'results' in result._result: 192 | for r in result._result['results']: 193 | failed = 'failed' in r 194 | self._print_host_or_item(r['item'], 195 | r.get('changed', False), 196 | unicode(r.get('msg', '')), 197 | r.get('diff', None), 198 | is_host=False, 199 | error=failed, 200 | stdout=r.get('module_stdout', None), 201 | stderr=r.get('module_stderr', None), 202 | ) 203 | else: 204 | self.last_skipped = True 205 | print('.', end="") 206 | 207 | def v2_playbook_on_stats(self, stats): 208 | """Display info about playbook statistics.""" 209 | print() 210 | self.printed_last_task = False 211 | self._print_task('STATS') 212 | 213 | hosts = sorted(stats.processed.keys()) 214 | for host in hosts: 215 | s = stats.summarize(host) 216 | 217 | if s['failures'] or s['unreachable']: 218 | color = 'failed' 219 | elif s['changed']: 220 | color = 'changed' 221 | else: 222 | color = 'ok' 223 | 224 | msg = '{} : ok={}\tchanged={}\tfailed={}\tunreachable={}'.format( 225 | host, s['ok'], s['changed'], s['failures'], s['unreachable']) 226 | print(colorize(msg, color)) 227 | 228 | def v2_runner_on_skipped(self, result, **kwargs): 229 | """Run when a task is skipped.""" 230 | if self._display.verbosity > 1: 231 | self._print_task() 232 | self.last_skipped = False 233 | 234 | line_length = 120 235 | spaces = ' ' * (31-len(result._host.name)-4) 236 | 237 | line = " * {}{}- {}".format(colorize(result._host.name, 'not_so_bold'), 238 | spaces, 239 | colorize("skipped", 'skipped'),) 240 | 241 | reason = result._result.get('skipped_reason', '') or \ 242 | result._result.get('skip_reason', '') 243 | if len(reason) < 50: 244 | line += ' -- {}'.format(reason) 245 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 246 | else: 247 | print("{} {}".format(line, '-' * (line_length - len(line)))) 248 | print(self._indent_text(reason, 8)) 249 | print(reason) 250 | 251 | v2_playbook_on_handler_task_start = v2_playbook_on_task_start 252 | v2_runner_on_failed = v2_runner_on_ok 253 | v2_runner_on_unreachable = v2_runner_on_ok 254 | -------------------------------------------------------------------------------- /tutorial-4-data-driven-configuration/callback_plugins/selective.py: -------------------------------------------------------------------------------- 1 | # (c) Fastly, inc 2016 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | 18 | """ 19 | selective.py callback plugin. 20 | 21 | This callback only prints tasks that have been tagged with `print_action` or that have failed. 22 | Tasks that are not printed are placed with a '.'. 23 | 24 | For example: 25 | 26 | - debug: msg="This will not be printed" 27 | - debug: msg="But this will" 28 | tags: [print_action]" 29 | 30 | This allows operators to focus on the tasks that provide value only. 31 | 32 | If you increase verbosity all tasks are printed. 33 | """ 34 | 35 | from __future__ import (absolute_import, division, print_function) 36 | 37 | import difflib 38 | import os 39 | 40 | from ansible.plugins.callback import CallbackBase 41 | 42 | __metaclass__ = type 43 | 44 | 45 | COLORS = { 46 | 'normal': '\033[0m', 47 | 'ok': '\033[92m', 48 | 'bold': '\033[1m', 49 | 'not_so_bold': '\033[1m\033[34m', 50 | 'changed': '\033[93m', 51 | 'failed': '\033[91m', 52 | 'endc': '\033[0m', 53 | 'skipped': '\033[96m', 54 | } 55 | 56 | DONT_COLORIZE = os.getenv('ANSIBLY_DONT_COLORIZE', default=False) 57 | 58 | 59 | def dict_diff(prv, nxt): 60 | """Return a dict of keys that differ with another config object.""" 61 | keys = set(prv.keys() + nxt.keys()) 62 | result = {} 63 | for k in keys: 64 | if prv.get(k) != nxt.get(k): 65 | result[k] = (prv.get(k), nxt.get(k)) 66 | return result 67 | 68 | 69 | def colorize(msg, color): 70 | """Given a string add necessary codes to format the string.""" 71 | if DONT_COLORIZE: 72 | return msg 73 | else: 74 | return '{}{}{}'.format(COLORS[color], msg, COLORS['endc']) 75 | 76 | 77 | class CallbackModule(CallbackBase): 78 | """selective.py callback plugin.""" 79 | 80 | CALLBACK_VERSION = 2.0 81 | CALLBACK_TYPE = 'stdout' 82 | CALLBACK_NAME = 'selective' 83 | 84 | def __init__(self, display=None): 85 | """selective.py callback plugin.""" 86 | super(CallbackModule, self).__init__(display) 87 | self.last_skipped = False 88 | self.last_task_name = None 89 | self.printed_last_task = False 90 | 91 | def _print_task(self, task_name=None): 92 | if task_name is None: 93 | task_name = self.last_task_name 94 | 95 | if not self.printed_last_task: 96 | self.printed_last_task = True 97 | line_length = 120 98 | if self.last_skipped: 99 | print() 100 | msg = colorize("# {} {}".format(task_name, 101 | '*' * (line_length - len(task_name))), 'bold') 102 | print(msg) 103 | 104 | def _indent_text(self, text, indent_level): 105 | lines = text.splitlines() 106 | result_lines = [] 107 | for l in lines: 108 | result_lines.append("{}{}".format(' '*indent_level, l)) 109 | return '\n'.join(result_lines) 110 | 111 | def _print_diff(self, diff, indent_level): 112 | if isinstance(diff, dict): 113 | try: 114 | diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(), 115 | diff['after'].splitlines(), 116 | fromfile=diff.get('before_header', 117 | 'new_file'), 118 | tofile=diff['after_header'])) 119 | except AttributeError: 120 | diff = dict_diff(diff['before'], diff['after']) 121 | if diff: 122 | diff = colorize(diff, 'changed') 123 | print(self._indent_text(diff, indent_level+4)) 124 | 125 | def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr): 126 | if is_host: 127 | indent_level = 0 128 | name = colorize(host_or_item.name, 'not_so_bold') 129 | else: 130 | indent_level = 4 131 | name = colorize(unicode(host_or_item['key']), 'bold') 132 | 133 | if error: 134 | color = 'failed' 135 | change_string = colorize('FAILED!!!', color) 136 | else: 137 | color = 'changed' if changed else 'ok' 138 | change_string = colorize("changed={}".format(changed), color) 139 | 140 | msg = colorize(msg, color) 141 | 142 | line_length = 120 143 | spaces = ' ' * (40-len(name)-indent_level) 144 | line = "{} * {}{}- {}".format(' ' * indent_level, name, spaces, change_string) 145 | 146 | if len(msg) < 50: 147 | line += ' -- {}'.format(msg) 148 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 149 | else: 150 | print("{} {}".format(line, '-' * (line_length - len(line)))) 151 | print(self._indent_text(msg, indent_level+4)) 152 | 153 | if diff is not None: 154 | self._print_diff(diff, indent_level) 155 | if stdout is not None: 156 | stdout = colorize(stdout, 'failed') 157 | print(self._indent_text(stdout, indent_level+4)) 158 | if stderr is not None: 159 | stderr = colorize(stderr, 'failed') 160 | print(self._indent_text(stderr, indent_level+4)) 161 | 162 | def v2_playbook_on_play_start(self, play): 163 | """Run on start of the play.""" 164 | pass 165 | 166 | def v2_playbook_on_task_start(self, task, **kwargs): 167 | """Run when a task starts.""" 168 | self.last_task_name = task.get_name() 169 | self.printed_last_task = False 170 | 171 | def v2_runner_on_ok(self, result, **kwargs): 172 | """Run when a task finishes correctly.""" 173 | failed = 'failed' in result._result 174 | unreachable = 'unreachable' in result._result 175 | 176 | if 'print_action' in result._task.tags or failed or unreachable or \ 177 | self._display.verbosity > 1: 178 | self._print_task() 179 | self.last_skipped = False 180 | msg = unicode(result._result.get('msg', '')) or\ 181 | unicode(result._result.get('reason', '')) 182 | self._print_host_or_item(result._host, 183 | result._result.get('changed', False), 184 | msg, 185 | result._result.get('diff', None), 186 | is_host=True, 187 | error=failed or unreachable, 188 | stdout=result._result.get('module_stdout', None), 189 | stderr=result._result.get('module_stderr', None), 190 | ) 191 | if 'results' in result._result: 192 | for r in result._result['results']: 193 | failed = 'failed' in r 194 | self._print_host_or_item(r['item'], 195 | r.get('changed', False), 196 | unicode(r.get('msg', '')), 197 | r.get('diff', None), 198 | is_host=False, 199 | error=failed, 200 | stdout=r.get('module_stdout', None), 201 | stderr=r.get('module_stderr', None), 202 | ) 203 | else: 204 | self.last_skipped = True 205 | print('.', end="") 206 | 207 | def v2_playbook_on_stats(self, stats): 208 | """Display info about playbook statistics.""" 209 | print() 210 | self.printed_last_task = False 211 | self._print_task('STATS') 212 | 213 | hosts = sorted(stats.processed.keys()) 214 | for host in hosts: 215 | s = stats.summarize(host) 216 | 217 | if s['failures'] or s['unreachable']: 218 | color = 'failed' 219 | elif s['changed']: 220 | color = 'changed' 221 | else: 222 | color = 'ok' 223 | 224 | msg = '{} : ok={}\tchanged={}\tfailed={}\tunreachable={}'.format( 225 | host, s['ok'], s['changed'], s['failures'], s['unreachable']) 226 | print(colorize(msg, color)) 227 | 228 | def v2_runner_on_skipped(self, result, **kwargs): 229 | """Run when a task is skipped.""" 230 | if self._display.verbosity > 1: 231 | self._print_task() 232 | self.last_skipped = False 233 | 234 | line_length = 120 235 | spaces = ' ' * (31-len(result._host.name)-4) 236 | 237 | line = " * {}{}- {}".format(colorize(result._host.name, 'not_so_bold'), 238 | spaces, 239 | colorize("skipped", 'skipped'),) 240 | 241 | reason = result._result.get('skipped_reason', '') or \ 242 | result._result.get('skip_reason', '') 243 | if len(reason) < 50: 244 | line += ' -- {}'.format(reason) 245 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 246 | else: 247 | print("{} {}".format(line, '-' * (line_length - len(line)))) 248 | print(self._indent_text(reason, 8)) 249 | print(reason) 250 | 251 | v2_playbook_on_handler_task_start = v2_playbook_on_task_start 252 | v2_runner_on_failed = v2_runner_on_ok 253 | v2_runner_on_unreachable = v2_runner_on_ok 254 | -------------------------------------------------------------------------------- /tutorial-5-data-driven-configuration_with_backend/callback_plugins/selective.py: -------------------------------------------------------------------------------- 1 | # (c) Fastly, inc 2016 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | 18 | """ 19 | selective.py callback plugin. 20 | 21 | This callback only prints tasks that have been tagged with `print_action` or that have failed. 22 | Tasks that are not printed are placed with a '.'. 23 | 24 | For example: 25 | 26 | - debug: msg="This will not be printed" 27 | - debug: msg="But this will" 28 | tags: [print_action]" 29 | 30 | This allows operators to focus on the tasks that provide value only. 31 | 32 | If you increase verbosity all tasks are printed. 33 | """ 34 | 35 | from __future__ import (absolute_import, division, print_function) 36 | 37 | import difflib 38 | import os 39 | 40 | from ansible.plugins.callback import CallbackBase 41 | 42 | __metaclass__ = type 43 | 44 | 45 | COLORS = { 46 | 'normal': '\033[0m', 47 | 'ok': '\033[92m', 48 | 'bold': '\033[1m', 49 | 'not_so_bold': '\033[1m\033[34m', 50 | 'changed': '\033[93m', 51 | 'failed': '\033[91m', 52 | 'endc': '\033[0m', 53 | 'skipped': '\033[96m', 54 | } 55 | 56 | DONT_COLORIZE = os.getenv('ANSIBLY_DONT_COLORIZE', default=False) 57 | 58 | 59 | def dict_diff(prv, nxt): 60 | """Return a dict of keys that differ with another config object.""" 61 | keys = set(prv.keys() + nxt.keys()) 62 | result = {} 63 | for k in keys: 64 | if prv.get(k) != nxt.get(k): 65 | result[k] = (prv.get(k), nxt.get(k)) 66 | return result 67 | 68 | 69 | def colorize(msg, color): 70 | """Given a string add necessary codes to format the string.""" 71 | if DONT_COLORIZE: 72 | return msg 73 | else: 74 | return '{}{}{}'.format(COLORS[color], msg, COLORS['endc']) 75 | 76 | 77 | class CallbackModule(CallbackBase): 78 | """selective.py callback plugin.""" 79 | 80 | CALLBACK_VERSION = 2.0 81 | CALLBACK_TYPE = 'stdout' 82 | CALLBACK_NAME = 'selective' 83 | 84 | def __init__(self, display=None): 85 | """selective.py callback plugin.""" 86 | super(CallbackModule, self).__init__(display) 87 | self.last_skipped = False 88 | self.last_task_name = None 89 | self.printed_last_task = False 90 | 91 | def _print_task(self, task_name=None): 92 | if task_name is None: 93 | task_name = self.last_task_name 94 | 95 | if not self.printed_last_task: 96 | self.printed_last_task = True 97 | line_length = 120 98 | if self.last_skipped: 99 | print() 100 | msg = colorize("# {} {}".format(task_name, 101 | '*' * (line_length - len(task_name))), 'bold') 102 | print(msg) 103 | 104 | def _indent_text(self, text, indent_level): 105 | lines = text.splitlines() 106 | result_lines = [] 107 | for l in lines: 108 | result_lines.append("{}{}".format(' '*indent_level, l)) 109 | return '\n'.join(result_lines) 110 | 111 | def _print_diff(self, diff, indent_level): 112 | if isinstance(diff, dict): 113 | try: 114 | diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(), 115 | diff['after'].splitlines(), 116 | fromfile=diff.get('before_header', 117 | 'new_file'), 118 | tofile=diff['after_header'])) 119 | except AttributeError: 120 | diff = dict_diff(diff['before'], diff['after']) 121 | if diff: 122 | diff = colorize(diff, 'changed') 123 | print(self._indent_text(diff, indent_level+4)) 124 | 125 | def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr): 126 | if is_host: 127 | indent_level = 0 128 | name = colorize(host_or_item.name, 'not_so_bold') 129 | else: 130 | indent_level = 4 131 | name = colorize(unicode(host_or_item['key']), 'bold') 132 | 133 | if error: 134 | color = 'failed' 135 | change_string = colorize('FAILED!!!', color) 136 | else: 137 | color = 'changed' if changed else 'ok' 138 | change_string = colorize("changed={}".format(changed), color) 139 | 140 | msg = colorize(msg, color) 141 | 142 | line_length = 120 143 | spaces = ' ' * (40-len(name)-indent_level) 144 | line = "{} * {}{}- {}".format(' ' * indent_level, name, spaces, change_string) 145 | 146 | if len(msg) < 50: 147 | line += ' -- {}'.format(msg) 148 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 149 | else: 150 | print("{} {}".format(line, '-' * (line_length - len(line)))) 151 | print(self._indent_text(msg, indent_level+4)) 152 | 153 | if diff is not None: 154 | self._print_diff(diff, indent_level) 155 | if stdout is not None: 156 | stdout = colorize(stdout, 'failed') 157 | print(self._indent_text(stdout, indent_level+4)) 158 | if stderr is not None: 159 | stderr = colorize(stderr, 'failed') 160 | print(self._indent_text(stderr, indent_level+4)) 161 | 162 | def v2_playbook_on_play_start(self, play): 163 | """Run on start of the play.""" 164 | pass 165 | 166 | def v2_playbook_on_task_start(self, task, **kwargs): 167 | """Run when a task starts.""" 168 | self.last_task_name = task.get_name() 169 | self.printed_last_task = False 170 | 171 | def v2_runner_on_ok(self, result, **kwargs): 172 | """Run when a task finishes correctly.""" 173 | failed = 'failed' in result._result 174 | unreachable = 'unreachable' in result._result 175 | 176 | if 'print_action' in result._task.tags or failed or unreachable or \ 177 | self._display.verbosity > 1: 178 | self._print_task() 179 | self.last_skipped = False 180 | msg = unicode(result._result.get('msg', '')) or\ 181 | unicode(result._result.get('reason', '')) 182 | self._print_host_or_item(result._host, 183 | result._result.get('changed', False), 184 | msg, 185 | result._result.get('diff', None), 186 | is_host=True, 187 | error=failed or unreachable, 188 | stdout=result._result.get('module_stdout', None), 189 | stderr=result._result.get('module_stderr', None), 190 | ) 191 | if 'results' in result._result: 192 | for r in result._result['results']: 193 | failed = 'failed' in r 194 | self._print_host_or_item(r['item'], 195 | r.get('changed', False), 196 | unicode(r.get('msg', '')), 197 | r.get('diff', None), 198 | is_host=False, 199 | error=failed, 200 | stdout=r.get('module_stdout', None), 201 | stderr=r.get('module_stderr', None), 202 | ) 203 | else: 204 | self.last_skipped = True 205 | print('.', end="") 206 | 207 | def v2_playbook_on_stats(self, stats): 208 | """Display info about playbook statistics.""" 209 | print() 210 | self.printed_last_task = False 211 | self._print_task('STATS') 212 | 213 | hosts = sorted(stats.processed.keys()) 214 | for host in hosts: 215 | s = stats.summarize(host) 216 | 217 | if s['failures'] or s['unreachable']: 218 | color = 'failed' 219 | elif s['changed']: 220 | color = 'changed' 221 | else: 222 | color = 'ok' 223 | 224 | msg = '{} : ok={}\tchanged={}\tfailed={}\tunreachable={}'.format( 225 | host, s['ok'], s['changed'], s['failures'], s['unreachable']) 226 | print(colorize(msg, color)) 227 | 228 | def v2_runner_on_skipped(self, result, **kwargs): 229 | """Run when a task is skipped.""" 230 | if self._display.verbosity > 1: 231 | self._print_task() 232 | self.last_skipped = False 233 | 234 | line_length = 120 235 | spaces = ' ' * (31-len(result._host.name)-4) 236 | 237 | line = " * {}{}- {}".format(colorize(result._host.name, 'not_so_bold'), 238 | spaces, 239 | colorize("skipped", 'skipped'),) 240 | 241 | reason = result._result.get('skipped_reason', '') or \ 242 | result._result.get('skip_reason', '') 243 | if len(reason) < 50: 244 | line += ' -- {}'.format(reason) 245 | print("{} {}---------".format(line, '-' * (line_length - len(line)))) 246 | else: 247 | print("{} {}".format(line, '-' * (line_length - len(line)))) 248 | print(self._indent_text(reason, 8)) 249 | print(reason) 250 | 251 | v2_playbook_on_handler_task_start = v2_playbook_on_task_start 252 | v2_runner_on_failed = v2_runner_on_ok 253 | v2_runner_on_unreachable = v2_runner_on_ok 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------