Shrink a laptop hard disk to an SSD🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

#sysadmin #debian #computer

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.

From Hard drive to SSD
From Hard drive to SSD

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:

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.