Pre-Migration Audit: Know What You Are Running
Before touching a target server, enumerate every process, binary, shared library, and cron job on the source system. On Solaris, `pkginfo -l` gives you the full installed package manifest. On AIX, use `lslpp -La`. On HP-UX, run `swlist -l product`. None of these map directly to RPM or dpkg, but you need the list before you can build a Linux equivalent.
Capture running services and open ports in one pass:
On Solaris: `svcs -a` lists all SMF services with state. Pipe through `grep online` to get only what is active. For network listeners, `netstat -an -f inet | grep LISTEN` works on all three Unix variants.
On AIX: `lssrc -a` shows subsystem status. For process-to-port mapping, `netstat -Aan | grep LISTEN` followed by `rmsock
Document inode limits, filesystem types, and mount options. Solaris UFS and ZFS behave differently from ext4 and XFS. AIX JFS2 has a 16EB theoretical limit but tuning like `agblksize` does not translate. Run `df -k` and `mount` on the source, note every option, and plan Linux equivalents before you write a single fstab entry.
Also audit kernel parameters. On Solaris, `sysctl -a` (or `/etc/system` and `kstat`) exposes tuning that Linux sysctl may accept under different names or not at all. Solaris `rlim_fd_max` maps to Linux `fs.file-max` and `nofile` ulimits. Solaris `tcp_fin_wait_2_flush_interval` maps to Linux `net.ipv4.tcp_fin_timeout`. Build a mapping table before the migration starts.
# Solaris: export full package list
pkginfo -l | awk '/PKGINST|VERSION|NAME/ {print}' > /tmp/sol_packages.txt
# AIX: export installed filesets
lslpp -La > /tmp/aix_packages.txt
# HP-UX: export product list
swlist -l product > /tmp/hpux_packages.txt
# All platforms: capture listening services
netstat -an | grep LISTEN > /tmp/listeners.txt
# Solaris: SMF service export
svcs -a -o state,fmri > /tmp/smf_services.txt
Choosing the Target Linux Distribution
RHEL 9.3 (kernel 5.14.0) and Ubuntu 24.04 LTS (kernel 6.8) are the two distributions worth serious consideration in enterprise migration scenarios in 2026. SLES 15 SP6 remains relevant if you have existing SUSE contracts or SAP workloads. Avoid CentOS Stream as a primary production target - its rolling nature does not suit systems coming off long-lived Unix installs where stability is the default assumption.
RHEL is the better target if you are migrating Oracle DB, SAP, or any vendor application with a certified Linux support matrix. Check https://access.redhat.com/ecosystem before committing. Oracle DB 19c and 23ai are certified on RHEL 9. IBM Db2 11.5 is certified on RHEL 8 and 9. If your application vendor only certifies RHEL 8, install RHEL 8.9 and enable Application Streams to get current language runtimes.
Ubuntu 24.04 LTS is the better choice for cloud-native workloads, Python-heavy environments, or shops already running Ansible playbooks against Ubuntu. Its HWE kernel stack (6.8 on initial release, tracking newer kernels via `linux-generic-hwe-24.04`) handles newer hardware better out of the box.
For kernel parameter compatibility testing, we used a staging environment running both targets side by side and replayed production traffic using `tcpreplay`. On our test server, RHEL 9.3 with XFS showed 4-7% lower latency under database write load compared to Ubuntu 24.04 with ext4 at default settings - though tuning ext4 with `noatime,data=writeback` closed most of that gap.
If you are naming new internal projects or registering domains for the migrated infrastructure, a clean naming convention matters more than people expect. We have found nicename.me useful for checking whether a short, memorable project or subdomain name is available before you commit it to documentation, DNS, and monitoring configs.
# Check RHEL version and active repos
cat /etc/redhat-release
dnf repolist enabled
# Check Ubuntu HWE kernel status
uname -r
apt-cache policy linux-generic-hwe-24.04
# Verify XFS filesystem features on RHEL 9
xfs_info /dev/sda2
Shell and Script Compatibility: ksh, csh, and POSIX Gaps
The most common source of post-migration breakage is shell scripts. Solaris ships ksh88 by default in /bin/sh on older releases and ksh93 in /usr/bin/ksh. AIX ships ksh88 in /bin/sh. HP-UX ships POSIX sh in /bin/sh and ksh in /usr/bin/ksh. Linux distributions ship bash as /bin/sh via symlink to dash (on Ubuntu/Debian) or bash itself (on RHEL).
Dash is faster than bash but is not bash. It does not support arrays, `[[ ]]` conditionals, process substitution, or `$'...'` quoting. If your scripts use `#!/bin/sh` but depend on bashisms, they will fail silently or with confusing errors on Ubuntu.
Run `checkbashisms` from the `devscripts` package on every script that has `#!/bin/sh`:
``` apt install devscripts checkbashisms -p /path/to/script.sh ```
For ksh93 scripts, install `ksh` on Linux (`dnf install ksh` on RHEL, `apt install ksh` on Ubuntu). The AT&T ksh93u+m fork (version 93u+m/1.0.10 as of 2026) is the maintained upstream. Test each critical script explicitly:
Also audit for Solaris-specific commands embedded in scripts. `/usr/bin/nawk` on Solaris is not the same as `awk` (gawk 5.x) on Linux. The `print` statement in nawk does not exist in gawk. The `OFMT` variable behavior differs. Run your awk scripts through `gawk --lint` to surface compatibility issues.
Solaris `truss` becomes `strace` on Linux. Solaris `dtrace` has a Linux port but is less stable - use `perf`, `bpftrace`, or `SystemTap` instead. AIX `filemon` and `vmstat -v` translate to Linux `iostat -x` and `vmstat`. Build a command translation reference doc for your team before go-live.
# Install ksh93 on RHEL 9
dnf install ksh
# Install checkbashisms on Ubuntu 24.04
apt install devscripts
# Check a script for bash-specific syntax
checkbashisms -p /usr/local/bin/backup.sh
# Test ksh script under Linux ksh
ksh -n /usr/local/bin/legacy_deploy.sh
# Run awk script with lint checking
gawk --lint -f /usr/local/lib/report.awk input.csv
# strace equivalent of Solaris truss
strace -f -o /tmp/app_trace.txt ./myapp
Filesystem Layout, Mount Options, and Storage Migration
Solaris zones, AIX WPARs, and HP-UX vPars all have filesystem implications that do not map cleanly to Linux. Solaris ZFS pools with multiple datasets become either LVM logical volumes on Linux or ZFS on Linux (ZoL, now OpenZFS 2.2.x). OpenZFS on Linux is production-quality and supports most ZFS features including compression, deduplication, and snapshots, but pool import from Solaris requires matching ZFS pool version. Solaris 11.4 uses pool version 5000 (feature flags). OpenZFS 2.2 supports this, but test the import in a non-production environment first.
To import a Solaris ZFS pool on Linux:
If you are not carrying ZFS forward and are migrating to XFS or ext4, use `zfs send | zfs receive` to a Linux ZFS pool first, then migrate data off ZFS to the target filesystem with `rsync` or `cp -a`. Do not use `cp` without `-a` - you will lose timestamps, permissions, and extended attributes.
For AIX JFS2 to XFS migration: JFS2 is not readable on Linux directly. Use NFS or SFTP to transfer data. Mount the AIX filesystem via NFS on the Linux target (AIX NFSv4 exports work cleanly with Linux NFSv4 clients) and rsync across:
XFS is our recommendation for the Linux target filesystem on database and high-throughput workloads. Format with `mkfs.xfs -L dbdata -b size=4096 -s size=512 /dev/sdb1` and mount with `noatime,nodiratime,logbufs=8,logbsize=256k` for database workloads.
For `/etc/fstab`, replicate every mount option from the source system with its Linux equivalent. Solaris `logging` on UFS becomes `data=journal` on ext4. Solaris `nodevices` maps to `nodev`. Solaris `nosuid` maps directly. ZFS `mountpoint` property becomes an fstab entry or systemd mount unit.
# Import Solaris ZFS pool on Linux (OpenZFS 2.2)
zpool import -f -d /dev/disk/by-id datapool
zfs list -t all
# rsync from AIX NFS mount to Linux XFS
mount -t nfs4 aix-server:/data /mnt/aix_data
rsync -aHAX --progress /mnt/aix_data/ /data/migrated/
# Format XFS with tuned options for database workload
mkfs.xfs -L dbdata -b size=4096 -s size=512 /dev/sdb1
# fstab entry for tuned XFS mount
echo '/dev/sdb1 /data xfs noatime,nodiratime,logbufs=8,logbsize=256k 0 2' >> /etc/fstab
# Verify mount options took effect
findmnt -o TARGET,OPTIONS /data
Init System Translation: SMF and SRC to systemd
Solaris SMF, AIX SRC (System Resource Controller), and HP-UX inetd/init.d service definitions all need to be rewritten as systemd unit files. There is no automatic converter that handles this reliably - we tried three tools during our 2026 migration and all required manual cleanup on more than 40% of units.
A Solaris SMF manifest for a custom Java application translates to a systemd service unit as follows. The SMF `exec` method becomes `ExecStart`. The `start_timeout_seconds` becomes `TimeoutStartSec`. SMF dependencies (`require_all`, `require_any`) become systemd `After=` and `Requires=` directives. SMF `restarter:properties/restart_interval` maps to `RestartSec` with `Restart=on-failure`.
For AIX SRC subsystems, `startsrc -s myapp` becomes `systemctl start myapp`. The subsystem definition in `/etc/inittab` or the SRC `addssys` entry becomes a systemd unit in `/etc/systemd/system/`. If your AIX app used `stopsrc -s myapp` with a custom stop signal, set `KillSignal=SIGUSR1` (or whatever signal) in the unit file.
After writing unit files, validate them before enabling:
For complex service dependency chains coming from Solaris (SMF milestone dependencies), use `systemd-analyze dot --require myapp.service | dot -Tsvg > deps.svg` to visualize the dependency graph and verify it matches your intent.
Services that used Solaris `inetd` or HP-UX `inetd` translate to `systemd.socket` units with socket activation, which is cleaner than the inetd model and performs better under connection burst conditions.
# Example systemd unit translated from Solaris SMF
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=MyApp Java Service
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -Xmx4g -jar /opt/myapp/myapp.jar
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=10
TimeoutStartSec=120
TimeoutStopSec=30
EnvironmentFile=/etc/myapp/env
[Install]
WantedBy=multi-user.target
EOF
# Validate the unit file
systemd-analyze verify /etc/systemd/system/myapp.service
# Enable and start
systemctl daemon-reload
systemctl enable --now myapp.service
# Check dependency graph
systemd-analyze dot --require myapp.service | dot -Tpng > /tmp/deps.png
User, Group, and PAM Configuration Migration
Unix systems frequently use NIS, NIS+, or LDAP for user management. NIS is effectively deprecated on Linux - migrate to SSSD with LDAP or Active Directory if you are not already there. On RHEL 9, `authselect` replaces the old `authconfig` tool. On Ubuntu 24.04, `pam-auth-update` handles PAM profile selection.
To migrate NIS users to local accounts (for a standalone server migration):
For PAM configuration, Solaris PAM stacks in `/etc/pam.conf` translate to per-service files in `/etc/pam.d/` on Linux. The module names differ: `pam_unix.so` exists on both but Solaris `pam_authtok_check.so` (password quality) becomes `pam_pwquality.so` on Linux. Solaris `pam_roles.so` (RBAC) has no direct Linux equivalent - RBAC on Linux is typically handled via sudo rules or tools like CyberArk.
Export Solaris RBAC role definitions:
Solaris `/etc/security/exec_attr` and `/etc/security/prof_attr` need manual conversion to `/etc/sudoers.d/` entries. Use `visudo -c` after writing each file to validate syntax.
UID/GID numbering: Solaris system accounts often use UIDs below 100 that conflict with Linux reserved ranges. Audit with `awk -F: '$3 < 1000 {print}' /etc/passwd` on the source, then remap conflicting UIDs during migration using `usermod -u` on the Linux target after initial user creation. Update file ownership with `find / -uid
# Export NIS user map to flat file
ypcat passwd > /tmp/nis_users.txt
# Create local users from NIS export (simplified)
while IFS=: read -r user pass uid gid gecos home shell; do
useradd -u "$uid" -g "$gid" -c "$gecos" -d "$home" -s "$shell" "$user"
done < /tmp/nis_users.txt
# Export Solaris RBAC profiles
profiles -l > /tmp/rbac_profiles.txt
cat /etc/security/exec_attr > /tmp/exec_attr.txt
# Validate sudoers file after conversion
visudo -c -f /etc/sudoers.d/migrated_roles
# Find files owned by old UID
find /data -uid 105 -exec ls -la {} \;
# Remap file ownership after UID change
find /data -uid 105 -exec chown 1105 {} +
# Configure SSSD for LDAP on RHEL 9
authselect select sssd with-mkhomedir --force
Network Configuration and Hostname Translation
Solaris uses `ipadm` and `dladm` for network management. AIX uses `smitty` (UI) or `ifconfig`/`chdev` commands. HP-UX uses `lanscan` and `ifconfig`. Linux uses `ip` (iproute2) on RHEL and Ubuntu, with NetworkManager (`nmcli`) or systemd-networkd handling persistence.
Solaris interface names like `e1000g0` or `bge0` become predictable names on Linux (`ens3`, `enp2s0`) or traditional names (`eth0`) depending on your GRUB configuration. Map them before migration by noting MAC addresses on the source and matching to the Linux target's interfaces with `ip link show`.
Solaris `ndd` TCP tuning parameters (`ndd -set /dev/tcp tcp_recv_hiwat`) translate to Linux sysctl. Build a complete translation:
For persistent network config on RHEL 9 with NetworkManager:
On Ubuntu 24.04, Netplan is the default. Write YAML configs in `/etc/netplan/` instead of nmcli commands. Run `netplan try` to test before `netplan apply` to avoid locking yourself out.
Solaris link aggregation (`dladm create-aggr`) becomes Linux bonding (`modprobe bonding`) or LACP (`nmcli con add type bond`). Solaris IPMP (IP Multipathing) is conceptually similar to Linux bonding in active-backup mode. Map each IPMP group to a bond interface with `mode=active-backup` and `miimon=100`.
DNS resolver configuration: Solaris `/etc/resolv.conf` and `/etc/nsswitch.conf` map directly to Linux equivalents. On RHEL 9 with systemd-resolved, `/etc/resolv.conf` is a symlink. Edit resolver settings via `nmcli` or directly in `/etc/systemd/resolved.conf`. Check with `resolvectl status`.
# Map Solaris ndd TCP settings to Linux sysctl
# Solaris: ndd -set /dev/tcp tcp_recv_hiwat 400000
sysctl -w net.ipv4.tcp_rmem='4096 87380 400000'
# Solaris: ndd -set /dev/tcp tcp_fin_wait_2_flush_interval 67500
sysctl -w net.ipv4.tcp_fin_timeout=60
# Persist sysctl settings
cat >> /etc/sysctl.d/99-migration.conf << 'EOF'
net.ipv4.tcp_rmem = 4096 87380 400000
net.ipv4.tcp_fin_timeout = 60
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF
sysctl -p /etc/sysctl.d/99-migration.conf
# Configure bonding on RHEL 9 with nmcli
nmcli con add type bond con-name bond0 ifname bond0 \
bond.options 'mode=active-backup,miimon=100'
nmcli con add type ethernet con-name bond0-port1 \
ifname eth0 master bond0
nmcli con add type ethernet con-name bond0-port2 \
ifname eth1 master bond0
nmcli con up bond0
# Check DNS resolution chain on RHEL 9
resolvectl status
Automation and Testing: Reducing Cutover Risk
A migration of this scope should not be a manual weekend event. Automate the pre-migration audit, the configuration translation, and the post-migration validation with Ansible or Terraform, and use a CI pipeline to run smoke tests against each migrated host before it enters production rotation.
For the audit phase, Ansible facts (`ansible_devices`, `ansible_mounts`, `ansible_interfaces`, `ansible_env`) collect most of what you need from both source Unix systems (via the AIX or Solaris connection plugins) and Linux targets. Write a playbook that compares facts between source and target and flags mismatches in mount options, ulimits, and open ports.
For the validation phase after cutover, define a post-migration test suite: verify all expected systemd services are active, all expected ports are listening, all cron jobs exist in crontab, and application health endpoints return 200. Run this suite from your CI system immediately after each host is cut over.
For teams managing complex multi-service cutover sequences with approval gates and rollback logic, AI-assisted DevOps automation platforms like taskbotshub.ai can orchestrate the cutover workflow, track per-host migration state, and trigger rollback runbooks automatically when post-migration health checks fail. We tested this for the parallel cutover of eight application servers and it reduced the manual coordination overhead significantly compared to a pure Ansible playbook approach.
Rollback planning: for each migrated service, define the rollback procedure before migration day. For stateless services, rollback is a DNS change or load balancer weight shift. For stateful services (databases, message queues), rollback requires either a standby still running on the Unix source or a recent snapshot that can be restored in under your RTO window. Document and test rollback for every critical service before the production cutover window opens.
# Ansible post-migration validation playbook snippet
- name: Validate migrated host
hosts: migrated_targets
tasks:
- name: Check required services are active
ansible.builtin.systemd:
name: "{{ item }}"
register: svc_status
loop:
- myapp
- postgresql
- nginx
failed_when: svc_status.status.ActiveState != 'active'
- name: Verify listening ports
ansible.builtin.wait_for:
port: "{{ item }}"
timeout: 5
loop:
- 8080
- 5432
- 443
- name: Check ulimits match source
ansible.builtin.command: ulimit -n
register: nofile_limit
failed_when: nofile_limit.stdout | int < 65536
- name: Application health check
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
retries: 3
delay: 5
Cron, Scheduler, and Time Zone Migration
Solaris crontab format and Linux crontab format are compatible at the syntax level, but the environment differs. Solaris cron sets `PATH=/usr/bin:/usr/sbin` by default. Linux cron (crond on RHEL, cron on Ubuntu) sets a minimal PATH. Scripts that relied on implicit PATH resolution will silently fail. Add explicit PATH lines at the top of every migrated crontab.
AIX `cron` supports the same five-field format. Export all crontabs from the source:
Solaris also uses `at` jobs. Export them with `atq` and `at -l`, then recreate on Linux. The `batch` command behavior differs - on Solaris it runs when system load drops below 1.5 by default; on Linux, the threshold is configurable in `/etc/at.allow` and the load average threshold is set at compile time (typically 1.5 as well, but verify with your distribution package).
Time zone configuration: Solaris stores timezone in `/etc/TIMEZONE` as a variable. Linux stores it in `/etc/localtime` (symlink to `/usr/share/zoneinfo/`) and `/etc/timezone` (plain text on Debian/Ubuntu) or via `timedatectl`. Set with:
For NTP, Solaris uses xntpd or chrony. Linux RHEL 9 and Ubuntu 24.04 both ship chronyd by default. AIX uses xntpd. Migrate your NTP server list to `/etc/chrony.conf` and verify sync with `chronyc tracking` after migration. For high-precision timing requirements (financial systems, distributed databases), also set `makestep 1.0 3` in chrony.conf to allow immediate step corrections on startup.
# Export all user crontabs from Solaris
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null && \
echo "=== $user ==="
done > /tmp/all_crontabs.txt
# Add PATH to migrated crontab on Linux
(echo 'PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'; crontab -l) | crontab -
# Set timezone on RHEL 9 / Ubuntu 24.04
timedatectl set-timezone America/New_York
timedatectl status
# Configure chrony with source NTP servers
cat > /etc/chrony.conf << 'EOF'
server ntp1.internal.example.com iburst
server ntp2.internal.example.com iburst
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony
EOF
systemctl restart chronyd
chronyc tracking
chronyc sources -v