Update the subvolume menu - fix for #1278 (#1297)

* Update subvolume

* Add mypy compliance

Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com>
Co-authored-by: Anton Hvornum <anton@hvornum.se>
This commit is contained in:
Daniel Girtler 2022-06-07 01:28:46 +10:00 committed by GitHub
parent 2d4b262046
commit a7ca037a26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 269 additions and 428 deletions

View File

@ -15,4 +15,4 @@ jobs:
# one day this will be enabled # one day this will be enabled
# run: mypy --strict --module archinstall || exit 0 # run: mypy --strict --module archinstall || exit 0
- name: run mypy - name: run mypy
run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/translation.py run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py archinstall/lib/user_interaction/subvolume_config.py archinstall/lib/disk/btrfs/btrfs_helpers.py archinstall/lib/translation.py

View File

@ -226,8 +226,6 @@ def post_process_arguments(arguments):
load_plugin(arguments['plugin']) load_plugin(arguments['plugin'])
if arguments.get('disk_layouts', None) is not None: if arguments.get('disk_layouts', None) is not None:
# if 'disk_layouts' not in storage:
# storage['disk_layouts'] = {}
layout_storage = {} layout_storage = {}
if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],layout_storage): if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],layout_storage):
exit(1) exit(1)
@ -236,10 +234,12 @@ def post_process_arguments(arguments):
arguments['harddrives'] = [disk for disk in layout_storage] arguments['harddrives'] = [disk for disk in layout_storage]
# backward compatibility. Change partition.format for partition.wipe # backward compatibility. Change partition.format for partition.wipe
for disk in layout_storage: for disk in layout_storage:
for i,partition in enumerate(layout_storage[disk].get('partitions',[])): for i, partition in enumerate(layout_storage[disk].get('partitions',[])):
if 'format' in partition: if 'format' in partition:
partition['wipe'] = partition['format'] partition['wipe'] = partition['format']
del partition['format'] del partition['format']
elif 'btrfs' in partition:
partition['btrfs']['subvolumes'] = Subvolume.parse_arguments(partition['btrfs']['subvolumes'])
arguments['disk_layouts'] = layout_storage arguments['disk_layouts'] = layout_storage
load_config() load_config()

View File

