Introduction

About 2.5 years ago I bought a Dell XPS 9360 laptop, so that I had a modern Linux laptop to take to conferences. At the time I was optimising for light weight, and relatively low cost, so went with an i7 system, with 8 GiB RAM, and 256 GiB M.2 storage system.

It came with Windows 10 Home (Pro cost extra :-) ), and I configured it to dual boot Windows 10 and Ubuntu Linux 16.04 LTS because I wanted to mostly run Ubuntu Linux, but still have the opportunity to run Windows 10 occasionally (since I have no other Windows systems).

Because I have mostly used the Ubuntu Linux 16.04 install for FPGA development, across a variety of vendors (Xilinx, Lattice) and FPGA models (Spartan6, Artix7, iCE40, iCE40 UP), all of which need different FPGA development tools -- and because most FPGA vendor tools are huge (many GiB for each Xilinx tool set) -- I ran out of storage space on my Linux partition pretty frequently.

While -- like most small thin laptops -- the Dell XPS 9360 CPU, RAM, etc is not upgradable, as they are directly soldered onto the motherboard, it turns out that the storage is a regular M.2 2280 NVMe drive, and can be swapped out for another M.2 NVMe drive. So I decided to replace the M.2 NVMe drive in my Dell XPS 9360 with a larger one to give the laptop a longer lease of life. (I would like to have more RAM, but in practice for what I do 8 GiB of RAM is sufficient even if it is not exactly ample RAM. So I can probably live with 8 GiB of RAM indefinitely. And the i7 CPU is still relatively fast for what I do with the laptop.)

After looking around for a while I settled on a 1TB Samsung 970 EVO Plus because:

  • It was available in stock from my local retailer, for a reasonable price

  • 1TB was a large increase in storage space, which would reduce the impact of solid state drive overwrite limits

  • It reviewed pretty well: AnandTech, TomsHardware, PCWorld, StorageReview, Guru 3D, etc.

  • While it is a TLC drive (3-bits per cell), it has both a TurboWrite feature (initial write to SLC (2-bits per cell) section) and a RAM cache to reduce the speed impact. This means it is well tuned to the sort of bursty write you get on a laptop (and not well tuned for sustained "enterprise" writes). Also the existing Toshiba drive supplied with the Dell XPS 9360 was also a TLC drive, and performed good enough for my laptop use.

  • People had reported putting other Samsung 9xx EVO drives into Dell XPS 93x0 models (eg, on Reddit, iFixIt, and the Dell forums, etc).

  • Samsung make their own storage chips, and have a pretty reasonable reputation (and so the 5 year warranty offered is likely to actually indicate the quality).

Of note, while the Samsung 970 EVO Plus is capable of up to about 3 GB/s transfer speeds via M.2, the Dell XPS 9360 runs the M.2 interface in a power saving mode, which limits the maximum transfer speed to about 1.8 GB/s. Which means that one could choose to buy a slower drive and still get the same performance -- but I chose to pay a little bit more now for hopefully a more reliably drive, that could potentially be moved to another system later.

Swapping the drive

Physically swapping the M.2 drive requires disassembling the laptop, but I found lots of guides to replacing the M.2 drive, so I was fairly confident that I could physically replace the M.2 drive itself. The major challenges was going to be getting the data from one drive to the other, because the Dell XPS 9360 has only one M.2 slot and M.2 external adapters are not common, so copying directly from the old drive to the new drive was not possible.

In terms of physically replacing the drive I suggest you look at some of the other guides. My only extra hints would be:

  • I used a Torx T5 bit to remove most of the screws, and a Philips #00 to remove the one under the bottom flap and on the M.2 drive itself.

  • There are a lot of plastic clips around all sides of the rear of the laptop, which need to be unclipped with a spudger or similar (I used a plastic spudger that came in a "phone repair" kit, which worked but was not ideal).

  • The clips at the front of the laptop and the sides are smaller, and should be unclipped first.

  • There are large clips across the rear in the hinge area, so the best option is to unclip all the others, and then lift the bottom forwards (away from the hinge area towards the front of the laptop) to unclip those clips (and remember to reinstall the base in the reverse manner: large clips by the hinge first, then around the sides).

  • Make certain you have everything copied off the old drive before opening the laptop to swap out the M.2 drive, as you will want to avoid having to open the laptop more than once.

