Repository: ansible-collections/community.general

Issue:
lvol: extending to ' +100%FREE' calculating wrong size
### Summary

When I try to extend lv to max free available disk space, it calculates the wrong size after upgrading from 2.5.1 to current release (3.0.0)

Error:
--------
```
fatal: [vm-001]: FAILED! => {"changed": false, "msg": "Logical Volume root could not be extended. Not enough free space left (5120.0m required / 5116.0m available)"}
```


Possibly related to:
-------------------------
https://github.com/ansible-collections/community.general/issues/1988
https://github.com/ansible-collections/community.general/pull/2267

### Issue Type

Bug Report

### Component Name

lvol

### Ansible Version

```console (paste below)
ansible 2.10.8
  config file = /home/dominik.kaminski/ansible/ansible.cfg
  configured module search path = ['/home/dominik.kaminski/ansible/library']
  ansible python module location = /home/dominik.kaminski/.local/lib/python3.6/site-packages/ansible
  executable location = /home/dominik.kaminski/.local/bin/ansible
  python version = 3.6.8 (default, Aug 24 2020, 17:57:11) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]

```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

UCS

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: "Extend logical volume to available space"
  community.general.lvol:
    vg: "{{ extend_root_lvm_volume_lvm_vg_name }}"
    lv: "{{ extend_root_lvm_volume_lvm_data_volume }}"
    resizefs: "yes"
    size: "+100%FREE"
  tags:
    - extend_root_lvm_volume_resize
```


### Expected Results

Volumes should be extended to available free space or calculating free available size correct.

### Actual Results

```console (paste below)
fatal: [vm-001]: FAILED! => {"changed": false, "msg": "Logical Volume root could not be extended. Not enough free space left (5120.0m required / 5116.0m available)"}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/system/lvol.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/lvol.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @abulimov @jhoekx @unkaputtbar112 @zigaSRC
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@dominikkaminski can you check whether the PR #2369 fixes the issue for you?
@felixfontein I'm sorry to say but it didn't fix my issue.
@dominikkaminski might have been a lucky guess :)
@zigaSRC in that case #2267 might have introduced another problem.
I just tested a few scenarios, I see I should have tested the module more... 

The #2369 had no chance of fixing it since this bug is related to the calculation of the logical volume size in the module. That's the concern I mentioned when I was asking if it's smart to do the calculations inside the module and not just let the tool handle it.

Here's a version of the documentation for LVEXTEND(8) --size option which is being used in this case:

> The resulting value is rounded upward. N.B. In a future release, when expressed as a percentage with PVS, VG or FREE, the number will be treated as an approximate upper limit for the total number of physical extents to be allocated (including extents used by any mirrors, for example). The code may currently allocate more space than you might otherwise expect.

As mentioned there are slight differences between versions and the documentation also differs. Given that the size is dependant on the version of the tool on the system, it's most likely preferable to do the calculation in the module for consistency's sake. We have a couple of options:

- Round the calculated size down to the nearest extent. 
- Round up to the nearest extent like it's mentioned in the documentation but also provide an upper limit equal to the amount of free space available. 
- Leave it to the tool to decide; pass the % option through to the tool. 


For a bit of extra context, I think this is because you're trying to round up/down when you don't need to.

I create a new LV, with space `+100%FREE`. I then re-run the lvol module. My `size_requested` is calculated initially as zero as there is no free space, and my lv size (11260) is a multiple of the vg ext_size (4). See annotated code below.

lvol.py#L511
```
        size_requested += this_lv['size']        # 0 +=  0 + 11260.0
        size_requested += this_vg['ext_size'] - (size_requested % this_vg['ext_size'])  # 11260 += 4 - (11260 % 4)
        # Result is size_requested = 11264.
````
A fix could be to add a protection whereby is the size_requested is zero, simply return rather than trying to attempt rounding? Or only add the second line above `if size_requested % this_vg['ext_size'] != 0`
That's true. I missed that one. 
Could just do the rounding in a more proper way to solve this bug: `this_vg['ext_size'] * ceil(size_requested / this_vg['ext_size'])`

This will still leave us with the discrepancy between the newer versions of the actual tools and the module though. 

Resolving patch:
diff --git a/plugins/modules/system/lvol.py b/plugins/modules/system/lvol.py
--- a/plugins/modules/system/lvol.py
+++ b/plugins/modules/system/lvol.py
@@ -389,7 +389,7 @@ def main():
     # Get information on volume group requested
     vgs_cmd = module.get_bin_path("vgs", required=True)
     rc, current_vgs, err = module.run_command(
-        "%s --noheadings --nosuffix -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit, vg))
+        "%s --noheadings --nosuffix -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit.lower(), vg))
 
     if rc != 0:
         if state == 'absent':
@@ -403,7 +403,7 @@ def main():
     # Get information on logical volume requested
     lvs_cmd = module.get_bin_path("lvs", required=True)
     rc, current_lvs, err = module.run_command(
-        "%s -a --noheadings --nosuffix -o lv_name,size,lv_attr --units %s --separator ';' %s" % (lvs_cmd, unit, vg))
+        "%s -a --noheadings --nosuffix -o lv_name,size,lv_attr --units %s --separator ';' %s" % (lvs_cmd, unit.lower(), vg))
 
     if rc != 0:
         if state == 'absent':
@@ -505,16 +505,13 @@ def main():
             else:  # size_whole == 'FREE':
                 size_requested = size_percent * this_vg['free'] / 100
 
-            # from LVEXTEND(8) - The resulting value is rounded upward.
-            # from LVREDUCE(8) - The resulting value for the substraction is rounded downward, for the absolute size it is rounded upward.
             if size_operator == '+':
                 size_requested += this_lv['size']
-                size_requested += this_vg['ext_size'] - (size_requested % this_vg['ext_size'])
             elif size_operator == '-':
                 size_requested = this_lv['size'] - size_requested
-                size_requested -= (size_requested % this_vg['ext_size'])
-            else:
-                size_requested += this_vg['ext_size'] - (size_requested % this_vg['ext_size'])
+
+            # According to latest documentation (LVM2-2.03.11) all tools round down
+            size_requested -= (size_requested % this_vg['ext_size'])
 
             if this_lv['size'] < size_requested:
                 if (size_free > 0) and (size_free >= (size_requested - this_lv['size'])):


Test patch: