#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2016, Alain Dejoux <adejoux@djouxtech.net>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r'''
---
author:
    - Alain Dejoux (@adejoux)
module: aix_lvol
short_description: Configure AIX LVM logical volumes
description:
  - This module creates, removes or resizes AIX logical volumes. Inspired by lvol module.
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  vg:
    description:
    - The volume group this logical volume is part of.
    type: str
    required: true
  lv:
    description:
    - The name of the logical volume.
    type: str
    required: true
  lv_type:
    description:
    - The type of the logical volume.
    type: str
    default: jfs2
  size:
    description:
    - The size of the logical volume with one of the [MGT] units.
    type: str
  copies:
    description:
    - The number of copies of the logical volume.
    - Maximum copies are 3.
    type: int
    default: 1
  policy:
    description:
    - Sets the interphysical volume allocation policy.
    - V(maximum) allocates logical partitions across the maximum number of physical volumes.
    - V(minimum) allocates logical partitions across the minimum number of physical volumes.
    type: str
    choices: [ maximum, minimum ]
    default: maximum
  state:
    description:
    - Control if the logical volume exists. If V(present) and the
      volume does not already exist then the O(size) option is required.
    type: str
    choices: [ absent, present ]
    default: present
  opts:
    description:
    - Free-form options to be passed to the mklv command.
    type: str
    default: ''
  pvs:
    description:
    - A list of physical volumes, for example V(hdisk1,hdisk2).
    type: list
    elements: str
    default: []
'''

EXAMPLES = r'''
- name: Create a logical volume of 512M
  community.general.aix_lvol:
    vg: testvg
    lv: testlv
    size: 512M

- name: Create a logical volume of 512M with disks hdisk1 and hdisk2
  community.general.aix_lvol:
    vg: testvg
    lv: test2lv
    size: 512M
    pvs: [ hdisk1, hdisk2 ]

- name: Create a logical volume of 512M mirrored
  community.general.aix_lvol:
    vg: testvg
    lv: test3lv
    size: 512M
    copies: 2

- name: Create a logical volume of 1G with a minimum placement policy
  community.general.aix_lvol:
    vg: rootvg
    lv: test4lv
    size: 1G
    policy: minimum

- name: Create a logical volume with special options like mirror pool
  community.general.aix_lvol:
    vg: testvg
    lv: testlv
    size: 512M
    opts: -p copy1=poolA -p copy2=poolB

- name: Extend the logical volume to 1200M
  community.general.aix_lvol:
    vg: testvg
    lv: test4lv
    size: 1200M

- name: Remove the logical volume
  community.general.aix_lvol:
    vg: testvg
    lv: testlv
    state: absent
'''

RETURN = r'''
msg:
  type: str
  description: A friendly message describing the task result.
  returned: always
  sample: Logical volume testlv created.
