Repository: ansible/ansible

Issue:
meraki_device should be able to claim device into an organization
##### SUMMARY
Devices within Meraki need to be claimed into the network and organization. Currently it only claims into a network but it should also be able to be claimed into the organization.

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
meraki_device


Discussion:
Files identified in the description:
* [lib/ansible/modules/network/meraki/meraki_device.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/meraki/meraki_device.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @dagwieers
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/network/meraki/meraki_device.py b/lib/ansible/modules/network/meraki/meraki_device.py
--- a/lib/ansible/modules/network/meraki/meraki_device.py
+++ b/lib/ansible/modules/network/meraki/meraki_device.py
@@ -211,6 +211,14 @@ def is_device_valid(meraki, serial, data):
     return False
 
 
+def get_org_devices(meraki, org_id):
+    path = meraki.construct_path('get_all_org', org_id=org_id)
+    response = meraki.request(path, method='GET')
+    if meraki.status != 200:
+        meraki.fail_json(msg='Failed to query all devices belonging to the organization')
+    return response
+
+
 def temp_get_nets(meraki, org_name, net_name):
     org_id = meraki.get_org_id(org_name)
     path = meraki.construct_path('get_all', function='network', org_id=org_id)
@@ -263,24 +271,19 @@ def main():
 
     meraki.params['follow_redirects'] = 'all'
 
-    query_urls = {'device': '/networks/{net_id}/devices',
-                  }
-
-    query_device_urls = {'device': '/networks/{net_id}/devices/',
-                         }
-
-    claim_device_urls = {'device': '/networks/{net_id}/devices/claim',
-                         }
-
-    update_device_urls = {'device': '/networks/{net_id}/devices/',
-                          }
-
-    delete_device_urls = {'device': '/networks/{net_id}/devices/',
-                          }
+    query_urls = {'device': '/networks/{net_id}/devices'}
+    query_org_urls = {'device': '/organizations/{org_id}/deviceStatuses'}
+    query_device_urls = {'device': '/networks/{net_id}/devices/'}
+    claim_device_urls = {'device': '/networks/{net_id}/devices/claim'}
+    bind_org_urls = {'device': '/organizations/{org_id}/claim'}
+    update_device_urls = {'device': '/networks/{net_id}/devices/'}
+    delete_device_urls = {'device': '/networks/{net_id}/devices/'}
 
     meraki.url_catalog['get_all'].update(query_urls)
+    meraki.url_catalog['get_all_org'] = query_org_urls
     meraki.url_catalog['get_device'] = query_device_urls
     meraki.url_catalog['create'] = claim_device_urls
+    meraki.url_catalog['bind_org'] = bind_org_urls
     meraki.url_catalog['update'] = update_device_urls
     meraki.url_catalog['delete'] = delete_device_urls
 
@@ -297,15 +300,19 @@ def main():
 
     # manipulate or modify the state as needed (this is going to be the
     # part where your module will do what it needs to do)
-    nets = temp_get_nets(meraki, meraki.params['org_name'], meraki.params['net_name'])
+
+    org_id = meraki.params['org_id']
+    if org_id is None:
+        org_id = meraki.get_org_id(meraki.params['org_name'])
+    net_id = meraki.params['net_id']
+    if net_id is None:
+        if meraki.params['net_name']:
+            nets = temp_get_nets(meraki, meraki.params['org_name'], meraki.params['net_name'])
+            net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
 
     if meraki.params['state'] == 'query':
         if meraki.params['net_name'] or meraki.params['net_id']:
             device = []
-            if meraki.params['net_name']:
-                net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
-            elif meraki.params['net_id']:
-                net_id = meraki.params['net_id']
             if meraki.params['serial']:
                 path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial']
                 request = meraki.request(path, method='GET')
@@ -341,24 +348,16 @@ def main():
                 request = meraki.request(path, method='GET')
                 meraki.result['data'] = request
         else:
-            devices = []
-            for net in nets:  # Gather all devices in all networks
-                path = meraki.construct_path('get_all', net_id=net['id'])
-                request = meraki.request(path, method='GET')
-                devices.append(request)
+            path = meraki.construct_path('get_all_org', org_id=org_id)
+            devices = meraki.request(path, method='GET')
             if meraki.params['serial']:
-                for network in devices:
-                    for dev in network:
-                        if dev['serial'] == meraki.params['serial']:
-                            meraki.result['data'] = [dev]
+                for device in devices:
+                    if device['serial'] == meraki.params['serial']:
+                        meraki.result['data'] = device
             else:
                 meraki.result['data'] = devices
     elif meraki.params['state'] == 'present':
         device = []
-        if meraki.params['net_name']:
-            net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
-        elif meraki.params['net_id']:
-            net_id = meraki.params['net_id']
         if meraki.params['hostname']:
             query_path = meraki.construct_path('get_all', net_id=net_id)
             device_list = meraki.request(query_path, method='GET')
@@ -380,21 +379,28 @@ def main():
                     meraki.result['data'] = updated_device
                     meraki.result['changed'] = True
         else:
-            query_path = meraki.construct_path('get_all', net_id=net_id)
-            device_list = meraki.request(query_path, method='GET')
-            if is_device_valid(meraki, meraki.params['serial'], device_list) is False:
-                payload = {'serial': meraki.params['serial']}
-                path = meraki.construct_path('create', net_id=net_id)
-                created_device = []
-                created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload)))
-                meraki.result['data'] = created_device
-                meraki.result['changed'] = True
+            if net_id is None:
+                device_list = get_org_devices(meraki, org_id)
+                if is_device_valid(meraki, meraki.params['serial'], device_list) is False:
+                    payload = {'serial': meraki.params['serial']}
+                    path = meraki.construct_path('bind_org', org_id=org_id)
+                    created_device = []
+                    created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload)))
+                    meraki.result['data'] = created_device
+                    meraki.result['changed'] = True
+            else:
+                query_path = meraki.construct_path('get_all', net_id=net_id)
+                device_list = meraki.request(query_path, method='GET')
+                if is_device_valid(meraki, meraki.params['serial'], device_list) is False:
+                    if net_id:
+                        payload = {'serial': meraki.params['serial']}
+                        path = meraki.construct_path('create', net_id=net_id)
+                        created_device = []
+                        created_device.append(meraki.request(path, method='POST', payload=json.dumps(payload)))
+                        meraki.result['data'] = created_device
+                        meraki.result['changed'] = True
     elif meraki.params['state'] == 'absent':
         device = []
