{"action":"created","comment":{"url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments/78407137","id":78407137,"diff_hunk":"@@ -0,0 +1,888 @@\n+#!/usr/bin/python\n+\n+# (c) 2016, NetApp, Inc\n+#\n+# This file is part of Ansible\n+#\n+# Ansible is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# Ansible is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.\n+\n+DOCUMENTATION = '''\n+---\n+module: netapp_e_storagepool\n+short_description: Manage disk groups and disk pools\n+version_added: '2.2'\n+description:\n+    - Create or remove disk groups and disk pools for NetApp E-series storage arrays.\n+options:\n+  api_username:\n+      required: true\n+      description:\n+      - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.\n+  api_password:\n+      required: true\n+      description:\n+      - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.\n+  api_url:\n+      required: true\n+      description:\n+      - The url to the SANtricity WebServices Proxy or embedded REST API.\n+      example:\n+      - https://prod-1.wahoo.acme.com/devmgr/v2\n+  validate_certs:\n+      required: false\n+      default: true\n+      description:\n+      - Should https certificates be validated?\n+  ssid:\n+    required: true\n+    description:\n+    - The ID of the array to manage (as configured on the web services proxy).\n+  state:\n+    required: true\n+    description:\n+    - Whether the specified storage pool should exist or not.\n+    - Note that removing a storage pool currently requires the removal of all defined volumes first.\n+    choices: ['present', 'absent']\n+  name:\n+    required: true\n+    description:\n+    - The name of the storage pool to manage\n+  criteria_drive_count:\n+    description:\n+    - The number of disks to use for building the storage pool. The pool will be expanded if this number exceeds the number of disks already in place\n+  criteria_drive_type:\n+    description:\n+    - The type of disk (hdd or ssd) to use when searching for candidates to use.\n+    choices: ['hdd','ssd']\n+  criteria_size_unit:\n+    description:\n+    - The unit used to interpret size parameters\n+    choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']\n+    default: 'gb'\n+  criteria_drive_min_size:\n+    description:\n+    - The minimum individual drive size (in size_unit) to consider when choosing drives for the storage pool.\n+  criteria_min_usable_capacity:\n+    description:\n+    - The minimum size of the storage pool (in size_unit). The pool will be expanded if this value exceeds itscurrent size.\n+  criteria_drive_interface_type:\n+    description:\n+    - The interface type to use when selecting drives for the storage pool (no value means all interface types will be considered)\n+    choices: ['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata']\n+  criteria_drive_require_fde:\n+    description:\n+    - Whether full disk encryption ability is required for drives to be added to the storage pool\n+  raid_level:\n+    required: true\n+    choices: ['raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool']\n+    description:\n+    - \"Only required when the requested state is 'present'.  The RAID level of the storage pool to be created.\"\n+  erase_secured_drives:\n+    required: false\n+    choices: ['true', 'false']\n+    description:\n+    - Whether to erase secured disks before adding to storage pool\n+  secure_pool:\n+    required: false\n+    choices: ['true', 'false']\n+    description:\n+    - Whether to convert to a secure storage pool. Will only work if all drives in the pool are security capable.\n+  reserve_drive_count:\n+    required: false\n+    description:\n+    - Set the number of drives reserved by the storage pool for reconstruction operations. Only valide on raid disk pools.\n+  remove_volumes:\n+    required: false\n+    default: False\n+    description:\n+    - Prior to removing a storage pool, delete all volumes in the pool.\n+author: Kevin Hulquest (@hulquest)\n+\n+'''\n+EXAMPLES = '''\n+    - name: No disk groups\n+      netapp_e_storagepool:\n+        ssid: \"{{ ssid }}\"\n+        name: \"{{ item }}\"\n+        state: absent\n+        api_url: \"{{ netapp_api_url }}\"\n+        api_username: \"{{ netapp_api_username }}\"\n+        api_password: \"{{ netapp_api_password }}\"\n+        validate_certs: \"{{ netapp_api_validate_certs }}\"\n+'''\n+RETURN = '''\n+msg:\n+    description: Success message\n+    returned: success\n+    type: string\n+    sample: Json facts for the pool that was created.\n+'''\n+\n+import json\n+import logging\n+import sys\n+from traceback import format_exc\n+\n+from ansible.module_utils.api import basic_auth_argument_spec\n+from ansible.module_utils.basic import AnsibleModule\n+from ansible.module_utils.pycompat24 import get_exception\n+from ansible.module_utils.urls import open_url\n+from ansible.module_utils.six.moves.urllib.error import HTTPError\n+\n+\n+def request(url, data=None, headers=None, method='GET', use_proxy=True,\n+            force=False, last_mod_time=None, timeout=10, validate_certs=True,\n+            url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):\n+    try:\n+        r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,\n+                     force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,\n+                     url_username=url_username, url_password=url_password, http_agent=http_agent,\n+                     force_basic_auth=force_basic_auth)\n+    except HTTPError:\n+        err = get_exception()\n+        r = err.fp\n+\n+    try:\n+        raw_data = r.read()\n+        if raw_data:\n+            data = json.loads(raw_data)\n+        else:\n+            raw_data = None\n+    except:\n+        if ignore_errors:\n+            pass\n+        else:\n+            raise Exception(raw_data)\n+\n+    resp_code = r.getcode()\n+\n+    if resp_code >= 400 and not ignore_errors:\n+        raise Exception(resp_code, data)\n+    else:\n+        return resp_code, data\n+\n+\n+def select(predicate, iterable):\n+    # python 2, 3 generic filtering.\n+    if predicate is None:\n+        predicate = bool\n+    for x in iterable:\n+        if predicate(x):\n+            yield x\n+\n+\n+class groupby(object):\n+    # python 2, 3 generic grouping.\n+    def __init__(self, iterable, key=None):\n+        if key is None:\n+            key = lambda x: x\n+        self.keyfunc = key\n+        self.it = iter(iterable)\n+        self.tgtkey = self.currkey = self.currvalue = object()\n+\n+    def __iter__(self):\n+        return self\n+\n+    def next(self):\n+        while self.currkey == self.tgtkey:\n+            self.currvalue = next(self.it)  # Exit on StopIteration\n+            self.currkey = self.keyfunc(self.currvalue)\n+        self.tgtkey = self.currkey\n+        return (self.currkey, self._grouper(self.tgtkey))\n+\n+    def _grouper(self, tgtkey):\n+        while self.currkey == tgtkey:\n+            yield self.currvalue\n+            self.currvalue = next(self.it)  # Exit on StopIteration\n+            self.currkey = self.keyfunc(self.currvalue)\n+\n+\n+class NetAppESeriesStoragePool(object):\n+    def __init__(self):\n+        self._sp_drives_cached = None\n+\n+        self._size_unit_map = dict(\n+            bytes=1,\n+            b=1,\n+            kb=1024,\n+            mb=1024 ** 2,\n+            gb=1024 ** 3,\n+            tb=1024 ** 4,\n+            pb=1024 ** 5,\n+            eb=1024 ** 6,\n+            zb=1024 ** 7,\n+            yb=1024 ** 8\n+        )\n+\n+        argument_spec = basic_auth_argument_spec()\n+        argument_spec.update(dict(\n+            api_username=dict(type='str', required=True),\n+            api_password=dict(type='str', required=True, no_log=True),\n+            api_url=dict(type='str', required=True),\n+            state=dict(required=True, choices=['present', 'absent'], type='str'),\n+            ssid=dict(required=True, type='str'),\n+            name=dict(required=True, type='str'),\n+            criteria_size_unit=dict(default='gb', type='str'),\n+            criteria_drive_count=dict(type='int'),\n+            criteria_drive_interface_type=dict(choices=['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata'],\n+                                               type='str'),\n+            criteria_drive_type=dict(choices=['ssd', 'hdd'], type='str'),\n+            criteria_drive_min_size=dict(type='int'),\n+            criteria_drive_require_fde=dict(type='bool'),\n+            criteria_min_usable_capacity=dict(type='int'),\n+            raid_level=dict(\n+                choices=['raidUnsupported', 'raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool']),\n+            erase_secured_drives=dict(type='bool'),\n+            log_path=dict(type='str'),\n+            remove_drives=dict(type='list'),\n+            secure_pool=dict(type='bool', default=False),\n+            reserve_drive_count=dict(type='int'),\n+            remove_volumes=dict(type='bool', default=False)\n+        ))\n+\n+        self.module = AnsibleModule(\n+            argument_spec=argument_spec,\n+            required_if=[\n+                ('state', 'present', ['raid_level'])\n+            ],\n+            mutually_exclusive=[\n+\n+            ],\n+            # TODO: update validation for various selection criteria\n+            supports_check_mode=True\n+        )\n+\n+        p = self.module.params\n+\n+        log_path = p['log_path']\n+\n+        # logging setup\n+        self._logger = logging.getLogger(self.__class__.__name__)\n+        self.debug = self._logger.debug\n+\n+        if log_path:\n+            logging.basicConfig(level=logging.DEBUG, filename=log_path)\n+\n+        self.state = p['state']\n+        self.ssid = p['ssid']\n+        self.name = p['name']\n+        self.validate_certs = p['validate_certs']\n+\n+        self.criteria_drive_count = p['criteria_drive_count']\n+        self.criteria_drive_type = p['criteria_drive_type']\n+        self.criteria_size_unit = p['criteria_size_unit']\n+        self.criteria_drive_min_size = p['criteria_drive_min_size']\n+        self.criteria_min_usable_capacity = p['criteria_min_usable_capacity']\n+        self.criteria_drive_interface_type = p['criteria_drive_interface_type']\n+        self.criteria_drive_require_fde = p['criteria_drive_require_fde']\n+\n+        self.raid_level = p['raid_level']\n+        self.erase_secured_drives = p['erase_secured_drives']\n+        self.remove_drives = p['remove_drives']\n+        self.secure_pool = p['secure_pool']\n+        self.reserve_drive_count = p['reserve_drive_count']\n+        self.remove_volumes = p['remove_volumes']\n+\n+        try:\n+            self.api_usr = p['api_username']\n+            self.api_pwd = p['api_password']\n+            self.api_url = p['api_url']\n+        except KeyError:\n+            self.module.fail_json(msg=\"You must pass in api_username \"\n+                                      \"and api_password and api_url to the module.\")\n+\n+        self.post_headers = dict(Accept=\"application/json\")\n+        self.post_headers['Content-Type'] = 'application/json'\n+\n+    # Quick and dirty drive selector, since the one provided by web service proxy is broken for min_disk_size as of 2016-03-12.\n+    # Doesn't really need to be a class once this is in module_utils or retired- just groups everything together so we\n+    # can copy/paste to other modules more easily.\n+    # Filters all disks by specified criteria, then groups remaining disks by capacity, interface and disk type, and selects\n+    # the first set that matches the specified count and/or aggregate capacity.\n+    # class DriveSelector(object):\n+    def filter_drives(\n+            self,\n+            drives,  # raw drives resp\n+            interface_type=None,  # sas, sata, fibre, etc\n+            drive_type=None,  # ssd/hdd\n+            spindle_speed=None,  # 7200, 10000, 15000, ssd (=0)\n+            min_drive_size=None,\n+            max_drive_size=None,\n+            fde_required=None,\n+            size_unit='gb',\n+            min_total_capacity=None,\n+            min_drive_count=None,\n+            exact_drive_count=None,\n+            raid_level=None\n+    ):\n+        if min_total_capacity is None and exact_drive_count is None:\n+            raise Exception(\"One of criteria_min_total_capacity or criteria_drive_count must be specified.\")\n+\n+        if min_total_capacity:\n+            min_total_capacity = min_total_capacity * self._size_unit_map[size_unit]\n+\n+        # filter clearly invalid/unavailable drives first\n+        drives = select(lambda d: self._is_valid_drive(d), drives)\n+\n+        if interface_type:\n+            drives = select(lambda d: d['phyDriveType'] == interface_type, drives)\n+\n+        if drive_type:\n+            drives = select(lambda d: d['driveMediaType'] == drive_type, drives)\n+\n+        if spindle_speed != None:  # 0 is valid for ssds\n+            drives = select(lambda d: d['spindleSpeed'] == spindle_speed, drives)\n+\n+        if min_drive_size:\n+            min_drive_size_bytes = min_drive_size * self._size_unit_map[size_unit]\n+            drives = select(lambda d: int(d['rawCapacity']) >= min_drive_size_bytes, drives)\n+\n+        if max_drive_size:\n+            max_drive_size_bytes = max_drive_size * self._size_unit_map[size_unit]\n+            drives = select(lambda d: int(d['rawCapacity']) <= max_drive_size_bytes, drives)\n+\n+        if fde_required:\n+            drives = select(lambda d: d['fdeCapable'], drives)\n+\n+        # initial implementation doesn't have a preference for any of these values...\n+        # just return the first set we find that matches the requested disk count and/or minimum total capacity\n+        for (cur_capacity, drives_by_capacity) in groupby(drives, lambda d: int(d['rawCapacity'])):\n+            for (cur_interface_type, drives_by_interface_type) in groupby(drives_by_capacity,\n+                                                                          lambda d: d['phyDriveType']):\n+                for (cur_drive_type, drives_by_drive_type) in groupby(drives_by_interface_type,\n+                                                                      lambda d: d['driveMediaType']):\n+                    # listify so we can consume more than once\n+                    drives_by_drive_type = list(drives_by_drive_type)\n+                    candidate_set = list()  # reset candidate list on each iteration of the innermost loop\n+\n+                    if exact_drive_count:\n+                        if len(drives_by_drive_type) < exact_drive_count:\n+                            continue  # we know this set is too small, move on\n+\n+                    for drive in drives_by_drive_type:\n+                        candidate_set.append(drive)\n+                        if self._candidate_set_passes(candidate_set, min_capacity_bytes=min_total_capacity,\n+                                                      min_drive_count=min_drive_count,\n+                                                      exact_drive_count=exact_drive_count, raid_level=raid_level):\n+                            return candidate_set\n+\n+        raise Exception(\"couldn't find an available set of disks to match specified criteria\")\n+\n+    def _is_valid_drive(self, d):\n+        is_valid = d['available'] \\\n+                   and d['status'] == 'optimal' \\\n+                   and not d['pfa'] \\\n+                   and not d['removed'] \\\n+                   and not d['uncertified'] \\\n+                   and not d['invalidDriveData'] \\\n+                   and not d['nonRedundantAccess']\n+\n+        return is_valid\n+\n+    def _candidate_set_passes(self, candidate_set, min_capacity_bytes=None, min_drive_count=None,\n+                              exact_drive_count=None, raid_level=None):\n+        if not self._is_drive_count_valid(len(candidate_set), min_drive_count=min_drive_count,\n+                                          exact_drive_count=exact_drive_count, raid_level=raid_level):\n+            return False\n+        # TODO: this assumes candidate_set is all the same size- if we want to allow wastage, need to update to use min size of set\n+        if min_capacity_bytes != None and self._calculate_usable_capacity(int(candidate_set[0]['rawCapacity']),\n+                                                                          len(candidate_set),\n+                                                                          raid_level=raid_level) < min_capacity_bytes:\n+            return False\n+\n+        return True\n+\n+    def _calculate_usable_capacity(self, disk_size_bytes, disk_count, raid_level=None):\n+        if raid_level in [None, 'raid0']:\n+            return disk_size_bytes * disk_count\n+        if raid_level == 'raid1':\n+            return (disk_size_bytes * disk_count) / 2\n+        if raid_level in ['raid3', 'raid5']:\n+            return (disk_size_bytes * disk_count) - disk_size_bytes\n+        if raid_level in ['raid6', 'raidDiskPool']:\n+            return (disk_size_bytes * disk_count) - (disk_size_bytes * 2)\n+        raise Exception(\"unsupported raid_level: %s\" % raid_level)\n+\n+    def _is_drive_count_valid(self, drive_count, min_drive_count=0, exact_drive_count=None, raid_level=None):\n+        if exact_drive_count and exact_drive_count != drive_count:\n+            return False\n+        if raid_level == 'raidDiskPool':\n+            if drive_count < 11:\n+                return False\n+        if raid_level == 'raid1':\n+            if drive_count % 2 != 0:\n+                return False\n+        if raid_level in ['raid3', 'raid5']:\n+            if drive_count < 3:\n+                return False\n+        if raid_level == 'raid6':\n+            if drive_count < 4:\n+                return False\n+        if min_drive_count and drive_count < min_drive_count:\n+            return False\n+\n+        return True\n+\n+    def get_storage_pool(self, storage_pool_name):\n+        # global ifilter\n+        self.debug(\"fetching storage pools\")\n+        # map the storage pool name to its id\n+        try:\n+            (rc, resp) = request(self.api_url + \"/storage-systems/%s/storage-pools\" % (self.ssid),\n+                                 headers=dict(Accept=\"application/json\"), url_username=self.api_usr,\n+                                 url_password=self.api_pwd, validate_certs=self.validate_certs)\n+        except Exception:\n+            err = get_exception()\n+            rc = err.args[0]\n+            if rc == 404 and self.state == 'absent':\n+                self.module.exit_json(\n+                    msg=\"Storage pool [%s] did not exist.\" % (self.name))\n+            else:\n+                err = get_exception()\n+                self.module.exit_json(\n+                    msg=\"Failed to get storage pools. Array id [%s].  Error[%s]. State[%s]. RC[{3}].\" % (\n+                        self.ssid, str(err), self.state, rc))\n+\n+        self.debug(\"searching for storage pool '%s'\" % storage_pool_name)\n+\n+        pool_detail = next(select(lambda a: a['name'] == storage_pool_name, resp), None)\n+\n+        if pool_detail:\n+            found = 'found'\n+        else:\n+            found = 'not found'\n+        self.debug(found)\n+\n+        return pool_detail\n+\n+    def get_candidate_disks(self):\n+        self.debug(\"getting candidate disks...\")\n+\n+        # driveCapacityMin is broken on /drives POST. Per NetApp request we built our own\n+        # switch back to commented code below if it gets fixed\n+        # drives_req = dict(\n+        #     driveCount = self.criteria_drive_count,\n+        #     sizeUnit = 'mb',\n+        #     raidLevel = self.raid_level\n+        # )\n+        #\n+        # if self.criteria_drive_type:\n+        #     drives_req['driveType'] = self.criteria_drive_type\n+        # if self.criteria_disk_min_aggregate_size_mb:\n+        #     drives_req['targetUsableCapacity'] = self.criteria_disk_min_aggregate_size_mb\n+        #\n+        # # TODO: this arg appears to be ignored, uncomment if it isn't\n+        # #if self.criteria_disk_min_size_gb:\n+        # #    drives_req['driveCapacityMin'] = self.criteria_disk_min_size_gb * 1024\n+        # (rc,drives_resp) = request(self.api_url + \"/storage-systems/%s/drives\" % (self.ssid), data=json.dumps(drives_req), headers=self.post_headers, method='POST', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs)\n+        #\n+        # if rc == 204:\n+        #     self.module.fail_json(msg='Cannot find disks to match requested criteria for storage pool')\n+\n+        # disk_ids = [d['id'] for d in drives_resp]\n+\n+        try:\n+            (rc, drives_resp) = request(self.api_url + \"/storage-systems/%s/drives\" % (self.ssid), method='GET',\n+                                        url_username=self.api_usr, url_password=self.api_pwd,\n+                                        validate_certs=self.validate_certs)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to fetch disk drives. Array id [%s].  Error[%s].\" % (pool_id, self.ssid, str(err)))\n+\n+        try:\n+            candidate_set = self.filter_drives(drives_resp,\n+                                               exact_drive_count=self.criteria_drive_count,\n+                                               drive_type=self.criteria_drive_type,\n+                                               min_drive_size=self.criteria_drive_min_size,\n+                                               raid_level=self.raid_level,\n+                                               size_unit=self.criteria_size_unit,\n+                                               min_total_capacity=self.criteria_min_usable_capacity,\n+                                               interface_type=self.criteria_drive_interface_type,\n+                                               fde_required=self.criteria_drive_require_fde\n+                                               )\n+        except:\n+            err = get_exception()\n+            self.module.fail_json(\n+                msg=\"Failed to allocate adequate drive count. Id [%s]. Error [%s].\" % (self.ssid, str(err)))\n+\n+        disk_ids = [d['id'] for d in candidate_set]\n+\n+        return disk_ids\n+\n+    def create_storage_pool(self):\n+        self.debug(\"creating storage pool...\")\n+\n+        sp_add_req = dict(\n+            raidLevel=self.raid_level,\n+            diskDriveIds=self.disk_ids,\n+            name=self.name\n+        )\n+\n+        if self.erase_secured_drives:\n+            sp_add_req['eraseSecuredDrives'] = self.erase_secured_drives\n+\n+        try:\n+            (rc, resp) = request(self.api_url + \"/storage-systems/%s/storage-pools\" % (self.ssid),\n+                                 data=json.dumps(sp_add_req), headers=self.post_headers, method='POST',\n+                                 url_username=self.api_usr, url_password=self.api_pwd,\n+                                 validate_certs=self.validate_certs,\n+                                 timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to create storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (pool_id,\n+                                                                                                 self.ssid,\n+                                                                                                 str(err)))\n+\n+        self.pool_detail = self.get_storage_pool(self.name)\n+\n+        if self.secure_pool:\n+            secure_pool_data = dict(securePool=True)\n+            try:\n+                (retc, r) = request(\n+                    self.api_url + \"/storage-systems/%s/storage-pools/%s\" % (self.ssid, self.pool_detail['id']),\n+                    data=json.dumps(secure_pool_data), headers=self.post_headers, method='POST',\n+                    url_username=self.api_usr,\n+                    url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120, ignore_errors=True)\n+            except:\n+                err = get_exception()\n+                pool_id = self.pool_detail['id']\n+                self.module.exit_json(\n+                    msg=\"Failed to update storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (pool_id,\n+                                                                                                     self.ssid,\n+                                                                                                     str(err)))\n+\n+    @property\n+    def needs_raid_level_migration(self):\n+        current_raid_level = self.pool_detail['raidLevel']\n+        needs_migration = self.raid_level != current_raid_level\n+\n+        if needs_migration:  # sanity check some things so we can fail early/check-mode\n+            if current_raid_level == 'raidDiskPool':\n+                self.module.fail_json(msg=\"raid level cannot be changed for disk pools\")\n+\n+        return needs_migration\n+\n+    def migrate_raid_level(self):\n+        self.debug(\"migrating storage pool to raid level '%s'...\" % self.raid_level)\n+        sp_raid_migrate_req = dict(\n+            raidLevel=self.raid_level\n+        )\n+        try:\n+            (rc, resp) = request(\n+                self.api_url + \"/storage-systems/%s/storage-pools/%s/raid-type-migration\" % (self.ssid,\n+                                                                                             self.name),\n+                data=json.dumps(sp_raid_migrate_req), headers=self.post_headers, method='POST',\n+                url_username=self.api_usr,\n+                url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to change the raid level of storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (\n+                    pool_id, self.ssid, str(err)))\n+\n+    @property\n+    def sp_drives(self, exclude_hotspares=True):\n+        if not self._sp_drives_cached:\n+\n+            self.debug(\"fetching drive list...\")\n+            try:\n+                (rc, resp) = request(self.api_url + \"/storage-systems/%s/drives\" % (self.ssid), method='GET',\n+                                     url_username=self.api_usr, url_password=self.api_pwd,\n+                                     validate_certs=self.validate_certs)\n+            except:\n+                err = get_exception()\n+                pool_id = self.pool_detail['id']\n+                self.module.exit_json(\n+                    msg=\"Failed to fetch disk drives. Array id [%s].  Error[%s].\" % (pool_id, self.ssid, str(err)))\n+\n+            sp_id = self.pool_detail['id']\n+            if exclude_hotspares:\n+                self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id and not d['hotSpare']]\n+            else:\n+                self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id]\n+\n+        return self._sp_drives_cached\n+\n+    @property\n+    def reserved_drive_count_differs(self):\n+        if int(self.pool_detail['volumeGroupData']['diskPoolData'][\n+                   'reconstructionReservedDriveCount']) != self.reserve_drive_count:\n+            return True\n+        return False\n+\n+    @property\n+    def needs_expansion(self):\n+        if self.criteria_drive_count > len(self.sp_drives):\n+            return True\n+        # TODO: is totalRaidedSpace the best attribute for \"how big is this SP\"?\n+        if self.criteria_min_usable_capacity and (\n+                    self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit]) > int(\n+            self.pool_detail['totalRaidedSpace']):\n+            return True\n+\n+        return False\n+\n+    def get_expansion_candidate_drives(self):\n+        # sanity checks; don't call this if we can't/don't need to expand\n+        if not self.needs_expansion:\n+            self.module.fail_json(msg=\"can't get expansion candidates when pool doesn't need expansion\")\n+\n+        self.debug(\"fetching expansion candidate drives...\")\n+        try:\n+            (rc, resp) = request(\n+                self.api_url + \"/storage-systems/%s/storage-pools/%s/expand\" % (self.ssid,\n+                                                                                self.pool_detail['id']),\n+                method='GET', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs,\n+                timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to fetch candidate drives for storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (\n+                    pool_id, self.ssid, str(err)))\n+\n+        current_drive_count = len(self.sp_drives)\n+        current_capacity_bytes = int(self.pool_detail['totalRaidedSpace'])  # TODO: is this the right attribute to use?\n+\n+        if self.criteria_min_usable_capacity:\n+            requested_capacity_bytes = self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit]\n+        else:\n+            requested_capacity_bytes = current_capacity_bytes\n+\n+        if self.criteria_drive_count:\n+            minimum_disks_to_add = max((self.criteria_drive_count - current_drive_count), 1)\n+        else:\n+            minimum_disks_to_add = 1\n+\n+        minimum_bytes_to_add = max(requested_capacity_bytes - current_capacity_bytes, 0)\n+\n+        # FUTURE: allow more control over expansion candidate selection?\n+        # loop over candidate disk sets and add until we've met both criteria\n+\n+        added_drive_count = 0\n+        added_capacity_bytes = 0\n+\n+        drives_to_add = set()\n+\n+        for s in resp:\n+            # don't trust the API not to give us duplicate drives across candidate sets, especially in multi-drive sets\n+            candidate_drives = s['drives']\n+            if len(drives_to_add.intersection(candidate_drives)) != 0:\n+                # duplicate, skip\n+                continue\n+            drives_to_add.update(candidate_drives)\n+            added_drive_count += len(candidate_drives)\n+            added_capacity_bytes += int(s['usableCapacity'])\n+\n+            if added_drive_count >= minimum_disks_to_add and added_capacity_bytes >= minimum_bytes_to_add:\n+                break\n+\n+        if (added_drive_count < minimum_disks_to_add) or (added_capacity_bytes < minimum_bytes_to_add):\n+            self.module.fail_json(\n+                msg=\"unable to find at least %s drives to add that would add at least %s bytes of capacity\" % (\n+                    minimum_disks_to_add, minimum_bytes_to_add))\n+\n+        return list(drives_to_add)\n+\n+    def expand_storage_pool(self):\n+        drives_to_add = self.get_expansion_candidate_drives()\n+\n+        self.debug(\"adding %s drives to storage pool...\" % len(drives_to_add))\n+        sp_expand_req = dict(\n+            drives=drives_to_add\n+        )\n+        try:\n+            (rc, resp) = request(\n+                self.api_url + \"/storage-systems/%s/storage-pools/%s/expand\" % (self.ssid,\n+                                                                                self.pool_detail['id']),\n+                data=json.dumps(sp_expand_req), headers=self.post_headers, method='POST', url_username=self.api_usr,\n+                url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to add drives to storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (pool_id,\n+                                                                                                        self.ssid,\n+                                                                                                        str(\n+                                                                                                            err)))\n+\n+            # TODO: check response\n+            # TODO: support blocking wait?\n+\n+    def reduce_drives(self, drive_list):\n+        if all(drive in drive_list for drive in self.sp_drives):\n+            # all the drives passed in are present in the system\n+            pass\n+        else:\n+            self.module.fail_json(\n+                msg=\"One of the drives you wish to remove does not currently exist in the storage pool you specified\")\n+\n+        try:\n+            (rc, resp) = request(\n+                self.api_url + \"/storage-systems/%s/storage-pools/%s/reduction\" % (self.ssid,\n+                                                                                   self.pool_detail['id']),\n+                data=json.dumps(drive_list), headers=self.post_headers, method='POST', url_username=self.api_usr,\n+                url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to remove drives from storage pool. Pool id [%s]. Array id [%s].  Error[%s].\" % (\n+                    pool_id, self.ssid, str(err)))\n+\n+    def update_reserve_drive_count(self, qty):\n+        data = dict(reservedDriveCount=qty)\n+        try:\n+            (rc, resp) = request(\n+                self.api_url + \"/storage-systems/%s/storage-pools/%s\" % (self.ssid, self.pool_detail['id']),\n+                data=json.dumps(data), headers=self.post_headers, method='POST', url_username=self.api_usr,\n+                url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)\n+        except:\n+            err = get_exception()\n+            pool_id = self.pool_detail['id']\n+            self.module.exit_json(\n+                msg=\"Failed to update reserve drive count. Pool id [%s]. Array id [%s].  Error[%s].\" % (pool_id,\n+                                                                                                        self.ssid,\n+                                                                                                        str(\n+                                                                                                            err)))\n+\n+    def apply(self):\n+        changed = False\n+        pool_exists = False\n+\n+        self.pool_detail = self.get_storage_pool(self.name)\n+\n+        if self.pool_detail:\n+            pool_exists = True\n+            pool_id = self.pool_detail['id']\n+\n+            if self.state == 'absent':\n+                self.debug(\"CHANGED: storage pool exists, but requested state is 'absent'\")\n+                changed = True\n+            elif self.state == 'present':\n+                # sanity checks first- we can't change these, so we'll bomb if they're specified\n+                if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail['driveMediaType']:\n+                    self.module.fail_json(\n+                        msg=\"drive media type %s cannot be changed to %s\" % (self.pool_detail['driveMediaType'],\n+                                                                             self.criteria_drive_type))\n+\n+                # now the things we can change...\n+                if self.needs_expansion:\n+                    self.debug(\"CHANGED: storage pool needs expansion\")\n+                    changed = True\n+\n+                if self.needs_raid_level_migration:\n+                    self.debug(\n+                        \"CHANGED: raid level migration required; storage pool uses '%s', requested is '%s'\" % (\n+                            self.pool_detail['raidLevel'], self.raid_level))\n+                    changed = True\n+\n+                    # if self.reserved_drive_count_differs:\n+                    # changed = True\n+\n+                    # TODO: validate other state details? (pool priority, alert threshold)\n+\n+                    # per FPoole and others, pool reduce operations will not be supported. Automatic \"smart\" reduction\n+                    # presents a difficult parameter issue, as the disk count can increase due to expansion, so we\n+                    # can't just use disk count > criteria_drive_count.\n+\n+        else:  # pool does not exist\n+            if self.state == 'present':\n+                self.debug(\"CHANGED: storage pool does not exist, but requested state is 'present'\")\n+                changed = True\n+\n+                # ensure we can get back a workable set of disks\n+                # (doing this early so candidate selection runs under check mode)\n+                self.disk_ids = self.get_candidate_disks()","path":"storage/netapp/netapp_e_storagepool.py","position":812,"original_position":812,"commit_id":"49740dd8111c347afcf537630e7d3fd6b3b0b0e2","original_commit_id":"49740dd8111c347afcf537630e7d3fd6b3b0b0e2","user":{"login":"alikins","id":15162,"avatar_url":"https://avatars.githubusercontent.com/u/15162?v=3","gravatar_id":"","url":"https://api.github.com/users/alikins","html_url":"https://github.com/alikins","followers_url":"https://api.github.com/users/alikins/followers","following_url":"https://api.github.com/users/alikins/following{/other_user}","gists_url":"https://api.github.com/users/alikins/gists{/gist_id}","starred_url":"https://api.github.com/users/alikins/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alikins/subscriptions","organizations_url":"https://api.github.com/users/alikins/orgs","repos_url":"https://api.github.com/users/alikins/repos","events_url":"https://api.github.com/users/alikins/events{/privacy}","received_events_url":"https://api.github.com/users/alikins/received_events","type":"User","site_admin":false},"body":"Should self.disk_ids get initialized in __init__?  I can't tell if it would cause a problem or not (can create_storage_pool() get called before self.disk_ids is set?)","created_at":"2016-09-12T16:35:13Z","updated_at":"2016-09-12T16:35:13Z","html_url":"https://github.com/ansible/ansible-modules-extras/pull/2883#discussion_r78407137","pull_request_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883","_links":{"self":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments/78407137"},"html":{"href":"https://github.com/ansible/ansible-modules-extras/pull/2883#discussion_r78407137"},"pull_request":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883"}}},"pull_request":{"url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883","id":84497794,"html_url":"https://github.com/ansible/ansible-modules-extras/pull/2883","diff_url":"https://github.com/ansible/ansible-modules-extras/pull/2883.diff","patch_url":"https://github.com/ansible/ansible-modules-extras/pull/2883.patch","issue_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2883","number":2883,"state":"open","locked":false,"title":"Remaining NetApp modules","user":{"login":"hulquest","id":4061574,"avatar_url":"https://avatars.githubusercontent.com/u/4061574?v=3","gravatar_id":"","url":"https://api.github.com/users/hulquest","html_url":"https://github.com/hulquest","followers_url":"https://api.github.com/users/hulquest/followers","following_url":"https://api.github.com/users/hulquest/following{/other_user}","gists_url":"https://api.github.com/users/hulquest/gists{/gist_id}","starred_url":"https://api.github.com/users/hulquest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hulquest/subscriptions","organizations_url":"https://api.github.com/users/hulquest/orgs","repos_url":"https://api.github.com/users/hulquest/repos","events_url":"https://api.github.com/users/hulquest/events{/privacy}","received_events_url":"https://api.github.com/users/hulquest/received_events","type":"User","site_admin":false},"body":"##### ISSUE TYPE\r\n\r\n - New Module Pull Request\r\n\r\n##### COMPONENT NAME\r\nstorage/netapp/netapp_e_amg_role.py\r\nstorage/netapp/netapp_e_flashcache.py\r\nstorage/netapp/netapp_e_volume.py\r\nstorage/netapp/netapp_e_snapshot_group.py\r\nstorage/netapp/netapp_e_snapshot_images.py\r\nstorage/netapp/netapp_e_storagepool.py\r\nstorage/netapp/netapp_e_auth.py\r\nstorage/netapp/netapp_e_host.py\r\nstorage/netapp/netapp_e_amg_sync.py\r\nstorage/netapp/netapp_e_amg.py\r\nstorage/netapp/netapp_e_lun_mapping.py\r\nstorage/netapp/netapp_e_volume_copy.py\r\nstorage/netapp/netapp_e_hostgroup.py\r\nstorage/netapp/netapp_e_snapshot_volume.py\r\n\r\n\r\n##### ANSIBLE VERSION\r\n```\r\n(khulques) [ansible-modules-extras]$ ansible --version\r\nansible 2.2.0 (devel 4cc4dc6793) last updated 2016/07/19 12:02:12 (GMT -600)\r\n  lib/ansible/modules/core: (detached HEAD 7de287237f) last updated 2016/07/19 12:02:18 (GMT -600)\r\n  lib/ansible/modules/extras: (detached HEAD 68ca157f3b) last updated 2016/07/19 12:02:21 (GMT -600)\r\n  config file = /home/khulques/ansible.cfg\r\n  configured module search path = ['/home/khulques/src/projects/ansible_eseries/ansible_modules']\r\n```\r\n\r\n##### SUMMARY\r\n<!--- Describe the change, including rationale and design decisions -->\r\nAdding the remainder of the NetApp SANtricity Modules.  All of these modules should have the changes discussed in #2748 \r\n\r\n","created_at":"2016-09-08T15:17:03Z","updated_at":"2016-09-12T16:35:13Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0a670235f21a4817465b07c65a4508dd471326f5","assignee":{"login":"alikins","id":15162,"avatar_url":"https://avatars.githubusercontent.com/u/15162?v=3","gravatar_id":"","url":"https://api.github.com/users/alikins","html_url":"https://github.com/alikins","followers_url":"https://api.github.com/users/alikins/followers","following_url":"https://api.github.com/users/alikins/following{/other_user}","gists_url":"https://api.github.com/users/alikins/gists{/gist_id}","starred_url":"https://api.github.com/users/alikins/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alikins/subscriptions","organizations_url":"https://api.github.com/users/alikins/orgs","repos_url":"https://api.github.com/users/alikins/repos","events_url":"https://api.github.com/users/alikins/events{/privacy}","received_events_url":"https://api.github.com/users/alikins/received_events","type":"User","site_admin":false},"assignees":[{"login":"alikins","id":15162,"avatar_url":"https://avatars.githubusercontent.com/u/15162?v=3","gravatar_id":"","url":"https://api.github.com/users/alikins","html_url":"https://github.com/alikins","followers_url":"https://api.github.com/users/alikins/followers","following_url":"https://api.github.com/users/alikins/following{/other_user}","gists_url":"https://api.github.com/users/alikins/gists{/gist_id}","starred_url":"https://api.github.com/users/alikins/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alikins/subscriptions","organizations_url":"https://api.github.com/users/alikins/orgs","repos_url":"https://api.github.com/users/alikins/repos","events_url":"https://api.github.com/users/alikins/events{/privacy}","received_events_url":"https://api.github.com/users/alikins/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883/commits","review_comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883/comments","review_comment_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2883/comments","statuses_url":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/49740dd8111c347afcf537630e7d3fd6b3b0b0e2","head":{"label":"hulquest:remaining-modules","ref":"remaining-modules","sha":"49740dd8111c347afcf537630e7d3fd6b3b0b0e2","user":{"login":"hulquest","id":4061574,"avatar_url":"https://avatars.githubusercontent.com/u/4061574?v=3","gravatar_id":"","url":"https://api.github.com/users/hulquest","html_url":"https://github.com/hulquest","followers_url":"https://api.github.com/users/hulquest/followers","following_url":"https://api.github.com/users/hulquest/following{/other_user}","gists_url":"https://api.github.com/users/hulquest/gists{/gist_id}","starred_url":"https://api.github.com/users/hulquest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hulquest/subscriptions","organizations_url":"https://api.github.com/users/hulquest/orgs","repos_url":"https://api.github.com/users/hulquest/repos","events_url":"https://api.github.com/users/hulquest/events{/privacy}","received_events_url":"https://api.github.com/users/hulquest/received_events","type":"User","site_admin":false},"repo":{"id":65928177,"name":"ansible-modules-extras","full_name":"hulquest/ansible-modules-extras","owner":{"login":"hulquest","id":4061574,"avatar_url":"https://avatars.githubusercontent.com/u/4061574?v=3","gravatar_id":"","url":"https://api.github.com/users/hulquest","html_url":"https://github.com/hulquest","followers_url":"https://api.github.com/users/hulquest/followers","following_url":"https://api.github.com/users/hulquest/following{/other_user}","gists_url":"https://api.github.com/users/hulquest/gists{/gist_id}","starred_url":"https://api.github.com/users/hulquest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hulquest/subscriptions","organizations_url":"https://api.github.com/users/hulquest/orgs","repos_url":"https://api.github.com/users/hulquest/repos","events_url":"https://api.github.com/users/hulquest/events{/privacy}","received_events_url":"https://api.github.com/users/hulquest/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/hulquest/ansible-modules-extras","description":"Ansible extra modules - these modules ship with ansible","fork":true,"url":"https://api.github.com/repos/hulquest/ansible-modules-extras","forks_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/forks","keys_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/teams","hooks_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/hooks","issue_events_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/issues/events{/number}","events_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/events","assignees_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/assignees{/user}","branches_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/branches{/branch}","tags_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/tags","blobs_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/git/refs{/sha}","trees_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/statuses/{sha}","languages_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/languages","stargazers_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/stargazers","contributors_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/contributors","subscribers_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/subscribers","subscription_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/subscription","commits_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/commits{/sha}","git_commits_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/git/commits{/sha}","comments_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/comments{/number}","issue_comment_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/issues/comments{/number}","contents_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/contents/{+path}","compare_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/merges","archive_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/downloads","issues_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/issues{/number}","pulls_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/pulls{/number}","milestones_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/milestones{/number}","notifications_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/labels{/name}","releases_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/releases{/id}","deployments_url":"https://api.github.com/repos/hulquest/ansible-modules-extras/deployments","created_at":"2016-08-17T17:28:54Z","updated_at":"2016-08-17T17:29:00Z","pushed_at":"2016-09-09T18:33:59Z","git_url":"git://github.com/hulquest/ansible-modules-extras.git","ssh_url":"git@github.com:hulquest/ansible-modules-extras.git","clone_url":"https://github.com/hulquest/ansible-modules-extras.git","svn_url":"https://github.com/hulquest/ansible-modules-extras","homepage":"","size":8081,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"devel"}},"base":{"label":"ansible:devel","ref":"devel","sha":"9a338b05eb52b13056366f0cafca759513110e32","user":{"login":"ansible","id":1507452,"avatar_url":"https://avatars.githubusercontent.com/u/1507452?v=3","gravatar_id":"","url":"https://api.github.com/users/ansible","html_url":"https://github.com/ansible","followers_url":"https://api.github.com/users/ansible/followers","following_url":"https://api.github.com/users/ansible/following{/other_user}","gists_url":"https://api.github.com/users/ansible/gists{/gist_id}","starred_url":"https://api.github.com/users/ansible/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ansible/subscriptions","organizations_url":"https://api.github.com/users/ansible/orgs","repos_url":"https://api.github.com/users/ansible/repos","events_url":"https://api.github.com/users/ansible/events{/privacy}","received_events_url":"https://api.github.com/users/ansible/received_events","type":"Organization","site_admin":false},"repo":{"id":24474138,"name":"ansible-modules-extras","full_name":"ansible/ansible-modules-extras","owner":{"login":"ansible","id":1507452,"avatar_url":"https://avatars.githubusercontent.com/u/1507452?v=3","gravatar_id":"","url":"https://api.github.com/users/ansible","html_url":"https://github.com/ansible","followers_url":"https://api.github.com/users/ansible/followers","following_url":"https://api.github.com/users/ansible/following{/other_user}","gists_url":"https://api.github.com/users/ansible/gists{/gist_id}","starred_url":"https://api.github.com/users/ansible/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ansible/subscriptions","organizations_url":"https://api.github.com/users/ansible/orgs","repos_url":"https://api.github.com/users/ansible/repos","events_url":"https://api.github.com/users/ansible/events{/privacy}","received_events_url":"https://api.github.com/users/ansible/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ansible/ansible-modules-extras","description":"Ansible extra modules - these modules ship with ansible","fork":false,"url":"https://api.github.com/repos/ansible/ansible-modules-extras","forks_url":"https://api.github.com/repos/ansible/ansible-modules-extras/forks","keys_url":"https://api.github.com/repos/ansible/ansible-modules-extras/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ansible/ansible-modules-extras/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ansible/ansible-modules-extras/teams","hooks_url":"https://api.github.com/repos/ansible/ansible-modules-extras/hooks","issue_events_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/events{/number}","events_url":"https://api.github.com/repos/ansible/ansible-modules-extras/events","assignees_url":"https://api.github.com/repos/ansible/ansible-modules-extras/assignees{/user}","branches_url":"https://api.github.com/repos/ansible/ansible-modules-extras/branches{/branch}","tags_url":"https://api.github.com/repos/ansible/ansible-modules-extras/tags","blobs_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/refs{/sha}","trees_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/{sha}","languages_url":"https://api.github.com/repos/ansible/ansible-modules-extras/languages","stargazers_url":"https://api.github.com/repos/ansible/ansible-modules-extras/stargazers","contributors_url":"https://api.github.com/repos/ansible/ansible-modules-extras/contributors","subscribers_url":"https://api.github.com/repos/ansible/ansible-modules-extras/subscribers","subscription_url":"https://api.github.com/repos/ansible/ansible-modules-extras/subscription","commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/commits{/sha}","git_commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/commits{/sha}","comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/comments{/number}","issue_comment_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/comments{/number}","contents_url":"https://api.github.com/repos/ansible/ansible-modules-extras/contents/{+path}","compare_url":"https://api.github.com/repos/ansible/ansible-modules-extras/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ansible/ansible-modules-extras/merges","archive_url":"https://api.github.com/repos/ansible/ansible-modules-extras/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ansible/ansible-modules-extras/downloads","issues_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues{/number}","pulls_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls{/number}","milestones_url":"https://api.github.com/repos/ansible/ansible-modules-extras/milestones{/number}","notifications_url":"https://api.github.com/repos/ansible/ansible-modules-extras/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ansible/ansible-modules-extras/labels{/name}","releases_url":"https://api.github.com/repos/ansible/ansible-modules-extras/releases{/id}","deployments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/deployments","created_at":"2014-09-25T20:28:42Z","updated_at":"2016-09-12T12:36:46Z","pushed_at":"2016-09-12T15:41:52Z","git_url":"git://github.com/ansible/ansible-modules-extras.git","ssh_url":"git@github.com:ansible/ansible-modules-extras.git","clone_url":"https://github.com/ansible/ansible-modules-extras.git","svn_url":"https://github.com/ansible/ansible-modules-extras","homepage":"","size":53540,"stargazers_count":704,"watchers_count":704,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1447,"mirror_url":null,"open_issues_count":606,"forks":1447,"open_issues":606,"watchers":704,"default_branch":"devel"}},"_links":{"self":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883"},"html":{"href":"https://github.com/ansible/ansible-modules-extras/pull/2883"},"issue":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2883"},"comments":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2883/comments"},"review_comments":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883/comments"},"review_comment":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2883/commits"},"statuses":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/49740dd8111c347afcf537630e7d3fd6b3b0b0e2"}}}}
{"actor":{"display_login":"alikins"}}