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