~smoser/curtin/yakkety.lp1666986

« back to all changes in this revision

Viewing changes to curtin/block/clear_holders.py

  • Committer: Ryan Harper
  • Date: 2016-09-29 18:36:01 UTC
  • mfrom: (1.1.41)
  • mto: This revision was merged to the branch mainline in revision 59.
  • Revision ID: ryan.harper@canonical.com-20160929183601-utf54hgpsizdfzxp
* New upstream snapshot.
  - unittest,tox.ini: catch and fix issue with trusty-level mock of open 
  - block/mdadm: add option to ignore mdadm_assemble errors  (LP: #1618429)
  - curtin/doc: overhaul curtin documentation for readthedocs.org  (LP: #1351085)
  - curtin.util: re-add support for RunInChroot  (LP: #1617375)
  - curtin/net: overhaul of eni rendering to handle mixed ipv4/ipv6 configs 
  - curtin.block: refactor clear_holders logic into block.clear_holders and cli cmd 
  - curtin.apply_net should exit non-zero upon exception.  (LP: #1615780)
  - apt: fix bug in disable_suites if sources.list line is blank. 
  - vmtests: disable Wily in vmtests 
  - Fix the unittests for test_apt_source. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#   Copyright (C) 2016 Canonical Ltd.
 
2
#
 
3
#   Author: Wesley Wiedenmeier <wesley.wiedenmeier@canonical.com>
 
4
#
 
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.
 
9
#
 
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
 
13
#   more details.
 
14
#
 
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/>.
 
17
 
 
18
"""
 
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
 
22
"""
 
23
 
 
24
import os
 
25
 
 
26
from curtin import (block, udev, util)
 
27
from curtin.block import lvm
 
28
from curtin.log import LOG
 
29
 
 
30
 
 
31
def _define_handlers_registry():
 
32
    """
 
33
    returns instantiated dev_types
 
34
    """
 
35
    return {
 
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},
 
43
    }
 
44
 
 
45
 
 
46
def get_dmsetup_uuid(device):
 
47
    """
 
48
    get the dm uuid for a specified dmsetup device
 
49
    """
 
50
    blockdev = block.sysfs_to_devpath(device)
 
51
    (out, _) = util.subp(['dmsetup', 'info', blockdev, '-C', '-o', 'uuid',
 
52
                          '--noheadings'], capture=True)
 
53
    return out.strip()
 
54
 
 
55
 
 
56
def get_bcache_using_dev(device):
 
57
    """
 
58
    Get the /sys/fs/bcache/ path of the bcache volume using specified device
 
59
    """
 
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'))
 
63
 
 
64
 
 
65
def shutdown_bcache(device):
 
66
    """
 
67
    Shut down bcache for specified bcache device
 
68
    """
 
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)
 
75
        return
 
76
 
 
77
    bcache_sysfs = get_bcache_using_dev(device)
 
78
    if not os.path.exists(bcache_sysfs):
 
79
        LOG.info(bcache_shutdown_message)
 
80
        return
 
81
 
 
82
    LOG.debug('stopping bcache at: %s', bcache_sysfs)
 
83
    util.write_file(os.path.join(bcache_sysfs, 'stop'), '1', mode=None)
 
84
 
 
85
 
 
86
def shutdown_lvm(device):
 
87
    """
 
88
    Shutdown specified lvm device.
 
89
    """
 
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])
 
103
    # refresh lvmetad
 
104
    lvm.lvm_scan()
 
105
 
 
106
 
 
107
def shutdown_crypt(device):
 
108
    """
 
109
    Shutdown specified cryptsetup device
 
110
    """
 
111
    blockdev = block.sysfs_to_devpath(device)
 
112
    util.subp(['cryptsetup', 'remove', blockdev], capture=True)
 
113
 
 
114
 
 
115
def shutdown_mdadm(device):
 
116
    """
 
117
    Shutdown specified mdadm device.
 
118
    """
 
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)
 
123
 
 
124
 
 
125
def wipe_superblock(device):
 
126
    """
 
127
    Wrapper for block.wipe_volume compatible with shutdown function interface
 
128
    """
 
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'",
 
134
                 blockdev)
 
135
    else:
 
136
        LOG.info('wiping superblock on %s', blockdev)
 
137
        block.wipe_volume(blockdev, mode='superblock')
 
138
 
 
139
 
 
140
def identify_lvm(device):
 
141
    """
 
142
    determine if specified device is a lvm device
 
143
    """
 
144
    return (block.path_to_kname(device).startswith('dm') and
 
145
            get_dmsetup_uuid(device).startswith('LVM'))
 
146
 
 
147
 
 
148
def identify_crypt(device):
 
149
    """
 
150
    determine if specified device is dm-crypt device
 
151
    """
 
152
    return (block.path_to_kname(device).startswith('dm') and
 
153
            get_dmsetup_uuid(device).startswith('CRYPT'))
 
