How to write an Ansible module for Velocloud

Share on:

In this post, we’re going to cover the different steps involved to create a basic Ansible module for the Velocloud SDWAN Solution.

VCO

As an example, we’ll start with a quite basic module: how to configure the static routing at the Edge Level.

VMware provides a very good API publicly available at http://code.vmware.com

Let’s start with the basic structure of an Ansible module. As you may know, modules are generally written in Python. Basically, we need to import the Ansible python libraries and use the AnsibleModule function as the entry point of our module.

This module uses a dictionary structure to define the arguments list of the Ansible module. In our example, we’ll need some information about the Velocloud Orchestrator to connect to, the tenant and edge that we want to configure, and some information about the static route (subnet, prefix, nexthop, interface…). Finally, a state argument is used by the module to specify if the route should be created or deleted.

Here is the basic module with these arguments:

 1from ansible.module_utils.basic import AnsibleModule
 2import urllib3
 3 
 4def main():
 5    urllib3.disable_warnings()
 6    module = AnsibleModule(
 7        argument_spec = dict(
 8            host                = dict(required=True),
 9            username            = dict(required=True),
10            password            = dict(required=True),
11            operator            = dict(required=True, type='bool'),
12            enterprise          = dict(required=True),
13            edge                = dict(required=True),
14            state               = dict(default='present', choices=['present', 'absent']),
15            subnet              = dict(required=True),
16            prefix              = dict(required=True),
17            nexthop             = dict(required=True),
18            interface           = dict(required=True),
19            cost                = dict(default=0, type='int'),
20            advertise           = dict(default=True, type='bool'),
21            preferred           = dict(default=True, type='bool'),
22            description         = dict(required=False)
23        )
24    )
25 
26if __name__ == '__main__':
27    main()

Now that we have our Ansible module, let’s code the Velocloud logic to be able to configure some static routes on a Edge, we need to go through these steps:

get the tenant ID
get the edge ID within this tenant
get the “DeviceSettings” configuration module for this edge
modify this module to create/modify/delete the routes

First, we need a client to be able to connect to the Velocloud SDWAN API. We use this client: https://code.vmware.com/samples/5554/velocloud-orchestrator-json-rpc-api-client—python

This client is straightforward to use. We just have to create the client by calling the VcoRequestManager class and use the “authenticate” method with correct credentials

Here is the code we use, just after the AnsibleModule function initialization:

1try:
2        client = VcoRequestManager(hostname=module.params['host'], verify_ssl=False)
3        client.authenticate(module.params['username'], module.params['password'], is_operator=module.params['operator'])
4    except ApiException as e:
5        module.fail_json(msg=e)

Once authenticated, we need to retrieve the enterprise Id. Let’s define a function to get this Id:

1def get_enteprise(client, module):
2    params = { "networkId": 1, "with": [] }
3    enterprises = client.call_api('/network/getNetworkEnterprises', params)
4 
5    for enterprise in enterprises:
6            if enterprise['name'] == module.params['enterprise']:
7                return enterprise
8                break
9    module.fail_json(msg='Cant find enterprise')

Then, we need to get the Edge Id:

1def get_edge(client, module, ent_id):
2    params = { "enterpriseId": ent_id, "with": [] }
3    edges = client.call_api('/enterprise/getEnterpriseEdges', params)
4 
5    for edge in edges:
6            if edge['name'] == module.params['edge']:
7                return edge
8                break
9    module.fail_json(msg='Cant find edge') 

Then, the “deviceSettings” module ID for this Edge:

1def get_device_module(client, module, ent_id, edge_id):
2    params = { 'enterpriseId': ent_id, 'edgeId': edge_id }
3    modules = client.call_api('/edge/getEdgeConfigurationStack', params)
4 
5    for module in modules[0]['modules']:
6            if module['name'] == 'deviceSettings':
7                return module
8                break
9    module.fail_json(msg='Cant find device settings module')

