NTFS Root Filesystem Support (#748)
* For fun, allow NTFS as a root filesystem type Add ability to format a filesystem as NTFS Try to force filesystem type Fix FAT mounting * Split out mount fs type method * Handle rootfstype on non-GRUB bootloaders * Add -Q to mkfs.ntfs command line for quick formatting * I believe this will fix GRUB with NTFS root * Remove the fsck hook if NTFS is used as the root partition * Looks like the string is ntfs3 not ntfs so this logic wasn't running
This commit is contained in:
parent
29d0b3d155
commit
0c96ae049d
|
|
@ -13,6 +13,7 @@ from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat
|
||||||
from ..output import log
|
from ..output import log
|
||||||
from ..general import SysCommand
|
from ..general import SysCommand
|
||||||
|
|
||||||
|
|
||||||
class Partition:
|
class Partition:
|
||||||
def __init__(self, path: str, block_device: BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True):
|
def __init__(self, path: str, block_device: BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True):
|
||||||
if not part_id:
|
if not part_id:
|
||||||
|
|
@ -71,17 +72,17 @@ class Partition:
|
||||||
|
|
||||||
def __dump__(self):
|
def __dump__(self):
|
||||||
return {
|
return {
|
||||||
'type' : 'primary',
|
'type': 'primary',
|
||||||
'PARTUUID' : self._safe_uuid,
|
'PARTUUID': self._safe_uuid,
|
||||||
'wipe' : self.allow_formatting,
|
'wipe': self.allow_formatting,
|
||||||
'boot' : self.boot,
|
'boot': self.boot,
|
||||||
'ESP' : self.boot,
|
'ESP': self.boot,
|
||||||
'mountpoint' : self.target_mountpoint,
|
'mountpoint': self.target_mountpoint,
|
||||||
'encrypted' : self._encrypted,
|
'encrypted': self._encrypted,
|
||||||
'start' : self.start,
|
'start': self.start,
|
||||||
'size' : self.end,
|
'size': self.end,
|
||||||
'filesystem' : {
|
'filesystem': {
|
||||||
'format' : get_filesystem_type(self.path)
|
'format': get_filesystem_type(self.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +99,7 @@ class Partition:
|
||||||
|
|
||||||
for partition in output.get('partitiontable', {}).get('partitions', []):
|
for partition in output.get('partitiontable', {}).get('partitions', []):
|
||||||
if partition['node'] == self.path:
|
if partition['node'] == self.path:
|
||||||
return partition['start']# * self.sector_size
|
return partition['start'] # * self.sector_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end(self):
|
def end(self):
|
||||||
|
|
@ -107,7 +108,7 @@ class Partition:
|
||||||
|
|
||||||
for partition in output.get('partitiontable', {}).get('partitions', []):
|
for partition in output.get('partitiontable', {}).get('partitions', []):
|
||||||
if partition['node'] == self.path:
|
if partition['node'] == self.path:
|
||||||
return partition['size']# * self.sector_size
|
return partition['size'] # * self.sector_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self):
|
def boot(self):
|
||||||
|
|
@ -301,6 +302,13 @@ class Partition:
|
||||||
raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}")
|
raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}")
|
||||||
self.filesystem = filesystem
|
self.filesystem = filesystem
|
||||||
|
|
||||||
|
elif filesystem == 'ntfs':
|
||||||
|
options = ['-f'] + options
|
||||||
|
|
||||||
|
if (handle := SysCommand(f"/usr/bin/mkfs.ntfs -Q {' '.join(options)} {path}")).exit_code != 0:
|
||||||
|
raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}")
|
||||||
|
self.filesystem = filesystem
|
||||||
|
|
||||||
elif filesystem == 'crypto_LUKS':
|
elif filesystem == 'crypto_LUKS':
|
||||||
# from ..luks import luks2
|
# from ..luks import luks2
|
||||||
# encrypted_partition = luks2(self, None, None)
|
# encrypted_partition = luks2(self, None, None)
|
||||||
|
|
@ -333,13 +341,15 @@ class Partition:
|
||||||
raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
|
raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.')
|
||||||
fs = self.filesystem
|
fs = self.filesystem
|
||||||
|
|
||||||
|
fs_type = get_mount_fs_type(fs)
|
||||||
|
|
||||||
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
|
pathlib.Path(target).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if options:
|
if options:
|
||||||
mnt_handle = SysCommand(f"/usr/bin/mount -o {options} {self.path} {target}")
|
mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} -o {options} {self.path} {target}")
|
||||||
else:
|
else:
|
||||||
mnt_handle = SysCommand(f"/usr/bin/mount {self.path} {target}")
|
mnt_handle = SysCommand(f"/usr/bin/mount -t {fs_type} {self.path} {target}")
|
||||||
|
|
||||||
# TODO: Should be redundant to check for exit_code
|
# TODO: Should be redundant to check for exit_code
|
||||||
if mnt_handle.exit_code != 0:
|
if mnt_handle.exit_code != 0:
|
||||||
|
|
@ -382,3 +392,11 @@ class Partition:
|
||||||
except UnknownFilesystemFormat as err:
|
except UnknownFilesystemFormat as err:
|
||||||
raise err
|
raise err
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_mount_fs_type(fs):
|
||||||
|
if fs == 'ntfs':
|
||||||
|
return 'ntfs3' # Needed to use the Paragon R/W NTFS driver
|
||||||
|
elif fs == 'fat32':
|
||||||
|
return 'vfat' # This is the actual type used for fat32 mounting.
|
||||||
|
return fs
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from .storage import storage
|
||||||
from .output import log
|
from .output import log
|
||||||
from .profiles import Profile
|
from .profiles import Profile
|
||||||
from .disk.btrfs import create_subvolume, mount_subvolume
|
from .disk.btrfs import create_subvolume, mount_subvolume
|
||||||
|
from .disk.partition import get_mount_fs_type
|
||||||
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError
|
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError
|
||||||
|
|
||||||
# Any package that the Installer() is responsible for (optional and the default ones)
|
# Any package that the Installer() is responsible for (optional and the default ones)
|
||||||
|
|
@ -40,16 +41,17 @@ class InstallationFile:
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.fh.close()
|
self.fh.close()
|
||||||
self.installation.chown(self.owner, self.filename)
|
self.installation.chown(self.owner, self.filename)
|
||||||
|
|
||||||
def write(self, data :Union[str, bytes]):
|
def write(self, data: Union[str, bytes]):
|
||||||
return self.fh.write(data)
|
return self.fh.write(data)
|
||||||
|
|
||||||
def read(self, *args):
|
def read(self, *args):
|
||||||
return self.fh.read(*args)
|
return self.fh.read(*args)
|
||||||
|
|
||||||
def poll(self, *args):
|
def poll(self, *args):
|
||||||
return self.fh.poll(*args)
|
return self.fh.poll(*args)
|
||||||
|
|
||||||
|
|
||||||
class Installer:
|
class Installer:
|
||||||
"""
|
"""
|
||||||
`Installer()` is the wrapper for most basic installation steps.
|
`Installer()` is the wrapper for most basic installation steps.
|
||||||
|
|
@ -165,7 +167,7 @@ class Installer:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mount_ordered_layout(self, layouts :dict):
|
def mount_ordered_layout(self, layouts: dict):
|
||||||
from .luks import luks2
|
from .luks import luks2
|
||||||
|
|
||||||
mountpoints = {}
|
mountpoints = {}
|
||||||
|
|
@ -295,7 +297,7 @@ class Installer:
|
||||||
def activate_time_syncronization(self):
|
def activate_time_syncronization(self):
|
||||||
self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO)
|
self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO)
|
||||||
self.enable_service('systemd-timesyncd')
|
self.enable_service('systemd-timesyncd')
|
||||||
|
|
||||||
with open(f"{self.target}/etc/systemd/timesyncd.conf", "w") as fh:
|
with open(f"{self.target}/etc/systemd/timesyncd.conf", "w") as fh:
|
||||||
fh.write("[Time]\n")
|
fh.write("[Time]\n")
|
||||||
fh.write("NTP=0.arch.pool.ntp.org 1.arch.pool.ntp.org 2.arch.pool.ntp.org 3.arch.pool.ntp.org\n")
|
fh.write("NTP=0.arch.pool.ntp.org 1.arch.pool.ntp.org 2.arch.pool.ntp.org 3.arch.pool.ntp.org\n")
|
||||||
|
|
@ -446,6 +448,11 @@ class Installer:
|
||||||
self.MODULES.append('btrfs')
|
self.MODULES.append('btrfs')
|
||||||
if '/usr/bin/btrfs-progs' not in self.BINARIES:
|
if '/usr/bin/btrfs-progs' not in self.BINARIES:
|
||||||
self.BINARIES.append('/usr/bin/btrfs')
|
self.BINARIES.append('/usr/bin/btrfs')
|
||||||
|
|
||||||
|
# There is not yet an fsck tool for NTFS. If it's being used for the root filesystem, the hook should be removed.
|
||||||
|
if partition.filesystem == 'ntfs3' and partition.mountpoint == self.target:
|
||||||
|
if 'fsck' in self.HOOKS:
|
||||||
|
self.HOOKS.remove('fsck')
|
||||||
|
|
||||||
if self.detect_encryption(partition):
|
if self.detect_encryption(partition):
|
||||||
if 'encrypt' not in self.HOOKS:
|
if 'encrypt' not in self.HOOKS:
|
||||||
|
|
@ -499,7 +506,7 @@ class Installer:
|
||||||
if kind == 'zram':
|
if kind == 'zram':
|
||||||
self.log(f"Setting up swap on zram")
|
self.log(f"Setting up swap on zram")
|
||||||
self.pacstrap('zram-generator')
|
self.pacstrap('zram-generator')
|
||||||
|
|
||||||
# We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813
|
# We could use the default example below, but maybe not the best idea: https://github.com/archlinux/archinstall/pull/678#issuecomment-962124813
|
||||||
# zram_example_location = '/usr/share/doc/zram-generator/zram-generator.conf.example'
|
# zram_example_location = '/usr/share/doc/zram-generator/zram-generator.conf.example'
|
||||||
# shutil.copy2(f"{self.target}{zram_example_location}", f"{self.target}/usr/lib/systemd/zram-generator.conf")
|
# shutil.copy2(f"{self.target}{zram_example_location}", f"{self.target}/usr/lib/systemd/zram-generator.conf")
|
||||||
|
|
@ -521,11 +528,14 @@ class Installer:
|
||||||
|
|
||||||
boot_partition = None
|
boot_partition = None
|
||||||
root_partition = None
|
root_partition = None
|
||||||
|
root_partition_fs = None
|
||||||
for partition in self.partitions:
|
for partition in self.partitions:
|
||||||
if partition.mountpoint == self.target + '/boot':
|
if partition.mountpoint == self.target + '/boot':
|
||||||
boot_partition = partition
|
boot_partition = partition
|
||||||
elif partition.mountpoint == self.target:
|
elif partition.mountpoint == self.target:
|
||||||
root_partition = partition
|
root_partition = partition
|
||||||
|
root_partition_fs = partition.filesystem
|
||||||
|
root_fs_type = get_mount_fs_type(root_partition_fs)
|
||||||
|
|
||||||
if boot_partition is None and root_partition is None:
|
if boot_partition is None and root_partition is None:
|
||||||
raise ValueError(f"Could not detect root (/) or boot (/boot) in {self.target} based on: {self.partitions}")
|
raise ValueError(f"Could not detect root (/) or boot (/boot) in {self.target} based on: {self.partitions}")
|
||||||
|
|
@ -597,10 +607,10 @@ class Installer:
|
||||||
# TODO: We need to detect if the encrypted device is a whole disk encryption,
|
# TODO: We need to detect if the encrypted device is a whole disk encryption,
|
||||||
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
||||||
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG)
|
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG)
|
||||||
entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp {" ".join(self.KERNEL_PARAMS)}\n')
|
entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n')
|
||||||
else:
|
else:
|
||||||
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG)
|
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG)
|
||||||
entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp {" ".join(self.KERNEL_PARAMS)}\n')
|
entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}\n')
|
||||||
|
|
||||||
self.helper_flags['bootloader'] = bootloader
|
self.helper_flags['bootloader'] = bootloader
|
||||||
|
|
||||||
|
|
@ -610,12 +620,16 @@ class Installer:
|
||||||
if real_device := self.detect_encryption(root_partition):
|
if real_device := self.detect_encryption(root_partition):
|
||||||
root_uuid = SysCommand(f"blkid -s UUID -o value {real_device.path}").decode().rstrip()
|
root_uuid = SysCommand(f"blkid -s UUID -o value {real_device.path}").decode().rstrip()
|
||||||
_file = "/etc/default/grub"
|
_file = "/etc/default/grub"
|
||||||
add_to_CMDLINE_LINUX = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_uuid}:cryptlvm\"/'"
|
add_to_CMDLINE_LINUX = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cryptdevice=UUID={root_uuid}:cryptlvm rootfstype={root_fs_type}\"/'"
|
||||||
enable_CRYPTODISK = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'"
|
enable_CRYPTODISK = "sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/'"
|
||||||
|
|
||||||
log(f"Using UUID {root_uuid} of {real_device} as encrypted root identifier.", level=logging.INFO)
|
log(f"Using UUID {root_uuid} of {real_device} as encrypted root identifier.", level=logging.INFO)
|
||||||
SysCommand(f"/usr/bin/arch-chroot {self.target} {add_to_CMDLINE_LINUX} {_file}")
|
SysCommand(f"/usr/bin/arch-chroot {self.target} {add_to_CMDLINE_LINUX} {_file}")
|
||||||
SysCommand(f"/usr/bin/arch-chroot {self.target} {enable_CRYPTODISK} {_file}")
|
SysCommand(f"/usr/bin/arch-chroot {self.target} {enable_CRYPTODISK} {_file}")
|
||||||
|
else:
|
||||||
|
_file = "/etc/default/grub"
|
||||||
|
add_to_CMDLINE_LINUX = f"sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"rootfstype={root_fs_type}\"/'"
|
||||||
|
SysCommand(f"/usr/bin/arch-chroot {self.target} {add_to_CMDLINE_LINUX} {_file}")
|
||||||
|
|
||||||
log(f"GRUB uses {boot_partition.path} as the boot partition.", level=logging.INFO)
|
log(f"GRUB uses {boot_partition.path} as the boot partition.", level=logging.INFO)
|
||||||
if has_uefi():
|
if has_uefi():
|
||||||
|
|
@ -664,10 +678,10 @@ class Installer:
|
||||||
# TODO: We need to detect if the encrypted device is a whole disk encryption,
|
# TODO: We need to detect if the encrypted device is a whole disk encryption,
|
||||||
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
# or simply a partition encryption. Right now we assume it's a partition (and we always have)
|
||||||
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG)
|
log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.DEBUG)
|
||||||
kernel_parameters.append(f'cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp {" ".join(self.KERNEL_PARAMS)}')
|
kernel_parameters.append(f'cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}')
|
||||||
else:
|
else:
|
||||||
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG)
|
log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG)
|
||||||
kernel_parameters.append(f'root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp {" ".join(self.KERNEL_PARAMS)}')
|
kernel_parameters.append(f'root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp rootfstype={root_fs_type} {" ".join(self.KERNEL_PARAMS)}')
|
||||||
|
|
||||||
SysCommand(f'efibootmgr --disk {boot_partition.path[:-1]} --part {boot_partition.path[-1]} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose')
|
SysCommand(f'efibootmgr --disk {boot_partition.path[:-1]} --part {boot_partition.path[-1]} --create --label "{label}" --loader {loader} --unicode \'{" ".join(kernel_parameters)}\' --verbose')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -486,7 +486,8 @@ def ask_for_main_filesystem_format():
|
||||||
'btrfs': 'btrfs',
|
'btrfs': 'btrfs',
|
||||||
'ext4': 'ext4',
|
'ext4': 'ext4',
|
||||||
'xfs': 'xfs',
|
'xfs': 'xfs',
|
||||||
'f2fs': 'f2fs'
|
'f2fs': 'f2fs',
|
||||||
|
'ntfs': 'ntfs'
|
||||||
}
|
}
|
||||||
|
|
||||||
value = generic_select(options, "Select which filesystem your main partition should use (by number or name): ", allow_empty_input=False)
|
value = generic_select(options, "Select which filesystem your main partition should use (by number or name): ", allow_empty_input=False)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue