mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-06-11 10:35:34 +00:00
* parted: ignore MBR partition type codes reported as flags by SUSE parted SUSE's patched parted reports MBR partition type codes (e.g., type=8e for Linux LVM) in the machine-parseable flags output. The module was trying to unset these pseudo-flags via 'parted set N type=8e off', which is not a valid parted command, causing the task to fail when using flags: [lvm] on msdos-labelled disks on SUSE systems. Fixes #6292 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(changelog): add fragment for PR 12121 * Update changelogs/fragments/12121-parted-suse-msdos-type-code.yml Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Felix Fontein <felix@fontein.de>
551 lines
18 KiB
Python
551 lines
18 KiB
Python
# (c) 2017 Red Hat Inc.
|
|
# 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 annotations
|
|
|
|
from unittest.mock import call, patch
|
|
|
|
from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import (
|
|
AnsibleExitJson,
|
|
AnsibleFailJson,
|
|
ModuleTestCase,
|
|
set_module_args,
|
|
)
|
|
|
|
from ansible_collections.community.general.plugins.modules import parted as parted_module
|
|
from ansible_collections.community.general.plugins.modules.parted import parse_parted_version, parse_partition_info
|
|
|
|
# Example of output : parted -s -m /dev/sdb -- unit 'MB' print
|
|
parted_output1 = """
|
|
BYT;
|
|
/dev/sdb:286061MB:scsi:512:512:msdos:ATA TOSHIBA THNSFJ25:;
|
|
1:1.05MB:106MB:105MB:fat32::esp;
|
|
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": {
|
|
"dev": "/dev/sdb",
|
|
"size": 286061.0,
|
|
"unit": "mb",
|
|
"table": "msdos",
|
|
"model": "ATA TOSHIBA THNSFJ25",
|
|
"logical_block": 512,
|
|
"physical_block": 512,
|
|
},
|
|
"partitions": [
|
|
{
|
|
"num": 1,
|
|
"begin": 1.05,
|
|
"end": 106.0,
|
|
"size": 105.0,
|
|
"fstype": "fat32",
|
|
"name": "",
|
|
"flags": ["esp"],
|
|
"unit": "mb",
|
|
},
|
|
{
|
|
"num": 2,
|
|
"begin": 106.0,
|
|
"end": 368.0,
|
|
"size": 262.0,
|
|
"fstype": "ext2",
|
|
"name": "",
|
|
"flags": [],
|
|
"unit": "mb",
|
|
},
|
|
{
|
|
"num": 3,
|
|
"begin": 368.0,
|
|
"end": 256061.0,
|
|
"size": 255692.0,
|
|
"fstype": "",
|
|
"name": "",
|
|
"flags": [],
|
|
"unit": "mb",
|
|
},
|
|
],
|
|
}
|
|
|
|
parted_output2 = """
|
|
BYT;
|
|
/dev/sdb:286061MB:scsi:512:512:msdos:ATA TOSHIBA THNSFJ25:;"""
|
|
|
|
# corresponding dictionary after parsing by parse_partition_info
|
|
parted_dict2 = {
|
|
"generic": {
|
|
"dev": "/dev/sdb",
|
|
"size": 286061.0,
|
|
"unit": "mb",
|
|
"table": "msdos",
|
|
"model": "ATA TOSHIBA THNSFJ25",
|
|
"logical_block": 512,
|
|
"physical_block": 512,
|
|
},
|
|
"partitions": [],
|
|
}
|
|
|
|
# fake some_flag exists
|
|
parted_dict3 = {
|
|
"generic": {
|
|
"dev": "/dev/sdb",
|
|
"size": 286061.0,
|
|
"unit": "mb",
|
|
"table": "msdos",
|
|
"model": "ATA TOSHIBA THNSFJ25",
|
|
"logical_block": 512,
|
|
"physical_block": 512,
|
|
},
|
|
"partitions": [
|
|
{
|
|
"num": 1,
|
|
"begin": 1.05,
|
|
"end": 106.0,
|
|
"size": 105.0,
|
|
"fstype": "fat32",
|
|
"name": "",
|
|
"flags": ["some_flag"],
|
|
"unit": "mb",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
# Simulates SUSE-patched parted that exposes MBR type codes as flags.
|
|
# Partition 1 has "lvm" set + SUSE adds "type=8e" as a pseudo-flag.
|
|
parted_dict_suse_lvm = {
|
|
"generic": {
|
|
"dev": "/dev/sdb",
|
|
"size": 286061.0,
|
|
"unit": "mb",
|
|
"table": "msdos",
|
|
"model": "ATA TOSHIBA THNSFJ25",
|
|
"logical_block": 512,
|
|
"physical_block": 512,
|
|
},
|
|
"partitions": [
|
|
{
|
|
"num": 1,
|
|
"begin": 1.05,
|
|
"end": 106.0,
|
|
"size": 105.0,
|
|
"fstype": "",
|
|
"name": "",
|
|
"flags": ["lvm", "type=8e"],
|
|
"unit": "mb",
|
|
}
|
|
],
|
|
}
|
|
|
|
# Simulates SUSE-patched parted where partition has no logical flags yet,
|
|
# but SUSE reports "type=83" (Linux MBR type code) as a pseudo-flag.
|
|
parted_dict_suse_noflag = {
|
|
"generic": {
|
|
"dev": "/dev/sdb",
|
|
"size": 286061.0,
|
|
"unit": "mb",
|
|
"table": "msdos",
|
|
"model": "ATA TOSHIBA THNSFJ25",
|
|
"logical_block": 512,
|
|
"physical_block": 512,
|
|
},
|
|
"partitions": [
|
|
{
|
|
"num": 1,
|
|
"begin": 1.05,
|
|
"end": 106.0,
|
|
"size": 105.0,
|
|
"fstype": "",
|
|
"name": "",
|
|
"flags": ["type=83"],
|
|
"unit": "mb",
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class TestParted(ModuleTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.module = parted_module
|
|
self.mock_check_parted_label = patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.check_parted_label", return_value=False
|
|
)
|
|
self.check_parted_label = self.mock_check_parted_label.start()
|
|
|
|
self.mock_parted = patch("ansible_collections.community.general.plugins.modules.parted.parted")
|
|
self.parted = self.mock_parted.start()
|
|
|
|
self.mock_run_command = patch("ansible.module_utils.basic.AnsibleModule.run_command")
|
|
self.run_command = self.mock_run_command.start()
|
|
|
|
self.mock_get_bin_path = patch("ansible.module_utils.basic.AnsibleModule.get_bin_path")
|
|
self.get_bin_path = self.mock_get_bin_path.start()
|
|
|
|
def tearDown(self):
|
|
super().tearDown()
|
|
self.mock_run_command.stop()
|
|
self.mock_get_bin_path.stop()
|
|
self.mock_parted.stop()
|
|
self.mock_check_parted_label.stop()
|
|
|
|
def execute_module(self, failed=False, changed=False, script=None):
|
|
if failed:
|
|
result = self.failed()
|
|
self.assertTrue(result["failed"], result)
|
|
else:
|
|
result = self.changed(changed)
|
|
self.assertEqual(result["changed"], changed, result)
|
|
|
|
if script:
|
|
self.assertEqual(script, result["script"], result["script"])
|
|
|
|
return result
|
|
|
|
def failed(self):
|
|
with self.assertRaises(AnsibleFailJson) as exc:
|
|
self.module.main()
|
|
|
|
result = exc.exception.args[0]
|
|
self.assertTrue(result["failed"], result)
|
|
return result
|
|
|
|
def changed(self, changed=False):
|
|
with self.assertRaises(AnsibleExitJson) as exc:
|
|
self.module.main()
|
|
|
|
result = exc.exception.args[0]
|
|
self.assertEqual(result["changed"], changed, result)
|
|
return result
|
|
|
|
def test_parse_partition_info(self):
|
|
"""Test that the parse_partition_info returns the expected dictionary"""
|
|
self.assertEqual(parse_partition_info(parted_output1, "MB", False), parted_dict1)
|
|
self.assertEqual(parse_partition_info(parted_output2, "MB", False), parted_dict2)
|
|
|
|
def test_partition_already_exists(self):
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=False)
|
|
|
|
def test_create_new_partition(self):
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 4,
|
|
"state": "present",
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=True, script=["unit", "KiB", "mkpart", "primary", "0%", "100%"])
|
|
|
|
def test_create_new_partition_1G(self):
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 4,
|
|
"state": "present",
|
|
"part_end": "1GiB",
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=True, script=["unit", "KiB", "mkpart", "primary", "0%", "1GiB"])
|
|
|
|
def test_create_new_partition_minus_1G(self):
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 4,
|
|
"state": "present",
|
|
"fs_type": "ext2",
|
|
"part_start": "-1GiB",
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=True, script=["unit", "KiB", "mkpart", "primary", "ext2", "-1GiB", "100%"])
|
|
|
|
def test_remove_partition_number_1(self):
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "absent",
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=True, script=["rm", "1"])
|
|
|
|
def test_resize_partition(self):
|
|
with set_module_args(
|
|
{"device": "/dev/sdb", "number": 3, "state": "present", "part_end": "100%", "resize": True}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(changed=True, script=["resizepart", "3", "100%"])
|
|
|
|
def test_change_flag(self):
|
|
# Flags are set in a second run of parted().
|
|
# Between the two runs, the partition dict is updated.
|
|
# use checkmode here allow us to continue even if the dictionary is
|
|
# not updated.
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 3,
|
|
"state": "present",
|
|
"flags": ["lvm", "boot"],
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.parted.reset_mock()
|
|
self.execute_module(changed=True)
|
|
# When using multiple flags:
|
|
# order of execution is non deterministic, because set() operations are used in
|
|
# the current implementation.
|
|
expected_calls_order1 = [
|
|
call(["unit", "KiB", "set", "3", "lvm", "on", "set", "3", "boot", "on"], "/dev/sdb", "optimal")
|
|
]
|
|
expected_calls_order2 = [
|
|
call(["unit", "KiB", "set", "3", "boot", "on", "set", "3", "lvm", "on"], "/dev/sdb", "optimal")
|
|
]
|
|
self.assertTrue(
|
|
self.parted.mock_calls == expected_calls_order1 or self.parted.mock_calls == expected_calls_order2
|
|
)
|
|
|
|
def test_create_new_primary_lvm_partition(self):
|
|
# use check_mode, see previous test comment
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 4,
|
|
"flags": ["boot"],
|
|
"state": "present",
|
|
"part_start": "257GiB",
|
|
"fs_type": "ext3",
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(
|
|
changed=True,
|
|
script=[
|
|
"unit",
|
|
"KiB",
|
|
"mkpart",
|
|
"primary",
|
|
"ext3",
|
|
"257GiB",
|
|
"100%",
|
|
"unit",
|
|
"KiB",
|
|
"set",
|
|
"4",
|
|
"boot",
|
|
"on",
|
|
],
|
|
)
|
|
|
|
def test_create_label_gpt(self):
|
|
# Like previous test, current implementation use parted to create the partition and
|
|
# then retrieve and update the dictionary. Use check_mode to force to continue even if
|
|
# dictionary is not updated.
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"flags": ["lvm"],
|
|
"label": "gpt",
|
|
"name": "lvmpartition",
|
|
"state": "present",
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict2,
|
|
):
|
|
self.execute_module(
|
|
changed=True,
|
|
script=[
|
|
"unit",
|
|
"KiB",
|
|
"mklabel",
|
|
"gpt",
|
|
"mkpart",
|
|
"primary",
|
|
"0%",
|
|
"100%",
|
|
"unit",
|
|
"KiB",
|
|
"name",
|
|
"1",
|
|
'"lvmpartition"',
|
|
"set",
|
|
"1",
|
|
"lvm",
|
|
"on",
|
|
],
|
|
)
|
|
|
|
def test_change_label_gpt(self):
|
|
# When partitions already exists and label is changed, mkpart should be called even when partition already exists,
|
|
# because new empty label will be created anyway
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
"label": "gpt",
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict1,
|
|
):
|
|
self.execute_module(
|
|
changed=True, script=["unit", "KiB", "mklabel", "gpt", "mkpart", "primary", "0%", "100%"]
|
|
)
|
|
|
|
def test_check_mode_unchanged(self):
|
|
# Test that get_device_info result is checked in check mode too
|
|
# No change on partition 1
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
"flags": ["some_flag"],
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict3,
|
|
):
|
|
self.execute_module(changed=False)
|
|
|
|
def test_check_mode_changed(self):
|
|
# Test that get_device_info result is checked in check mode too
|
|
# Flag change on partition 1
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
"flags": ["other_flag"],
|
|
"_ansible_check_mode": True,
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict3,
|
|
):
|
|
self.execute_module(changed=True)
|
|
|
|
def test_suse_msdos_type_code_with_lvm_already_set(self):
|
|
# When SUSE parted reports "type=8e" alongside "lvm" in the flags output,
|
|
# the module must not try to unset the pseudo-flag (issue #6292).
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
"flags": ["lvm"],
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict_suse_lvm,
|
|
):
|
|
self.execute_module(changed=False)
|
|
|
|
def test_suse_msdos_type_code_set_lvm_flag(self):
|
|
# When SUSE parted only reports "type=83" (no logical flags) and the user
|
|
# requests lvm, the module must set lvm without trying to unset type=83.
|
|
with set_module_args(
|
|
{
|
|
"device": "/dev/sdb",
|
|
"number": 1,
|
|
"state": "present",
|
|
"flags": ["lvm"],
|
|
}
|
|
):
|
|
with patch(
|
|
"ansible_collections.community.general.plugins.modules.parted.get_device_info",
|
|
return_value=parted_dict_suse_noflag,
|
|
):
|
|
self.parted.reset_mock()
|
|
self.execute_module(changed=True)
|
|
self.assertEqual(
|
|
self.parted.mock_calls,
|
|
[call(["unit", "KiB", "set", "1", "lvm", "on"], "/dev/sdb", "optimal")],
|
|
)
|
|
|
|
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)
|