├── .gitignore ├── .yamllint ├── README.md ├── ansible.cfg ├── filter_plugins └── list.py ├── getinfo ├── nxos-get-interfaces.yml ├── nxos-get-vlans.yml ├── nxos-interfaces.j2 └── nxos-vlans.j2 ├── group_vars └── all.yml ├── hosts ├── provision ├── cleanup-prepare.yml ├── nxos-add.yml └── nxos-remove.yml ├── read-transaction-file.yml ├── services.yml ├── templates ├── cleanup-remove-ports.j2 ├── cleanup-remove-vlans.j2 ├── dump-variables.j2 ├── node-model.j2 └── transaction-to-node.j2 ├── tests ├── ansible.cfg ├── cleanup-test.yml ├── create-getinfo-scenario.sh ├── create-model-scenario.sh ├── dumpvars.yml ├── enable-debugging.yml ├── generate-transaction-tests.sh ├── getinfo.yml ├── hosts-tests ├── inputs │ ├── README.md │ ├── get-if-interfaces-1.json │ ├── get-if-interfaces-100-200.json │ ├── get-if-interfaces-100-202.json │ ├── get-if-raw-1.json │ ├── get-if-raw-100-200.json │ ├── get-if-raw-100-202.json │ ├── get-vlan-raw-1.json │ ├── get-vlan-raw-100-200.json │ ├── get-vlan-raw-100-202.json │ ├── get-vlans-1.json │ ├── get-vlans-100-200.json │ ├── get-vlans-100-202.json │ ├── model-100-200.json │ └── model-100.json ├── list-plugin.yml ├── model.yml ├── predeploy.yml ├── run-predeploy-tests.sh ├── run-transaction-model-tests.sh ├── services │ ├── svc-100-200.yml │ ├── svc-100-202-absent.yml │ ├── svc-100-202.yml │ ├── svc-pd-initial.yml │ ├── svc-pdf-dup-VLAN.yml │ ├── svc-pdf-dup-customer.yml │ ├── svc-pdf-dup-port.yml │ ├── svc-pdf-missing-interface.yml │ ├── svc-pdf-missing-node.yml │ ├── svc-pdok-dup-VLAN.yml │ └── svc-pdok-dup-port.yml ├── trans-model.yml ├── transactions │ ├── cust-remove.yml │ ├── port-add-2node.yml │ ├── port-add.yml │ ├── port-remove.yml │ └── valid │ │ ├── cust-remove-leaf-1-model.json │ │ ├── cust-remove-leaf-2-model.json │ │ ├── port-add-2node-leaf-1-model.json │ │ ├── port-add-2node-leaf-2-model.json │ │ ├── port-add-leaf-1-model.json │ │ ├── port-remove-leaf-1-model.json │ │ └── port-remove-leaf-2-model.json └── unique-plugin.yml ├── tools └── fix ├── transaction.yml ├── validate ├── deployed-services.yml ├── input-file-format.yml ├── predeploy-checks.yml ├── predeploy-interface-check.yml ├── predeploy-node-check.yml ├── predeploy-port-check.yml ├── predeploy-vlan-check.yml ├── transaction-data.yml └── transaction-node-check.yml └── vlan.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /configs 2 | /printouts 3 | /logging 4 | *.pyc 5 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | max-spaces-inside: 1 7 | colons: 8 | max-spaces-after: -1 9 | brackets: 10 | max-spaces-inside: 1 11 | indentation: 12 | indent-sequences: false 13 | truthy: false 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manage a simple VLAN service 2 | 3 | The **vlan.yml** playbook in this repository manages (add/modify/remove) a simple VLAN service described in the *services.yml* data model on NX-OS switches. 4 | 5 | ## Explore the progress 6 | 7 | I created a number of git branches while developing this example to allow you to explore various stages of the project. Use git checkout *branch* to check out the desired stage of the project into your working directory. 8 | 9 | The branches you can explore are: 10 | 11 | * ***VLAN_Initial*** - initial implementation that provisions VLAN database, interfaces and access VLANs straight from the services data model 12 | * ***VLAN_Data_Model*** - transforms services data model into per-node data model at the beginning of the playbook for easier processing. The introduction of per-node data model simplified the provisioning process. 13 | * ***VLAN_Decommission*** - add support for service decommissioning and customer site description in the services data model, which triggered changes in data model transformation and minor adjustments in service provisioning process. 14 | * ***VLAN_Validation*** - validates successful deployment of VLAN services: VLANs are created and configured as access VLANs on service ports. 15 | * ***VLAN_Cleanup*** - cleanup of VLANs and service ports no longer used by active services. 16 | * ***VLAN_PreDeploy_Check*** - check the actual hardware before service deployment 17 | * ***Logging*** - extensive logging and debugging support 18 | * ***Plugins*** - Jinja2 filter plugins are used for unique key and composite key tests 19 | * ***Transactions*** - integration with an external transaction system 20 | * ***MultiVendor*** - add multi-platform/vendor support 21 | 22 | ## Data model 23 | 24 | Service definition file (services.yml) is a YAML dictionary with every customer being a key/value pair. Each customer record has a **vlan** key (VLAN# used for this customer) and a list of active service ports (**ports** key). Later stages of the project add **state** key to indicate a service should be removed (when **state** is set to **absent**). 25 | 26 | The list of service ports is a list of dictionaries, each one of them having: 27 | * **node**: device on which the service port is configured 28 | * **port**: port on the device 29 | * **site**: site name within customer network (used for interface descriptions) 30 | 31 | Here's a sample service definition file: 32 | 33 | --- 34 | ACME: 35 | vlan: 100 36 | ports: 37 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 38 | - { node: leaf-2, port: "GigabitEthernet0/1", site: "ACME Remote" } 39 | 40 | Wonka: 41 | vlan: 200 42 | state: absent 43 | ports: 44 | - { node: leaf-1, port: "Ethernet2/3"} 45 | - { node: leaf-2, port: "GigabitEthernet0/3" } 46 | 47 | ## Transaction data model 48 | 49 | Transaction file (samples in ***tests/transactions*** directory) is a YAML dictionary describing a single customer. 50 | 51 | Every transaction must have: 52 | * **cid** - customer ID (number) 53 | * **vlan** - VLAN# used for the customer (number) 54 | * **name** - name of the customer (no spaces allowed) 55 | * **ports** - list of ports used by the customer. 56 | * **state** (optional) - value `absent` indicates customer removal request). 57 | 58 | The list of service ports is a list of dictionaries, each one of them having: 59 | * **node**: device on which the service port is configured 60 | * **port**: port on the device 61 | * **site**: site name within customer network (used for interface descriptions) 62 | * **state**: optional - value `absent` indicates port removal request 63 | 64 | Here's a sample transaction file: 65 | 66 | cid: 42 67 | vlan: 100 68 | name: ACME 69 | ports: 70 | - { node: leaf-1, port: "Ethernet2/1", state: absent } 71 | - { node: leaf-2, port: "Ethernet2/3", site: "Remote" } 72 | 73 | ## Tests 74 | 75 | All the tests included in this project are in *tests* directory: 76 | 77 | * ***model.yml*** - creates per-node data models and stores them in *printouts* directory 78 | * ***cleanup-test.yml*** - test harness for the cleanup module 79 | * ***predeploy.yml*** - test harness for pre-deploy checks 80 | * ***list-plugin.yml*** - unit tests for `append` plugin in ***list.py*** 81 | * ***unique-plugin.yml*** - unit tests for `dupattr` plugin in ***list.py*** 82 | * ***trans-model.yml*** - transforms a single transaction into a set of per-node data models 83 | 84 | The same directory contains bash scripts that execute a suite of unit tests: 85 | 86 | * ***run-predeploy-tests*** executes ***predeploy.yml*** playbook for every `*pdf*` test in the ***services*** subdirectory 87 | * ***run-transaction-model-tests*** executes ***trans-model.yml*** for every transaction in the ***transactions*** directory 88 | 89 | The tests use multiple scenarios stored in various subdirectory of the tests directory: 90 | 91 | * ***input*** - data generated by various ***show*** commands (used for cleanup tests). Scenarios are generated by executing **show** commands on NX-OS with the **create-getinfo-scenario.sh** scripts. 92 | * ***services*** - unit test scenarios for pre-deploy checks; include numerous valid service scenarios as well as scenarios that are supposed to fail (`*pdf*`) or succeed (`*pdok*`). 93 | * ***transactions*** - sample transactions and valid per-node data models generated from those transactions 94 | 95 | Further bash scripts create additional testing files: 96 | 97 | * ***create-model-scenario.sh*** creates a set of JSON per-node data models from a services data model. 98 | * ***generate-transaction-tests.sh*** creates valid JSON per-node data models from sample transactions (run only when you're absolutely sure the transformation template works correctly) -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory=./hosts 3 | gathering=explicit 4 | retry_files_enabled=false 5 | transport=local 6 | filter_plugins=./filter_plugins 7 | -------------------------------------------------------------------------------- /filter_plugins/list.py: -------------------------------------------------------------------------------- 1 | # 2 | # Simple list append filter 3 | # 4 | from __future__ import (absolute_import, division, print_function) 5 | __metaclass__ = type 6 | 7 | from jinja2 import TemplateError 8 | 9 | class FilterModule(object): 10 | 11 | 12 | # 13 | # Append a number of items to the list 14 | # 15 | def list_append(self,l,*argv): 16 | if type(l) is not list: 17 | raise TemplateError("First argument of append filter must be a list") 18 | 19 | for element in argv: 20 | if type(element) is list: 21 | l.extend(element) 22 | else: 23 | l.append(element) 24 | return l 25 | 26 | def list_flatten(self,l): 27 | if type(l) is not list: 28 | raise TemplateError("flatten filter takes a list") 29 | 30 | def recurse_flatten(l): 31 | if type(l) is not list: 32 | return [l] 33 | r = [] 34 | for i in l: 35 | r.extend(recurse_flatten(i)) 36 | return r 37 | 38 | return recurse_flatten(l) 39 | 40 | def check_duplicate_attr(self,d,attr = None,mandatory = False): 41 | seen = {} 42 | stat = [] 43 | 44 | def get_value(value): 45 | 46 | def get_single_value(v,k): 47 | if not(k in v): 48 | if mandatory: 49 | raise TemplateError("Missing mandatory attribute %s in %s" % (k,v)) 50 | else: 51 | return None 52 | return v[k] 53 | 54 | if type(attr) is list: 55 | retval = "" 56 | for a in attr: 57 | item = get_single_value(value,a) 58 | retval += " " if retval else "" 59 | retval += "%s=%s" % (a,item) 60 | return retval 61 | else: 62 | return get_single_value(value,attr) 63 | 64 | def check_unique_value(key,value): 65 | if key is not None: 66 | value['key'] = key 67 | v = get_value(value) 68 | if v in seen: 69 | stat.append("Duplicate value %s of attribute %s found in %s and %s" % 70 | (v,attr, 71 | seen[v]['key'] if ('key' in seen[v]) else seen[v], 72 | value['key'] if ('key' in value) else value)) 73 | else: 74 | seen[v] = value 75 | 76 | # sanity check: do we know which attribute to check? 77 | # 78 | if attr is None: 79 | raise TemplateError("You have to specify attr=name in checkunique") 80 | 81 | # iterate over a list or a dictionary, fail otherwise 82 | # 83 | if type(d) is list: 84 | for value in d: 85 | check_unique_value(None,value) 86 | elif type(d) is dict: 87 | for key in d: 88 | check_unique_value(key,d[key]) 89 | else: 90 | raise TemplateError("") 91 | 92 | if len(stat) == 0: 93 | return None 94 | else: 95 | return stat 96 | 97 | def filters(self): 98 | return { 99 | 'append': self.list_append, 100 | 'flatten': self.list_flatten, 101 | 'dupattr': self.check_duplicate_attr 102 | } -------------------------------------------------------------------------------- /getinfo/nxos-get-interfaces.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Get interface and VLAN information from NX-OS 3 | # 4 | --- 5 | - nxos_command: 6 | provider: "{{cli}}" 7 | commands: 8 | - { command: "show interface status", output: "json" } 9 | register: nxos_results 10 | 11 | - block: 12 | - copy: 13 | content: "{{nxos_results.stdout[0]}}" 14 | dest: "{{debug_output}}/{{inventory_hostname}}-get-raw.json" 15 | delegate_to: localhost 16 | - template: 17 | src: nxos-interfaces.j2 18 | dest: "{{debug_output}}/{{inventory_hostname}}-get-interfaces.json" 19 | delegate_to: localhost 20 | when: debug_output is defined 21 | 22 | - set_fact: 23 | vlan_interfaces: | 24 | {{lookup("template","nxos-interfaces.j2")}} 25 | -------------------------------------------------------------------------------- /getinfo/nxos-get-vlans.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Get interface and VLAN information from NX-OS 3 | # 4 | - nxos_command: 5 | provider: "{{cli}}" 6 | commands: 7 | - { command: "show vlan", output: "json" } 8 | register: nxos_results 9 | 10 | - block: 11 | - copy: 12 | content: "{{nxos_results.stdout[0]}}" 13 | dest: "{{debug_output}}/{{inventory_hostname}}-get-vlan-raw.json" 14 | delegate_to: localhost 15 | - template: 16 | src: nxos-vlans.j2 17 | dest: "{{debug_output}}/{{inventory_hostname}}-get-vlans.json" 18 | delegate_to: localhost 19 | when: debug_output is defined 20 | 21 | - set_fact: 22 | vlan_list: | 23 | {{lookup("template","nxos-vlans.j2")}} 24 | -------------------------------------------------------------------------------- /getinfo/nxos-interfaces.j2: -------------------------------------------------------------------------------- 1 | {% macro output(s) -%}{% 2 | if s == "connected" %}up{% 3 | elif s == "disabled" %}disabled{% 4 | else %}down{% endif 5 | %}{%- endmacro %} 6 | {# 7 | Create a list of interfaces in common data model from NXOS JSON data 8 | #} 9 | { 10 | {% for intf in nxos_results.stdout[0].TABLE_interface.ROW_interface %} 11 | "{{intf.interface}}": { 12 | {% if intf.vlan|search("^\d+$") %} 13 | "mode": "access", "vlan": {{intf.vlan}}, 14 | {% elif intf.vlan == "routed" %} 15 | "mode": "L3", 16 | {% elif intf.vlan == "trunk" %} 17 | "mode": "trunk", 18 | {% else %} 19 | "mode": "unknown", 20 | {% endif %} 21 | "state": "{{output(intf.state)}}" }{% if not(loop.last) %},{% endif %} 22 | 23 | {% endfor %} 24 | } 25 | -------------------------------------------------------------------------------- /getinfo/nxos-vlans.j2: -------------------------------------------------------------------------------- 1 | {% macro output(s) -%}{% 2 | if s == "connected" %}up{% 3 | elif s == "disabled" %}disabled{% 4 | else %}down{% endif 5 | %}{%- endmacro %} 6 | {# 7 | Create a list object based on NXOS VLAN object 8 | #} 9 | {% macro vlan(v) -%} 10 | "{{v['vlanshowbr-vlanid-utf']}}": { 11 | "name": "{{v['vlanshowbr-vlanname']|default('')}}", 12 | {% if v['vlanshowbr-shutstate'] == 'shutdown' %} 13 | "state": "shutdown" 14 | {% elif v['vlanshowbr-vlanstate'] in [ 'active','suspend' ] %} 15 | "state": "{{v['vlanshowbr-vlanstate']}}" 16 | {% else %} 17 | "state": "unknown" 18 | {% endif %} 19 | } 20 | {%- endmacro %} 21 | {# 22 | Create a list of vlans in common data model from NXOS JSON data 23 | #} 24 | { 25 | {% set data = nxos_results.stdout[0].TABLE_vlanbrief.ROW_vlanbrief %} 26 | {% if data['vlanshowbr-vlanid-utf'] is defined %} 27 | {{ vlan(data) }} 28 | {% else %} 29 | {% for v in data %} 30 | {{ vlan(v) }}{% if not(loop.last) %}, 31 | {% endif %} 32 | {% endfor %} 33 | {% endif %} 34 | } 35 | -------------------------------------------------------------------------------- /group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ansible_user: cisco 3 | ansible_ssh_pass: cisco 4 | 5 | cli: 6 | username: "{{ansible_user}}" 7 | password: "{{ansible_ssh_pass}}" 8 | host: "{{ansible_host|default(inventory_hostname)}}" 9 | transport: cli 10 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | leaf-1 ansible_host=172.16.1.110 ansible_os=nxos 2 | leaf-2 ansible_host=172.16.1.111 ansible_os=nxos 3 | -------------------------------------------------------------------------------- /provision/cleanup-prepare.yml: -------------------------------------------------------------------------------- 1 | # 2 | # List of tasks needed to validate deployed services 3 | # 4 | --- 5 | - name: Get VLAN information from the device in common data model 6 | include: "../getinfo/{{ansible_os}}-get-vlans.yml" 7 | when: vlan_list|length == 0 8 | 9 | - name: Get interface information from the device in common data model 10 | include: "../getinfo/{{ansible_os}}-get-interfaces.yml" 11 | when: vlan_interfaces|length == 0 12 | 13 | - set_fact: 14 | data: "{{data|combine(lookup('template','../templates/cleanup-remove-ports.j2'))}}" 15 | when: vlan_interfaces is defined 16 | 17 | - set_fact: 18 | data: "{{data|combine(lookup('template','../templates/cleanup-remove-vlans.j2'))}}" 19 | when: vlan_list is defined 20 | 21 | - set_fact: debug_phase="-cleanup" -------------------------------------------------------------------------------- /provision/nxos-add.yml: -------------------------------------------------------------------------------- 1 | # 2 | # List of tasks needed to provision new VLAN services on NX-OS 3 | # 4 | --- 5 | - name: Add NX-OS VLANs 6 | nxos_vlan: 7 | provider: "{{cli}}" 8 | vlan_id: "{{item.key}}" 9 | name: "{{item.value}}" 10 | state: "present" 11 | admin_state: "up" 12 | with_dict: "{{data.vlans}}" 13 | register: provision_vlans 14 | tags: [ print_action ] 15 | - local_action: > 16 | copy content="{{provision_vlans|to_nice_json}}" 17 | dest="{{debug_output}}/{{inventory_hostname}}-provision-vlans.json" 18 | when: debug_output is defined 19 | 20 | - name: Enable NX-OS Interfaces 21 | nxos_interface: 22 | provider: "{{cli}}" 23 | admin_state: up 24 | interface: "{{item.key}}" 25 | mode: layer2 26 | description: "{{item.value.description}}" 27 | with_dict: "{{data.ports}}" 28 | when: "item.key" 29 | register: provision_interfaces 30 | tags: [ print_action ] 31 | - local_action: > 32 | copy content="{{provision_interfaces|to_nice_json}}" 33 | dest="{{debug_output}}/{{inventory_hostname}}-provision-interfaces.json" 34 | when: debug_output is defined 35 | 36 | - name: Enable NX-OS switchports 37 | nxos_switchport: 38 | provider: "{{cli}}" 39 | mode: access 40 | access_vlan: "{{item.value.vlan}}" 41 | interface: "{{item.key}}" 42 | with_dict: "{{data.ports}}" 43 | when: "item.key" 44 | register: provision_switchports 45 | tags: [ print_action ] 46 | - local_action: > 47 | copy content="{{provision_switchports|to_nice_json}}" 48 | dest="{{debug_output}}/{{inventory_hostname}}-provision-switchports.json" 49 | when: debug_output is defined 50 | -------------------------------------------------------------------------------- /provision/nxos-remove.yml: -------------------------------------------------------------------------------- 1 | # 2 | # List of tasks needed to decommission VLAN services on NX-OS 3 | # 4 | --- 5 | - name: Shut down NX-OS interfaces 6 | nxos_interface: 7 | provider: "{{cli}}" 8 | admin_state: down 9 | interface: "{{item}}" 10 | mode: layer2 11 | description: "No service on this port" 12 | with_items: "{{data.remove_ports}}" 13 | register: nxos_remove_ports 14 | tags: [ print_action ] 15 | - local_action: > 16 | copy content="{{nxos_remove_ports|to_nice_json}}" 17 | dest="{{debug_output}}/{{inventory_hostname}}-remove-ports{{debug_phase|default('')}}.json" 18 | when: debug_output is defined 19 | 20 | - name: Disable NX-OS switchports 21 | nxos_switchport: 22 | provider: "{{cli}}" 23 | mode: access 24 | access_vlan: 1 25 | interface: "{{item}}" 26 | with_items: "{{data.remove_ports}}" 27 | register: nxos_remove_access 28 | tags: [ print_action ] 29 | - local_action: > 30 | copy content="{{nxos_remove_access|to_nice_json}}" 31 | dest="{{debug_output}}/{{inventory_hostname}}-remove-access{{debug_phase|default('')}}.json" 32 | when: debug_output is defined 33 | 34 | - name: Remove NX-OS VLANs 35 | nxos_vlan: 36 | provider: "{{cli}}" 37 | vlan_id: "{{item}}" 38 | state: "absent" 39 | with_items: "{{data.remove_vlans}}" 40 | register: nxos_remove_vlans 41 | tags: [ print_action ] 42 | - local_action: > 43 | copy content="{{nxos_remove_vlans|to_nice_json}}" 44 | dest="{{debug_output}}/{{inventory_hostname}}-remove-vlans{{debug_phase|default('')}}.json" 45 | when: debug_output is defined 46 | 47 | -------------------------------------------------------------------------------- /read-transaction-file.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Find transaction file based on tid variable and read it 3 | # 4 | - set_fact: transaction_file={{item}} 5 | with_first_found: 6 | - files: "{{tid}}" 7 | paths: 8 | - . 9 | - "{{inventory_dir}}" 10 | - files: "{{trans}}.yml" 11 | paths: 12 | - . 13 | - "{{inventory_dir}}" 14 | - tests/transactions 15 | tags: [ always ] 16 | - include_vars: 17 | file: "{{transaction_file}}" 18 | name: "transaction" 19 | tags: [ always ] -------------------------------------------------------------------------------- /services.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "Ethernet2/2" } 14 | -------------------------------------------------------------------------------- /templates/cleanup-remove-ports.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Create a list of interface names to be removed because they 3 | are access interfaces not associated with active services 4 | #} 5 | { 6 | "remove_ports": [ 7 | {% for ifname,ifdata in vlan_interfaces.iteritems() 8 | if ifdata.mode == "access" and ifdata.vlan != 1 9 | and not(data.ports[ifname] is defined) %} 10 | "{{ ifname }}"{% if not(loop.last) %},{% endif %} 11 | {% endfor %} 12 | ]} 13 | -------------------------------------------------------------------------------- /templates/cleanup-remove-vlans.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Create a list of VLANs to be removed because they 3 | are within service VLAN range but are not needed 4 | by active services 5 | #} 6 | { 7 | "remove_vlans": [ 8 | {% for vid,vdata in vlan_list.iteritems() 9 | if vid|int >= min_service_vlan|default(100) 10 | and vid|int <= max_service_vlan|default(2000) 11 | and not(data.vlans[vid] is defined) %} 12 | "{{ vid }}"{% if not(loop.last) %},{% endif %} 13 | {% endfor %} 14 | ]} 15 | -------------------------------------------------------------------------------- /templates/dump-variables.j2: -------------------------------------------------------------------------------- 1 | --- 2 | {% for v in include 3 | if vars[v] is defined %} 4 | {{v}}: {{vars[v]|to_nice_json(indent=2)|indent(2)}} 5 | {% endfor %} -------------------------------------------------------------------------------- /templates/node-model.j2: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | {% for customer,svc in services.iteritems() 4 | if inventory_hostname in svc.ports|map(attribute='node') 5 | and svc.state|default("") != "absent" %} 6 | "{{svc.vlan}}": "{{customer}}"{% if not(loop.last) %},{% endif %} 7 | 8 | {% endfor %} 9 | }, 10 | "remove_vlans": [ 11 | {% for customer,svc in services.iteritems() 12 | if inventory_hostname in svc.ports|map(attribute='node') 13 | and svc.state|default("") == "absent" %} 14 | {{svc.vlan}}{% if not(loop.last) %},{% endif %} 15 | {% endfor %} 16 | ], 17 | "vlan_list": [ 1, 18 | {% for customer,svc in services.iteritems() %} 19 | {% if inventory_hostname in svc.ports|map(attribute='node') 20 | and svc.state|default("") != "absent" %} 21 | {{svc.vlan}}{% if not(loop.last) %},{% endif %} 22 | {% endif %} 23 | {% endfor %} 24 | ], 25 | "ports": { 26 | {% set count = [] %} 27 | {% for customer,svc in services.iteritems() 28 | if svc.state|default("") != "absent" %} 29 | {% for p in svc.ports if p.node == inventory_hostname %} 30 | {% if count|length > 0 %},{% endif %}{% set _ = count.append(1) %} 31 | "{{p.port}}": { "vlan": {{svc.vlan}}, "description": "{{customer}} - {{p.site|default("")}}" } 32 | {% endfor %} 33 | {% endfor %} 34 | }, 35 | "remove_ports": [ 36 | {% set count = [] %} 37 | {% for customer,svc in services.iteritems() 38 | if svc.state|default("") == "absent" %} 39 | {% for p in svc.ports if p.node == inventory_hostname %} 40 | {% if count|length > 0 %},{% endif %}{% set _ = count.append(1) %} 41 | "{{p.port}}" 42 | {% endfor %} 43 | {% endfor %} 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /templates/transaction-to-node.j2: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | {% if transaction.state|default("") != "absent" %} 4 | "{{transaction.vlan}}": "{{transaction.name}}[{{transaction.cid}}]" 5 | {% endif %} 6 | }, 7 | "remove_vlans": [ 8 | {% if transaction.state|default("") == "absent" %} 9 | {{transaction.vlan}} 10 | {% endif %} 11 | ], 12 | "vlan_list": [ 1 {% if transaction.state|default("") != "absent" %}, {{transaction.vlan}}{% endif %} ], 13 | "ports": { 14 | {% if transaction.state|default("") != "absent" %} 15 | {% for p in transaction.ports 16 | if p.node == inventory_hostname and p.state|default("") != "absent" %} 17 | "{{p.port}}": { "vlan": {{transaction.vlan}}, 18 | "description": "{{transaction.name}} [{{transaction.cid}}]{% if p.site is defined %} - {{p.site}} {% endif %}" } 19 | {% if not loop.last %},{% endif %} 20 | {% endfor %} 21 | {% endif %} 22 | }, 23 | "remove_ports": [ 24 | {% for p in transaction.ports if p.node == inventory_hostname %} 25 | {% if transaction.state|default("") == "absent" or p.state|default("") == "absent" %} 26 | "{{p.port}}"{% if not loop.last %},{% endif %} 27 | {% endif %} 28 | {% endfor %} 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tests/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | gathering=explicit 3 | retry_files_enabled=false 4 | transport=local 5 | -------------------------------------------------------------------------------- /tests/cleanup-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | name: Test cleanup code 4 | vars: 5 | output: "{{inventory_dir}}/printouts" 6 | tasks: 7 | - assert: 8 | that: 9 | - model is defined 10 | - model != "" 11 | - input is defined 12 | - input != "" 13 | msg: Have to specify model and input scenarios as extra-vars 14 | 15 | - include_vars: 16 | file: "inputs/model-{{model}}.json" 17 | name: data 18 | - include_vars: 19 | file: "inputs/get-if-interfaces-{{input}}.json" 20 | name: vlan_interfaces 21 | - include_vars: 22 | file: "inputs/get-vlans-{{input}}.json" 23 | name: vlan_list 24 | 25 | - include: "../provision/cleanup-prepare.yml" 26 | 27 | - file: path="{{output}}/{{inventory_hostname}}-remove-vlans.json" state=absent 28 | - file: path="{{output}}/{{inventory_hostname}}-remove-ports.json" state=absent 29 | 30 | - copy: 31 | content: "{{data.remove_vlans|to_nice_json}}" 32 | dest: "{{output}}/{{inventory_hostname}}-remove-vlans.json" 33 | when: remove_vlans is defined 34 | - copy: 35 | content: "{{data.remove_ports|to_nice_json}}" 36 | dest: "{{output}}/{{inventory_hostname}}-remove-ports.json" 37 | when: remove_ports is defined -------------------------------------------------------------------------------- /tests/create-getinfo-scenario.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Create getinfo scenario 4 | # - executes getinfo.yml playbook 5 | # - copies printotus as a scenario into inputs directory 6 | # 7 | if [ -z "$1" ]; then 8 | echo "You have to specify the scenario name" 9 | exit 10 | fi 11 | # 12 | rm ../printouts/*.json 13 | ansible-playbook -i ../hosts getinfo.yml 14 | cp ../printouts/leaf-1-get-vlan-raw.json inputs/get-vlan-raw-$1.json 15 | cp ../printouts/leaf-1-get-vlans.json inputs/get-vlans-$1.json 16 | cp ../printouts/leaf-1-get-raw.json inputs/get-if-raw-$1.json 17 | cp ../printouts/leaf-1-get-interfaces.json inputs/get-if-interfaces-$1.json -------------------------------------------------------------------------------- /tests/create-model-scenario.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Create model scenario 4 | # - executes model.yml playbook 5 | # - copies generated data model for leaf-1 as a scenario into inputs directory 6 | # 7 | if [ -z "$1" ]; then 8 | echo "You have to specify the scenario name" 9 | exit 10 | fi 11 | # 12 | rm ../printouts/*.json 13 | ansible-playbook -i ../hosts model.yml 14 | cp ../printouts/leaf-1-model.json inputs/model-$1.json 15 | -------------------------------------------------------------------------------- /tests/dumpvars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | name: test variable dumps 4 | vars: 5 | input: "{{inventory_dir}}/services.yml" 6 | output: "{{inventory_dir}}/printouts" 7 | model: "../templates/node-model.j2" 8 | dump: "../templates/dump-variables.j2" 9 | tasks: 10 | - include_vars: 11 | file: "{{input}}" 12 | name: services 13 | tags: [ always ] 14 | - set_fact: data="{{lookup('template',model)}}" 15 | tags: [ always ] 16 | - debug: var=vars 17 | - local_action: > 18 | copy content="{{vars|to_yaml}}" 19 | dest="{{output}}/{{inventory_hostname}}-vars.yml" 20 | - local_action: > 21 | template src="{{dump}}" 22 | dest="{{output}}/{{inventory_hostname}}-dump.yml" 23 | vars: 24 | include: [ services,data ] 25 | 26 | -------------------------------------------------------------------------------- /tests/enable-debugging.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Enable debugging when the 'debug' variable is set 3 | # 4 | - block: 5 | - set_fact: 6 | debug_output: "{{inventory_dir}}/printouts/debug" 7 | name: Enable debugging 8 | - local_action: file path="{{debug_output}}" state=directory 9 | run_once: true 10 | when: debug is defined -------------------------------------------------------------------------------- /tests/generate-transaction-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Batch-generate correct data for transaction tests 4 | # Use this script only when you're absolutely sure the transaction-to-node 5 | # translational model is correct 6 | # 7 | cat </dev/null 2>/dev/null 16 | if [ $? -ne 0 ]; then 17 | echo ".. initial test failed, cannot proceed" 18 | exit 1 19 | fi 20 | echo " .. OK, proceeding" 21 | 22 | exitstatus=0 23 | for svctest in tests/services/svc-pdf-*.yml 24 | do 25 | echo "Running scenario $svctest" 26 | ansible-playbook tests/predeploy.yml -e svcs=../$svctest >/dev/null 2>/dev/null 27 | if [ $? -ne 0 ]; then 28 | echo " .. failed as expected" 29 | else 30 | echo " >>> DID NOT FAIL" 31 | exitstatus=1 32 | fi 33 | done 34 | 35 | if [ $exitstatus -ne 0 ]; then echo "Test suite failed"; fi 36 | exit $exitstatus -------------------------------------------------------------------------------- /tests/run-transaction-model-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run pre-deploy unit tests 4 | # 5 | echo "Testing transaction model transformation" 6 | if [ ! -f hosts ]; then 7 | echo ".. Cannot find Ansible inventory file hosts in current directory, aborting" 8 | echo 9 | echo "Hint: run this script from main project directory." 10 | echo "Typical command would be tests/run-transaction-model-tests.sh" 11 | exit 12 | fi 13 | 14 | cd tests 15 | export ANSIBLE_STDOUT_CALLBACK=dense 16 | if [ -f ../../Plugins/dense.py ]; then 17 | export ANSIBLE_CALLBACK_PLUGINS=../../Plugins 18 | fi 19 | 20 | exitstatus=0 21 | for trans in transactions/*.yml 22 | do 23 | echo "Running transaction $trans" 24 | ansible-playbook -i ../hosts trans-model.yml -e "trans=$trans" -t validate 25 | if [ $? -ne 0 ]; then 26 | echo " .. test failed for transaction $trans" 27 | exitstatus=1 28 | fi 29 | done 30 | 31 | if [ $exitstatus -ne 0 ]; then echo "Test suite failed"; fi 32 | exit $exitstatus -------------------------------------------------------------------------------- /tests/services/svc-100-200.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Test services file where all VLAN2 are configured on the switch 3 | # 4 | --- 5 | ACME: 6 | vlan: 100 7 | ports: 8 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 9 | - { node: leaf-2, port: "GigabitEthernet0/1", site: "ACME Remote" } 10 | 11 | Wonka: 12 | vlan: 200 13 | ports: 14 | - { node: leaf-1, port: "Ethernet2/3"} 15 | - { node: leaf-2, port: "GigabitEthernet0/3" } 16 | -------------------------------------------------------------------------------- /tests/services/svc-100-202-absent.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "GigabitEthernet0/1", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "GigabitEthernet0/3" } 14 | 15 | Willy: 16 | vlan: 201 17 | ports: 18 | - { node: leaf-1, port: "Ethernet2/4" } 19 | # - { node: leaf-2, port: "GigabitEthernet0/4" } 20 | 21 | Coyote: 22 | vlan: 202 23 | state: absent 24 | ports: 25 | - { node: leaf-1, port: "Ethernet2/4" } 26 | - { node: leaf-2, port: "GigabitEthernet0/4" } 27 | -------------------------------------------------------------------------------- /tests/services/svc-100-202.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "GigabitEthernet0/1", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "GigabitEthernet0/3" } 14 | 15 | Willy: 16 | vlan: 201 17 | ports: 18 | - { node: leaf-1, port: "Ethernet2/4" } 19 | # - { node: leaf-2, port: "GigabitEthernet0/4" } 20 | 21 | Coyote: 22 | vlan: 202 23 | ports: 24 | # - { node: leaf-1, port: "Ethernet2/4} 25 | - { node: leaf-2, port: "GigabitEthernet0/4" } 26 | -------------------------------------------------------------------------------- /tests/services/svc-pd-initial.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 101 10 | ports: 11 | - { node: leaf-1, port: "Ethernet2/3"} 12 | - { node: leaf-2, port: "Ethernet2/2" } 13 | -------------------------------------------------------------------------------- /tests/services/svc-pdf-dup-VLAN.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "Ethernet2/2" } 14 | 15 | Willy: 16 | vlan: 100 17 | ports: 18 | - { node: leaf-1, port: "Ethernet2/4" } 19 | 20 | Coyote: 21 | vlan: 202 22 | ports: 23 | - { node: leaf-2, port: "Ethernet2/5" } 24 | -------------------------------------------------------------------------------- /tests/services/svc-pdf-dup-customer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "GigabitEthernet0/3" } 14 | 15 | ACME: 16 | vlan: 201 17 | ports: 18 | - { node: leaf-1, port: "Ethernet2/4" } 19 | # - { node: leaf-2, port: "GigabitEthernet0/4" } 20 | 21 | Coyote: 22 | vlan: 202 23 | ports: 24 | # - { node: leaf-1, port: "Ethernet2/4} 25 | - { node: leaf-2, port: "GigabitEthernet0/4" } 26 | -------------------------------------------------------------------------------- /tests/services/svc-pdf-dup-port.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | ports: 11 | - { node: leaf-1, port: "Ethernet2/1"} 12 | - { node: leaf-2, port: "Ethernet2/2" } 13 | -------------------------------------------------------------------------------- /tests/services/svc-pdf-missing-interface.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "GigabitEthernet0/1", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "GigabitEthernet0/3" } 14 | -------------------------------------------------------------------------------- /tests/services/svc-pdf-missing-node.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | ports: 11 | - { node: leaf-1, port: "Ethernet2/3"} 12 | - { node: leaf-3, port: "GigabitEthernet0/3" } 13 | -------------------------------------------------------------------------------- /tests/services/svc-pdok-dup-VLAN.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 100 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/3"} 13 | - { node: leaf-2, port: "Ethernet2/2" } 14 | 15 | Willy: 16 | vlan: 201 17 | ports: 18 | - { node: leaf-1, port: "Ethernet2/4" } 19 | -------------------------------------------------------------------------------- /tests/services/svc-pdok-dup-port.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ACME: 3 | vlan: 100 4 | ports: 5 | - { node: leaf-1, port: "Ethernet2/1", site: "ACME Downtown" } 6 | - { node: leaf-2, port: "Ethernet2/3", site: "ACME Remote" } 7 | 8 | Wonka: 9 | vlan: 200 10 | state: absent 11 | ports: 12 | - { node: leaf-1, port: "Ethernet2/1"} 13 | - { node: leaf-2, port: "Ethernet2/2" } 14 | -------------------------------------------------------------------------------- /tests/trans-model.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | name: test data model 4 | vars: 5 | output: "{{inventory_dir}}/printouts" 6 | model: "../templates/transaction-to-node.j2" 7 | tasks: 8 | - set_fact: 9 | input: "{{item}}" 10 | with_first_found: 11 | - files: "{{trans}}" 12 | - files: "{{trans}}.yml" 13 | paths: 14 | - . 15 | - "{{inventory_dir}}" 16 | - transactions 17 | - ../transactions 18 | - .. 19 | - ../.. 20 | tags: [ always ] 21 | - include_vars: 22 | file: "{{input}}" 23 | name: transaction 24 | tags: [ always ] 25 | - set_fact: 26 | trans_id: "{% set x = trans|basename %}{{ x.split('.')[0] }}" 27 | tags: [ always ] 28 | - block: 29 | - template: 30 | src: "{{model}}" 31 | dest: "{{output}}/{{trans_id}}-{{inventory_hostname}}-model.json" 32 | tags: [ always ] 33 | - shell: jq --argfile a {{output}}/{{trans_id}}-{{inventory_hostname}}-model.json --argfile b transactions/valid/{{trans_id}}-{{inventory_hostname}}-model.json -n '$a == $b' 34 | register: result 35 | failed_when: "'true' not in result.stdout" 36 | changed_when: false 37 | tags: [ validate ] 38 | when: "inventory_hostname in transaction.ports|map(attribute='node')" 39 | -------------------------------------------------------------------------------- /tests/transactions/cust-remove.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Sample transaction: remove port 3 | # 4 | cid: 42 5 | vlan: 100 6 | name: ACME 7 | state: absent 8 | ports: 9 | - { node: leaf-1, port: "Ethernet2/1" } 10 | - { node: leaf-2, port: "Ethernet2/3" } 11 | -------------------------------------------------------------------------------- /tests/transactions/port-add-2node.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Sample transaction: add port 3 | # 4 | cid: 42 5 | vlan: 100 6 | name: ACME 7 | ports: 8 | - { node: leaf-1, port: "Ethernet2/1", site: "Downtown" } 9 | - { node: leaf-2, port: "Ethernet2/3", site: "Remote" } 10 | -------------------------------------------------------------------------------- /tests/transactions/port-add.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Sample transaction: add port 3 | # 4 | cid: 42 5 | vlan: 100 6 | name: ACME 7 | ports: 8 | - { node: leaf-1, port: "Ethernet2/1", site: "Downtown" } 9 | -------------------------------------------------------------------------------- /tests/transactions/port-remove.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Sample transaction: add port 3 | # 4 | cid: 42 5 | vlan: 100 6 | name: ACME 7 | ports: 8 | - { node: leaf-1, port: "Ethernet2/1", state: absent } 9 | - { node: leaf-2, port: "Ethernet2/3", site: "Remote" } 10 | -------------------------------------------------------------------------------- /tests/transactions/valid/cust-remove-leaf-1-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | }, 4 | "remove_vlans": [ 5 | 6 | 100 7 | ], 8 | "vlan_list": [ 1 ], 9 | "ports": { 10 | }, 11 | "remove_ports": [ 12 | 13 | "Ethernet2/1" ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/transactions/valid/cust-remove-leaf-2-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | }, 4 | "remove_vlans": [ 5 | 6 | 100 7 | ], 8 | "vlan_list": [ 1 ], 9 | "ports": { 10 | }, 11 | "remove_ports": [ 12 | 13 | "Ethernet2/3" ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/transactions/valid/port-add-2node-leaf-1-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | 4 | "100": "ACME[42]" 5 | }, 6 | "remove_vlans": [ 7 | ], 8 | "vlan_list": [ 1 , 100 ], 9 | "ports": { 10 | "Ethernet2/1": { "vlan": 100, 11 | "description": "ACME [42] - Downtown " } 12 | }, 13 | "remove_ports": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/transactions/valid/port-add-2node-leaf-2-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | 4 | "100": "ACME[42]" 5 | }, 6 | "remove_vlans": [ 7 | ], 8 | "vlan_list": [ 1 , 100 ], 9 | "ports": { 10 | "Ethernet2/3": { "vlan": 100, 11 | "description": "ACME [42] - Remote " } 12 | }, 13 | "remove_ports": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/transactions/valid/port-add-leaf-1-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | 4 | "100": "ACME[42]" 5 | }, 6 | "remove_vlans": [ 7 | ], 8 | "vlan_list": [ 1 , 100 ], 9 | "ports": { 10 | "Ethernet2/1": { "vlan": 100, 11 | "description": "ACME [42] - Downtown " } 12 | }, 13 | "remove_ports": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/transactions/valid/port-remove-leaf-1-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | 4 | "100": "ACME[42]" 5 | }, 6 | "remove_vlans": [ 7 | ], 8 | "vlan_list": [ 1 , 100 ], 9 | "ports": { 10 | }, 11 | "remove_ports": [ 12 | 13 | "Ethernet2/1" ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/transactions/valid/port-remove-leaf-2-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "vlans": { 3 | 4 | "100": "ACME[42]" 5 | }, 6 | "remove_vlans": [ 7 | ], 8 | "vlan_list": [ 1 , 100 ], 9 | "ports": { 10 | "Ethernet2/3": { "vlan": 100, 11 | "description": "ACME [42] - Remote " } 12 | }, 13 | "remove_ports": [ 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/unique-plugin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - include_vars: 5 | file: "{{svcs}}" 6 | name: "services" 7 | when: svcs is defined 8 | - set_fact: x={{services|dupattr}} 9 | ignore_errors: true 10 | register: result 11 | - assert: 12 | that: "'You have to specify attr=name' in result.exception" 13 | msg: "Something's wrong: plugin didn't detect missing attribute {{result.exception}}" 14 | 15 | - set_fact: 16 | check: "{{[ { 'a': 100 }, { 'a' : 200} ]|dupattr(attr='a')}}" 17 | - assert: 18 | that: not check 19 | msg: "Failed on unique list: {{check}}" 20 | 21 | - set_fact: 22 | check: "{{[ { 'a': 100 }, { 'a' : 100} ]|dupattr(attr='a')}}" 23 | - assert: 24 | that: check 25 | msg: "Failed on non-unique list: {{check}}" 26 | - assert: 27 | that: "'100}' in check|join" 28 | msg: "Unexpected duplicate list result: {{check}}" 29 | 30 | - set_fact: 31 | check: "{{ { 'x':{ 'a': 100 }, 'y': { 'a' : 200} } |dupattr(attr='a')}}" 32 | - assert: 33 | that: not check 34 | msg: "Failed on unique dictionary: {{check}}" 35 | 36 | - set_fact: 37 | check: "{{ { 'x':{ 'a': 100 }, 'y': { 'a' : 100} } |dupattr(attr='a')}}" 38 | - assert: 39 | that: check 40 | msg: "Failed on non-unique dictionary: {{check}}" 41 | - assert: 42 | that: "'x and y' in check|join or 'y and x' in check|join" 43 | msg: "Unexpected duplicate dictionary result: {{check}}" 44 | 45 | - block: 46 | - set_fact: 47 | check: "{{services|dupattr(attr='vlan')}}" 48 | - debug: var=check 49 | - set_fact: 50 | check: "{{services.values()|map(attribute='ports')|list|flatten|dupattr(attr=['node','port'])}}" 51 | - debug: var=check 52 | when: services is defined 53 | -------------------------------------------------------------------------------- /tools/fix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Use this script to copy one or more changed files into 4 | # multiple branches 5 | # 6 | git checkout $1 && \ 7 | git checkout VLAN_PreDeploy_Check -- README.md && \ 8 | git status && \ 9 | git commit -m 'Update test description in README.md' 10 | -------------------------------------------------------------------------------- /transaction.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | name: Select network nodes participating in transaction 4 | tasks: 5 | - include: read-transaction-file.yml 6 | - include: tests/enable-debugging.yml 7 | - include: validate/transaction-data.yml 8 | tags: [ predeploy ] 9 | - include: validate/transaction-node-check.yml 10 | - name: Build the list of participating network nodes 11 | add_host: 12 | name: "{{item.node}}" 13 | groups: "deploy" 14 | with_items: "{{transaction.ports}}" 15 | tags: [ print_action ] 16 | 17 | - hosts: deploy 18 | name: configure a single VLAN transaction 19 | tasks: 20 | - include: read-transaction-file.yml 21 | - include: tests/enable-debugging.yml 22 | - set_fact: data="{{lookup('template','templates/transaction-to-node.j2')}}" 23 | tags: [ always ] 24 | - local_action: > 25 | copy content="{{data|to_nice_json}}" 26 | dest="{{debug_output}}/{{inventory_hostname}}-model.json" 27 | when: debug_output is defined 28 | 29 | - include: "validate/predeploy-interface-check.yml" 30 | tags: [ predeploy ] 31 | 32 | - name: "Add/modify VLAN services" 33 | include: "provision/{{ansible_os}}-add.yml" 34 | tags: [ add, config ] 35 | 36 | - name: "Remove decommissioned VLAN services" 37 | include: "provision/{{ansible_os}}-remove.yml" 38 | tags: [ remove, config ] 39 | 40 | - name: "Reset interface facts" 41 | set_fact: 42 | vlan_list: [] 43 | vlan_interfaces: [] 44 | 45 | - name: "Validate deployed VLAN services" 46 | include: "validate/deployed-services.yml" 47 | tags: [ validate ] 48 | -------------------------------------------------------------------------------- /validate/deployed-services.yml: -------------------------------------------------------------------------------- 1 | # 2 | # List of tasks needed to validate deployed services 3 | # 4 | --- 5 | - name: Get VLAN information from the device in common data model 6 | include: "../getinfo/{{ansible_os}}-get-vlans.yml" 7 | 8 | - block: 9 | - name: "Check: service VLANs are configured" 10 | assert: 11 | that: "item.key in vlan_list.keys()" 12 | msg: "VLAN {{item.value}} id {{item.key}} is not configured" 13 | with_dict: "{{data.vlans}}" 14 | 15 | - name: "Check: service VLANs are active" 16 | assert: 17 | that: "vlan_list[item.key].state == 'active'" 18 | msg: "VLAN {{item.value}} id {{item.key}} is not active" 19 | with_dict: "{{data.vlans}}" 20 | 21 | - name: "Check: decomissioned service VLANs are not configured" 22 | assert: 23 | that: "not(item in vlan_list.keys())" 24 | msg: "VLAN {{item}} is still configured on {{inventory_hostname}}" 25 | with_items: "{{data.remove_vlans}}" 26 | when: vlan_list is defined 27 | 28 | - name: Get VLAN information from the device in common data model 29 | include: "../getinfo/{{ansible_os}}-get-interfaces.yml" 30 | 31 | - block: 32 | - name: "Check: Service ports are access interfaces" 33 | assert: 34 | that: "vlan_interfaces[item.key].mode == 'access'" 35 | msg: "Interface {{item.key}} is not a VLAN access interface" 36 | with_dict: "{{data.ports}}" 37 | 38 | - name: "Check: Access VLANs are configured on service ports" 39 | assert: 40 | that: "item.value.vlan == vlan_interfaces[item.key].vlan" 41 | msg: > 42 | VLAN {{item.value.vlan}} is not 43 | access VLAN configured on {{item.key}} 44 | with_dict: "{{data.ports}}" 45 | 46 | - name: "Check: decomissioned ports are not active" 47 | assert: 48 | that: "vlan_interfaces[item].state == 'disabled'" 49 | msg: "Port {{item}} is not disabled on {{inventory_hostname}}" 50 | with_items: "{{data.remove_ports}}" 51 | when: vlan_interfaces is defined 52 | -------------------------------------------------------------------------------- /validate/input-file-format.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Validate correctness of input file(s) 3 | # 4 | --- 5 | - hosts: localhost 6 | name: Validate services.yml with yamllint 7 | gather_facts: no 8 | vars: 9 | - services_file: "{{svcs|default(inventory_dir~'/services.yml')}}" 10 | tasks: 11 | - shell: "yamllint {{services_file}} -c {{inventory_dir~'/.yamllint'}}" 12 | changed_when: False 13 | -------------------------------------------------------------------------------- /validate/predeploy-checks.yml: -------------------------------------------------------------------------------- 1 | # 2 | # List of pre-deployment checks 3 | # 4 | - include: "predeploy-node-check.yml" 5 | - include: "predeploy-vlan-check.yml" 6 | - include: "predeploy-port-check.yml" 7 | - include: "predeploy-interface-check.yml" 8 | -------------------------------------------------------------------------------- /validate/predeploy-interface-check.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Validate interface names in services data model 3 | # 4 | - name: Get VLAN information from the device in common data model 5 | include: "../getinfo/{{ansible_os}}-get-interfaces.yml" 6 | when: not(vlan_interfaces is defined) 7 | 8 | - assert: 9 | that: item.key in vlan_interfaces.keys() 10 | msg: "Interface {{item.key}} is not present on node {{inventory_hostname}}" 11 | with_dict: "{{data.ports}}" 12 | - assert: 13 | that: item in vlan_interfaces.keys() 14 | msg: "Interface {{item}} is not present on node {{inventory_hostname}}" 15 | with_items: "{{data.remove_ports}}" 16 | -------------------------------------------------------------------------------- /validate/predeploy-node-check.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Validate node names in services data model and node reachability 3 | # 4 | - assert: 5 | that: item.1.node in groups['all'] 6 | msg: "Edge switch {{item.1.node}} using VLAN# {{item.0.vlan}} is unknown" 7 | with_subelements: 8 | - "{{services}}" 9 | - ports 10 | run_once: true 11 | -------------------------------------------------------------------------------- /validate/predeploy-port-check.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Check for unique node/port combination using 3 | # a custom plugin 4 | # 5 | - set_fact: 6 | dup_port: "{{services.values()|map(attribute='ports')|list|flatten|dupattr(attr=['node','port'])}}" 7 | - debug: var=dup_port 8 | - assert: 9 | that: not dup_port 10 | msg: "Duplicate port: {{dup_port}}" 11 | run_once: true -------------------------------------------------------------------------------- /validate/predeploy-vlan-check.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Check for unique VLANs using a custom Jinja2 filter plugin 3 | # 4 | - assert: 5 | that: not services|dupattr(attr='vlan') 6 | msg: "Duplicate VLAN: {{services|dupattr(attr='vlan')}}" 7 | run_once: true 8 | -------------------------------------------------------------------------------- /validate/transaction-data.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Validate data supplied in a transaction 3 | # 4 | - assert: 5 | that: transaction.cid is defined 6 | msg: "Customer ID is missing" 7 | - assert: 8 | that: transaction.name is defined 9 | msg: "Customer name is missing" 10 | - assert: 11 | that: "' ' not in transaction.name" 12 | msg: "Customer name should not contain spaces" 13 | - assert: 14 | that: transaction.vlan is defined 15 | msg: "VLAN ID is missing" 16 | - assert: 17 | that: transaction.ports is defined 18 | msg: "List of ports is missing" 19 | - assert: 20 | that: (item.node is defined) and (item.port is defined) 21 | msg: "Node name or interface name is missing in the list of ports" 22 | with_items: "{{transaction.ports}}" 23 | - assert: 24 | that: > 25 | item.site is defined or 26 | (item.state|default('') == 'absent') or 27 | (transaction.state|default('') == 'absent') 28 | msg: "One of the non-absent customer ports has no description" 29 | with_items: "{{transaction.ports}}" 30 | -------------------------------------------------------------------------------- /validate/transaction-node-check.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Validate node names in services data model and node reachability 3 | # 4 | - assert: 5 | that: item.node in groups['all'] 6 | msg: "Edge switch {{item.node}} is unknown" 7 | with_items: "{{transaction.ports}}" 8 | -------------------------------------------------------------------------------- /vlan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: "validate/input-file-format.yml" 3 | 4 | - hosts: all 5 | name: configure VLAN service 6 | tasks: 7 | - include_vars: 8 | file: "{{svcs|default(inventory_dir~'/services.yml')}}" 9 | name: "services" 10 | tags: [ always ] 11 | - include: tests/enable-debugging.yml 12 | 13 | - set_fact: data="{{lookup('template','templates/node-model.j2')}}" 14 | tags: [ always ] 15 | - local_action: > 16 | copy content="{{data|to_nice_json}}" 17 | dest="{{debug_output}}/{{inventory_hostname}}-model.json" 18 | when: debug_output is defined 19 | 20 | - include: "validate/predeploy-checks.yml" 21 | tags: [ predeploy ] 22 | 23 | - name: "Add/modify VLAN services" 24 | include: "provision/{{ansible_os}}-add.yml" 25 | tags: [ add, config ] 26 | 27 | - name: "Remove decommissioned VLAN services" 28 | include: "provision/{{ansible_os}}-remove.yml" 29 | tags: [ remove, config ] 30 | 31 | - name: "Reset interface facts" 32 | set_fact: 33 | vlan_list: [] 34 | vlan_interfaces: [] 35 | 36 | - name: "Validate deployed VLAN services" 37 | include: "validate/deployed-services.yml" 38 | tags: [ validate ] 39 | 40 | - name: "Prepare for cleanup" 41 | include: "provision/cleanup-prepare.yml" 42 | tags: [ cleanup ] 43 | 44 | - name: "Execute cleanup" 45 | include: "provision/{{ansible_os}}-remove.yml" 46 | tags: [ cleanup ] 47 | --------------------------------------------------------------------------------