#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2020, 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_vol version_added: 2.3 short_description: Create, Delete or Modify volumes on Infinibox description: - This module creates, deletes or modifies a volume on Infinibox. author: Gregory Shulov (@GR360RY) options: name: description: - Volume Name required: true state: description: - Creates/Modifies master volume or snapshot when present or removes when absent. required: false default: present choices: [ "stat", "present", "absent" ] size: description: - Volume size in MB, GB or TB units. Required for creating a master volume, but not a snapshot required: false snapshot_lock_expires_at: description: - This will cause a snapshot to be locked at the specified date-time. Uses python's datetime format YYYY-mm-dd HH:MM:SS.ffffff, e.g. 2020-02-13 16:21:59.699700 required: false snapshot_lock_only: description: - This will lock an existing snapshot but will suppress refreshing the snapshot. type: bool required: false default: false thin_provision: description: - Whether the master volume should be thin provisioned. Required for creating a master volume, but not a snapshot. type: bool required: false default: true version_added: '2.8' pool: description: - Pool that master volume will reside within. Required for creating a master volume, but not a snapshot. required: false volume_type: description: - Specifies the volume type, regular volume or snapshot. required: false default: master choices: [ "master", "snapshot" ] write_protected: description: - Specifies if the volume should be write protected. Default will be True for snapshots, False for regular volumes. required: false default: "Default" choices: ["Default", "True", "False"] version_added: '2.10' parent_volume_name: description: - Specify a volume name. This is the volume parent for creating a snapshot. Required if volume_type is snapshot. required: false extends_documentation_fragment: - infinibox requirements: - capacity ''' EXAMPLES = r''' - name: Create new volume named foo under pool named bar infini_vol: name: foo # volume_type: master # Default size: 1TB thin_provision: yes pool: bar state: present user: admin password: secret system: ibox001 - name: Create snapshot named foo_snap from volume named foo infini_vol: name: foo_snap volume_type: snapshot parent_volume_name: foo state: present user: admin password: secret system: ibox001 - name: Stat snapshot, also a volume, named foo_snap infini_vol: name: foo_snap state: present user: admin password: secret system: ibox001 - name: Remove snapshot, also a volume, named foo_snap infini_vol: name: foo_snap state: absent user: admin password: secret system: ibox001 ''' # RETURN = r''' # ''' try: from capacity import KiB, Capacity HAS_CAPACITY = True except ImportError: HAS_CAPACITY = False import arrow 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, ObjectNotFound, \ get_pool, get_system, get_volume, get_vol_sn @api_wrapper def create_volume(module, system): """Create Volume""" if not module.check_mode: if module.params['thin_provision']: prov_type = 'THIN' else: prov_type = 'THICK' pool = get_pool(module, system) volume = system.volumes.create(name=module.params['name'], provtype=prov_type, pool=pool) if module.params['size']: size = Capacity(module.params['size']).roundup(64 * KiB) volume.update_size(size) if module.params['write_protected'] is not None: is_write_prot = volume.is_write_protected() desired_is_write_prot = module.params['write_protected'] if is_write_prot != desired_is_write_prot: volume.update_field('write_protected', desired_is_write_prot) changed = True return changed @api_wrapper def update_volume(module, volume): """Update Volume""" changed = False if module.params['size']: size = Capacity(module.params['size']).roundup(64 * KiB) if volume.get_size() != size: if not module.check_mode: volume.update_size(size) changed = True if module.params['thin_provision'] is not None: type = str(volume.get_provisioning()) if type == 'THICK' and module.params['thin_provision']: if not module.check_mode: volume.update_provisioning('THIN') changed = True if type == 'THIN' and not module.params['thin_provision']: if not module.check_mode: volume.update_provisioning('THICK') changed = True if module.params['write_protected'] is not None: is_write_prot = volume.is_write_protected() desired_is_write_prot = module.params['write_protected'] if is_write_prot != desired_is_write_prot: volume.update_field('write_protected', desired_is_write_prot) return changed @api_wrapper def delete_volume(module, volume): """ Delete Volume. Volume could be a snapshot.""" if not module.check_mode: volume.delete() changed = True return True @api_wrapper def create_snapshot(module, system): """Create Snapshot from parent volume""" snapshot_name = module.params['name'] parent_volume_name = module.params['parent_volume_name'] try: parent_volume = system.volumes.get(name=parent_volume_name) except ObjectNotFound as err: msg = 'Cannot create snapshot {}. Parent volume {} not found'.format( snapshot_name, parent_volume_name) module.fail_json(msg=msg) if not parent_volume: msg = "Cannot find new snapshot's parent volume named {}".format(parent_volume_name) module.fail_json(msg=msg) if not module.check_mode: if module.params['snapshot_lock_only']: msg = "Snapshot does not exist. Cannot comply with 'snapshot_lock_only: true'." module.fail_json(msg=msg) check_snapshot_lock_options(module) snapshot = parent_volume.create_snapshot(name=snapshot_name) if module.params['write_protected'] is not None: is_write_prot = snapshot.is_write_protected() desired_is_write_prot = module.params['write_protected'] if is_write_prot != desired_is_write_prot: snapshot.update_field('write_protected', desired_is_write_prot) manage_snapshot_locks(module, snapshot) changed = True return changed @api_wrapper def update_snapshot(module, snapshot): """ Update/refresh snapshot. May also lock it. """ refresh_changed = False if not module.params['snapshot_lock_only']: snap_is_locked = snapshot.get_lock_state() == "LOCKED" if not snap_is_locked: if not module.check_mode: snapshot.refresh_snapshot() refresh_changed = True else: msg = "Snapshot is locked and may not be refreshed" module.fail_json(msg=msg) check_snapshot_lock_options(module) lock_changed = manage_snapshot_locks(module, snapshot) if not module.check_mode: if module.params['write_protected'] is not None: is_write_prot = snapshot.is_write_protected() desired_is_write_prot = module.params['write_protected'] if is_write_prot != desired_is_write_prot: snapshot.update_field('write_protected', desired_is_write_prot) return refresh_changed or lock_changed def get_sys_pool_vol_parname(module): system = get_system(module) pool = get_pool(module, system) if module.params['name']: volume = get_volume(module, system) else: volume = get_vol_sn(module, system) parname = module.params['parent_volume_name'] return (system, pool, volume, parname) def check_snapshot_lock_options(module): """ Check if specified options are feasible for a snapshot. Prevent very long lock times. max_delta_minutes limits locks to 30 days (43200 minutes). This functionality is broken out from manage_snapshot_locks() to allow it to be called by create_snapshot() before the snapshot is actually created. """ snapshot_lock_expires_at = module.params['snapshot_lock_expires_at'] if snapshot_lock_expires_at: # Then user has specified wish to lock snap lock_expires_at = arrow.get(snapshot_lock_expires_at) # Check for lock in the past now = arrow.utcnow() if lock_expires_at <= now: msg = "Cannot lock snapshot with a snapshot_lock_expires_at " msg += "of '{}' from the past".format(snapshot_lock_expires_at) module.fail_json(msg=msg) # Check for lock later than max lock, i.e. too far in future. max_delta_minutes = 43200 # 30 days in minutes max_lock_expires_at = now.shift(minutes=max_delta_minutes) if lock_expires_at >= max_lock_expires_at: msg = "snapshot_lock_expires_at exceeds {} days in the future".format( max_delta_minutes//24//60) module.fail_json(msg=msg) def manage_snapshot_locks(module, snapshot): """ Manage the locking of a snapshot. Check for bad lock times. See check_snapshot_lock_options() which has additional checks. """ name = module.params["name"] snapshot_lock_expires_at = module.params['snapshot_lock_expires_at'] snap_is_locked = snapshot.get_lock_state() == "LOCKED" current_lock_expires_at = snapshot.get_lock_expires_at() changed = False check_snapshot_lock_options(module) if snapshot_lock_expires_at: # Then user has specified wish to lock snap lock_expires_at = arrow.get(snapshot_lock_expires_at) if snap_is_locked and lock_expires_at < current_lock_expires_at: # Lock earlier than current lock msg = "snapshot_lock_expires_at '{}' preceeds the current lock time of '{}'".format( lock_expires_at, current_lock_expires_at) module.fail_json(msg=msg) elif snap_is_locked and lock_expires_at == current_lock_expires_at: # Lock already set to correct time pass else: # Set lock if not module.check_mode: snapshot.update_lock_expires_at(lock_expires_at) changed = True return changed def handle_stat(module): system, pool, volume, parname = get_sys_pool_vol_parname(module) if not volume: msg = "Volume {} not found. Cannot stat.".format(module.params['name']) module.fail_json(msg=msg) fields = volume.get_fields() #from_cache=True, raw_value=True) created_at = str(fields.get('created_at', None)) has_children = fields.get('has_children', None) lock_expires_at= str(volume.get_lock_expires_at()) lock_state = volume.get_lock_state() mapped = str(fields.get('mapped', None)) name = fields.get('name', None) parent_id = fields.get('parent_id', None) serial = str(volume.get_serial()) size = str(volume.get_size()) updated_at = str(fields.get('updated_at', None)) used = str(fields.get('used_size', None)) volume_id = fields.get('id', None) volume_type = fields.get('type', None) write_protected = fields.get('write_protected', None) if volume_type == 'SNAPSHOT': msg = 'Snapshot stat found' else: msg = 'Volume stat found' result = dict( changed=False, name=name, created_at=created_at, has_children=has_children, lock_expires_at=lock_expires_at, lock_state=lock_state, mapped=mapped, msg=msg, parent_id=parent_id, serial=serial, size=size, updated_at=updated_at, used=used, volume_id=volume_id, volume_type=volume_type, write_protected=write_protected, ) module.exit_json(**result) def handle_present(module): system, pool, volume, parname = get_sys_pool_vol_parname(module) volume_type = module.params['volume_type'] if volume_type == 'master': if not volume: changed = create_volume(module, system) module.exit_json(changed=changed, msg="Volume created") else: changed = update_volume(module, volume) module.exit_json(changed=changed, msg="Volume updated") elif volume_type == 'snapshot': snapshot = volume if not snapshot: changed = create_snapshot(module, system) module.exit_json(changed=changed, msg="Snapshot created") else: changed = update_snapshot(module, snapshot) module.exit_json(changed=changed, msg="Snapshot updated") else: module.fail_json(msg='A programming error has occurred') def handle_absent(module): system, pool, volume, parname = get_sys_pool_vol_parname(module) volume_type = module.params['volume_type'] if volume and volume.get_lock_state() == "LOCKED": msg = "Cannot delete snapshot. Locked." module.fail_json(msg=msg) if volume_type == 'master': if not volume: module.exit_json(changed=False, msg="Volume already absent") else: changed = delete_volume(module, volume) module.exit_json(changed=changed, msg="Volume removed") elif volume_type == 'snapshot': if not volume: module.exit_json(changed=False, msg="Snapshot already absent") else: snapshot = volume changed = delete_volume(module, snapshot) module.exit_json(changed=changed, msg="Snapshot removed") else: module.fail_json(msg='A programming error has occured') def execute_state(module): # Handle different write_protected defaults depending on volume_type. if module.params['volume_type'] == "snapshot": if module.params['write_protected'] == 'Default': module.params['write_protected'] = True elif module.params['volume_type'] == "master": if module.params['write_protected'] == 'Default': module.params['write_protected'] = False else: msg = "A programming error has occurred handling volume type value" module.fail_json(msg=msg) assert module.params['write_protected'] in [True, False] 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_options(module): """Verify module options are sane""" state = module.params['state'] size = module.params['size'] pool = module.params['pool'] volume_type = module.params['volume_type'] parent_volume_name = module.params['parent_volume_name'] if state == 'present': if volume_type == 'master': if state == 'present': if parent_volume_name: msg = "parent_volume_name should not be specified " msg += "if volume_type is 'volume'. Snapshots only." module.fail_json(msg=msg) if not size: msg = "Size is required to create a volume" module.fail_json(msg=msg) elif volume_type == "snapshot": if size or pool: msg = "Neither pool nor size should not be specified " msg += "for volume_type snapshot" module.fail_json(msg=msg) if state == "present": if not parent_volume_name: msg = "For state 'present' and volume_type 'snapshot', " msg += "parent_volume_name is required" module.fail_json(msg=msg) else: msg = "A programming error has occurred" module.fail_json(msg=msg) def main(): argument_spec = infinibox_argument_spec() argument_spec.update( dict( name=dict(required=False), parent_volume_name=dict(required=False), pool=dict(required=False), size=dict(), serial=dict(), snapshot_lock_expires_at=dict(), snapshot_lock_only=dict(type='bool', default=False), state=dict(default='present', choices=['stat', 'present', 'absent']), thin_provision=dict(type='bool', default=True), write_protected=dict(default='Default', choices=['Default', 'True', 'False']), volume_type=dict(default='master', choices=['master', 'snapshot']), ) ) module = AnsibleModule(argument_spec, supports_check_mode=True) if not HAS_INFINISDK: module.fail_json(msg=missing_required_lib('infinisdk')) if module.params['size']: try: Capacity(module.params['size']) except Exception: module.fail_json(msg='size (Physical Capacity) should be defined in MB, GB, TB or PB units') check_options(module) execute_state(module) if __name__ == '__main__': main()