summaryrefslogtreecommitdiffstats
path: root/roles/kube_nfs_volumes/library
diff options
context:
space:
mode:
authorJan Safranek <jsafrane@redhat.com>2015-05-20 11:40:35 +0200
committerJan Safranek <jsafrane@redhat.com>2015-05-20 11:40:35 +0200
commit98acbb6d24921ded065fca3b57943ef1ae7fb15f (patch)
tree17571cd1d1ef49cc81b9dcd8f1684ff65c30e37f /roles/kube_nfs_volumes/library
parent8dedb32348549d8aa279201cfb89c50d5dc83ba5 (diff)
downloadopenshift-98acbb6d24921ded065fca3b57943ef1ae7fb15f.tar.gz
openshift-98acbb6d24921ded065fca3b57943ef1ae7fb15f.tar.bz2
openshift-98acbb6d24921ded065fca3b57943ef1ae7fb15f.tar.xz
openshift-98acbb6d24921ded065fca3b57943ef1ae7fb15f.zip
Add nfs-volumes role.
This role is useful to use (physical) disks as persistent volumes in Kubernetes. It partitions the disks, exports the partitions as NFS shares and registers the shares as physical volumes at remote Kubernetes installation.
Diffstat (limited to 'roles/kube_nfs_volumes/library')
-rw-r--r--roles/kube_nfs_volumes/library/partitionpool.py240
1 files changed, 240 insertions, 0 deletions
diff --git a/roles/kube_nfs_volumes/library/partitionpool.py b/roles/kube_nfs_volumes/library/partitionpool.py
new file mode 100644
index 000000000..1ac8eed4d
--- /dev/null
+++ b/roles/kube_nfs_volumes/library/partitionpool.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python
+"""
+Ansible module for partitioning.
+"""
+
+# There is no pyparted on our Jenkins worker
+# pylint: disable=import-error
+import parted
+
+DOCUMENTATION = """
+---
+module: partitionpool
+short_description; Partition a disk into parititions.
+description:
+ - Creates partitions on given disk based on partition sizes and their weights.
+ Unless 'force' option is set to True, it ignores already partitioned disks.
+
+ When the disk is empty or 'force' is set to True, it always creates a new
+ GPT partition table on the disk. Then it creates number of partitions, based
+ on their weights.
+
+ This module should be used when a system admin wants to split existing disk(s)
+ into pools of partitions of specific sizes. It is not intended as generic disk
+ partitioning module!
+
+ Independent on 'force' parameter value and actual disk state, the task
+ always fills 'partition_pool' fact with all partitions on given disks,
+ together with their sizes (in bytes). E.g.:
+ partition_sizes = [
+ { name: sda1, Size: 1048576000 },
+ { name: sda2, Size: 1048576000 },
+ { name: sdb1, Size: 1048576000 },
+ ...
+ ]
+
+options:
+ disk:
+ description:
+ - Disk to partition.
+ size:
+ description:
+ - Sizes of partitions to create and their weights. Has form of:
+ <size1>[:<weigth1>][,<size2>[:<weight2>][,...]]
+ - Any <size> can end with 'm' or 'M' for megabyte, 'g/G' for gigabyte
+ and 't/T' for terabyte. Megabyte is used when no unit is specified.
+ - If <weight> is missing, 1.0 is used.
+ - From each specified partition <sizeX>, number of these partitions are
+ created so they occupy spaces represented by <weightX>, proportionally to
+ other weights.
+
+ - Example 1: size=100G says, that the whole disk is split in number of 100 GiB
+ partitions. On 1 TiB disk, 10 partitions will be created.
+
+ - Example 2: size=100G:1,10G:1 says that ratio of space occupied by 100 GiB
+ partitions and 10 GiB partitions is 1:1. Therefore, on 1 TiB disk, 500 GiB
+ will be split into five 100 GiB partition and 500 GiB will be split into fifty
+ 10GiB partitions.
+ - size=100G:1,10G:1 = 5x 100 GiB and 50x 10 GiB partitions (on 1 TiB disk).
+
+ - Example 3: size=200G:1,100G:2 says that the ratio of space occupied by 200 GiB
+ partitions and 100GiB partition is 1:2. Therefore, on 1 TiB disk, 1/3
+ (300 GiB) should be occupied by 200 GiB partitions. Only one fits there,
+ so only one is created (we always round nr. of partitions *down*). Teh rest
+ (800 GiB) is split into eight 100 GiB partitions, even though it's more
+ than 2/3 of total space - free space is always allocated as much as possible.
+ - size=200G:1,100G:2 = 1x 200 GiB and 8x 100 GiB partitions (on 1 TiB disk).
+
+ - Example: size=200G:1,100G:1,50G:1 says that the ratio of space occupied by
+ 200 GiB, 100 GiB and 50 GiB partitions is 1:1:1. Therefore 1/3 of 1 TiB disk
+ is dedicated to 200 GiB partitions. Only one fits there and only one is
+ created. The rest (800 GiB) is distributed according to remaining weights:
+ 100 GiB vs 50 GiB is 1:1, we create four 100 GiB partitions (400 GiB in total)
+ and eight 50 GiB partitions (again, 400 GiB).
+ - size=200G:1,100G:1,50G:1 = 1x 200 GiB, 4x 100 GiB and 8x 50 GiB partitions
+ (on 1 TiB disk).
+
+ force:
+ description:
+ - If True, it will always overwite partition table on the disk and create new one.
+ - If False (default), it won't change existing partition tables.
+
+"""
+
+# It's not class, it's more a simple struct with almost no functionality.
+# pylint: disable=too-few-public-methods
+class PartitionSpec(object):
+ """ Simple class to represent required partitions."""
+ def __init__(self, size, weight):
+ """ Initialize the partition specifications."""
+ # Size of the partitions
+ self.size = size
+ # Relative weight of this request
+ self.weight = weight
+ # Number of partitions to create, will be calculated later
+ self.count = -1
+
+ def set_count(self, count):
+ """ Set count of parititions of this specification. """
+ self.count = count
+
+def assign_space(total_size, specs):
+ """
+ Satisfy all the PartitionSpecs according to their weight.
+ In other words, calculate spec.count of all the specs.
+ """
+ total_weight = 0.0
+ for spec in specs:
+ total_weight += float(spec.weight)
+
+ for spec in specs:
+ num_blocks = int((float(spec.weight) / total_weight) * (total_size / float(spec.size)))
+ spec.set_count(num_blocks)
+ total_size -= num_blocks * spec.size
+ total_weight -= spec.weight
+
+def partition(diskname, specs, force=False, check_mode=False):
+ """
+ Create requested partitions.
+ Returns nr. of created partitions or 0 when the disk was already partitioned.
+ """
+ count = 0
+
+ dev = parted.getDevice(diskname)
+ try:
+ disk = parted.newDisk(dev)
+ except parted.DiskException:
+ # unrecognizable format, treat as empty disk
+ disk = None
+
+ if disk and len(disk.partitions) > 0 and not force:
+ print "skipping", diskname
+ return 0
+
+ # create new partition table, wiping all existing data
+ disk = parted.freshDisk(dev, 'gpt')
+ # calculate nr. of partitions of each size
+ assign_space(dev.getSize(), specs)
+ last_megabyte = 1
+ for spec in specs:
+ for _ in range(spec.count):
+ # create the partition
+ start = parted.sizeToSectors(last_megabyte, "MiB", dev.sectorSize)
+ length = parted.sizeToSectors(spec.size, "MiB", dev.sectorSize)
+ geo = parted.Geometry(device=dev, start=start, length=length)
+ filesystem = parted.FileSystem(type='ext4', geometry=geo)
+ part = parted.Partition(
+ disk=disk,
+ type=parted.PARTITION_NORMAL,
+ fs=filesystem,
+ geometry=geo)
+ disk.addPartition(partition=part, constraint=dev.optimalAlignedConstraint)
+ last_megabyte += spec.size
+ count += 1
+ try:
+ if not check_mode:
+ disk.commit()
+ except parted.IOException:
+ # partitions have been written, but we have been unable to inform the
+ # kernel of the change, probably because they are in use.
+ # Ignore it and hope for the best...
+ pass
+ return count
+
+def parse_spec(text):
+ """ Parse string with partition specification. """
+ tokens = text.split(",")
+ specs = []
+ for token in tokens:
+ if not ":" in token:
+ token += ":1"
+
+ (sizespec, weight) = token.split(':')
+ weight = float(weight) # throws exception with reasonable error string
+
+ units = {"m": 1, "g": 1 << 10, "t": 1 << 20, "p": 1 << 30}
+ unit = units.get(sizespec[-1].lower(), None)
+ if not unit:
+ # there is no unit specifier, it must be just the number
+ size = float(sizespec)
+ unit = 1
+ else:
+ size = float(sizespec[:-1])
+ spec = PartitionSpec(int(size * unit), weight)
+ specs.append(spec)
+ return specs
+
+def get_partitions(diskpath):
+ """ Return array of partition names for given disk """
+ dev = parted.getDevice(diskpath)
+ disk = parted.newDisk(dev)
+ partitions = []
+ for part in disk.partitions:
+ (_, _, pname) = part.path.rsplit("/")
+ partitions.append({"name": pname, "size": part.getLength() * dev.sectorSize})
+
+ return partitions
+
+
+def main():
+ """ Ansible module main method. """
+ module = AnsibleModule(
+ argument_spec=dict(
+ disks=dict(required=True, type='str'),
+ force=dict(required=False, default="no", type='bool'),
+ sizes=dict(required=True, type='str')
+ ),
+ supports_check_mode=True,
+ )
+
+ disks = module.params['disks']
+ force = module.params['force']
+ if force is None:
+ force = False
+ sizes = module.params['sizes']
+
+ try:
+ specs = parse_spec(sizes)
+ except ValueError, ex:
+ err = "Error parsing sizes=" + sizes + ": " + str(ex)
+ module.fail_json(msg=err)
+
+ partitions = []
+ changed_count = 0
+ for disk in disks.split(","):
+ try:
+ changed_count += partition(disk, specs, force, module.check_mode)
+ except Exception, ex:
+ err = "Error creating partitions on " + disk + ": " + str(ex)
+ raise
+ #module.fail_json(msg=err)
+ partitions += get_partitions(disk)
+
+ module.exit_json(changed=(changed_count > 0), ansible_facts={"partition_pool": partitions})
+
+# ignore pylint errors related to the module_utils import
+# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
+# import module snippets
+from ansible.module_utils.basic import *
+main()
+