├── .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 |
--------------------------------------------------------------------------------