#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2018, Kevin Breit (@kbreit) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = r''' --- module: meraki_ssid short_description: Manage wireless SSIDs in the Meraki cloud version_added: "2.7" description: - Allows for management of SSIDs in a Meraki wireless environment. notes: - Deleting an SSID does not delete RADIUS servers. options: state: description: - Specifies whether SNMP information should be queried or modified. choices: ['absent', 'query', 'present'] default: present number: description: - SSID number within network. aliases: [ssid_number] name: description: - Name of SSID. org_name: description: - Name of organization. org_id: description: - ID of organization. net_name: description: - Name of network. net_id: description: - ID of network. enabled: description: - Enable or disable SSID network. type: bool auth_mode: description: - Set authentication mode of network. choices: [open, psk, open-with-radius, 8021x-meraki, 8021x-radius] encryption_mode: description: - Set encryption mode of network. choices: [wpa, eap, wpa-eap] psk: description: - Password for wireless network. - Requires auth_mode to be set to psk. wpa_encryption_mode: description: - Encryption mode within WPA2 specification. choices: [WPA1 and WPA2, WPA2 only] splash_page: description: - Set to enable splash page and specify type of splash. choices: ['None', 'Click-through splash page', 'Billing', 'Password-protected with Meraki RADIUS', 'Password-protected with custom RADIUS', 'Password-protected with Active Directory', 'Password-protected with LDAP', 'SMS authentication', 'Systems Manager Sentry', 'Facebook Wi-Fi', 'Google OAuth', 'Sponsored guest'] radius_servers: description: - List of RADIUS servers. suboptions: host: description: - IP addres or hostname of RADIUS server. port: description: - Port number RADIUS server is listening to. secret: description: - RADIUS password. radius_coa_enabled: description: - Enable or disable RADIUS CoA (Change of Authorization) on SSID. type: bool radius_failover_policy: description: - Set client access policy in case RADIUS servers aren't available. choices: [Deny access, Allow access] radius_load_balancing_policy: description: - Set load balancing policy when multiple RADIUS servers are specified. choices: [Strict priority order, Round robin] radius_accounting_enabled: description: - Enable or disable RADIUS accounting. type: bool radius_accounting_servers: description: - List of RADIUS servers for RADIUS accounting. suboptions: host: description: - IP addres or hostname of RADIUS server. port: description: - Port number RADIUS server is listening to. secret: description: - RADIUS password. ip_assignment_mode: description: - Method of which SSID uses to assign IP addresses. choices: ['NAT mode', 'Bridge mode', 'Layer 3 roaming', 'Layer 3 roaming with a concentrator', 'VPN'] use_vlan_tagging: description: - Set whether to use VLAN tagging. type: bool default_vlan_id: description: - Default VLAN ID. vlan_id: description: - ID number of VLAN on SSID. ap_tags_vlan_ids: description: - List of VLAN tags. suboptions: tags: description: - List of AP tags. vlan_id: description: - Numerical identifier that is assigned to the VLAN. walled_garden_enabled: description: - Enable or disable walled garden functionality. type: bool walled_garden_ranges: description: - List of walled garden ranges. min_bitrate: description: - Minimum bitrate (Mbps) allowed on SSID. choices: [1, 2, 5.5, 6, 9, 11, 12, 18, 24, 36, 48, 54] band_selection: description: - Set band selection mode. choices: ['Dual band operation', '5 GHz band only', 'Dual band operation with Band Steering'] per_client_bandwidth_limit_up: description: - Maximum bandwidth in Mbps devices on SSID can upload. per_client_bandwidth_limit_down: description: - Maximum bandwidth in Mbps devices on SSID can download. concentrator_network_id: description: - The concentrator to use for 'Layer 3 roaming with a concentrator' or 'VPN'. author: - Kevin Breit (@kbreit) extends_documentation_fragment: meraki ''' EXAMPLES = r''' ''' RETURN = r''' data: description: Information about queried or updated object. type: list returned: info sample: "data": { } ''' import os from ansible.module_utils.basic import AnsibleModule, json, env_fallback from ansible.module_utils.urls import fetch_url from ansible.module_utils._text import to_native from ansible.module_utils.network.meraki.meraki import MerakiModule, meraki_argument_spec def get_available_number(data): for item in data: if 'Unconfigured SSID' in item['name']: return item['number'] return False def get_ssid_number(name, data): for ssid in data: if name == ssid['name']: return ssid['number'] return False def get_ssids(meraki, net_id): path = meraki.construct_path('get_all', net_id=net_id) return meraki.request(path, method='GET') def main(): param_map = {'name': 'name', 'enabled': 'enabled', 'authMode': 'auth_mode', 'encryptionMode': 'encryption_mode', 'psk': 'psk', 'wpaEncryptionMode': 'wpa_encryption_mode', 'splashPage': 'splash_page', 'radiusServers': 'radius_servers', 'radiusCoaEnabled': 'radius_coa_enabled', 'radiusFailoverPolicy': 'radius_failover_policy', 'radiusLoadBalancingPolicy': 'radius_load_balancing_policy', 'radiusAccountingEnabled': 'radius_accounting_enabled', 'radiusAccountingServers': 'radius_accounting_servers', 'ipAssignmentMode': 'ip_assignment_mode', 'useVlanTagging': 'use_vlan_tagging', 'concentratorNetworkId': 'concentrator_network_id', 'vlanId': 'vlan_id', 'defaultVlanId': 'default_vlan_id', 'apTagsAndVlanIds': 'ap_tags_vlan_ids', 'walledGardenEnabled': 'walled_garden_enabled', 'walledGardenRanges': 'walled_garden_ranges', 'minBitrate': 'min_bitrate', 'bandSelection': 'band_selection', 'perClientBandwidthLimitUp': 'per_client_bandwidth_limit_up', 'perClientBandwidthLimitDown': 'per_client_bandwidth_limit_down', } default_payload = {'name': 'Unconfigured SSID', 'auth_mode': 'open', 'splashPage': 'None', 'perClientBandwidthLimitUp': 0, 'perClientBandwidthLimitDown': 0, 'ipAssignmentMode': 'NAT mode', 'enabled': False, 'bandSelection': 'Dual band operation', 'minBitrate': 11, } # define the available arguments/parameters that a user can pass to # the module radius_arg_spec = dict(host=dict(type='str', required=True), port=dict(type='int'), secret=dict(type='str', no_log=True), ) vlan_arg_spec = dict(tags=dict(type='list'), vlan_id=dict(type='int'), ) argument_spec = meraki_argument_spec() argument_spec.update(state=dict(type='str', choices=['absent', 'present', 'query'], default='present'), number=dict(type='int', aliases=['ssid_number']), name=dict(type='str'), org_name=dict(type='str', aliases=['organization']), org_id=dict(type='int'), net_name=dict(type='str'), net_id=dict(type='str'), enabled=dict(type='bool'), auth_mode=dict(type='str', choices=['open', 'psk', 'open-with-radius', '8021x-meraki', '8021x-radius']), encryption_mode=dict(type='str', choices=['wpa', 'eap', 'wpa-eap']), psk=dict(type='str', no_log=True), wpa_encryption_mode=dict(type='str', choices=['WPA1 and WPA2', 'WPA2 only']), splash_page=dict(type='str', choices=['None', 'Click-through splash page', 'Billing', 'Password-protected with Meraki RADIUS', 'Password-protected with custom RADIUS', 'Password-protected with Active Directory', 'Password-protected with LDAP', 'SMS authentication', 'Systems Manager Sentry', 'Facebook Wi-Fi', 'Google OAuth', 'Sponsored guest']), radius_servers=dict(type='list', default=None, element='dict', options=radius_arg_spec), radius_coa_enabled=dict(type='bool'), radius_failover_policy=dict(type='str', choices=['Deny access', 'Allow access']), radius_load_balancing_policy=dict(type='str', choices=['Strict priority order', 'Round robin']), radius_accounting_enabled=dict(type='bool'), radius_accounting_servers=dict(type='list', element='dict', options=radius_arg_spec), ip_assignment_mode=dict(type='str', choices=['NAT mode', 'Bridge mode', 'Layer 3 roaming', 'Layer 3 roaming with a concentrator', 'VPN']), use_vlan_tagging=dict(type='bool'), concentrator_network_id=dict(type='str'), vlan_id=dict(type='int'), default_vlan_id=dict(type='int'), ap_tags_vlan_ids=dict(type='list', default=None, element='dict', options=vlan_arg_spec), walled_garden_enabled=dict(type='bool'), walled_garden_ranges=dict(type='list'), min_bitrate=dict(type='float', choices=[1, 2, 5.5, 6, 9, 11, 12, 18, 24, 36, 48, 54]), band_selection=dict(type='str', choices=['Dual band operation', '5 GHz band only', 'Dual band operation with Band Steering']), per_client_bandwidth_limit_up=dict(type='int'), per_client_bandwidth_limit_down=dict(type='int'), ) # seed the result dict in the object # we primarily care about changed and state # change is if this module effectively modified the target # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( changed=False, ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, ) meraki = MerakiModule(module, function='ssid') meraki.params['follow_redirects'] = 'all' query_urls = {'ssid': '/networks/{net_id}/ssids'} query_url = {'ssid': 'networks/{net_id}/ssids/'} update_url = {'ssid': 'networks/{net_id}/ssids/'} meraki.url_catalog['get_all'].update(query_urls) meraki.url_catalog['get_one'].update(query_url) meraki.url_catalog['update'] = update_url payload = None # if the user is working with this module in only check mode we do not # want to make any changes to the environment, just return the current # state with no modifications # FIXME: Work with Meraki so they can implement a check mode if module.check_mode: meraki.exit_json(**meraki.result) # execute checks for argument completeness if meraki.params['psk']: if meraki.params['auth_mode'] != 'psk': meraki.fail_json(msg='PSK is only allowed when auth_mode is set to psk') if meraki.params['encryption_mode'] != 'wpa': meraki.fail_json(msg='PSK requires encryption_mode be set to wpa') if meraki.params['radius_servers']: if meraki.params['auth_mode'] not in ('open-with-radius', '8021x-radius'): meraki.fail_json(msg='radius_servers requires auth_mode to be open-with-radius or 8021x-radius') if meraki.params['radius_accounting_enabled'] is True: if meraki.params['auth_mode'] not in ('open-with-radius', '8021x-radius'): meraki.fails_json(msg='radius_accounting_enabled is only allowed when auth_mode is open-with-radius or 8021x-radius') if meraki.params['radius_accounting_servers'] is True: if meraki.params['auth_mode'] not in ('open-with-radius', '8021x-radius') or meraki.params['radius_accounting_enabled'] is False: meraki.fail_json(msg='radius_accounting_servers is only allowed when auth_mode is open_with_radius or 8021x-radius and \ radius_accounting_enabled is true') # manipulate or modify the state as needed (this is going to be the # part where your module will do what it needs to do) org_id = meraki.params['org_id'] net_id = meraki.params['net_id'] if org_id is None: org_id = meraki.get_org_id(meraki.params['org_name']) if net_id is None: nets = meraki.get_nets(org_id=org_id) net_id = meraki.get_net_id(org_id, meraki.params['net_name'], data=nets) if meraki.params['state'] == 'query': if meraki.params['name']: ssid_id = get_ssid_number(meraki.params['name'], get_ssids(meraki, net_id)) path = meraki.construct_path('get_one', net_id=net_id) + str(ssid_id) meraki.result['data'] = meraki.request(path, method='GET') elif meraki.params['number']: path = meraki.construct_path('get_one', net_id=net_id) + meraki.params['number'] meraki.result['data'] = meraki.request(path, method='GET') else: meraki.result['data'] = get_ssids(meraki, net_id) elif meraki.params['state'] == 'present': payload = dict() for k, v in param_map.items(): if meraki.params[v] is not None: payload[k] = meraki.params[v] ssids = get_ssids(meraki, net_id) original = ssids[get_ssid_number(meraki.params['name'], ssids)] # meraki.fail_json(msg=meraki.is_update_required(original, payload), original=original, payload=payload) if meraki.is_update_required(original, payload): ssid_id = meraki.params['number'] if ssid_id is None: # Name should be used to lookup number ssid_id = get_ssid_number(meraki.params['name'], ssids) if ssid_id is False: ssid_id = get_available_number(ssids) if ssid_id is False: meraki.fail_json(msg='No unconfigured SSIDs are available. Specify a number.') path = meraki.construct_path('update', net_id=net_id) + str(ssid_id) result = meraki.request(path, 'PUT', payload=json.dumps(payload)) meraki.result['data'] = result meraki.result['changed'] = True elif meraki.params['state'] == 'absent': ssids = get_ssids(meraki, net_id) ssid_id = meraki.params['number'] if ssid_id is None: # Name should be used to lookup number ssid_id = get_ssid_number(meraki.params['name'], ssids) if ssid_id is False: ssid_id = get_available_number(ssids) if ssid_id is False: meraki.fail_json(msg='No SSID found by specified name and no number was referenced.') path = meraki.construct_path('update', net_id=net_id) + str(ssid_id) payload = default_payload payload['name'] = payload['name'] + ' ' + str(ssid_id + 1) # meraki.fail_json(msg='Payload', payload=payload) result = meraki.request(path, 'PUT', payload=json.dumps(payload)) meraki.result['data'] = result meraki.result['changed'] = True # in the event of a successful module execution, you will want to # simple AnsibleModule.exit_json(), passing the key/value results meraki.exit_json(**meraki.result) if __name__ == '__main__': main()