343 lines
14 KiB
Python
343 lines
14 KiB
Python
# 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 logging
|
|
import os
|
|
from subiquitycore.controller import BaseController
|
|
from subiquitycore.models.actions import preserve_action
|
|
from subiquitycore.models import (FilesystemModel, RaidModel)
|
|
from subiquitycore.models.filesystem import (_humanize_size)
|
|
from subiquitycore.ui.views import (DiskPartitionView, AddPartitionView,
|
|
AddFormatView, FilesystemView,
|
|
DiskInfoView, RaidView, BcacheView,
|
|
LVMVolumeGroupView)
|
|
from subiquitycore.ui.dummy import DummyView
|
|
from subiquitycore.ui.error import ErrorView
|
|
from subiquitycore.curtin import (curtin_write_storage_actions,
|
|
curtin_write_preserved_actions)
|
|
|
|
|
|
log = logging.getLogger("subiquitycore.controller.filesystem")
|
|
|
|
BIOS_GRUB_SIZE_BYTES = 2 * 1024 * 1024 # 2MiB
|
|
UEFI_GRUB_SIZE_BYTES = 512 * 1024 * 1024 # 512MiB EFI partition
|
|
|
|
|
|
class FilesystemController(BaseController):
|
|
def __init__(self, common):
|
|
super().__init__(common)
|
|
self.model = FilesystemModel(self.prober, self.opts)
|
|
# self.iscsi_model = IscsiDiskModel()
|
|
# self.ceph_model = CephDiskModel()
|
|
self.raid_model = RaidModel()
|
|
|
|
def filesystem(self, reset=False):
|
|
# FIXME: Is this the best way to zero out this list for a reset?
|
|
if reset:
|
|
log.info("Resetting Filesystem model")
|
|
self.model.reset()
|
|
|
|
title = "Filesystem setup"
|
|
footer = ("Select available disks to format and mount")
|
|
self.ui.set_header(title)
|
|
self.ui.set_footer(footer, 30)
|
|
self.ui.set_body(FilesystemView(self.model,
|
|
self.signal))
|
|
|
|
def filesystem_error(self, error_fname):
|
|
title = "Filesystem error"
|
|
footer = ("Error while installing Ubuntu")
|
|
error_msg = "Failed to obtain write permissions to /tmp"
|
|
self.ui.set_header(title)
|
|
self.ui.set_footer(footer, 30)
|
|
self.ui.set_body(ErrorView(self.signal, error_msg))
|
|
|
|
def filesystem_handler(self, reset=False, actions=None):
|
|
if actions is None and reset is False:
|
|
self.signal.emit_signal('network:show')
|
|
|
|
log.info("Rendering curtin config from user choices")
|
|
try:
|
|
curtin_write_storage_actions(actions=actions)
|
|
except PermissionError:
|
|
log.exception('Failed to write storage actions')
|
|
self.signal.emit_signal('filesystem:error',
|
|
'curtin_write_storage_actions')
|
|
return None
|
|
|
|
log.info("Rendering preserved config for post install")
|
|
preserved_actions = [preserve_action(a) for a in actions]
|
|
try:
|
|
curtin_write_preserved_actions(actions=preserved_actions)
|
|
except PermissionError:
|
|
log.exception('Failed to write preserved actions')
|
|
self.signal.emit_signal('filesystem:error',
|
|
'curtin_write_preserved_actions')
|
|
return None
|
|
|
|
# mark that we've writting out curtin config
|
|
self.signal.emit_signal('installprogress:wrote-install')
|
|
|
|
# start curtin install in background
|
|
self.signal.emit_signal('installprogress:curtin-install')
|
|
|
|
# switch to identity view
|
|
self.signal.emit_signal('menu:identity:main')
|
|
|
|
# Filesystem/Disk partition -----------------------------------------------
|
|
def disk_partition(self, disk):
|
|
log.debug("In disk partition view, using {} as the disk.".format(disk))
|
|
title = ("Partition, format, and mount {}".format(disk))
|
|
footer = ("Partition the disk, or format the entire device "
|
|
"without partitions.")
|
|
self.ui.set_header(title)
|
|
self.ui.set_footer(footer)
|
|
dp_view = DiskPartitionView(self.model,
|
|
self.signal,
|
|
disk)
|
|
|
|
self.ui.set_body(dp_view)
|
|
|
|
def disk_partition_handler(self, spec=None):
|
|
log.debug("Disk partition: {}".format(spec))
|
|
if spec is None:
|
|
self.signal.prev_signal()
|
|
self.signal.emit_signal('menu:filesystem:main:show-disk-partition', [])
|
|
|
|
def add_disk_partition(self, disk):
|
|
log.debug("Adding partition to {}".format(disk))
|
|
footer = ("Select whole disk, or partition, to format and mount.")
|
|
self.ui.set_footer(footer)
|
|
adp_view = AddPartitionView(self.model,
|
|
self.signal,
|
|
disk)
|
|
self.ui.set_body(adp_view)
|
|
|
|
def add_disk_partition_handler(self, disk, spec):
|
|
current_disk = self.model.get_disk(disk)
|
|
log.debug('spec: {}'.format(spec))
|
|
log.debug('disk.freespace: {}'.format(current_disk.freespace))
|
|
|
|
try:
|
|
''' create a gpt boot partition if one doesn't exist, only
|
|
one one disk'''
|
|
|
|
system_bootable = self.model.bootable()
|
|
log.debug('model has bootable device? {}'.format(system_bootable))
|
|
if system_bootable is False and \
|
|
current_disk.parttype == 'gpt' and \
|
|
len(current_disk.disk.partitions) == 0:
|
|
if self.is_uefi():
|
|
log.debug('Adding EFI partition first')
|
|
size_added = \
|
|
current_disk.add_partition(partnum=1,
|
|
size=UEFI_GRUB_SIZE_BYTES,
|
|
flag='bios_grub',
|
|
fstype='fat32',
|
|
mountpoint='/boot/efi')
|
|
else:
|
|
log.debug('Adding grub_bios gpt partition first')
|
|
size_added = \
|
|
current_disk.add_partition(partnum=1,
|
|
size=BIOS_GRUB_SIZE_BYTES,
|
|
fstype=None,
|
|
flag='bios_grub')
|
|
current_disk.set_tag('(boot)')
|
|
|
|
# adjust downward the partition size to accommodate
|
|
# the offset and bios/grub partition
|
|
log.debug("Adjusting request down:" +
|
|
"{} - {} = {}".format(spec['bytes'], size_added,
|
|
spec['bytes'] - size_added))
|
|
spec['bytes'] -= size_added
|
|
spec['partnum'] = 2
|
|
|
|
if spec["fstype"] in ["swap"]:
|
|
current_disk.add_partition(partnum=spec["partnum"],
|
|
size=spec["bytes"],
|
|
fstype=spec["fstype"])
|
|
else:
|
|
current_disk.add_partition(partnum=spec["partnum"],
|
|
size=spec["bytes"],
|
|
fstype=spec["fstype"],
|
|
mountpoint=spec["mountpoint"])
|
|
except Exception:
|
|
log.exception('Failed to add disk partition')
|
|
log.debug('Returning to add-disk-partition')
|
|
# FIXME: on failure, we should repopulate input values
|
|
self.signal.emit_signal('filesystem:add-disk-partition', disk)
|
|
|
|
log.info("Successfully added partition")
|
|
|
|
log.debug("FS Table: {}".format(current_disk.get_fs_table()))
|
|
self.signal.emit_signal('menu:filesystem:main:show-disk-partition',
|
|
disk)
|
|
|
|
def add_disk_format_handler(self, disk, spec):
|
|
log.debug('add_disk_format_handler')
|
|
current_disk = self.model.get_disk(disk)
|
|
log.debug('format spec: {}'.format(spec))
|
|
log.debug('disk.freespace: {}'.format(current_disk.freespace))
|
|
current_disk.format_device(spec['fstype'], spec['mountpoint'])
|
|
log.debug("FS Table: {}".format(current_disk.get_fs_table()))
|
|
self.signal.emit_signal('menu:filesystem:main:show-disk-partition',
|
|
disk)
|
|
|
|
def connect_iscsi_disk(self, *args, **kwargs):
|
|
# title = ("Disk and filesystem setup")
|
|
# excerpt = ("Connect to iSCSI cluster")
|
|
# self.ui.set_header(title, excerpt)
|
|
# self.ui.set_footer("")
|
|
# self.ui.set_body(IscsiDiskView(self.iscsi_model,
|
|
# self.signal))
|
|
self.ui.set_body(DummyView(self.signal))
|
|
|
|
def connect_ceph_disk(self, *args, **kwargs):
|
|
# title = ("Disk and filesystem setup")
|
|
# footer = ("Select available disks to format and mount")
|
|
# excerpt = ("Connect to Ceph storage cluster")
|
|
# self.ui.set_header(title, excerpt)
|
|
# self.ui.set_footer(footer)
|
|
# self.ui.set_body(CephDiskView(self.ceph_model,
|
|
# self.signal))
|
|
self.ui.set_body(DummyView(self.signal))
|
|
|
|
def create_volume_group(self, *args, **kwargs):
|
|
title = ("Create Logical Volume Group (\"LVM2\") disk")
|
|
footer = ("ENTER on a disk will show detailed "
|
|
"information for that disk")
|
|
excerpt = ("Use SPACE to select disks to form your LVM2 volume group, "
|
|
"and then specify the Volume Group name. ")
|
|
self.ui.set_header(title, excerpt)
|
|
self.ui.set_footer(footer)
|
|
self.ui.set_body(LVMVolumeGroupView(self.model, self.signal))
|
|
|
|
def create_raid(self, *args, **kwargs):
|
|
title = ("Create software RAID (\"MD\") disk")
|
|
footer = ("ENTER on a disk will show detailed "
|
|
"information for that disk")
|
|
excerpt = ("Use SPACE to select disks to form your RAID array, "
|
|
"and then specify the RAID parameters. Multiple-disk "
|
|
"arrays work best when all the disks in an array are "
|
|
"the same size and speed.")
|
|
self.ui.set_header(title, excerpt)
|
|
self.ui.set_footer(footer)
|
|
self.ui.set_body(RaidView(self.model,
|
|
self.signal))
|
|
|
|
def create_bcache(self, *args, **kwargs):
|
|
title = ("Create hierarchical storage (\"bcache\") disk")
|
|
footer = ("ENTER on a disk will show detailed "
|
|
"information for that disk")
|
|
excerpt = ("Use SPACE to select a cache disk and a backing disk"
|
|
" to form your bcache device.")
|
|
|
|
self.ui.set_header(title, excerpt)
|
|
self.ui.set_footer(footer)
|
|
self.ui.set_body(BcacheView(self.model,
|
|
self.signal))
|
|
|
|
def add_raid_dev(self, result):
|
|
log.debug('add_raid_dev: result={}'.format(result))
|
|
self.model.add_raid_device(result)
|
|
self.signal.prev_signal()
|
|
|
|
def add_first_gpt_partition(self, *args, **kwargs):
|
|
self.ui.set_body(DummyView(self.signal))
|
|
|
|
def create_swap_entire_device(self, disk):
|
|
log.debug('create_swap_entire_device')
|
|
log.debug("formatting whole {}".format(disk))
|
|
footer = ("Format or mount whole disk.")
|
|
self.ui.set_footer(footer)
|
|
afv_view = AddFormatView(self.model,
|
|
self.signal,
|
|
disk)
|
|
self.ui.set_body(afv_view)
|
|
|
|
def show_disk_information_next(self, curr_device):
|
|
log.debug('show_disk_info_next: curr_device={}'.format(curr_device))
|
|
available = self.model.get_available_disk_names()
|
|
idx = available.index(curr_device)
|
|
next_idx = (idx + 1) % len(available)
|
|
next_device = available[next_idx]
|
|
self.show_disk_information(next_device)
|
|
|
|
def show_disk_information_prev(self, curr_device):
|
|
log.debug('show_disk_info_prev: curr_device={}'.format(curr_device))
|
|
available = self.model.get_available_disk_names()
|
|
idx = available.index(curr_device)
|
|
next_idx = (idx - 1) % len(available)
|
|
next_device = available[next_idx]
|
|
self.show_disk_information(next_device)
|
|
|
|
def show_disk_information(self, device):
|
|
""" Show disk information, requires sudo/root
|
|
"""
|
|
disk_info = self.model.get_disk_info(device)
|
|
disk = self.model.get_disk(device)
|
|
|
|
bus = disk_info.raw.get('ID_BUS', None)
|
|
major = disk_info.raw.get('MAJOR', None)
|
|
if bus is None and major == '253':
|
|
bus = 'virtio'
|
|
|
|
devpath = disk_info.raw.get('DEVPATH', disk.devpath)
|
|
rotational = '1'
|
|
try:
|
|
dev = os.path.basename(devpath)
|
|
rfile = '/sys/class/block/{}/queue/rotational'.format(dev)
|
|
rotational = open(rfile, 'r').read().strip()
|
|
except (PermissionError, FileNotFoundError, IOError):
|
|
log.exception('WARNING: Failed to read file {}'.format(rfile))
|
|
pass
|
|
|
|
dinfo = {
|
|
'bus': bus,
|
|
'devname': disk.devpath,
|
|
'devpath': devpath,
|
|
'model': disk.model,
|
|
'serial': disk.serial,
|
|
'size': disk.size,
|
|
'humansize': _humanize_size(disk.size),
|
|
'vendor': disk_info.vendor,
|
|
'rotational': 'true' if rotational == '1' else 'false',
|
|
}
|
|
|
|
template = """\n
|
|
{devname}:\n
|
|
Vendor: {vendor}
|
|
Model: {model}
|
|
SerialNo: {serial}
|
|
Size: {humansize} ({size}B)
|
|
Bus: {bus}
|
|
Rotational: {rotational}
|
|
Path: {devpath}
|
|
"""
|
|
result = template.format(**dinfo)
|
|
log.debug('calling DiskInfoView()')
|
|
disk_info_view = DiskInfoView(self.model, self.signal,
|
|
device, result)
|
|
footer = ('Select next or previous disks with n and p')
|
|
self.ui.set_footer(footer, 30)
|
|
self.ui.set_body(disk_info_view)
|
|
|
|
def is_uefi(self):
|
|
if self.opts.dry_run and self.opts.uefi:
|
|
log.debug('forcing is_uefi True beacuse of options')
|
|
return True
|
|
|
|
return os.path.exists('/sys/firmware/efi')
|