2 # -*- coding: utf-8 -*-
4 # (c) 2016, Fabrizio Colonna <colofabrix@tin.it>
6 # This file is part of Ansible
8 # Ansible is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # Ansible is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
21 ANSIBLE_METADATA = {'metadata_version': '1.0',
22 'status': ['preview'],
23 'supported_by': 'curated'}
29 - "Fabrizio Colonna (@ColOfAbRiX)"
31 short_description: Configure block device partitions
34 - This module allows configuring block device partition using the C(parted)
35 command line tool. For a full description of the fields and the options
36 check the GNU parted manual.
38 - When fetching information about a new disk and when the version of parted
39 installed on the system is before version 3.1, the module queries the kernel
40 through C(/sys/) to obtain disk information. In this case the units CHS and
41 CYL are not supported.
43 - This module requires parted version 1.8.3 and above.
44 - If the version of parted is below 3.1, it requires a Linux version running
45 the sysfs file system C(/sys/).
48 description: The block device (disk) where to operate.
51 description: Set alignment for newly created partitions.
52 choices: ['none', 'cylinder', 'minimal', 'optimal']
56 - The number of the partition to work with or the number of the partition
57 that will be created. Required when performing any action on the disk,
58 except fetching information.
61 - Selects the current default unit that Parted will use to display
62 locations and capacities on the disk and to interpret those given by the
63 user if they are not suffixed by an unit. When fetching information about
64 a disk, it is always recommended to specify a unit.
66 's', 'B', 'KB', 'KiB', 'MB', 'MiB', 'GB', 'GiB', 'TB', 'TiB', '%', 'cyl',
71 description: Creates a new disk label.
73 'aix', 'amiga', 'bsd', 'dvh', 'gpt', 'loop', 'mac', 'msdos', 'pc98',
79 - Is one of 'primary', 'extended' or 'logical' and may be specified only
80 with 'msdos' or 'dvh' partition tables. A name must be specified for a
81 'gpt' partition table. Neither part-type nor name may be used with a
82 'sun' partition table.
83 choices: ['primary', 'extended', 'logical']
86 - Where the partition will start as offset from the beginning of the disk,
87 that is, the "distance" from the start of the disk. The distance can be
88 specified with all the units supported by parted (except compat) and
89 it is case sensitive. E.g. C(10GiB), C(15%).
93 - Where the partition will end as offset from the beginning of the disk,
94 that is, the "distance" from the start of the disk. The distance can be
95 specified with all the units supported by parted (except compat) and
96 it is case sensitive. E.g. C(10GiB), C(15%).
100 - Sets the name for the partition number (GPT, Mac, MIPS and PC98 only).
102 description: A list of the flags that has to be set on the partition.
105 - If to create or delete a partition. If set to C(info) the module will
106 only return the device information.
107 choices: ['present', 'absent', 'info']
113 description: Current partition information
118 description: Generic device information.
121 description: List of device partitions.
127 "logical_block": 512,
128 "model": "VMware Virtual disk",
129 "physical_block": 512,
137 "flags": ["boot", "lvm"],
153 # Create a new primary partition
159 # Remove partition number 1
165 # Create a new primary partition with a size of 1GiB
172 # Create a new primary partition for LVM
180 # Read device information (always use unit when probing)
181 - parted: device=/dev/sdb unit=MiB
184 # Remove all partitions from disk
187 number: "{{ item.num }}"
190 - "{{ sdb_info.partitions }}"
194 from ansible.module_utils.basic import AnsibleModule
201 # Reference prefixes (International System of Units and IEC)
202 units_si = ['B', 'KB', 'MB', 'GB', 'TB']
203 units_iec = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
204 parted_units = units_si + units_iec + ['s', '%', 'cyl', 'chs', 'compact']
207 def parse_unit(size_str, unit=''):
209 Parses a string containing a size of information
211 matches = re.search(r'^([\d.]+)([\w%]+)?$', size_str)
213 # "<cylinder>,<head>,<sector>" format
214 matches = re.search(r'^(\d+),(\d+),(\d+)$', size_str)
217 msg="Error interpreting parted size output: '%s'" % size_str
221 'cylinder': int(matches.group(1)),
222 'head': int(matches.group(2)),
223 'sector': int(matches.group(3))
228 # Normal format: "<number>[<unit>]"
229 if matches.group(2) is not None:
230 unit = matches.group(2)
232 size = float(matches.group(1))
237 def parse_partition_info(parted_output, unit):
239 Parses the output of parted and transforms the data into
242 Parted Machine Parseable Output:
243 See: https://lists.alioth.debian.org/pipermail/parted-devel/2006-December/00
245 - All lines end with a semicolon (;)
246 - The first line indicates the units in which the output is expressed.
247 CHS, CYL and BYT stands for CHS, Cylinder and Bytes respectively.
248 - The second line is made of disk information in the following format:
249 "path":"size":"transport-type":"logical-sector-size":"physical-sector-siz
250 e":"partition-table-type":"model-name";
251 - If the first line was either CYL or CHS, the next line will contain
252 information on no. of cylinders, heads, sectors and cylinder size.
253 - Partition information begins from the next line. This is of the format:
255 "number":"begin":"end":"size":"filesystem-type":"partition-name":"flags-s
258 "number":"begin":"end":"filesystem-type":"partition-name":"flags-set";
260 lines = [x for x in parted_output.split('\n') if x.strip() != '']
262 # Generic device info
263 generic_params = lines[1].rstrip(';').split(':')
265 # The unit is read once, because parted always returns the same unit
266 size, unit = parse_unit(generic_params[1], unit)
269 'dev': generic_params[0],
271 'unit': unit.lower(),
272 'table': generic_params[5],
273 'model': generic_params[6],
274 'logical_block': int(generic_params[3]),
275 'physical_block': int(generic_params[4])
278 # CYL and CHS have an additional line in the output
279 if unit in ['cyl', 'chs']:
280 chs_info = lines[2].rstrip(';').split(':')
281 cyl_size, cyl_unit = parse_unit(chs_info[3])
282 generic['chs_info'] = {
283 'cylinders': int(chs_info[0]),
284 'heads': int(chs_info[1]),
285 'sectors': int(chs_info[2]),
286 'cyl_size': cyl_size,
287 'cyl_size_unit': cyl_unit.lower()
292 for line in lines[2:]:
293 part_params = line.rstrip(';').split(':')
295 # CHS use a different format than BYT, but contrary to what stated by
296 # the author, CYL is the same as BYT. I've tested this undocumented
297 # behaviour down to parted version 1.8.3, which is the first version
298 # that supports the machine parseable output.
300 size = parse_unit(part_params[3])[0]
301 fstype = part_params[4]
302 flags = part_params[5]
305 fstype = part_params[3]
306 flags = part_params[4]
309 'num': int(part_params[0]),
310 'begin': parse_unit(part_params[1])[0],
311 'end': parse_unit(part_params[2])[0],
314 'flags': [f.strip() for f in flags.split(', ') if f != ''],
315 'unit': unit.lower(),
318 return {'generic': generic, 'partitions': parts}
321 def format_disk_size(size_bytes, unit):
323 Formats a size in bytes into a different unit, like parted does. It doesn't
324 manage CYL and CHS formats, though.
325 This function has been adapted from https://github.com/Distrotech/parted/blo
326 b/279d9d869ff472c52b9ec2e180d568f0c99e30b0/libparted/unit.c
328 global units_si, units_iec
336 # Cases where we default to 'compact'
337 if unit in ['', 'compact', 'cyl', 'chs']:
339 (math.log10(size_bytes) - 1.0) / 3.0
342 if index < len(units_si):
343 unit = units_si[index]
345 # Find the appropriate multiplier
348 multiplier = 1000.0 ** units_si.index(unit)
349 elif unit in units_iec:
350 multiplier = 1024.0 ** units_iec.index(unit)
352 output = size_bytes / multiplier * (1 + 1E-16)
354 # Corrections to round up as per IEEE754 standard
370 return round(output, precision), unit
373 def get_unlabeled_device_info(device, unit):
375 Fetches device information directly from the kernel and it is used when
376 parted cannot work because of a missing label. It always returns a 'unknown'
379 device_name = os.path.basename(device)
380 base = "/sys/block/%s" % device_name
382 vendor = read_record(base + "/device/vendor", "Unknown")
383 model = read_record(base + "/device/model", "model")
384 logic_block = int(read_record(base + "/queue/logical_block_size", 0))
385 phys_block = int(read_record(base + "/queue/physical_block_size", 0))
386 size_bytes = int(read_record(base + "/size", 0)) * logic_block
388 size, unit = format_disk_size(size_bytes, unit)
396 'logical_block': logic_block,
397 'physical_block': phys_block,
398 'model': "%s %s" % (vendor, model),
404 def get_device_info(device, unit):
406 Fetches information about a disk and its partitions and it returns a
411 # If parted complains about missing labels, it means there are no partitions.
412 # In this case only, use a custom function to fetch information and emulate
413 # parted formats for the unit.
414 label_needed = check_parted_label(device)
416 return get_unlabeled_device_info(device, unit)
418 command = "parted -s -m %s -- unit '%s' print" % (device, unit)
419 rc, out, err = module.run_command(command)
420 if rc != 0 and 'unrecognised disk label' not in err:
421 module.fail_json(msg=(
422 "Error while getting device information with parted "
423 "script: '%s'" % command),
424 rc=rc, out=out, err=err
427 return parse_partition_info(out, unit)
430 def check_parted_label(device):
432 Determines if parted needs a label to complete its duties. Versions prior
433 to 3.1 don't return data when there is no label. For more information see:
434 http://upstream.rosalinux.ru/changelogs/libparted/3.1/changelog.html
437 parted_major, parted_minor, _ = parted_version()
438 if (parted_major == 3 and parted_minor >= 1) or parted_major > 3:
441 # Older parted versions return a message in the stdout and RC > 0.
442 rc, out, err = module.run_command("parted -s -m %s print" % device)
443 if rc != 0 and 'unrecognised disk label' in out.lower():
449 def parted_version():
451 Returns the major and minor version of parted installed on the system.
455 rc, out, err = module.run_command("parted --version")
458 msg="Failed to get parted version.", rc=rc, out=out, err=err
461 lines = [x for x in out.split('\n') if x.strip() != '']
463 module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
465 matches = re.search(r'^parted.+(\d+)\.(\d+)(?:\.(\d+))?$', lines[0])
467 module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
469 # Convert version to numbers
470 major = int(matches.group(1))
471 minor = int(matches.group(2))
473 if matches.group(3) is not None:
474 rev = int(matches.group(3))
476 return major, minor, rev
479 def parted(script, device, align):
481 Runs a parted script.
485 if script and not module.check_mode:
486 command = "parted -s -m -a %s %s -- %s" % (align, device, script)
487 rc, out, err = module.run_command(command)
491 msg="Error while running parted script: %s" % command.strip(),
492 rc=rc, out=out, err=err
496 def read_record(file_path, default=None):
498 Reads the first line of a file and returns it.
501 f = open(file_path, 'r')
503 return f.readline().strip()
510 def part_exists(partitions, attribute, number):
512 Looks if a partition that has a specific value for a specific attribute
517 part[attribute] == number for part in partitions
521 def check_size_format(size_str):
523 Checks if the input string is an allowed size
525 size, unit = parse_unit(size_str)
526 return unit in parted_units
530 global module, units_si, units_iec
535 module = AnsibleModule(
537 'device': {'required': True, 'type': 'str'},
539 'default': 'optimal',
540 'choices': ['none', 'cylinder', 'minimal', 'optimal'],
543 'number': {'default': None, 'type': 'int'},
545 # unit <unit> command
548 'choices': parted_units,
552 # mklabel <label-type> command
555 'aix', 'amiga', 'bsd', 'dvh', 'gpt', 'loop', 'mac', 'msdos',
561 # mkpart <part-type> [<fs-type>] <start> <end> command
563 'default': 'primary',
564 'choices': ['primary', 'extended', 'logical'],
567 'part_start': {'default': '0%', 'type': 'str'},
568 'part_end': {'default': '100%', 'type': 'str'},
570 # name <partition> <name> command
571 'name': {'type': 'str'},
573 # set <partition> <flag> <state> command
574 'flags': {'type': 'list'},
578 'choices': ['present', 'absent', 'info'],
583 supports_check_mode=True,
587 device = module.params['device']
588 align = module.params['align']
589 number = module.params['number']
590 unit = module.params['unit']
591 label = module.params['label']
592 part_type = module.params['part_type']
593 part_start = module.params['part_start']
594 part_end = module.params['part_end']
595 name = module.params['name']
596 state = module.params['state']
597 flags = module.params['flags']
600 if number and number < 0:
601 module.fail_json(msg="The partition number must be non negative.")
602 if not check_size_format(part_start):
604 msg="The argument 'part_start' doesn't respect required format."
605 "The size unit is case sensitive.",
606 err=parse_unit(part_start)
608 if not check_size_format(part_end):
610 msg="The argument 'part_end' doesn't respect required format."
611 "The size unit is case sensitive.",
612 err=parse_unit(part_end)
615 # Read the current disk information
616 current_device = get_device_info(device, unit)
617 current_parts = current_device['partitions']
619 if state == 'present':
620 # Default value for the label
621 if not current_device['generic']['table'] or \
622 current_device['generic']['table'] == 'unknown' and \
626 # Assign label if required
628 script += "mklabel %s " % label
630 # Create partition if required
631 if part_type and not part_exists(current_parts, 'num', number):
632 script += "mkpart %s %s %s " % (
638 # Set the unit of the run
640 script = "unit %s %s" % (unit, script)
642 # Execute the script and update the data structure.
643 # This will create the partition for the next steps
645 output_script += script
646 parted(script, device, align)
650 current_parts = get_device_info(device, unit)['partitions']
652 if part_exists(current_parts, 'num', number) or module.check_mode:
653 partition = {'flags': []} # Empty structure for the check-mode
654 if not module.check_mode:
655 partition = [p for p in current_parts if p['num'] == number][0]
657 # Assign name to the the partition
659 script += "name %s %s " % (number, name)
663 # Compute only the changes in flags status
664 flags_off = list(set(partition['flags']) - set(flags))
665 flags_on = list(set(flags) - set(partition['flags']))
668 script += "set %s %s on " % (number, f)
671 script += "set %s %s off " % (number, f)
673 # Set the unit of the run
675 script = "unit %s %s" % (unit, script)
679 output_script += script
681 parted(script, device, align)
683 elif state == 'absent':
684 # Remove the partition
685 if part_exists(current_parts, 'num', number) or module.check_mode:
686 script = "rm %s " % number
687 output_script += script
689 parted(script, device, align)
691 elif state == 'info':
692 output_script = "unit '%s' print " % unit
694 # Final status of the device
695 final_device_status = get_device_info(device, unit)
698 disk=final_device_status['generic'],
699 partitions=final_device_status['partitions'],
700 script=output_script.strip()
704 if __name__ == '__main__':