@ -2,8 +2,7 @@ from __future__ import annotations
import pathlib import pathlib
import glob import glob
import logging import logging
import re from typing import Union, Dict, TYPE_CHECKING
from typing import Union, Dict, TYPE_CHECKING, Any, Iterator
# https://stackoverflow.com/a/39757388/929999 # https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING: if TYPE_CHECKING:
@ -15,30 +14,15 @@ from .btrfs_helpers import (
setup_subvolumes as setup_subvolumes, setup_subvolumes as setup_subvolumes,
mount_subvolume as mount_subvolume mount_subvolume as mount_subvolume
) )
from .btrfssubvolume import BtrfsSubvolume as BtrfsSubvolume from .btrfssubvolumeinfo import BtrfsSubvolumeInfo as BtrfsSubvolume
from .btrfspartition import BTRFSPartition as BTRFSPartition from .btrfspartition import BTRFSPartition as BTRFSPartition
from ..helpers import get_mount_info
from ...exceptions import DiskError, Deprecated from ...exceptions import DiskError, Deprecated
from ...general import SysCommand from ...general import SysCommand
from ...output import log from ...output import log
from ...exceptions import SysCallError
def get_subvolume_info(path :pathlib.Path) -> Dict[str, Any]:
try:
output = SysCommand(f"btrfs subvol show {path}").decode()
except SysCallError as error:
print('Error:', error)
result = {} def create_subvolume(installation: Installer, subvolume_location :Union[pathlib.Path, str]) -> bool:
for line in output.replace('\r\n', '\n').split('\n'):
if ':' in line:
key, val = line.replace('\t', '').split(':', 1)
result[key.strip().lower().replace(' ', '_')] = val.strip()
return result
def create_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str]) -> bool:
""" """
This function uses btrfs to create a subvolume. This function uses btrfs to create a subvolume.
@ -71,112 +55,6 @@ def create_subvolume(installation :Installer, subvolume_location :Union[pathlib.
if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0:
raise DiskError(f"Could not create a subvolume at {target}: {cmd}") raise DiskError(f"Could not create a subvolume at {target}: {cmd}")
def _has_option(option :str,options :list) -> bool:
""" auxiliary routine to check if an option is present in a list.
we check if the string appears in one of the options, 'cause it can appear in several forms (option, option=val,...)
"""
if not options:
return False
for item in options:
if option in item:
return True
return False
def manage_btrfs_subvolumes(installation :Installer,
partition :Dict[str, str],) -> list:
def manage_btrfs_subvolumes(installation :Installer, partition :Dict[str, str]) -> list:
raise Deprecated("Use setup_subvolumes() instead.") raise Deprecated("Use setup_subvolumes() instead.")
from copy import deepcopy
""" we do the magic with subvolumes in a centralized place
parameters:
* the installation object
* the partition dictionary entry which represents the physical partition
returns
* mountpoinst, the list which contains all the "new" partititon to be mounted
We expect the partition has been mounted as / , and it to be unmounted after the processing
Then we create all the subvolumes inside btrfs as demand
We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs
Then we return a list of "new" partitions to be processed as "normal" partitions
# TODO For encrypted devices we need some special processing prior to it
"""
# We process each of the pairs <subvolume name: mount point | None | mount info dict>
# th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list
# of mount options (or similar used by brtfs)
mountpoints = []
subvolumes = partition['btrfs']['subvolumes']
for name, right_hand in subvolumes.items():
try:
# we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use
if name.startswith('/'):
name = name[1:]
# renormalize the right hand.
location = None
subvol_options = []
# no contents, so it is not to be mounted
if not right_hand:
location = None
# just a string. per backward compatibility the mount point
elif isinstance(right_hand,str):
location = right_hand
# a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿?
elif isinstance(right_hand,dict):
location = right_hand.get('mountpoint',None)
subvol_options = right_hand.get('options',[])
# we create the subvolume
create_subvolume(installation,name)
# Make the nodatacow processing now
# It will be the main cause of creation of subvolumes which are not to be mounted
# it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new
if 'nodatacow' in subvol_options:
if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so nodatacow doesn't propagate to the mount options
del subvol_options[subvol_options.index('nodatacow')]
# Make the compress processing now
# it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new
# in this way only zstd compression is activaded
# TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated
if 'compress' in subvol_options:
if not _has_option('compress',partition.get('filesystem',{}).get('mount_options',[])):
if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so compress doesn't propagate to the mount options
del subvol_options[subvol_options.index('compress')]
# END compress processing.
# we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume
if not partition['mountpoint'] and location is not None:
# we begin to create a fake partition entry. First we copy the original -the one that corresponds to
# the primary partition. We make a deepcopy to avoid altering the original content in any case
fake_partition = deepcopy(partition)
# we start to modify entries in the "fake partition" to match the needs of the subvolumes
# to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy
del fake_partition['btrfs']
fake_partition['encrypted'] = False
fake_partition['generate-encryption-key-file'] = False
# Mount destination. As of now the right hand part
fake_partition['mountpoint'] = location
# we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path.
fake_partition['subvolume'] = name
# here we add the special mount options for the subvolume, if any.
# if the original partition['options'] is not a list might give trouble
if fake_partition.get('filesystem',{}).get('mount_options',[]):
fake_partition['filesystem']['mount_options'].extend(subvol_options)
else:
fake_partition['filesystem']['mount_options'] = subvol_options
# Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer.
# As the rest will query there the path of the "partition" to be mounted, we feed it with the bind name needed to mount subvolumes
# As we made a deepcopy we have a fresh instance of this object we can manipulate problemless
fake_partition['device_instance'].path = f"{partition['device_instance'].path}[/{name}]"
# Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted,
# as "normal" ones
mountpoints.append(fake_partition)
except Exception as e:
raise e
return mountpoints

View File

@ -1,72 +1,42 @@
import pathlib
import logging import logging
from typing import Optional from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING
from ...models.subvolume import Subvolume
from ...exceptions import SysCallError, DiskError from ...exceptions import SysCallError, DiskError
from ...general import SysCommand from ...general import SysCommand
from ...output import log from ...output import log
from ..helpers import get_mount_info from ..helpers import get_mount_info
from .btrfssubvolume import BtrfsSubvolume from .btrfssubvolumeinfo import BtrfsSubvolumeInfo
if TYPE_CHECKING:
from .btrfspartition import BTRFSPartition
from ...installer import Installer
def mount_subvolume(installation, device, name, subvolume_information): def mount_subvolume(installation: 'Installer', device: 'BTRFSPartition', subvolume: Subvolume):
# we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. # we normalize the subvolume name (getting rid of slash at the start if exists.
# In our implementation has no semantic load.
# Every subvolume is created from the top of the hierarchy- and simplifies its further use # Every subvolume is created from the top of the hierarchy- and simplifies its further use
name = name.lstrip('/') name = subvolume.name.lstrip('/')
mountpoint = Path(subvolume.mountpoint)
# renormalize the right hand. installation_target = Path(installation.target)
mountpoint = subvolume_information.get('mountpoint', None)
if not mountpoint:
return None
if type(mountpoint) == str:
mountpoint = pathlib.Path(mountpoint)
installation_target = installation.target
if type(installation_target) == str:
installation_target = pathlib.Path(installation_target)
mountpoint = installation_target / mountpoint.relative_to(mountpoint.anchor) mountpoint = installation_target / mountpoint.relative_to(mountpoint.anchor)
mountpoint.mkdir(parents=True, exist_ok=True) mountpoint.mkdir(parents=True, exist_ok=True)
mount_options = subvolume.options + [f'subvol={name}']
mount_options = subvolume_information.get('options', [])
if not any('subvol=' in x for x in mount_options):
mount_options += [f'subvol={name}']
log(f"Mounting subvolume {name} on {device} to {mountpoint}", level=logging.INFO, fg="gray") log(f"Mounting subvolume {name} on {device} to {mountpoint}", level=logging.INFO, fg="gray")
SysCommand(f"mount {device.path} {mountpoint} -o {','.join(mount_options)}") SysCommand(f"mount {device.path} {mountpoint} -o {','.join(mount_options)}")
def setup_subvolumes(installation, partition_dict): def setup_subvolumes(installation: 'Installer', partition_dict: Dict[str, Any]):
"""
Taken from: ..user_guides.py
partition['btrfs'] = {
"subvolumes" : {
"@": "/",
"@home": "/home",
"@log": "/var/log",
"@pkg": "/var/cache/pacman/pkg",
"@.snapshots": "/.snapshots"
}
}
"""
log(f"Setting up subvolumes: {partition_dict['btrfs']['subvolumes']}", level=logging.INFO, fg="gray") log(f"Setting up subvolumes: {partition_dict['btrfs']['subvolumes']}", level=logging.INFO, fg="gray")
for name, right_hand in partition_dict['btrfs']['subvolumes'].items():
for subvolume in partition_dict['btrfs']['subvolumes']:
# we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load. # we normalize the subvolume name (getting rid of slash at the start if exists. In our implementation has no semantic load.
# Every subvolume is created from the top of the hierarchy- and simplifies its further use # Every subvolume is created from the top of the hierarchy- and simplifies its further use
name = name.lstrip('/') name = subvolume.name.lstrip('/')
# renormalize the right hand.
# mountpoint = None
subvol_options = []
match right_hand:
# case str(): # backwards-compatability
# mountpoint = right_hand
case dict():
# mountpoint = right_hand.get('mountpoint', None)
subvol_options = right_hand.get('options', [])
# We create the subvolume using the BTRFSPartition instance. # We create the subvolume using the BTRFSPartition instance.
# That way we ensure not only easy access, but also accurate mount locations etc. # That way we ensure not only easy access, but also accurate mount locations etc.
@ -76,27 +46,25 @@ def setup_subvolumes(installation, partition_dict):
# It will be the main cause of creation of subvolumes which are not to be mounted # It will be the main cause of creation of subvolumes which are not to be mounted
# it is not an options which can be established by subvolume (but for whole file systems), and can be # it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
if 'nodatacow' in subvol_options: if subvolume.nodatacow:
if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so nodatacow doesn't propagate to the mount options
del subvol_options[subvol_options.index('nodatacow')]
# Make the compress processing now # Make the compress processing now
# it is not an options which can be established by subvolume (but for whole file systems), and can be # it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
# in this way only zstd compression is activaded # in this way only zstd compression is activaded
# TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated
if 'compress' in subvol_options: if subvolume.compress:
if not any(['compress' in filesystem_option for filesystem_option in partition_dict.get('filesystem', {}).get('mount_options', [])]): if not any(['compress' in filesystem_option for filesystem_option in partition_dict.get('filesystem', {}).get('mount_options', [])]):
if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so compress doesn't propagate to the mount options
del subvol_options[subvol_options.index('compress')]
def subvolume_info_from_path(path :pathlib.Path) -> Optional[BtrfsSubvolume]:
def subvolume_info_from_path(path: Path) -> Optional[BtrfsSubvolumeInfo]:
try: try:
subvolume_name = None subvolume_name = ''
result = {} result = {}
for index, line in enumerate(SysCommand(f"btrfs subvolume show {path}")): for index, line in enumerate(SysCommand(f"btrfs subvolume show {path}")):
if index == 0: if index == 0:
@ -110,14 +78,14 @@ def subvolume_info_from_path(path :pathlib.Path) -> Optional[BtrfsSubvolume]:
# allows for hooking in a pre-processor to do this we have to do it here: # allows for hooking in a pre-processor to do this we have to do it here:
result[key.lower().replace(' ', '_').replace('(s)', 's')] = value.strip() result[key.lower().replace(' ', '_').replace('(s)', 's')] = value.strip()
return BtrfsSubvolume(**{'full_path' : path, 'name' : subvolume_name, **result}) return BtrfsSubvolumeInfo(**{'full_path' : path, 'name' : subvolume_name, **result}) # type: ignore
except SysCallError as error: except SysCallError as error:
log(f"Could not retrieve subvolume information from {path}: {error}", level=logging.WARNING, fg="orange") log(f"Could not retrieve subvolume information from {path}: {error}", level=logging.WARNING, fg="orange")
return None return None
def find_parent_subvolume(path :pathlib.Path, filters=[]):
def find_parent_subvolume(path: Path, filters=[]) -> Optional[BtrfsSubvolumeInfo]:
# A root path cannot have a parent # A root path cannot have a parent
if str(path) == '/': if str(path) == '/':
return None return None
@ -127,6 +95,8 @@ def find_parent_subvolume(path :pathlib.Path, filters=[]):
if found_mount['target'] == '/': if found_mount['target'] == '/':
return None return None
return find_parent_subvolume(path.parent, traverse=True, filters=[*filters, found_mount['target']]) return find_parent_subvolume(path.parent, filters=[*filters, found_mount['target']])
return subvolume return subvolume
return None

View File

@ -15,7 +15,7 @@ from .btrfs_helpers import (
if TYPE_CHECKING: if TYPE_CHECKING:
from ...installer import Installer from ...installer import Installer
from .btrfssubvolume import BtrfsSubvolume from .btrfssubvolumeinfo import BtrfsSubvolumeInfo
class BTRFSPartition(Partition): class BTRFSPartition(Partition):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -50,7 +50,7 @@ class BTRFSPartition(Partition):
for child in iterate_children(filesystem): for child in iterate_children(filesystem):
yield child yield child
def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolume': def create_subvolume(self, subvolume :pathlib.Path, installation :Optional['Installer'] = None) -> 'BtrfsSubvolumeInfo':
""" """
Subvolumes have to be created within a mountpoint. Subvolumes have to be created within a mountpoint.
This means we need to get the current installation target. This means we need to get the current installation target.

View File

@ -16,8 +16,9 @@ from ...general import SysCommand
from ...output import log from ...output import log
from ...storage import storage from ...storage import storage
@dataclass @dataclass
class BtrfsSubvolume: class BtrfsSubvolumeInfo:
full_path :pathlib.Path full_path :pathlib.Path
name :str name :str
uuid :str uuid :str

View File

@ -8,6 +8,8 @@ import time
import glob import glob
from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999 # https://stackoverflow.com/a/39757388/929999
from ..models.subvolume import Subvolume
if TYPE_CHECKING: if TYPE_CHECKING:
from .partition import Partition from .partition import Partition
@ -469,6 +471,7 @@ def convert_device_to_uuid(path :str) -> str:
raise DiskError(f"Could not retrieve the UUID of {path} within a timely manner.") raise DiskError(f"Could not retrieve the UUID of {path} within a timely manner.")
def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, strict: bool = True) -> bool: def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, strict: bool = True) -> bool:
""" Determine if a certain partition is mounted (or has a mountpoint) as specific target (path) """ Determine if a certain partition is mounted (or has a mountpoint) as specific target (path)
Coded for clarity rather than performance Coded for clarity rather than performance
@ -485,10 +488,12 @@ def has_mountpoint(partition: Union[dict,Partition,MapperDev], target: str, stri
""" """
# we create the mountpoint list # we create the mountpoint list
if isinstance(partition,dict): if isinstance(partition,dict):
subvols = partition.get('btrfs',{}).get('subvolumes',{}) subvolumes: List[Subvolume] = partition.get('btrfs',{}).get('subvolumes', [])
mountpoints = [partition.get('mountpoint'),] + [subvols[subvol] if isinstance(subvols[subvol],str) or not subvols[subvol] else subvols[subvol].get('mountpoint') for subvol in subvols] mountpoints = [partition.get('mountpoint')]
mountpoints += [volume.mountpoint for volume in subvolumes]
else: else:
mountpoints = [partition.mountpoint,] + [subvol.target for subvol in partition.subvolumes] mountpoints = [partition.mountpoint,] + [subvol.target for subvol in partition.subvolumes]
# we check # we check
if strict or target == '/': if strict or target == '/':
if target in mountpoints: if target in mountpoints:

View File

@ -10,7 +10,7 @@ from ..general import SysCommand
from ..output import log from ..output import log
if TYPE_CHECKING: if TYPE_CHECKING:
from .btrfs import BtrfsSubvolume from .btrfs import BtrfsSubvolumeInfo
@dataclass @dataclass
class MapperDev: class MapperDev:
@ -75,7 +75,7 @@ class MapperDev:
return get_filesystem_type(self.path) return get_filesystem_type(self.path)
@property @property
def subvolumes(self) -> Iterator['BtrfsSubvolume']: def subvolumes(self) -> Iterator['BtrfsSubvolumeInfo']:
from .btrfs import subvolume_info_from_path from .btrfs import subvolume_info_from_path
for mountpoint in self.mount_information: for mountpoint in self.mount_information:

View File

@ -14,7 +14,7 @@ from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from ..output import log from ..output import log
from ..general import SysCommand from ..general import SysCommand
from .btrfs.btrfs_helpers import subvolume_info_from_path from .btrfs.btrfs_helpers import subvolume_info_from_path
from .btrfs.btrfssubvolume import BtrfsSubvolume from .btrfs.btrfssubvolumeinfo import BtrfsSubvolumeInfo
class Partition: class Partition:
def __init__(self, def __init__(self,
@ -294,7 +294,7 @@ class Partition:
return bind_name return bind_name
@property @property
def subvolumes(self) -> Iterator[BtrfsSubvolume]: def subvolumes(self) -> Iterator[BtrfsSubvolumeInfo]:
from .helpers import findmnt from .helpers import findmnt
def iterate_children_recursively(information): def iterate_children_recursively(information):

View File

@ -3,6 +3,8 @@ import logging
from typing import Optional, Dict, Any, List, TYPE_CHECKING from typing import Optional, Dict, Any, List, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999 # https://stackoverflow.com/a/39757388/929999
from ..models.subvolume import Subvolume
if TYPE_CHECKING: if TYPE_CHECKING:
from .blockdevice import BlockDevice from .blockdevice import BlockDevice
_: Any _: Any
@ -107,17 +109,14 @@ def suggest_single_disk_layout(block_device :BlockDevice,
# https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash
# https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh
layout[block_device.path]['partitions'][1]['btrfs'] = { layout[block_device.path]['partitions'][1]['btrfs'] = {
"subvolumes" : { 'subvolumes': [
"@":"/", Subvolume('@', '/'),
"@home": "/home", Subvolume('@home', '/home'),
"@log": "/var/log", Subvolume('@log', '/var/log'),
"@pkg": "/var/cache/pacman/pkg", Subvolume('@pkg', '/var/cache/pacman/pkg'),
"@.snapshots": "/.snapshots" Subvolume('@.snapshots', '/.snapshots')
} ]
} }
# else:
# pass # ... implement a guided setup
elif using_home_partition: elif using_home_partition:
# If we don't want to use subvolumes, # If we don't want to use subvolumes,
# But we want to be able to re-use data between re-installs.. # But we want to be able to re-use data between re-installs..

View File

@ -24,6 +24,7 @@ from .disk.partition import get_mount_fs_type
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
from .hsm import fido2_enroll from .hsm import fido2_enroll
from .models.users import User from .models.users import User
from .models.subvolume import Subvolume
if TYPE_CHECKING: if TYPE_CHECKING:
_: Any _: Any
@ -263,47 +264,25 @@ class Installer:
hsm_device_path = storage['arguments']['HSM'] hsm_device_path = storage['arguments']['HSM']
fido2_enroll(hsm_device_path, partition['device_instance'], password) fido2_enroll(hsm_device_path, partition['device_instance'], password)
# we manage the btrfs partitions btrfs_subvolumes = [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', [])]
if any(btrfs_subvolumes := [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]):
for partition in btrfs_subvolumes:
if mount_options := ','.join(partition.get('filesystem',{}).get('mount_options',[])):
self.mount(partition['device_instance'], "/", options=mount_options)
else:
self.mount(partition['device_instance'], "/")
setup_subvolumes( for partition in btrfs_subvolumes:
installation=self, device_instance = partition['device_instance']
partition_dict=partition mount_options = partition.get('filesystem', {}).get('mount_options', [])
) self.mount(device_instance, "/", options=','.join(mount_options))
setup_subvolumes(installation=self, partition_dict=partition)
partition['device_instance'].unmount() device_instance.unmount()
# We then handle any special cases, such as btrfs # We then handle any special cases, such as btrfs
if any(btrfs_subvolumes := [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]): for partition in btrfs_subvolumes:
for partition_information in btrfs_subvolumes: subvolumes: List[Subvolume] = partition['btrfs']['subvolumes']
for name, mountpoint in sorted(partition_information['btrfs']['subvolumes'].items(), key=lambda item: item[1]): for subvolume in sorted(subvolumes, key=lambda item: item.mountpoint):
btrfs_subvolume_information = {} # We cache the mount call for later
mount_queue[subvolume.mountpoint] = lambda sub_vol=subvolume, device=partition['device_instance']: mount_subvolume(
match mountpoint: installation=self,
case str(): # backwards-compatability device=device,
btrfs_subvolume_information['mountpoint'] = mountpoint subvolume=sub_vol
btrfs_subvolume_information['options'] = [] )
case dict():
btrfs_subvolume_information['mountpoint'] = mountpoint.get('mountpoint', None)
btrfs_subvolume_information['options'] = mountpoint.get('options', [])
case _:
continue
if mountpoint_parsed := btrfs_subvolume_information.get('mountpoint'):
# We cache the mount call for later
mount_queue[mountpoint_parsed] = lambda device=partition_information['device_instance'], \
name=name, \
subvolume_information=btrfs_subvolume_information: mount_subvolume(
installation=self,
device=device,
name=name,
subvolume_information=subvolume_information
)
# We mount ordinary partitions, and we sort them by the mountpoint # We mount ordinary partitions, and we sort them by the mountpoint
for partition in sorted([entry for entry in list_part if entry.get('mountpoint', False)], key=lambda part: part['mountpoint']): for partition in sorted([entry for entry in list_part if entry.get('mountpoint', False)], key=lambda part: part['mountpoint']):

View File

@ -325,22 +325,23 @@ class GlobalMenu(GeneralMenu):
def _select_harddrives(self, old_harddrives : list) -> List: def _select_harddrives(self, old_harddrives : list) -> List:
harddrives = select_harddrives(old_harddrives) harddrives = select_harddrives(old_harddrives)
if len(harddrives) == 0: if harddrives:
prompt = _( if len(harddrives) == 0:
"You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n" prompt = _(
"WARNING: Archinstall won't check the suitability of this setup\n" "You decided to skip harddrive selection\nand will use whatever drive-setup is mounted at {} (experimental)\n"
"Do you wish to continue?" "WARNING: Archinstall won't check the suitability of this setup\n"
).format(storage['MOUNT_POINT']) "Do you wish to continue?"
).format(storage['MOUNT_POINT'])
choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run() choice = Menu(prompt, Menu.yes_no(), default_option=Menu.yes(), skip=False).run()
if choice.value == Menu.no(): if choice.value == Menu.no():
return self._select_harddrives(old_harddrives) return self._select_harddrives(old_harddrives)
# in case the harddrives got changed we have to reset the disk layout as well # in case the harddrives got changed we have to reset the disk layout as well
if old_harddrives != harddrives: if old_harddrives != harddrives:
self._menu_options['disk_layouts'].set_current_selection(None) self._menu_options['disk_layouts'].set_current_selection(None)
storage['arguments']['disk_layouts'] = {} storage['arguments']['disk_layouts'] = {}
return harddrives return harddrives

View File

@ -137,34 +137,35 @@ class ListManager:
else: else:
self._default_action = [str(default_action),] self._default_action = [str(default_action),]
self.header = header if header else None self._header = header if header else None
self.cancel_action = str(_('Cancel')) self._cancel_action = str(_('Cancel'))
self.confirm_action = str(_('Confirm and exit')) self._confirm_action = str(_('Confirm and exit'))
self.separator = '' self._separator = ''
self.bottom_list = [self.confirm_action,self.cancel_action] self._bottom_list = [self._confirm_action, self._cancel_action]
self.bottom_item = [self.cancel_action] self._bottom_item = [self._cancel_action]
self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))] self._base_actions = base_actions if base_actions else [str(_('Add')), str(_('Copy')), str(_('Edit')), str(_('Delete'))]
self._original_data = copy.deepcopy(base_list) self._original_data = copy.deepcopy(base_list)
self._data = copy.deepcopy(base_list) # as refs, changes are immediate self._data = copy.deepcopy(base_list) # as refs, changes are immediate
# default values for the null case # default values for the null case
self.target: Optional[Any] = None self.target: Optional[Any] = None
self.action = self._null_action self.action = self._null_action
if len(self._data) == 0 and self._null_action:
self._data = self.exec_action(self._data)
def run(self): def run(self):
while True: while True:
# this will return a dictionary with the key as the menu entry to be displayed # this will return a dictionary with the key as the menu entry to be displayed
# and the value is the original value from the self._data container # and the value is the original value from the self._data container
data_formatted = self.reformat(self._data) data_formatted = self.reformat(self._data)
options = list(data_formatted.keys()) options = list(data_formatted.keys())
options.append(self.separator)
if len(options) > 0:
options.append(self._separator)
if self._default_action: if self._default_action:
options += self._default_action options += self._default_action
options += self.bottom_list options += self._bottom_list
system('clear') system('clear')
@ -174,12 +175,12 @@ class ListManager:
sort=False, sort=False,
clear_screen=False, clear_screen=False,
clear_menu_on_exit=False, clear_menu_on_exit=False,
header=self.header, header=self._header,
skip_empty_entries=True, skip_empty_entries=True,
skip=False skip=False
).run() ).run()
if not target.value or target.value in self.bottom_list: if not target.value or target.value in self._bottom_list:
self.action = target self.action = target
break break
@ -201,13 +202,13 @@ class ListManager:
# Possible enhancement. If run_actions returns false a message line indicating the failure # Possible enhancement. If run_actions returns false a message line indicating the failure
self.run_actions(target.value) self.run_actions(target.value)
if target.value == self.cancel_action: # TODO dubious if target.value == self._cancel_action: # TODO dubious
return self._original_data # return the original list return self._original_data # return the original list
else: else:
return self._data return self._data
def run_actions(self,prompt_data=None): def run_actions(self,prompt_data=None):
options = self.action_list() + self.bottom_item options = self.action_list() + self._bottom_item
prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target) prompt = _("Select an action for < {} >").format(prompt_data if prompt_data else self.target)
choice = Menu( choice = Menu(
prompt, prompt,
@ -215,13 +216,13 @@ class ListManager:
sort=False, sort=False,
clear_screen=False, clear_screen=False,
clear_menu_on_exit=False, clear_menu_on_exit=False,
preset_values=self.bottom_item, preset_values=self._bottom_item,
show_search_hint=False show_search_hint=False
).run() ).run()
self.action = choice.value self.action = choice.value
if self.action and self.action != self.cancel_action: if self.action and self.action != self._cancel_action:
self._data = self.exec_action(self._data) self._data = self.exec_action(self._data)
""" """
@ -243,7 +244,7 @@ class ListManager:
can define alternate action list or customize the list for each item. can define alternate action list or customize the list for each item.
Executed after any item is selected, contained in self.target Executed after any item is selected, contained in self.target
""" """
return self.base_actions return self._base_actions
def exec_action(self, data: Any): def exec_action(self, data: Any):
""" """

