1
# Copyright (C) 2016 Canonical Ltd.
3
# Author: Wesley Wiedenmeier <wesley.wiedenmeier@canonical.com>
5
# Curtin is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU Affero General Public License as published by the
7
# Free Software Foundation, either version 3 of the License, or (at your
8
# option) any later version.
10
# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
15
# You should have received a copy of the GNU Affero General Public License
16
# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
19
This module provides a mechanism for shutting down virtual storage layers on
20
top of a block device, making it possible to reuse the block device without
21
having to reboot the system
26
from curtin import (block, udev, util)
27
from curtin.block import lvm
28
from curtin.log import LOG
31
def _define_handlers_registry():
33
returns instantiated dev_types
36
'partition': {'shutdown': wipe_superblock,
37
'ident': identify_partition},
38
'lvm': {'shutdown': shutdown_lvm, 'ident': identify_lvm},
39
'crypt': {'shutdown': shutdown_crypt, 'ident': identify_crypt},
40
'raid': {'shutdown': shutdown_mdadm, 'ident': identify_mdadm},
41
'bcache': {'shutdown': shutdown_bcache, 'ident': identify_bcache},
42
'disk': {'ident': lambda x: False, 'shutdown': wipe_superblock},
46
def get_dmsetup_uuid(device):
48
get the dm uuid for a specified dmsetup device
50
blockdev = block.sysfs_to_devpath(device)
51
(out, _) = util.subp(['dmsetup', 'info', blockdev, '-C', '-o', 'uuid',
52
'--noheadings'], capture=True)
56
def get_bcache_using_dev(device):
58
Get the /sys/fs/bcache/ path of the bcache volume using specified device
60
# FIXME: when block.bcache is written this should be moved there
61
sysfs_path = block.sys_block_path(device)
62
return os.path.realpath(os.path.join(sysfs_path, 'bcache', 'cache'))
65
def shutdown_bcache(device):
67
Shut down bcache for specified bcache device
69
bcache_shutdown_message = ('shutdown_bcache running on {} has determined '
70
'that the device has already been shut down '
71
'during handling of another bcache dev. '
72
'skipping'.format(device))
73
if not os.path.exists(device):
74
LOG.info(bcache_shutdown_message)
77
bcache_sysfs = get_bcache_using_dev(device)
78
if not os.path.exists(bcache_sysfs):
79
LOG.info(bcache_shutdown_message)
82
LOG.debug('stopping bcache at: %s', bcache_sysfs)
83
util.write_file(os.path.join(bcache_sysfs, 'stop'), '1', mode=None)
86
def shutdown_lvm(device):
88
Shutdown specified lvm device.
90
device = block.sys_block_path(device)
91
# lvm devices have a dm directory that containes a file 'name' containing
92
# '{volume group}-{logical volume}'. The volume can be freed using lvremove
93
name_file = os.path.join(device, 'dm', 'name')
94
(vg_name, lv_name) = lvm.split_lvm_name(util.load_file(name_file))
95
# use two --force flags here in case the volume group that this lv is
96
# attached two has been damaged
97
LOG.debug('running lvremove on %s/%s', vg_name, lv_name)
98
util.subp(['lvremove', '--force', '--force',
99
'{}/{}'.format(vg_name, lv_name)], rcs=[0, 5])
100
# if that was the last lvol in the volgroup, get rid of volgroup
101
if len(lvm.get_lvols_in_volgroup(vg_name)) == 0:
102
util.subp(['vgremove', '--force', '--force', vg_name], rcs=[0, 5])
107
def shutdown_crypt(device):
109
Shutdown specified cryptsetup device
111
blockdev = block.sysfs_to_devpath(device)
112
util.subp(['cryptsetup', 'remove', blockdev], capture=True)
115
def shutdown_mdadm(device):
117
Shutdown specified mdadm device.
119
blockdev = block.sysfs_to_devpath(device)
120
LOG.debug('using mdadm.mdadm_stop on dev: %s', blockdev)
121
block.mdadm.mdadm_stop(blockdev)
122
block.mdadm.mdadm_remove(blockdev)
125
def wipe_superblock(device):
127
Wrapper for block.wipe_volume compatible with shutdown function interface
129
blockdev = block.sysfs_to_devpath(device)
130
# when operating on a disk that used to have a dos part table with an
131
# extended partition, attempting to wipe the extended partition will fail
132
if block.is_extended_partition(blockdev):
133
LOG.info("extended partitions do not need wiping, so skipping: '%s'",
136
LOG.info('wiping superblock on %s', blockdev)
137
block.wipe_volume(blockdev, mode='superblock')
140
def identify_lvm(device):
142
determine if specified device is a lvm device
144
return (block.path_to_kname(device).startswith('dm') and
145
get_dmsetup_uuid(device).startswith('LVM'))
148
def identify_crypt(device):
150
determine if specified device is dm-crypt device
152
return (block.path_to_kname(device).startswith('dm') and
153
get_dmsetup_uuid(device).startswith('CRYPT'))
156
def identify_mdadm(device):
158
determine if specified device is a mdadm device
160
return block.path_to_kname(device).startswith('md')
163
def identify_bcache(device):
165
determine if specified device is a bcache device
167
return block.path_to_kname(device).startswith('bcache')
170
def identify_partition(device):
172
determine if specified device is a partition
174
path = os.path.join(block.sys_block_path(device), 'partition')
175
return os.path.exists(path)
178
def get_holders(device):
180
Look up any block device holders, return list of knames
182
# block.sys_block_path works when given a /sys or /dev path
183
sysfs_path = block.sys_block_path(device)
185
holders = os.listdir(os.path.join(sysfs_path, 'holders'))
186
LOG.debug("devname '%s' had holders: %s", device, holders)
190
def gen_holders_tree(device):
192
generate a tree representing the current storage hirearchy above 'device'
194
device = block.sys_block_path(device)
195
dev_name = block.path_to_kname(device)
196
# the holders for a device should consist of the devices in the holders/
197
# dir in sysfs and any partitions on the device. this ensures that a
198
# storage tree starting from a disk will include all devices holding the
200
holder_paths = ([block.sys_block_path(h) for h in get_holders(device)] +
201
block.get_sysfs_partitions(device))
202
# the DEV_TYPE registry contains a function under the key 'ident' for each
203
# device type entry that returns true if the device passed to it is of the
204
# correct type. there should never be a situation in which multiple
205
# identify functions return true. therefore, it will always work to take
206
# the device type with the first identify function that returns true as the
207
# device type for the current device. in the event that no identify
208
# functions return true, the device will be treated as a disk
209
# (DEFAULT_DEV_TYPE). the identify function for disk never returns true.
210
# the next() builtin in python will not raise a StopIteration exception if
211
# there is a default value defined
212
dev_type = next((k for k, v in DEV_TYPES.items() if v['ident'](device)),
215
'device': device, 'dev_type': dev_type, 'name': dev_name,
216
'holders': [gen_holders_tree(h) for h in holder_paths],
220
def plan_shutdown_holder_trees(holders_trees):
222
plan best order to shut down holders in, taking into account high level
223
storage layers that may have many devices below them
225
returns a sorted list of descriptions of storage config entries including
226
their path in /sys/block and their dev type
228
can accept either a single storage tree or a list of storage trees assumed
229
to start at an equal place in storage hirearchy (i.e. a list of trees
232
# holds a temporary registry of holders to allow cross references
233
# key = device sysfs path, value = {} of priority level, shutdown function
236
# normalize to list of trees
237
if not isinstance(holders_trees, (list, tuple)):
238
holders_trees = [holders_trees]
240
def flatten_holders_tree(tree, level=0):
242
add entries from holders tree to registry with level key corresponding
243
to how many layers from raw disks the current device is at
245
device = tree['device']
247
# always go with highest level if current device has been
248
# encountered already. since the device and everything above it is
249
# re-added to the registry it ensures that any increase of level
250
# required here will propagate down the tree
251
# this handles a scenario like mdadm + bcache, where the backing
252
# device for bcache is a 3nd level item like mdadm, but the cache
253
# device is 1st level (disk) or second level (partition), ensuring
254
# that the bcache item is always considered higher level than
255
# anything else regardless of whether it was added to the tree via
256
# the cache device or backing device first
258
level = max(reg[device]['level'], level)
260
reg[device] = {'level': level, 'device': device,
261
'dev_type': tree['dev_type']}
263
# handle holders above this level
264
for holder in tree['holders']:
265
flatten_holders_tree(holder, level=level + 1)
267
# flatten the holders tree into the registry
268
for holders_tree in holders_trees:
269
flatten_holders_tree(holders_tree)
271
# return list of entry dicts with highest level first
272
return [reg[k] for k in sorted(reg, key=lambda x: reg[x]['level'] * -1)]
275
def format_holders_tree(holders_tree):
277
draw a nice dirgram of the holders tree
279
# spacer styles based on output of 'tree --charset=ascii'
280
spacers = (('`-- ', ' ' * 4), ('|-- ', '|' + ' ' * 3))
282
def format_tree(tree):
284
format entry and any subentries
286
result = [tree['name']]
287
holders = tree['holders']
288
for (holder_no, holder) in enumerate(holders):
289
spacer_style = spacers[min(len(holders) - (holder_no + 1), 1)]
290
subtree_lines = format_tree(holder)
291
for (line_no, line) in enumerate(subtree_lines):
292
result.append(spacer_style[min(line_no, 1)] + line)
295
return '\n'.join(format_tree(holders_tree))
298
def get_holder_types(tree):
300
get flattened list of types of holders in holders tree and the devices
303
types = {(tree['dev_type'], tree['device'])}
304
for holder in tree['holders']:
305
types.update(get_holder_types(holder))
309
def assert_clear(base_paths):
311
Check if all paths in base_paths are clear to use
313
valid = ('disk', 'partition')
314
if not isinstance(base_paths, (list, tuple)):
315
base_paths = [base_paths]
316
base_paths = [block.sys_block_path(path) for path in base_paths]
317
for holders_tree in [gen_holders_tree(p) for p in base_paths]:
318
if any(holder_type not in valid and path not in base_paths
319
for (holder_type, path) in get_holder_types(holders_tree)):
320
raise OSError('Storage not clear, remaining:\n{}'
321
.format(format_holders_tree(holders_tree)))
324
def clear_holders(base_paths, try_preserve=False):
326
Clear all storage layers depending on the devices specified in 'base_paths'
327
A single device or list of devices can be specified.
328
Device paths can be specified either as paths in /dev or /sys/block
329
Will throw OSError if any holders could not be shut down
332
if not isinstance(base_paths, (list, tuple)):
333
base_paths = [base_paths]
335
# get current holders and plan how to shut them down
336
holder_trees = [gen_holders_tree(path) for path in base_paths]
337
LOG.info('Current device storage tree:\n%s',
338
'\n'.join(format_holders_tree(tree) for tree in holder_trees))
339
ordered_devs = plan_shutdown_holder_trees(holder_trees)
341
# run shutdown functions
342
for dev_info in ordered_devs:
343
dev_type = DEV_TYPES.get(dev_info['dev_type'])
344
shutdown_function = dev_type.get('shutdown')
345
if not shutdown_function:
347
if try_preserve and shutdown_function in DATA_DESTROYING_HANDLERS:
348
LOG.info('shutdown function for holder type: %s is destructive. '
349
'attempting to preserve data, so not skipping' %
350
dev_info['dev_type'])
352
LOG.info("shutdown running on holder type: '%s' syspath: '%s'",
353
dev_info['dev_type'], dev_info['device'])
354
shutdown_function(dev_info['device'])
355
udev.udevadm_settle()
358
def start_clear_holders_deps():
360
prepare system for clear holders to be able to scan old devices
362
# a mdadm scan has to be started in case there is a md device that needs to
363
# be detected. if the scan fails, it is either because there are no mdadm
364
# devices on the system, or because there is a mdadm device in a damaged
365
# state that could not be started. due to the nature of mdadm tools, it is
366
# difficult to know which is the case. if any errors did occur, then ignore
367
# them, since no action needs to be taken if there were no mdadm devices on
368
# the system, and in the case where there is some mdadm metadata on a disk,
369
# but there was not enough to start the array, the call to wipe_volume on
370
# all disks and partitions should be sufficient to remove the mdadm
372
block.mdadm.mdadm_assemble(scan=True, ignore_errors=True)
373
# the bcache module needs to be present to properly detect bcache devs
374
# on some systems (precise without hwe kernel) it may not be possible to
375
# lad the bcache module bcause it is not present in the kernel. if this
376
# happens then there is no need to halt installation, as the bcache devices
377
# will never appear and will never prevent the disk from being reformatted
378
util.subp(['modprobe', 'bcache'], rcs=[0, 1])
381
# anything that is not identified can assumed to be a 'disk' or similar
382
DEFAULT_DEV_TYPE = 'disk'
383
# handlers that should not be run if an attempt is being made to preserve data
384
DATA_DESTROYING_HANDLERS = [wipe_superblock]
385
# types of devices that could be encountered by clear holders and functions to
386
# identify them and shut them down
387
DEV_TYPES = _define_handlers_registry()