Shrink a laptop hard disk to an SSD🔗
Introduction
I bought a laptop in 2014 and progressively discarded (but fortunately not resell) it during COVID20 where my employer gently gave me a (quite good at the beginning) laptop: an HP EliteBook 820 G3. In 2023 this small laptop was replaced by a beefier and huge laptop: a HP ZBook Fury (I don't know why the made this choice, it is so overkilling). It is so heavy (about 3 Kg) that I decided to avoid to bring it back on train (I still work at 100km from my house and I take the train by rallying stations with my bike).
During the 2023 winter holidays, I had to use a laptop again during the travel and I decided to take back my old 2014 laptop. There is an old 2'5 (rusty) 500GB hard drive in it. I installed the latest stable version of Debian. It was a little bit slow, even with an heavy tweaked work environment (sway/foot/emacs) which is quite light compared to a mass like GNOME. The main bottleneck was the hard drive. Loading the system took about 2 minutes which is quite frustrating when you are used to less than 20 seconds.
I had an old Intel SSD device and after some tests it proved to be really faster for this small computer. I decided to use it but didn't plan to take the pain to reinstall a tweaked system on it from scratch. I started to plan to shrink the working system (on the rusty hard disk) towards the SSD and this article is the solution I found.
Step 0: System review
Before diving on the command line, we have to find informations about our devices. For convenience, the device name which will be written or read will be named /dev/sdb
on the workstation.
For the hard drive, you just have to use lsblk to grab information about disk and partition:
$ lsblk /dev/sdb NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sdb 8:16 0 465,8G 0 disk ├─sdb1 8:17 0 512M 0 part ├─sdb2 8:18 0 464,3G 0 part └─sdb3 8:19 0 976M 0 part
Much more important, the same information, but in bytes:
$ lsblk -b /dev/sdb NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sdb 8:16 0 500107862016 0 disk ├─sdb1 8:17 0 536870912 0 part ├─sdb2 8:18 0 498545459200 0 part └─sdb3 8:19 0 1023410176 0 part
And we also need to note the UUID of all the partitions as they are generally used in initramfs and by systemd/fstab (need to be root for using blkid:
# blkid /dev/sdb* /dev/sdb: PTUUID="77042c1d-f8d6-4596-8c33-9b6244d85b97" PTTYPE="gpt" /dev/sdb1: UUID="DB00-C8D9" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="93c56b16-bde9-455f-8e32-b115c8a27e61" /dev/sdb2: UUID="aad5a02a-1ade-42ae-9b59-4f76942b3404" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="dccb6d13-d8d1-4d24-aff5-fe5c0adb4381" /dev/sdb3: UUID="a40d9464-d002-44d8-9b22-ad08b1f71588" TYPE="swap" PARTUUID="072887af-2d5b-45ca-be38-9c31e9ae6b27"
You can also review the partition table with parted:
# parted /dev/sdb print Model: ATA WDC WD5000LPVX-0 (scsi) Disk /dev/sdb: 500GB Sector size (logical/physical): 512B/4096B Partition Table: gpt Disk Flags: Number Start End Size File system Name Flags 1 1049kB 538MB 537MB fat32 boot, esp 2 538MB 499GB 499GB ext4 3 499GB 500GB 1023MB linux-swap(v1) swap
You can read that we have three partitions:
- the first one is an EFI partition in fat32 filesystem for about 537MB.
- the second one is an ext4 partition with the whole system copied on it (with /, /home/, var/ etc.)
- the third one is a swap partition for about 1GB.
But much more important is the size in bytes:
# parted /dev/sdb unit b print Model: ATA WDC WD5000LPVX-0 (scsi) Disk /dev/sdb: 500107862016B Sector size (logical/physical): 512B/4096B Partition Table: gpt Disk Flags: Number Start End Size File system Name Flags 1 1048576B 537919487B 536870912B fat32 boot, esp 2 537919488B 499083378687B 498545459200B ext4 3 499083378688B 500106788863B 1023410176B linux-swap(v1) swap
And in sectors (for alignment):
# parted /dev/sdb unit s print Model: ATA WDC WD5000LPVX-0 (scsi) Disk /dev/sdb: 976773168s Sector size (logical/physical): 512B/4096B Partition Table: gpt Disk Flags: Number Start End Size File system Name Flags 1 2048s 1050623s 1048576s fat32 boot, esp 2 1050624s 974772223s 973721600s ext4 3 974772224s 976771071s 1998848s linux-swap(v1) swap
You can find that the alignment is put on 2048 sectors. To be aligned, size and start of partition must be a number where the modulo of 2048 is 0.
For reference, this is the SSD device size in GB and in bytes.
$ lsblk -d /dev/sdb NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sdb 8:16 0 167,7G 0 disk $ lsblk -b /dev/sdb NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS sdb 8:16 0 180045766656 0 disk
And the most important: you have to be sure that the total size of the stored files on the main filesystem is less than about the size of the ssd. We use dumpe2fs because our filesystem is an ext4 type.
# dumpe2fs -h /dev/sdb2 |& awk -F: '/Block count/{count=$2} /Free blocks/{free=$2} /Block size/{size=$2} END{print (count - free) * size " bytes"}' 35579392000 bytes
In our case, 35579392000 bytes is about 33GB. So it will fit in 170GB.
Step 1: Clone the disk
In this first step, we clone the disk with a simple classic command: dd
.
# dd if=/dev/sdb of=./trick.img bs=64K conv=noerror,sync status=progress
If you want to avoid working on the image file as root, feel free to modify the rights on the file for a normal user read/write access.
Once it is cloned, you can read the partition table with parted, as written above.
Step 2: Trim the disk image
Before working on it, we need to trim the image. Trimming means to zero all unused filesystem blocks. It will reduce the image allocated blocks (without reducing its declared size). And it is a requirement if you want to truncate things at the end. You can trim an image only if it is mounted as a real filesystem based on a block device. This is what loopback devices are for.
# losetup --partscan --find --show ./trick.img /dev/loop0 # lsblk /dev/loop0 NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS loop0 7:0 0 465,8G 0 loop ├─loop0p1 259:5 0 512M 0 part ├─loop0p2 259:6 0 464,3G 0 part └─loop0p3 259:7 0 976M 0 part
Now we can use fstrim to trim the image disk before compressing it. Here, we can trim only the two first partitions as we will discard the swap partition and recreate it.
# mount /dev/loop0p1 ./mnt # fstrim ./mnt # umount /dev/loop0p1 # mount /dev/loop0p2 ./mnt # fstrim ./mnt # umount /dev/loop0p2 # losetup -d /dev/loop0
Step 3: Calculations
resize2fs uses block size for units by default. In our configuration, we want to use the EFI partition as is, shrink ext4 system stuff and keep a 4GB for swap. Also, remember that we need at least 34 sectors (17408 bytes) after the end of swap partition for GPT duplication (GPT partition table is duplicated at the end of the device).
Total size of the SSD is 351651888 sectors of 512 bytes (180045766656 bytes).
The EFI partition ends at 1050623 sectors.
4GB for swap means 4*1024*1024*1024 bytes = 4294967296 bytes. 4294967296 bytes means 4294967296 / 512 = 8388608 sectors. Furthermore 8388608 % 2048 = 0 so it will be well aligned.
To compute the size of the ext4 partition: 351651888 - 1050623 - 8388608 = 342212657 sectors But 342212657 % 2048 is 49 and not aligned. The nearest well-aligned sector is 342212608. 342212608 sectors = 175212855296 bytes. Furthermore, with this sector count, we also have plenty of room for GPT duplication (49 sectors).
Now we can resize the filesystem. But first, we need to know the size of an ext2 block:
# losetup --partscan --find --show ./trick.img # dumpe2fs -h /dev/loop0p2 ... Block size: 4096 ... # losetup -d /dev/loop0
A block is 4096 bytes. We want a partition of 175212855296 bytes / 4096 = 42776576 blocks. We are ready to resize the filesystem (but we need to make a check with e2fsck before):
# losetup --partscan --find --show ./trick.img # e2fsck -f /dev/loop0p2 # resize2fs /dev/loop0p2 42776576 # losetup -d /dev/loop0
Step 4: Rewrite partition table
So, partition starts at sector 1050624 and ends at 1050624 + 342212608 sectors. Which is sector 343263231.
Then swap partition is starting at sector 343263232 and ends 8388608 sectors later which means 351651839.
$ parted trick.img (parted) resizepart 2 343263231 (parted) rm 3 (parted) mkpart linux-swap 343263232 351651839 (parted) quit
Step 5: Shrink the image file
Now, we can truncate the file to reduce its size before duplicating the GPT partition at the end of the file. We will use the exact size of the SSD in bytes (because truncate use bytes and not sectors).
$ truncate -s 180045766656 trick.img
But you also have to duplicate the GPT table at the end. gdisk is your friend: it will detect that the GTP table is not duplicated and will copy it for you:
$ gdisk trick.img Command (? for help): p Disk trick.img: 351651888 sectors, 167.7 GiB Sector size (logical): 512 bytes Disk identifier (GUID): 77042C1D-F8D6-4596-8C33-9B6244D85B97 Partition table holds up to 128 entries Main partition table begins at sector 2 and ends at sector 33 First usable sector is 34, last usable sector is 351651854 Partitions will be aligned on 2048-sector boundaries Total free space is 2029 sectors (1014.5 KiB) Number Start (sector) End (sector) Size Code Name 1 2048 1050623 512.0 MiB EF00 2 1050624 343263231 163.2 GiB 8300 3 343263232 351651839 4.0 GiB 8200 SWAP
Step 6: Control the UUIDs
We have deleted a swap partition and created a new one. It's UUID will not be the same than the other and you want to fix that otherwise, systemd or the initramfs will have some difficulties to start the system.
# losetup --partscan --find --show ./trick.img /dev/loop0 # blkid /dev/loop0* /dev/loop0: PTUUID="77042c1d-f8d6-4596-8c33-9b6244d85b97" PTTYPE="gpt" /dev/loop0p1: UUID="DB00-C8D9" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="93c56b16-bde9-455f-8e32-b115c8a27e61" /dev/loop0p2: UUID="aad5a02a-1ade-42ae-9b59-4f76942b3404" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="dccb6d13-d8d1-4d24-aff5-fe5c0adb4381" /dev/loop0p3: PARTLABEL="SWAP" PARTUUID="7b844f86-77db-40c5-bb6a-3b8b7fc4f104"
Old UUIDS where the following:
/dev/sdb: PTUUID="77042c1d-f8d6-4596-8c33-9b6244d85b97" PTTYPE="gpt" /dev/sdb1: UUID="DB00-C8D9" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="93c56b16-bde9-455f-8e32-b115c8a27e61" /dev/sdb2: UUID="aad5a02a-1ade-42ae-9b59-4f76942b3404" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="dccb6d13-d8d1-4d24-aff5-fe5c0adb4381" /dev/sdb3: UUID="a40d9464-d002-44d8-9b22-ad08b1f71588" TYPE="swap" PARTUUID="072887af-2d5b-45ca-be38-9c31e9ae6b27"
You can see that only the swap partition is different. This is because it is the only partition that have been deleted. So, it's time to fix this with mkswap for the UUID:
# mkswap --uuid a40d9464-d002-44d8-9b22-ad08b1f71588 /dev/loop0p3 # blkid /dev/loop0p3 /dev/loop0p3: UUID="a40d9464-d002-44d8-9b22-ad08b1f71588" TYPE="swap" PARTLABEL="SWAP" PARTUUID="7b844f86-77db-40c5-bb6a-3b8b7fc4f104" # losetup -d /dev/loop0
And with gdisk for the PARTUUID:
$ gdisk trick.img gdisk /media/data/VirtualMachines/trick.img GPT fdisk (gdisk) version 1.0.9 Partition table scan: MBR: protective BSD: not present APM: not present GPT: present Found valid GPT with protective MBR; using GPT. Command (? for help): x Expert command (? for help): c Partition number (1-3): 3 Enter the partition's new unique GUID ('R' to randomize): 072887af-2d5b-45ca-be38-9c31e9ae6b27 New GUID is 072887AF-2D5B-45CA-BE38-9C31E9AE6B27 Expert command (? for help): m Command (? for help): w Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING PARTITIONS!! Do you want to proceed? (Y/N): Y OK; writing new GUID partition table (GPT) to /media/data/VirtualMachines/trick.img. Warning: The kernel is still using the old partition table. The new table will be used at the next reboot or after you run partprobe(8) or kpartx(8) The operation has completed successfully.
Always check the result:
# losetup --partscan --find --show ./trick.img /dev/loop1 # blkid /dev/loop0p3 /dev/loop0p3: UUID="a40d9464-d002-44d8-9b22-ad08b1f71588" TYPE="swap" PARTLABEL="SWAP" PARTUUID="072887af-2d5b-45ca-be38-9c31e9ae6b27" # losetup -d /dev/loop0
Now you are done!
Step 7: Transfert to real disk
This is the easiest stuff:
# cp trick.img /dev/sdb # sync # partprobe /dev/sdb
Now, you have a working system on an SSD to put in your laptop.
Conclusions
This is a method to duplicate and shrink a block device to another. You need to do some manual calculations because most of the time the different involved tools don't use the same size units (sometimes it is bytes, sometimes blocks, sometimes sectors).
I could find a cleverer way to do it by compressing it at the beginning, but found that loop devices can't be used on compressed images.
I am sure there are other methods but this one works. It is not perfect but I hope that you have enjoyed all the subtleties of filesystem duplication.