summaryrefslogtreecommitdiffstats
path: root/roles/kube_nfs_volumes/library/partitionpool.py
blob: 1ac8eed4df1f660794525f48cacf165b8a70ee8d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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()