A few weeks ago I had to migrate a RHEL 5 server to Hyper-V.
Yes, RHEL 5.
Released in 2007.
Old enough to drive in some states.
Naturally, this server was still important. Because of course it was.
Nobody keeps legacy servers around because they are fun. They stay around because they run something important, mysterious, undocumented, or all three.
So there I was, staring at a server that had survived multiple generations of hardware, operating systems, hypervisors, managers, and probably a few organizational charts.
The plan sounded simple:
Move the server to Hyper-V.
That sentence should come with a warning label.
Everything Looked Fine Until It Didn’t
The migration itself looked normal at first.
The disk conversion completed.
The VM powered on.
The BIOS screen appeared.
GRUB loaded.
Linux started booting.
And then the server basically said:
Nice try. Rescue mode it is.
At that point, the maintenance window got very quiet.
Every infrastructure engineer knows that silence. It is the sound of people trying not to ask, “So… how bad is it?”
The Problem With Old Linux Migrations
The problem was not really Hyper-V.
Hyper-V was doing what Hyper-V does.
The problem was that RHEL 5 was trying to boot in a virtual hardware environment it was not originally prepared for.
Old Linux systems tend to make assumptions about:
- Storage controllers
- Disk names
- LVM availability
- Initrd contents
- Network adapters
- GRUB root devices
Those assumptions work great until you move the system somewhere else.
Then they become little landmines.
Step 1: Boot Into Rescue Mode
The first step was to boot from the RHEL 5 ISO and enter rescue mode.
At the boot prompt:
linux rescue
The installer may ask whether it should automatically mount the installed system.
In this case, that did not work cleanly.
That was fine.
Honestly, with RHEL 5, “that did not work cleanly” is less of a surprise and more of a lifestyle.
Eventually I got to a rescue shell:
sh-3.2#
Now the real work could start.
Step 2: Activate LVM Before Doing Anything Else
This was mandatory.
The installed system was using LVM, and rescue mode was not going to magically make everything available in the way I needed.
From the rescue shell, before chrooting, I ran:
lvm pvscan
lvm vgscan
lvm vgchange -ay
lvs
The goal was simple:
- Find the physical volumes
- Find the volume groups
- Activate the logical volumes
- Confirm the root volume was visible
Once the logical volumes showed as active, things started looking much better.
This was the first moment where I thought:
OK. Maybe this thing is not completely dead.
Always a comforting thought during a maintenance window.
Step 3: Mount the Installed Root Filesystem Manually
Once LVM was active, I mounted the installed root filesystem manually.
In this example, the root logical volume is genericized as /dev/vg00/lv00:
mkdir -p /mnt/sysimage
mount /dev/vg00/lv00 /mnt/sysimage
Then I checked whether it looked like a real Linux installation:
ls /mnt/sysimage
I wanted to see directories like:
bin
boot
etc
lib
sbin
usr
var
When those showed up, that was a good sign.
Not victory.
Just a good sign.
With legacy systems, celebrate small wins quietly. The server can hear confidence.
Step 4: Mount /boot If It Is Separate
On this system, /boot was separate.
Using a generic example partition:
mkdir -p /mnt/sysimage/boot
mount /dev/sda1 /mnt/sysimage/boot
Then I verified the boot files:
ls /mnt/sysimage/boot
I expected to see things like:
grub
vmlinuz-*
initrd-*
If the mount complains that the device or mount point is busy, do not panic. Check whether it is already mounted, unmount it if needed, and mount it again cleanly:
umount /mnt/sysimage/boot
mount /dev/sda1 /mnt/sysimage/boot
This is one of those moments where being careful matters more than being fast.
Fast is how you turn a recoverable migration into a resume-generating event.
Step 5: Bind-Mount Kernel Filesystems
This was the step that really mattered.
Before rebuilding the initrd, the installed system needs access to /dev, /proc, and /sys.
Without this, mkinitrd can fail with errors like:
error opening /sys/block: No such file or directory
So before chrooting, I ran:
mount -o bind /dev /mnt/sysimage/dev
mount -o bind /proc /mnt/sysimage/proc
mount -o bind /sys /mnt/sysimage/sys
This is the kind of detail that generic migration guides love to skip.
Unfortunately, this is also the kind of detail that decides whether your server boots or whether you spend the rest of the night questioning your career choices.
Step 6: Chroot Into the Installed System
Once the filesystems were mounted correctly, I entered the installed operating system environment:
chroot /mnt/sysimage
At that point, I was working inside the installed RHEL 5 system, not just the rescue environment.
That distinction matters.
A lot.
Step 7: Identify the Installed Kernel Version
Do not blindly use uname -r from the rescue environment.
That can show the rescue kernel, not necessarily the installed system kernel.
Instead, I checked the installed modules:
ls -1 /lib/modules
I also checked /boot:
ls -1 /boot/vmlinuz-*
And confirmed what GRUB was trying to boot:
grep "^kernel" /boot/grub/grub.conf
In my case, the target kernel was:
2.6.18-440.el5
Use the kernel version that actually exists on your migrated system.
Do not copy and paste kernel versions from blog posts.
Including this one.
Especially this one.
Step 8: Make GRUB Boring
GRUB is not where I want creativity during a migration.
I reviewed:
vi /boot/grub/grub.conf
For this migration, I made sure the kernel line used the LVM root path directly instead of something more fragile or confusing:
root=/dev/vg00/lv00
Example:
kernel /vmlinuz-2.6.18-440.el5 ro root=/dev/vg00/lv00 rhgb quiet
Could UUIDs work?
Sure.
Could labels work?
Also yes.
But in this case, using the explicit LVM root path made the boot process easier to reason about while troubleshooting.
And during a maintenance window, “easy to reason about” beats “technically elegant” every time.
Step 9: Rebuild the Initrd With Hyper-V Drivers
This was the heart of the fix.
The migrated server needed an initrd that understood the Hyper-V environment.
So I rebuilt the initrd with the Hyper-V modules explicitly included:
mkinitrd -f \
--with=hv_vmbus \
--with=hv_storvsc \
--with=hv_netvsc \
/boot/initrd-2.6.18-440.el5.img \
2.6.18-440.el5
The important modules were:
hv_vmbushv_storvschv_netvsc
Translated into normal human language:
- Hyper-V bus support
- Hyper-V storage support
- Hyper-V network support
Without the right drivers in the initrd, the kernel may start booting but fail before it can properly access the root filesystem.
That is when you get the classic legacy Linux migration experience:
It worked fine before we moved it.
Yes.
That is usually the point.
Step 10: Validate the Initrd Exists
After rebuilding the initrd, I verified that the file existed:
ls -lh /boot/initrd-2.6.18-440.el5.img
If you want to inspect it more deeply on RHEL 5, you can unpack it temporarily:
mkdir -p /tmp/initrdcheck
cd /tmp/initrdcheck
zcat /boot/initrd-2.6.18-440.el5.img | cpio -idmv
find . -name 'hv_*.ko*' -o -name lvm
cd /
rm -rf /tmp/initrdcheck
This is not always necessary, but when you are troubleshooting an old system, trust but verify.
Actually, with old systems, maybe just verify.
Trust is how you get paged.
Step 11: Exit Cleanly and Reboot
Once the initrd was rebuilt, I exited the chroot:
exit
Optionally, unmount everything in reverse order:
umount /mnt/sysimage/sys
umount /mnt/sysimage/proc
umount /mnt/sysimage/dev
umount /mnt/sysimage/boot
umount /mnt/sysimage
Then came the moment of truth:
reboot
Every engineer knows this part.
You watch the console.
You stop talking.
You pretend to be calm.
You are not calm.
GRUB loads.
The kernel starts.
The initrd loads.
LVM activates.
The root filesystem mounts.
And then, finally, the login prompt appears.
That login prompt is one of the most beautiful things in infrastructure.
Right up there with clean backups and change windows that end early.
Step 12: Do Not Declare Victory Too Early
A login prompt does not mean the migration is done.
It means the operating system booted.
That is only one boss battle.
After boot, I still validated:
df -h
mount
ifconfig -a
route -n
Then I checked:
- Application services
- Database connectivity
- Scheduled jobs
- Mount points
- Monitoring agents
- Backup agents
- Licensing
- Anything hardcoded to old hardware
The application owner still needs to test the application.
Infrastructure can prove that the server is alive.
Only the application owner can prove that the application is actually useful.
There is a difference.
A painful difference.
What Actually Fixed It
The winning combination was:
- Boot from RHEL 5 rescue media
- Activate LVM manually
- Mount the installed root filesystem
- Mount
/boot - Bind
/dev,/proc, and/sys - Chroot into the installed system
- Identify the real installed kernel
- Ensure GRUB points to the correct LVM root
- Rebuild the initrd with Hyper-V drivers
- Reboot and validate everything
The most important command was probably this one:
mkinitrd -f \
--with=hv_vmbus \
--with=hv_storvsc \
--with=hv_netvsc \
/boot/initrd-2.6.18-440.el5.img \
2.6.18-440.el5
That is the command that made the old operating system understand enough about its new Hyper-V home to boot properly.
Lessons Learned
The funny thing about legacy servers is that they are often legacy because they are important.
Nobody wants to upgrade them.
Nobody wants to replace them.
Nobody wants to document them.
But everybody wants them online.
So they sit there for years, quietly running critical workloads until one day someone says:
We just need to move it.
And that is how the adventure begins.
My main lessons from this migration:
- Assume the first boot will fail.
- Confirm LVM before blaming anything else.
- Do not trust rescue mode to mount everything correctly.
- Bind-mount
/dev,/proc, and/sysbefore rebuilding initrd. - Do not use the rescue kernel version by mistake.
- Make GRUB simple.
- Explicitly include Hyper-V drivers in the initrd.
- Keep your notes clean enough that future you can understand them.
Future you is tired.
Future you is probably under pressure.
Be nice to future you.
Final Thoughts
Would I recommend running RHEL 5 in production today?
Absolutely not.
Would I bet there are still plenty of RHEL 5 servers out there running important things?
Absolutely yes.
That is the reality of enterprise IT.
Not everything is greenfield.
Not everything is cloud-native.
Not everything has a clean Terraform module and a beautiful CI/CD pipeline.
Sometimes the job is convincing a 15-year-old Linux server to survive one more migration without throwing itself dramatically onto the floor.
And when it finally boots, you do what every experienced infrastructure engineer does.
You validate everything.
You update your notes.
You keep the rollback plan handy.
And you do not tell management it was easy.
Because it was not easy.
You just made it look that way.
Quick Command Reference
For the impatient future version of me:
# From rescue shell
lvm pvscan
lvm vgscan
lvm vgchange -ay
lvs
mkdir -p /mnt/sysimage
mount /dev/vg00/lv00 /mnt/sysimage
mkdir -p /mnt/sysimage/boot
mount /dev/sda1 /mnt/sysimage/boot
mount -o bind /dev /mnt/sysimage/dev
mount -o bind /proc /mnt/sysimage/proc
mount -o bind /sys /mnt/sysimage/sys
chroot /mnt/sysimage
ls -1 /lib/modules
ls -1 /boot/vmlinuz-*
grep "^kernel" /boot/grub/grub.conf
vi /boot/grub/grub.conf
mkinitrd -f \
--with=hv_vmbus \
--with=hv_storvsc \
--with=hv_netvsc \
/boot/initrd-2.6.18-440.el5.img \
2.6.18-440.el5
ls -lh /boot/initrd-2.6.18-440.el5.img
exit
umount /mnt/sysimage/sys
umount /mnt/sysimage/proc
umount /mnt/sysimage/dev
umount /mnt/sysimage/boot
umount /mnt/sysimage
reboot
About Ops Under Pressure
Ops Under Pressure is a collection of real-world infrastructure stories, troubleshooting adventures, automation projects, and lessons learned from the trenches of enterprise IT.
Because the most interesting part of the job starts right after someone says:
This should be easy.