Setup NetBoot Service on Mac OS X 10.6.x Client - Diskless NetBoot

By: Richard Glaser - Revised: 2011-07-21 rcg

Index

Introduction

In certain, situations you never want use a clients local disk for the NetBoot shadow file, like if you want to run hard disk utilities like Disk Utility or Disk Warrior, or for us if we want to run the file system maintenance software we use, radmind, out-of-context. Where we boot from NetBoot, but we update the file system on the clients local drives.

Also, due to the limitations of number concurrent Apple File Protocol [AFP] connections using Mac OS X 10.6.x (and others) client's File Sharing. If you need to NetBoot more than 10 clients and run maintenance utilities, you can't use a network shadow file, because after the tenth client connecting, it will either try to use a local disk for the shadow file or not NetBoot properly.

NetInstall - rc.cdrom

But, there is a 3rd option that NetInstall & NetRestore use saving the shadow file to a RAM disk. If you view a NetInstall image, you have the following...

NetInstall Image

Mount the "NetInstall.dmg" and then open the file...

etc/rc.cdrom

You will get the idea on how to setup a RAM disk that will work with NetBoot, NetInstall & NetRestore...

For example, here is the default rc.cdrom file included with Mac OS X 10.6.7 NetInstall image...

#!/bin/sh
# Copyright 2000-2009, Apple Inc.

#
#
# NOTICE!
# Most of rc.cdrom is in rc.install temporarily while portions are migrated to launchd
#
#

#
# Disable prebinding-on-the-fly while we're CD booted
#
export DYLD_NO_FIX_PREBINDING=1

#
# mount root_device to update vnode information
#
mount -u -o ro /

#
# Sanity check date & time. Pin to 4/1/1976.
#
if [ `date +%s` -lt 197193600 ]; then
    date 040100001976
fi

#
# Create a RAM disk with same perms as mountpoint
#
RAMDisk()
{
    mntpt=$1
    rdsize=$2
    echo "Creating RAM Disk for $mntpt"
    dev=`hdik -drivekey system-image=yes -nomount ram://$rdsize`
    if [ $? -eq 0 ] ; then
        newfs_hfs $dev
        # save & restore fs permissions covered by the mount
        eval `/usr/bin/stat -s $mntpt`
        mount -t hfs -o union -o nobrowse $dev $mntpt
        chown $st_uid:$st_gid $mntpt
        chmod $st_mode $mntpt
    fi
}

RAMDisk /Volumes 1024
RAMDisk /var/tmp 1024
RAMDisk /var/run 1024

RAMDisk /var/db 1024
mkdir -m 1777 /var/db/mds

# language prefs, colorsync need to be able to write some preferences (5424449)
RAMDisk /Library/Preferences 1024
RAMDisk /Library/ColorSync/Profiles/Displays 2048

# use or create the boot cache playlist, and allow B&I to force 32-bit playlist generation
FORCETHIRTYTWO="false"
if nvram boot-args | grep "no64exec" ; then
    FORCETHIRTYTWO="true"
fi

SIXTYFOURBIT=`sysctl -n hw.cpu64bit_capable`

if [ $SIXTYFOURBIT = "0" -o $FORCETHIRTYTWO = "true" ] ; then
    echo "using 32-bit bootcache playlist"
    BootCacheControl -f /var/db/BootCache.playlist32 start
elif [ $SIXTYFOURBIT = "1" ] ; then
    echo "using 64-bit bootcache playlist"
    BootCacheControl -f /var/db/BootCache.playlist start
fi

# tell launchd to commence with loading the system.
# for the OS Install environment only, /etc/rc.install is included in this process.
launchctl load -D system

# this script sleeps forever; the installer or startup disk will always reboot the system.
sleep 9999999

NetBoot - rc.netboot

And if you view a NetBoot image, you have the following...

NetBoot Image Structure

Mount the "NetBoot.dmg" and then open the file...

etc/rc.netboot

For example, here is the default rc.netboot file included with Mac OS X 10.6.7 NetBoot image.

#!/bin/sh
##
# Copyright 2002-2009 Apple Inc.
#
# This script configures NetBoot
##

. /etc/rc.common

#
# Define: NETBOOT_SHADOW
# Purpose:
# To change the behavior of the system when choosing a netboot shadow
# to use.
# Values:
# -NETWORK- Try to use the network for the shadow file, if
# that fails, use the local drive
# -NETWORK_ONLY- Only use the network, fail if not available
# -LOCAL- Use the local drive for the shadow file, if that
# fails, use the network
# -LOCAL_ONLY- Only use the local drive for the shadow, fail if
# not available

NETBOOT_MOUNT=/var/netboot
NETBOOT_SHADOW=${NETBOOT_SHADOW:-NETWORK-}

Failed()
{
    echo rc.netboot: $1
    echo rc.netboot: $1 > /dev/console
    sleep 5
    exit 1
}

common_start()
{
    netboot_dir=$1
    netboot_shadow=$2
    if [ "${netboot_dir}" = "" ] ; then
    Failed "netboot_dir is empty"
    fi
    if [ "${netboot_shadow}" = "" ] ; then
    Failed "netboot_shadow is empty"
    fi
    netboot_shadow="${netboot_dir}/${netboot_shadow}"
    if ! mkdir -p "${netboot_dir}" ; then
    Failed "create ${netboot_dir} failed"
    fi
    chmod 700 "${netboot_dir}"
    mount -u -o ro /
    root_device=$(mount | sed -n 's:/dev/\(.*\) on / .*:\1:p')
    case "${root_device}" in
    vn*)
        if ! touch "${netboot_shadow}" ; then
        Failed "create ${netboot_shadow} failed"
        fi
        chmod 600 "${netboot_shadow}"
            if ! /usr/libexec/vndevice shadow "/dev/r${root_device}" "${netboot_shadow}" ; then
        Failed "vndevice shadow failed"
        fi
        ;;
    "")
        Failed "root device unknown"
        ;;
    *)
        if ! touch "${netboot_shadow}" ; then
        Failed "failed to create shadow ${netboot_shadow}"
        fi
        chmod 600 "${netboot_shadow}"
        if ! /usr/bin/nbdst -recycle "${root_device}" "${netboot_shadow}" ; then
        Failed "nbdst failed"
        fi
        ;;
    esac
}

local_mount()
{
    tries=0
    limit=11
    while [ $tries -lt $limit ]; do
    tries=$(( tries + 1 ))
    volinfo=`autodiskmount -F 2>/dev/null`
    if [ $? -ne 0 ]; then
        if [ $tries -lt $limit ]; then
        echo "Waiting for local drives..."
        echo "Waiting for local drives (retry ${tries}/$(( limit - 1 )))..." > /dev/console
        sleep 5
        else
        echo "autodiskmount -F found no local drives"
        return 1
        fi
    else
        tries=$limit
    fi
    done
    set ${volinfo}
    devname=$1
    fstype=$2

    mount -t "${fstype}" -o nosuid,nodev "/dev/${devname}" "${NETBOOT_MOUNT}" 2>&1
    if [ $? -ne 0 ]; then
    echo "mount of ${devname} failed"
    return 1
    fi
    common_start "${NETBOOT_MOUNT}/.com.apple.NetBootX" shadowfile
    return 0
}

network_mount()
{
    mount_from=$(ipconfig netbootoption shadow_mount_path 2>&1)
    if [ $? -ne 0 ]; then
    echo "no network shadow mount path available"
    return 1
    fi
    shadow_path=$(ipconfig netbootoption shadow_file_path 2>&1)
    if [ $? -ne 0 ]; then
    echo "no network shadow file path available"
    return 1
    fi
    case "${mount_from}" in
    afp:*)
        fstype=afp
        kextutil -v 0 /System/Library/Filesystems/AppleShare/asp_tcp.kext
        kextutil -v 0 /System/Library/Filesystems/AppleShare/afpfs.kext
        ;;
    nfs:*) fstype=nfs;;
    *) echo "unknown network filesystem mount from ${mount_from}"
        return 1
        ;;
    esac
    mount -t "${fstype}" -o nobrowse "${mount_from}" "${NETBOOT_MOUNT}"
    if [ $? -ne 0 ]; then
    echo "mount -t ${fstype} -o nobrowse ${mount_from} ${NETBOOT_MOUNT} failed"
    return 1
    fi
    common_start "${NETBOOT_MOUNT}" "${shadow_path}"
    return 0
}

do_start()
{
    case "${NETBOOT_SHADOW}" in
    -LOCAL_ONLY-)
        err=$(local_mount)
        if [ $? -ne 0 ]; then
            Failed "${err}"
        fi
        ;;
    -LOCAL-)
        err=$(local_mount)
        if [ $? -ne 0 ]; then
            err=$(network_mount)
            if [ $? -ne 0 ]; then
                Failed "Could not find a local or network drive"
            fi
        fi
        ;;
    -NETWORK_ONLY-)
    err=$(network_mount)
    if [ $? -ne 0 ]; then
        Failed "${err}"
    fi
    ;;

    *)
    err=$(network_mount)
    if [ $? -ne 0 ]; then
        err=$(local_mount)
        if [ $? -ne 0 ]; then
            Failed "Could not find a network or local drive"
        fi
    fi
    ;;
    esac

}

do_init()
{
    # attach the shadow file to the root disk image
    do_start

    # make sure the root filesystem is clean
    fsck -p || fsck -fy || Failed "Could not clean root filesystem"

    # make it writable
    mount -uw /

    # adjust /private/var/vm to point to the writable area (if not diskless)
    swapdir=/private/var/vm
    mounted_from=$(mount | sed -n 's:\(.*\) on .*/var/netboot.*:\1:p')
    case "${mounted_from}" in
    /dev/*)
        netboot_dir="${NETBOOT_MOUNT}/.com.apple.NetBootX"
        if [ -d "${netboot_dir}" ]; then
            rm -rf "${swapdir}"
            ln -s "${netboot_dir}" "${swapdir}"
        fi
        ;;
    *)
    ;;
    esac

    # set the ComputerName based on what the NetBoot server told us it was
    machine_name=$(ipconfig netbootoption machine_name 2>&1)
    if [ $? -ne 0 ]; then
    echo "no machine name option available"
    else
    echo "Setting ComputerName to ${machine_name}"
    scutil --set ComputerName "${machine_name}"
    fi
}

if [ $# -lt 1 ] ; then
    exit 0
fi

command=$1

shift

case "${command}" in
    init)
    do_init $@
    ;;
esac

##
# Exit
##
exit 0

Modifying rc.netboot to support RAM Disk

To work around a variable assignment bug in the original rc.neboot script, change the following line from...

NETBOOT_SHADOW=${NETBOOT_SHADOW:-NETWORK-}

to...

# Change to "-LOCAL_ONLY-" to use RAM Disk instead of network
NETBOOT_SHADOW=-LOCAL_ONLY-

In the local_mount() subroutine, comment out the following lines...

#     tries=0
#     limit=11
#     while [ $tries -lt $limit ]; do
#     tries=$(( tries + 1 ))
#     volinfo=`autodiskmount -F 2>/dev/null`
#     if [ $? -ne 0 ]; then
#         if [ $tries -lt $limit ]; then
#             echo "Waiting for local drives..."
#             echo "Waiting for local drives (retry ${tries}/$(( limit - 1 )))..." > /dev/console
#             sleep 5
#         else
#         echo "autodiskmount -F found no local drives"
#         return 1
#         fi
#     else
#         tries=$limit
#     fi
#     done
#     set ${volinfo}
#     devname=$1
#     fstype=$2
#
#     mount -t "${fstype}" -o nosuid,nodev "/dev/${devname}" "${NETBOOT_MOUNT}" 2>&1
#     if [ $? -ne 0 ]; then
#         echo "mount of ${devname} failed"
#         return 1
#     fi

Add the following two lines to the local_mount() subroutine...

volinfo=`autodiskmount -F 2>/dev/null`
RAMDisk "${NETBOOT_MOUNT}"

So, the entire updated local_mount() subroutine should look like this...

local_mount()
{
#     tries=0
#     limit=11
#     while [ $tries -lt $limit ]; do
#     tries=$(( tries + 1 ))
#     volinfo=`autodiskmount -F 2>/dev/null`
#     if [ $? -ne 0 ]; then
#         if [ $tries -lt $limit ]; then
#             echo "Waiting for local drives..."
#             echo "Waiting for local drives (retry ${tries}/$(( limit - 1 )))..." > /dev/console
#             sleep 5
#         else
#         echo "autodiskmount -F found no local drives"
#         return 1
#         fi
#     else
#         tries=$limit
#     fi
#     done
#     set ${volinfo}
#     devname=$1
#     fstype=$2
#
#     mount -t "${fstype}" -o nosuid,nodev "/dev/${devname}" "${NETBOOT_MOUNT}" 2>&1
#     if [ $? -ne 0 ]; then
#         echo "mount of ${devname} failed"
#         return 1
#     fi
      volinfo=`autodiskmount -F 2>/dev/null`
      RAMDisk "${NETBOOT_MOUNT}"
      common_start "${NETBOOT_MOUNT}/.com.apple.NetBootX" shadowfile
      return 0
}

Lastly, add the following RAMDisk() subroutine to the rc.netboot script.

#
# Create a RAM disk with same perms as mountpoint
#

RAMDisk()
{
     mntpt=$1
     rdsize=500000
     echo "Creating RAM Disk for $mntpt"
     dev=`hdik -drivekey system-image=yes -nomount ram://$rdsize`
     if [ $? -eq 0 ] ; then
          newfs_hfs $dev
          # save & restore fs permissions covered by the mount
          eval `/usr/bin/stat -s $mntpt`
          mount -t hfs -o union -o nobrowse $dev $mntpt
          chown $st_uid:$st_gid $mntpt
          chmod $st_mode $mntpt
     fi
}

Download edited rc.netboot that supports diskless or ram-disk NetBoot.

rc.netboot NETBOOT_SHADOW Variable Assignment Bug

The rc.netboot created with "System Image Utility" using Mac OS X 10.6.7 has a bug in the NETBOOT_SHADOW variable Assignment...

NETBOOT_SHADOW=${NETBOOT_SHADOW:-NETWORK-}

This assignment loses the beginning "-" so it never correctly matches the variable in the case statement in the do_start() sub routine and always uses the "*" branch. No matter what value you assign to the NETBOOT_SHADOW variable.

# Define: NETBOOT_SHADOW
# Purpose:
# To change the behavior of the system when choosing a netboot shadow
# to use.
# Values:
# -NETWORK- Try to use the network for the shadow file, if
# that fails, use the local drive
# -NETWORK_ONLY- Only use the network, fail if not available
# -LOCAL- Use the local drive for the shadow file, if that
# fails, use the network
# -LOCAL_ONLY- Only use the local drive for the shadow, fail if
# not available

See the highlighted "*" branch in the default case statement below.

do_start()
{
    case "${NETBOOT_SHADOW}" in
    -LOCAL_ONLY-)
        err=$(local_mount)
        if [ $? -ne 0 ]; then
            Failed "${err}"
        fi
        ;;
    -LOCAL-)
        err=$(local_mount)
        if [ $? -ne 0 ]; then
            err=$(network_mount)
            if [ $? -ne 0 ]; then
                Failed "Could not find a local or network drive"
            fi
        fi
        ;;
    -NETWORK_ONLY-)
        err=$(network_mount)
        if [ $? -ne 0 ]; then
            Failed "${err}"
        fi
        ;;

    *)
        err=$(network_mount)
        if [ $? -ne 0 ]; then
           err=$(local_mount)
            if [ $? -ne 0 ]; then
                Failed "Could not find a network or local drive"
            fi
        fi

        ;;
    esac
}

I have file a bu with Apple BugReport, if you would like to file a bug on this issue, you can reference my Problem ID: 9665982

A workaround this bug, is either escape the "-" character in the variable assigment or change the assigment to something like...

NETBOOT_SHADOW=-LOCAL_ONLY-

Lastly, you take this updated rc.netboot file and replace it with the one you are using with your NetBoot image.