Other than getting the bottom of the case off (possible, but fiddly and time consuming), swapping the M.2 drive physically is fairly easy, and anyone used to working with PC internals would be able to swap the drive. (Lots of extra care is required in opening the case, though, due to all the plastic clips; fortunately there is no glue.)

Transferring data

Because the hardware limitations prevented a direct copy between the drives my approach was:

  • Make a backup of everything on the laptop (copying everything onto my NAS, both the Windows 10 and Ubuntu Linux partitions, and all the other partitions)

  • Boot into Windows 10 and create both a Recovery Boot USB stick (small, about 1GiB), and a Recovery Install USB stick (about 16GiB) just in case. (I needed the Recovery Boot USB stick to get Windows 10 booting again, so do not skip that one; fortunately I did not need to use the Recovery Install USB stick again.)

  • Boot off a Ubuntu 18.04 Live USB stick, and then use dd to copy each individual partition into its own file on an external hard drive. (To boot from the USB stick, the easiest option is to plug in the USB stick, and then press F12 repeatedly when the Dell logo is displayed after power on until it says "Preparing One Time Boot Menu"; note that on the Dell XPS 9360 the Fn key should not be pressed, as unlike a Mac laptop, those keys are function keys by default, and Fn is needed for the other features.)

  • Use md5sum to verify that the original drive partition contents and the dd copies were bit for bit identical.

  • Make multiple printouts of the partition table of the old drive (in various units), using parted list, so I could recreate it again on the new drive (by hand).

Then once I was happy that I had a fully copy of the old drive, I powered off the laptop, opened it up (as above), and installed the new Samsung 970 EVO Plus drive.

Once the laptop was back together I:

  • Plugged the Ubuntu 18.04 Live USB stick back in again, and used F12 to bring up the One Time Boot Menu, and booted back into the Live CD.

  • Manually partitioned the new M.2 drive with a gpt partition table, with the partitions the same size as the previous drive, and with the same flags, etc.

  • Used dd to copy the partition contents off the external drive onto the new Samsung drive.

  • Used md5sum to verify that those copies on the Samsung drive partitions were bit for bit identical to what had been copied off the old Toshiba drives.

And then I rebooted, and it failed to boot at all :-(

As best I can tell, even though I maintained the partition contents identically (and the first time, the partition locations and flags identically), the UEFI booting in both Ubuntu Linux 16.04 LTS and in Windows 10 (neither would boot), was relying at least in part on something else -- the Partition UUID which is part of the gpt format, perhaps -- which changed, and that threw off the booting process. (Plus changing the drive clearly caused the UEFI BIOS to forget the boot order sequence it previously had.)

Getting Ubuntu Linux 16.04 LTS to boot again

To fix the booting of Ubuntu Linux 16.04 LTS (via grub) I:

  • Booted off the Ubuntu Linux 18.04 live CD again

  • Mounted the Ubuntu Linux 16.04 root file system (which is in a LVM volume group, inside a LUKS encrypted container) with:

    sudo cryptsetup luksOpen /dev/nvme0n1p4 dell
    sudo pvscan
    sudo mkdir /install
    sudo mount /dev/vg/root /install
    

    which requires the password for the LUKS volume at the luksOpen stage.

  • Mounted /proc, /sys, dev, etc inside that:

    sudo mount -t sysfs sys /install/sys
    sudo mount -t proc proc /install/proc
    sudo mount -t devtmpfs udev /install/dev
    sudo mount -t devpts devpts /install/dev/pts
    
  • Changed into the chroot, and used that to mount the remaining volumes:

    sudo chroot /install
    mount -a
    

    and checked that /boot and /boot/efi had mounted:

    df -Pm | grep /boot
    
  • Then updated/reinstalled grub:

    update-grub
    grub-install
    

    from inside the chroot.

  • Then exited the chroot, and unmounted everything:

    exit
    sudo umount /install/dev/pts
    sudo umount /install/dev
    sudo umount /install/proc
    sudo umount /install/sys
    sudo umount /install/boot/efi
    sudo umount /install/boot
    sudo umount /install
    

And then I rebooted again. This time, to my relief, the laptop booted into grub normally, and then booted into Ubuntu Linux 16.04 LTS normally.

Unfortunately it still could not boot Windows 10 :-( It just kept coming up with the "Recovery" screen ("Your PC Device needs to be repaired"). Even after running sudo update-grub again from within the working Ubuntu Linux 16.04 LTS environment, to ensure that Windows 10 was found in the boot environment. Since I knew the file system contents was bit for bit identical, I figured the boot process had become confused (possibly due to the new partition UUIDs).

Getting Windows 10 to boot again

I first tried booting off the Windows 10 Boot Recovery (1GiB) USB stick I had made (using F12 to get a One Time Boot Menu to boot the USB stick), then navigating into Advanced Options / Startup Repair / Windows 10 but that just reported "Startup Repair couldn't repair your PC". So clearly Windows 10 was very confused.

Next I found a Dell guide to Repairing the Windows EFI bootloader which I tried, by going into Advanced Options / Command Prompt from the Windows 10 Boot Recovery USB stick. Unfortunately I got stuck at step 7 of that guide, because ESP (EFI boot partition) was hidden for some reason, which meant those instructions would not allow me to assign a drive letter to reinstall the Windows 10 boot config. (My guess is the same issue caused the "Startup Repair" to fail.) I do not know why it was shown up as Hidden, without a drive letter, as it did not have the hidden flag in the gpt partition table (and I could be certain it was the ESP partition by the exact size).

Fortunately I found another way to assign a drive letter to the ESP partition:

diskpart
list disk
sel disk 0
list partition
select partition 1
assign

which let me move on.

Unfortunately, the next step bootrec /FixBoot then failed with "Access is Denied". Some guides recommend reformatting the ESP partition at this point but I was reluctant to do that because there were both Ubuntu Linux 16.04 UEFI boot files on there and Dell UEFI boot files on there (eg, for recovery tools), so I kept looking.

Another guide suggested, bootrec /REBUILDBCD which I tried next, but after scanning the system that then reported "The system cannot find the path specified." :-(

With some more hunting online, I found someone who had encountered and fixed that issue, by doing:

cd /d H:\EFI\Microsoft\Boot
ren BCD BCD.bak
bcdboot c:\Windows /l en-nz /s h: /f ALL

where c:\Windows is the Windows 10 directory on the drive letter found as the main Windows 10 Drive, en-nz is the preferred local (en-us seems likely to be the default), /s h: specifies the drive letter assigned to the EFI partition, and /f ALL specifies that the UEFI, BIOS, and NVRAM boot settings should all be updated. For good measure I also tried:

bootrec /fixboot

again, but that still failed ("Access is Denied").

However after exiting out the recovery shell and rebooting the laptop, it actually automatically booted into the Windows 10 environment using the Windows boot manager. At this point it only booted Windows 10, but I was able to get into the boot manager (eg, F12), ask it to boot Ubuntu Linux 16.04 LTS, and then do:

sudo update-grub
sudo grub-install

inside a Ubuntu Linux 16.04 LTS terminal window, and then the laptop was booting normally, with grub able to boot both Ubuntu Linux 16.04 LTS and Windows 10 as it did on the old drive. Phew.

Expanding the Linux root partition

Once everything was copied onto the new Samsung 970 EVO Plus 1TB drive, and booting successfully, that just left the original purpose: expanding the Linux file system. (Because I hardly use Windows 10 on the laptop, and had not run into space problems on that partition -- about 90 GiB -- I chose to dedicate all the extra space on the new drive to Linux.)

My original plan was just to create an additional partition on the end of the drive (with the remaining 700 GiB of space), and then use LVM to join the two partitions together, given that the root file system was already on LVM -- and that was how I laid out the drive when I first copied the data over. However I realised that with the Linux filesystem inside a LUKS encrypted partition, that would both be more fragile (two LUKS containers, or some data in a LUKS container and the rest outside it), and potentially require entering two passwords on boot (to unlock each partitions).

So I spent a while shuffling partitions around so that all the ESP / Windows / Dell partitions were at the start of the drive, followed by the Ubuntu /boot partition (which needs to be outside the LUKS encryption unless you do special EFI boot tricks), followed by the main LUKS / LVM partition at the end of the drive. (It was fiddly to shuffle things around, but fortunately when you have a drive that is four times as big as the original content it is easy to make more partitions to temporarily hold copies of the data you want to move: so it was just more use of dd and md5sum to make sure everything was copied correctly, into the right places. I even had to delete some of the partitions, quit parted, and then recreate the partitions at the identical spot and size to get them to show up in the right partition order that I wanted.)

Once all the partitions were in the right order, and the right size, and the laptop was booting Ubuntu Linux 16.04 LTS and Windows 10 correctly, I was ready to carry on with expanding the Linux root drive. My Linux root drive is:

  • In an ext4 file system (Ubuntu 16.04 LTS default)

  • On a LVM logical volume (LV)

  • Inside a LVM volume group (VG)

  • Inside a LVM physical volume (PV)

  • Inside a LUKS encrypted container

  • Inside a partition on /dev/nvme0n1 (/dev/nvme0n1p7 by the end of all the partition shuffling).

Which means in order to expand the root file system, all of those layers need to be expanded, in the opposite order. That's a lot of layers to potentially go wrong.

Since I still had a couple of recent backups of the laptop drive, as well as the original M.2 drive, which I had recently tested, and I knew how to recover from booting issues, I felt it was worth giving these steps ago. (Seriously have known tested good backups before trying to do this: there is a lot to go wrong, and typos or interrupted operations could wreak havoc.)

(Several of these instructions suggest creating a temporary partition after the one you are expanding, and writing random data to that partition before expanding the LUKS volume into it: I did not do that because (a) it takes a bunch of time to do, (b) it forces the SSD to assume the entire disk is in use, and copy more data around thus using up SSD drive life due to write amplification, (c) the encrypted partition is already large enough for my level of paranoia about this particular volume being recovered, and (d) the data on this laptop is not that sensitive -- it's mostly just a FPGA development laptop at this point, and almost all of that development is open source on GitHub anyway, so I do not feel the need to do that much to protect it: it is just encrypted because that is what I do with all my computers that I might travel with, to make data recovery by someone else non-trivial.)

Expanding the partition

To expand the partition I booted off the Ubuntu 18.04 Live USB install, and then used parted to do:

resizepart 7 953869MiB

where 953869MiB was 1MiB lower than the maximum size of the drive displayed by parted at the top of the partition table list result (the exact size does not work, I think due to rounding and/or the partition elements starting at 0).

Then I rebooted back into Ubuntu 18.04 LTS Live CD to expand the LUKS container, while it was open but not mounted.

Expanding the LUKS container

Finding some guides to enlarging a LVM on LUKS install was what convinced me that rearranging the disk partitions to have a single LUKS container was the best option. (Previously I had assumed expanding the LUKS container was not possible, even though I knew all the other steps were possible.)

Once the partition is expanded, and you have rebooted back into the Ubuntu Linux Live environment, to ensure that Linux consistently sees the new partition as expanded but not mounted, then open the LUKS container and expand it to the new size of the partition (note: new partition number as I rearranged the partitions, above, to have the LUKS / LVM partition at the end):

sudo cryptsetup luksOpen /dev/nvme0n1p7 dell
ls -l /dev/mapper/dell
sudo vgscan
sudo vgchange -ay
sudo cryptsetup resize dell

That command completes pretty much immediately, and the default new size is "the size of the disk partition" which is exactly what we want here.

Verify the new size of the LUKS container with:

sudo cryptsetup -v status dell

which reports the size in "512 byte sectors" (one of the most useless units for modern drives :-( ); but fortunately dividing by 2048 (2 * 1024) gives us MiB, and we can verify the new size is very close to the partition size (in my case 2 MiB smaller; it also revealed the LUKS container was injecting a 4096 sector offset, which is exactly 2 MiB: 4096 * 512 = 2097152 = 2 * 1024 * 1024; I am not sure if that is a requirement, a default, or something I chose when I first set it up).

Expanding the LVM physical volume (PV)

Expanding the LVM physical volume is just a matter of asking LVM to recognise the additional space:

sudo pvresize /dev/mapper/dell

and it should return almost immediately reporting "1 physical volume(s) resized / 0 physical volume(s) not resized)". We can verify the new physical volume size with:

sudo pvdisplay /dev/mapper/dell

and that should show a "PV Size" around 832 GiB, as well as lots of "Free PE" now the physical volume is much larger.

Expanding the LVM volume group (VG)

The volume group is automatically expanded when it has physical volumes with spare space in them, which we can verify with:

sudo vgdisplay

That should also show a "VG Size" around 832 GiB, as well as lots of "Free PE / Size".

Expanding the LVM logical volume (LV)

My existing install had two logical volumes, created during the original Ubuntu Linux 16.04 LTS:

  • /dev/vg/root

  • /dev/vg/swap

and unfortunately they were in that order on the disk, as shown by:

sudo lvdisplay

Since I preferred to have my root LV contiguous, I chose to remove the swap logical volume, then expand the root logical volume, then create a new swap logical volume and initialise that again. (Because we are booted into an Ubuntu Live USB environment, none of these are mounted, and the swap usage is obviously transitory anyway, so the contents did not need to be retained.)

To do this I did:

sudo lvchange -an /dev/vg/swap
sudo lvremove /dev/vg/swap

which prompts for confirmation of removing the swap logical volume.

Then I expanded the root logical volume to 512 GiB (chosen to not completely use up the extra disk space to allow more flexibility, but to be about 4 times as big as the existing Linux root filesystem):

sudo lvresize -L 512G /dev/vg/root

and verified that worked as expected with:

sudo lvdisplay /dev/vg/root

which should show a "LV Size" of 512.00 GiB as a result.

Then I made a new swap logical volume, of 2 GiB again:

sudo lvcreate -n swap -L 2G /dev/vg
sudo lvdisplay /dev/vg/swap

and reinitialised the swap space:

sudo mkswp -L swap /dev/vg/swap

(Note that this process changes the UUID of the swap partition, which might need to be fixed up, if you are mounting the swap by UUID rather than LV path or volume label.)

Resizing the ext4 root file system

Now that everything below is resized, we can resize the ext4 filesystem. With ext4 this could actually be done online (while mounted), but because I was still booted into the Ubuntu Linux live environment, I did the resize offline, starting by checking the file system:

sudo e2fsck -f /dev/vg/root
sudo resize2fs -p /dev/vg/root

where the -p is for progress messages, but in practice the resize only took a few seconds on the Samsung 970 EVO Plus drive (as it only moves metadata around). The new file system size is reported in 4KiB blocks (another not very useful unit :-( ), as 134217728 4KiB blocks, which we can check is correct with 134217728 / 4 * 1024 * 1024 = 512 GiB.

After that I did another e2fsck -f /dev/vg/root check out of abundance of precaution, and then mounted the partition to check the expected contents were there (and verify the way the swap partition was mounted to reduce boot issues):

sudo mkdir /install
sudo mount /dev/vg/root /install
grep swap /install/etc/install

Fortunately the swap was being mounted by LV path (/dev/mapper/vg-swap) so it should survive being recreated elsewhere on the disk.

While it was mounted, I also checked the /dev/vg/root filesystem usage, to make sure I now had lots of free space:

sudo df -Pm /install

and that showed I had gone from about 98% used on the root partition to about 27% used. So I unmounted the file system again:

sudo umount /install

Running:

sudo vgdisplay

showed I had a bit over 300GiB of unallocated space in the volume group saved for later. (And if I do want to expand the root logical volume I would probably remove the swap again, and then recreate it, to keep the root logical volume in one LVM extent for simplicity.)

Conclusion

Once all the expansion steps were done from the Ubuntu Linux live environment, I simply rebooted, and Ubuntu 16.04 LTS and Windows 10 booted fine -- and I had lots more space in my Linux environment.

With a couple of days of effort, and a few hundred dollars for a new M.2 drive, I have managed to change my Dell XPS 9360 laptop from a persistently almost full root file system, to one which is about 27% full (and has about 300 GiB still available to allocate later). That makes it much more useful, potentially for several more years.

About a day of that time was consumed by:

  • Making backups

  • Copying the file system partitions around (especially to/from a USB 3 spinning disk)

  • Checking those backups, and copies

  • Waiting for Windows 10 to create USB Recovery drives (the 16GiB system install recovery drive took over 2.5 hours to write!)

and the remainder was research, getting Ubuntu Linux 16.04 LTS and Windows 10 booting again, etc. (Actually physically swapping the M.2 drive inside the Dell XPS 9360 took maybe half an hour including all the disassembly and reassembly.) I expect if I did it again the process would be faster, as I could have avoided some of the file system rearrangement I did by going directly to the final partition layout, knowing that I was going to have to make everything bootable again anyway.

Of note:

  • One potential advantage of not using the whole SSD, is that writes to the drive will be constrained to about the first 60% of the drive, which should reduce the amount of data that the SSD firmware feels it needs to shuffle around (particularly important because by default LUKS does not pass on TRIM/DISCARD requests for security reasons, so any block written to will then be copied around by the SSD firmware forever).

  • It turned out almost impossible to find the SSD erase block size, and align anything to those erase blocks (cf XFS Storage Layout Considerations). As best I could tell erase block sizes seem to be trade secrets now, certainly not appearing in data sheets and in some cases not even available by requests; and everything seems to be defaulting to aligning to 1 MiB blocks as being sufficient. Aligning to 1 MiB seems likely to be reasonable (especially after 5+ years of OSes doing that automatically, and vendors designing for those OS), but possibly not the most optimal choice in theory. So for single drive systems it probably makes sense just to let everything auto-align to 1 MiB boundaries, and ignore the problem.

Even modern local (internal) storage is basically a "network attached storage server" of its own, with its own ideas about how to store data, and its own storage API. It just happens to speak SATA or SAS or NVMe or similar, rather than Ethernet and TCP/IP.

ETA 2019-04-30: After doing all of this, when I next booted into Windows 10 for an extended period, I found that Windows Update was going to install (no option) the update:

Dell, Inc - Firmware - 9/27/2018 12:00:00 AM - 2.10.0
Status: Pending Install

(with the lovely message "We'll automatically install updates when you aren't using your device, or you can install them now if you want.").

It was not clear to me what it is. By searching on the Microsoft Catalog for 2.10.0 I could find three versions, with the matching date (and three 2.10.0 versions with a later update date of 3/25/2019); I think the three versions are for different versions of Windows 10, and my guess is this version is the one that would be installed on my Dell XPS 9360, since I think I have already updated to the latest Windows 10 release. Unfortunately there were no other details for what it is. And it was unclear if the install is being prompted by replacing the drive, or just by the date.

Since Windows 10 was not giving me an option, and I hate having things break randomly in the background, I chose to plug the laptop into the mains power, and let it "Install Now". Naturally it wanted to restart, and when it restarted it then proceeded to update the BIOS and firmware of everything in the laptop :-(

I am unclear whether this user hostile behaviour of forcing compulsory unscheduled firmware updates is Dell behaviour or new Microsoft behaviour (or both), why it was forced to a September 2018 version, and whether it was triggered by replacing the Toshiba M.2 drive that came with the laptop with the Samsung 970 EVO Plus drive -- or just triggered by the date / first sufficiently long Windows boot that something decided it had time to treat my laptop as its own.

Hopefully this unplanned firmware update does not adversely impact is on the Dell XPS 9360 that I had just spent a couple of days upgrading the drive :-( Fortunately Windows 10 and Ubuntu Linux 16.04 LTS do seem to boot up again.

(After rebooting into Windows again, Dell Update -- not Windows Update -- decided it had a further 12 updates it wanted to install, including a BIOS released 2019-04-22; but fortunately I could choose "Remind Later" to those, so that is what I did.)