Merge "Add test case file and document of Tardstick TC056(HA_TC013)"
[yardstick.git] / ansible / library / parted.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # (c) 2016, Fabrizio Colonna <colofabrix@tin.it>
5 #
6 # This file is part of Ansible
7 #
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.
12 #
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.
17 #
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/>.
20
21 ANSIBLE_METADATA = {'metadata_version': '1.0',
22                     'status': ['preview'],
23                     'supported_by': 'curated'}
24
25
26 DOCUMENTATION = '''
27 ---
28 author:
29  - "Fabrizio Colonna (@ColOfAbRiX)"
30 module: parted
31 short_description: Configure block device partitions
32 version_added: "2.3"
33 description:
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.
37 notes:
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.
42 requirements:
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/).
46 options:
47   device:
48     description: The block device (disk) where to operate.
49     required: True
50   align:
51     description: Set alignment for newly created partitions.
52     choices: ['none', 'cylinder', 'minimal', 'optimal']
53     default: optimal
54   number:
55     description:
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.
59   unit:
60     description:
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.
65     choices: [
66        's', 'B', 'KB', 'KiB', 'MB', 'MiB', 'GB', 'GiB', 'TB', 'TiB', '%', 'cyl',
67        'chs', 'compact'
68     ]
69     default: KiB
70   label:
71     description: Creates a new disk label.
72     choices: [
73        'aix', 'amiga', 'bsd', 'dvh', 'gpt', 'loop', 'mac', 'msdos', 'pc98',
74        'sun', ''
75     ]
76     default: msdos
77   part_type:
78     description:
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']
84   part_start:
85     description:
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%).
90     default: 0%
91   part_end :
92     description:
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%).
97     default: 100%
98   name:
99     description:
100      - Sets the name for the partition number (GPT, Mac, MIPS and PC98 only).
101   flags:
102     description: A list of the flags that has to be set on the partition.
103   state:
104     description:
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']
108     default: info
109 '''
110
111 RETURN = '''
112 partition_info:
113   description: Current partition information
114   returned: success
115   type: dict
116   contains:
117     device:
118       description: Generic device information.
119       type: dict
120     partitions:
121       description: List of device partitions.
122       type: list
123     sample: >
124       {
125         "disk": {
126           "dev": "/dev/sdb",
127           "logical_block": 512,
128           "model": "VMware Virtual disk",
129           "physical_block": 512,
130           "size": 5.0,
131           "table": "msdos",
132           "unit": "gib"
133         },
134         "partitions": [{
135           "begin": 0.0,
136           "end": 1.0,
137           "flags": ["boot", "lvm"],
138           "fstype": null,
139           "num": 1,
140           "size": 1.0
141         }, {
142           "begin": 1.0,
143           "end": 5.0,
144           "flags": [],
145           "fstype": null,
146           "num": 2,
147           "size": 4.0
148         }]
149       }
150 '''
151
152 EXAMPLES = """
153 # Create a new primary partition
154 - parted:
155     device: /dev/sdb
156     number: 1
157     state: present
158
159 # Remove partition number 1
160 - parted:
161     device: /dev/sdb
162     number: 1
163     state: absent
164
165 # Create a new primary partition with a size of 1GiB
166 - parted:
167     device: /dev/sdb
168     number: 1
169     state: present
170     part_end: 1gib
171
172 # Create a new primary partition for LVM
173 - parted:
174     device: /dev/sdb
175     number: 2
176     flags: [ lvm ]
177     state: present
178     part_start: 1gib
179
180 # Read device information (always use unit when probing)
181 - parted: device=/dev/sdb unit=MiB
182   register: sdb_info
183
184 # Remove all partitions from disk
185 - parted:
186     device: /dev/sdb
187     number: "{{ item.num }}"
188     state: absent
189   with_items:
190    - "{{ sdb_info.partitions }}"
191 """
192
193
194 from ansible.module_utils.basic import AnsibleModule
195 import locale
196 import math
197 import re
198 import os
199
200
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']
205
206
207 def parse_unit(size_str, unit=''):
208     """
209     Parses a string containing a size of information
210     """
211     matches = re.search(r'^([\d.]+)([\w%]+)?$', size_str)
212     if matches is None:
213         # "<cylinder>,<head>,<sector>" format
214         matches = re.search(r'^(\d+),(\d+),(\d+)$', size_str)
215         if matches is None:
216             module.fail_json(
217                 msg="Error interpreting parted size output: '%s'" % size_str
218             )
219
220         size = {
221             'cylinder': int(matches.group(1)),
222             'head':     int(matches.group(2)),
223             'sector':   int(matches.group(3))
224         }
225         unit = 'chs'
226
227     else:
228         # Normal format: "<number>[<unit>]"
229         if matches.group(2) is not None:
230             unit = matches.group(2)
231
232         size = float(matches.group(1))
233
234     return size, unit
235
236
237 def parse_partition_info(parted_output, unit):
238     """
239     Parses the output of parted and transforms the data into
240     a dictionary.
241
242     Parted Machine Parseable Output:
243     See: https://lists.alioth.debian.org/pipermail/parted-devel/2006-December/00
244     0573.html
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:
254        (for BYT)
255        "number":"begin":"end":"size":"filesystem-type":"partition-name":"flags-s
256        et";
257        (for CHS/CYL)
258        "number":"begin":"end":"filesystem-type":"partition-name":"flags-set";
259     """
260     lines = [x for x in parted_output.split('\n') if x.strip() != '']
261
262     # Generic device info
263     generic_params = lines[1].rstrip(';').split(':')
264
265     # The unit is read once, because parted always returns the same unit
266     size, unit = parse_unit(generic_params[1], unit)
267
268     generic = {
269         'dev':   generic_params[0],
270         'size':  size,
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])
276     }
277
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()
288         }
289         lines = lines[1:]
290
291     parts = []
292     for line in lines[2:]:
293         part_params = line.rstrip(';').split(':')
294
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.
299         if unit != 'chs':
300             size   = parse_unit(part_params[3])[0]
301             fstype = part_params[4]
302             flags  = part_params[5]
303         else:
304             size   = ""
305             fstype = part_params[3]
306             flags  = part_params[4]
307
308         parts.append({
309             'num':    int(part_params[0]),
310             'begin':  parse_unit(part_params[1])[0],
311             'end':    parse_unit(part_params[2])[0],
312             'size':   size,
313             'fstype': fstype,
314             'flags':  [f.strip() for f in flags.split(', ') if f != ''],
315             'unit':  unit.lower(),
316         })
317
318     return {'generic': generic, 'partitions': parts}
319
320
321 def format_disk_size(size_bytes, unit):
322     """
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
327     """
328     global units_si, units_iec
329
330     unit = unit.lower()
331
332     # Shortcut
333     if size_bytes == 0:
334         return 0.0
335
336     # Cases where we default to 'compact'
337     if unit in ['', 'compact', 'cyl', 'chs']:
338         index = max(0, int(
339             (math.log10(size_bytes) - 1.0) / 3.0
340         ))
341         unit = 'b'
342         if index < len(units_si):
343             unit = units_si[index]
344
345     # Find the appropriate multiplier
346     multiplier = 1.0
347     if unit in units_si:
348         multiplier = 1000.0 ** units_si.index(unit)
349     elif unit in units_iec:
350         multiplier = 1024.0 ** units_iec.index(unit)
351
352     output = size_bytes / multiplier * (1 + 1E-16)
353
354     # Corrections to round up as per IEEE754 standard
355     if output < 10:
356         w = output + 0.005
357     elif output < 100:
358         w = output + 0.05
359     else:
360         w = output + 0.5
361
362     if w < 10:
363         precision = 2
364     elif w < 100:
365         precision = 1
366     else:
367         precision = 0
368
369     # Round and return
370     return round(output, precision), unit
371
372
373 def get_unlabeled_device_info(device, unit):
374     """
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'
377     label.
378     """
379     device_name = os.path.basename(device)
380     base = "/sys/block/%s" % device_name
381
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
387
388     size, unit  = format_disk_size(size_bytes, unit)
389
390     return {
391         'generic': {
392             'dev':            device,
393             'table':          "unknown",
394             'size':           size,
395             'unit':           unit,
396             'logical_block':  logic_block,
397             'physical_block': phys_block,
398             'model':          "%s %s" % (vendor, model),
399         },
400         'partitions': []
401     }
402
403
404 def get_device_info(device, unit):
405     """
406     Fetches information about a disk and its partitions and it returns a
407     dictionary.
408     """
409     global module
410
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)
415     if label_needed:
416         return get_unlabeled_device_info(device, unit)
417
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
425         )
426
427     return parse_partition_info(out, unit)
428
429
430 def check_parted_label(device):
431     """
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
435     """
436     # Check the version
437     parted_major, parted_minor, _ = parted_version()
438     if (parted_major == 3 and parted_minor >= 1) or parted_major > 3:
439         return False
440
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():
444         return True
445
446     return False
447
448
449 def parted_version():
450     """
451     Returns the major and minor version of parted installed on the system.
452     """
453     global module
454
455     rc, out, err = module.run_command("parted --version")
456     if rc != 0:
457         module.fail_json(
458             msg="Failed to get parted version.", rc=rc, out=out, err=err
459         )
460
461     lines = [x for x in out.split('\n') if x.strip() != '']
462     if len(lines) == 0:
463         module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
464
465     matches = re.search(r'^parted.+(\d+)\.(\d+)(?:\.(\d+))?$', lines[0])
466     if matches is None:
467         module.fail_json(msg="Failed to get parted version.", rc=0, out=out)
468
469     # Convert version to numbers
470     major = int(matches.group(1))
471     minor = int(matches.group(2))
472     rev   = 0
473     if matches.group(3) is not None:
474         rev = int(matches.group(3))
475
476     return major, minor, rev
477
478
479 def parted(script, device, align):
480     """
481     Runs a parted script.
482     """
483     global module
484
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)
488
489         if rc != 0:
490             module.fail_json(
491                 msg="Error while running parted script: %s" % command.strip(),
492                 rc=rc, out=out, err=err
493             )
494
495
496 def read_record(file_path, default=None):
497     """
498     Reads the first line of a file and returns it.
499     """
500     try:
501         f = open(file_path, 'r')
502         try:
503             return f.readline().strip()
504         finally:
505             f.close()
506     except IOError:
507         return default
508
509
510 def part_exists(partitions, attribute, number):
511     """
512     Looks if a partition that has a specific value for a specific attribute
513     actually exists.
514     """
515     return any(
516         part[attribute] and
517         part[attribute] == number for part in partitions
518     )
519
520
521 def check_size_format(size_str):
522     """
523     Checks if the input string is an allowed size
524     """
525     size, unit = parse_unit(size_str)
526     return unit in parted_units
527
528
529 def main():
530     global module, units_si, units_iec
531
532     changed = False
533     output_script = ""
534     script = ""
535     module = AnsibleModule(
536         argument_spec={
537             'device': {'required': True, 'type': 'str'},
538             'align': {
539                 'default': 'optimal',
540                 'choices': ['none', 'cylinder', 'minimal', 'optimal'],
541                 'type': 'str'
542             },
543             'number': {'default': None, 'type': 'int'},
544
545             # unit <unit> command
546             'unit': {
547                 'default': 'KiB',
548                 'choices': parted_units,
549                 'type': 'str'
550             },
551
552             # mklabel <label-type> command
553             'label': {
554                 'choices': [
555                     'aix', 'amiga', 'bsd', 'dvh', 'gpt', 'loop', 'mac', 'msdos',
556                     'pc98', 'sun', ''
557                 ],
558                 'type': 'str'
559             },
560
561             # mkpart <part-type> [<fs-type>] <start> <end> command
562             'part_type': {
563                 'default': 'primary',
564                 'choices': ['primary', 'extended', 'logical'],
565                 'type': 'str'
566             },
567             'part_start': {'default': '0%', 'type': 'str'},
568             'part_end': {'default': '100%', 'type': 'str'},
569
570             # name <partition> <name> command
571             'name': {'type': 'str'},
572
573             # set <partition> <flag> <state> command
574             'flags': {'type': 'list'},
575
576             # rm/mkpart command
577             'state': {
578                 'choices': ['present', 'absent', 'info'],
579                 'default': 'info',
580                 'type': 'str'
581             }
582         },
583         supports_check_mode=True,
584     )
585
586     # Data extraction
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']
598
599     # Conditioning
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):
603         module.fail_json(
604             msg="The argument 'part_start' doesn't respect required format."
605                 "The size unit is case sensitive.",
606             err=parse_unit(part_start)
607         )
608     if not check_size_format(part_end):
609         module.fail_json(
610             msg="The argument 'part_end' doesn't respect required format."
611                 "The size unit is case sensitive.",
612             err=parse_unit(part_end)
613         )
614
615     # Read the current disk information
616     current_device = get_device_info(device, unit)
617     current_parts = current_device['partitions']
618
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 \
623            not label:
624             label = 'msdos'
625
626         # Assign label if required
627         if label:
628             script += "mklabel %s " % label
629
630         # Create partition if required
631         if part_type and not part_exists(current_parts, 'num', number):
632             script += "mkpart %s %s %s " % (
633                 part_type,
634                 part_start,
635                 part_end
636             )
637
638         # Set the unit of the run
639         if unit and script:
640             script = "unit %s %s" % (unit, script)
641
642         # Execute the script and update the data structure.
643         # This will create the partition for the next steps
644         if script:
645             output_script += script
646             parted(script, device, align)
647             changed = True
648             script = ""
649
650             current_parts = get_device_info(device, unit)['partitions']
651
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]
656
657             # Assign name to the the partition
658             if name:
659                 script += "name %s %s " % (number, name)
660
661             # Manage flags
662             if flags:
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']))
666
667                 for f in flags_on:
668                     script += "set %s %s on " % (number, f)
669
670                 for f in flags_off:
671                     script += "set %s %s off " % (number, f)
672
673         # Set the unit of the run
674         if unit and script:
675             script = "unit %s %s" % (unit, script)
676
677         # Execute the script
678         if script:
679             output_script += script
680             changed = True
681             parted(script, device, align)
682
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
688             changed = True
689             parted(script, device, align)
690
691     elif state == 'info':
692         output_script = "unit '%s' print " % unit
693
694     # Final status of the device
695     final_device_status = get_device_info(device, unit)
696     module.exit_json(
697         changed=changed,
698         disk=final_device_status['generic'],
699         partitions=final_device_status['partitions'],
700         script=output_script.strip()
701     )
702
703
704 if __name__ == '__main__':
705     main()