Merge pull request #375 from mwhudson/fs-tweaks

filesystem tweaks
This commit is contained in:
Michael Hudson-Doyle 2018-07-04 14:17:30 +12:00 committed by GitHub
commit 55532a2362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 135 deletions

View File

@ -280,7 +280,7 @@ class FilesystemController(BaseController):
bootable = self.model.bootable()
log.debug('model has bootable device? {}'.format(bootable))
can_be_boot = disk.supports_action(DeviceAction.MAKE_BOOT)
can_be_boot = DeviceAction.MAKE_BOOT in disk.supported_actions
if not bootable and len(disk.partitions()) == 0 and can_be_boot:
part = self._create_boot_partition(disk)

View File

@ -162,16 +162,17 @@ def asdict(inst):
class DeviceAction(enum.Enum):
INFO = enum.auto()
EDIT = enum.auto()
PARTITION = enum.auto()
FORMAT = enum.auto()
DELETE = enum.auto()
MAKE_BOOT = enum.auto()
INFO = _("Info")
EDIT = _("Edit")
PARTITION = _("Add Partition")
CREATE_LV = _("Create Logical Volume")
FORMAT = _("Format")
DELETE = _("Delete")
MAKE_BOOT = _("Make Boot Device")
@attr.s(cmp=False)
class _Formattable:
class _Formattable(ABC):
# Base class for anything that can be formatted and mounted,
# e.g. a disk or a RAID or a partition.
@ -189,8 +190,19 @@ class _Formattable:
def constructed_device(self):
return self._constructed_device
def supports_action(self, action):
return getattr(self, "_supports_" + action.name)
@property
@abstractmethod
def supported_actions(self):
pass
def action_possible(self, action):
assert action in self.supported_actions
return getattr(self, "_can_" + action.name)
@property
@abstractmethod
def ok_for_raid(self):
pass
# Nothing is put in the first and last megabytes of the disk to allow
@ -337,18 +349,24 @@ class Disk(_Device):
return self.serial
return self.path
_supports_INFO = True
_supports_EDIT = False
_supports_PARTITION = property(lambda self: self.free_for_partitions > 0)
_supports_FORMAT = property(
supported_actions = [
DeviceAction.INFO,
DeviceAction.PARTITION,
DeviceAction.FORMAT,
DeviceAction.MAKE_BOOT,
]
_can_INFO = True
_can_PARTITION = property(lambda self: self.free_for_partitions > 0)
_can_FORMAT = property(
lambda self: len(self._partitions) == 0 and
self._constructed_device is None)
_supports_DELETE = False
_supports_MAKE_BOOT = property(
_can_MAKE_BOOT = property(
lambda self:
not self.grub_device and self._fs is None
and self._constructed_device is None)
ok_for_raid = _can_FORMAT
@attr.s(cmp=False)
class Partition(_Formattable):
@ -385,15 +403,24 @@ class Partition(_Formattable):
def path(self):
return "%s%s" % (self.device.path, self._number)
_supports_INFO = False
_supports_EDIT = True
_supports_PARTITION = False
_supports_FORMAT = property(
lambda self: self.flag not in ('boot', 'bios_grub') and
self._constructed_device is None)
_supports_DELETE = property(
supported_actions = [
DeviceAction.EDIT,
DeviceAction.DELETE,
]
_can_EDIT = True
_can_DELETE = property(
lambda self: self.flag not in ('boot', 'bios_grub'))
_supports_MAKE_BOOT = False
@property
def ok_for_raid(self):
if self.flag:
return False
if self._fs is not None:
return False
if self._constructed_device is not None:
return False
return True
@attr.s(cmp=False)
@ -417,19 +444,35 @@ class Raid(_Device):
def desc(self):
return _("software RAID {}").format(self.raidlevel)
_supports_INFO = False
_supports_EDIT = True
_supports_PARTITION = Disk._supports_PARTITION
_supports_FORMAT = property(
supported_actions = [
DeviceAction.EDIT,
DeviceAction.PARTITION,
DeviceAction.FORMAT,
DeviceAction.DELETE,
]
_can_EDIT = True
_can_PARTITION = Disk._can_PARTITION
_can_FORMAT = property(
lambda self: len(self._partitions) == 0 and
self._constructed_device is None)
_supports_DELETE = True
_supports_MAKE_BOOT = False
_can_DELETE = True
@property
def path(self):
return "/dev/{}".format(self.name)
@property
def ok_for_raid(self):
if self._fs is not None:
return False
if self._constructed_device is not None:
return False
return True
# What is a device that makes up this device referred to as?
component_name = "component"
@attr.s(cmp=False)
class Filesystem:

View File

@ -32,7 +32,7 @@ from urwid import (
from subiquitycore.ui.actionmenu import (
Action,
ActionMenu,
ActionMenuButton,
ActionMenuOpenButton,
)
from subiquitycore.ui.buttons import (
back_btn,
@ -62,7 +62,10 @@ from subiquitycore.ui.utils import (
)
from subiquitycore.view import BaseView
from subiquity.models.filesystem import DeviceAction, humanize_size
from subiquity.models.filesystem import (
DeviceAction,
humanize_size,
)
from .delete import can_delete, ConfirmDeleteStretchy
from .disk_info import DiskInfoStretchy
@ -173,7 +176,6 @@ class MountList(WidgetWrap):
self._no_mounts_content = Color.info_minor(
Text(_("No disks or partitions mounted.")))
super().__init__(self.table)
self.refresh_model_inputs()
def _mount_action(self, sender, action, mount):
log.debug('_mount_action %s %s', action, mount)
@ -251,6 +253,7 @@ class MountList(WidgetWrap):
def _stretchy_shower(cls):
def impl(self, device):
self.parent.show_stretchy_overlay(cls(self.parent, device))
impl.opens_dialog = True
return impl
@ -272,9 +275,6 @@ class DeviceList(WidgetWrap):
text = _("No used devices")
self._no_devices_content = Color.info_minor(Text(text))
super().__init__(self.table)
self.refresh_model_inputs()
# I don't really know why this is required:
self.table._select_first_selectable()
_disk_INFO = _stretchy_shower(DiskInfoStretchy)
_disk_PARTITION = _stretchy_shower(PartitionStretchy)
@ -287,7 +287,6 @@ class DeviceList(WidgetWrap):
_partition_EDIT = _stretchy_shower(
lambda parent, part: PartitionStretchy(parent, part.device, part))
_partition_DELETE = _stretchy_shower(ConfirmDeleteStretchy)
_partition_FORMAT = _disk_FORMAT
_raid_EDIT = _stretchy_shower(RaidStretchy)
_raid_PARTITION = _disk_PARTITION
@ -300,26 +299,29 @@ class DeviceList(WidgetWrap):
getattr(self, meth_name)(device)
def _action_menu_for_device(self, device):
if can_delete(device)[0]:
delete_btn = Color.danger_button(ActionMenuButton(_("Delete")))
else:
delete_btn = _("Delete *")
device_actions = [
(_("Information"), DeviceAction.INFO),
(_("Edit"), DeviceAction.EDIT),
(_("Add Partition"), DeviceAction.PARTITION),
(_("Format / Mount"), DeviceAction.FORMAT),
(delete_btn, DeviceAction.DELETE),
(_("Make boot device"), DeviceAction.MAKE_BOOT),
]
actions = []
for label, action in device_actions:
actions.append(Action(
device_actions = []
can_delete_device = can_delete(device)[0]
for action in device.supported_actions:
if action == DeviceAction.DELETE:
enabled = True
if can_delete_device:
label = Color.danger_button(
ActionMenuOpenButton(_("Delete")))
else:
label = _("Delete *")
else:
label = _(action.value)
enabled = device.action_possible(action)
meth_name = '_{}_{}'.format(device.type, action.name)
meth = getattr(self, meth_name)
opens_dialog = getattr(meth, 'opens_dialog', False)
device_actions.append(Action(
label=label,
enabled=device.supports_action(action),
enabled=enabled,
value=action,
opens_dialog=action != DeviceAction.MAKE_BOOT))
menu = ActionMenu(actions, "\N{BLACK RIGHT-POINTING SMALL TRIANGLE}")
opens_dialog=opens_dialog))
menu = ActionMenu(
device_actions, "\N{BLACK RIGHT-POINTING SMALL TRIANGLE}")
connect_signal(menu, 'action', self._action, device)
return menu
@ -337,20 +339,23 @@ class DeviceList(WidgetWrap):
log.debug('FileSystemView: building device list')
rows = []
def _fmt_fs(label, fs):
r = _("{} {}").format(label, fs.fstype)
if not self.parent.model.fs_by_name[fs.fstype].is_mounted:
return r
m = fs.mount()
if m:
r += _(", {}").format(m.path)
def _usage_label(obj):
cd = obj.constructed_device()
if cd is not None:
return _("{component_name} of {name}").format(
component_name=cd.component_name, name=cd.name)
fs = obj.fs()
if fs is not None:
m = fs.mount()
if m:
return _(
"formatted as {fstype}, mounted at {path}").format(
fstype=fs.fstype, path=m.path)
else:
return _("formatted as {fstype}, not mounted").format(
fstype=fs.fstype)
else:
r += _(", not mounted")
return r
def _fmt_constructed(label, device):
return _("{} part of {} ({})").format(
label, device.label, device.desc())
return _("unused")
rows.append(TableRow([Color.info_minor(heading) for heading in [
Text(" "),
@ -374,19 +379,10 @@ class DeviceList(WidgetWrap):
menu, row, 'menu_button', 'menu_button focus')
rows.append(row)
entire_label = None
if device.fs():
entire_label = _fmt_fs(
" " + _("entire device formatted as"),
device.fs())
elif device.constructed_device():
entire_label = _fmt_constructed(
" " + _("entire device"),
device.constructed_device())
if entire_label is not None:
if not device.partitions():
rows.append(TableRow([
Text(""),
(3, Text(entire_label)),
(3, Text(" " + _usage_label(device))),
Text(""),
Text(""),
]))
@ -394,23 +390,13 @@ class DeviceList(WidgetWrap):
for part in device.partitions():
if part.available() != self.show_available:
continue
prefix = _("partition {},").format(part._number)
if part.flag == "bios_grub":
label = prefix + " bios_grub"
elif part.fs():
label = _fmt_fs(prefix, part.fs())
elif part.constructed_device():
label = _fmt_constructed(
prefix, part.constructed_device())
else:
label = _("{} not formatted").format(prefix)
menu = self._action_menu_for_device(part)
part_size = "{:>9} ({}%)".format(
humanize_size(part.size),
int(100 * part.size / device.size))
menu = self._action_menu_for_device(part)
row = TableRow([
Text("["),
Text(" " + label),
Text(" " + _("partition {}").format(part._number)),
(2, Text(part_size)),
menu,
Text("]"),
@ -419,6 +405,16 @@ class DeviceList(WidgetWrap):
menu, row, 'menu_button', 'menu_button focus',
cursor_x=4)
rows.append(row)
if part.flag == "bios_grub":
label = "bios_grub"
else:
label = _usage_label(part)
rows.append(TableRow([
Text(""),
(3, Text(" " + label)),
Text(""),
Text(""),
]))
if (self.show_available
and device.used > 0
and device.free_for_partitions > 0):
@ -456,9 +452,9 @@ class FilesystemView(BaseView):
self.avail_list = DeviceList(self, True)
self.used_list = DeviceList(self, False)
self.avail_list.table.bind(self.used_list.table)
self._create_raid_btn = menu_btn(
self._create_raid_btn = Toggleable(menu_btn(
label=_("Create software RAID (md)"),
on_press=self.create_raid)
on_press=self.create_raid))
bp = button_pile([self._create_raid_btn])
bp.align = 'left'
@ -488,13 +484,12 @@ class FilesystemView(BaseView):
focus_buttons=self.model.can_install())
frame.width = ('relative', 95)
super().__init__(frame)
self.refresh_model_inputs()
log.debug('FileSystemView init complete()')
def _build_buttons(self):
log.debug('FileSystemView: building buttons')
self.done = Toggleable(done_btn(_("Done"), on_press=self.done))
if not self.model.can_install():
self.done.disable()
return [
self.done,
@ -503,18 +498,20 @@ class FilesystemView(BaseView):
]
def refresh_model_inputs(self):
raid_devices = set()
for d in self.model.all_devices():
if d.ok_for_raid:
raid_devices.add(d)
for p in d.partitions():
if p.ok_for_raid:
raid_devices.add(p)
self._create_raid_btn.enabled = len(raid_devices) > 1
self.mount_list.refresh_model_inputs()
self.avail_list.refresh_model_inputs()
self.used_list.refresh_model_inputs()
# If refreshing the view has left the focus widget with no
# selectable widgets, simulate a tab to move to the next
# selectable widget.
while not self.lb.base_widget.focus.selectable():
self.lb.base_widget.keypress((10, 10), 'tab')
if self.model.can_install():
self.done.enable()
else:
self.done.disable()
# This is an awful hack, actual thinking required:
self.lb.base_widget._select_first_selectable()
self.done.enabled = self.model.can_install()
def create_raid(self, button=None):
self.show_stretchy_overlay(RaidStretchy(self))

View File

@ -54,7 +54,6 @@ from subiquitycore.ui.utils import (
from .partition import FSTypeField
from subiquity.ui.mount import MountField
from subiquity.models.filesystem import (
DeviceAction,
get_raid_size,
humanize_size,
raidlevels,
@ -90,10 +89,10 @@ class MultiDeviceChooser(WidgetWrap, WantsToKnowFormField):
self.devices = value
for d, s in self.device_to_selector.items():
if d in self.devices:
s.enable()
s.enabled = True
s.base_widget.value = self.devices[d]
else:
s.disable()
s.enabled = False
for d, b in self.device_to_checkbox.items():
b.set_state(d in self.devices)
@ -113,24 +112,24 @@ class MultiDeviceChooser(WidgetWrap, WantsToKnowFormField):
self.supports_spares = val
if val:
for device in list(self.devices):
self.device_to_selector[device].enable()
selector = self.device_to_selector[device]
selector.enabled = True
self.devices[device] = selector.base_widget.value
self.table.set_contents(self.all_rows)
else:
for device in list(self.devices):
self.device_to_selector[device].disable()
self.device_to_selector[device].enabled = False
self.devices[device] = 'active'
self.table.set_contents(self.no_selector_rows)
def _state_change_device(self, sender, state, device):
if state:
if self.supports_spares:
self.device_to_selector[device].enable()
self.device_to_selector[device].enabled = True
selector = self.device_to_selector[device]
self.devices[device] = selector.base_widget.value
else:
self.device_to_selector[device].disable()
self.device_to_selector[device].enabled = False
del self.devices[device]
self._emit('change', self.devices)
@ -188,7 +187,7 @@ class MultiDeviceChooser(WidgetWrap, WantsToKnowFormField):
UrwidPadding(
Color.menu_button(selector),
left=len(prefix)))
selector.disable()
selector.enabled = False
self.device_to_selector[device] = selector
self.all_rows.append(TableRow([(2, selector)]))
# Do not append that one to no_selector_rows!
@ -341,9 +340,11 @@ class RaidStretchy(Stretchy):
cur_devices = existing.devices | existing.spare_devices
def device_ok(dev):
return (dev not in omits
and (dev.supports_action(DeviceAction.FORMAT)
or dev in cur_devices))
if dev in omits:
return False
if dev in cur_devices:
return True
return dev.ok_for_raid
for dev in self.parent.model.all_devices():
if device_ok(dev):
@ -375,7 +376,8 @@ class RaidStretchy(Stretchy):
Text("You cannot save edit to RAIDs just yet."),
Text(""),
]
self.form.validated = lambda *args: self.form.done_btn.disable()
self.form.validated = lambda *args: setattr(
self.form.done_btn, 'enabled', False)
self.form.validated()
super().__init__(

View File

@ -58,18 +58,20 @@ class Toggleable(delegate_to_widget_mixin('_original_widget'),
def __init__(self, original):
self.original = original
self.enabled = False
self.enable()
self._enabled = False
self.enabled = True
def enable(self):
if not self.enabled:
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, val):
if val and not self._enabled:
self.original_widget = self.original
self.enabled = True
def disable(self):
if self.enabled:
elif not val and self._enabled:
self.original_widget = disabled(self.original)
self.enabled = False
self._enabled = val
class _Validator(WidgetWrap):
@ -244,14 +246,9 @@ class BoundFormField(object):
@enabled.setter
def enabled(self, val):
if val != self._enabled:
self._enabled = val
if val:
for row in self._rows:
row.enable()
else:
for row in self._rows:
row.disable()
self._enabled = val
for row in self._rows:
row.enabled = val
def simple_field(widget_maker):
@ -401,10 +398,10 @@ class Form(object, metaclass=MetaForm):
in_error = True
break
if in_error:
self.buttons.base_widget.contents[0][0].disable()
self.buttons.base_widget.contents[0][0].enabled = False
self.buttons.base_widget.focus_position = 1
else:
self.buttons.base_widget.contents[0][0].enable()
self.buttons.base_widget.contents[0][0].enabled = True
def as_data(self):
data = {}