-        if meraki.params['net_name']:
-            net_id = meraki.get_net_id(net_name=meraki.params['net_name'], data=nets)
-        elif meraki.params['net_id']:
-            net_id = meraki.params['net_id']
         query_path = meraki.construct_path('get_all', net_id=net_id)
         device_list = meraki.request(query_path, method='GET')
         if is_device_valid(meraki, meraki.params['serial'], device_list) is True:


Test patch:
diff --git a/test/integration/targets/meraki_device/tasks/main.yml b/test/integration/targets/meraki_device/tasks/main.yml
--- a/test/integration/targets/meraki_device/tasks/main.yml
+++ b/test/integration/targets/meraki_device/tasks/main.yml
@@ -1,202 +1,213 @@
 ---
-- name: Claim a device
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial: '{{serial}}'
-    state: present
-  delegate_to: localhost
-  register: claim_device
-
-- debug:
-    msg: '{{claim_device}}'
-
-- assert:
-    that:
-      - claim_device.changed == true
-
-- name: Query all devices
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    state: query
-  delegate_to: localhost
-  register: query_all
-
-- debug:
-    msg: '{{query_all}}'
-
-- assert:
-    that:
-      - query_all.changed == False
-
-- name: Query all devices in one network by network ID
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_id: '{{test_net_id}}'
-    state: query
-  delegate_to: localhost
-  register: query_one_net_id
-
-- debug:
-    msg: '{{query_one_net_id}}'
-
-- name: Query all devices in one network
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    state: query
-  delegate_to: localhost
-  register: query_one_net
-
-- debug:
-    msg: '{{query_one_net}}'
-
-- name: Query device by serial
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    serial: '{{serial}}'
-    state: query
-  delegate_to: localhost
-  register: query_serial_no_net
-
-- debug:
-    msg: '{{query_serial_no_net}}'
-
-- name: Query device by serial
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial: '{{serial}}'
-    state: query
-  delegate_to: localhost
-  register: query_serial
-
-- debug:
-    msg: '{{query_serial}}'
-
-- assert:
-    that:
-      - query_serial.changed == False
-
-- name: Query uplink information for a device
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial_uplink: '{{serial}}'
-    state: query
-  delegate_to: localhost
-  register: query_serial_uplink
-
-- debug:
-    msg: '{{query_serial_uplink}}'
-
-- name: Query LLDP/CDP information about a device
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial_lldp_cdp: '{{serial}}'
-    lldp_cdp_timespan: 6000
-    state: query
-  delegate_to: localhost
-  register: query_serial_lldp_cdp
-
-- debug:
-    msg: '{{query_serial_lldp_cdp}}'
-
-- name: Query a device by hostname
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    hostname: test-hostname
-    state: query
-  delegate_to: localhost
-  register: query_hostname
-
-- debug:
-    msg: '{{query_hostname}}'
-
-- name: Query a device by model
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    model: MR26
-    state: query
-  delegate_to: localhost
-  register: query_model
-
-- debug:
-    msg: '{{query_model}}'
-
-- name: Update a device
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial: '{{serial}}'
-    name: mr26
-    address: 1060 W. Addison St., Chicago, IL
-    lat: 41.948038
-    lng: -87.65568
-    tags: recently-added
-    state: present
-    move_map_marker: True
-  delegate_to: localhost
-  register: update_device
-
-- debug:
-    msg: '{{update_device}}'
-
-# - assert:
-#     that:
-#       - update_device.changed == true
-#       - '"1060 W. Addison St., Chicago, IL" in update_device.data.0.address'
-
-- name: Update a device with idempotency
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial: '{{serial}}'
-    name: mr26
-    address: 1060 W. Addison St., Chicago, IL
-    lat: 41.948038
-    lng: -87.65568
-    tags: recently-added
-    state: present
-    move_map_marker: True
-  delegate_to: localhost
-  register: update_device_idempotent
-
-- debug:
-    msg: '{{update_device_idempotent}}'
-
-- assert:
-    that:
-      - update_device_idempotent.changed == False
-
-- name: Remove a device
-  meraki_device:
-    auth_key: '{{auth_key}}'
-    org_name: '{{test_org_name}}'
-    net_name: '{{test_net_name}}'
-    serial: '{{serial}}'
-    state: absent
-  delegate_to: localhost
-  register: delete_device
-
-- debug:
-    msg: '{{delete_device}}'
-
-- assert:
-    that:
-      - delete_device.changed == true
\ No newline at end of file
+- block:
+  - name: Claim a device into an organization
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      serial: '{{serial}}'
+      state: present
+    delegate_to: localhost
+    register: claim_device_org
+
+  - assert:
+      that:
+        - claim_device_org.changed == true
+
+  - name: Query status of all devices in an organization
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      state: query
+    delegate_to: localhost
+    register: query_device_org
+
+  - debug:
+      msg: '{{query_device_org}}'
+
+  - name: Claim a device into a network
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial: '{{serial}}'
+      state: present
+    delegate_to: localhost
+    register: claim_device
+
+  - debug:
+      msg: '{{claim_device}}'
+
+  - assert:
+      that:
+        - claim_device.changed == true
+
+  - name: Query all devices in one network by network ID
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_id: '{{test_net_id}}'
+      state: query
+    delegate_to: localhost
+    register: query_one_net_id
+
+  - debug:
+      msg: '{{query_one_net_id}}'
+
+  - name: Query all devices in one network
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      state: query
+    delegate_to: localhost
+    register: query_one_net
+
+  - debug:
+      msg: '{{query_one_net}}'
+
+  - name: Query device by serial
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      serial: '{{serial}}'
+      state: query
+    delegate_to: localhost
+    register: query_serial_no_net
+
+  - debug:
+      msg: '{{query_serial_no_net}}'
+
+  - name: Query device by serial
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial: '{{serial}}'
+      state: query
+    delegate_to: localhost
+    register: query_serial
+
+  - debug:
+      msg: '{{query_serial}}'
+
+  - assert:
+      that:
+        - query_serial.changed == False
+
+  - name: Query uplink information for a device
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial_uplink: '{{serial}}'
+      state: query
+    delegate_to: localhost
+    register: query_serial_uplink
+
+  - debug:
+      msg: '{{query_serial_uplink}}'
+
+  - name: Query LLDP/CDP information about a device
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial_lldp_cdp: '{{serial}}'
+      lldp_cdp_timespan: 6000
+      state: query
+    delegate_to: localhost
+    register: query_serial_lldp_cdp
+
+  - debug:
+      msg: '{{query_serial_lldp_cdp}}'
+
+  - name: Query a device by hostname
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      hostname: test-hostname
+      state: query
+    delegate_to: localhost
+    register: query_hostname
+
+  - debug:
+      msg: '{{query_hostname}}'
+
+  - name: Query a device by model
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      model: MR26
+      state: query
+    delegate_to: localhost
+    register: query_model
+
+  - debug:
+      msg: '{{query_model}}'
+
+  - name: Update a device
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial: '{{serial}}'
+      name: mr26
+      address: 1060 W. Addison St., Chicago, IL
+      lat: 41.948038
+      lng: -87.65568
+      tags: recently-added
+      state: present
+      move_map_marker: True
+    delegate_to: localhost
+    register: update_device
+
+  - debug:
+      msg: '{{update_device}}'
+
+  # - assert:
+  #     that:
+  #       - update_device.changed == true
+  #       - '"1060 W. Addison St., Chicago, IL" in update_device.data.0.address'
+
+  - name: Update a device with idempotency
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial: '{{serial}}'
+      name: mr26
+      address: 1060 W. Addison St., Chicago, IL
+      lat: 41.948038
+      lng: -87.65568
+      tags: recently-added
+      state: present
+      move_map_marker: True
+    delegate_to: localhost
+    register: update_device_idempotent
+
+  - debug:
+      msg: '{{update_device_idempotent}}'
+
+  - assert:
+      that:
+        - update_device_idempotent.changed == False
+
+  always:
+  - name: Remove a device
+    meraki_device:
+      auth_key: '{{auth_key}}'
+      org_name: '{{test_org_name}}'
+      net_name: '{{test_net_name}}'
+      serial: '{{serial}}'
+      state: absent
+    delegate_to: localhost
+    register: delete_device
+
+  - debug:
+      msg: '{{delete_device}}'
+
+  - assert:
+      that:
+        - delete_device.changed == true
\ No newline at end of file