And finally, we can modify this module to create/update/delete the routes:

 1def add_static_route(client, module, ent_id, dev):
 2    route = {
 3        "destination"       : module.params['subnet'],
 4        "gateway"           : module.params['nexthop'],
 5        "cidrPrefix"        : module.params['prefix'],
 6        "subinterfaceId"    : -1,
 7        "wanInterface"      : module.params['interface'],
 8        "cost"              : module.params['cost'],
 9        "advertise"         : module.params['advertise'],
10        "preferred"         : module.params['preferred'],
11        "description"       : module.params['description'],
12        "icmpProbeLogicalId": None,
13        "sourceIp"          : None,
14        "vlanId"            : None
15    }
16     
17    # Check if route is already here
18    existing_routes = dev['data']['segments'][0]['routes']['static']
19    index = 0
20    for existing_route in existing_routes:
21        if existing_route['destination'] == module.params['subnet']:
22            if module.params['state'] == 'present':
23                # Check if route needs to be changed
24                if existing_route != route:
25                    dev['data']['segments'][0]['routes']['static'][index] = route
26                    params = { 'enterpriseId': ent_id, 'id': dev['id'], '_update': dev }
27                    update = client.call_api('/configuration/updateConfigurationModule', params)
28 
29                    module.exit_json(changed=True, argument_spec=module.params, meta=existing_route)
30                else:
31                    module.exit_json(changed=False, argument_spec=module.params, meta=existing_route)
32                break
33        index = index+1
34 
35    if module.params['state'] == 'present':
36        dev['data']['segments'][0]['routes']['static'].append(route)
37    else:
38        dev['data']['segments'][0]['routes']['static'].remove(route)
39 
40    params = { 'enterpriseId': ent_id, 'id': dev['id'], '_update': dev }
41    update = client.call_api('/configuration/updateConfigurationModule', params)
42 
43    return update

Basically, we compare the desired static route against the existing one. If no route exists and ‘state=present’, we can create the route. If the route exists, is not the same one, and ‘state=present’, we can update the route. Finally, if ‘state=absent’, we delete the route.

We just have to update the main function to implement the whole logic and it’s done!

1ent = get_enteprise(client, module)
2edge = get_edge(client, module, ent['id'])
3dev = get_device_module(client, module, ent['id'], edge['id'])
4route = add_static_route(client, module, ent['id'], dev)
5 
6module.exit_json(changed=True, argument_spec=module.params, meta=route)

Here is the final code:

  1from ansible.module_utils.basic import AnsibleModule
  2import urllib3
  3 
  4import client
  5from client import *
  6 
  7def get_enteprise(client, module):
  8    params = { "networkId": 1, "with": [] }
  9    enterprises = client.call_api('/network/getNetworkEnterprises', params)
 10 
 11    for enterprise in enterprises:
 12            if enterprise['name'] == module.params['enterprise']:
 13                return enterprise
 14                break
 15    module.fail_json(msg='Cant find enterprise')
 16 
 17def get_edge(client, module, ent_id):
 18    params = { "enterpriseId": ent_id, "with": [] }
 19    edges = client.call_api('/enterprise/getEnterpriseEdges', params)
 20 
 21    for edge in edges:
 22            if edge['name'] == module.params['edge']:
 23                return edge
 24                break
 25    module.fail_json(msg='Cant find edge')    
 26 
 27def get_device_module(client, module, ent_id, edge_id):
 28    params = { 'enterpriseId': ent_id, 'edgeId': edge_id }
 29    modules = client.call_api('/edge/getEdgeConfigurationStack', params)
 30 
 31    for module in modules[0]['modules']:
 32            if module['name'] == 'deviceSettings':
 33                return module
 34                break
 35    module.fail_json(msg='Cant find device settings module')
 36 
 37def add_static_route(client, module, ent_id, dev):
 38    route = {
 39        "destination"       : module.params['subnet'],
 40        "gateway"           : module.params['nexthop'],
 41        "cidrPrefix"        : module.params['prefix'],
 42        "subinterfaceId"    : -1,
 43        "wanInterface"      : module.params['interface'],
 44        "cost"              : module.params['cost'],
 45        "advertise"         : module.params['advertise'],
 46        "preferred"         : module.params['preferred'],
 47        "description"       : module.params['description'],
 48        "icmpProbeLogicalId": None,
 49        "sourceIp"          : None,
 50        "vlanId"            : None
 51    }
 52     
 53    # Check if route is already here
 54    existing_routes = dev['data']['segments'][0]['routes']['static']
 55    index = 0
 56    for existing_route in existing_routes:
 57        if existing_route['destination'] == module.params['subnet']:
 58            if module.params['state'] == 'present':
 59                # Check if route needs to be changed
 60                if existing_route != route:
 61                    dev['data']['segments'][0]['routes']['static'][index] = route
 62                    params = { 'enterpriseId': ent_id, 'id': dev['id'], '_update': dev }
 63                    update = client.call_api('/configuration/updateConfigurationModule', params)
 64 
 65                    module.exit_json(changed=True, argument_spec=module.params, meta=existing_route)
 66                else:
 67                    module.exit_json(changed=False, argument_spec=module.params, meta=existing_route)
 68                break
 69        index = index+1
 70 
 71    if module.params['state'] == 'present':
 72        dev['data']['segments'][0]['routes']['static'].append(route)
 73    else:
 74        dev['data']['segments'][0]['routes']['static'].remove(route)
 75 
 76    params = { 'enterpriseId': ent_id, 'id': dev['id'], '_update': dev }
 77    update = client.call_api('/configuration/updateConfigurationModule', params)
 78 
 79    return update
 80 
 81def main():
 82    urllib3.disable_warnings()
 83    module = AnsibleModule(
 84        argument_spec = dict(
 85            host                = dict(required=True),
 86            username            = dict(required=True),
 87            password            = dict(required=True),
 88            operator            = dict(required=True, type='bool'),
 89            enterprise          = dict(required=True),
 90            edge                = dict(required=True),
 91            state               = dict(default='present', choices=['present', 'absent']),
 92            subnet              = dict(required=True),
 93            prefix              = dict(required=True),
 94            nexthop             = dict(required=True),
 95            interface           = dict(required=True),
 96            cost                = dict(default=0, type='int'),
 97            advertise           = dict(default=True, type='bool'),
 98            preferred           = dict(default=True, type='bool'),
 99            description         = dict(required=False)
100        )
101    )
102 
103    try:
104        client = VcoRequestManager(hostname=module.params['host'], verify_ssl=False)
105        client.authenticate(module.params['username'], module.params['password'], is_operator=module.params['operator'])
106    except ApiException as e:
107        module.fail_json(msg=e)
108 
109    ent = get_enteprise(client, module)
110    edge = get_edge(client, module, ent['id'])
111    dev = get_device_module(client, module, ent['id'], edge['id'])
112    route = add_static_route(client, module, ent['id'], dev)
113 
114    module.exit_json(changed=True, argument_spec=module.params, meta=route)
115 
116 
117if __name__ == '__main__':
118    main()