View File

@ -0,0 +1,68 @@
from dataclasses import dataclass
from typing import List, Any, Dict
@dataclass
class Subvolume:
name: str
mountpoint: str
compress: bool = False
nodatacow: bool = False
def display(self) -> str:
options_str = ','.join(self.options)
return f'{_("Subvolume")}: {self.name:15} {_("Mountpoint")}: {self.mountpoint:20} {_("Options")}: {options_str}'
@property
def options(self) -> List[str]:
options = [
'compress' if self.compress else '',
'nodatacow' if self.nodatacow else ''
]
return [o for o in options if len(o)]
def json(self) -> Dict[str, Any]:
return {
'name': self.name,
'mountpoint': self.mountpoint,
'compress': self.compress,
'nodatacow': self.nodatacow
}
@classmethod
def _parse(cls, config_subvolumes: List[Dict[str, Any]]) -> List['Subvolume']:
subvolumes = []
for entry in config_subvolumes:
if not entry.get('name', None) or not entry.get('mountpoint', None):
continue
subvolumes.append(
Subvolume(
entry['name'],
entry['mountpoint'],
entry.get('compress', False),
entry.get('nodatacow', False)
)
)
return subvolumes
@classmethod
def _parse_backwards_compatible(cls, config_subvolumes) -> List['Subvolume']:
subvolumes = []
for name, mountpoint in config_subvolumes.items():
if not name or not mountpoint:
continue
subvolumes.append(Subvolume(name, mountpoint))
return subvolumes
@classmethod
def parse_arguments(cls, config_subvolumes: Any) -> List['Subvolume']:
if isinstance(config_subvolumes, list):
return cls._parse(config_subvolumes)
elif isinstance(config_subvolumes, dict):
return cls._parse_backwards_compatible(config_subvolumes)
raise ValueError('Unknown disk layout btrfs subvolume format')

