# 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 . 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')