Repository: ansible-collections/community.general

Issue:
parted.py - Incorrect regex when parsing size output
SUMMARY

Using the parted module, I get an error stating: "Error interpreting parted size output: '1 MB' "

The arguments I am using are:
parted:
device: "{{ t_rawdevice }}"
label: msdos
unit: MiB
number: 1
flags: boot
part_start: 1MiB
part_end: 1025MiB
part_type: primary
state: present

I expected the requested partition to be created. I also expect the partition table to be created as well.
ISSUE TYPE

    Bug Report

COMPONENT NAME

parted
ANSIBLE VERSION

ansible-playbook 2.9.6
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/home/xxxxxx/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/dist-packages/ansible
executable location = /usr/bin/ansible-playbook
python version = 2.7.17 (default, Nov 7 2019, 10:07:09) [GCC 7.4.0]
OS / ENVIRONMENT

parted on the target
parted (GNU parted) 3.2
STEPS TO REPRODUCE

1 - Start with an empty device, no partition table.
2 - Use this this play:
- name: Create table and main root FS partition
parted:
device: "{{ t_rawdevice }}"
label: msdos
unit: MiB
number: 1
flags: boot
part_start: 1MiB
part_end: 1025MiB
part_type: primary
state: present
3 - Execute ansible-playbook, using correct option for your environment
EXPECTED RESULTS

I expected the requested partition to be created. I also expect the partition table to be created as well.
ACTUAL RESULTS

"Error interpreting parted size output: '1 MB'"
How to fix the issue

I did modify parted.py, where the following line
matches = re.search(r'^([\d.]+)([\w%]+)?$', size_str)

was changed to
matches = re.search(r'^([\d.]+) ([\w%]+)?$', size_str) # Notice the extra space

But then I got the error:
"Error interpreting parted size output: '10240MB'"

I changed again the parted.py line, this time to
matches = re.search(r'^([\d.]+) *([\w%]+)?$', size_str) # Notice the * after the added space

This time, the module runs smoothly and the expected partition table and partition were created.


Discussion:
Files identified in the description:
* [`plugins/modules/system/parted.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/parted.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 @ColOfAbRiX @MorrisA @bcoca @d-little @flynn1973 @gforster @kairoaraujo @marvin-sinister @mator @molekuul @ramooncamacho @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/system/parted.py b/plugins/modules/system/parted.py
--- a/plugins/modules/system/parted.py
+++ b/plugins/modules/system/parted.py
@@ -241,7 +241,7 @@ def parse_unit(size_str, unit=''):
     """
     Parses a string containing a size or boundary information
     """
-    matches = re.search(r'^(-?[\d.]+)([\w%]+)?$', size_str)
+    matches = re.search(r'^(-?[\d.]+) *([\w%]+)?$', size_str)
     if matches is None:
         # "<cylinder>,<head>,<sector>" format
         matches = re.search(r'^(\d+),(\d+),(\d+)$', size_str)
@@ -500,25 +500,22 @@ def check_parted_label(device):
     return False
 
 
-def parted_version():
+def parse_parted_version(out):
     """
-    Returns the major and minor version of parted installed on the system.
+    Returns version tuple from the output of "parted --version" command
     """
-    global module, parted_exec
-
-    rc, out, err = module.run_command("%s --version" % parted_exec)
-    if rc != 0:
-        module.fail_json(
-            msg="Failed to get parted version.", rc=rc, out=out, err=err
-        )
-
     lines = [x for x in out.split('\n') if x.strip() != '']
     if len(lines) == 0:
-        module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
+        return None, None, None
+
+    # Sample parted versions (see as well test unit):
+    # parted (GNU parted) 3.3
+    # parted (GNU parted) 3.4.5
+    # parted (GNU parted) 3.3.14-dfc61
+    matches = re.search(r'^parted.+\s(\d+)\.(\d+)(?:\.(\d+))?', lines[0].strip())
 
-    matches = re.search(r'^parted.+(\d+)\.(\d+)(?:\.(\d+))?$', lines[0])
     if matches is None:
-        module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
+        return None, None, None
 
     # Convert version to numbers
     major = int(matches.group(1))
@@ -530,6 +527,25 @@ def parted_version():
     return major, minor, rev
 
 
+def parted_version():
+    """
+    Returns the major and minor version of parted installed on the system.
+    """
+    global module, parted_exec
+
+    rc, out, err = module.run_command("%s --version" % parted_exec)
+    if rc != 0:
+        module.fail_json(
+            msg="Failed to get parted version.", rc=rc, out=out, err=err
+        )
+
+    (major, minor, rev) = parse_parted_version(out)
+    if major is None:
+        module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
+
+    return major, minor, rev
+
+
 def parted(script, device, align):
     """
     Runs a parted script.


Test patch:
diff --git a/tests/unit/plugins/modules/system/test_parted.py b/tests/unit/plugins/modules/system/test_parted.py
--- a/tests/unit/plugins/modules/system/test_parted.py
+++ b/tests/unit/plugins/modules/system/test_parted.py
@@ -6,6 +6,7 @@
 
 from ansible_collections.community.general.tests.unit.compat.mock import patch, call
 from ansible_collections.community.general.plugins.modules.system import parted as parted_module
+from ansible_collections.community.general.plugins.modules.system.parted import parse_parted_version
 from ansible_collections.community.general.plugins.modules.system.parted import parse_partition_info
 from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
 
@@ -17,6 +18,32 @@
 2:106MB:368MB:262MB:ext2::;
 3:368MB:256061MB:255692MB:::;"""
 
+parted_version_info = {"""
+        parted (GNU parted) 3.3
+        Copyright (C) 2019 Free Software Foundation, Inc.
+        License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+        This is free software: you are free to change and redistribute it.
+        There is NO WARRANTY, to the extent permitted by law.
+
+        Written by <http://git.debian.org/?p=parted/parted.git;a=blob_plain;f=AUTHORS>.
+        """: (3, 3, 0), """
+        parted (GNU parted) 3.4.5
+        Copyright (C) 2019 Free Software Foundation, Inc.
+        License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+        This is free software: you are free to change and redistribute it.
+        There is NO WARRANTY, to the extent permitted by law.
+
+        Written by <http://git.debian.org/?p=parted/parted.git;a=blob_plain;f=AUTHORS>.
+        """: (3, 4, 5), """
+        parted (GNU parted) 3.3.14-dfc61
+        Copyright (C) 2019 Free Software Foundation, Inc.
+        License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
+        This is free software: you are free to change and redistribute it.
+        There is NO WARRANTY, to the extent permitted by law.
+
+        Written by <http://git.debian.org/?p=parted/parted.git;a=blob_plain;f=AUTHORS>.
+        """: (3, 3, 14)}
+
 # corresponding dictionary after parsing by parse_partition_info
 parted_dict1 = {
     "generic": {
@@ -311,3 +338,8 @@ def test_check_mode_changed(self):
         })
         with patch('ansible_collections.community.general.plugins.modules.system.parted.get_device_info', return_value=parted_dict3):
             self.execute_module(changed=True)
+
+    def test_version_info(self):
+        """Test that the parse_parted_version returns the expected tuple"""
+        for key, value in parted_version_info.items():
+            self.assertEqual(parse_parted_version(key), value)