Meraki - Convert response keys to snake_case from camelCase (#53891)

* Initial proposal for new parameter option for response format
- output_version parameter dictates the response key case
- new is snake_case, old is camelCase
- If new, conversion is done at the end of module execution
- This is purely a proposal and not a final draft

* Add support for ANSIBLE_MERAKI_FORMAT env var
- If env var is set to 'camelcase' it will output camelcase
- Otherwise, will default to snakecase
- Added note to documentation fragment
- As of now, all module documentation needs to be updated

* Fix pep8 errors and remove output_version args

* Restructure check in exit_json so it actually works

* Add changelog fragment

* Change output_format to a parameter with env var fallback
- ANSIBLE_MERAKI_FORMAT is the valid env var
- Added documentation

* Convert to camel_dict_to_snake_dict() which is from Ansible
- Fixed integration tests

* Fix yaml lint error

* exit_json camel_case conversion handles no data
- exit_json would fail if data wasn't provided
- Updated 3 integration tests for new naming convention

* convert_camel_to_snake() handles lists and dicts
- The native Ansible method doesn't handle first level lists
- convert_camel_to_snake() acts simply as a wrapper for the method
- There maybe a situation where nested lists are a problem, must test
- Fixed integration tests in some modules

* A few integration test fixes

* Convert response documentation to snake case
This commit is contained in:
Kevin Breit 2019-06-13 14:07:30 -05:00 committed by Nathaniel Case
parent 5243cba0b2
commit a85750dc98
16 changed files with 141 additions and 93 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- Meraki modules now return data in snake_case instead of camelCase. The ANSIBLE_MERAKI_FORMAT environment variable can be set to camelcase to revert back to camelcase until deprecation in Ansible 2.13.

View file

@ -30,7 +30,9 @@
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import re
from ansible.module_utils.basic import AnsibleModule, json, env_fallback
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils._text import to_native, to_bytes, to_text
@ -42,6 +44,7 @@ def meraki_argument_spec():
use_proxy=dict(type='bool', default=False),
use_https=dict(type='bool', default=True),
validate_certs=dict(type='bool', default=True),
output_format=dict(type='str', choices=['camelcase', 'snakecase'], default='snakecase', fallback=(env_fallback, ['ANSIBLE_MERAKI_FORMAT'])),
output_level=dict(type='str', default='normal', choices=['normal', 'debug']),
timeout=dict(type='int', default=30),
org_name=dict(type='str', aliases=['organization']),
@ -62,6 +65,7 @@ class MerakiModule(object):
self.org_id = None
self.net_id = None
self.check_mode = module.check_mode
self.key_map = {}
# normal output
self.existing = None
@ -130,16 +134,25 @@ class MerakiModule(object):
else:
self.params['protocol'] = 'http'
def sanitize(self, original, proposed):
"""Determine which keys are unique to original"""
keys = []
for k, v in original.items():
try:
if proposed[k] and k not in self.ignored_keys:
pass
except KeyError:
keys.append(k)
return keys
def sanitize_keys(self, data):
if isinstance(data, dict):
items = {}
for k, v in data.items():
try:
new = {self.key_map[k]: data[k]}
items[self.key_map[k]] = self.sanitize_keys(data[k])
except KeyError:
snake_k = re.sub('([a-z0-9])([A-Z])', r'\1_\2', k).lower()
new = {snake_k: data[k]}
items[snake_k] = self.sanitize_keys(data[k])
return items
elif isinstance(data, list):
items = []
for i in data:
items.append(self.sanitize_keys(i))
return items
elif isinstance(data, int) or isinstance(data, str) or isinstance(data, float):
return data
def is_update_required(self, original, proposed, optional_ignore=None):
''' Compare two data-structures '''
@ -269,6 +282,20 @@ class MerakiModule(object):
return template['id']
self.fail_json(msg='No configuration template named {0} found'.format(name))
def convert_camel_to_snake(self, data):
"""
Converts a dictionary or list to snake case from camel case
:type data: dict or list
:return: Converted data structure, if list or dict
"""
if isinstance(data, dict):
return camel_dict_to_snake_dict(data, ignore_list=('tags', 'tag'))
elif isinstance(data, list):
return [camel_dict_to_snake_dict(item, ignore_list=('tags', 'tag')) for item in data]
else:
return data
def construct_params_list(self, keys, aliases=None):
qs = {}
for key in keys:
@ -344,8 +371,15 @@ class MerakiModule(object):
if self.params['output_level'] == 'debug':
self.result['method'] = self.method
self.result['url'] = self.url
self.result.update(**kwargs)
if self.params['output_format'] == 'camelcase':
self.module.deprecate("Update your playbooks to support snake_case format instead of camelCase format.", version=2.13)
else:
if 'data' in self.result:
try:
self.result['data'] = self.convert_camel_to_snake(self.result['data'])
except (KeyError, AttributeError):
pass
self.module.exit_json(**self.result)
def fail_json(self, msg, **kwargs):

View file

@ -193,22 +193,22 @@ data:
returned: success
type: str
sample: John Doe
accountStatus:
account_status:
description: Status of account.
returned: success
type: str
sample: ok
twoFactorAuthEnabled:
two_factor_auth_enabled:
description: Enabled state of two-factor authentication for administrator.
returned: success
type: bool
sample: false
hasApiKey:
has_api_key:
description: Defines whether administrator has an API assigned to their account.
returned: success
type: bool
sample: false
lastActive:
last_active:
description: Date and time of time the administrator was active within Dashboard.
returned: success
type: str
@ -243,7 +243,7 @@ data:
returned: when tag permissions are set
type: str
sample: full
orgAccess:
org_access:
description: The privilege of the dashboard administrator on the organization. Options are 'full', 'read-only', or 'none'.
returned: success
type: str

View file

@ -154,7 +154,7 @@ data:
returned: success
type: str
sample: YourNet
organizationId:
organization_id:
description: Organization ID which owns the network.
returned: success
type: str
@ -164,7 +164,7 @@ data:
returned: success
type: str
sample: " production wireless "
timeZone:
time_zone:
description: Timezone where network resides.
returned: success
type: str
@ -174,7 +174,7 @@ data:
returned: success
type: str
sample: switch
disableMyMerakiCom:
disable_my_meraki_com:
description: States whether U(my.meraki.com) and other device portals should be disabled.
returned: success
type: bool

View file

@ -117,32 +117,32 @@ data:
returned: success
type: str
sample: 16100
v2cEnabled:
v2c_enabled:
description: Shows enabled state of SNMPv2c
returned: success
type: bool
sample: true
v3Enabled:
v3_enabled:
description: Shows enabled state of SNMPv3
returned: success
type: bool
sample: true
v3AuthMode:
v3_auth_mode:
description: The SNMP version 3 authentication mode either MD5 or SHA.
returned: success
type: str
sample: SHA
v3PrivMode:
v3_priv_mode:
description: The SNMP version 3 privacy mode DES or AES128.
returned: success
type: str
sample: AES128
v2CommunityString:
v2_community_string:
description: Automatically generated community string for SNMPv2c.
returned: When SNMPv2c is enabled.
type: str
sample: o/8zd-JaSb
v3User:
v3_user:
description: Automatically generated username for SNMPv3.
returned: When SNMPv3c is enabled.
type: str

View file

@ -294,17 +294,17 @@ data:
returned: success
type: bool
sample: true
splashPage:
splash_page:
description: Splash page to show when user authenticates.
returned: success
type: str
sample: Click-through splash page
ssidAdminAccessible:
ssid_admin_accessible:
description: Whether SSID is administratively accessible.
returned: success
type: bool
sample: true
authMode:
auth_mode:
description: Authentication method.
returned: success
type: str
@ -314,37 +314,37 @@ data:
returned: success
type: str
sample: SecretWiFiPass
encryptionMode:
encryption_mode:
description: Wireless traffic encryption method.
returned: success
type: str
sample: wpa
wpaEncryptionMode:
wpa_encryption_mode:
description: Enabled WPA versions.
returned: success
type: str
sample: WPA2 only
ipAssignmentMode:
ip_assignment_mode:
description: Wireless client IP assignment method.
returned: success
type: str
sample: NAT mode
minBitrate:
min_bitrate:
description: Minimum bitrate a wireless client can connect at.
returned: success
type: int
sample: 11
bandSelection:
band_selection:
description: Wireless RF frequency wireless network will be broadcast on.
returned: success
type: str
sample: 5 GHz band only
perClientBandwidthLimitUp:
per_client_bandwidth_limit_up:
description: Maximum upload bandwidth a client can use.
returned: success
type: int
sample: 1000
perClientBandwidthLimitDown:
per_client_bandwidth_limit_down:
description: Maximum download bandwidth a client can use.
returned: success
type: int

View file

@ -192,7 +192,7 @@ data:
returned: success
type: bool
sample: true
poeEnabled:
poe_enabled:
description: Power Over Ethernet enabled state of port.
returned: success
type: bool
@ -207,32 +207,32 @@ data:
returned: success
type: int
sample: 10
voiceVlan:
voice_vlan:
description: VLAN assigned to port with voice VLAN enabled devices.
returned: success
type: int
sample: 20
isolationEnabled:
isolation_enabled:
description: Port isolation status of port.
returned: success
type: bool
sample: true
rstpEnabled:
rstp_enabled:
description: Enabled or disabled state of Rapid Spanning Tree Protocol (RSTP)
returned: success
type: bool
sample: true
stpGuard:
stp_guard:
description: State of STP guard
returned: success
type: str
sample: "Root Guard"
accessPolicyNumber:
access_policy_number:
description: Number of assigned access policy. Only applicable to access ports.
returned: success
type: int
sample: 1234
linkNegotiation:
link_negotiation:
description: Link speed for the port.
returned: success
type: str

View file

@ -138,7 +138,7 @@ response:
returned: success
type: complex
contains:
applianceIp:
appliance_ip:
description: IP address of Meraki appliance in the VLAN
returned: success
type: str
@ -148,7 +148,7 @@ response:
returned: success
type: str
sample: upstream_dns
fixedIpAssignments:
fixed_ip_assignments:
description: List of MAC addresses which have IP addresses assigned.
returned: success
type: complex
@ -168,7 +168,7 @@ response:
returned: success
type: str
sample: fixed_ip
reservedIpRanges:
reserved_ip_ranges:
description: List of IP address ranges which are reserved for static assignment.
returned: success
type: complex
@ -208,32 +208,32 @@ response:
returned: success
type: str
sample: "192.0.1.0/24"
dhcpHandling:
dhcp_handling:
description: Status of DHCP server on VLAN.
returned: success
type: str
sample: Run a DHCP server
dhcpLeaseTime:
dhcp_lease_time:
description: DHCP lease time when server is active.
returned: success
type: str
sample: 1 day
dhcpBootOptionsEnabled:
dhcp_boot_options_enabled:
description: Whether DHCP boot options are enabled.
returned: success
type: bool
sample: no
dhcpBootNextServer:
dhcp_boot_next_server:
description: DHCP boot option to direct boot clients to the server to load the boot file from.
returned: success
type: str
sample: 192.0.1.2
dhcpBootFilename:
dhcp_boot_filename:
description: Filename for boot file.
returned: success
type: str
sample: boot.txt
dhcpOptions:
dhcp_options:
description: DHCP options.
returned: success
type: complex

View file

@ -10,6 +10,8 @@ class ModuleDocFragment(object):
notes:
- More information about the Meraki API can be found at U(https://dashboard.meraki.com/api_docs).
- Some of the options are likely only used for developers within Meraki.
- As of Ansible 2.9, Meraki modules output keys as snake case. To use camel case, set the C(ANSIBLE_MERAKI_FORMAT) environment variable to C(camelcase).
- Ansible's Meraki modules will stop supporting camel case output in Ansible 2.13. Please update your playbooks.
options:
auth_key:
description:
@ -32,6 +34,12 @@ options:
- Only useful for internal Meraki developers.
type: bool
default: yes
output_format:
description:
- Instructs module whether response keys should be snake case (ex. C(net_id)) or camel case (ex. C(netId)).
type: str
choices: [snakecase, camelcase]
default: snakecase
output_level:
description:
- Set amount of debug output during module execution.

View file

@ -49,7 +49,7 @@
- assert:
that:
- single_allowed_check.data.allowedUrlPatterns | length == 1
- single_allowed_check.data.allowed_url_patterns | length == 1
- single_allowed_check is changed
- name: Set single allowed URL pattern
@ -64,7 +64,7 @@
- assert:
that:
- single_allowed.data.allowedUrlPatterns | length == 1
- single_allowed.data.allowed_url_patterns | length == 1
- name: Set single allowed URL pattern for idempotency with check mode
meraki_content_filtering:
@ -83,6 +83,7 @@
- assert:
that:
- single_allowed_idempotent_check is not changed
- single_allowed.data.allowed_url_patterns | length == 1
- name: Set single allowed URL pattern for idempotency
meraki_content_filtering:
@ -117,7 +118,7 @@
- assert:
that:
- single_blocked.data.blockedUrlPatterns | length == 1
- single_blocked.data.blocked_url_patterns | length == 1
- name: Set two allowed URL pattern
meraki_content_filtering:
@ -136,7 +137,7 @@
- assert:
that:
- two_allowed.changed == True
- two_allowed.data.allowedUrlPatterns | length == 2
- two_allowed.data.allowed_url_patterns | length == 2
- name: Set blocked URL category
meraki_content_filtering:
@ -155,8 +156,8 @@
- assert:
that:
- blocked_category.changed == True
- blocked_category.data.blockedUrlCategories | length == 1
- blocked_category.data.urlCategoryListSize == "fullList"
- blocked_category.data.blocked_url_categories | length == 1
- blocked_category.data.url_category_list_size == "fullList"
- name: Set blocked URL category with top sites
meraki_content_filtering:
@ -175,8 +176,8 @@
- assert:
that:
- blocked_category.changed == True
- blocked_category.data.blockedUrlCategories | length == 1
- blocked_category.data.urlCategoryListSize == "topSites"
- blocked_category.data.blocked_url_categories | length == 1
- blocked_category.data.url_category_list_size == "topSites"
always:
- name: Reset policies

View file

@ -81,7 +81,7 @@
- assert:
that:
- create_one.data|length == 2
- create_one.data.0.destCidr == '192.0.1.1/32'
- create_one.data.0.dest_cidr == '192.0.1.1/32'
- create_one.data.0.protocol == 'tcp'
- create_one.data.0.policy == 'deny'
- create_one.changed == True
@ -165,7 +165,7 @@
- assert:
that:
- query.data.1.syslogEnabled == True
- query.data.1.syslog_enabled == True
- default_syslog.changed == True
- name: Disable syslog for default rule
@ -207,7 +207,7 @@
- assert:
that:
- query.data.1.syslogEnabled == False
- query.data.1.syslog_enabled == False
- disable_syslog.changed == True
always:

View file

@ -198,7 +198,7 @@
- assert:
that:
- disable_remote_status.data.disableRemoteStatusPage == False
- disable_remote_status.data.disable_remote_status_page == False
- name: Disable remote status page
meraki_network:
@ -215,7 +215,7 @@
- assert:
that:
- enable_remote_status.data.disableRemoteStatusPage == True
- enable_remote_status.data.disable_remote_status_page == True
- name: Test status pages are mutually exclusive when on
meraki_network:
@ -300,15 +300,15 @@
assert:
that:
- create_net_combined.data.type == 'combined'
- create_net_combined.data.disableMyMerakiCom == True
- enable_meraki_com.data.disableMyMerakiCom == False
- create_net_combined.data.disable_my_meraki_com == True
- enable_meraki_com.data.disable_my_meraki_com == False
- '"org_name or org_id parameters are required" in create_net_no_org.msg'
- '"IntTestNetworkAppliance" in create_net_appliance_no_tz.data.name'
- create_net_appliance_no_tz.changed == True
- '"IntTestNetworkSwitch" in create_net_switch.data.name'
- '"IntTestNetworkSwitchOrgID" in create_net_switch_org_id.data.name'
- '"IntTestNetworkWireless" in create_net_wireless.data.name'
- create_net_wireless.data.disableMyMerakiCom == True
- create_net_wireless.data.disable_my_meraki_com == True
- create_net_wireless_idempotent.changed == False
- create_net_wireless_idempotent.data is defined
- '"first_tag" in create_net_tag.data.tags'

View file

@ -33,8 +33,8 @@
- assert:
that:
- snmp_v2_enable.data.v2CommunityString is defined
- snmp_v2_enable.data.v2cEnabled == true
- snmp_v2_enable.data.v2_community_string is defined
- snmp_v2_enable.data.v2c_enabled == true
- name: Disable SNMPv2c
meraki_snmp:
@ -47,8 +47,8 @@
- assert:
that:
- snmp_v2_disable.data.v2CommunityString is not defined
- snmp_v2_disable.data.v2cEnabled == False
- snmp_v2_disable.data.v2_community_string is not defined
- snmp_v2_disable.data.v2c_enabled == False
- name: Enable SNMPv2c with org_id
meraki_snmp:
@ -64,8 +64,8 @@
- assert:
that:
- snmp_v2_enable_id.data.v2CommunityString is defined
- snmp_v2_enable_id.data.v2cEnabled == true
- snmp_v2_enable_id.data.v2_community_string is defined
- snmp_v2_enable_id.data.v2c_enabled == true
- name: Disable SNMPv2c with org_id
meraki_snmp:
@ -78,8 +78,8 @@
- assert:
that:
- snmp_v2_disable_id.data.v2CommunityString is not defined
- snmp_v2_disable_id.data.v2cEnabled == False
- snmp_v2_disable_id.data.v2_community_string is not defined
- snmp_v2_disable_id.data.v2c_enabled == False
- name: Enable SNMPv3
meraki_snmp:
@ -96,7 +96,7 @@
- assert:
that:
- snmp_v3_enable.data.v3Enabled == True
- snmp_v3_enable.data.v3_enabled == True
- snmp_v3_enable.changed == True
- name: Check for idempotency
@ -139,7 +139,7 @@
- assert:
that:
- peers.data.peerIps is defined
- peers.data.peer_ips is defined
- name: Add invalid peer IPs
meraki_snmp:

View file

@ -229,9 +229,9 @@
- assert:
that:
- psk.data.authMode == 'psk'
- psk.data.encryptionMode == 'wpa'
- psk.data.wpaEncryptionMode == 'WPA2 only'
- psk.data.auth_mode == 'psk'
- psk.data.encryption_mode == 'wpa'
- psk.data.wpa_encryption_mode == 'WPA2 only'
- name: Set PSK with idempotency
meraki_ssid:
@ -269,7 +269,7 @@
- assert:
that:
- splash_click.data.splashPage == 'Click-through splash page'
- splash_click.data.splash_page == 'Click-through splash page'
- name: Configure RADIUS servers
meraki_ssid:
@ -291,7 +291,7 @@
- assert:
that:
- set_radius_server.data.radiusServers.0.host == '192.0.1.200'
- set_radius_server.data.radius_servers.0.host == '192.0.1.200'
- name: Configure RADIUS servers with idempotency
meraki_ssid:

View file

@ -194,7 +194,7 @@
- assert:
that:
- update_port_vvlan.data.voiceVlan == 11
- update_port_vvlan.data.voice_vlan == 11
- update_port_vvlan.changed == True
- name: Check access port for idempotenty
@ -242,7 +242,7 @@
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowedVlans == 'all'
- update_trunk.data.allowed_vlans == 'all'
- name: Configure trunk port with specific VLANs
meraki_switchport:
@ -269,7 +269,7 @@
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowedVlans == '8,10,15,20'
- update_trunk.data.allowed_vlans == '8,10,15,20'
- name: Configure trunk port with specific VLANs and native VLAN
meraki_switchport:
@ -296,7 +296,7 @@
that:
- update_trunk.data.tags == 'server'
- update_trunk.data.type == 'trunk'
- update_trunk.data.allowedVlans == '2,10,15,20'
- update_trunk.data.allowed_vlans == '2,10,15,20'
- name: Check for idempotency on trunk port
meraki_switchport:

View file

@ -20,7 +20,7 @@
register: invalid_domain
ignore_errors: yes
- name: Disable HTTP
- name: Disable HTTPS
meraki_vlan:
auth_key: '{{ auth_key }}'
use_https: false
@ -101,6 +101,8 @@
appliance_ip: 192.168.250.1
delegate_to: localhost
register: create_vlan
environment:
ANSIBLE_MERAKI_FORMAT: camelcase
- debug:
msg: '{{create_vlan}}'
@ -109,6 +111,7 @@
that:
- create_vlan.data.id == 2
- create_vlan.changed == True
- create_vlan.data.networkId is defined
- name: Update VLAN with check mode
meraki_vlan:
@ -167,7 +170,7 @@
- assert:
that:
- update_vlan.data.applianceIp == '192.168.250.2'
- update_vlan.data.appliance_ip == '192.168.250.2'
- update_vlan.changed == True
- name: Update VLAN with idempotency and check mode
@ -264,8 +267,8 @@
- assert:
that:
- update_vlan_add_ip.changed == True
- update_vlan_add_ip.data.fixedIpAssignments | length == 2
- update_vlan_add_ip.data.reservedIpRanges | length == 2
- update_vlan_add_ip.data.fixed_ip_assignments | length == 2
- update_vlan_add_ip.data.reserved_ip_ranges | length == 2
- name: Remove IP assignments and reserved IP ranges
meraki_vlan:
@ -295,8 +298,8 @@
- assert:
that:
- update_vlan_remove_ip.changed == True
- update_vlan_remove_ip.data.fixedIpAssignments | length == 1
- update_vlan_remove_ip.data.reservedIpRanges | length == 1
- update_vlan_remove_ip.data.fixed_ip_assignments | length == 1
- update_vlan_remove_ip.data.reserved_ip_ranges | length == 1
- name: Update VLAN with idempotency
meraki_vlan:
@ -355,7 +358,7 @@
- assert:
that:
- '"1.1.1.1" in update_vlan_dns_list.data.dnsNameservers'
- '"1.1.1.1" in update_vlan_dns_list.data.dns_nameservers'
- update_vlan_dns_list.changed == True
- name: Query all VLANs in network