View File

@ -27,8 +27,10 @@ class User:
} }
def display(self) -> str: def display(self) -> str:
strength = PasswordStrength.strength(self.password) password = '*' * (len(self.password) if self.password else 0)
password = '*' * len(self.password) + f' ({strength.value})' if password:
strength = PasswordStrength.strength(self.password)
password += f' ({strength.value})'
return f'{_("Username")}: {self.username:16} {_("Password")}: {password:20} sudo: {str(self.sudo)}' return f'{_("Username")}: {self.username:16} {_("Password")}: {password:20} sudo: {str(self.sudo)}'
@classmethod @classmethod

View File

@ -351,18 +351,16 @@ def manage_new_and_existing_partitions(block_device: 'BlockDevice') -> Dict[str,
if partition is not None: if partition is not None:
if not block_device_struct["partitions"][partition].get('btrfs', {}): if not block_device_struct["partitions"][partition].get('btrfs', {}):
block_device_struct["partitions"][partition]['btrfs'] = {} block_device_struct["partitions"][partition]['btrfs'] = {}
if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', {}): if not block_device_struct["partitions"][partition]['btrfs'].get('subvolumes', []):
block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = {} block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = []
prev = block_device_struct["partitions"][partition]['btrfs']['subvolumes'] prev = block_device_struct["partitions"][partition]['btrfs']['subvolumes']
result = SubvolumeList(_("Manage btrfs subvolumes for current partition"),prev).run() result = SubvolumeList(_("Manage btrfs subvolumes for current partition"), prev).run()
if result: block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result
block_device_struct["partitions"][partition]['btrfs']['subvolumes'] = result
else:
del block_device_struct["partitions"][partition]['btrfs']
return block_device_struct return block_device_struct
def select_encrypted_partitions( def select_encrypted_partitions(
title :str, title :str,
partitions :List[Partition], partitions :List[Partition],

View File

@ -1,155 +1,94 @@
from typing import Dict, List from typing import Dict, List, Optional, Any, TYPE_CHECKING
from ..menu.list_manager import ListManager from ..menu.list_manager import ListManager
from ..menu.menu import MenuSelectionType from ..menu.menu import MenuSelectionType
from ..menu.selection_menu import Selector, GeneralMenu
from ..menu.text_input import TextInput from ..menu.text_input import TextInput
from ..menu import Menu from ..menu import Menu
from ..models.subvolume import Subvolume
if TYPE_CHECKING:
_: Any
"""
UI classes
"""
class SubvolumeList(ListManager): class SubvolumeList(ListManager):
def __init__(self,prompt,list): def __init__(self, prompt: str, current_volumes: List[Subvolume]):
self.ObjectNullAction = None # str(_('Add')) self._actions = [
self.ObjectDefaultAction = str(_('Add')) str(_('Add subvolume')),
super().__init__(prompt,list,None,self.ObjectNullAction,self.ObjectDefaultAction) str(_('Edit subvolume')),
str(_('Delete subvolume'))
]
super().__init__(prompt, current_volumes, self._actions, self._actions[0])
def reformat(self, data: Dict) -> Dict: def reformat(self, data: List[Subvolume]) -> Dict[str, Subvolume]:
def presentation(key :str, value :Dict): return {e.display(): e for e in data}
text = _(" Subvolume :{:16}").format(key)
if isinstance(value,str):
text += _(" mounted at {:16}").format(value)
else:
if value.get('mountpoint'):
text += _(" mounted at {:16}").format(value['mountpoint'])
else:
text += (' ' * 28)
if value.get('options',[]):
text += _(" with option {}").format(', '.join(value['options']))
return text
formatted = {presentation(k, v): k for k, v in data.items()}
return {k: v for k, v in sorted(formatted.items(), key=lambda e: e[0])}
def action_list(self): def action_list(self):
return super().action_list() active_user = self.target if self.target else None
def exec_action(self, data: Dict): if active_user is None:
if self.target: return [self._actions[0]]
origkey, origval = list(self.target.items())[0]
else: else:
origkey = None return self._actions[1:]
if self.action == str(_('Delete')): def _prompt_options(self, editing: Optional[Subvolume] = None) -> List[str]:
del data[origkey] preset_options = []
else: if editing:
if self.action == str(_('Add')): preset_options = editing.options
self.target = {}
print(_('\n Fill the desired values for a new subvolume \n'))
with SubvolumeMenu(self.target,self.action) as add_menu:
for elem in ['name','mountpoint','options']:
add_menu.exec_option(elem)
else:
SubvolumeMenu(self.target,self.action).run()
data.update(self.target)
return data
class SubvolumeMenu(GeneralMenu):
def __init__(self,parameters,action=None):
self.data = parameters
self.action = action
self.ds = {}
self.ds['name'] = None
self.ds['mountpoint'] = None
self.ds['options'] = None
if self.data:
origkey,origval = list(self.data.items())[0]
self.ds['name'] = origkey
if isinstance(origval,str):
self.ds['mountpoint'] = origval
else:
self.ds['mountpoint'] = self.data[origkey].get('mountpoint')
self.ds['options'] = self.data[origkey].get('options')
super().__init__(data_store=self.ds)
def _setup_selection_menu_options(self):
self._menu_options['name'] = Selector(
str(_('Subvolume name ')),
self._select_subvolume_name if not self.action or self.action in (str(_('Add')), str(_('Copy'))) else None,
mandatory=True,
enabled=True)
self._menu_options['mountpoint'] = Selector(
str(_('Subvolume mountpoint')),
self._select_subvolume_mount_point if not self.action or self.action in (str(_('Add')),str(_('Edit'))) else None,
enabled=True)
self._menu_options['options'] = Selector(
str(_('Subvolume options')),
self._select_subvolume_options if not self.action or self.action in (str(_('Add')),str(_('Edit'))) else None,
enabled=True)
self._menu_options['save'] = Selector(
str(_('Save')),
exec_func=lambda n,v:True,
enabled=True)
self._menu_options['cancel'] = Selector(
str(_('Cancel')),
# func = lambda pre:True,
exec_func=lambda n,v:self.fast_exit(n),
enabled=True)
self.cancel_action = 'cancel'
self.save_action = 'save'
self.bottom_list = [self.save_action,self.cancel_action]
def fast_exit(self,accion):
if self.option(accion).get_selection():
for item in self.list_options():
if self.option(item).is_mandatory():
self.option(item).set_mandatory(False)
return True
def exit_callback(self):
# we exit without moving data
if self.option(self.cancel_action).get_selection():
return
if not self.ds['name']:
return
else:
key = self.ds['name']
value = {}
if self.ds['mountpoint']:
value['mountpoint'] = self.ds['mountpoint']
if self.ds['options']:
value['options'] = self.ds['options']
self.data.update({key : value})
def _select_subvolume_name(self,value):
return TextInput(str(_("Subvolume name :")),value).run()
def _select_subvolume_mount_point(self,value):
return TextInput(str(_("Select a mount point :")),value).run()
def _select_subvolume_options(self,value) -> List[str]:
# def __init__(self, title, p_options, skip=True, multi=False, default_option=None, sort=True):
choice = Menu( choice = Menu(
str(_("Select the desired subvolume options ")), str(_("Select the desired subvolume options ")),
['nodatacow','compress'], ['nodatacow','compress'],
skip=True, skip=True,
preset_values=value, preset_values=preset_options,
multi=True multi=True
).run() ).run()
if choice.type_ == MenuSelectionType.Selection: if choice.type_ == MenuSelectionType.Selection:
return choice.value return choice.value # type: ignore
return [] return []
def _add_subvolume(self, editing: Optional[Subvolume] = None) -> Optional[Subvolume]:
name = TextInput(f'\n\n{_("Subvolume name")}: ', editing.name if editing else '').run()
if not name:
return None
mountpoint = TextInput(f'\n{_("Subvolume mountpoint")}: ', editing.mountpoint if editing else '').run()
if not mountpoint:
return None
options = self._prompt_options(editing)
subvolume = Subvolume(name, mountpoint)
subvolume.compress = 'compress' in options
subvolume.nodatacow = 'nodatacow' in options
return subvolume
def exec_action(self, data: List[Subvolume]) -> List[Subvolume]:
if self.target:
active_subvolume = self.target
else:
active_subvolume = None
if self.action == self._actions[0]: # add
new_subvolume = self._add_subvolume()
if new_subvolume is not None:
# in case a user with the same username as an existing user
# was created we'll replace the existing one
data = [d for d in data if d.name != new_subvolume.name]
data += [new_subvolume]
elif self.action == self._actions[1]: # edit subvolume
new_subvolume = self._add_subvolume(active_subvolume)
if new_subvolume is not None:
# we'll remove the original subvolume and add the modified version
data = [d for d in data if d.name != active_subvolume.name and d.name != new_subvolume.name]
data += [new_subvolume]
elif self.action == self._actions[2]: # delete
data = [d for d in data if d != active_subvolume]
return data