1 /* === This file is part of Calamares - <https://calamares.io> ===
2 *
3 * SPDX-FileCopyrightText: 2015-2016 Teo Mrnjavac <teo@kde.org>
4 * SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
5 * SPDX-FileCopyrightText: 2019 Collabora Ltd <arnaud.ferraris@collabora.com>
6 * SPDX-License-Identifier: GPL-3.0-or-later
7 *
8 * Calamares is Free Software: see the License-Identifier above.
9 *
10 */
11
12 #include "PartUtils.h"
13
14 #include "core/DeviceModel.h"
15 #include "core/KPMHelpers.h"
16 #include "core/PartitionInfo.h"
17
18 #include "GlobalStorage.h"
19 #include "JobQueue.h"
20 #include "partition/Mount.h"
21 #include "partition/PartitionIterator.h"
22 #include "partition/PartitionQuery.h"
23 #include "utils/CalamaresUtilsSystem.h"
24 #include "utils/Logger.h"
25 #include "utils/RAII.h"
26
27 #include <kpmcore/backend/corebackend.h>
28 #include <kpmcore/backend/corebackendmanager.h>
29 #include <kpmcore/core/device.h>
30 #include <kpmcore/core/partition.h>
31
32 #include <QProcess>
33 #include <QTemporaryDir>
34
35 using CalamaresUtils::Partition::isPartitionFreeSpace;
36 using CalamaresUtils::Partition::isPartitionNew;
37
38 namespace PartUtils
39 {
40
41 QString
convenienceName(const Partition * const candidate)42 convenienceName( const Partition* const candidate )
43 {
44 if ( !candidate->mountPoint().isEmpty() )
45 {
46 return candidate->mountPoint();
47 }
48 if ( !candidate->partitionPath().isEmpty() )
49 {
50 return candidate->partitionPath();
51 }
52 if ( !candidate->devicePath().isEmpty() )
53 {
54 return candidate->devicePath();
55 }
56 if ( !candidate->deviceNode().isEmpty() )
57 {
58 return candidate->devicePath();
59 }
60
61 QString p;
62 QTextStream s( &p );
63 s << static_cast< const void* >( candidate ); // No good name available, use pointer address
64
65 return p;
66 }
67
68 /** @brief Get the globalStorage setting for required space. */
69 static double
getRequiredStorageGiB(bool & ok)70 getRequiredStorageGiB( bool& ok )
71 {
72 return Calamares::JobQueue::instance()->globalStorage()->value( "requiredStorageGiB" ).toDouble( &ok );
73 }
74
75 bool
canBeReplaced(Partition * candidate,const Logger::Once & o)76 canBeReplaced( Partition* candidate, const Logger::Once& o )
77 {
78 if ( !candidate )
79 {
80 cDebug() << o << "Partition* is NULL";
81 return false;
82 }
83
84 cDebug() << o << "Checking if" << convenienceName( candidate ) << "can be replaced.";
85 if ( candidate->isMounted() )
86 {
87 cDebug() << Logger::SubEntry << "NO, it is mounted.";
88 return false;
89 }
90
91 bool ok = false;
92 double requiredStorageGiB = getRequiredStorageGiB( ok );
93 if ( !ok )
94 {
95 cDebug() << Logger::SubEntry << "NO, requiredStorageGiB is not set correctly.";
96 return false;
97 }
98
99 qint64 availableStorageB = candidate->capacity();
100 qint64 requiredStorageB = CalamaresUtils::GiBtoBytes( requiredStorageGiB + 0.5 );
101
102 if ( availableStorageB > requiredStorageB )
103 {
104 cDebug() << o << "Partition" << convenienceName( candidate ) << "authorized for replace install.";
105 return true;
106 }
107 else
108 {
109 Logger::CDebug deb;
110 deb << Logger::SubEntry << "NO, insufficient storage";
111 deb << Logger::Continuation << "Required storage B:" << requiredStorageB
112 << QString( "(%1GiB)" ).arg( requiredStorageGiB );
113 deb << Logger::Continuation << "Available storage B:" << availableStorageB
114 << QString( "(%1GiB)" ).arg( CalamaresUtils::BytesToGiB( availableStorageB ) );
115 return false;
116 }
117 }
118
119
120 bool
canBeResized(Partition * candidate,const Logger::Once & o)121 canBeResized( Partition* candidate, const Logger::Once& o )
122 {
123 if ( !candidate )
124 {
125 cDebug() << o << "Partition* is NULL";
126 return false;
127 }
128
129 if ( !candidate->fileSystem().supportGrow() || !candidate->fileSystem().supportShrink() )
130 {
131 cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", filesystem"
132 << candidate->fileSystem().name() << "does not support resize.";
133 return false;
134 }
135
136 if ( isPartitionFreeSpace( candidate ) )
137 {
138 cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition is free space";
139 return false;
140 }
141
142 if ( candidate->isMounted() )
143 {
144 cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition is mounted";
145 return false;
146 }
147
148 if ( candidate->roles().has( PartitionRole::Primary ) )
149 {
150 PartitionTable* table = dynamic_cast< PartitionTable* >( candidate->parent() );
151 if ( !table )
152 {
153 cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", no partition table found";
154 return false;
155 }
156
157 if ( table->numPrimaries() >= table->maxPrimaries() )
158 {
159 cDebug() << o << "Can not resize" << convenienceName( candidate ) << ", partition table already has"
160 << table->maxPrimaries() << "primary partitions.";
161 return false;
162 }
163 }
164
165 bool ok = false;
166 double requiredStorageGiB = getRequiredStorageGiB( ok );
167 if ( !ok )
168 {
169 cDebug() << o << "Can not resize" << convenienceName( candidate )
170 << ", requiredStorageGiB is not set correctly.";
171 return false;
172 }
173
174 // We require a little more for partitioning overhead and swap file
175 double advisedStorageGiB = requiredStorageGiB + 0.5 + 2.0;
176 qint64 availableStorageB = candidate->available();
177 qint64 advisedStorageB = CalamaresUtils::GiBtoBytes( advisedStorageGiB );
178
179 if ( availableStorageB > advisedStorageB )
180 {
181 cDebug() << o << "Partition" << convenienceName( candidate )
182 << "authorized for resize + autopartition install.";
183 return true;
184 }
185 else
186 {
187 Logger::CDebug deb;
188 deb << Logger::SubEntry << "NO, insufficient storage";
189 deb << Logger::Continuation << "Required storage B:" << advisedStorageB
190 << QString( "(%1GiB)" ).arg( advisedStorageGiB );
191 deb << Logger::Continuation << "Available storage B:" << availableStorageB
192 << QString( "(%1GiB)" ).arg( CalamaresUtils::BytesToGiB( availableStorageB ) ) << "for"
193 << convenienceName( candidate ) << "length:" << candidate->length()
194 << "sectorsUsed:" << candidate->sectorsUsed() << "fsType:" << candidate->fileSystem().name();
195 return false;
196 }
197 }
198
199
200 bool
canBeResized(DeviceModel * dm,const QString & partitionPath,const Logger::Once & o)201 canBeResized( DeviceModel* dm, const QString& partitionPath, const Logger::Once& o )
202 {
203 if ( partitionPath.startsWith( "/dev/" ) )
204 {
205 for ( int i = 0; i < dm->rowCount(); ++i )
206 {
207 Device* dev = dm->deviceForIndex( dm->index( i ) );
208 Partition* candidate = CalamaresUtils::Partition::findPartitionByPath( { dev }, partitionPath );
209 if ( candidate )
210 {
211 return canBeResized( candidate, o );
212 }
213 }
214 cWarning() << "Can not resize" << partitionPath << ", no Partition* found.";
215 return false;
216 }
217 else
218 {
219 cWarning() << "Can not resize" << partitionPath << ", does not start with /dev";
220 return false;
221 }
222 }
223
224
225 static FstabEntryList
lookForFstabEntries(const QString & partitionPath)226 lookForFstabEntries( const QString& partitionPath )
227 {
228 QStringList mountOptions { "ro" };
229
230 auto r = CalamaresUtils::System::runCommand( CalamaresUtils::System::RunLocation::RunInHost,
231 { "blkid", "-s", "TYPE", "-o", "value", partitionPath } );
232 if ( r.getExitCode() )
233 {
234 cWarning() << "blkid on" << partitionPath << "failed.";
235 }
236 else
237 {
238 QString fstype = r.getOutput().trimmed();
239 if ( ( fstype == "ext3" ) || ( fstype == "ext4" ) )
240 {
241 mountOptions.append( "noload" );
242 }
243 }
244
245 cDebug() << "Checking device" << partitionPath << "for fstab (fs=" << r.getOutput() << ')';
246
247 FstabEntryList fstabEntries;
248
249 CalamaresUtils::Partition::TemporaryMount mount( partitionPath, QString(), mountOptions.join( ',' ) );
250 if ( mount.isValid() )
251 {
252 QFile fstabFile( mount.path() + "/etc/fstab" );
253
254 if ( fstabFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
255 {
256 const QStringList fstabLines = QString::fromLocal8Bit( fstabFile.readAll() ).split( '\n' );
257
258 for ( const QString& rawLine : fstabLines )
259 {
260 fstabEntries.append( FstabEntry::fromEtcFstab( rawLine ) );
261 }
262 fstabFile.close();
263 const int lineCount = fstabEntries.count();
264 std::remove_if(
265 fstabEntries.begin(), fstabEntries.end(), []( const FstabEntry& x ) { return !x.isValid(); } );
266 cDebug() << Logger::SubEntry << "got" << fstabEntries.count() << "fstab entries from" << lineCount
267 << "lines in" << fstabFile.fileName();
268 }
269 else
270 {
271 cWarning() << "Could not read fstab from mounted fs";
272 }
273 }
274 else
275 {
276 cWarning() << "Could not mount existing fs";
277 }
278
279 return fstabEntries;
280 }
281
282
283 static QString
findPartitionPathForMountPoint(const FstabEntryList & fstab,const QString & mountPoint)284 findPartitionPathForMountPoint( const FstabEntryList& fstab, const QString& mountPoint )
285 {
286 if ( fstab.isEmpty() )
287 {
288 return QString();
289 }
290
291 for ( const FstabEntry& entry : fstab )
292 {
293 if ( entry.mountPoint == mountPoint )
294 {
295 QProcess readlink;
296 QString partPath;
297
298 if ( entry.partitionNode.startsWith( "/dev" ) ) // plain dev node
299 {
300 partPath = entry.partitionNode;
301 }
302 else if ( entry.partitionNode.startsWith( "LABEL=" ) )
303 {
304 partPath = entry.partitionNode.mid( 6 );
305 partPath.remove( "\"" );
306 partPath.replace( "\\040", "\\ " );
307 partPath.prepend( "/dev/disk/by-label/" );
308 }
309 else if ( entry.partitionNode.startsWith( "UUID=" ) )
310 {
311 partPath = entry.partitionNode.mid( 5 );
312 partPath.remove( "\"" );
313 partPath = partPath.toLower();
314 partPath.prepend( "/dev/disk/by-uuid/" );
315 }
316 else if ( entry.partitionNode.startsWith( "PARTLABEL=" ) )
317 {
318 partPath = entry.partitionNode.mid( 10 );
319 partPath.remove( "\"" );
320 partPath.replace( "\\040", "\\ " );
321 partPath.prepend( "/dev/disk/by-partlabel/" );
322 }
323 else if ( entry.partitionNode.startsWith( "PARTUUID=" ) )
324 {
325 partPath = entry.partitionNode.mid( 9 );
326 partPath.remove( "\"" );
327 partPath = partPath.toLower();
328 partPath.prepend( "/dev/disk/by-partuuid/" );
329 }
330
331 // At this point we either have /dev/sda1, or /dev/disk/by-something/...
332
333 if ( partPath.startsWith( "/dev/disk/by-" ) ) // we got a fancy node
334 {
335 readlink.start( "readlink", { "-en", partPath } );
336 if ( !readlink.waitForStarted( 1000 ) )
337 {
338 return QString();
339 }
340 if ( !readlink.waitForFinished( 1000 ) )
341 {
342 return QString();
343 }
344 if ( readlink.exitCode() != 0 || readlink.exitStatus() != QProcess::NormalExit )
345 {
346 return QString();
347 }
348 partPath = QString::fromLocal8Bit( readlink.readAllStandardOutput() ).trimmed();
349 }
350
351 return partPath;
352 }
353 }
354
355 return QString();
356 }
357
358
359 OsproberEntryList
runOsprober(DeviceModel * dm)360 runOsprober( DeviceModel* dm )
361 {
362 Logger::Once o;
363
364 QString osproberOutput;
365 QProcess osprober;
366 osprober.setProgram( "os-prober" );
367 osprober.setProcessChannelMode( QProcess::SeparateChannels );
368 osprober.start();
369 if ( !osprober.waitForStarted() )
370 {
371 cError() << "os-prober cannot start.";
372 }
373 else if ( !osprober.waitForFinished( 60000 ) )
374 {
375 cError() << "os-prober timed out.";
376 }
377 else
378 {
379 osproberOutput.append( QString::fromLocal8Bit( osprober.readAllStandardOutput() ).trimmed() );
380 }
381
382 QStringList osproberCleanLines;
383 OsproberEntryList osproberEntries;
384 const auto lines = osproberOutput.split( '\n' );
385 for ( const QString& line : lines )
386 {
387 if ( !line.simplified().isEmpty() )
388 {
389 QStringList lineColumns = line.split( ':' );
390 QString prettyName;
391 if ( !lineColumns.value( 1 ).simplified().isEmpty() )
392 {
393 prettyName = lineColumns.value( 1 ).simplified();
394 }
395 else if ( !lineColumns.value( 2 ).simplified().isEmpty() )
396 {
397 prettyName = lineColumns.value( 2 ).simplified();
398 }
399
400 QString file, path = lineColumns.value( 0 ).simplified();
401 if ( !path.startsWith( "/dev/" ) ) //basic sanity check
402 {
403 continue;
404 }
405
406 // strip extra file after device: /dev/name@/path/to/file
407 int index = path.indexOf( '@' );
408 if ( index != -1 )
409 {
410 file = path.right( path.length() - index - 1 );
411 path = path.left( index );
412 }
413
414 FstabEntryList fstabEntries = lookForFstabEntries( path );
415 QString homePath = findPartitionPathForMountPoint( fstabEntries, "/home" );
416
417 osproberEntries.append( { prettyName,
418 path,
419 file,
420 QString(),
421 canBeResized( dm, path, o ),
422 lineColumns,
423 fstabEntries,
424 homePath } );
425 osproberCleanLines.append( line );
426 }
427 }
428
429 if ( osproberCleanLines.count() > 0 )
430 {
431 cDebug() << o << "os-prober lines after cleanup:" << Logger::DebugList( osproberCleanLines );
432 }
433 else
434 {
435 cDebug() << o << "os-prober gave no output.";
436 }
437
438 Calamares::JobQueue::instance()->globalStorage()->insert( "osproberLines", osproberCleanLines );
439
440 return osproberEntries;
441 }
442
443 bool
isEfiSystem()444 isEfiSystem()
445 {
446 return QDir( "/sys/firmware/efi/efivars" ).exists();
447 }
448
449 bool
isEfiFilesystemSuitableType(const Partition * candidate)450 isEfiFilesystemSuitableType( const Partition* candidate )
451 {
452 auto type = candidate->fileSystem().type();
453
454 switch ( type )
455 {
456 case FileSystem::Type::Fat32:
457 return true;
458 #ifdef WITH_KPMCORE4API
459 case FileSystem::Type::Fat12:
460 #endif
461 case FileSystem::Type::Fat16:
462 cWarning() << "FAT12 and FAT16 are probably not supported by EFI";
463 return false;
464 default:
465 cWarning() << "EFI boot partition must be FAT32";
466 return false;
467 }
468 }
469
470 bool
isEfiFilesystemSuitableSize(const Partition * candidate)471 isEfiFilesystemSuitableSize( const Partition* candidate )
472 {
473 auto size = candidate->capacity(); // bytes
474
475 using CalamaresUtils::Units::operator""_MiB;
476 if ( size >= 300_MiB )
477 {
478 return true;
479 }
480 else
481 {
482 cWarning() << "Filesystem for EFI is too small (" << size << "bytes)";
483 return false;
484 }
485 }
486
487
488 bool
isEfiBootable(const Partition * candidate)489 isEfiBootable( const Partition* candidate )
490 {
491 const auto flags = PartitionInfo::flags( candidate );
492
493 #if defined( WITH_KPMCORE4API )
494 // In KPMCore4, the flags are remapped, and the ESP flag is the same as Boot.
495 static_assert( KPM_PARTITION_FLAG_ESP == KPM_PARTITION_FLAG( Boot ), "KPMCore API enum changed" );
496 return flags.testFlag( KPM_PARTITION_FLAG_ESP );
497 #else
498 // In KPMCore3, bit 17 is the old-style Esp flag, and it's OK
499 if ( flags.testFlag( KPM_PARTITION_FLAG_ESP ) )
500 {
501 return true;
502 }
503
504 /* Otherwise, if it's a GPT table, Boot (bit 0) is the same as Esp */
505 const PartitionTable* table = CalamaresUtils::Partition::getPartitionTable( candidate );
506 if ( !table )
507 {
508 cWarning() << "Root of partition table is not a PartitionTable object";
509 return false;
510 }
511 if ( table->type() == PartitionTable::TableType::gpt )
512 {
513 const auto bootFlag = KPM_PARTITION_FLAG( Boot );
514 return flags.testFlag( bootFlag );
515 }
516 return false;
517 #endif
518 }
519
520 // TODO: this is configurable via the config file **already**
521 size_t
efiFilesystemMinimumSize()522 efiFilesystemMinimumSize()
523 {
524 using CalamaresUtils::Units::operator""_MiB;
525 return 300_MiB;
526 }
527
528
529 QString
canonicalFilesystemName(const QString & fsName,FileSystem::Type * fsType)530 canonicalFilesystemName( const QString& fsName, FileSystem::Type* fsType )
531 {
532 cScopedAssignment type( fsType );
533 if ( fsName.isEmpty() )
534 {
535 type = FileSystem::Ext4;
536 return QStringLiteral( "ext4" );
537 }
538
539 QStringList fsLanguage { QLatin1String( "C" ) }; // Required language list to turn off localization
540
541 if ( ( type = FileSystem::typeForName( fsName, fsLanguage ) ) != FileSystem::Unknown )
542 {
543 return fsName;
544 }
545
546 // Second pass: try case-insensitive
547 const auto fstypes = FileSystem::types();
548 for ( FileSystem::Type t : fstypes )
549 {
550 if ( 0 == QString::compare( fsName, FileSystem::nameForType( t, fsLanguage ), Qt::CaseInsensitive ) )
551 {
552 QString fsRealName = FileSystem::nameForType( t, fsLanguage );
553 if ( fsType )
554 {
555 *fsType = t;
556 }
557 return fsRealName;
558 }
559 }
560
561 cWarning() << "Filesystem" << fsName << "not found, using ext4";
562 // fsType can be used to check whether fsName was a valid filesystem.
563 if ( fsType )
564 {
565 *fsType = FileSystem::Unknown;
566 }
567 #ifdef DEBUG_FILESYSTEMS
568 // This bit is for distro's debugging their settings, and shows
569 // all the strings that KPMCore is matching against for FS type.
570 {
571 Logger::CDebug d;
572 using TR = Logger::DebugRow< int, QString >;
573 const auto fstypes = FileSystem::types();
574 d << "Available types (" << fstypes.count() << ')';
575 for ( FileSystem::Type t : fstypes )
576 {
577 d << TR( static_cast< int >( t ), FileSystem::nameForType( t, fsLanguage ) );
578 }
579 }
580 #endif
581 type = FileSystem::Unknown;
582 return QStringLiteral( "ext4" );
583 }
584
585 } // namespace PartUtils
586
587 /* Implementation of methods for FstabEntry, from OsproberEntry.h */
588
589 bool
isValid() const590 FstabEntry::isValid() const
591 {
592 return !partitionNode.isEmpty() && !mountPoint.isEmpty() && !fsType.isEmpty();
593 }
594
595 FstabEntry
fromEtcFstab(const QString & rawLine)596 FstabEntry::fromEtcFstab( const QString& rawLine )
597 {
598 QString line = rawLine.simplified();
599 if ( line.startsWith( '#' ) )
600 return FstabEntry { QString(), QString(), QString(), QString(), 0, 0 };
601
602 QStringList splitLine = line.split( ' ' );
603 if ( splitLine.length() != 6 )
604 return FstabEntry { QString(), QString(), QString(), QString(), 0, 0 };
605
606 return FstabEntry {
607 splitLine.at( 0 ), // path, or UUID, or LABEL, etc.
608 splitLine.at( 1 ), // mount point
609 splitLine.at( 2 ), // fs type
610 splitLine.at( 3 ), // options
611 splitLine.at( 4 ).toInt(), //dump
612 splitLine.at( 5 ).toInt() //pass
613 };
614 }
615