]>
Commit | Line | Data |
---|---|---|
ce85f2b8 MT |
1 | ############################################################################### |
2 | # # | |
3 | # Bricklayer - An Installer for IPFire # | |
4 | # Copyright (C) 2021 IPFire Development Team # | |
5 | # # | |
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. # | |
10 | # # | |
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. # | |
15 | # # | |
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/>. # | |
18 | # # | |
19 | ############################################################################### | |
20 | ||
21 | import logging | |
5567dbe8 | 22 | import os |
ce85f2b8 | 23 | import parted |
5567dbe8 | 24 | import stat |
ce85f2b8 MT |
25 | |
26 | from . import step | |
89990e4c | 27 | from . import util |
ce85f2b8 MT |
28 | from .errors import * |
29 | from .i18n import _ | |
30 | ||
31 | # Setup logging | |
32 | log = logging.getLogger("bricklayer.disk") | |
33 | ||
f39f51ad | 34 | DEFAULT_FILESYSTEM = "btrfs" |
3f0095dd | 35 | |
d8389ba0 MT |
36 | # Check if parted.PARTITION_ESP is set |
37 | try: | |
38 | parted.PARTITION_ESP | |
39 | except AttributeError: | |
40 | parted.PARTITION_ESP = 18 | |
41 | ||
ce85f2b8 MT |
42 | class Disks(object): |
43 | """ | |
44 | Disks abstraction class | |
45 | """ | |
46 | def __init__(self, bricklayer): | |
47 | self.bricklayer = bricklayer | |
48 | ||
49 | # Disks | |
50 | self.disks = [] | |
51 | ||
52 | def scan(self): | |
53 | """ | |
54 | Scans for all disks | |
55 | """ | |
016d1cdf MT |
56 | # Don't scan for disks if already done |
57 | if self.disks: | |
58 | return | |
ce85f2b8 MT |
59 | |
60 | log.debug("Scanning for disks...") | |
61 | ||
5567dbe8 MT |
62 | for device in parted.getAllDevices(): |
63 | disk = Disk(self.bricklayer, device) | |
89990e4c | 64 | |
5567dbe8 MT |
65 | # Skip whatever isn't suitable |
66 | if not disk.supported: | |
67 | continue | |
ce85f2b8 | 68 | |
5567dbe8 | 69 | self.disks.append(disk) |
ce85f2b8 MT |
70 | |
71 | # Sort them alphabetically | |
72 | self.disks.sort() | |
73 | ||
016d1cdf | 74 | def add_disk(self, path, selected=False): |
89990e4c | 75 | """ |
5567dbe8 | 76 | Adds the disk at path |
89990e4c | 77 | """ |
5567dbe8 MT |
78 | # Check if the disk is already on the list |
79 | for disk in self.disks: | |
80 | if disk.path == path: | |
81 | return disk | |
89990e4c | 82 | |
5567dbe8 | 83 | st = os.stat(path) |
c96be18e | 84 | |
5567dbe8 MT |
85 | # Setup regular files as loop devices |
86 | if stat.S_ISREG(st.st_mode): | |
87 | path = self._losetup(path) | |
89990e4c MT |
88 | |
89 | # Create a Disk object | |
5567dbe8 | 90 | disk = Disk(self.bricklayer, path) |
89990e4c MT |
91 | self.disks.append(disk) |
92 | ||
016d1cdf MT |
93 | # Select this disk |
94 | if selected: | |
95 | disk.selected = True | |
96 | ||
5567dbe8 MT |
97 | return disk |
98 | ||
c96be18e MT |
99 | def _losetup(self, path): |
100 | # Find a free loop device | |
101 | device = self.bricklayer.command(["losetup", "-f"]) | |
c96be18e MT |
102 | device = device.rstrip() |
103 | ||
104 | # Connect the image to the loop device | |
105 | self.bricklayer.command(["losetup", device, path]) | |
106 | ||
107 | # Return the name of the loop device | |
108 | return device | |
109 | ||
ce85f2b8 MT |
110 | @property |
111 | def supported(self): | |
112 | """ | |
113 | The supported disks | |
114 | """ | |
115 | return [disk for disk in self.disks if disk.supported] | |
116 | ||
117 | @property | |
118 | def selected(self): | |
119 | """ | |
120 | The selected disks | |
121 | """ | |
122 | return [disk for disk in self.disks if disk.selected] | |
123 | ||
3f0095dd MT |
124 | def calculate_partition_layout(self): |
125 | """ | |
126 | This creates the partition layout, but doesn't write it to disk, yet | |
127 | """ | |
128 | # Find the root device | |
129 | root = self.selected[0] # XXX select the first harddisk only | |
130 | ||
131 | # Create one giant root partition | |
132 | root.create_system_partitions() | |
133 | ||
4f78c42f MT |
134 | def _find_partition(self, name): |
135 | """ | |
136 | Returns the partition with name | |
137 | """ | |
138 | for disk in self.selected: | |
139 | for partition in disk.partitions: | |
140 | if partition.name == name: | |
141 | return partition | |
142 | ||
143 | def mount(self): | |
144 | """ | |
145 | Mounts all filesystems | |
146 | """ | |
147 | # Find root partition | |
148 | partition = self._find_partition("ROOT") | |
149 | if not partition: | |
150 | FileNotFoundError("Could not find root partition") | |
151 | ||
152 | # Mount the root partition | |
153 | self._mount(partition.path, self.bricklayer.root) | |
154 | ||
155 | # Find ESP partition | |
156 | partition = self._find_partition("ESP") | |
157 | if partition: | |
158 | self._mount(partition.path, os.path.join(self.bricklayer.root, "boot/efi")) | |
159 | ||
160 | def _mount(self, source, target): | |
161 | """ | |
162 | Mounts source to target | |
163 | """ | |
164 | # Make sure the target exists | |
165 | os.makedirs(target, exist_ok=True) | |
166 | ||
167 | # Call mount(8) | |
168 | self.bricklayer.command(["mount", source, target]) | |
169 | ||
170 | def umount(self): | |
171 | """ | |
172 | Umounts all filesystems | |
173 | """ | |
174 | # Umount everything mounted in root | |
175 | self.bricklayer.command(["umount", "-Rv", self.bricklayer.root], error_ok=True) | |
176 | ||
f193c98a MT |
177 | def tear_down(self): |
178 | """ | |
179 | Shuts down any storage | |
180 | """ | |
4f78c42f MT |
181 | self.umount() |
182 | ||
f193c98a MT |
183 | for disk in self.disks: |
184 | disk.tear_down() | |
185 | ||
ce85f2b8 MT |
186 | |
187 | class Disk(object): | |
188 | def __init__(self, bricklayer, device): | |
189 | self.bricklayer = bricklayer | |
190 | ||
191 | # The parted device | |
5567dbe8 MT |
192 | if not isinstance(device, parted.Device): |
193 | device = parted.Device(device) | |
194 | ||
ce85f2b8 MT |
195 | self.device = device |
196 | ||
3f0095dd MT |
197 | # The parted disk (with a blank partition table) |
198 | self.parted = parted.freshDisk(self.device, "gpt") | |
199 | ||
ce85f2b8 MT |
200 | # Has this device been selected? |
201 | self.selected = False | |
202 | ||
3f0095dd MT |
203 | # Where are we starting with the next partition? |
204 | self._start = 1 | |
205 | ||
ce85f2b8 | 206 | def __repr__(self): |
3f0095dd | 207 | return "<%s %s>" % (self.__class__.__name__, self.path) |
ce85f2b8 MT |
208 | |
209 | def __str__(self): | |
aeeca31e | 210 | return "%s (%s)" % (self.model, util.format_size(self.size)) |
ce85f2b8 MT |
211 | |
212 | def __hash__(self): | |
213 | return hash(self.path) | |
214 | ||
215 | def __eq__(self, other): | |
216 | if isinstance(other, self.__class__): | |
217 | return self.device == other.device | |
218 | ||
219 | return NotImplemented | |
220 | ||
221 | def __lt__(self, other): | |
222 | if isinstance(other, self.__class__): | |
223 | return self.model < other.model or self.path < other.path | |
224 | ||
225 | return NotImplemented | |
226 | ||
227 | @property | |
228 | def supported(self): | |
229 | """ | |
230 | Is this device supported? | |
231 | """ | |
65f94856 MT |
232 | # Skip any device-mapper devices |
233 | if self.device.type == parted.DEVICE_DM: | |
234 | return False | |
235 | ||
236 | # Skip any CD/DVD drives | |
237 | if self.device.path.startswith("/dev/sr"): | |
238 | return False | |
239 | ||
240 | # Skip MDRAID devices | |
241 | if self.device.path.startswith("/dev/md"): | |
242 | return False | |
243 | ||
ce85f2b8 MT |
244 | # We do not support read-only devices |
245 | if self.device.readOnly: | |
246 | return False | |
247 | ||
248 | # Ignore any busy devices | |
249 | if self.device.busy: | |
250 | return False | |
251 | ||
252 | return True | |
253 | ||
254 | @property | |
255 | def path(self): | |
256 | return self.device.path | |
257 | ||
258 | @property | |
259 | def model(self): | |
aeeca31e MT |
260 | return self.device.model or _("Unknown Model") |
261 | ||
262 | @property | |
263 | def size(self): | |
264 | return self.device.length * self.device.sectorSize | |
ce85f2b8 | 265 | |
6aaa8c32 MT |
266 | @property |
267 | def partitions(self): | |
268 | """ | |
269 | Returns a list of all partitions on this device | |
270 | """ | |
271 | return [Partition(self.bricklayer, p) for p in self.parted.partitions] | |
272 | ||
3f0095dd MT |
273 | def create_system_partitions(self): |
274 | """ | |
275 | This method creates a basic partition layout on this disk with all | |
276 | partitions that the systems needs. This is as follows: | |
277 | ||
278 | 1) BIOS boot partition (used for GRUB, etc.) | |
279 | 2) A FAT-formatted EFI partition | |
280 | 3) A swap partition | |
281 | 4) A / partition | |
282 | """ | |
283 | log.debug("Creating partition layout on %s" % self.path) | |
284 | ||
285 | # Create a bootloader partition of exactly 1 MiB | |
140eee62 MT |
286 | if any((bl.requires_bootldr_partition for bl in self.bricklayer.bootloaders)): |
287 | self._add_partition("BOOTLDR", DEFAULT_FILESYSTEM, length=1024**2, | |
288 | flags=[parted.PARTITION_BIOS_GRUB]) | |
3f0095dd MT |
289 | |
290 | # Create an EFI-partition of exactly 32 MiB | |
140eee62 MT |
291 | if any((bl.requires_efi_partition for bl in self.bricklayer.bootloaders)): |
292 | self._add_partition("ESP", "fat32", length=32 * 1024**2, | |
293 | flags=[parted.PARTITION_ESP]) | |
3f0095dd MT |
294 | |
295 | # Create a swap partition | |
296 | swap_size = self.bricklayer.settings.get("swap-size", 0) | |
297 | if swap_size: | |
298 | self._add_partition("SWAP", "linux-swap(v1)", length=swap_size) | |
299 | ||
300 | # Use all remaining space for root | |
301 | self._add_partition("ROOT", DEFAULT_FILESYSTEM) | |
302 | ||
303 | def _add_partition(self, name, filesystem, length=None, flags=[]): | |
304 | # The entire device | |
305 | if length is None: | |
306 | length = self.device.getLength() - self._start | |
307 | ||
308 | # Otherwise convert bytes into sectors | |
309 | else: | |
310 | length = length // self.device.sectorSize | |
311 | ||
312 | # Calculate the partitions geometry | |
313 | geometry = parted.Geometry(self.device, start=self._start, length=length) | |
314 | ||
315 | # Create the filesystem | |
316 | fs = parted.FileSystem(type=filesystem, geometry=geometry) | |
317 | ||
318 | # Create the partition | |
319 | partition = parted.Partition(disk=self.parted, type=parted.PARTITION_NORMAL, | |
320 | fs=fs, geometry=geometry) | |
321 | ||
322 | # Set name | |
323 | partition.name = name | |
324 | ||
325 | # Set flags | |
326 | for flag in flags: | |
327 | partition.setFlag(flag) | |
328 | ||
329 | # Add the partition and align the most optimal way | |
330 | self.parted.addPartition(partition, | |
331 | constraint=self.device.optimalAlignedConstraint) | |
332 | ||
333 | # Store end to know where to begin the next partition | |
334 | self._start = geometry.end + 1 | |
335 | ||
336 | def commit(self): | |
337 | """ | |
338 | Write the partition table to disk | |
339 | """ | |
49df51b8 MT |
340 | # Destroy any existing partition table |
341 | self.device.clobber() | |
342 | ||
343 | # Write the new partition table | |
3f0095dd MT |
344 | self.parted.commit() |
345 | ||
6aaa8c32 MT |
346 | # For loop devices, we have to manually create the partition mappings |
347 | if self.path.startswith("/dev/loop"): | |
348 | self.bricklayer.command(["kpartx", "-av", self.path]) | |
349 | ||
f193c98a MT |
350 | def tear_down(self): |
351 | """ | |
352 | Shuts down this disk | |
353 | """ | |
354 | # Unmap partitions on loop devices | |
355 | if self.path.startswith("/dev/loop"): | |
356 | self.bricklayer.command(["kpartx", "-dv", self.path]) | |
357 | ||
358 | # Free the loop device | |
359 | self.bricklayer.command(["losetup", "-d", self.path]) | |
360 | ||
6aaa8c32 MT |
361 | |
362 | class Partition(object): | |
363 | def __init__(self, bricklayer, parted): | |
364 | self.bricklayer = bricklayer | |
365 | ||
366 | # The parted device | |
367 | self.parted = parted | |
368 | ||
369 | def __repr__(self): | |
370 | return "<%s %s>" % (self.__class__.__name__, self.name or self.path) | |
371 | ||
372 | @property | |
373 | def name(self): | |
374 | return self.parted.name | |
375 | ||
376 | @property | |
377 | def path(self): | |
378 | # Map path for loop devices | |
379 | if self.parted.path.startswith("/dev/loop"): | |
380 | return self.parted.path.replace("/dev/loop", "/dev/mapper/loop") | |
381 | ||
382 | return self.parted.path | |
383 | ||
384 | def wipe(self): | |
385 | """ | |
386 | Wipes the entire partition (i.e. writes zeroes) | |
387 | """ | |
388 | log.info("Wiping %s (%s)..." % (self.name, self.path)) | |
389 | ||
390 | zero = bytearray(1024) | |
391 | ||
392 | with open(self.path, "wb") as f: | |
393 | f.write(zero) | |
394 | ||
395 | def format(self): | |
396 | """ | |
397 | Formats the filesystem | |
398 | """ | |
399 | # Wipe BIOS_GRUB partitions instead of formatting them | |
400 | if self.parted.getFlag(parted.PARTITION_BIOS_GRUB): | |
401 | return self.wipe() | |
402 | ||
403 | # Fetch file-system type | |
404 | filesystem = self.parted.fileSystem.type | |
405 | ||
406 | log.info("Formatting %s (%s) with %s..." % (self.name, self.path, filesystem)) | |
407 | ||
408 | if filesystem == "fat32": | |
409 | command = ["mkfs.vfat", self.path] | |
410 | elif filesystem == "linux-swap(v1)": | |
411 | command = ["mkswap", "-v1", self.path] | |
412 | else: | |
f39f51ad | 413 | command = ["mkfs.%s" % filesystem, "-f", self.path] |
6aaa8c32 MT |
414 | |
415 | # Run command | |
416 | self.bricklayer.command(command) | |
417 | ||
ce85f2b8 | 418 | |
016d1cdf MT |
419 | class UnattendedSelectDisk(step.UnattendedStep): |
420 | """ | |
421 | Scans for any disks | |
422 | """ | |
423 | def run(self): | |
424 | # Nothing to do if disks have already been selected on the CLI | |
425 | if self.bricklayer.disks.selected: | |
426 | return | |
427 | ||
428 | # Scan for disks | |
429 | self.bricklayer.disks.scan() | |
430 | ||
431 | # End here if we could not find any disks | |
432 | if not self.bricklayer.disks.supported: | |
433 | self.tui.error( | |
434 | _("No Disks Found"), | |
435 | _("No supported disks were found") | |
436 | ) | |
437 | ||
438 | raise InstallAbortedError("No disks found") | |
439 | ||
440 | # Automatically select the first disk | |
441 | for disk in self.bricklayer.disks.supported: | |
442 | disk.selected = True | |
443 | break | |
444 | ||
445 | ||
59cf3c62 | 446 | class SelectDisk(step.InteractiveStep): |
ce85f2b8 MT |
447 | """ |
448 | Ask the user which disk(s) to use for the installation process | |
449 | """ | |
e4fb5285 | 450 | def run(self): |
ce85f2b8 | 451 | # Scan for disks |
e4fb5285 | 452 | self.bricklayer.disks.scan() |
ce85f2b8 | 453 | |
ce85f2b8 | 454 | # Create a dictionary with all disks |
e4fb5285 | 455 | disks = { disk : "%s" % disk for disk in self.bricklayer.disks.supported } |
ce85f2b8 MT |
456 | |
457 | # Show an error if no suitable disks were found | |
458 | if not disks: | |
dee72c34 | 459 | self.tui.error( |
ce85f2b8 MT |
460 | _("No Disks Found"), |
461 | _("No supported disks were found") | |
462 | ) | |
463 | ||
464 | raise InstallAbortedError("No disks found") | |
465 | ||
466 | # Get the current selection | |
467 | selection = [disk for disk in disks if disk.selected] | |
468 | ||
469 | while True: | |
470 | # Select disks | |
dee72c34 | 471 | selection = self.tui.select( |
ce85f2b8 MT |
472 | _("Disk Selection"), |
473 | _("Please select all disks for installation"), | |
baf888f4 | 474 | disks, default=selection, multi=True, width=60, |
ce85f2b8 MT |
475 | ) |
476 | ||
477 | # Is at least one disk selected? | |
478 | if not selection: | |
dee72c34 | 479 | self.tui.error( |
ce85f2b8 MT |
480 | _("No Disk Selected"), |
481 | _("Please select a disk to continue the installation"), | |
482 | buttons=[_("Back")], | |
483 | ) | |
484 | continue | |
485 | ||
486 | # Apply selection | |
487 | for disk in disks: | |
488 | disk.selected = disk in selection | |
489 | ||
490 | break | |
3f0095dd MT |
491 | |
492 | ||
493 | class CalculatePartitionLayout(step.Step): | |
494 | """ | |
495 | Calculates the partition layout | |
496 | """ | |
dee72c34 | 497 | def run(self): |
3f0095dd MT |
498 | # This probably will be fast enough that we do not need to show anything |
499 | ||
500 | # Perform the job | |
501 | self.bricklayer.disks.calculate_partition_layout() | |
502 | ||
503 | ||
504 | class CreatePartitionLayout(step.Step): | |
505 | """ | |
506 | Creates the desired partition layout on disk | |
507 | """ | |
dee72c34 | 508 | def run(self): |
3f0095dd MT |
509 | log.debug("Creating partitions") |
510 | ||
dee72c34 | 511 | with self.tui.progress( |
766749e0 MT |
512 | _("Creating Partition Layout"), |
513 | _("Creating partition layout..."), | |
514 | ): | |
515 | for disk in self.bricklayer.disks.selected: | |
516 | disk.commit() | |
6aaa8c32 MT |
517 | |
518 | ||
519 | class CreateFilesystems(step.Step): | |
520 | """ | |
521 | Formats all newly created partitions | |
522 | """ | |
dee72c34 | 523 | def run(self): |
6aaa8c32 MT |
524 | for disk in self.bricklayer.disks.selected: |
525 | for partition in disk.partitions: | |
dee72c34 | 526 | with self.tui.progress( |
fbbd02fe MT |
527 | _("Creating Filesystems"), |
528 | _("Formatting partition \"%s\"...") % (partition.name or partition.path) | |
529 | ): | |
530 | partition.format() | |
4f78c42f MT |
531 | |
532 | ||
533 | class MountFilesystems(step.Step): | |
534 | """ | |
535 | Mount all filesystems | |
536 | """ | |
dee72c34 MT |
537 | def run(self): |
538 | with self.tui.progress( | |
4f78c42f MT |
539 | _("Mounting Filesystems"), |
540 | _("Mounting filesystems..."), | |
541 | ): | |
542 | self.bricklayer.disks.mount() | |
543 | ||
544 | ||
545 | class UmountFilesystems(step.Step): | |
546 | """ | |
547 | Umount all filesystems | |
548 | """ | |
dee72c34 MT |
549 | def run(self): |
550 | with self.tui.progress( | |
4f78c42f MT |
551 | _("Umounting Filesystems"), |
552 | _("Umounting filesystems..."), | |
553 | ): | |
554 | self.bricklayer.disks.umount() |