partitionpool.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/python
  2. """
  3. Ansible module for partitioning.
  4. """
  5. from __future__ import print_function
  6. # There is no pyparted on our Jenkins worker
  7. # pylint: disable=import-error
  8. import parted
  9. DOCUMENTATION = """
  10. ---
  11. module: partitionpool
  12. short_description; Partition a disk into parititions.
  13. description:
  14. - Creates partitions on given disk based on partition sizes and their weights.
  15. Unless 'force' option is set to True, it ignores already partitioned disks.
  16. When the disk is empty or 'force' is set to True, it always creates a new
  17. GPT partition table on the disk. Then it creates number of partitions, based
  18. on their weights.
  19. This module should be used when a system admin wants to split existing disk(s)
  20. into pools of partitions of specific sizes. It is not intended as generic disk
  21. partitioning module!
  22. Independent on 'force' parameter value and actual disk state, the task
  23. always fills 'partition_pool' fact with all partitions on given disks,
  24. together with their sizes (in bytes). E.g.:
  25. partition_sizes = [
  26. { name: sda1, Size: 1048576000 },
  27. { name: sda2, Size: 1048576000 },
  28. { name: sdb1, Size: 1048576000 },
  29. ...
  30. ]
  31. options:
  32. disk:
  33. description:
  34. - Disk to partition.
  35. size:
  36. description:
  37. - Sizes of partitions to create and their weights. Has form of:
  38. <size1>[:<weigth1>][,<size2>[:<weight2>][,...]]
  39. - Any <size> can end with 'm' or 'M' for megabyte, 'g/G' for gigabyte
  40. and 't/T' for terabyte. Megabyte is used when no unit is specified.
  41. - If <weight> is missing, 1.0 is used.
  42. - From each specified partition <sizeX>, number of these partitions are
  43. created so they occupy spaces represented by <weightX>, proportionally to
  44. other weights.
  45. - Example 1: size=100G says, that the whole disk is split in number of 100 GiB
  46. partitions. On 1 TiB disk, 10 partitions will be created.
  47. - Example 2: size=100G:1,10G:1 says that ratio of space occupied by 100 GiB
  48. partitions and 10 GiB partitions is 1:1. Therefore, on 1 TiB disk, 500 GiB
  49. will be split into five 100 GiB partition and 500 GiB will be split into fifty
  50. 10GiB partitions.
  51. - size=100G:1,10G:1 = 5x 100 GiB and 50x 10 GiB partitions (on 1 TiB disk).
  52. - Example 3: size=200G:1,100G:2 says that the ratio of space occupied by 200 GiB
  53. partitions and 100GiB partition is 1:2. Therefore, on 1 TiB disk, 1/3
  54. (300 GiB) should be occupied by 200 GiB partitions. Only one fits there,
  55. so only one is created (we always round nr. of partitions *down*). The rest
  56. (800 GiB) is split into eight 100 GiB partitions, even though it's more
  57. than 2/3 of total space - free space is always allocated as much as possible.
  58. - size=200G:1,100G:2 = 1x 200 GiB and 8x 100 GiB partitions (on 1 TiB disk).
  59. - Example: size=200G:1,100G:1,50G:1 says that the ratio of space occupied by
  60. 200 GiB, 100 GiB and 50 GiB partitions is 1:1:1. Therefore 1/3 of 1 TiB disk
  61. is dedicated to 200 GiB partitions. Only one fits there and only one is
  62. created. The rest (800 GiB) is distributed according to remaining weights:
  63. 100 GiB vs 50 GiB is 1:1, we create four 100 GiB partitions (400 GiB in total)
  64. and eight 50 GiB partitions (again, 400 GiB).
  65. - size=200G:1,100G:1,50G:1 = 1x 200 GiB, 4x 100 GiB and 8x 50 GiB partitions
  66. (on 1 TiB disk).
  67. force:
  68. description:
  69. - If True, it will always overwite partition table on the disk and create new one.
  70. - If False (default), it won't change existing partition tables.
  71. """
  72. # It's not class, it's more a simple struct with almost no functionality.
  73. # pylint: disable=too-few-public-methods
  74. class PartitionSpec(object):
  75. """ Simple class to represent required partitions."""
  76. def __init__(self, size, weight):
  77. """ Initialize the partition specifications."""
  78. # Size of the partitions
  79. self.size = size
  80. # Relative weight of this request
  81. self.weight = weight
  82. # Number of partitions to create, will be calculated later
  83. self.count = -1
  84. def set_count(self, count):
  85. """ Set count of parititions of this specification. """
  86. self.count = count
  87. def assign_space(total_size, specs):
  88. """
  89. Satisfy all the PartitionSpecs according to their weight.
  90. In other words, calculate spec.count of all the specs.
  91. """
  92. total_weight = 0.0
  93. for spec in specs:
  94. total_weight += float(spec.weight)
  95. for spec in specs:
  96. num_blocks = int((float(spec.weight) / total_weight) * (total_size / float(spec.size)))
  97. spec.set_count(num_blocks)
  98. total_size -= num_blocks * spec.size
  99. total_weight -= spec.weight
  100. def partition(diskname, specs, force=False, check_mode=False):
  101. """
  102. Create requested partitions.
  103. Returns nr. of created partitions or 0 when the disk was already partitioned.
  104. """
  105. count = 0
  106. dev = parted.getDevice(diskname)
  107. try:
  108. disk = parted.newDisk(dev)
  109. except parted.DiskException:
  110. # unrecognizable format, treat as empty disk
  111. disk = None
  112. if disk and len(disk.partitions) > 0 and not force:
  113. print("skipping", diskname)
  114. return 0
  115. # create new partition table, wiping all existing data
  116. disk = parted.freshDisk(dev, 'gpt')
  117. # calculate nr. of partitions of each size
  118. assign_space(dev.getSize(), specs)
  119. last_megabyte = 1
  120. for spec in specs:
  121. for _ in range(spec.count):
  122. # create the partition
  123. start = parted.sizeToSectors(last_megabyte, "MiB", dev.sectorSize)
  124. length = parted.sizeToSectors(spec.size, "MiB", dev.sectorSize)
  125. geo = parted.Geometry(device=dev, start=start, length=length)
  126. filesystem = parted.FileSystem(type='ext4', geometry=geo)
  127. part = parted.Partition(
  128. disk=disk,
  129. type=parted.PARTITION_NORMAL,
  130. fs=filesystem,
  131. geometry=geo)
  132. disk.addPartition(partition=part, constraint=dev.optimalAlignedConstraint)
  133. last_megabyte += spec.size
  134. count += 1
  135. try:
  136. if not check_mode:
  137. disk.commit()
  138. except parted.IOException:
  139. # partitions have been written, but we have been unable to inform the
  140. # kernel of the change, probably because they are in use.
  141. # Ignore it and hope for the best...
  142. pass
  143. return count
  144. def parse_spec(text):
  145. """ Parse string with partition specification. """
  146. tokens = text.split(",")
  147. specs = []
  148. for token in tokens:
  149. if ":" not in token:
  150. token += ":1"
  151. (sizespec, weight) = token.split(':')
  152. weight = float(weight) # throws exception with reasonable error string
  153. units = {"m": 1, "g": 1 << 10, "t": 1 << 20, "p": 1 << 30}
  154. unit = units.get(sizespec[-1].lower(), None)
  155. if not unit:
  156. # there is no unit specifier, it must be just the number
  157. size = float(sizespec)
  158. unit = 1
  159. else:
  160. size = float(sizespec[:-1])
  161. spec = PartitionSpec(int(size * unit), weight)
  162. specs.append(spec)
  163. return specs
  164. def get_partitions(diskpath):
  165. """ Return array of partition names for given disk """
  166. dev = parted.getDevice(diskpath)
  167. disk = parted.newDisk(dev)
  168. partitions = []
  169. for part in disk.partitions:
  170. (_, _, pname) = part.path.rsplit("/")
  171. partitions.append({"name": pname, "size": part.getLength() * dev.sectorSize})
  172. return partitions
  173. def main():
  174. """ Ansible module main method. """
  175. module = AnsibleModule( # noqa: F405
  176. argument_spec=dict(
  177. disks=dict(required=True, type='str'),
  178. force=dict(required=False, default="no", type='bool'),
  179. sizes=dict(required=True, type='str')
  180. ),
  181. supports_check_mode=True,
  182. )
  183. disks = module.params['disks']
  184. force = module.params['force']
  185. if force is None:
  186. force = False
  187. sizes = module.params['sizes']
  188. try:
  189. specs = parse_spec(sizes)
  190. except ValueError as ex:
  191. err = "Error parsing sizes=" + sizes + ": " + str(ex)
  192. module.fail_json(msg=err)
  193. partitions = []
  194. changed_count = 0
  195. for disk in disks.split(","):
  196. try:
  197. changed_count += partition(disk, specs, force, module.check_mode)
  198. except Exception as ex:
  199. err = "Error creating partitions on " + disk + ": " + str(ex)
  200. raise
  201. # module.fail_json(msg=err)
  202. partitions += get_partitions(disk)
  203. module.exit_json(changed=(changed_count > 0), ansible_facts={"partition_pool": partitions})
  204. # ignore pylint errors related to the module_utils import
  205. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, wrong-import-order, wrong-import-position
  206. # import module snippets
  207. from ansible.module_utils.basic import * # noqa: E402,F403
  208. main()