Fix Btrfs mount options (#2404)

This commit is contained in:
codefiles 2024-03-11 03:09:26 -04:00 committed by GitHub
parent 1064f74846
commit c210cdcb8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 91 additions and 134 deletions

View File

@ -437,9 +437,19 @@ class DeviceHandler(object):
if not luks_handler.mapper_dev:
raise DiskError('Failed to unlock luks device')
self.mount(luks_handler.mapper_dev, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
self.mount(
luks_handler.mapper_dev,
self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True,
options=part_mod.mount_options
)
else:
self.mount(part_mod.safe_dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
self.mount(
part_mod.safe_dev_path,
self._TMP_BTRFS_MOUNT,
create_target_mountpoint=True,
options=part_mod.mount_options
)
for sub_vol in part_mod.btrfs_subvols:
debug(f'Creating subvolume: {sub_vol.name}')
@ -451,18 +461,6 @@ class DeviceHandler(object):
SysCommand(f"btrfs subvolume create {subvol_path}")
if sub_vol.nodatacow:
try:
SysCommand(f'chattr +C {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set nodatacow attribute at {subvol_path}: {err}')
if sub_vol.compress:
try:
SysCommand(f'chattr +c {subvol_path}')
except SysCallError as err:
raise DiskError(f'Could not set compress attribute at {subvol_path}: {err}')
if luks_handler is not None and luks_handler.mapper_dev is not None:
self.umount(luks_handler.mapper_dev)
luks_handler.lock()

View File

@ -315,6 +315,11 @@ class Size:
return self._normalize() >= other._normalize()
class BtrfsMountOption(Enum):
compress = 'compress=zstd'
nodatacow = 'nodatacow'
@dataclass
class _BtrfsSubvolumeInfo:
name: Path
@ -458,8 +463,6 @@ class _DeviceInfo:
class SubvolumeModification:
name: Path
mountpoint: Optional[Path] = None
compress: bool = False
nodatacow: bool = False
@classmethod
def from_existing_subvol_info(cls, info: _BtrfsSubvolumeInfo) -> SubvolumeModification:
@ -475,30 +478,10 @@ class SubvolumeModification:
mountpoint = Path(entry['mountpoint']) if entry['mountpoint'] else None
compress = entry.get('compress', False)
nodatacow = entry.get('nodatacow', False)
if compress and nodatacow:
raise ValueError('compress and nodatacow flags cannot be enabled simultaneously on a btfrs subvolume')
mods.append(
SubvolumeModification(
entry['name'],
mountpoint,
compress,
nodatacow
)
)
mods.append(SubvolumeModification(entry['name'], mountpoint))
return mods
@property
def mount_options(self) -> List[str]:
options = []
options += ['compress'] if self.compress else []
options += ['nodatacow'] if self.nodatacow else []
return options
@property
def relative_mountpoint(self) -> Path:
"""
@ -516,12 +499,7 @@ class SubvolumeModification:
return False
def json(self) -> Dict[str, Any]:
return {
'name': str(self.name),
'mountpoint': str(self.mountpoint),
'compress': self.compress,
'nodatacow': self.nodatacow
}
return {'name': str(self.name), 'mountpoint': str(self.mountpoint)}
def table_data(self) -> Dict[str, Any]:
return self.json()

View File

@ -5,7 +5,7 @@ from pathlib import Path
from typing import Any, Dict, TYPE_CHECKING, List, Optional, Tuple
from .device_model import PartitionModification, FilesystemType, BDevice, Size, Unit, PartitionType, PartitionFlag, \
ModificationStatus, DeviceGeometry, SectorSize
ModificationStatus, DeviceGeometry, SectorSize, BtrfsMountOption
from ..hardware import SysInfo
from ..menu import Menu, ListManager, MenuSelection, TextInput
from ..output import FormattedOutput, warn
@ -30,6 +30,7 @@ class PartitioningList(ListManager):
'mark_bootable': str(_('Mark/Unmark as bootable')),
'set_filesystem': str(_('Change filesystem')),
'btrfs_mark_compressed': str(_('Mark/Unmark as compressed')), # btrfs only
'btrfs_mark_nodatacow': str(_('Mark/Unmark as nodatacow')), # btrfs only
'btrfs_set_subvolumes': str(_('Set subvolumes')), # btrfs only
'delete_partition': str(_('Delete partition'))
}
@ -71,12 +72,17 @@ class PartitioningList(ListManager):
self._actions['set_filesystem'],
self._actions['mark_bootable'],
self._actions['btrfs_mark_compressed'],
self._actions['btrfs_mark_nodatacow'],
self._actions['btrfs_set_subvolumes']
]
# non btrfs partitions shouldn't get btrfs options
if selection.fs_type != FilesystemType.Btrfs:
not_filter += [self._actions['btrfs_mark_compressed'], self._actions['btrfs_set_subvolumes']]
not_filter += [
self._actions['btrfs_mark_compressed'],
self._actions['btrfs_mark_nodatacow'],
self._actions['btrfs_set_subvolumes']
]
else:
not_filter += [self._actions['assign_mountpoint']]
@ -122,7 +128,9 @@ class PartitioningList(ListManager):
if fs_type == FilesystemType.Btrfs:
entry.mountpoint = None
case 'btrfs_mark_compressed' if entry:
self._set_compressed(entry)
self._toggle_mount_option(entry, BtrfsMountOption.compress)
case 'btrfs_mark_nodatacow' if entry:
self._toggle_mount_option(entry, BtrfsMountOption.nodatacow)
case 'btrfs_set_subvolumes' if entry:
self._set_btrfs_subvolumes(entry)
case 'delete_partition' if entry:
@ -141,13 +149,28 @@ class PartitioningList(ListManager):
else:
return [d for d in data if d != entry]
def _set_compressed(self, partition: PartitionModification):
compression = 'compress=zstd'
def _toggle_mount_option(
self,
partition: PartitionModification,
option: BtrfsMountOption
):
if option.value not in partition.mount_options:
if option == BtrfsMountOption.compress:
partition.mount_options = [
o for o in partition.mount_options
if o != BtrfsMountOption.nodatacow.value
]
if compression in partition.mount_options:
partition.mount_options = [o for o in partition.mount_options if o != compression]
partition.mount_options = [
o for o in partition.mount_options
if not o.startswith(BtrfsMountOption.compress.name)
]
partition.mount_options.append(option.value)
else:
partition.mount_options.append(compression)
partition.mount_options = [
o for o in partition.mount_options if o != option.value
]
def _set_btrfs_subvolumes(self, partition: PartitionModification):
partition.btrfs_subvols = SubvolumeMenu(

View File

@ -2,7 +2,7 @@ from pathlib import Path
from typing import Dict, List, Optional, Any, TYPE_CHECKING
from .device_model import SubvolumeModification
from ..menu import Menu, TextInput, MenuSelectionType, ListManager
from ..menu import TextInput, ListManager
from ..output import FormattedOutput
if TYPE_CHECKING:
@ -36,23 +36,6 @@ class SubvolumeMenu(ListManager):
def selected_action_display(self, subvolume: SubvolumeModification) -> str:
return str(subvolume.name)
def _prompt_options(self, editing: Optional[SubvolumeModification] = None) -> List[str]:
preset_options = []
if editing:
preset_options = editing.mount_options
choice = Menu(
str(_("Select the desired subvolume options ")),
['nodatacow', 'compress'],
skip=True,
preset_values=preset_options,
).run()
if choice.type_ == MenuSelectionType.Selection:
return choice.value # type: ignore
return []
def _add_subvolume(self, editing: Optional[SubvolumeModification] = None) -> Optional[SubvolumeModification]:
name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run()
@ -64,13 +47,7 @@ class SubvolumeMenu(ListManager):
if not mountpoint:
return None
options = self._prompt_options(editing)
subvolume = SubvolumeModification(Path(name), Path(mountpoint))
subvolume.compress = 'compress' in options
subvolume.nodatacow = 'nodatacow' in options
return subvolume
return SubvolumeModification(Path(name), Path(mountpoint))
def handle_action(
self,

View File

@ -240,7 +240,11 @@ class Installer:
disk.device_handler.mount(part_mod.dev_path, target, options=part_mod.mount_options)
if part_mod.fs_type == disk.FilesystemType.Btrfs and part_mod.dev_path:
self._mount_btrfs_subvol(part_mod.dev_path, part_mod.btrfs_subvols)
self._mount_btrfs_subvol(
part_mod.dev_path,
part_mod.btrfs_subvols,
part_mod.mount_options
)
def _mount_luks_partition(self, part_mod: disk.PartitionModification, luks_handler: Luks2):
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined
@ -251,11 +255,18 @@ class Installer:
if part_mod.fs_type == disk.FilesystemType.Btrfs and luks_handler.mapper_dev:
self._mount_btrfs_subvol(luks_handler.mapper_dev, part_mod.btrfs_subvols)
def _mount_btrfs_subvol(self, dev_path: Path, subvolumes: List[disk.SubvolumeModification]):
def _mount_btrfs_subvol(
self,
dev_path: Path,
subvolumes: List[disk.SubvolumeModification],
mount_options: List[str] = []
):
for subvol in subvolumes:
mountpoint = self.target / subvol.relative_mountpoint
mount_options = subvol.mount_options + [f'subvol={subvol.name}']
disk.device_handler.mount(dev_path, mountpoint, options=mount_options)
disk.device_handler.mount(
dev_path,
self.target / subvol.relative_mountpoint,
options=mount_options + [f'subvol={subvol.name}']
)
def generate_key_files(self):
for part_mod in self._disk_encryption.partitions:
@ -382,37 +393,6 @@ class Installer:
for entry in self._fstab_entries:
fp.write(f'{entry}\n')
for mod in self._disk_config.device_modifications:
for part_mod in mod.partitions:
if part_mod.fs_type != disk.FilesystemType.Btrfs:
continue
with fstab_path.open('r') as fp:
fstab = fp.readlines()
# Replace the {installation}/etc/fstab with entries
# using the compress=zstd where the mountpoint has compression set.
for index, line in enumerate(fstab):
# So first we grab the mount options by using subvol=.*? as a locator.
# And we also grab the mountpoint for the entry, for instance /var/log
subvoldef = re.findall(',.*?subvol=.*?[\t ]', line)
mountpoint = re.findall('[\t ]/.*?[\t ]', line)
if not subvoldef or not mountpoint:
continue
for sub_vol in part_mod.btrfs_subvols:
# We then locate the correct subvolume and check if it's compressed,
# and skip entries where compression is already defined
# We then sneak in the compress=zstd option if it doesn't already exist:
if sub_vol.compress and str(sub_vol.mountpoint) == Path(
mountpoint[0].strip()) and ',compress=zstd,' not in line:
fstab[index] = line.replace(subvoldef[0], f',compress=zstd{subvoldef[0]}')
break
with fstab_path.open('w') as fp:
fp.writelines(fstab)
def set_hostname(self, hostname: str, *args: str, **kwargs: str) -> None:
with open(f'{self.target}/etc/hostname', 'w') as fh:
fh.write(hostname + '\n')

View File

@ -5,6 +5,7 @@ from typing import Any, TYPE_CHECKING
from typing import Optional, List, Tuple
from .. import disk
from ..disk.device_model import BtrfsMountOption
from ..hardware import SysInfo
from ..menu import Menu
from ..menu import TableMenu
@ -214,6 +215,20 @@ def select_main_filesystem_format(advanced_options=False) -> disk.FilesystemType
return options[choice.single_value]
def select_mount_options() -> List[str]:
prompt = str(_('Would you like to use compression or disable CoW?'))
options = [str(_('Use compression')), str(_('Disable Copy-on-Write'))]
choice = Menu(prompt, options, sort=False).run()
if choice.type_ == MenuSelectionType.Selection:
if choice.single_value == options[0]:
return [BtrfsMountOption.compress.value]
else:
return [BtrfsMountOption.nodatacow.value]
return []
def suggest_single_disk_layout(
device: disk.BDevice,
filesystem_type: Optional[disk.FilesystemType] = None,
@ -228,7 +243,7 @@ def suggest_single_disk_layout(
root_partition_size = disk.Size(20, disk.Unit.GiB, sector_size)
using_subvolumes = False
using_home_partition = False
compression = False
mount_options = []
device_size_gib = device.device_info.total_size
if filesystem_type == disk.FilesystemType.Btrfs:
@ -236,9 +251,7 @@ def suggest_single_disk_layout(
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
using_subvolumes = choice.value == Menu.yes()
prompt = str(_('Would you like to use BTRFS compression?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
compression = choice.value == Menu.yes()
mount_options = select_mount_options()
device_modification = disk.DeviceModification(device, wipe=True)
@ -290,7 +303,7 @@ def suggest_single_disk_layout(
length=root_length,
mountpoint=Path('/') if not using_subvolumes else None,
fs_type=filesystem_type,
mount_options=['compress=zstd'] if compression else [],
mount_options=mount_options
)
device_modification.add_partition(root_partition)
@ -323,7 +336,7 @@ def suggest_single_disk_layout(
length=home_length,
mountpoint=Path('/home'),
fs_type=filesystem_type,
mount_options=['compress=zstd'] if compression else []
mount_options=mount_options
)
device_modification.add_partition(home_partition)
@ -344,7 +357,7 @@ def suggest_multi_disk_layout(
min_home_partition_size = disk.Size(40, disk.Unit.GiB, disk.SectorSize.default())
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
desired_root_partition_size = disk.Size(20, disk.Unit.GiB, disk.SectorSize.default())
compression = False
mount_options = []
if not filesystem_type:
filesystem_type = select_main_filesystem_format(advanced_options)
@ -371,9 +384,7 @@ def suggest_multi_disk_layout(
return []
if filesystem_type == disk.FilesystemType.Btrfs:
prompt = str(_('Would you like to use BTRFS compression?'))
choice = Menu(prompt, Menu.yes_no(), skip=False, default_option=Menu.yes()).run()
compression = choice.value == Menu.yes()
mount_options = select_mount_options()
device_paths = ', '.join([str(d.device_info.path) for d in devices])
@ -409,7 +420,7 @@ def suggest_multi_disk_layout(
start=root_start,
length=root_length,
mountpoint=Path('/'),
mount_options=['compress=zstd'] if compression else [],
mount_options=mount_options,
fs_type=filesystem_type
)
root_device_modification.add_partition(root_partition)
@ -427,7 +438,7 @@ def suggest_multi_disk_layout(
start=home_start,
length=home_length,
mountpoint=Path('/home'),
mount_options=['compress=zstd'] if compression else [],
mount_options=mount_options,
fs_type=filesystem_type,
)
home_device_modification.add_partition(home_partition)

View File

@ -186,34 +186,24 @@ This example contains both subvolumes and compression.
{
"btrfs": [
{
"compress": false,
"mountpoint": "/",
"name": "@",
"nodatacow": false
},
{
"compress": false,
"mountpoint": "/home",
"name": "@home",
"nodatacow": false
},
{
"compress": false,
"mountpoint": "/var/log",
"name": "@log",
"nodatacow": false
},
{
"compress": false,
"mountpoint": "/var/cache/pacman/pkg",
"name": "@pkg",
"nodatacow": false
},
{
"compress": false,
"mountpoint": "/.snapshots",
"name": "@.snapshots",
"nodatacow": false
}
],
"dev_path": null,