For testing, just create a basic Ansible Playbook, such this Yaml File:

 1---
 2- name: Velocloud Ansible Module
 3  hosts: localhost
 4  gather_facts: False
 5  tasks:
 6    - name: Static Routes
 7      velocloud_static_route:
 8        host: '****'
 9        username: '****'
10        password: '****'
11        operator: True
12        enterprise: 'test-tenant'
13        edge: 'test-edge'
14        subnet: "{{ item.subnet }}"
15        prefix: "{{ item.prefix }}"
16        nexthop: "{{ item.nexthop }}"
17        interface: "{{ item.interface }}"
18        advertise: "{{ item.advertise }}"
19        description: "{{ item.description }}"
20        state: absent
21      with_items:
22        - {subnet: '10.0.0.0', prefix: '24', nexthop: '20.0.0.2', interface: 'GE3', advertise: True, description: 'route1'}
23        - {subnet: '20.0.0.0', prefix: '24', nexthop: '20.0.0.1', interface: 'GE3', advertise: False, description: 'route2'}
24        - {subnet: '30.0.0.0', prefix: '24', nexthop: '20.0.0.3', interface: 'GE3', advertise: False, description: 'route3'}
25        - {subnet: '40.0.0.0', prefix: '24', nexthop: '20.0.0.1', interface: 'GE3', advertise: True, description: 'route4'}

And test it with an ansible-playbook command:

 1ansible-playbook test.yaml
 2 
 3PLAY [Velocloud Ansible Module] **********************************************************************************************************************************************
 4 
 5TASK [Static Routes] *********************************************************************************************************************************************************
 6changed: [localhost] => (item={u'subnet': u'10.0.0.0', u'prefix': u'24', u'description': u'route1', u'interface': u'GE3', u'advertise': True, u'nexthop': u'20.0.0.2'})
 7changed: [localhost] => (item={u'subnet': u'20.0.0.0', u'prefix': u'24', u'description': u'route2', u'interface': u'GE3', u'advertise': False, u'nexthop': u'20.0.0.1'})
 8changed: [localhost] => (item={u'subnet': u'30.0.0.0', u'prefix': u'24', u'description': u'route3', u'interface': u'GE3', u'advertise': False, u'nexthop': u'20.0.0.3'})
 9changed: [localhost] => (item={u'subnet': u'40.0.0.0', u'prefix': u'24', u'description': u'route4', u'interface': u'GE3', u'advertise': True, u'nexthop': u'20.0.0.1'})
10[WARNING]: Module did not set no_log for password
11 
12PLAY RECAP *******************************************************************************************************************************************************************
13localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

DISCLAIMER: The views and opinions expressed on this blog are our own and may not reflect the views and opinions of our employer