diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/general/plugins/modules/parted.py | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/parted.py')
-rw-r--r-- | ansible_collections/community/general/plugins/modules/parted.py | 810 |
1 files changed, 810 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/parted.py b/ansible_collections/community/general/plugins/modules/parted.py new file mode 100644 index 000000000..8e6038180 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/parted.py @@ -0,0 +1,810 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016, Fabrizio Colonna <colofabrix@tin.it> +# 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: + - Fabrizio Colonna (@ColOfAbRiX) +module: parted +short_description: Configure block device partitions +description: + - This module allows configuring block device partition using the C(parted) + command line tool. For a full description of the fields and the options + check the GNU parted manual. +requirements: + - This module requires C(parted) version 1.8.3 and above. + - Option I(align) (except C(undefined)) requires C(parted) 2.1 or above. + - If the version of C(parted) is below 3.1, it requires a Linux version running + the C(sysfs) file system C(/sys/). + - Requires the C(resizepart) command when using the I(resize) parameter. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + device: + description: + - The block device (disk) where to operate. + - Regular files can also be partitioned, but it is recommended to create a + loopback device using C(losetup) to easily access its partitions. + type: str + required: true + align: + description: + - Set alignment for newly created partitions. Use C(undefined) for parted default aligment. + type: str + choices: [ cylinder, minimal, none, optimal, undefined ] + default: optimal + number: + description: + - The partition number being affected. + - Required when performing any action on the disk, except fetching information. + type: int + unit: + description: + - Selects the current default unit that Parted will use to display + locations and capacities on the disk and to interpret those given by the + user if they are not suffixed by an unit. + - When fetching information about a disk, it is recommended to always specify a unit. + type: str + choices: [ s, B, KB, KiB, MB, MiB, GB, GiB, TB, TiB, '%', cyl, chs, compact ] + default: KiB + label: + description: + - Disk label type or partition table to use. + - If I(device) already contains a different label, it will be changed to I(label) + and any previous partitions will be lost. + - A I(name) must be specified for a C(gpt) partition table. + type: str + choices: [ aix, amiga, bsd, dvh, gpt, loop, mac, msdos, pc98, sun ] + default: msdos + part_type: + description: + - May be specified only with I(label=msdos) or I(label=dvh). + - Neither I(part_type) nor I(name) may be used with I(label=sun). + type: str + choices: [ extended, logical, primary ] + default: primary + part_start: + description: + - Where the partition will start as offset from the beginning of the disk, + that is, the "distance" from the start of the disk. Negative numbers + specify distance from the end of the disk. + - The distance can be specified with all the units supported by parted + (except compat) and it is case sensitive, e.g. C(10GiB), C(15%). + - Using negative values may require setting of I(fs_type) (see notes). + type: str + default: 0% + part_end: + description: + - Where the partition will end as offset from the beginning of the disk, + that is, the "distance" from the start of the disk. Negative numbers + specify distance from the end of the disk. + - The distance can be specified with all the units supported by parted + (except compat) and it is case sensitive, e.g. C(10GiB), C(15%). + type: str + default: 100% + name: + description: + - Sets the name for the partition number (GPT, Mac, MIPS and PC98 only). + type: str + flags: + description: A list of the flags that has to be set on the partition. + type: list + elements: str + state: + description: + - Whether to create or delete a partition. + - If set to C(info) the module will only return the device information. + type: str + choices: [ absent, present, info ] + default: info + fs_type: + description: + - If specified and the partition does not exist, will set filesystem type to given partition. + - Parameter optional, but see notes below about negative I(part_start) values. + type: str + version_added: '0.2.0' + resize: + description: + - Call C(resizepart) on existing partitions to match the size specified by I(part_end). + type: bool + default: false + version_added: '1.3.0' + +notes: + - When fetching information about a new disk and when the version of parted + installed on the system is before version 3.1, the module queries the kernel + through C(/sys/) to obtain disk information. In this case the units CHS and + CYL are not supported. + - Negative I(part_start) start values were rejected if I(fs_type) was not given. + This bug was fixed in parted 3.2.153. If you want to use negative I(part_start), + specify I(fs_type) as well or make sure your system contains newer parted. +''' + +RETURN = r''' +partition_info: + description: Current partition information + returned: success + type: complex + contains: + disk: + description: Generic device information. + type: dict + partitions: + description: List of device partitions. + type: list + script: + description: parted script executed by module + type: str + sample: { + "disk": { + "dev": "/dev/sdb", + "logical_block": 512, + "model": "VMware Virtual disk", + "physical_block": 512, + "size": 5.0, + "table": "msdos", + "unit": "gib" + }, + "partitions": [{ + "begin": 0.0, + "end": 1.0, + "flags": ["boot", "lvm"], + "fstype": "", + "name": "", + "num": 1, + "size": 1.0 + }, { + "begin": 1.0, + "end": 5.0, + "flags": [], + "fstype": "", + "name": "", + "num": 2, + "size": 4.0 + }], + "script": "unit KiB print " + } +''' + +EXAMPLES = r''' +- name: Create a new ext4 primary partition + community.general.parted: + device: /dev/sdb + number: 1 + state: present + fs_type: ext4 + +- name: Remove partition number 1 + community.general.parted: + device: /dev/sdb + number: 1 + state: absent + +- name: Create a new primary partition with a size of 1GiB + community.general.parted: + device: /dev/sdb + number: 1 + state: present + part_end: 1GiB + +- name: Create a new primary partition for LVM + community.general.parted: + device: /dev/sdb + number: 2 + flags: [ lvm ] + state: present + part_start: 1GiB + +- name: Create a new primary partition with a size of 1GiB at disk's end + community.general.parted: + device: /dev/sdb + number: 3 + state: present + fs_type: ext3 + part_start: -1GiB + +# Example on how to read info and reuse it in subsequent task +- name: Read device information (always use unit when probing) + community.general.parted: device=/dev/sdb unit=MiB + register: sdb_info + +- name: Remove all partitions from disk + community.general.parted: + device: /dev/sdb + number: '{{ item.num }}' + state: absent + loop: '{{ sdb_info.partitions }}' + +- name: Extend an existing partition to fill all available space + community.general.parted: + device: /dev/sdb + number: "{{ sdb_info.partitions | length }}" + part_end: "100%" + resize: true + state: present +''' + + +from ansible.module_utils.basic import AnsibleModule +import math +import re +import os + + +# Reference prefixes (International System of Units and IEC) +units_si = ['B', 'KB', 'MB', 'GB', 'TB'] +units_iec = ['KiB', 'MiB', 'GiB', 'TiB'] +parted_units = units_si + units_iec + ['s', '%', 'cyl', 'chs', 'compact'] + + +def parse_unit(size_str, unit=''): + """ + Parses a string containing a size or boundary information + """ + 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) + if matches is None: + module.fail_json( + msg="Error interpreting parted size output: '%s'" % size_str + ) + + size = { + 'cylinder': int(matches.group(1)), + 'head': int(matches.group(2)), + 'sector': int(matches.group(3)) + } + unit = 'chs' + + else: + # Normal format: "<number>[<unit>]" + if matches.group(2) is not None: + unit = matches.group(2) + + size = float(matches.group(1)) + + return size, unit + + +def parse_partition_info(parted_output, unit): + """ + Parses the output of parted and transforms the data into + a dictionary. + + Parted Machine Parseable Output: + See: https://lists.alioth.debian.org/pipermail/parted-devel/2006-December/00 + 0573.html + - All lines end with a semicolon (;) + - The first line indicates the units in which the output is expressed. + CHS, CYL and BYT stands for CHS, Cylinder and Bytes respectively. + - The second line is made of disk information in the following format: + "path":"size":"transport-type":"logical-sector-size":"physical-sector-siz + e":"partition-table-type":"model-name"; + - If the first line was either CYL or CHS, the next line will contain + information on no. of cylinders, heads, sectors and cylinder size. + - Partition information begins from the next line. This is of the format: + (for BYT) + "number":"begin":"end":"size":"filesystem-type":"partition-name":"flags-s + et"; + (for CHS/CYL) + "number":"begin":"end":"filesystem-type":"partition-name":"flags-set"; + """ + lines = [x for x in parted_output.split('\n') if x.strip() != ''] + + # Generic device info + generic_params = lines[1].rstrip(';').split(':') + + # The unit is read once, because parted always returns the same unit + size, unit = parse_unit(generic_params[1], unit) + + generic = { + 'dev': generic_params[0], + 'size': size, + 'unit': unit.lower(), + 'table': generic_params[5], + 'model': generic_params[6], + 'logical_block': int(generic_params[3]), + 'physical_block': int(generic_params[4]) + } + + # CYL and CHS have an additional line in the output + if unit in ['cyl', 'chs']: + chs_info = lines[2].rstrip(';').split(':') + cyl_size, cyl_unit = parse_unit(chs_info[3]) + generic['chs_info'] = { + 'cylinders': int(chs_info[0]), + 'heads': int(chs_info[1]), + 'sectors': int(chs_info[2]), + 'cyl_size': cyl_size, + 'cyl_size_unit': cyl_unit.lower() + } + lines = lines[1:] + + parts = [] + for line in lines[2:]: + part_params = line.rstrip(';').split(':') + + # CHS use a different format than BYT, but contrary to what stated by + # the author, CYL is the same as BYT. I've tested this undocumented + # behaviour down to parted version 1.8.3, which is the first version + # that supports the machine parseable output. + if unit != 'chs': + size = parse_unit(part_params[3])[0] + fstype = part_params[4] + name = part_params[5] + flags = part_params[6] + + else: + size = "" + fstype = part_params[3] + name = part_params[4] + flags = part_params[5] + + parts.append({ + 'num': int(part_params[0]), + 'begin': parse_unit(part_params[1])[0], + 'end': parse_unit(part_params[2])[0], + 'size': size, + 'fstype': fstype, + 'name': name, + 'flags': [f.strip() for f in flags.split(', ') if f != ''], + 'unit': unit.lower(), + }) + + return {'generic': generic, 'partitions': parts} + + +def format_disk_size(size_bytes, unit): + """ + Formats a size in bytes into a different unit, like parted does. It doesn't + manage CYL and CHS formats, though. + This function has been adapted from https://github.com/Distrotech/parted/blo + b/279d9d869ff472c52b9ec2e180d568f0c99e30b0/libparted/unit.c + """ + global units_si, units_iec # pylint: disable=global-variable-not-assigned + + unit = unit.lower() + + # Shortcut + if size_bytes == 0: + return 0.0, 'b' + + # Cases where we default to 'compact' + if unit in ['', 'compact', 'cyl', 'chs']: + index = max(0, int( + (math.log10(size_bytes) - 1.0) / 3.0 + )) + unit = 'b' + if index < len(units_si): + unit = units_si[index] + + # Find the appropriate multiplier + multiplier = 1.0 + if unit in units_si: + multiplier = 1000.0 ** units_si.index(unit) + elif unit in units_iec: + multiplier = 1024.0 ** units_iec.index(unit) + + output = size_bytes // multiplier * (1 + 1E-16) + + # Corrections to round up as per IEEE754 standard + if output < 10: + w = output + 0.005 + elif output < 100: + w = output + 0.05 + else: + w = output + 0.5 + + if w < 10: + precision = 2 + elif w < 100: + precision = 1 + else: + precision = 0 + + # Round and return + return round(output, precision), unit + + +def convert_to_bytes(size_str, unit): + size = float(size_str) + multiplier = 1.0 + if unit in units_si: + multiplier = 1000.0 ** units_si.index(unit) + elif unit in units_iec: + multiplier = 1024.0 ** (units_iec.index(unit) + 1) + elif unit in ['', 'compact', 'cyl', 'chs']: + # As per format_disk_size, default to compact, which defaults to megabytes + multiplier = 1000.0 ** units_si.index("MB") + + output = size * multiplier + return int(output) + + +def get_unlabeled_device_info(device, unit): + """ + Fetches device information directly from the kernel and it is used when + parted cannot work because of a missing label. It always returns a 'unknown' + label. + """ + device_name = os.path.basename(device) + base = "/sys/block/%s" % device_name + + vendor = read_record(base + "/device/vendor", "Unknown") + model = read_record(base + "/device/model", "model") + logic_block = int(read_record(base + "/queue/logical_block_size", 0)) + phys_block = int(read_record(base + "/queue/physical_block_size", 0)) + size_bytes = int(read_record(base + "/size", 0)) * logic_block + + size, unit = format_disk_size(size_bytes, unit) + + return { + 'generic': { + 'dev': device, + 'table': "unknown", + 'size': size, + 'unit': unit, + 'logical_block': logic_block, + 'physical_block': phys_block, + 'model': "%s %s" % (vendor, model), + }, + 'partitions': [] + } + + +def get_device_info(device, unit): + """ + Fetches information about a disk and its partitions and it returns a + dictionary. + """ + global module, parted_exec # pylint: disable=global-variable-not-assigned + + # If parted complains about missing labels, it means there are no partitions. + # In this case only, use a custom function to fetch information and emulate + # parted formats for the unit. + label_needed = check_parted_label(device) + if label_needed: + return get_unlabeled_device_info(device, unit) + + command = "%s -s -m %s -- unit '%s' print" % (parted_exec, device, unit) + rc, out, err = module.run_command(command) + if rc != 0 and 'unrecognised disk label' not in err: + module.fail_json(msg=( + "Error while getting device information with parted " + "script: '%s'" % command), + rc=rc, out=out, err=err + ) + + return parse_partition_info(out, unit) + + +def check_parted_label(device): + """ + Determines if parted needs a label to complete its duties. Versions prior + to 3.1 don't return data when there is no label. For more information see: + http://upstream.rosalinux.ru/changelogs/libparted/3.1/changelog.html + """ + global parted_exec # pylint: disable=global-variable-not-assigned + + # Check the version + parted_major, parted_minor, dummy = parted_version() + if (parted_major == 3 and parted_minor >= 1) or parted_major > 3: + return False + + # Older parted versions return a message in the stdout and RC > 0. + rc, out, err = module.run_command("%s -s -m %s print" % (parted_exec, device)) + if rc != 0 and 'unrecognised disk label' in out.lower(): + return True + + return False + + +def parse_parted_version(out): + """ + Returns version tuple from the output of "parted --version" command + """ + lines = [x for x in out.split('\n') if x.strip() != ''] + if len(lines) == 0: + 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()) + + if matches is None: + return None, None, None + + # Convert version to numbers + major = int(matches.group(1)) + minor = int(matches.group(2)) + rev = 0 + if matches.group(3) is not None: + rev = int(matches.group(3)) + + return major, minor, rev + + +def parted_version(): + """ + Returns the major and minor version of parted installed on the system. + """ + global module, parted_exec # pylint: disable=global-variable-not-assigned + + 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. + """ + global module, parted_exec # pylint: disable=global-variable-not-assigned + + align_option = '-a %s' % align + if align == 'undefined': + align_option = '' + + if script and not module.check_mode: + command = "%s -s -m %s %s -- %s" % (parted_exec, align_option, device, script) + rc, out, err = module.run_command(command) + + if rc != 0: + module.fail_json( + msg="Error while running parted script: %s" % command.strip(), + rc=rc, out=out, err=err + ) + + +def read_record(file_path, default=None): + """ + Reads the first line of a file and returns it. + """ + try: + f = open(file_path, 'r') + try: + return f.readline().strip() + finally: + f.close() + except IOError: + return default + + +def part_exists(partitions, attribute, number): + """ + Looks if a partition that has a specific value for a specific attribute + actually exists. + """ + return any( + part[attribute] and + part[attribute] == number for part in partitions + ) + + +def check_size_format(size_str): + """ + Checks if the input string is an allowed size + """ + size, unit = parse_unit(size_str) + return unit in parted_units + + +def main(): + global module, units_si, units_iec, parted_exec # pylint: disable=global-variable-not-assigned + + changed = False + output_script = "" + script = "" + module = AnsibleModule( + argument_spec=dict( + device=dict(type='str', required=True), + align=dict(type='str', default='optimal', choices=['cylinder', 'minimal', 'none', 'optimal', 'undefined']), + number=dict(type='int'), + + # unit <unit> command + unit=dict(type='str', default='KiB', choices=parted_units), + + # mklabel <label-type> command + label=dict(type='str', default='msdos', choices=['aix', 'amiga', 'bsd', 'dvh', 'gpt', 'loop', 'mac', 'msdos', 'pc98', 'sun']), + + # mkpart <part-type> [<fs-type>] <start> <end> command + part_type=dict(type='str', default='primary', choices=['extended', 'logical', 'primary']), + part_start=dict(type='str', default='0%'), + part_end=dict(type='str', default='100%'), + fs_type=dict(type='str'), + + # name <partition> <name> command + name=dict(type='str'), + + # set <partition> <flag> <state> command + flags=dict(type='list', elements='str'), + + # rm/mkpart command + state=dict(type='str', default='info', choices=['absent', 'info', 'present']), + + # resize part + resize=dict(type='bool', default=False), + ), + required_if=[ + ['state', 'present', ['number']], + ['state', 'absent', ['number']], + ], + supports_check_mode=True, + ) + module.run_command_environ_update = {'LANG': 'C', 'LC_ALL': 'C', 'LC_MESSAGES': 'C', 'LC_CTYPE': 'C'} + + # Data extraction + device = module.params['device'] + align = module.params['align'] + number = module.params['number'] + unit = module.params['unit'] + label = module.params['label'] + part_type = module.params['part_type'] + part_start = module.params['part_start'] + part_end = module.params['part_end'] + name = module.params['name'] + state = module.params['state'] + flags = module.params['flags'] + fs_type = module.params['fs_type'] + resize = module.params['resize'] + + # Parted executable + parted_exec = module.get_bin_path('parted', True) + + # Conditioning + if number is not None and number < 1: + module.fail_json(msg="The partition number must be greater then 0.") + if not check_size_format(part_start): + module.fail_json( + msg="The argument 'part_start' doesn't respect required format." + "The size unit is case sensitive.", + err=parse_unit(part_start) + ) + if not check_size_format(part_end): + module.fail_json( + msg="The argument 'part_end' doesn't respect required format." + "The size unit is case sensitive.", + err=parse_unit(part_end) + ) + + # Read the current disk information + current_device = get_device_info(device, unit) + current_parts = current_device['partitions'] + + if state == 'present': + + # Assign label if required + mklabel_needed = current_device['generic'].get('table', None) != label + if mklabel_needed: + script += "mklabel %s " % label + + # Create partition if required + if part_type and (mklabel_needed or not part_exists(current_parts, 'num', number)): + script += "mkpart %s %s%s %s " % ( + part_type, + '%s ' % fs_type if fs_type is not None else '', + part_start, + part_end + ) + + # Set the unit of the run + if unit and script: + script = "unit %s %s" % (unit, script) + + # If partition exists, try to resize + if resize and part_exists(current_parts, 'num', number): + # Ensure new end is different to current + partition = [p for p in current_parts if p['num'] == number][0] + current_part_end = convert_to_bytes(partition['end'], unit) + + size, parsed_unit = parse_unit(part_end, unit) + if parsed_unit == "%": + size = int((int(current_device['generic']['size']) * size) / 100) + parsed_unit = unit + + desired_part_end = convert_to_bytes(size, parsed_unit) + + if current_part_end != desired_part_end: + script += "resizepart %s %s " % ( + number, + part_end + ) + + # Execute the script and update the data structure. + # This will create the partition for the next steps + if script: + output_script += script + parted(script, device, align) + changed = True + script = "" + + if not module.check_mode: + current_parts = get_device_info(device, unit)['partitions'] + + if part_exists(current_parts, 'num', number) or module.check_mode: + if changed and module.check_mode: + partition = {'flags': []} # Empty structure for the check-mode + else: + partition = [p for p in current_parts if p['num'] == number][0] + + # Assign name to the partition + if name is not None and partition.get('name', None) != name: + # Wrap double quotes in single quotes so the shell doesn't strip + # the double quotes as those need to be included in the arg + # passed to parted + script += 'name %s \'"%s"\' ' % (number, name) + + # Manage flags + if flags: + # Parted infers boot with esp, if you assign esp, boot is set + # and if boot is unset, esp is also unset. + if 'esp' in flags and 'boot' not in flags: + flags.append('boot') + + # Compute only the changes in flags status + flags_off = list(set(partition['flags']) - set(flags)) + flags_on = list(set(flags) - set(partition['flags'])) + + for f in flags_on: + script += "set %s %s on " % (number, f) + + for f in flags_off: + script += "set %s %s off " % (number, f) + + # Set the unit of the run + if unit and script: + script = "unit %s %s" % (unit, script) + + # Execute the script + if script: + output_script += script + changed = True + parted(script, device, align) + + elif state == 'absent': + # Remove the partition + if part_exists(current_parts, 'num', number) or module.check_mode: + script = "rm %s " % number + output_script += script + changed = True + parted(script, device, align) + + elif state == 'info': + output_script = "unit '%s' print " % unit + + # Final status of the device + final_device_status = get_device_info(device, unit) + module.exit_json( + changed=changed, + disk=final_device_status['generic'], + partitions=final_device_status['partitions'], + script=output_script.strip() + ) + + +if __name__ == '__main__': + main() |