#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2021, Infinidat # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = r''' --- module: infini_map version_added: '2.10' short_description: Create and Delete mapping of a volume to a host or cluster on Infinibox description: - This module creates or deletes mappings of volumes to hosts or clusters on Infinibox. author: David Ohlemacher (@ohlemacher) options: host: description: - Host Name required: false cluster: description: - Cluster Name required: false volume: description: - Volume name to map to the host required: true state: description: - Creates mapping when present or removes when absent, or provides details of a mapping when stat. required: false default: present choices: [ "stat", "present", "absent" ] extends_documentation_fragment: - infinibox ''' EXAMPLES = r''' - name: Map a volume to an existing host infini_map: host: foo.example.com volume: bar state: present # Default user: admin password: secret system: ibox001 - name: Map a volume to an existing cluster infini_map: cluster: test-cluster volume: bar state: present # Default user: admin password: secret system: ibox001 - name: Unmap volume bar from host foo.example.com infini_map: host: foo.example.com volume: bar state: absent system: ibox01 user: admin password: secret - name: Stat mapping of volume bar to host foo.example.com infini_map: host: foo.example.com volume: bar state: stat system: ibox01 user: admin password: secret ''' # RETURN = r''' # ''' from infinisdk.core.exceptions import APICommandFailed, ObjectNotFound from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.infinidat.infinibox.plugins.module_utils.infinibox import \ HAS_INFINISDK, api_wrapper, infinibox_argument_spec, \ get_pool, get_system, get_volume, get_host, get_cluster, merge_two_dicts def vol_is_mapped_to_host(volume, host): volume_fields = volume.get_fields() volume_id = volume_fields.get('id') host_luns = host.get_luns() #print('volume id: {0}'.format(volume_id)) #print('host luns: {0}'.format(str(host_luns))) for lun in host_luns: if lun.volume == volume: #print('found mapped volume: {0}'.format(volume)) return True return False def vol_is_mapped_to_cluster(volume, cluster): volume_fields = volume.get_fields() volume_id = volume_fields.get('id') cluster_luns = cluster.get_luns() #print('volume id: {0}'.format(volume_id)) #print('host luns: {0}'.format(str(host_luns))) for lun in cluster_luns: #raise AssertionError("lun.volume: {0}, volume: {1}".format(lun.volume, volume)) if lun.volume == volume: #print('found mapped volume: {0}'.format(volume)) return True return False def find_host_lun_use(module, host, volume): check_result = {'lun_used': False, 'lun_volume_matches': False} desired_lun = module.params['lun'] if desired_lun: for host_lun in host.get_luns(): if desired_lun == host_lun.lun: if host_lun.volume == volume: check_result = {'lun_used': True, 'lun_volume_matches': True} else: check_result = {'lun_used': True, 'lun_volume_matches': False} return check_result def find_cluster_lun_use(module, cluster, volume): check_result = {'lun_used': False, 'lun_volume_matches': False} desired_lun = module.params['lun'] if desired_lun: for cluster_lun in cluster.get_luns(): if desired_lun == cluster.lun: if cluster.volume == volume: check_result = {'lun_used': True, 'lun_volume_matches': True} else: check_result = {'lun_used': True, 'lun_volume_matches': False} return check_result def find_host_lun(host, volume): found_lun = None luns = host.get_luns() for lun in luns: if lun.volume == volume: found_lun = lun.lun return found_lun def find_cluster_lun(cluster, volume): found_lun = None luns = cluster.get_luns() for lun in luns: if lun.volume == volume: found_lun = lun.lun return found_lun @api_wrapper def create_mapping(module, system): """ Create mapping of volume to host or cluster. If already mapped, exit_json with changed False. """ host_name = module.params['host'] cluster_name = module.params['cluster'] host = get_host(module, system) cluster = get_cluster(module, system) if host: changed = create_mapping_to_host(module, system) elif cluster: changed = create_mapping_to_cluster(module, system) else: msg = "A programming error has occurred in create_mapping()" module.fail_json(msg=msg) return changed @api_wrapper def create_mapping_to_cluster(module, system): """ Create mapping of volume to cluster. If already mapped, exit_json with changed False. """ changed = False cluster = get_cluster(module, system) volume = get_volume(module, system) lun_use = find_cluster_lun_use(module, cluster, volume) if lun_use['lun_used']: #assert not lun_use['lun_volume_matches'], "Cannot have matching lun and volume in create_mapping()" msg = "Cannot create mapping of volume '{}' to cluster '{}' using lun '{}'. Lun in use.".format( volume.get_name(), cluster.get_name(), module.params['lun']) module.fail_json(msg=msg) try: desired_lun = module.params['lun'] if not module.check_mode: cluster.map_volume(volume, lun=desired_lun) changed = True except APICommandFailed as err: if "is already mapped" not in str(err): module.fail_json('Cannot map volume {0} to cluster {1}: {2}. Already mapped.'.format( module.params['volume'], module.params['cluster'], str(err))) return changed @api_wrapper def create_mapping_to_host(module, system): """ Create mapping of volume to host. If already mapped, exit_json with changed False. """ changed = False host = system.hosts.get(name=module.params['host']) volume = get_volume(module, system) lun_use = find_host_lun_use(module, host, volume) if lun_use['lun_used']: #assert not lun_use['lun_volume_matches'], "Cannot have matching lun and volume in create_mapping()" msg = "Cannot create mapping of volume '{}' to host '{}' using lun '{}'. Lun in use.".format( volume.get_name(), host.get_name(), module.params['lun']) module.fail_json(msg=msg) try: desired_lun = module.params['lun'] if not module.check_mode: host.map_volume(volume, lun=desired_lun) changed = True except APICommandFailed as err: if "is already mapped" not in str(err): module.fail_json('Cannot map volume {0} to host {1}: {2}. Already mapped.'.format( module.params['volume'], module.params['host'], str(err))) return changed @api_wrapper def update_mapping_to_host(module, system): host = get_host(module, system) volume = get_volume(module, system) desired_lun = module.params['lun'] assert vol_is_mapped_to_host(volume, host) if desired_lun: found_lun = find_host_lun(host, volume) if found_lun != desired_lun: msg = "Cannot change the lun from '{}' to '{}' for existing mapping of volume '{}' to host '{}'".format( found_lun, desired_lun, volume.get_name(), host.get_name()) module.fail_json(msg=msg) changed = False return changed @api_wrapper def update_mapping_to_cluster(module, system): cluster = get_cluster(module, system) volume = get_volume(module, system) desired_lun = module.params['lun'] assert vol_is_mapped_to_cluster(volume, cluster) if desired_lun: found_lun = find_cluster_lun(cluster, volume) if found_lun != desired_lun: msg = "Cannot change the lun from '{}' to '{}' for existing mapping of volume '{}' to cluster '{}'".format( found_lun, desired_lun, volume.get_name(), cluster.get_name()) module.fail_json(msg=msg) changed = False return changed @api_wrapper def delete_mapping(module, system): host = get_host(module, system) cluster = get_cluster(module, system) if host: changed = delete_mapping_to_host(module, system) elif cluster: changed = delete_mapping_to_cluster(module, system) else: msg = "A programming error has occurred in delete_mapping()" module.fail_json(msg=msg) return changed @api_wrapper def delete_mapping_to_host(module, system): """ Remove mapping of volume from host. If the either the volume or host do not exist, then there should be no mapping to unmap. If unmapping generates a key error with 'has no logical units' in its message, then the volume is not mapped. Either case, return changed=False. """ changed = False msg = "" if not module.check_mode: volume = get_volume(module, system) host = get_host(module, system) if volume and host: try: existing_lun = find_host_lun(host, volume) host.unmap_volume(volume) changed = True msg = "Volume '{0}' was unmapped from host '{1}' freeing lun '{2}'".format( module.params['volume'], module.params['host'], existing_lun, ) except KeyError as err: if 'has no logical units' not in str(err): module.fail_json('Cannot unmap volume {0} from host {1}: {2}'.format( module.params['volume'], module.params['host'], str(err))) else: msg = "Volume {0} was not mapped to host {1} and so unmapping was not executed".format( module.params['volume'], module.params['host'], ) else: msg = "Either volume {0} or host {1} does not exist. Unmapping was not executed".format( module.params['volume'], module.params['host'], ) else: # check_mode changed = True module.exit_json(msg=msg, changed=changed) @api_wrapper def delete_mapping_to_cluster(module, system): """ Remove mapping of volume from cluster. If the either the volume or cluster do not exist, then there should be no mapping to unmap. If unmapping generates a key error with 'has no logical units' in its message, then the volume is not mapped. Either case, return changed=False. """ changed = False msg = "" if not module.check_mode: volume = get_volume(module, system) cluster = get_cluster(module, system) if volume and cluster: try: existing_lun = find_cluster_lun(cluster, volume) cluster.unmap_volume(volume) changed = True msg = "Volume '{0}' was unmapped from cluster '{1}' freeing lun '{2}'".format( module.params['volume'], module.params['cluster'], existing_lun, ) except KeyError as err: if 'has no logical units' not in str(err): module.fail_json('Cannot unmap volume {0} from cluster {1}: {2}'.format( module.params['volume'], module.params['cluster'], str(err))) else: msg = "Volume {0} was not mapped to cluster {1} and so unmapping was not executed".format( module.params['volume'], module.params['cluster'], ) else: msg = "Either volume {0} or cluster {1} does not exist. Unmapping was not executed".format( module.params['volume'], module.params['cluster'], ) else: # check_mode changed = True module.exit_json(msg=msg, changed=changed) def get_sys_vol_host_cluster(module): system = get_system(module) volume = get_volume(module, system) host = get_host(module, system) cluster = get_cluster(module, system) return (system, volume, host, cluster) def get_sys_vol_cluster(module): system = get_system(module) volume = get_volume(module, system) cluster = get_cluster(module, system) return (system, volume, cluster) def get_mapping_fields(volume, host_or_cluster): luns = host_or_cluster.get_luns() for lun in luns: if volume.get_name() == lun.volume.get_name(): field_dict = dict( id=lun.id, ) return field_dict return dict() def handle_stat(module): system, volume, host, cluster = get_sys_vol_host_cluster(module) volume_name = module.params['volume'] host_name = module.params['host'] if not host_name: host_name = "not specified" cluster_name = module.params['cluster'] if not cluster_name: cluster_name = "not specified" if not volume: module.fail_json(msg='Volume {0} not found'.format(volume_name)) if not host and not cluster: module.fail_json(msg='Neither host [{0}] nor cluster [{1}] found'.format(host_name, cluster_name)) if (not host or not vol_is_mapped_to_host(volume, host)) \ and (not cluster or not vol_is_mapped_to_cluster(volume, cluster)): msg = 'Volume {0} is mapped to neither host {1} nor cluster {2}'.format(volume_name, host_name, cluster_name) module.fail_json(msg=msg) if host: found_lun = find_host_lun(host, volume) field_dict = get_mapping_fields(volume, host) if found_lun is not None: msg = 'Volume {0} is mapped to host {1} using lun {2}'.format(volume_name, host_name, found_lun), result = dict( changed=False, volume_lun=found_lun, msg=msg, ) else: msg = 'Volume {0} is not mapped to host {1}'.format(volume_name, host_name) module.fail_json(msg=msg) elif cluster: found_lun = find_cluster_lun(cluster, volume) field_dict = get_mapping_fields(volume, cluster) if found_lun is not None: msg = 'Volume {0} is mapped to cluster {1} using lun {2}'.format(volume_name, cluster_name, found_lun) result = dict( changed=False, volume_lun=found_lun, msg=msg, ) else: msg = 'Volume {0} is not mapped to cluster {1}'.format(volume_name, cluster_name) module.fail_json(msg=msg) else: msg = 'A programming error has occurred in handle_stat()' module.fail_json(msg=msg) result = merge_two_dicts(result, field_dict) module.exit_json(**result) def handle_present(module): system, volume, host, cluster = get_sys_vol_host_cluster(module) volume_name = module.params['volume'] host_name = module.params['host'] cluster_name = module.params['cluster'] if not volume: module.fail_json(changed=False, msg='Volume {0} not found'.format(volume_name)) if not host and not cluster: if not host_name: host_name = "not specified" if not cluster_name: cluster_name = "not specified" module.fail_json(changed=False, msg='Neither host [{0}] nor cluster [{1}] found'.format(host_name, cluster_name)) if host: if not vol_is_mapped_to_host(volume, host): changed = create_mapping(module, system) # TODO: Why is find_host_lun() returning None after creating the mapping? # host.get_luns() returns an empty list, why? # existing_lun = find_host_lun(host, volume) # msg = "Volume '{0}' map to host '{1}' created using lun '{2}'".format( # volume.get_name(), # host.get_name(), # existing_lun, # ) msg = "Volume '{0}' map to host '{1}' created".format(volume_name, host_name) else: changed = update_mapping_to_host(module, system) existing_lun = find_host_lun(host, volume) msg = "Volume '{0}' map to host '{1}' already exists using lun '{2}'".format(volume_name, host_name, existing_lun) elif cluster: if not vol_is_mapped_to_cluster(volume, cluster): changed = create_mapping(module, system) # TODO: Why is find_host_lun() returning None after creating the mapping? # host.get_luns() returns an empty list, why? # existing_lun = find_host_lun(host, volume) # msg = "Volume '{0}' map to host '{1}' created using lun '{2}'".format( # volume.get_name(), # host.get_name(), # existing_lun, # ) msg = "Volume '{0}' map to cluster '{1}' created".format(volume_name, cluster_name) else: changed = update_mapping_to_cluster(module, system) existing_lun = find_cluster_lun(cluster, volume) msg = "Volume '{0}' map to cluster '{1}' already exists using lun '{2}'".format(volume_name, cluster_name, existing_lun) result = dict( changed=changed, msg=msg, ) module.exit_json(**result) def handle_absent(module): system, volume, host, cluster = get_sys_vol_host_cluster(module) volume_name = module.params['volume'] host_name = module.params['host'] cluster_name = module.params['cluster'] if not volume or (not host and not cluster): module.exit_json(changed=False, msg='Mapping of volume {0} to host {1} or cluster {2} already absent'.format(volume_name, host_name, cluster_name)) else: changed = delete_mapping(module, system) module.exit_json(changed=changed, msg="Mapping removed") def execute_state(module): state = module.params['state'] try: if state == 'stat': handle_stat(module) elif state == 'present': handle_present(module) elif state == 'absent': handle_absent(module) else: module.fail_json(msg='Internal handler error. Invalid state: {0}'.format(state)) finally: system = get_system(module) system.logout() def check_parameters(module): volume_name = module.params['volume'] host_name = module.params['host'] cluster_name = module.params['cluster'] if host_name and cluster_name: msg = "infini_map requires a host or a cluster but not both to be provided" module.fail_json(msg=msg) if not host_name and not cluster_name: msg = "infini_map requires a host or a cluster to be provided" module.fail_json(msg=msg) def main(): """ Gather auguments and manage mapping of vols to hosts. """ argument_spec = infinibox_argument_spec() argument_spec.update( dict( host=dict(required=False, default=""), cluster=dict(required=False, default=""), state=dict(default='present', choices=['stat', 'present', 'absent']), volume=dict(required=True), lun=dict(type=int), ) ) module = AnsibleModule(argument_spec, supports_check_mode=True) if not HAS_INFINISDK: module.fail_json(msg=missing_required_lib('infinisdk')) check_parameters(module) execute_state(module) if __name__ == '__main__': main()