From 06a5e6ae0f3aa6ae75ff335f3a6761ce7216abb8 Mon Sep 17 00:00:00 2001 From: ndswartz Date: Tue, 28 Aug 2018 06:13:32 -0500 Subject: [PATCH] Improve netapp_e_lun_mapping module and documentation (#44666) --- .../storage/netapp/netapp_e_lun_mapping.py | 345 +++++++----------- 1 file changed, 132 insertions(+), 213 deletions(-) diff --git a/lib/ansible/modules/storage/netapp/netapp_e_lun_mapping.py b/lib/ansible/modules/storage/netapp/netapp_e_lun_mapping.py index 1d4b0bf1fe..cf66247357 100644 --- a/lib/ansible/modules/storage/netapp/netapp_e_lun_mapping.py +++ b/lib/ansible/modules/storage/netapp/netapp_e_lun_mapping.py @@ -15,19 +15,18 @@ DOCUMENTATION = ''' --- module: netapp_e_lun_mapping author: Kevin Hulquest (@hulquest) -short_description: Create or Remove LUN Mappings +short_description: NetApp E-Series create, delete, or modify lun mappings description: - - Allows for the creation and removal of volume to host mappings for NetApp E-series storage arrays. + - Create, delete, or modify mappings between a volume and a targeted host/host+ group. version_added: "2.2" extends_documentation_fragment: - netapp.eseries options: - lun: + state: description: - - The LUN number you wish to give the mapping - - If the supplied I(volume_name) is associated with a different LUN, it will be updated to what is supplied here. - required: False - default: 0 + - Present will ensure the mapping exists, absent will remove the mapping. + required: True + choices: ["present", "absent"] target: description: - The name of host or hostgroup you wish to assign to the mapping @@ -38,44 +37,42 @@ options: description: - The name of the volume you wish to include in the mapping. required: True - target_type: - description: - - Whether the target is a host or group. - - Required if supplying an explicit target. - required: False - choices: ["host", "group"] - state: - description: - - Present will ensure the mapping exists, absent will remove the mapping. - - All parameters I(lun), I(target), I(target_type) and I(volume_name) must still be supplied. - required: True - choices: ["present", "absent"] ''' EXAMPLES = ''' --- - - name: Lun Mapping Example + - name: Map volume1 to the host target host1 netapp_e_lun_mapping: - state: present ssid: 1 - lun: 12 - target: Wilson - volume_name: Colby1 - target_type: group api_url: "{{ netapp_api_url }}" api_username: "{{ netapp_api_username }}" api_password: "{{ netapp_api_password }}" + validate_certs: no + state: present + target: host1 + volume_name: volume1 + - name: Delete the lun mapping between volume1 and host1 + netapp_e_lun_mapping: + ssid: 1 + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: yes + state: absent + target: host1 + volume_name: volume1 ''' RETURN = ''' msg: - description: Status of mapping + description: success of the module returned: always type: string - sample: 'Mapping existing' + sample: Lun mapping is complete ''' import json +import logging +from pprint import pformat -from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.netapp import request, eseries_host_argument_spec from ansible.module_utils._text import to_native @@ -86,212 +83,134 @@ HEADERS = { } -def get_host_and_group_map(module, ssid, api_url, user, pwd, validate_certs): - mapping = dict(host=dict(), group=dict()) +class LunMapping(object): + def __init__(self): + argument_spec = eseries_host_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=["present", "absent"]), + target=dict(required=False, default=None), + volume_name=dict(required=True))) + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + args = self.module.params - hostgroups = 'storage-systems/%s/host-groups' % ssid - groups_url = api_url + hostgroups - try: - hg_rc, hg_data = request(groups_url, headers=HEADERS, url_username=user, url_password=pwd, - validate_certs=validate_certs) - except Exception as err: - module.fail_json(msg="Failed to get host groups. Id [%s]. Error [%s]" % (ssid, to_native(err))) + self.state = args["state"] in ["present"] + self.target = args["target"] + self.volume = args["volume_name"] + self.ssid = args["ssid"] + self.url = args["api_url"] + self.check_mode = self.module.check_mode + self.creds = dict(url_username=args["api_username"], + url_password=args["api_password"], + validate_certs=args["validate_certs"]) + self.mapping_info = None - for group in hg_data: - mapping['group'][group['name']] = group['id'] + def update_mapping_info(self): + """Collect the current state of the storage array.""" + response = None + try: + rc, response = request(self.url + "storage-systems/%s/graph" % self.ssid, + method="GET", headers=HEADERS, **self.creds) - hosts = 'storage-systems/%s/hosts' % ssid - hosts_url = api_url + hosts - try: - h_rc, h_data = request(hosts_url, headers=HEADERS, url_username=user, url_password=pwd, - validate_certs=validate_certs) - except Exception as err: - module.fail_json(msg="Failed to get hosts. Id [%s]. Error [%s]" % (ssid, to_native(err))) + except Exception as error: + self.module.fail_json( + msg="Failed to retrieve storage array graph. Id [%s]. Error [%s]" % (self.ssid, to_native(error))) - for host in h_data: - mapping['host'][host['name']] = host['id'] + # Create dictionary containing host/cluster references mapped to their names + target_reference = {} + target_name = {} + for host in response["storagePoolBundle"]["host"]: + target_reference.update({host["hostRef"]: host["name"]}) + target_name.update({host["name"]: host["hostRef"]}) + for cluster in response["storagePoolBundle"]["cluster"]: + target_reference.update({cluster["clusterRef"]: cluster["name"]}) + target_name.update({cluster["name"]: cluster["clusterRef"]}) - return mapping + volume_reference = {} + volume_name = {} + lun_name = {} + for volume in response["volume"]: + volume_reference.update({volume["volumeRef"]: volume["name"]}) + volume_name.update({volume["name"]: volume["volumeRef"]}) + if volume["listOfMappings"]: + lun_name.update({volume["name"]: volume["listOfMappings"][0]["lun"]}) + # Build current mapping object + self.mapping_info = dict(lun_mapping=[dict(volume_reference=mapping["volumeRef"], + map_reference=mapping["mapRef"], + lun_mapping_reference=mapping["lunMappingRef"], + ) for mapping in response["storagePoolBundle"]["lunMapping"]], + volume_by_reference=volume_reference, + volume_by_name=volume_name, + lun_by_name=lun_name, + target_by_reference=target_reference, + target_by_name=target_name) -def get_volume_id(module, data, ssid, name, api_url, user, pwd): - qty = 0 - for volume in data: - if volume['name'] == name: - qty += 1 + def get_lun_mapping(self): + """Find the matching lun mapping reference. - if qty > 1: - module.fail_json(msg="More than one volume with the name: %s was found, " - "please use the volume WWN instead" % name) - else: - wwn = volume['wwn'] + Returns: tuple(bool, int): contains volume match and volume mapping reference + """ + target_match = False + reference = None - try: - return wwn - except NameError: - module.fail_json(msg="No volume with the name: %s, was found" % (name)) + # Verify volume and target exist if needed for expected state. + if self.state: + if self.volume not in self.mapping_info["volume_by_name"].keys(): + self.module.fail_json(msg="Volume does not exist. Id [%s]." % self.ssid) + if self.target and self.target not in self.mapping_info["target_by_name"].keys(): + self.module.fail_json(msg="Target does not exist. Id [%s'." % self.ssid) + for lun_mapping in self.mapping_info["lun_mapping"]: -def get_hostgroups(module, ssid, api_url, user, pwd, validate_certs): - groups = "storage-systems/%s/host-groups" % ssid - url = api_url + groups - try: - rc, data = request(url, headers=HEADERS, url_username=user, url_password=pwd, validate_certs=validate_certs) - return data - except Exception: - module.fail_json(msg="There was an issue with connecting, please check that your" - "endpoint is properly defined and your credentials are correct") + # Find matching volume reference + if lun_mapping["volume_reference"] == self.mapping_info["volume_by_name"][self.volume]: + reference = lun_mapping["lun_mapping_reference"] + # Determine if lun mapping is attached to target + if (lun_mapping["map_reference"] in self.mapping_info["target_by_reference"].keys() and + self.mapping_info["target_by_reference"][lun_mapping["map_reference"]] == self.target): + target_match = True -def get_volumes(module, ssid, api_url, user, pwd, mappable, validate_certs): - volumes = 'storage-systems/%s/%s' % (ssid, mappable) - url = api_url + volumes - try: - rc, data = request(url, url_username=user, url_password=pwd, validate_certs=validate_certs) - except Exception as err: - module.fail_json( - msg="Failed to mappable objects. Type[%s. Id [%s]. Error [%s]." % (mappable, ssid, to_native(err))) - return data + return target_match, reference + def update(self): + """Execute the changes the require changes on the storage array.""" + self.update_mapping_info() + target_match, lun_reference = self.get_lun_mapping() + update = (self.state and not target_match) or (not self.state and target_match) -def get_lun_mappings(ssid, api_url, user, pwd, validate_certs, get_all=None): - mappings = 'storage-systems/%s/volume-mappings' % ssid - url = api_url + mappings - rc, data = request(url, url_username=user, url_password=pwd, validate_certs=validate_certs) + if update and not self.check_mode: + try: + if self.state: + body = dict() + target = None if not self.target else self.mapping_info["target_by_name"][self.target] + if target: + body.update(dict(targetId=target)) - if not get_all: - remove_keys = ('ssid', 'perms', 'lunMappingRef', 'type', 'id') + if lun_reference: + rc, response = request(self.url + "storage-systems/%s/volume-mappings/%s/move" + % (self.ssid, lun_reference), method="POST", data=json.dumps(body), + headers=HEADERS, **self.creds) + else: + body.update(dict(mappableObjectId=self.mapping_info["volume_by_name"][self.volume])) + rc, response = request(self.url + "storage-systems/%s/volume-mappings" % self.ssid, + method="POST", data=json.dumps(body), headers=HEADERS, **self.creds) - for key in remove_keys: - for mapping in data: - del mapping[key] + else: # Remove existing lun mapping for volume and target + rc, response = request(self.url + "storage-systems/%s/volume-mappings/%s" + % (self.ssid, lun_reference), + method="DELETE", headers=HEADERS, **self.creds) + except Exception as error: + self.module.fail_json( + msg="Failed to update storage array lun mapping. Id [%s]. Error [%s]" + % (self.ssid, to_native(error))) - return data - - -def create_mapping(module, ssid, lun_map, vol_name, api_url, user, pwd, validate_certs): - mappings = 'storage-systems/%s/volume-mappings' % ssid - url = api_url + mappings - - if lun_map is not None: - post_body = json.dumps(dict( - mappableObjectId=lun_map['volumeRef'], - targetId=lun_map['mapRef'], - lun=lun_map['lun'] - )) - else: - post_body = json.dumps(dict( - mappableObjectId=lun_map['volumeRef'], - targetId=lun_map['mapRef'], - )) - - rc, data = request(url, data=post_body, method='POST', url_username=user, url_password=pwd, headers=HEADERS, - ignore_errors=True, validate_certs=validate_certs) - - if rc == 422 and lun_map['lun'] is not None: - data = move_lun(module, ssid, lun_map, vol_name, api_url, user, pwd, validate_certs) - # module.fail_json(msg="The volume you specified '%s' is already " - # "part of a different LUN mapping. If you " - # "want to move it to a different host or " - # "hostgroup, then please use the " - # "netapp_e_move_lun module" % vol_name) - return data - - -def move_lun(module, ssid, lun_map, vol_name, api_url, user, pwd, validate_certs): - lun_id = get_lun_id(module, ssid, lun_map, api_url, user, pwd, validate_certs) - move_lun = "storage-systems/%s/volume-mappings/%s/move" % (ssid, lun_id) - url = api_url + move_lun - post_body = json.dumps(dict(targetId=lun_map['mapRef'], lun=lun_map['lun'])) - rc, data = request(url, data=post_body, method='POST', url_username=user, url_password=pwd, headers=HEADERS, - validate_certs=validate_certs) - return data - - -def get_lun_id(module, ssid, lun_mapping, api_url, user, pwd, validate_certs): - data = get_lun_mappings(ssid, api_url, user, pwd, validate_certs, get_all=True) - - for lun_map in data: - if lun_map['volumeRef'] == lun_mapping['volumeRef']: - return lun_map['id'] - # This shouldn't ever get called - module.fail_json(msg="No LUN map found.") - - -def remove_mapping(module, ssid, lun_mapping, api_url, user, pwd, validate_certs): - lun_id = get_lun_id(module, ssid, lun_mapping, api_url, user, pwd) - lun_del = "storage-systems/%s/volume-mappings/%s" % (ssid, lun_id) - url = api_url + lun_del - rc, data = request(url, method='DELETE', url_username=user, url_password=pwd, headers=HEADERS, - validate_certs=validate_certs) - return data + self.module.exit_json(msg="Lun mapping is complete.", changed=update) def main(): - argument_spec = eseries_host_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - target=dict(required=False, default=None), - target_type=dict(required=False, choices=['host', 'group']), - lun=dict(required=False, type='int'), - volume_name=dict(required=True), - )) - - module = AnsibleModule(argument_spec=argument_spec) - - state = module.params['state'] - target = module.params['target'] - target_type = module.params['target_type'] - lun = module.params['lun'] - ssid = module.params['ssid'] - validate_certs = module.params['validate_certs'] - vol_name = module.params['volume_name'] - user = module.params['api_username'] - pwd = module.params['api_password'] - api_url = module.params['api_url'] - - if not api_url.endswith('/'): - api_url += '/' - - volume_map = get_volumes(module, ssid, api_url, user, pwd, "volumes", validate_certs) - thin_volume_map = get_volumes(module, ssid, api_url, user, pwd, "thin-volumes", validate_certs) - volref = None - - for vol in volume_map: - if vol['label'] == vol_name: - volref = vol['volumeRef'] - - if not volref: - for vol in thin_volume_map: - if vol['label'] == vol_name: - volref = vol['volumeRef'] - - if not volref: - module.fail_json(changed=False, msg="No volume with the name %s was found" % vol_name) - - host_and_group_mapping = get_host_and_group_map(module, ssid, api_url, user, pwd, validate_certs) - - desired_lun_mapping = dict( - mapRef=host_and_group_mapping[target_type][target], - lun=lun, - volumeRef=volref - ) - - lun_mappings = get_lun_mappings(ssid, api_url, user, pwd, validate_certs) - - if state == 'present': - if desired_lun_mapping in lun_mappings: - module.exit_json(changed=False, msg="Mapping exists") - else: - result = create_mapping(module, ssid, desired_lun_mapping, vol_name, api_url, user, pwd, validate_certs) - module.exit_json(changed=True, **result) - - elif state == 'absent': - if desired_lun_mapping in lun_mappings: - result = remove_mapping(module, ssid, desired_lun_mapping, api_url, user, pwd, validate_certs) - module.exit_json(changed=True, msg="Mapping removed") - else: - module.exit_json(changed=False, msg="Mapping absent") + lun_mapping = LunMapping() + lun_mapping.update() if __name__ == '__main__':