As a Sales Engineer focused on networks, I have come to learn about the Meraki product line from Cisco. It’s an easy to use, cloud managed infrastructure platform. For many users, it’s exactly what they need. Because it’s web managed it’s good for single task changes but not great for changing dozens or hundreds of preferences at a time. This is where Ansible is good. Ansible is a platform for automating infrastructure, including network equipment. Meraki uses modules to support integration with components including AWS, Cisco equipment, and Linux servers. Ansible lacked support for integrating with Meraki equipment so I took on the task of developing modules within Ansible’s codebase.
The first module was merged in Ansible 2.6. As of Ansible 2.7, there are 11 modules included. This post focuses on the modules more from an architecture level of how they’re structured and where I want to take them.
Up to date source code is available for viewing from the Ansible Github page in the Meraki modules path.
Module Flow
All Meraki modules have a relatively similar flow. First, parameters for use in playbooks are defined. Meraki requires these to be structured as a dictionary or nested dictionary. Each entry has numerous properties including type, aliases, and choices. For example, here is a truncated parameter definition for the meraki_admin
module.
argument_spec = meraki_argument_spec()
argument_spec.update(
state=dict(type='str', choices=['present', 'query', 'absent'], required=True),
name=dict(type='str'),
email=dict(type='str'),
Next, regular routine tasks, such as module registration occur.
Ansible’s Meraki modules use a data structure to define URLs. I call them a URL catalog. Each module has a built in, defined function. In the case of meraki_admin
the function would be admin
. Then a data structure is built which maps URLs to functions. A central URL catalog is stored in the Meraki module utility (more on that later) and the module URLs are registered against that.
query_urls = {'admin': '/organizations/{org_id}/admins',
}
create_urls = {'admin': '/organizations/{org_id}/admins',
}
update_urls = {'admin': '/organizations/{org_id}/admins/',
}
revoke_urls = {'admin': '/organizations/{org_id}/admins/',
}
meraki.url_catalog['query'] = query_urls
meraki.url_catalog['create'] = create_urls
meraki.url_catalog['update'] = update_urls
meraki.url_catalog['revoke'] = revoke_urls
Some modules do parameter checks. For example, the modules all support organization ID (org_id
) and organization name (org_name
). Both cannot be defined so a module may check if both are provided and fail if so.
All the modules that accept org_name
or net_name
will convert them into ID numbers as ID’s are used in Meraki’s API calls because names are just metadata. Once the IDs are discovered, the module standup is completed and the real work begins.
Almost all modules support either creation or editing (present
), deletion(absent
), or querying information (query
) within the state
parameter. The value of state
is what dictates the tasks. Some modules create payloads either for submission to Meraki or for idempotency checks. Each query relies on the Meraki module utility.
Meraki Module Utility
Ansible’s module utilities are a place to store shared code between modules. For Meraki, each module needs to submit requests to Meraki and store URLs. These functions are stored in the module utility. The module utility has a request()
method which is responsible for submitting the request to Meraki and returning information to the module. One of the functions provided by the utility is construct_path()
. construct_path
will use information provided to it, such as a net_id
, and the action to be taken (ex. create
) and construct the URL. The URL is passed to the request
method along with the HTTP method and optional payload.
r = meraki.request(path,
method='POST',
payload=json.dumps(payload)
)
Depending on the HTTP returned status, the changed
status is set. changed
is what Ansible uses to set color on the output and change statistics.
Idempotency
Ansible assumes all actions are idempotent. To ensure idempotency, the Meraki modules do manual checks against current state data to see if a request is needed. If the payload and current state are identical, no call is made. Idempotency checks are the most complicated and fickle parts of the module. Dictionary keys need to be translated to match Meraki’s. Certain items need to be skipped when comparing. Idempotency checks are one area I’d like to improve in the future and possibly get away from if possible.
Currently however, the Meraki module utility provides an is_update_required()
method which compares the current state and proposed state data structures. An optional_ignore
parameter can be passed to the method if certain keys need to be skipped for that execution. A basic check of is_update_required
may look like
if meraki.is_update_required(original, payload) is True:
...
If additional keys need to be ignored, such as in the meraki_vlan
module, these keys are passed as a list.
ignored = ['networkId']
if meraki.is_update_required(original, payload, optional_ignore=ignored):
...
Idempotency checking will break when Meraki adds parameters to a data structure. The good news is this would trigger the request to be submitted, so it will still work as expected, just with incorrect changed
status. Removing the idempotency checks would greatly simply the modules. Unfortunately, the only way I think it can be removed is if Meraki returns different HTTP return codes depending on whether anything changed. Meraki doesn’t show changed and unchanged return values in their API documentation. They may make this change in a future API version, but i don’t expect the change to happen soon.
Future
Besides removing idempotency checks and adding new modules, I have a few improvements I want to make. Most notably, I’d like to add a Meraki connection plugin which allows for rate limiting since it stays resident through the entire playbook execution and isn’t purged for each task. The Meraki API allows for 5 requests, per organization, per second. I’ve given some thought on how to implement this in a relatively straightforward way. My current plan is to create a dictionary whose key is the organization ID. Each value would be a list which has timestamps. At the beginning of each request, it would lookup the oldest timestamp. If the timestamp is older than a second, it removes it. If there are 5 timestamps, execution will pause for .5 seconds, prune timestamps older than a second, and resubmit. At some point I plan on creating the connection plugin, but for now it’s up to the module utility.
Conclusion
In my next post, I will document high level playbook development using the Meraki modules.