Commit 5e3092f9 authored by Nigel Kukard's avatar Nigel Kukard
Browse files

Merge branch 'nkupdates' into 'master'

Seamless support for EFI on RAID devices

See merge request !37
parents 9c402c7d 31f355b8
Pipeline #576 passed with stage
in 1 minute and 9 seconds
......@@ -15,4 +15,4 @@
"""IDMS Linux Installer package."""
__version__ = '0.2.0'
__version__ = '0.3.0'
......@@ -75,13 +75,16 @@ class IliState:
_supported_kernels: List[Dict[str, str]]
# Block devices we'll be creating fileystems on, indexed by 'efi', 'boot', 'root'
_blockdevices: Dict[str, str]
_blockdevices: Dict[str, Any]
# MBR's which we'll be installing boot records on
_boot_mbrs: List[str]
# Hint to use for grub to find the boot files
_boot_hint: Optional[str]
# Filesystems to install on, indexed by 'efi', 'boot', 'root'
_filesystems: Dict[str, Dict[str, str]]
_filesystems: Dict[str, Any]
# Filesystems we've mounted
_mounts: List[Mount]
......@@ -95,7 +98,7 @@ class IliState:
_target_mount: str
# Our fstab that will be written to the target system
_fstab: Dict[str, Dict[str, str]]
_fstab: Dict[str, Any]
# Sudo users
_sudo_users: List[Dict[str, Any]]
......@@ -145,6 +148,7 @@ class IliState:
self._blockdevices = {}
self._boot_mbrs = []
self._boot_hint = None
self._filesystems = {}
......@@ -463,10 +467,15 @@ class IliState:
if usage not in ['efi', 'boot', 'root']:
raise RuntimeError(f'Unknown blockdevice usage "{usage}"')
if usage in self._blockdevices:
raise RuntimeError(f'Blockdevice usage "{usage}" already set')
self._blockdevices[usage] = blockdevice
# Special handling for mulitple block devices
if usage == 'efi':
if usage not in self._blockdevices:
self._blockdevices[usage] = []
self._blockdevices[usage].append(blockdevice)
else:
if usage in self._blockdevices:
raise RuntimeError(f'Blockdevice usage "{usage}" already set')
self._blockdevices[usage] = blockdevice
def replace_blockdevice(self, usage: str, blockdevice: str):
"""Replace a block device."""
......@@ -474,6 +483,9 @@ class IliState:
if usage not in ['efi', 'boot', 'root']:
raise RuntimeError(f'Unknown blockdevice usage "{usage}"')
if usage == 'efi':
raise RuntimeError('Cannot replace EFI block device as it is a list')
if usage not in self._blockdevices:
raise RuntimeError(f'Blockdevice usage "{usage}" not set')
......@@ -495,6 +507,16 @@ class IliState:
"""Return the list of block devices that we should install MBR's on."""
return self._boot_mbrs
@property
def boot_hint(self):
"""Return the grub boot hint for the boot filesystem."""
return self._boot_hint
@boot_hint.setter
def boot_hint(self, boot_hint: str):
"""Set the grub boot hint for the boot filesystem."""
self._boot_hint = boot_hint
# Filesystems
def add_filesystem(self, usage: str, uuid: str, fstype: str, device: str):
"""Add a filesystem to install on."""
......@@ -502,10 +524,16 @@ class IliState:
if usage not in ['efi', 'boot', 'root']:
raise RuntimeError(f'Unknown filesystem usage "{usage}"')
if usage in self._filesystems:
raise RuntimeError(f'Filesystem usage "{usage}" already set')
# We treat efi slightly differently
if usage == 'efi':
if usage not in self._filesystems:
self._filesystems[usage] = []
self._filesystems[usage].append({'uuid': uuid, 'fstype': fstype, 'device': device})
self._filesystems[usage] = {'uuid': uuid, 'fstype': fstype, 'device': device}
else:
if usage in self._filesystems:
raise RuntimeError(f'Filesystem usage "{usage}" already set')
self._filesystems[usage] = {'uuid': uuid, 'fstype': fstype, 'device': device}
@property
def filesystems(self):
......@@ -525,19 +553,26 @@ class IliState:
if usage not in ['efi', 'boot', 'root']:
raise RuntimeError(f'Unknown fstab usage "{usage}"')
if usage in self._fstab:
raise RuntimeError(f'The fstab usage "{usage}" is already set')
# Add fstab entry
self._fstab[usage] = {
# Create fstab entry
entry = {
'device': device,
'mount_point': mount_point,
'fstype': fstype,
}
# Loop through optional items
# Loop through optional entry items
for option in ['options', 'fsdump', 'fspass']:
if kwargs.get(option):
self._fstab[usage][option] = str(kwargs.get(option))
entry[option] = str(kwargs.get(option))
# We treat EFI slightly differently
if usage == 'efi':
if 'efi' not in self._fstab:
self._fstab['efi'] = []
self._fstab['efi'].append(entry)
else:
if usage in self._fstab:
raise RuntimeError(f'The fstab usage "{usage}" is already set')
self._fstab[usage] = entry
@property
def fstab(self):
......
......@@ -42,7 +42,13 @@ class ConfigSystemEtcFstab(Plugin):
# Loop with each usage and add the associated entry
for usage in ['root', 'boot', 'efi']:
self._etc_fstab.add(**ili_state.fstab[usage])
if usage not in ili_state.fstab:
continue
if usage == 'efi':
for efi in ili_state.fstab['efi']:
self._etc_fstab.add(**efi)
else:
self._etc_fstab.add(**ili_state.fstab[usage])
# Write out entries
self._etc_fstab.write(ili_state.target_root)
......@@ -15,8 +15,6 @@
"""Configure system grub."""
import os
from idmslinux_installer.ilistate import IliState
from idmslinux_installer.plugin import Plugin
from idmslinux_installer.util.sysgrub import SysGrub
......@@ -51,9 +49,12 @@ class ConfigSystemGrub(Plugin):
sysgrub.install_mbr(ili_state.target_root, device, ili_state.output_callback)
# Install grub in the EFI directory if we have an efi filesystem and are on an efi enabled system
if (ili_state.fstab['efi'] and os.path.exists('/sys/firmware/efi/efivars')):
ili_state.output_callback('Installing grub EFI in %s' % ili_state.fstab['efi']['mount_point'])
sysgrub.install_efi(ili_state.target_root, ili_state.fstab['efi']['mount_point'], ili_state.output_callback)
# os.path.exists('/sys/firmware/efi/efivars'))
if 'efi' in ili_state.fstab:
for efi in ili_state.fstab['efi']:
ili_state.output_callback('Installing grub EFI in %s' % efi['mount_point'])
sysgrub.install_efi(ili_state.target_root, efi['mount_point'], ili_state.filesystems['boot']['uuid'],
ili_state.boot_hint, ili_state.output_callback)
# We configure grub after the installation becaues this is when the /boot/grub dir exists
sysgrub.configure(ili_state.target_root, ili_state.output_callback)
# Copyright (c) 2019-2020, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Configure system mdadm."""
from idmslinux_installer.ilistate import IliState
from idmslinux_installer.plugin import Plugin
from idmslinux_installer.util.sysmdadm import SysMdadm
# Ignore warning that we have not overridden all base class methods.
# pylama:ignore=W0223
class ConfigSystemMdadm(Plugin):
"""Configure system mdadm."""
def __init__(self):
"""Plugin init method."""
self.description = "System mdadm configuration"
Plugin.__init__(self)
def config_system(self, ili_state: IliState):
"""Configure system hostname."""
# If mdadm is in base_packages, we need to generate the mdadm configuration
if 'mdadm' in ili_state.base_packages:
ili_state.output_callback('Configuring mdadm')
sysmdadm = SysMdadm()
sysmdadm.configure(ili_state.target_root)
......@@ -22,7 +22,7 @@ from idmslinux_installer.util.sysuser import SysUser
# Ignore warning that we have not overridden all base class methods.
# pylama:ignore=W0223
class PostInstallUsers(Plugin):
class ConfigSystemUsers(Plugin):
"""Setup users."""
def __init__(self):
......@@ -51,8 +51,8 @@ class PostInstallUsers(Plugin):
ili_state.add_base_package('openssh')
ili_state.add_enable_service('sshd')
def post_install(self, ili_state: IliState):
"""Post install task to setup users."""
def config_system(self, ili_state: IliState):
"""Configure users. We must do this before mkinitcpio runs so the initramfs has the root user password."""
sysuser = SysUser()
......
......@@ -43,10 +43,12 @@ class CreateFilesystems(Plugin):
# Check which block devices we need to create filesystems for
if 'efi' in ili_state.blockdevices:
ili_state.output_callback('Configuring EFI filesystem')
uuid = mkfs.run('vfat', ili_state.blockdevices['efi'], fslabel="efi", args=['-F', '32'],
output_callback=ili_state.output_callback)
ili_state.add_filesystem('efi', uuid, 'vfat', ili_state.blockdevices['efi'])
# Loop with EFI block devices
for efi_block_device in ili_state.blockdevices['efi']:
ili_state.output_callback(f'Configuring EFI filesystem: {efi_block_device}')
uuid = mkfs.run('vfat', efi_block_device, fslabel="EFI", args=['-F', '32'],
output_callback=ili_state.output_callback)
ili_state.add_filesystem('efi', uuid, 'vfat', efi_block_device)
if 'boot' in ili_state.blockdevices:
ili_state.output_callback('Configuring boot filesystem')
......
......@@ -67,3 +67,6 @@ class DiskUsageASIS(Plugin):
# Add the MBR device to the installer
ili_state.output_callback('Adding device for MBR')
ili_state.add_boot_mbr(disk)
# Set the hint to use to find the boot fs
ili_state.boot_hint = 'hd0,gpt3'
......@@ -87,22 +87,22 @@ class DiskUsageMDRAID(Plugin):
if raid_level:
ili_state.output_callback('Creating MDRAID arrays')
efi_disk = ili_state.install_disks[0]
boot_disk = '/dev/md/0'
root_disk = '/dev/md/1'
mdadm = Mdadm()
# Add all part3 to array
mdadm.create(boot_disk, 1, [get_partition(x, 3) for x in ili_state.install_disks],
output_callback=ili_state.output_callback)
boot_attribs = mdadm.create(boot_disk, 1, [get_partition(x, 3) for x in ili_state.install_disks],
output_callback=ili_state.output_callback)
# Add all part4 partitions to array
mdadm.create(root_disk, raid_level, [get_partition(x, 4) for x in ili_state.install_disks],
output_callback=ili_state.output_callback)
# Add the block devices to the installer
ili_state.output_callback('Adding block devices')
ili_state.add_blockdevice('efi', get_partition(efi_disk, 2))
for efi_disk in ili_state.install_disks:
ili_state.add_blockdevice('efi', get_partition(efi_disk, 2))
ili_state.add_blockdevice('boot', boot_disk)
ili_state.add_blockdevice('root', root_disk)
......@@ -111,5 +111,9 @@ class DiskUsageMDRAID(Plugin):
for disk in ili_state.install_disks:
ili_state.add_boot_mbr(disk)
# Set the hint to use to find the boot fs
boot_uuid = boot_attribs['MD_UUID'].replace(':', '')
ili_state.boot_hint = f'mduuid/{boot_uuid}'
# Add mdadm to the base packages
ili_state.add_base_package('mdadm')
......@@ -35,7 +35,7 @@ class MountFilesystems(Plugin):
self.description = "Mount Filesystems"
self._boot_dir = '/boot'
self._efi_dir = '/boot/efi'
self._efi_dir = '/efi'
Plugin.__init__(self)
......@@ -64,16 +64,19 @@ class MountFilesystems(Plugin):
# If we have a efi filesystem, use it
if 'efi' in ili_state.filesystems:
ili_state.output_callback('Mounting EFI filesystem')
# Determine the boot mount point
mount_point = '%s%s' % (ili_state.target_mount, self._efi_dir)
# Determine the efi path
filesystem = ili_state.filesystems['efi']
# Mount the efi filesystem
mount = Mount(uuid=filesystem['uuid'], fstype=filesystem['fstype'], mount_point=mount_point)
ili_state.add_mount(mount)
# Add fstab entry
ili_state.add_fstab('efi', mount.mount_device, self._efi_dir, filesystem['fstype'], fspass=2)
ili_state.output_callback('Mounting EFI filesystems')
efi_no = 1
for filesystem in ili_state.filesystems['efi']:
# Determine the boot mount point
mount_point = f'{self._efi_dir}/{efi_no}'
mount_point_host = f'{ili_state.target_mount}{mount_point}'
# Mount the efi filesystem
mount = Mount(uuid=filesystem['uuid'], fstype=filesystem['fstype'], mount_point=mount_point_host)
ili_state.add_mount(mount)
# Add fstab entry
ili_state.add_fstab('efi', mount.mount_device, mount_point, filesystem['fstype'], fspass=2)
# Bump EFI number
efi_no += 1
# Set the target root
ili_state.target_root = ili_state.target_mount
......@@ -32,7 +32,7 @@ class ConfigSystemMkinitcpio(Plugin):
Plugin.__init__(self)
def config_system(self, ili_state: IliState):
def post_install(self, ili_state: IliState):
"""Configure system mkinitcpio."""
sysmkinitcpio = SysMkinitcpio()
......
......@@ -15,7 +15,7 @@
"""Support class for mdadm."""
from typing import List
from typing import Dict, List
from idmslinux_installer.util.asyncsubprocess import (AsyncSubprocess,
OutputCallback)
......@@ -28,8 +28,8 @@ class Mdadm:
def __init__(self, load: bool = True):
"""Initialize our class and load system partition types."""
def create(self, md_device: str, level: int, devices: List[str], output_callback: OutputCallback = None):
"""Create RAID device."""
def create(self, md_device: str, level: int, devices: List[str], output_callback: OutputCallback = None) -> Dict[str, str]:
"""Create a RAID device and return the details in a dict."""
# If we didn't get an output_callback, set it to our own class method
if not output_callback:
......@@ -47,6 +47,22 @@ class Mdadm:
if proc.retcode != 0:
raise OSError(f'Failed to create md device {md_device} return code {proc.retcode}')
# Grab the UUID
proc = AsyncSubprocess(['mdadm', '--detail', '--export', md_device])
result = proc.run()
# Raise an exception if we didn't get a positive response back
if proc.retcode != 0:
raise OSError(f'Failed to get mdadm attributes from device {md_device} return code {proc.retcode}')
# Strip value, then split on the =
attribs: Dict[str, str] = {}
for line in result:
key, value = line.rstrip().split('=', 1)
attribs[key] = value
return attribs
def _default_output_callback(self, line: str):
line.rstrip()
print(f'mdadm: {line}')
......@@ -15,14 +15,16 @@
"""Support class for grub on the target system."""
import distutils.dir_util
import os
import re
import shutil
from typing import Optional
from .asyncsubprocess import AsyncSubprocess, OutputCallback
# pylama:ignore=R0201,R0914
# pylama:ignore=R0201,R0913,R0914,C901
class SysGrub:
"""The SysGrub class handles configuring and installing grub on the target system."""
......@@ -113,7 +115,7 @@ class SysGrub:
output_callback = self._default_output_callback
# Run grub-install
proc = AsyncSubprocess(['arch-chroot', system_path, 'grub-install', device],
proc = AsyncSubprocess(['arch-chroot', system_path, 'grub-install', '--target', 'i386-pc', device],
output_callback=output_callback)
proc.run()
......@@ -121,19 +123,49 @@ class SysGrub:
if proc.retcode != 0:
raise OSError(f'Failed to run grub-install on the target device {device}, return code {proc.retcode}')
def install_efi(self, system_path: str, efi_dir: str, output_callback: OutputCallback = None):
# pylama:ignore=R0913
def install_efi(self, system_path: str, efi_dir: str, boot_uuid: str, boot_hint: str, output_callback: OutputCallback = None):
"""Install grub on the EFI."""
# If we didn't get an output_callback, set it to our own class method
if not output_callback:
output_callback = self._default_output_callback
# Run grub-install
proc = AsyncSubprocess(['arch-chroot', system_path, 'grub-install', '--target', 'x86_64-efi',
'--efi-directory', efi_dir, '--bootloader-id', 'IDMS Linux'],
# Make EFI boot directory
efi_path = f'{system_path}{efi_dir}/EFI/BOOT'
if not os.path.exists(efi_path):
os.makedirs(efi_path)
# Copy grub modules
efi_path_grub = f'{system_path}{efi_dir}/boot/grub/x86_64-efi'
distutils.dir_util.copy_tree('/usr/lib/grub/x86_64-efi', efi_path_grub)
grub_fontdir = f'{system_path}{efi_dir}/boot/grub/fonts'
grub_font = f'{system_path}/usr/share/grub/unicode.pf2'
# Make font dir and copy in the unicode.pf2 font
if not os.path.exists(grub_fontdir):
os.makedirs(grub_fontdir)
shutil.copy(grub_font, grub_fontdir)
grub_modules_extra = []
if boot_hint and boot_hint.startswith('mduuid'):
grub_modules_extra.append('mdraid1x')
# Run grub-mkimage
proc = AsyncSubprocess(['arch-chroot', system_path, 'grub-mkimage', '--format', 'x86_64-efi', '--prefix', '/boot/grub',
'--output', f'{efi_dir}/EFI/BOOT/BOOTX64.EFI', 'part_gpt', 'part_msdos', 'fat',
*grub_modules_extra],
output_callback=output_callback)
proc.run()
# Write out our stub grub.cfg to locate the main /boot grub.cfg and load it
with open(f'{system_path}{efi_dir}/boot/grub/grub.cfg', 'w') as grubcfg:
hint = ''
if boot_hint:
hint = f'--hint=\'{boot_hint}\' '
grubcfg.write(f'search --no-floppy --fs-uuid --set=bootfs {hint}{boot_uuid}\n')
grubcfg.write('configfile ($bootfs)/grub/grub.cfg\n')
# Raise an exception if we didn't get a positive response back
if proc.retcode != 0:
raise OSError(f'Failed to run grub-install on the efi directory {efi_dir}, return code {proc.retcode}')
......
# Copyright (c) 2019-2020, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Support class for mdadm."""
from typing import List
from .asyncsubprocess import AsyncSubprocess
# pylama:ignore=R0903,R0201
class SysMdadm:
"""The SysHostname class handles setting the mdadm configuration."""
def __init__(self):
"""Initialize our class."""
def configure(self, system_path: str):
"""Set the mdadm configuration."""
# Write out our mdadm configuration
with open(f'{system_path}/etc/mdadm.conf', 'a') as mdadmcfg:
mdadmcfg.write('\n')
mdadmcfg.write('\n'.join(self._config()))
mdadmcfg.write('MAILADDR root@localhost\n')
def _config(self) -> List[str]:
"""Return mdadm config."""
# Grab the RAID config
proc = AsyncSubprocess(['mdadm', '--examine', '--scan'])
result = proc.run()
# Raise an exception if we didn't get a positive response back
if proc.retcode != 0:
raise OSError(f'Failed to get mdadm config return code {proc.retcode}')
return result
......@@ -69,8 +69,8 @@ class SysMkinitcpio:
conf_file.close()
conf_file_new.close()
# Move new file ontop of old one
os.replace(sys_mkinitcpio_conf_new, sys_mkinitcpio_conf)
# Move new file ontop of old one
os.replace(sys_mkinitcpio_conf_new, sys_mkinitcpio_conf)
def create(self, system_path: str, preset: str, output_callback: OutputCallback = None):
"""Create mkinitcpio on target system."""
......@@ -82,7 +82,6 @@ class SysMkinitcpio:
# Run mkinitcpio
proc = AsyncSubprocess(['arch-chroot', system_path, 'mkinitcpio', '-p', preset], output_callback=output_callback)
proc.run()
# Raise an exception if we didn't get a positive response back
if proc.retcode != 0:
raise OSError(f'Failed to run mkinitcpio on the target system, return code {proc.retcode}')
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment