Tuesday, December 28, 2010

NFS Root

It's handy to be able to boot a system from an NFS root drive. I use it mostly for getting 'underneath' an operating system for things like installs or repairs, but you can also run indefinitely if that suits your environment.

If you look back to a post I did in September you will get the basics for setting up your PXE server. You will also need to create a custom kernel, or find one that allows for PXE booting with an NFS root partition. Generally I build a kernel with all of the required drivers installed including NFS root support.

The part that is generally a big problem for me is getting a root image setup to run from. I am a fan of building my own as I have more control in the operation and customization of a system but this can lead to problems. Local binaries included with the distributions I know all require libraries to which they are linked in order to run. So, I wrote a handy script to do all this work for me, from the current running system.

The following works for a RedHat based system. Unfortunately, distributions don't keep binaries in the same place, so if you want to use this you may have to change the file locations. There are four variables to do this job, SBIN_FILES, BIN_FILES, DEV_FILES, USER_BIN_FILES. Each correlates to a directory, for example SBIN_FILES = /sbin. Just find the files you want to include and put it in the corresponding variable. The script will take care of all the dependent libraries for you.

You can also specify some command line options if you like, -delete will clear out the destination directory before it does a build, and -bootdir will change the target build location.

The script uses udev so there isn't much need for /dev entries, otherwise have at it.

#!/bin/bash

BOOT_DIR=/tftpboot/boot-image
MOUNT_POINTS="proc sys mnt/src mnt/dest tmp"
LDD=/usr/bin/ldd

PRE_DELETE=0
SBIN_FILES="init ifconfig reboot poweroff mke2fs mkfs.ext3 mkfs.ext4 mkswap fdisk udevd udevadm mkinitrd ethtool fsck.ext3 fsck.ext4 hdparm shutdown"
BIN_FILES="bash mount cp umount dd rm ls cat more vim tar gzip ps"
DEV_FILES="console null"
USER_BIN_FILES="chroot tclsh vi"
LOG_FILE=/home/mike/nfs_build_log

######
### function setup_programs
### prepends required directories to overall file list to keep things looking clean
######
function setup_binaries {
        for file in $SBIN_FILES
        do
                FINAL_SBIN="$FINAL_SBIN /sbin/$file"
        done
        for file in $BIN_FILES
        do
                FINAL_BIN="$FINAL_BIN /bin/$file"
        done
        for file in $DEV_FILES
        do
                FINAL_DEV="$FINAL_DEV /dev/$file"
        done
        for file in $USER_BIN_FILES
        do
                FINAL_USER_BIN="$FINAL_USER_BIN /usr/bin/$file"
        done
        # delete everything in the boot directory if asked to
        if [ $PRE_DELETE -eq 1 ]
        then
                rm -rf $BOOT_DIR
        fi
}

function log {
        echo -e "`date +%H:%M:%S` -- "$1"" >> $LOG_FILE
}

######
### function create_etc
### populates the new /etc directory with a minimal set of startup scripts
### inittab, rc.sysinit, and boot.udev
######
function create_etc {
        mkdir -p $BOOT_DIR/etc

        # create inittab
        echo "id:3:initdefault:" > $BOOT_DIR/etc/inittab
        echo "si::sysinit:/etc/rc.sysinit" >> $BOOT_DIR/etc/inittab
        echo "id2:3:wait:/bin/bash" >> $BOOT_DIR/etc/inittab

        # create rc.sysinit
        echo "#!/bin/bash" > $BOOT_DIR/etc/rc.sysinit
        echo "echo \"--Running sysinit--\"" >> $BOOT_DIR/etc/rc.sysinit
        echo "echo -n \"Mounting proc filesystem: \"; mount -n -t proc /proc /proc; echo \"Done\"" >> $BOOT_DIR/etc/rc.sysinit
        echo "echo -n \"Mounting sys filesystem: \"; mount -n -t sysfs /sys /sys; echo \"Done\"" >> $BOOT_DIR/etc/rc.sysinit
        echo "echo -n \"Starting udev: \"; /etc/boot.udev; echo \"Done\"" >> $BOOT_DIR/etc/rc.sysinit
        echo "echo -n \"Configuring loopback network adapter: \"; /sbin/ifconfig lo 127.0.0.1; echo \"Done\"" >> $BOOT_DIR/etc/rc.sysinit
        echo "echo -n \"Clearing /etc/mtab\"; > /etc/mtab" >> $BOOT_DIR/etc/rc.sysinit
        chmod ug+x $BOOT_DIR/etc/rc.sysinit

        # create boot.udev assuming udevd and udevadm are in /sbin
        if [[ "$SBIN_FILES" == *udevd* ]] && [[ "$SBIN_FILES" == *udevadm* ]]
        then
                echo "#!/bin/bash" > $BOOT_DIR/etc/boot.udev
                echo "echo \"--Running udev--\"" >> $BOOT_DIR/etc/boot.udev
                echo "echo \"\" > /sys/kernel/uevent_helper" >> $BOOT_DIR/etc/boot.udev
                echo "echo -n \"Starting udevd \"" >> $BOOT_DIR/etc/boot.udev
                echo "rm -rf /dev/.udev" >> $BOOT_DIR/etc/boot.udev
                echo "/sbin/udevd --daemon" >> $BOOT_DIR/etc/boot.udev
                echo "/sbin/udevadm trigger --type=subsystems" >> $BOOT_DIR/etc/boot.udev
                echo "/sbin/udevadm trigger --type=devices" >> $BOOT_DIR/etc/boot.udev
                echo "/sbin/udevadm settle --timeout=180" >> $BOOT_DIR/etc/boot.udev
                chmod ug+x $BOOT_DIR/etc/boot.udev
        fi
}

######
### function get_libraries
### helper function for copy_files
### executes ldd against a binary to retrieve all dependent libraries
### executes copy_files with the "library_file" flag to prevent it from running get_libraries again
######
function get_libraries {
        local FILE_NAME="$1"
        log "Checking library for $FILE_NAME"
        LDD_OUTPUT=`ldd "${FILE_NAME}" 2> /dev/null`
        # e.g. libc.so.6 => /lib64/libc.so.6 (0x00007f7a03b64000)
        # look at each index in the output checking if it's a file, then pass it to copy_files e.g. /lib64/libc.so.6
        for index in $LDD_OUTPUT
        do
                if [ -f $index ]
                then
                        LIBRARY_FILES="$LIBRARY_FILES $index"
                fi
        done

}

######
### function copy_files
### copies a source file to BOOT_DIR with the proper destination directory
### e.g. /bin/ls becomes $BOOT_DIR/bin/ls
### if the file is a symbolic link, follow it through depending if it is a relative or absolute path
######
function copy_files {
        local FILE_NAME="$1"
        log "Copying $FILE_NAME"
        # find where this file should go based BOOT_DIR + it's original path
        # e.g. /tftpboot/boot-image + /sbin
        DEST_DIR="$BOOT_DIR/`dirname "$FILE_NAME"`"
        if [ ! -d "${DEST_DIR}" ]
        then
                mkdir -p "${DEST_DIR}"
        fi
        # the -a will preserve any symbolic links
        cp -a "${FILE_NAME}" "${DEST_DIR}"

        # if this is a link find what the real file is and copy that
        if [ -L "$FILE_NAME" ]
        then
                # ideally we could use canonicalize mode (-f) as that always returns an absolute path but that won't handle links pointing to links
                LINK_FILE=`readlink "$FILE_NAME"`
                # check to see if this is an absolute path
                # e.g. /lib64/libpthread.so.0 -> libpthread-2.10.1.so
                LINK_DIR=`dirname "$LINK_FILE"`         #e.g. ./
                if [ ! ${LINK_DIR:0:1} == "/" ]
                then
                        # find the path of the original file
                        FILE_DIR=`dirname "$FILE_NAME"` #e.g. /lib64
                        # change to the original path + relative path and find where we are with pwd
                        cd "$FILE_DIR"/"$LINK_DIR"              #e.g. cd /lib64/.
                        LINK_PATH=`pwd`                         #e.g. /lib64
                        # recreate the file with the proper path of the link
                        LINK_FILE="$LINK_PATH"/`basename "$LINK_FILE"`  # e.g. /lib64/libpthread-2.10.1.so
                fi
                copy_files "${LINK_FILE}" $2
                # need to copy the file, if it returns no path it is in the same dir, otherwise it will be absolute path
        fi
        if [ ! "$2" == "library_file" ]
        then
                get_libraries "$FILE_NAME"
        fi
}

######
### function create_mount_points
######
function create_mount_points {
        for mount in $MOUNT_POINTS
        do
                mkdir -p $BOOT_DIR/$mount
        done
}

######
### function usage
### outputs usage information for this script and then exits
######
function usage {
        echo ""
        echo -e "Usage:\n`basename $0` -delete -bootdir "
        echo "-delete"
        echo "  remove old data before creating new content, default is to keep"
        echo "-bootdir"
        echo "  directory to copy binaries to, default is /tftpboot/boot-image"
        echo ""
        exit 0
}

if [ $# -eq 0 ]
then
        usage
fi

until [ -z "$1" ]
do
        case "$1" in
        -delete)
                PRE_DELETE=1
                ;;
        -bootdir)
                shift
                BOOT_DIR="$1"
                ;;
        *)
                usage
                ;;
        esac
        shift
done


setup_binaries

for file in $FINAL_SBIN $FINAL_BIN $FINAL_DEV $FINAL_USER_BIN
do
        copy_files $file
done

if [ -n "$LIBRARY_FILES" ]
then
        LIBRARY_FILES=`echo $LIBRARY_FILES | tr " " "\n" | sort -u`
        for file in $LIBRARY_FILES
        do
                copy_files $file "library_file"
        done
fi

create_etc