'''

import re

from ansible.module_utils.basic import AnsibleModule


def convert_size(module, size):
    unit = size[-1].upper()
    units = ['M', 'G', 'T']
    try:
        multiplier = 1024 ** units.index(unit)
    except ValueError:
        module.fail_json(msg="No valid size unit specified.")

    return int(size[:-1]) * multiplier


def round_ppsize(x, base=16):
    new_size = int(base * round(float(x) / base))
    if new_size < x:
        new_size += base
    return new_size


def parse_lv(data):
    name = None

    for line in data.splitlines():
        match = re.search(r"LOGICAL VOLUME:\s+(\w+)\s+VOLUME GROUP:\s+(\w+)", line)
        if match is not None:
            name = match.group(1)
            vg = match.group(2)
            continue
        match = re.search(r"LPs:\s+(\d+).*PPs", line)
        if match is not None:
            lps = int(match.group(1))
            continue
        match = re.search(r"PP SIZE:\s+(\d+)", line)
        if match is not None:
            pp_size = int(match.group(1))
            continue
        match = re.search(r"INTER-POLICY:\s+(\w+)", line)
        if match is not None:
            policy = match.group(1)
            continue

    if not name:
        return None

    size = lps * pp_size

    return {'name': name, 'vg': vg, 'size': size, 'policy': policy}


def parse_vg(data):

    for line in data.splitlines():

        match = re.search(r"VOLUME GROUP:\s+(\w+)", line)
        if match is not None:
            name = match.group(1)
            continue

        match = re.search(r"TOTAL PP.*\((\d+)", line)
        if match is not None:
            size = int(match.group(1))
            continue

        match = re.search(r"PP SIZE:\s+(\d+)", line)
        if match is not None:
            pp_size = int(match.group(1))
            continue

        match = re.search(r"FREE PP.*\((\d+)", line)
        if match is not None:
            free = int(match.group(1))
            continue

    return {'name': name, 'size': size, 'free': free, 'pp_size': pp_size}


def main():
    module = AnsibleModule(
        argument_spec=dict(
            vg=dict(type='str', required=True),
            lv=dict(type='str', required=True),
            lv_type=dict(type='str', default='jfs2'),
            size=dict(type='str'),
            opts=dict(type='str', default=''),
            copies=dict(type='int', default=1),
            state=dict(type='str', default='present', choices=['absent', 'present']),
            policy=dict(type='str', default='maximum', choices=['maximum', 'minimum']),
            pvs=dict(type='list', elements='str', default=list())
        ),
        supports_check_mode=True,
    )

    vg = module.params['vg']
    lv = module.params['lv']
    lv_type = module.params['lv_type']
    size = module.params['size']
    opts = module.params['opts']
    copies = module.params['copies']
    policy = module.params['policy']
    state = module.params['state']
    pvs = module.params['pvs']

    if policy == 'maximum':
        lv_policy = 'x'
    else:
        lv_policy = 'm'

    # Add echo command when running in check-mode
    if module.check_mode:
        test_opt = [module.get_bin_path("echo", required=True)]
    else:
        test_opt = []

    # check if system commands are available
    lsvg_cmd = module.get_bin_path("lsvg", required=True)
    lslv_cmd = module.get_bin_path("lslv", required=True)

    # Get information on volume group requested
    rc, vg_info, err = module.run_command([lsvg_cmd, vg])

    if rc != 0:
        if state == 'absent':
            module.exit_json(changed=False, msg="Volume group %s does not exist." % vg)
        else:
            module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, out=vg_info, err=err)

    this_vg = parse_vg(vg_info)

    if size is not None:
        # Calculate pp size and round it up based on pp size.
        lv_size = round_ppsize(convert_size(module, size), base=this_vg['pp_size'])

    # Get information on logical volume requested
    rc, lv_info, err = module.run_command([lslv_cmd, lv])

    if rc != 0:
        if state == 'absent':
            module.exit_json(changed=False, msg="Logical Volume %s does not exist." % lv)

    changed = False

    this_lv = parse_lv(lv_info)

    if state == 'present' and not size:
        if this_lv is None:
            module.fail_json(msg="No size given.")

    if this_lv is None:
        if state == 'present':
            if lv_size > this_vg['free']:
                module.fail_json(msg="Not enough free space in volume group %s: %s MB free." % (this_vg['name'], this_vg['free']))

            # create LV
            mklv_cmd = module.get_bin_path("mklv", required=True)

            cmd = test_opt + [mklv_cmd, "-t", lv_type, "-y", lv, "-c", copies, "-e", lv_policy, opts, vg, "%sM" % (lv_size, )] + pvs
            rc, out, err = module.run_command(cmd)
            if rc == 0:
                module.exit_json(changed=True, msg="Logical volume %s created." % lv)
            else:
                module.fail_json(msg="Creating logical volume %s failed." % lv, rc=rc, out=out, err=err)
    else:
        if state == 'absent':
            # remove LV
            rmlv_cmd = module.get_bin_path("rmlv", required=True)
            rc, out, err = module.run_command(test_opt + [rmlv_cmd, "-f", this_lv['name']])
            if rc == 0:
                module.exit_json(changed=True, msg="Logical volume %s deleted." % lv)
            else:
                module.fail_json(msg="Failed to remove logical volume %s." % lv, rc=rc, out=out, err=err)
        else:
            if this_lv['policy'] != policy:
                # change lv allocation policy
                chlv_cmd = module.get_bin_path("chlv", required=True)
                rc, out, err = module.run_command(test_opt + [chlv_cmd, "-e", lv_policy, this_lv['name']])
                if rc == 0:
                    module.exit_json(changed=True, msg="Logical volume %s policy changed: %s." % (lv, policy))
                else:
                    module.fail_json(msg="Failed to change logical volume %s policy." % lv, rc=rc, out=out, err=err)

            if vg != this_lv['vg']:
                module.fail_json(msg="Logical volume %s already exist in volume group %s" % (lv, this_lv['vg']))

            # from here the last remaining action is to resize it, if no size parameter is passed we do nothing.
            if not size:
                module.exit_json(changed=False, msg="Logical volume %s already exist." % (lv))

            # resize LV based on absolute values
            if int(lv_size) > this_lv['size']:
                extendlv_cmd = module.get_bin_path("extendlv", required=True)
                cmd = test_opt + [extendlv_cmd, lv, "%sM" % (lv_size - this_lv['size'], )]
                rc, out, err = module.run_command(cmd)
                if rc == 0:
                    module.exit_json(changed=True, msg="Logical volume %s size extended to %sMB." % (lv, lv_size))
                else:
                    module.fail_json(msg="Unable to resize %s to %sMB." % (lv, lv_size), rc=rc, out=out, err=err)
            elif lv_size < this_lv['size']:
                module.fail_json(msg="No shrinking of Logical Volume %s permitted. Current size: %s MB" % (lv, this_lv['size']))
            else:
                module.exit_json(changed=False, msg="Logical volume %s size is already %sMB." % (lv, lv_size))


if __name__ == '__main__':
    main()
