1 /*
2 SPDX-FileCopyrightText: 2008-2012 Volker Lanz <vl@fidra.de>
3 SPDX-FileCopyrightText: 2012-2020 Andrius Štikonas <andrius@stikonas.eu>
4 SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
5 SPDX-FileCopyrightText: 2016 Chantara Tith <tith.chantara@gmail.com>
6 SPDX-FileCopyrightText: 2018 Caio Jordão Carvalho <caiojcarvalho@gmail.com>
7
8 SPDX-License-Identifier: GPL-3.0-or-later
9 */
10
11 #include "ops/resizeoperation.h"
12
13 #include "core/partition.h"
14 #include "core/device.h"
15 #include "core/lvmdevice.h"
16 #include "core/partitiontable.h"
17 #include "core/copysourcedevice.h"
18 #include "core/copytargetdevice.h"
19
20 #include "jobs/checkfilesystemjob.h"
21 #include "jobs/setpartgeometryjob.h"
22 #include "jobs/resizefilesystemjob.h"
23 #include "jobs/movefilesystemjob.h"
24
25 #include "ops/checkoperation.h"
26
27 #include "fs/filesystem.h"
28 #include "fs/luks.h"
29
30 #include "util/capacity.h"
31 #include "util/report.h"
32
33 #include <QDebug>
34 #include <QString>
35
36 #include <KLocalizedString>
37
38 /** Creates a new ResizeOperation.
39 @param d the Device to resize a Partition on
40 @param p the Partition to resize
41 @param newfirst the new first sector of the Partition
42 @param newlast the new last sector of the Partition
43 */
ResizeOperation(Device & d,Partition & p,qint64 newfirst,qint64 newlast)44 ResizeOperation::ResizeOperation(Device& d, Partition& p, qint64 newfirst, qint64 newlast) :
45 Operation(),
46 m_TargetDevice(d),
47 m_Partition(p),
48 m_OrigFirstSector(partition().firstSector()),
49 m_OrigLastSector(partition().lastSector()),
50 m_NewFirstSector(newfirst),
51 m_NewLastSector(newlast),
52 m_CheckOriginalJob(new CheckFileSystemJob(partition())),
53 m_MoveExtendedJob(nullptr),
54 m_ShrinkResizeJob(nullptr),
55 m_ShrinkSetGeomJob(nullptr),
56 m_MoveSetGeomJob(nullptr),
57 m_MoveFileSystemJob(nullptr),
58 m_GrowResizeJob(nullptr),
59 m_GrowSetGeomJob(nullptr),
60 m_CheckResizedJob(nullptr)
61 {
62 if (CheckOperation::canCheck(&partition()))
63 addJob(checkOriginalJob());
64
65 if (partition().roles().has(PartitionRole::Extended)) {
66 m_MoveExtendedJob = new SetPartGeometryJob(targetDevice(), partition(), newFirstSector(), newLength());
67 addJob(moveExtendedJob());
68 } else {
69 if (resizeAction() & Shrink) {
70 m_ShrinkResizeJob = new ResizeFileSystemJob(targetDevice(), partition(), newLength());
71 m_ShrinkSetGeomJob = new SetPartGeometryJob(targetDevice(), partition(), partition().firstSector(), newLength());
72
73 addJob(shrinkResizeJob());
74 addJob(shrinkSetGeomJob());
75 }
76
77 if ((resizeAction() & MoveLeft) || (resizeAction() & MoveRight)) {
78 // At this point, we need to set the partition's length to either the resized length, if it has already been
79 // shrunk, or to the original length (it may or may not then later be grown, we don't care here)
80 const qint64 currentLength = (resizeAction() & Shrink) ? newLength() : partition().length();
81
82 m_MoveSetGeomJob = new SetPartGeometryJob(targetDevice(), partition(), newFirstSector(), currentLength);
83 m_MoveFileSystemJob = new MoveFileSystemJob(targetDevice(), partition(), newFirstSector());
84
85 addJob(moveSetGeomJob());
86 addJob(moveFileSystemJob());
87 }
88
89 if (resizeAction() & Grow) {
90 m_GrowSetGeomJob = new SetPartGeometryJob(targetDevice(), partition(), newFirstSector(), newLength());
91 m_GrowResizeJob = new ResizeFileSystemJob(targetDevice(), partition(), newLength());
92
93 addJob(growSetGeomJob());
94 addJob(growResizeJob());
95 }
96
97 m_CheckResizedJob = new CheckFileSystemJob(partition());
98
99 if(CheckOperation::canCheck(&partition()))
100 addJob(checkResizedJob());
101 }
102 }
103
targets(const Device & d) const104 bool ResizeOperation::targets(const Device& d) const
105 {
106 return d == targetDevice();
107 }
108
targets(const Partition & p) const109 bool ResizeOperation::targets(const Partition& p) const
110 {
111 return p == partition();
112 }
113
preview()114 void ResizeOperation::preview()
115 {
116 // If the operation has already been executed, the partition will of course have newFirstSector and
117 // newLastSector as first and last sector. But to remove it from its original position, we need to
118 // temporarily set these values back to where they were before the operation was executed.
119 if (partition().firstSector() == newFirstSector() && partition().lastSector() == newLastSector()) {
120 partition().setFirstSector(origFirstSector());
121 partition().setLastSector(origLastSector());
122 }
123
124 removePreviewPartition(targetDevice(), partition());
125
126 partition().setFirstSector(newFirstSector());
127 partition().setLastSector(newLastSector());
128
129 insertPreviewPartition(targetDevice(), partition());
130 }
131
undo()132 void ResizeOperation::undo()
133 {
134 removePreviewPartition(targetDevice(), partition());
135 partition().setFirstSector(origFirstSector());
136 partition().setLastSector(origLastSector());
137 insertPreviewPartition(targetDevice(), partition());
138 }
139
execute(Report & parent)140 bool ResizeOperation::execute(Report& parent)
141 {
142 bool rval = true;
143
144 Report* report = parent.newChild(description());
145
146 if (CheckOperation::canCheck(&partition()))
147 rval = checkOriginalJob()->run(*report);
148
149 if (rval) {
150 // Extended partitions are a special case: They don't have any file systems and so there's no
151 // need to move, shrink or grow their contents before setting the new geometry. In fact, trying
152 // to first shrink THEN move would not work for an extended partition that has children, because
153 // they might temporarily be outside the extended partition and the backend would not let us do that.
154 if (moveExtendedJob()) {
155 if (!(rval = moveExtendedJob()->run(*report)))
156 report->line() << xi18nc("@info:status", "Moving extended partition <filename>%1</filename> failed.", partition().deviceNode());
157 } else {
158 // We run all three methods. Any of them returns true if it has nothing to do.
159 rval = shrink(*report) && move(*report) && grow(*report);
160
161 if (rval) {
162 if (CheckOperation::canCheck(&partition())) {
163 rval = checkResizedJob()->run(*report);
164 if (!rval)
165 report->line() << xi18nc("@info:status", "Checking partition <filename>%1</filename> after resize/move failed.", partition().deviceNode());
166 }
167 } else
168 report->line() << xi18nc("@info:status", "Resizing/moving partition <filename>%1</filename> failed.", partition().deviceNode());
169 }
170 } else
171 report->line() << xi18nc("@info:status", "Checking partition <filename>%1</filename> before resize/move failed.", partition().deviceNode());
172
173 setStatus(rval ? StatusFinishedSuccess : StatusError);
174
175 report->setStatus(xi18nc("@info:status (success, error, warning...) of operation", "%1: %2", description(), statusText()));
176
177 return rval;
178 }
179
description() const180 QString ResizeOperation::description() const
181 {
182 // There are eight possible things a resize operation might do:
183 // 1) Move a partition to the left (closer to the start of the disk)
184 // 2) Move a partition to the right (closer to the end of the disk)
185 // 3) Grow a partition
186 // 4) Shrink a partition
187 // 5) Move a partition to the left and grow it
188 // 6) Move a partition to the right and grow it
189 // 7) Move a partition to the left and shrink it
190 // 8) Move a partition to the right and shrink it
191 // Each of these needs a different description. And for reasons of i18n, we cannot
192 // just concatenate strings together...
193
194 const QString moveDelta = Capacity::formatByteSize(qAbs(newFirstSector() - origFirstSector()) * targetDevice().logicalSize());
195
196 const QString origCapacity = Capacity::formatByteSize(origLength() * targetDevice().logicalSize());
197 const QString newCapacity = Capacity::formatByteSize(newLength() * targetDevice().logicalSize());
198
199 switch (resizeAction()) {
200 case MoveLeft:
201 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the left by %2", partition().deviceNode(), moveDelta);
202
203 case MoveRight:
204 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the right by %2", partition().deviceNode(), moveDelta);
205
206 case Grow:
207 return xi18nc("@info:status describe resize/move action", "Grow partition <filename>%1</filename> from %2 to %3", partition().deviceNode(), origCapacity, newCapacity);
208
209 case Shrink:
210 return xi18nc("@info:status describe resize/move action", "Shrink partition <filename>%1</filename> from %2 to %3", partition().deviceNode(), origCapacity, newCapacity);
211
212 case MoveLeftGrow:
213 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the left by %2 and grow it from %3 to %4", partition().deviceNode(), moveDelta, origCapacity, newCapacity);
214
215 case MoveRightGrow:
216 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the right by %2 and grow it from %3 to %4", partition().deviceNode(), moveDelta, origCapacity, newCapacity);
217
218 case MoveLeftShrink:
219 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the left by %2 and shrink it from %3 to %4", partition().deviceNode(), moveDelta, origCapacity, newCapacity);
220
221 case MoveRightShrink:
222 return xi18nc("@info:status describe resize/move action", "Move partition <filename>%1</filename> to the right by %2 and shrink it from %3 to %4", partition().deviceNode(), moveDelta, origCapacity, newCapacity);
223
224 case None:
225 qWarning() << "Could not determine what to do with partition " << partition().deviceNode() << ".";
226 break;
227 }
228
229 return xi18nc("@info:status describe resize/move action", "Unknown resize/move action.");
230 }
231
resizeAction() const232 ResizeOperation::ResizeAction ResizeOperation::resizeAction() const
233 {
234 ResizeAction action = None;
235
236 // Grow?
237 if (newLength() > origLength())
238 action = Grow;
239
240 // Shrink?
241 if (newLength() < origLength())
242 action = Shrink;
243
244 // Move to the right?
245 if (newFirstSector() > origFirstSector())
246 action = static_cast<ResizeAction>(action | MoveRight);
247
248 // Move to the left?
249 if (newFirstSector() < origFirstSector())
250 action = static_cast<ResizeAction>(action | MoveLeft);
251
252 return action;
253 }
254
shrink(Report & report)255 bool ResizeOperation::shrink(Report& report)
256 {
257 if (shrinkResizeJob() && !shrinkResizeJob()->run(report)) {
258 report.line() << xi18nc("@info:status", "Resize/move failed: Could not resize file system to shrink partition <filename>%1</filename>.", partition().deviceNode());
259 return false;
260 }
261
262 if (shrinkSetGeomJob() && !shrinkSetGeomJob()->run(report)) {
263 report.line() << xi18nc("@info:status", "Resize/move failed: Could not shrink partition <filename>%1</filename>.", partition().deviceNode());
264 return false;
265
266 /** @todo if this fails, no one undoes the shrinking of the file system above, because we
267 rely upon there being a maximize job at the end, but that's no longer the case. */
268 }
269
270 return true;
271 }
272
move(Report & report)273 bool ResizeOperation::move(Report& report)
274 {
275 // We must make sure not to overwrite the partition's metadata if it's a logical partition
276 // and we're moving to the left. The easiest way to achieve this is to move the
277 // partition itself first (it's the backend's responsibility to then move the metadata) and
278 // only afterwards copy the filesystem. Disadvantage: We need to move the partition
279 // back to its original position if copyBlocks fails.
280 const qint64 oldStart = partition().firstSector();
281 if (moveSetGeomJob() && !moveSetGeomJob()->run(report)) {
282 report.line() << xi18nc("@info:status", "Moving partition <filename>%1</filename> failed.", partition().deviceNode());
283 return false;
284 }
285
286 if (moveFileSystemJob() && !moveFileSystemJob()->run(report)) {
287 report.line() << xi18nc("@info:status", "Moving the filesystem for partition <filename>%1</filename> failed. Rolling back.", partition().deviceNode());
288
289 // see above: We now have to move back the partition itself.
290 if (!SetPartGeometryJob(targetDevice(), partition(), oldStart, partition().length()).run(report))
291 report.line() << xi18nc("@info:status", "Moving back partition <filename>%1</filename> to its original position failed.", partition().deviceNode());
292
293 return false;
294 }
295
296 return true;
297 }
298
grow(Report & report)299 bool ResizeOperation::grow(Report& report)
300 {
301 const qint64 oldLength = partition().length();
302
303 if (growSetGeomJob() && !growSetGeomJob()->run(report)) {
304 report.line() << xi18nc("@info:status", "Resize/move failed: Could not grow partition <filename>%1</filename>.", partition().deviceNode());
305 return false;
306 }
307
308 if (growResizeJob() && !growResizeJob()->run(report)) {
309 report.line() << xi18nc("@info:status", "Resize/move failed: Could not resize the file system on partition <filename>%1</filename>", partition().deviceNode());
310
311 if (!SetPartGeometryJob(targetDevice(), partition(), partition().firstSector(), oldLength).run(report))
312 report.line() << xi18nc("@info:status", "Could not restore old partition size for partition <filename>%1</filename>.", partition().deviceNode());
313
314 return false;
315 }
316
317 return true;
318 }
319
320 /** Can a Partition be grown, i.e. increased in size?
321 @param p the Partition in question, may be nullptr.
322 @return true if @p p can be grown.
323 */
canGrow(const Partition * p)324 bool ResizeOperation::canGrow(const Partition* p)
325 {
326 if (p == nullptr)
327 return false;
328
329 // Whole block device filesystems cannot be resized
330 if (p->partitionTable()->type() == PartitionTable::TableType::none)
331 return false;
332
333 if (isLVMPVinNewlyVG(p))
334 return false;
335
336 // we can always grow, shrink or move a partition not yet written to disk
337 if (p->state() == Partition::State::New && !p->roles().has(PartitionRole::Luks))
338 return true;
339
340 if (p->isMounted())
341 return p->fileSystem().supportGrowOnline();
342
343 return p->fileSystem().supportGrow() != FileSystem::cmdSupportNone;
344 }
345
346 /** Can a Partition be shrunk, i.e. decreased in size?
347 @param p the Partition in question, may be nullptr.
348 @return true if @p p can be shrunk.
349 */
canShrink(const Partition * p)350 bool ResizeOperation::canShrink(const Partition* p)
351 {
352 if (p == nullptr)
353 return false;
354
355 // Whole block device filesystems cannot be resized
356 if (p->partitionTable()->type() == PartitionTable::TableType::none)
357 return false;
358
359 if (isLVMPVinNewlyVG(p))
360 return false;
361
362 // we can always grow, shrink or move a partition not yet written to disk
363 if (p->state() == Partition::State::New && !p->roles().has(PartitionRole::Luks))
364 return true;
365
366 if (p->state() == Partition::State::Copy)
367 return false;
368
369 if (p->isMounted())
370 return p->fileSystem().supportShrinkOnline();
371
372 return p->fileSystem().supportShrink() != FileSystem::cmdSupportNone;
373 }
374
375 /** Can a Partition be moved?
376 @param p the Partition in question, may be nullptr.
377 @return true if @p p can be moved.
378 */
canMove(const Partition * p)379 bool ResizeOperation::canMove(const Partition* p)
380 {
381 if (p == nullptr)
382 return false;
383
384 // Whole block device filesystems cannot be moved
385 if (p->partitionTable()->type() == PartitionTable::TableType::none)
386 return false;
387
388 if (isLVMPVinNewlyVG(p))
389 return false;
390
391 // we can always grow, shrink or move a partition not yet written to disk
392 if (p->state() == Partition::State::New)
393 // too many bad things can happen for LUKS partitions
394 return p->roles().has(PartitionRole::Luks) ? false : true;
395
396 if (p->isMounted())
397 return false;
398
399 // no moving of extended partitions if they have logicals
400 if (p->roles().has(PartitionRole::Extended) && p->hasChildren())
401 return false;
402
403 return p->fileSystem().supportMove() != FileSystem::cmdSupportNone;
404 }
405
isLVMPVinNewlyVG(const Partition * p)406 bool ResizeOperation::isLVMPVinNewlyVG(const Partition *p)
407 {
408 if (p->fileSystem().type() == FileSystem::Type::Lvm2_PV) {
409 if (LvmDevice::s_DirtyPVs.contains(p))
410 return true;
411 }
412 else if (p->fileSystem().type() == FileSystem::Type::Luks || p->fileSystem().type() == FileSystem::Type::Luks2) {
413 // See if innerFS is LVM
414 FileSystem *fs = static_cast<const FS::luks *>(&p->fileSystem())->innerFS();
415
416 if (fs) {
417 if (fs->type() == FileSystem::Type::Lvm2_PV) {
418 if (LvmDevice::s_DirtyPVs.contains(p))
419 return true;
420 }
421 }
422 }
423
424 return false;
425 }
426