2015-08-10 13:42:19 +00:00
|
|
|
# Copyright 2015 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
2016-01-04 13:26:22 +00:00
|
|
|
import os
|
2015-10-05 15:56:59 +00:00
|
|
|
import re
|
2015-08-10 13:42:19 +00:00
|
|
|
|
2015-12-01 19:30:53 +00:00
|
|
|
from .blockdev import (Bcachedev,
|
|
|
|
Blockdev,
|
|
|
|
LVMDev,
|
|
|
|
Raiddev,
|
|
|
|
sort_actions)
|
2015-08-10 13:42:19 +00:00
|
|
|
import math
|
2016-06-30 18:17:01 +00:00
|
|
|
from subiquitycore.model import ModelPolicy
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
|
2015-08-28 20:30:34 +00:00
|
|
|
HUMAN_UNITS = ['B', 'K', 'M', 'G', 'T', 'P']
|
2016-06-30 18:17:01 +00:00
|
|
|
log = logging.getLogger('subiquitycore.models.filesystem')
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
class AttrDict(dict):
|
2015-10-02 00:03:54 +00:00
|
|
|
__getattr__ = dict.__getitem__
|
|
|
|
__setattr__ = dict.__setitem__
|
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
|
2015-08-10 13:42:19 +00:00
|
|
|
class FilesystemModel(ModelPolicy):
|
|
|
|
""" Model representing storage options
|
|
|
|
"""
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal = 'menu:filesystem:main'
|
2015-08-10 13:42:19 +00:00
|
|
|
signals = [
|
|
|
|
('Filesystem view',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal,
|
2015-08-10 13:42:19 +00:00
|
|
|
'filesystem'),
|
2015-10-22 21:12:59 +00:00
|
|
|
('Filesystem error',
|
|
|
|
'filesystem:error',
|
|
|
|
'filesystem_error'),
|
2015-08-10 13:42:19 +00:00
|
|
|
('Filesystem finish',
|
|
|
|
'filesystem:finish',
|
|
|
|
'filesystem_handler'),
|
|
|
|
('Show disk partition view',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal + ':show-disk-partition',
|
2015-08-10 13:42:19 +00:00
|
|
|
'disk_partition'),
|
|
|
|
('Finish disk partition',
|
|
|
|
'filesystem:finish-disk-partition',
|
|
|
|
'disk_partition_handler'),
|
|
|
|
('Add disk partition',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal + ':add-disk-partition',
|
2015-08-10 13:42:19 +00:00
|
|
|
'add_disk_partition'),
|
|
|
|
('Finish add disk partition',
|
|
|
|
'filesystem:finish-add-disk-partition',
|
|
|
|
'add_disk_partition_handler'),
|
2015-10-26 14:57:19 +00:00
|
|
|
('Finish whole disk format/mount',
|
|
|
|
'filesystem:finish-add-disk-format',
|
|
|
|
'add_disk_format_handler'),
|
2015-08-10 13:42:19 +00:00
|
|
|
('Format or create swap on entire device (unusual, advanced)',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal + ':create-swap-entire-device',
|
2015-09-02 19:21:14 +00:00
|
|
|
'create_swap_entire_device'),
|
|
|
|
('Show disk information',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal + ':show-disk-information',
|
2015-10-02 00:03:54 +00:00
|
|
|
'show_disk_information'),
|
2015-10-21 21:25:44 +00:00
|
|
|
('Show next disk information',
|
|
|
|
'filesystem:show-disk-info-next',
|
|
|
|
'show_disk_information_next'),
|
|
|
|
('Show prev disk information',
|
|
|
|
'filesystem:show-disk-info-prev',
|
|
|
|
'show_disk_information_prev'),
|
2015-10-02 00:03:54 +00:00
|
|
|
('Add Raid Device',
|
2015-11-11 04:32:59 +00:00
|
|
|
'filesystem:add-raid-dev',
|
2015-10-02 00:03:54 +00:00
|
|
|
'add_raid_dev'),
|
2015-08-10 13:42:19 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
fs_menu = [
|
2015-09-24 12:53:15 +00:00
|
|
|
# ('Connect iSCSI network disk',
|
|
|
|
# 'filesystem:connect-iscsi-disk',
|
|
|
|
# 'connect_iscsi_disk'),
|
|
|
|
# ('Connect Ceph network disk',
|
|
|
|
# 'filesystem:connect-ceph-disk',
|
|
|
|
# 'connect_ceph_disk'),
|
2015-12-01 19:30:53 +00:00
|
|
|
('Create volume group (LVM2)',
|
|
|
|
base_signal + ':create-volume-group',
|
|
|
|
'create_volume_group'),
|
2015-10-02 00:03:54 +00:00
|
|
|
('Create software RAID (MD)',
|
2015-10-29 20:58:51 +00:00
|
|
|
base_signal + ':create-raid',
|
2015-10-02 00:03:54 +00:00
|
|
|
'create_raid'),
|
2015-11-13 16:27:57 +00:00
|
|
|
('Setup hierarchichal storage (bcache)',
|
|
|
|
base_signal + ':setup-bcache',
|
|
|
|
'create_bcache'),
|
2015-08-10 13:42:19 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
supported_filesystems = [
|
|
|
|
'ext4',
|
|
|
|
'xfs',
|
|
|
|
'btrfs',
|
|
|
|
'swap',
|
|
|
|
'bcache cache',
|
|
|
|
'bcache store',
|
|
|
|
'leave unformatted'
|
|
|
|
]
|
|
|
|
|
2015-10-05 20:09:47 +00:00
|
|
|
partition_flags = [
|
|
|
|
'boot',
|
|
|
|
'lvm',
|
|
|
|
'raid',
|
|
|
|
'bios_grub',
|
|
|
|
]
|
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
# TODO: what is "linear" level?
|
|
|
|
raid_levels = [
|
|
|
|
"0",
|
|
|
|
"1",
|
|
|
|
"5",
|
|
|
|
"6",
|
|
|
|
"10",
|
|
|
|
]
|
|
|
|
|
2015-11-20 00:31:30 +00:00
|
|
|
# th following blocktypes cannot be partitioned
|
|
|
|
no_partition_blocktypes = [
|
|
|
|
"bcache",
|
|
|
|
"lvm_partition",
|
|
|
|
"lvm_volgroup",
|
|
|
|
"raid",
|
|
|
|
]
|
|
|
|
|
2015-09-28 16:28:26 +00:00
|
|
|
def __init__(self, prober, opts):
|
|
|
|
self.opts = opts
|
2015-08-28 18:19:13 +00:00
|
|
|
self.prober = prober
|
2015-08-10 13:42:19 +00:00
|
|
|
self.info = {}
|
|
|
|
self.devices = {}
|
2015-10-02 00:03:54 +00:00
|
|
|
self.raid_devices = {}
|
2015-11-13 16:27:57 +00:00
|
|
|
self.bcache_devices = {}
|
2015-12-01 19:30:53 +00:00
|
|
|
self.lvm_devices = {}
|
2015-08-28 18:19:13 +00:00
|
|
|
self.storage = {}
|
2015-12-02 15:40:17 +00:00
|
|
|
self.holders = {}
|
|
|
|
self.tags = {}
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
log.debug('FilesystemModel: resetting disks')
|
2015-10-05 15:56:59 +00:00
|
|
|
self.devices = {}
|
2015-11-02 21:19:50 +00:00
|
|
|
self.info = {}
|
2015-11-13 16:27:57 +00:00
|
|
|
self.raid_devices = {}
|
|
|
|
self.bcache_devices = {}
|
2015-12-01 19:30:53 +00:00
|
|
|
self.lvm_devices = {}
|
2015-12-02 15:40:17 +00:00
|
|
|
self.holders = {}
|
|
|
|
self.tags = {}
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
def get_signal_by_name(self, selection):
|
|
|
|
for x, y, z in self.get_signals():
|
|
|
|
if x == selection:
|
|
|
|
return y
|
|
|
|
|
|
|
|
def get_signals(self):
|
|
|
|
return self.signals + self.fs_menu
|
|
|
|
|
|
|
|
def get_menu(self):
|
|
|
|
return self.fs_menu
|
|
|
|
|
|
|
|
def probe_storage(self):
|
2015-08-28 18:19:13 +00:00
|
|
|
log.debug('model.probe_storage: probing storage')
|
|
|
|
self.storage = self.prober.get_storage()
|
|
|
|
log.debug('got storage:\n{}'.format(self.storage))
|
2015-08-10 13:42:19 +00:00
|
|
|
# TODO: Put this into a logging namespace for probert
|
|
|
|
# since its quite a bit of log information.
|
|
|
|
# log.debug('storage probe data:\n{}'.format(
|
|
|
|
# json.dumps(self.storage, indent=4, sort_keys=True)))
|
|
|
|
|
|
|
|
# TODO: replace this with Storage.get_device_by_match()
|
|
|
|
# which takes a lambda fn for matching
|
|
|
|
VALID_MAJORS = ['8', '253']
|
|
|
|
for disk in self.storage.keys():
|
|
|
|
if self.storage[disk]['DEVTYPE'] == 'disk' and \
|
|
|
|
self.storage[disk]['MAJOR'] in VALID_MAJORS:
|
|
|
|
log.debug('disk={}\n{}'.format(disk,
|
|
|
|
json.dumps(self.storage[disk], indent=4,
|
|
|
|
sort_keys=True)))
|
2015-08-28 18:19:13 +00:00
|
|
|
self.info[disk] = self.prober.get_storage_info(disk)
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
def get_disk(self, disk):
|
2015-10-05 15:56:59 +00:00
|
|
|
'''get disk object given path. If provided a partition, then
|
|
|
|
return the parent disk. /dev/sda2 --> /dev/sda obj'''
|
2015-10-02 18:49:10 +00:00
|
|
|
log.debug('probe_storage: get_disk({})'.format(disk))
|
2015-10-05 15:56:59 +00:00
|
|
|
|
2016-01-04 13:26:22 +00:00
|
|
|
if not disk.startswith('/dev/'):
|
|
|
|
disk = os.path.join('/dev', disk)
|
|
|
|
|
2015-08-10 13:42:19 +00:00
|
|
|
if disk not in self.devices:
|
2015-10-05 15:56:59 +00:00
|
|
|
try:
|
|
|
|
self.devices[disk] = Blockdev(disk, self.info[disk].serial,
|
|
|
|
self.info[disk].model,
|
|
|
|
size=self.info[disk].size)
|
|
|
|
except KeyError:
|
|
|
|
''' if it looks like a partition, try again with
|
|
|
|
parent device '''
|
|
|
|
if disk[-1].isdigit():
|
|
|
|
return self.get_disk(re.split('[\d+]', disk)[0])
|
|
|
|
|
2015-08-10 13:42:19 +00:00
|
|
|
return self.devices[disk]
|
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
def get_available_disks(self):
|
|
|
|
''' currently only returns available disks '''
|
2015-12-02 15:40:17 +00:00
|
|
|
disks = [d for d in self.get_all_disks()
|
|
|
|
if (d.available and
|
|
|
|
len(self.get_holders(d.devpath)) == 0)]
|
2015-10-02 18:49:10 +00:00
|
|
|
log.debug('get_available_disks -> {}'.format(
|
|
|
|
",".join([d.devpath for d in disks])))
|
|
|
|
return disks
|
|
|
|
|
|
|
|
def get_all_disks(self):
|
2015-10-02 00:03:54 +00:00
|
|
|
possible_devices = list(set(list(self.devices.keys()) +
|
|
|
|
list(self.info.keys())))
|
|
|
|
possible_disks = [self.get_disk(d) for d in sorted(possible_devices)]
|
2015-10-02 18:49:10 +00:00
|
|
|
log.debug('get_all_disks -> {}'.format(",".join([d.devpath for d in
|
|
|
|
possible_disks])))
|
|
|
|
return possible_disks
|
2015-10-02 00:03:54 +00:00
|
|
|
|
|
|
|
def calculate_raid_size(self, raid_level, raid_devices, spare_devices):
|
|
|
|
'''
|
|
|
|
0: array size is the size of the smallest component partition times
|
|
|
|
the number of component partitions
|
|
|
|
1: array size is the size of the smallest component partition
|
|
|
|
5: array size is the size of the smallest component partition times
|
|
|
|
the number of component partitions munus 1
|
|
|
|
6: array size is the size of the smallest component partition times
|
|
|
|
the number of component partitions munus 2
|
|
|
|
'''
|
|
|
|
# https://raid.wiki.kernel.org/ \
|
|
|
|
# index.php/RAID_superblock_formats#Total_Size_of_superblock
|
|
|
|
# Version-1 superblock format on-disk layout:
|
|
|
|
# Total size of superblock: 256 Bytes plus 2 bytes per device in the
|
|
|
|
# array
|
|
|
|
log.debug('calc_raid_size: level={} rd={} sd={}'.format(raid_level,
|
|
|
|
raid_devices,
|
|
|
|
spare_devices))
|
|
|
|
overhead_bytes = 256 + (2 * (len(raid_devices) + len(spare_devices)))
|
|
|
|
log.debug('calc_raid_size: overhead_bytes={}'.format(overhead_bytes))
|
|
|
|
|
|
|
|
# find the smallest device
|
|
|
|
min_dev_size = min([d.size for d in raid_devices])
|
|
|
|
log.debug('calc_raid_size: min_dev_size={}'.format(min_dev_size))
|
|
|
|
|
|
|
|
if raid_level == 0:
|
|
|
|
array_size = min_dev_size * len(raid_devices)
|
|
|
|
elif raid_level == 1:
|
|
|
|
array_size = min_dev_size
|
|
|
|
elif raid_level == 5:
|
|
|
|
array_size = min_dev_size * (len(raid_devices) - 1)
|
|
|
|
elif raid_level == 10:
|
|
|
|
array_size = min_dev_size * int((len(raid_devices) /
|
|
|
|
len(spare_devices)))
|
|
|
|
total_size = array_size - overhead_bytes
|
|
|
|
log.debug('calc_raid_size: array_size:{} - overhead:{} = {}'.format(
|
|
|
|
array_size, overhead_bytes, total_size))
|
|
|
|
return total_size
|
|
|
|
|
|
|
|
def add_raid_device(self, raidspec):
|
|
|
|
# assume raidspec has already been valided in view/controller
|
|
|
|
log.debug('Attempting to create a raid device')
|
|
|
|
'''
|
|
|
|
raidspec = {
|
|
|
|
'devices': ['/dev/sdb 1.819T, HDS5C3020ALA632',
|
|
|
|
'/dev/sdc 1.819T, 001-9YN164',
|
|
|
|
'/dev/sdf 1.819T, 001-9YN164',
|
|
|
|
'/dev/sdg 1.819T, 001-9YN164',
|
|
|
|
'/dev/sdh 1.819T, HDS5C3020ALA632',
|
|
|
|
'/dev/sdi 1.819T, 001-9YN164'],
|
2015-10-26 14:57:19 +00:00
|
|
|
'/dev/sdj 1.819T, Unknown Model'],
|
2015-10-02 00:03:54 +00:00
|
|
|
'raid_level': '0',
|
|
|
|
'hot_spares': '0',
|
|
|
|
'chunk_size': '4K',
|
|
|
|
}
|
2015-10-05 15:56:59 +00:00
|
|
|
could be /dev/sda1, /dev/md0, /dev/bcache1, /dev/vg_foo/foobar2?
|
2015-10-02 00:03:54 +00:00
|
|
|
'''
|
|
|
|
raid_devices = []
|
|
|
|
spare_devices = []
|
|
|
|
all_devices = [r.split() for r in raidspec.get('devices', [])]
|
|
|
|
nr_spares = int(raidspec.get('hot_spares'))
|
|
|
|
|
|
|
|
# XXX: curtin requires a partition table on the base devices
|
|
|
|
# and then one partition of type raid
|
2015-10-26 14:57:19 +00:00
|
|
|
for (devpath, *_) in all_devices:
|
2015-10-02 00:03:54 +00:00
|
|
|
disk = self.get_disk(devpath)
|
2015-10-05 15:56:59 +00:00
|
|
|
|
|
|
|
# add or update a partition to be raid type
|
|
|
|
if disk.path != devpath: # we must have got a partition
|
|
|
|
raiddev = disk.get_partition(devpath)
|
|
|
|
raiddev.flags = 'raid'
|
|
|
|
else:
|
|
|
|
disk.add_partition(1, disk.freespace, None, None, flag='raid')
|
|
|
|
raiddev = disk
|
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
if len(raid_devices) + nr_spares < len(all_devices):
|
2015-10-05 15:56:59 +00:00
|
|
|
raid_devices.append(raiddev)
|
2015-10-02 00:03:54 +00:00
|
|
|
else:
|
2015-10-05 15:56:59 +00:00
|
|
|
spare_devices.append(raiddev)
|
2015-10-02 00:03:54 +00:00
|
|
|
|
|
|
|
# auto increment md number based in registered devices
|
2015-11-17 22:12:45 +00:00
|
|
|
raid_shortname = 'md{}'.format(len(self.raid_devices))
|
|
|
|
raid_dev_name = '/dev/' + raid_shortname
|
2015-10-02 00:03:54 +00:00
|
|
|
raid_serial = '{}_serial'.format(raid_dev_name)
|
|
|
|
raid_model = '{}_model'.format(raid_dev_name)
|
|
|
|
raid_parttype = 'gpt'
|
|
|
|
raid_level = int(raidspec.get('raid_level'))
|
|
|
|
raid_size = self.calculate_raid_size(raid_level, raid_devices,
|
|
|
|
spare_devices)
|
2015-10-05 15:56:59 +00:00
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
# create a Raiddev (pass in only the names)
|
2015-10-26 14:57:19 +00:00
|
|
|
raid_parts = []
|
|
|
|
for dev in raid_devices:
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_holder(dev.devpath, raid_dev_name)
|
|
|
|
self.set_tag(dev.devpath, 'member of MD ' + raid_shortname)
|
2015-10-26 14:57:19 +00:00
|
|
|
for num, action in dev.partitions.items():
|
|
|
|
raid_parts.append(action.action_id)
|
|
|
|
spare_parts = []
|
|
|
|
for dev in spare_devices:
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_holder(dev.devpath, raid_dev_name)
|
|
|
|
self.set_tag(dev.devpath, 'member of MD ' + raid_shortname)
|
2015-10-26 14:57:19 +00:00
|
|
|
for num, action in dev.partitions.items():
|
|
|
|
spare_parts.append(action.action_id)
|
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
raid_dev = Raiddev(raid_dev_name, raid_serial, raid_model,
|
|
|
|
raid_parttype, raid_size,
|
2015-10-26 14:57:19 +00:00
|
|
|
raid_parts,
|
2015-10-02 00:03:54 +00:00
|
|
|
raid_level,
|
2015-10-26 14:57:19 +00:00
|
|
|
spare_parts)
|
2015-10-02 00:03:54 +00:00
|
|
|
|
|
|
|
# add it to the model's info dict
|
|
|
|
raid_dev_info = {
|
|
|
|
'type': 'disk',
|
|
|
|
'name': raid_dev_name,
|
|
|
|
'size': raid_size,
|
|
|
|
'serial': raid_serial,
|
|
|
|
'vendor': 'Linux Software RAID',
|
|
|
|
'model': raid_model,
|
|
|
|
'is_virtual': True,
|
2015-11-02 19:39:09 +00:00
|
|
|
'raw': {
|
|
|
|
'MAJOR': '9',
|
|
|
|
},
|
2015-10-02 00:03:54 +00:00
|
|
|
}
|
|
|
|
self.info[raid_dev_name] = AttrDict(raid_dev_info)
|
|
|
|
|
|
|
|
# add it to the model's raid devices
|
|
|
|
self.raid_devices[raid_dev_name] = raid_dev
|
|
|
|
# add it to the model's devices
|
|
|
|
self.add_device(raid_dev_name, raid_dev)
|
|
|
|
|
|
|
|
log.debug('Successfully added raid_dev: {}'.format(raid_dev))
|
|
|
|
|
2015-12-01 19:30:53 +00:00
|
|
|
def add_lvm_volgroup(self, lvmspec):
|
|
|
|
log.debug('Attempting to create an LVM volgroup device')
|
|
|
|
'''
|
|
|
|
lvm_volgroup_spec = {
|
|
|
|
'volgroup': 'my_volgroup_name',
|
|
|
|
'devices': ['/dev/sdb 1.819T, HDS5C3020ALA632']
|
|
|
|
}
|
|
|
|
'''
|
2015-12-02 15:40:17 +00:00
|
|
|
lvm_shortname = lvmspec.get('volgroup')
|
|
|
|
lvm_dev_name = '/dev/' + lvm_shortname
|
|
|
|
lvm_serial = '{}_serial'.format(lvm_dev_name)
|
|
|
|
lvm_model = '{}_model'.format(lvm_dev_name)
|
|
|
|
lvm_parttype = 'gpt'
|
2015-12-01 19:30:53 +00:00
|
|
|
lvm_devices = []
|
2015-12-02 15:40:17 +00:00
|
|
|
|
2015-12-01 19:30:53 +00:00
|
|
|
# extract just the device name for disks in this volgroup
|
|
|
|
all_devices = [r.split() for r in lvmspec.get('devices', [])]
|
|
|
|
|
|
|
|
# XXX: curtin requires a partition table on the base devices
|
|
|
|
# and then one partition of type lvm
|
|
|
|
for (devpath, *_) in all_devices:
|
|
|
|
disk = self.get_disk(devpath)
|
|
|
|
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_holder(devpath, lvm_dev_name)
|
|
|
|
self.set_tag(devpath, 'member of LVM ' + lvm_shortname)
|
|
|
|
|
2015-12-01 19:30:53 +00:00
|
|
|
# add or update a partition to be raid type
|
|
|
|
if disk.path != devpath: # we must have got a partition
|
|
|
|
pv_dev = disk.get_partition(devpath)
|
|
|
|
pv_dev.flags = 'lvm'
|
|
|
|
else:
|
|
|
|
disk.add_partition(1, disk.freespace, None, None, flag='lvm')
|
|
|
|
pv_dev = disk
|
|
|
|
|
|
|
|
lvm_devices.append(pv_dev)
|
|
|
|
|
|
|
|
lvm_size = sum([pv.size for pv in lvm_devices])
|
2015-12-02 15:40:17 +00:00
|
|
|
lvm_device_names = [pv.id for pv in lvm_devices]
|
2015-12-01 19:30:53 +00:00
|
|
|
|
2015-12-02 15:40:17 +00:00
|
|
|
log.debug('lvm_devices: {}'.format(lvm_device_names))
|
2015-12-01 19:30:53 +00:00
|
|
|
lvm_dev = LVMDev(lvm_dev_name, lvm_serial, lvm_model,
|
|
|
|
lvm_parttype, lvm_size,
|
2015-12-02 15:40:17 +00:00
|
|
|
lvm_shortname, lvm_device_names)
|
2015-12-01 19:30:53 +00:00
|
|
|
log.debug('{} volgroup: {} devices: {}'.format(lvm_dev.id,
|
|
|
|
lvm_dev.volgroup,
|
|
|
|
lvm_dev.devices))
|
|
|
|
|
|
|
|
# add it to the model's info dict
|
|
|
|
lvm_dev_info = {
|
|
|
|
'type': 'disk',
|
|
|
|
'name': lvm_dev_name,
|
|
|
|
'size': lvm_size,
|
|
|
|
'serial': lvm_serial,
|
|
|
|
'vendor': 'Linux Volume Group (LVM2)',
|
|
|
|
'model': lvm_model,
|
|
|
|
'is_virtual': True,
|
|
|
|
'raw': {
|
|
|
|
'MAJOR': '9',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
self.info[lvm_dev_name] = AttrDict(lvm_dev_info)
|
|
|
|
|
|
|
|
# add it to the model's lvm devices
|
|
|
|
self.lvm_devices[lvm_dev_name] = lvm_dev
|
|
|
|
# add it to the model's devices
|
|
|
|
self.add_device(lvm_dev_name, lvm_dev)
|
|
|
|
|
|
|
|
log.debug('Successfully added lvm_dev: {}'.format(lvm_dev))
|
|
|
|
|
2015-11-13 16:27:57 +00:00
|
|
|
def add_bcache_device(self, bcachespec):
|
|
|
|
# assume bcachespec has already been valided in view/controller
|
|
|
|
log.debug('Attempting to create a bcache device')
|
|
|
|
'''
|
|
|
|
bcachespec = {
|
|
|
|
'backing_device': '/dev/sdc 1.819T, 001-9YN164',
|
2015-11-13 16:59:38 +00:00
|
|
|
'cache_device': '/dev/sdb 1.819T, HDS5C3020ALA632',
|
2015-11-13 16:27:57 +00:00
|
|
|
}
|
|
|
|
could be /dev/sda1, /dev/md0, /dev/vg_foo/foobar2?
|
|
|
|
'''
|
|
|
|
backing_device = self.get_disk(bcachespec['backing_device'].split()[0])
|
2015-11-13 16:59:38 +00:00
|
|
|
cache_device = self.get_disk(bcachespec['cache_device'].split()[0])
|
2015-11-13 16:27:57 +00:00
|
|
|
|
|
|
|
# auto increment md number based in registered devices
|
2015-11-17 22:12:45 +00:00
|
|
|
bcache_shortname = 'bcache{}'.format(len(self.bcache_devices))
|
|
|
|
bcache_dev_name = '/dev/' + bcache_shortname
|
2015-11-13 16:27:57 +00:00
|
|
|
bcache_serial = '{}_serial'.format(bcache_dev_name)
|
|
|
|
bcache_model = '{}_model'.format(bcache_dev_name)
|
|
|
|
bcache_parttype = 'gpt'
|
|
|
|
bcache_size = backing_device.size
|
|
|
|
|
|
|
|
# create a Bcachedev (pass in only the names)
|
|
|
|
bcache_dev = Bcachedev(bcache_dev_name, bcache_serial, bcache_model,
|
|
|
|
bcache_parttype, bcache_size,
|
2016-01-04 13:26:22 +00:00
|
|
|
backing_device, cache_device)
|
2015-11-13 19:42:36 +00:00
|
|
|
|
|
|
|
# mark bcache holders
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_holder(backing_device.devpath, bcache_dev_name)
|
|
|
|
self.set_holder(cache_device.devpath, bcache_dev_name)
|
2015-11-13 16:27:57 +00:00
|
|
|
|
2015-11-17 22:12:45 +00:00
|
|
|
# tag device use
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_tag(backing_device.devpath,
|
|
|
|
'backing store for ' + bcache_shortname)
|
|
|
|
cache_tag = self.get_tag(cache_device.devpath)
|
2015-11-17 22:12:45 +00:00
|
|
|
if len(cache_tag) > 0:
|
|
|
|
cache_tag += ", " + bcache_shortname
|
|
|
|
else:
|
|
|
|
cache_tag = "cache for " + bcache_shortname
|
2015-12-02 15:40:17 +00:00
|
|
|
self.set_tag(cache_device.devpath, cache_tag)
|
2015-11-17 22:12:45 +00:00
|
|
|
|
2015-11-13 16:27:57 +00:00
|
|
|
# add it to the model's info dict
|
|
|
|
bcache_dev_info = {
|
|
|
|
'type': 'disk',
|
|
|
|
'name': bcache_dev_name,
|
|
|
|
'size': bcache_size,
|
|
|
|
'serial': bcache_serial,
|
|
|
|
'vendor': 'Linux bcache',
|
|
|
|
'model': bcache_model,
|
|
|
|
'is_virtual': True,
|
|
|
|
'raw': {
|
|
|
|
'MAJOR': '9',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
self.info[bcache_dev_name] = AttrDict(bcache_dev_info)
|
|
|
|
|
|
|
|
# add it to the model's bcache devices
|
|
|
|
self.bcache_devices[bcache_dev_name] = bcache_dev
|
|
|
|
# add it to the model's devices
|
|
|
|
self.add_device(bcache_dev_name, bcache_dev)
|
|
|
|
|
|
|
|
log.debug('Successfully added bcache_dev: {}'.format(bcache_dev))
|
|
|
|
|
2015-11-17 21:02:23 +00:00
|
|
|
def get_bcache_cachedevs(self):
|
|
|
|
''' return uniq list of bcache cache devices '''
|
|
|
|
cachedevs = list(set([bcache_dev.cache_device for bcache_dev in
|
2015-11-17 22:12:45 +00:00
|
|
|
self.bcache_devices.values()]))
|
2015-11-17 21:02:23 +00:00
|
|
|
log.debug('bcache cache devs: {}'.format(cachedevs))
|
|
|
|
return cachedevs
|
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
def add_device(self, devpath, device):
|
|
|
|
log.debug("adding device: {} = {}".format(devpath, device))
|
|
|
|
self.devices[devpath] = device
|
2015-09-28 16:28:26 +00:00
|
|
|
|
2015-08-10 13:42:19 +00:00
|
|
|
def get_partitions(self):
|
2015-08-28 18:19:13 +00:00
|
|
|
log.debug('probe_storage: get_partitions()')
|
2015-08-10 13:42:19 +00:00
|
|
|
partitions = []
|
|
|
|
for dev in self.devices.values():
|
2015-10-07 20:24:19 +00:00
|
|
|
partnames = [part.devpath for (num, part) in
|
2015-08-27 19:26:37 +00:00
|
|
|
dev.disk.partitions.items()]
|
2015-08-10 13:42:19 +00:00
|
|
|
partitions += partnames
|
|
|
|
|
2015-10-07 21:34:57 +00:00
|
|
|
partitions = sorted(partitions)
|
2015-09-01 16:28:23 +00:00
|
|
|
log.debug('probe_storage: get_partitions() returns: {}'.format(
|
|
|
|
partitions))
|
2015-08-10 13:42:19 +00:00
|
|
|
return partitions
|
|
|
|
|
2015-10-26 14:57:19 +00:00
|
|
|
def get_filesystems(self):
|
|
|
|
log.debug('get_fs')
|
|
|
|
fs = []
|
|
|
|
for dev in self.devices.values():
|
|
|
|
fs += dev.filesystems
|
|
|
|
|
|
|
|
return fs
|
|
|
|
|
2015-10-02 00:03:54 +00:00
|
|
|
def installable(self):
|
|
|
|
''' one or more disks has used space
|
|
|
|
and has "/" as a mount
|
|
|
|
'''
|
2015-10-02 18:49:10 +00:00
|
|
|
for disk in self.get_all_disks():
|
2015-10-02 00:03:54 +00:00
|
|
|
if disk.usedspace > 0 and "/" in disk.mounts:
|
|
|
|
return True
|
|
|
|
|
2015-10-07 22:11:10 +00:00
|
|
|
return False
|
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
def bootable(self):
|
|
|
|
''' true if one disk has a boot partition '''
|
|
|
|
log.debug('bootable check')
|
|
|
|
for disk in self.get_all_disks():
|
|
|
|
for (num, action) in disk.partitions.items():
|
|
|
|
if action.flags in ['bios_grub']:
|
|
|
|
log.debug('bootable check: we\'ve got boot!')
|
|
|
|
return True
|
2015-08-10 13:42:19 +00:00
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
log.debug('bootable check: no disks have been marked bootable')
|
|
|
|
return False
|
|
|
|
|
2015-12-02 15:40:17 +00:00
|
|
|
def set_holder(self, held_device, holder_devpath):
|
|
|
|
''' insert a hold on `held_device' by adding `holder_devpath' to
|
|
|
|
a list at self.holders[`held_device']
|
|
|
|
'''
|
|
|
|
if held_device not in self.holders:
|
|
|
|
self.holders[held_device] = [holder_devpath]
|
|
|
|
else:
|
|
|
|
self.holders[held_device].append(holder_devpath)
|
|
|
|
|
|
|
|
def clear_holder(self, held_device, holder_devpath):
|
|
|
|
if held_device in self.holders:
|
|
|
|
self.holders[held_device].remove(holder_devpath)
|
|
|
|
|
|
|
|
def get_holders(self, held_device):
|
|
|
|
return self.holders.get(held_device, [])
|
|
|
|
|
|
|
|
def set_tag(self, device, tag):
|
|
|
|
self.tags[device] = tag
|
|
|
|
|
|
|
|
def get_tag(self, device):
|
|
|
|
return self.tags.get(device, '')
|
|
|
|
|
2015-11-02 17:43:33 +00:00
|
|
|
def valid_mount(self, format_spec):
|
|
|
|
""" format spec
|
|
|
|
|
|
|
|
{
|
2015-12-01 21:19:54 +00:00
|
|
|
'fstype' Str(ext4|btrfs..,
|
2015-11-02 17:43:33 +00:00
|
|
|
'mountpoint': Str
|
|
|
|
}
|
|
|
|
"""
|
2015-12-01 21:19:54 +00:00
|
|
|
log.debug('valid_mount: spec: {}'.format(format_spec))
|
2015-12-01 19:30:53 +00:00
|
|
|
# if user is not formatting, ignore mountpoint
|
2015-12-01 21:19:54 +00:00
|
|
|
fmt = format_spec.get('fstype')
|
2015-12-01 19:30:53 +00:00
|
|
|
if fmt in ['leave unformatted']:
|
|
|
|
format_spec['mountpoint'] = None
|
2015-12-01 21:19:54 +00:00
|
|
|
return True
|
2015-12-01 19:30:53 +00:00
|
|
|
|
2015-11-02 17:43:33 +00:00
|
|
|
mountpoint = format_spec.get('mountpoint')
|
|
|
|
if not mountpoint:
|
|
|
|
raise ValueError('Is None')
|
|
|
|
|
|
|
|
# If the value is in error state, return the
|
|
|
|
# the same error
|
|
|
|
err_prefix = 'error: '
|
|
|
|
if mountpoint.lower().startswith(err_prefix):
|
|
|
|
mountpoint = mountpoint[len(err_prefix):]
|
|
|
|
raise ValueError(mountpoint)
|
|
|
|
|
|
|
|
if not mountpoint.startswith('/'):
|
|
|
|
raise ValueError('Does not start with /')
|
|
|
|
|
2015-11-19 19:55:58 +00:00
|
|
|
# remove redundent // and ..
|
2016-01-04 13:26:22 +00:00
|
|
|
mountpoint = os.path.realpath(mountpoint)
|
2015-11-19 19:55:58 +00:00
|
|
|
|
2015-11-02 18:06:51 +00:00
|
|
|
# /usr/include/linux/limits.h:PATH_MAX
|
|
|
|
if len(mountpoint) > 4095:
|
|
|
|
raise ValueError('Path exceeds PATH_MAX')
|
|
|
|
|
2015-11-02 17:43:33 +00:00
|
|
|
all_mounts = self.get_mounts()
|
|
|
|
if mountpoint in all_mounts:
|
|
|
|
raise ValueError('Already mounted')
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2015-10-05 15:56:59 +00:00
|
|
|
def get_empty_disks(self):
|
|
|
|
''' empty disk is one that does not have any
|
|
|
|
partitions, filesystems or mounts, and is non-zero size '''
|
|
|
|
empty = []
|
|
|
|
for dev in self.get_available_disks():
|
|
|
|
if len(dev.partitions) == 0 and \
|
|
|
|
len(dev.mounts) == 0 and \
|
|
|
|
len(dev.filesystems) == 0:
|
|
|
|
empty.append(dev)
|
2015-11-02 22:48:37 +00:00
|
|
|
log.debug('empty_disks: {}'.format(
|
|
|
|
", ".join([dev.path for dev in empty])))
|
2015-10-05 15:56:59 +00:00
|
|
|
return empty
|
|
|
|
|
|
|
|
def get_empty_disk_names(self):
|
|
|
|
return [dev.disk.devpath for dev in self.get_empty_disks()]
|
|
|
|
|
|
|
|
def get_empty_partition_names(self):
|
|
|
|
''' empty partitions have non-zero size, but are not part
|
|
|
|
of a filesystem or mount point or other raid '''
|
|
|
|
empty = []
|
|
|
|
for dev in self.get_available_disks():
|
|
|
|
empty += dev.available_partitions
|
|
|
|
|
|
|
|
log.debug('empty_partitions: {}'.format(", ".join(empty)))
|
|
|
|
return empty
|
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
def get_available_disk_names(self):
|
|
|
|
return [dev.disk.devpath for dev in self.get_available_disks()]
|
|
|
|
|
|
|
|
def get_used_disk_names(self):
|
|
|
|
return [dev.disk.devpath for dev in self.get_all_disks()
|
2015-08-10 13:42:19 +00:00
|
|
|
if dev.available is False]
|
|
|
|
|
|
|
|
def get_disk_info(self, disk):
|
2015-10-02 00:03:54 +00:00
|
|
|
return self.info.get(disk, {})
|
2015-08-10 13:42:19 +00:00
|
|
|
|
2015-10-02 18:49:10 +00:00
|
|
|
def get_mounts(self):
|
|
|
|
mounts = []
|
|
|
|
for dev in self.get_all_disks():
|
|
|
|
mounts += dev.mounts
|
|
|
|
|
|
|
|
return mounts
|
|
|
|
|
2015-08-10 13:42:19 +00:00
|
|
|
def get_disk_action(self, disk):
|
|
|
|
return self.devices[disk].get_actions()
|
|
|
|
|
|
|
|
def get_actions(self):
|
|
|
|
actions = []
|
|
|
|
for dev in self.devices.values():
|
2016-01-04 13:26:22 +00:00
|
|
|
# don't write out actions for devices not in use
|
|
|
|
if not dev.available:
|
|
|
|
actions += dev.get_actions()
|
2015-10-26 14:57:19 +00:00
|
|
|
|
|
|
|
log.debug('****')
|
|
|
|
log.debug('all actions:{}'.format(actions))
|
|
|
|
log.debug('****')
|
|
|
|
return sort_actions(actions)
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _humanize_size(size):
|
|
|
|
size = abs(size)
|
|
|
|
if size == 0:
|
|
|
|
return "0B"
|
|
|
|
p = math.floor(math.log(size, 2) / 10)
|
2015-08-25 16:49:27 +00:00
|
|
|
return "%.3f%s" % (size / math.pow(1024, p), HUMAN_UNITS[int(p)])
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _dehumanize_size(size):
|
|
|
|
# convert human 'size' to integer
|
|
|
|
size_in = size
|
|
|
|
if size.endswith("B"):
|
|
|
|
size = size[:-1]
|
|
|
|
|
2015-08-28 20:30:34 +00:00
|
|
|
# build mpliers based on HUMAN_UNITS
|
|
|
|
mpliers = {}
|
|
|
|
for (unit, exponent) in zip(HUMAN_UNITS, range(0, len(HUMAN_UNITS))):
|
|
|
|
mpliers.update({unit: 2 ** (exponent * 10)})
|
2015-08-10 13:42:19 +00:00
|
|
|
|
|
|
|
num = size
|
|
|
|
mplier = 'B'
|
|
|
|
for m in mpliers:
|
|
|
|
if size.endswith(m):
|
|
|
|
mplier = m
|
|
|
|
num = size[0:-len(m)]
|
|
|
|
|
|
|
|
try:
|
|
|
|
num = float(num)
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError("'{}' is not valid input.".format(size_in))
|
|
|
|
|
|
|
|
if num < 0:
|
|
|
|
raise ValueError("'{}': cannot be negative".format(size_in))
|
|
|
|
|
|
|
|
return int(num * mpliers[mplier])
|