154
 
 
155
 
 
156
def identify_mdadm(device):
 
157
    """
 
158
    determine if specified device is a mdadm device
 
159
    """
 
160
    return block.path_to_kname(device).startswith('md')
 
161
 
 
162
 
 
163
def identify_bcache(device):
 
164
    """
 
165
    determine if specified device is a bcache device
 
166
    """
 
167
    return block.path_to_kname(device).startswith('bcache')
 
168
 
 
169
 
 
170
def identify_partition(device):
 
171
    """
 
172
    determine if specified device is a partition
 
173
    """
 
174
    path = os.path.join(block.sys_block_path(device), 'partition')
 
175
    return os.path.exists(path)
 
176
 
 
177
 
 
178
def get_holders(device):
 
179
    """
 
180
    Look up any block device holders, return list of knames
 
181
    """
 
182
    # block.sys_block_path works when given a /sys or /dev path
 
183
    sysfs_path = block.sys_block_path(device)
 
184
    # get holders
 
185
    holders = os.listdir(os.path.join(sysfs_path, 'holders'))
 
186
    LOG.debug("devname '%s' had holders: %s", device, holders)
 
187
    return holders
 
188
 
 
189
 
 
190
def gen_holders_tree(device):
 
191
    """
 
192
    generate a tree representing the current storage hirearchy above 'device'
 
193
    """
 
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
 
199
    # disk's partitions
 
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)),
 
213
                    DEFAULT_DEV_TYPE)
 
214
    return {
 
215
        'device': device, 'dev_type': dev_type, 'name': dev_name,
 
216
        'holders': [gen_holders_tree(h) for h in holder_paths],
 
217
    }
 
218
 
 
219
 
 
220
def plan_shutdown_holder_trees(holders_trees):
 
221
    """
 
222
    plan best order to shut down holders in, taking into account high level
 
223
    storage layers that may have many devices below them
 
224
 
 
225
    returns a sorted list of descriptions of storage config entries including
 
226
    their path in /sys/block and their dev type
 
227
 
 
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
 
230
    starting from disk)
 
231
    """
 
232
    # holds a temporary registry of holders to allow cross references
 
233
    # key = device sysfs path, value = {} of priority level, shutdown function
 
234
    reg = {}
 
235
 
 
236
    # normalize to list of trees
 
237
    if not isinstance(holders_trees, (list, tuple)):
 
238
        holders_trees = [holders_trees]
 
239
 
 
240
    def flatten_holders_tree(tree, level=0):
 
241
        """
 
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
 
244
        """
 
245
        device = tree['device']
 
246
 
 
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
 
257
        if device in reg:
 
258
            level = max(reg[device]['level'], level)
 
259
 
 
260
        reg[device] = {'level': level, 'device': device,
 
261
                       'dev_type': tree['dev_type']}
 
262
 
 
263
        # handle holders above this level
 
264
        for holder in tree['holders']:
 
265
            flatten_holders_tree(holder, level=level + 1)
 
266
 
 
267
    # flatten the holders tree into the registry
 
268
    for holders_tree in holders_trees:
 
269
        flatten_holders_tree(holders_tree)
 
270
 
 
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)]
 
273
 
 
274
 
 
275
def format_holders_tree(holders_tree):
 
276
    """
 
277
    draw a nice dirgram of the holders tree
 
278
    """
 
279
    # spacer styles based on output of 'tree --charset=ascii'
 
280
    spacers = (('`-- ', ' ' * 4), ('|-- ', '|' + ' ' * 3))
 
281
 
 
282
    def format_tree(tree):
 
283
        """
 
284
        format entry and any subentries
 
285
        """
 
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)
 
293
        return result
 
294
 
 
295
    return '\n'.join(format_tree(holders_tree))
 
296
 
 
297
 
 
298
def get_holder_types(tree):
 
299
    """
 
300
    get flattened list of types of holders in holders tree and the devices
 
301
    they correspond to
 
302
    """
 
303
    types = {(tree['dev_type'], tree['device'])}
 
304
    for holder in tree['holders']:
 
305
        types.update(get_holder_types(holder))
 
306
    return types
 
307
 
 
308
 
 
309
def assert_clear(base_paths):
 
310
    """
 
311
    Check if all paths in base_paths are clear to use
 
312
    """
 
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)))
 
322
 
 
323
 
 
324
def clear_holders(base_paths, try_preserve=False):
 
325
    """
 
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
 
330
    """
 
331
    # handle single path
 
332
    if not isinstance(base_paths, (list, tuple)):
 
333
        base_paths = [base_paths]
 
334
 
 
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)
 
340
 
 
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:
 
346
            continue
 
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'])
 
351
            continue
 
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()
 
356
 
 
357
 
 
358
def start_clear_holders_deps():
 
359
    """
 
360
    prepare system for clear holders to be able to scan old devices
 
361
    """
 
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
 
371
    # metadata
 
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])
 
379
 
 
380
 
 
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()