3
# Copyright (C) 2009-2010 Canonical Ltd.
4
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
6
# Author: Ben Howard <ben.howard@canonical.com>
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License version 3, as
10
# published by the Free Software Foundation.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
from cloudinit import util
20
from cloudinit.settings import PER_INSTANCE
24
frequency = PER_INSTANCE
26
# Define the commands to use
27
UDEVADM_CMD = util.which('udevadm')
28
SFDISK_CMD = util.which("sfdisk")
29
LSBLK_CMD = util.which("lsblk")
30
BLKID_CMD = util.which("blkid")
31
BLKDEV_CMD = util.which("blockdev")
33
LOG = logging.getLogger(__name__)
36
def handle(_name, cfg, cloud, log, _args):
38
Call util.prep_disk for disk_setup cloud-config.
39
See doc/examples/cloud-config_disk-setup.txt for documentation on the
42
disk_setup = cfg.get("disk_setup")
43
if isinstance(disk_setup, dict):
44
log.info("Partitioning disks.")
45
for disk, definition in disk_setup.items():
46
if not isinstance(definition, dict):
47
log.warn("Invalid disk definition for %s" % disk)
51
log.debug("Creating new partition table/disk")
52
util.log_time(logfunc=LOG.debug,
53
msg="Creating partition on %s" % disk,
54
func=mkpart, args=(disk, cloud, definition))
55
except Exception as e:
56
util.logexc(LOG, "Failed partitioning operation\n%s" % e)
58
fs_setup = cfg.get("fs_setup")
59
if isinstance(fs_setup, list):
60
log.info("Creating file systems.")
61
for definition in fs_setup:
62
if not isinstance(definition, dict):
63
log.warn("Invalid file system definition: %s" % definition)
67
log.debug("Creating new filesystem.")
68
device = definition.get('device')
69
util.log_time(logfunc=LOG.debug,
70
msg="Creating fs for %s" % device,
71
func=mkfs, args=(cloud, definition))
72
except Exception as e:
73
util.logexc(LOG, "Failed during filesystem operation\n%s" % e)
76
def is_default_device(name, cloud, fallback=None):
78
Ask the cloud datasource if the 'name' maps to a default
79
device. If so, return that value, otherwise return 'name', or
80
fallback if so defined.
85
_dev = cloud.device_name_to_device(name)
86
except Exception as e:
87
util.logexc(LOG, "Failed to find mapping for %s" % e)
98
def value_splitter(values, start=None):
100
Returns the key/value pairs of output sent as string
101
like: FOO='BAR' HOME='127.0.0.1'
103
_values = shlex.split(values)
105
_values = _values[start:]
107
for key, value in [x.split('=') for x in _values]:
111
def device_type(device):
113
Return the device type of the device by calling lsblk.
116
lsblk_cmd = [LSBLK_CMD, '--pairs', '--nodeps', '--out', 'NAME,TYPE',
120
info, _err = util.subp(lsblk_cmd)
121
except Exception as e:
122
raise Exception("Failed during disk check for %s\n%s" % (device, e))
124
for key, value in value_splitter(info):
125
if key.lower() == "type":
131
def is_device_valid(name, partition=False):
133
Check if the device is a valid device.
137
d_type = device_type(name)
139
LOG.warn("Query against device %s failed" % name)
142
if partition and d_type == 'part':
144
elif not partition and d_type == 'disk':
149
def check_fs(device):
151
Check if the device has a filesystem on it
153
Output of blkid is generally something like:
154
/dev/sda: LABEL="Backup500G" UUID="..." TYPE="ext4"
156
Return values are device, label, type, uuid
158
out, label, fs_type, uuid = None, None, None, None
160
blkid_cmd = [BLKID_CMD, '-c', '/dev/null', device]
162
out, _err = util.subp(blkid_cmd, rcs=[0, 2])
163
except Exception as e:
164
raise Exception("Failed during disk check for %s\n%s" % (device, e))
167
if len(out.splitlines()) == 1:
168
for key, value in value_splitter(out, start=1):
169
if key.lower() == 'label':
171
elif key.lower() == 'type':
173
elif key.lower() == 'uuid':
176
return label, fs_type, uuid
179
def is_filesystem(device):
181
Returns true if the device has a file system.
183
_, fs_type, _ = check_fs(device)
187
def find_device_node(device, fs_type=None, label=None, valid_targets=None,
190
Find a device that is either matches the spec, or the first
192
The return is value is (<device>, <bool>) where the device is the
193
device to use and the bool is whether the device matches the
196
Note: This works with GPT partition tables!
198
if not valid_targets:
199
valid_targets = ['disk', 'part']
201
lsblk_cmd = [LSBLK_CMD, '--pairs', '--out', 'NAME,TYPE,FSTYPE,LABEL',
205
info, _err = util.subp(lsblk_cmd)
206
except Exception as e:
207
raise Exception("Failed during disk check for %s\n%s" % (device, e))
209
raw_device_used = False
210
parts = [x for x in (info.strip()).splitlines() if len(x.split()) > 0]
219
for key, value in value_splitter(part):
220
d[key.lower()] = value
222
if d['fstype'] == fs_type and \
223
((label_match and d['label'] == label) or not label_match):
224
# If we find a matching device, we return that
225
return ('/dev/%s' % d['name'], True)
227
if d['type'] in valid_targets:
229
if d['type'] != 'disk' or d['fstype']:
230
raw_device_used = True
232
if d['type'] == 'disk':
233
# Skip the raw disk, its the default
236
elif not d['fstype']:
237
return ('/dev/%s' % d['name'], False)
239
if not raw_device_used:
240
return (device, False)
242
LOG.warn("Failed to find device during available device search.")
246
def is_disk_used(device):
248
Check if the device is currently used. Returns false if there
249
is no filesystem found on the disk.
251
lsblk_cmd = [LSBLK_CMD, '--pairs', '--out', 'NAME,TYPE',
255
info, _err = util.subp(lsblk_cmd)
256
except Exception as e:
257
# if we error out, we can't use the device
259
"Error checking for filesystem on %s\n%s" % (device, e))
262
# If there is any output, then the device has something
263
if len(info.splitlines()) > 1:
269
def get_hdd_size(device):
271
Returns the hard disk size.
272
This works with any disk type, including GPT.
275
size_cmd = [SFDISK_CMD, '--show-size', device]
278
size, _err = util.subp(size_cmd)
279
except Exception as e:
280
raise Exception("Failed to get %s size\n%s" % (device, e))
282
return int(size.strip())
285
def get_dyn_func(*args):
287
Call the appropriate function.
289
The first value is the template for function name
290
The second value is the template replacement
291
The remain values are passed to the function
293
For example: get_dyn_func("foo_%s", 'bar', 1, 2, 3,)
294
would call "foo_bar" with args of 1, 2, 3
297
raise Exception("Unable to determine dynamic funcation name")
299
func_name = (args[0] % args[1])
304
return globals()[func_name](*func_args)
306
return globals()[func_name]
309
raise Exception("No such function %s to call!" % func_name)
312
def check_partition_mbr_layout(device, layout):
314
Returns true if the partition layout matches the one on the disk
316
Layout should be a list of values. At this time, this only
317
verifies that the number of partitions and their labels is correct.
321
prt_cmd = [SFDISK_CMD, "-l", device]
323
out, _err = util.subp(prt_cmd, data="%s\n" % layout)
324
except Exception as e:
325
raise Exception("Error running partition command on %s\n%s" % (
329
for line in out.splitlines():
334
if device in _line[0]:
335
# We don't understand extended partitions yet
336
if _line[-1].lower() in ['extended', 'empty']:
339
# Find the partition types
341
for x in sorted(range(1, len(_line)), reverse=True):
342
if _line[x].isdigit() and _line[x] != '/':
343
type_label = _line[x]
346
found_layout.append(type_label)
348
if isinstance(layout, bool):
349
# if we are using auto partitioning, or "True" be happy
350
# if a single partition exists.
351
if layout and len(found_layout) >= 1:
356
if len(found_layout) != len(layout):
359
# This just makes sure that the number of requested
360
# partitions and the type labels are right
361
for x in range(1, len(layout) + 1):
362
if isinstance(layout[x - 1], tuple):
363
_, part_type = layout[x]
364
if int(found_layout[x]) != int(part_type):
371
def check_partition_layout(table_type, device, layout):
373
See if the partition lay out matches.
375
This is future a future proofing function. In order
376
to add support for other disk layout schemes, add a
377
function called check_partition_%s_layout
379
return get_dyn_func("check_partition_%s_layout", table_type, device,
383
def get_partition_mbr_layout(size, layout):
385
Calculate the layout of the partition table. Partition sizes
386
are defined as percentage values or a tuple of percentage and
392
Defines the first partition to be a size of 1/3 the disk,
393
while the remaining 2/3's will be of type Linux Swap.
396
if not isinstance(layout, list) and isinstance(layout, bool):
397
# Create a single partition
400
if (len(layout) == 0 and isinstance(layout, list)) or \
401
not isinstance(layout, list):
402
raise Exception("Partition layout is invalid")
404
last_part_num = len(layout)
405
if last_part_num > 4:
406
raise Exception("Only simply partitioning is allowed.")
411
part_type = 83 # Default to Linux
415
if isinstance(part, list):
417
raise Exception("Partition was incorrectly defined: %s" % \
419
percent, part_type = part
421
part_size = int((float(size) * (float(percent) / 100)) / 1024)
423
if part_num == last_part_num:
424
part_definition.append(",,%s" % part_type)
426
part_definition.append(",%s,%s" % (part_size, part_type))
428
sfdisk_definition = "\n".join(part_definition)
429
if len(part_definition) > 4:
430
raise Exception("Calculated partition definition is too big\n%s" %
433
return sfdisk_definition
436
def get_partition_layout(table_type, size, layout):
438
Call the appropriate function for creating the table
439
definition. Returns the table definition
441
This is a future proofing function. To add support for
442
other layouts, simply add a "get_partition_%s_layout"
445
return get_dyn_func("get_partition_%s_layout", table_type, size, layout)
448
def read_parttbl(device):
450
Use partprobe instead of 'udevadm'. Partprobe is the only
451
reliable way to probe the partition table.
453
blkdev_cmd = [BLKDEV_CMD, '--rereadpt', device]
454
udev_cmd = [UDEVADM_CMD, 'settle']
457
util.subp(blkdev_cmd)
459
except Exception as e:
460
util.logexc(LOG, "Failed reading the partition table %s" % e)
463
def exec_mkpart_mbr(device, layout):
465
Break out of mbr partition to allow for future partition
468
# Create the partitions
469
prt_cmd = [SFDISK_CMD, "--Linux", "-uM", device]
471
util.subp(prt_cmd, data="%s\n" % layout)
472
except Exception as e:
473
raise Exception("Failed to partition device %s\n%s" % (device, e))
478
def exec_mkpart(table_type, device, layout):
480
Fetches the function for creating the table type.
481
This allows to dynamically find which function to call.
484
table_type: type of partition table to use
485
device: the device to work on
486
layout: layout definition specific to partition table
488
return get_dyn_func("exec_mkpart_%s", table_type, device, layout)
491
def mkpart(device, cloud, definition):
493
Creates the partition table.
496
cloud: the cloud object
497
definition: dictionary describing how to create the partition.
499
The following are supported values in the dict:
500
overwrite: Should the partition table be created regardless
501
of any pre-exisiting data?
502
layout: the layout of the partition table
503
table_type: Which partition table to use, defaults to MBR
504
device: the device to work on.
507
LOG.debug("Checking values for %s definition" % device)
508
overwrite = definition.get('overwrite', False)
509
layout = definition.get('layout', False)
510
table_type = definition.get('table_type', 'mbr')
511
_device = is_default_device(device, cloud)
513
# Check if the default device is a partition or not
514
LOG.debug("Checking against default devices")
515
if _device and (_device != device):
516
if not is_device_valid(_device):
517
_device = _device[:-1]
519
if not is_device_valid(_device):
520
raise Exception("Unable to find backing block device for %s" % \
523
LOG.debug("Mapped %s to physical device %s" % (device, _device))
526
if (isinstance(layout, bool) and not layout) or not layout:
527
LOG.debug("Device is not to be partitioned, skipping")
528
return # Device is not to be partitioned
530
# This prevents you from overwriting the device
531
LOG.debug("Checking if device %s is a valid device" % device)
532
if not is_device_valid(device):
533
raise Exception("Device %s is not a disk device!" % device)
535
LOG.debug("Checking if device layout matches")
536
if check_partition_layout(table_type, device, layout):
537
LOG.debug("Device partitioning layout matches")
540
LOG.debug("Checking if device is safe to partition")
541
if not overwrite and (is_disk_used(device) or is_filesystem(device)):
542
LOG.debug("Skipping partitioning on configured device %s" % device)
545
LOG.debug("Checking for device size")
546
device_size = get_hdd_size(device)
548
LOG.debug("Calculating partition layout")
549
part_definition = get_partition_layout(table_type, device_size, layout)
550
LOG.debug(" Layout is: %s" % part_definition)
552
LOG.debug("Creating partition table on %s" % device)
553
exec_mkpart(table_type, device, part_definition)
555
LOG.debug("Partition table created for %s" % device)
558
def mkfs(cloud, fs_cfg):
560
Create a file system on the device.
562
label: defines the label to use on the device
563
fs_cfg: defines how the filesystem is to look
564
The following values are required generally:
565
device: which device or cloud defined default_device
566
filesystem: which file system type
567
overwrite: indiscriminately create the file system
568
partition: when device does not define a partition,
569
setting this to a number will mean
570
device + partition. When set to 'auto', the
571
first free device or the first device which
572
matches both label and type will be used.
574
'any' means the first filesystem that matches
577
When 'cmd' is provided then no other parameter is required.
579
fs_cfg['partition'] = 'any'
580
label = fs_cfg.get('label')
581
device = fs_cfg.get('device')
582
partition = str(fs_cfg.get('partition'))
583
fs_type = fs_cfg.get('filesystem')
584
fs_cmd = fs_cfg.get('cmd', [])
585
fs_opts = fs_cfg.get('extra_opts', [])
586
overwrite = fs_cfg.get('overwrite', False)
588
# This allows you to define the default ephemeral or swap
589
LOG.debug("Checking %s against default devices" % device)
590
_device = is_default_device(label, cloud, fallback=device)
591
if _device and (_device != device):
592
if not is_device_valid(_device):
593
raise Exception("Unable to find backing block device for %s" % \
596
LOG.debug("Mapped %s to physical device %s" % (device, _device))
599
if not partition or partition.isdigit():
600
# Handle manual definition of partition
601
if partition.isdigit():
602
device = "%s%s" % (device, partition)
603
LOG.debug("Manual request of partition %s for %s" % (
606
# Check to see if the fs already exists
607
LOG.debug("Checking device %s" % device)
608
check_label, check_fstype, _ = check_fs(device)
609
LOG.debug("Device %s has %s %s" % (device, check_label, check_fstype))
611
if check_label == label and check_fstype == fs_type:
612
LOG.debug("Existing file system found at %s" % device)
615
LOG.warn("Device %s has required file system" % device)
618
LOG.warn("Destroying filesystem on %s" % device)
621
LOG.debug("Device %s is cleared for formating" % device)
623
elif partition and str(partition).lower() in ('auto', 'any'):
624
# For auto devices, we match if the filesystem does exist
626
LOG.debug("Identifying device to create %s filesytem on" % label)
628
# any mean pick the first match on the device with matching fs_type
630
if partition.lower() == 'any':
633
device, reuse = find_device_node(device, fs_type=fs_type, label=label,
634
label_match=label_match)
635
LOG.debug("Automatic device for %s identified as %s" % (
639
LOG.debug("Found filesystem match, skipping formating.")
643
LOG.debug("No device aviable that matches request.")
644
LOG.debug("Skipping fs creation for %s" % fs_cfg)
648
LOG.debug("Error in device identification handling.")
651
LOG.debug("File system %s will be created on %s" % (label, device))
653
# Make sure the device is defined
655
LOG.critical("Device is not known: %s" % fs_cfg)
658
# Check that we can create the FS
659
if not label or not fs_type:
660
LOG.debug("Command to create filesystem %s is bad. Skipping." % \
663
# Create the commands
665
fs_cmd = fs_cfg['cmd'] % {'label': label,
666
'filesystem': fs_type,
670
# Find the mkfs command
671
mkfs_cmd = util.which("mkfs.%s" % fs_type)
673
mkfs_cmd = util.which("mk%s" % fs_type)
676
LOG.critical("Unable to locate command to create filesystem.")
679
fs_cmd = [mkfs_cmd, device]
682
fs_cmd.extend(["-L", label])
684
# Add the extends FS options
686
fs_cmd.extend(fs_opts)
688
LOG.debug("Creating file system %s on %s" % (label, device))
689
LOG.debug(" Using cmd: %s" % "".join(fs_cmd))
692
except Exception as e:
693
raise Exception("Failed to exec of '%s':\n%s" % (fs_cmd, e))