1 ###############################################################################
3 # Bricklayer - An Installer for IPFire #
4 # Copyright (C) 2021 IPFire Development Team #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19 ###############################################################################
32 log
= logging
.getLogger("bricklayer.disk")
34 DEFAULT_FILESYSTEM
= "btrfs"
36 # Check if parted.PARTITION_ESP is set
39 except AttributeError:
40 parted
.PARTITION_ESP
= 18
44 Disks abstraction class
46 def __init__(self
, bricklayer
):
47 self
.bricklayer
= bricklayer
56 # Don't scan for disks if already done
60 log
.debug("Scanning for disks...")
62 for device
in parted
.getAllDevices():
63 disk
= Disk(self
.bricklayer
, device
)
65 # Skip whatever isn't suitable
66 if not disk
.supported
:
69 self
.disks
.append(disk
)
71 # Sort them alphabetically
74 def add_disk(self
, path
, selected
=False):
78 # Check if the disk is already on the list
79 for disk
in self
.disks
:
85 # Setup regular files as loop devices
86 if stat
.S_ISREG(st
.st_mode
):
87 path
= self
._losetup
(path
)
89 # Create a Disk object
90 disk
= Disk(self
.bricklayer
, path
)
91 self
.disks
.append(disk
)
99 def _losetup(self
, path
):
100 # Find a free loop device
101 device
= self
.bricklayer
.command(["losetup", "-f"])
102 device
= device
.rstrip()
104 # Connect the image to the loop device
105 self
.bricklayer
.command(["losetup", device
, path
])
107 # Return the name of the loop device
115 return [disk
for disk
in self
.disks
if disk
.supported
]
122 return [disk
for disk
in self
.disks
if disk
.selected
]
124 def calculate_partition_layout(self
):
126 This creates the partition layout, but doesn't write it to disk, yet
128 # Find the root device
129 root
= self
.selected
[0] # XXX select the first harddisk only
131 # Create one giant root partition
132 root
.create_system_partitions()
134 def _find_partition(self
, name
):
136 Returns the partition with name
138 for disk
in self
.selected
:
139 for partition
in disk
.partitions
:
140 if partition
.name
== name
:
145 Mounts all filesystems
147 # Find root partition
148 partition
= self
._find
_partition
("ROOT")
150 FileNotFoundError("Could not find root partition")
152 # Mount the root partition
153 self
._mount
(partition
.path
, self
.bricklayer
.root
)
156 partition
= self
._find
_partition
("ESP")
158 self
._mount
(partition
.path
, os
.path
.join(self
.bricklayer
.root
, "boot/efi"))
160 def _mount(self
, source
, target
):
162 Mounts source to target
164 # Make sure the target exists
165 os
.makedirs(target
, exist_ok
=True)
168 self
.bricklayer
.command(["mount", source
, target
])
172 Umounts all filesystems
174 # Umount everything mounted in root
175 self
.bricklayer
.command(["umount", "-Rv", self
.bricklayer
.root
], error_ok
=True)
179 Shuts down any storage
183 for disk
in self
.disks
:
186 def write_fstab(self
):
188 Writes the disk configuration to /etc/fstab
190 path
= os
.path
.join(self
.bricklayer
.root
, "etc/fstab")
192 with
open(path
, "w") as f
:
193 for disk
in self
.selected
:
194 for partition
in disk
.partitions
:
195 e
= partition
.make_fstab_entry()
197 # Do nothing if the entry is empty
204 # Write it to the file
209 def __init__(self
, bricklayer
, device
):
210 self
.bricklayer
= bricklayer
213 if not isinstance(device
, parted
.Device
):
214 device
= parted
.Device(device
)
218 # The parted disk (with a blank partition table)
219 self
.parted
= parted
.freshDisk(self
.device
, "gpt")
221 # Has this device been selected?
222 self
.selected
= False
224 # Where are we starting with the next partition?
228 return "<%s %s>" % (self
.__class
__.__name
__, self
.path
)
231 return "%s (%s)" % (self
.model
, util
.format_size(self
.size
))
234 return hash(self
.path
)
236 def __eq__(self
, other
):
237 if isinstance(other
, self
.__class
__):
238 return self
.device
== other
.device
240 return NotImplemented
242 def __lt__(self
, other
):
243 if isinstance(other
, self
.__class
__):
244 return self
.model
< other
.model
or self
.path
< other
.path
246 return NotImplemented
251 Is this device supported?
253 # Skip any device-mapper devices
254 if self
.device
.type == parted
.DEVICE_DM
:
257 # Skip any CD/DVD drives
258 if self
.device
.path
.startswith("/dev/sr"):
261 # Skip MDRAID devices
262 if self
.device
.path
.startswith("/dev/md"):
265 # We do not support read-only devices
266 if self
.device
.readOnly
:
269 # Ignore any busy devices
277 return self
.device
.path
281 return self
.device
.model
or _("Unknown Model")
285 return self
.device
.length
* self
.device
.sectorSize
288 def partitions(self
):
290 Returns a list of all partitions on this device
292 return [Partition(self
.bricklayer
, p
) for p
in self
.parted
.partitions
]
294 def create_system_partitions(self
):
296 This method creates a basic partition layout on this disk with all
297 partitions that the systems needs. This is as follows:
299 1) BIOS boot partition (used for GRUB, etc.)
300 2) A FAT-formatted EFI partition
304 log
.debug("Creating partition layout on %s" % self
.path
)
306 # Create a bootloader partition of exactly 1 MiB
307 if any((bl
.requires_bootldr_partition
for bl
in self
.bricklayer
.bootloaders
)):
308 self
._add
_partition
("BOOTLDR", DEFAULT_FILESYSTEM
, length
=1024**2,
309 flags
=[parted
.PARTITION_BIOS_GRUB
])
311 # Create an EFI-partition of exactly 32 MiB
312 if any((bl
.requires_efi_partition
for bl
in self
.bricklayer
.bootloaders
)):
313 self
._add
_partition
("ESP", "fat32", length
=32 * 1024**2,
314 flags
=[parted
.PARTITION_ESP
])
316 # Create a swap partition
317 swap_size
= self
.bricklayer
.settings
.get("swap-size", 0)
319 self
._add
_partition
("SWAP", "linux-swap(v1)", length
=swap_size
)
321 # Use all remaining space for root
322 self
._add
_partition
("ROOT", DEFAULT_FILESYSTEM
)
324 def _add_partition(self
, name
, filesystem
, length
=None, flags
=[]):
327 length
= self
.device
.getLength() - self
._start
329 # Otherwise convert bytes into sectors
331 length
= length
// self
.device
.sectorSize
333 # Calculate the partitions geometry
334 geometry
= parted
.Geometry(self
.device
, start
=self
._start
, length
=length
)
336 # Create the filesystem
337 fs
= parted
.FileSystem(type=filesystem
, geometry
=geometry
)
339 # Create the partition
340 partition
= parted
.Partition(disk
=self
.parted
, type=parted
.PARTITION_NORMAL
,
341 fs
=fs
, geometry
=geometry
)
344 partition
.name
= name
348 partition
.setFlag(flag
)
350 # Add the partition and align the most optimal way
351 self
.parted
.addPartition(partition
,
352 constraint
=self
.device
.optimalAlignedConstraint
)
354 # Store end to know where to begin the next partition
355 self
._start
= geometry
.end
+ 1
359 Write the partition table to disk
361 # Destroy any existing partition table
362 self
.device
.clobber()
364 # Write the new partition table
367 # For loop devices, we have to manually create the partition mappings
368 if self
.path
.startswith("/dev/loop"):
369 self
.bricklayer
.command(["kpartx", "-av", self
.path
])
375 # Unmap partitions on loop devices
376 if self
.path
.startswith("/dev/loop"):
377 self
.bricklayer
.command(["kpartx", "-dv", self
.path
])
379 # Free the loop device
380 self
.bricklayer
.command(["losetup", "-d", self
.path
])
383 class Partition(object):
384 def __init__(self
, bricklayer
, parted
):
385 self
.bricklayer
= bricklayer
391 return "<%s %s>" % (self
.__class
__.__name
__, self
.name
or self
.path
)
395 return self
.parted
.name
399 # Map path for loop devices
400 if self
.parted
.path
.startswith("/dev/loop"):
401 return self
.parted
.path
.replace("/dev/loop", "/dev/mapper/loop")
403 return self
.parted
.path
406 def mountpoint(self
):
408 Returns the mountpoint for this partition (or None)
410 if self
.name
== "ROOT":
413 elif self
.name
== "ESP":
417 def filesystem(self
):
418 type = self
.parted
.fileSystem
.type
421 if type == "linux-swap(v1)":
428 Wipes the entire partition (i.e. writes zeroes)
430 log
.info("Wiping %s (%s)..." % (self
.name
, self
.path
))
432 # Wipes any previous signatures from this disk
433 self
.bricklayer
.command(["wipefs", "--all", self
.path
])
437 Formats the filesystem
439 # Wipe BIOS_GRUB partitions instead of formatting them
440 if self
.parted
.getFlag(parted
.PARTITION_BIOS_GRUB
):
443 log
.info("Formatting %s (%s) with %s..." % \
444 (self
.name
, self
.path
, self
.filesystem
))
446 if self
.filesystem
== "fat32":
447 command
= ["mkfs.vfat", self
.path
]
448 elif self
.filesystem
== "swap":
449 command
= ["mkswap", "-v1", self
.path
]
451 command
= ["mkfs.%s" % self
.filesystem
, "-f", self
.path
]
454 self
.bricklayer
.command(command
)
459 Returns the UUID of the filesystem
461 uuid
= self
.bricklayer
.command([
464 # Don't use the cache
467 # Return the UUID only
468 "--match-tag", "UUID",
470 # Only return the value
473 # Operate on this device
477 # Remove the trailing newline
480 def make_fstab_entry(self
):
482 Returns a /etc/fstab entry for this partition
484 # The bootloader partition does not get an entry
485 if self
.name
== "BOOTLDR":
488 return "UUID=%s %s %s defaults 0 0\n" % (
490 self
.mountpoint
or "none",
492 # Let the kernel automatically detect the filesystem unless it is swap space
493 "swap" if self
.filesystem
== "swap" else "auto",
497 class Scan(step
.Step
):
499 with self
.tui
.progress(
500 _("Scanning for Disks"),
501 _("Scanning for disks..."),
503 self
.bricklayer
.disks
.scan()
506 class UnattendedSelectDisk(step
.UnattendedStep
):
511 # Nothing to do if disks have already been selected on the CLI
512 if self
.bricklayer
.disks
.selected
:
515 # End here if we could not find any disks
516 if not self
.bricklayer
.disks
.supported
:
519 _("No supported disks were found")
522 raise InstallAbortedError("No disks found")
524 # Automatically select the first disk
525 for disk
in self
.bricklayer
.disks
.supported
:
530 class SelectDisk(step
.InteractiveStep
):
532 Ask the user which disk(s) to use for the installation process
535 # Create a dictionary with all disks
536 disks
= { disk
: "%s" % disk
for disk
in self
.bricklayer
.disks
.supported
}
538 # Show an error if no suitable disks were found
542 _("No supported disks were found")
545 raise InstallAbortedError("No disks found")
547 # Get the current selection
548 selection
= [disk
for disk
in disks
if disk
.selected
]
552 selection
= self
.tui
.select(
554 _("Please select all disks for installation"),
555 disks
, default
=selection
, multi
=True, width
=60,
558 # Is at least one disk selected?
561 _("No Disk Selected"),
562 _("Please select a disk to continue the installation"),
569 disk
.selected
= disk
in selection
574 class CalculatePartitionLayout(step
.Step
):
576 Calculates the partition layout
579 # This probably will be fast enough that we do not need to show anything
582 self
.bricklayer
.disks
.calculate_partition_layout()
585 class CreatePartitionLayout(step
.Step
):
587 Creates the desired partition layout on disk
590 log
.debug("Creating partitions")
592 with self
.tui
.progress(
593 _("Creating Partition Layout"),
594 _("Creating partition layout..."),
596 for disk
in self
.bricklayer
.disks
.selected
:
600 class CreateFilesystems(step
.Step
):
602 Formats all newly created partitions
605 for disk
in self
.bricklayer
.disks
.selected
:
606 for partition
in disk
.partitions
:
607 with self
.tui
.progress(
608 _("Creating Filesystems"),
609 _("Formatting partition \"%s\"...") % (partition
.name
or partition
.path
)
614 class MountFilesystems(step
.Step
):
616 Mount all filesystems
619 with self
.tui
.progress(
620 _("Mounting Filesystems"),
621 _("Mounting filesystems..."),
623 self
.bricklayer
.disks
.mount()
626 class UmountFilesystems(step
.Step
):
628 Umount all filesystems
631 with self
.tui
.progress(
632 _("Umounting Filesystems"),
633 _("Umounting filesystems..."),
636 self
.bricklayer
.disks
.umount()
639 class WriteFilesystemTable(step
.Step
):
641 self
.bricklayer
.disks
.write_fstab()