PSARC 2012/074 UEFI/GRUB2/Large Disk Boot (UGLDB) Install Phase I:
authorNiall Power <niall.power@oracle.com>
Fri, 23 Mar 2012 04:40:57 -0700
changeset 1617 0a764388bf3b
parent 1616 019e7d84cab8
child 1618 b30e8ae1f173
PSARC 2012/074 UEFI/GRUB2/Large Disk Boot (UGLDB) Install Phase I: 7155522 Install needs to support UEFI & GPT (phase I) 7154007 Distro Constructor support for UEFI/GRUB2/Large Disk Boot Project 7047968 installadm should utilize pybootmgmt 7126748 installadm create-client generates unwanted error message if DHCP service is not configured
usr/src/cmd/distro_const/checkpoints/create_iso.py
usr/src/cmd/distro_const/checkpoints/pre_pkg_img_mod.py
usr/src/cmd/install-tools/usbcopy
usr/src/cmd/install-tools/usbgen
usr/src/cmd/installadm/client_control.py
usr/src/cmd/installadm/create_client.py
usr/src/cmd/installadm/create_service.py
usr/src/cmd/installadm/delete_service.py
usr/src/cmd/installadm/dhcp.py
usr/src/cmd/installadm/grub.py
usr/src/cmd/installadm/installadm-convert.py
usr/src/cmd/installadm/rename_service.py
usr/src/cmd/installadm/service.py
usr/src/cmd/installadm/service_config.py
usr/src/cmd/installadm/set_service.py
usr/src/cmd/installadm/test/test_create_service.py
usr/src/cmd/installadm/test/test_dhcp.py
usr/src/cmd/installadm/test/test_grub.py
usr/src/lib/install_boot/boot.py
usr/src/lib/install_boot/test/test_boot.py
usr/src/pkg/manifests/install-distribution-constructor.mf
usr/src/pkg/manifests/install-installadm.mf
usr/src/pkg/manifests/system-library-install.mf
--- a/usr/src/cmd/distro_const/checkpoints/create_iso.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/distro_const/checkpoints/create_iso.py	Fri Mar 23 04:40:57 2012 -0700
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 """ create_iso.py - Generates an ISO media based on the prepared package image
@@ -59,6 +59,7 @@
         self.media_dir = None
 
         self.bios_eltorito = None
+        self.uefi_eltorito = None
         self.add_timestamp = None
         self.distro_name = None
         self.dist_iso = None
@@ -111,7 +112,9 @@
             if dc_pers_dict:
                 self.dc_pers_dict = dc_pers_dict[0].data_dict
             if self.arch == 'i386':
+                # We must have at least a bios eltorito. UEFI may be provided.
                 self.bios_eltorito = self.dc_pers_dict["bios-eltorito-img"]
+                self.uefi_eltorito = self.dc_pers_dict.get("uefi-eltorito-img")
         except KeyError, msg:
             raise RuntimeError("Error retrieving a value from the DOC: " + \
                 str(msg))
@@ -142,14 +145,26 @@
 
         # set the mkisofs_cmd
         if self.arch == "i386":
-            self.mkisofs_cmd = [cli.MKISOFS, "-quiet", "-o", self.dist_iso,
-                                "-b", self.bios_eltorito, "-c",
-                                ".catalog", "-no-emul-boot",
-                                "-boot-load-size", "4", "-boot-info-table",
-                                "-N", "-l", "-R", "-U", "-allow-multidot",
-                                "-no-iso-translate", "-cache-inodes", "-d",
-                                "-D", "-volset", self.volsetid, "-V",
-                                self.partial_distro_name, self.pkg_img_path]
+            # The mkisofs args are split up like this to allow optional
+            # inclusion/exclusion of the uefi_args. The order of the boot
+            # arguments for making a hybrid BIOS/UEFI bootabable is very
+            # rigid and any reordering will almost certainly result in an
+            # unbootable image.
+            base_args = [cli.MKISOFS, "-quiet", "-o", self.dist_iso]
+            ctlg_args = ["-c", ".catalog"]
+            bios_args = ["-b", self.bios_eltorito, "-no-emul-boot",
+                         "-boot-load-size", "4", "-boot-info-table"]
+            uefi_args = ["-eltorito-platform", "efi",
+                         "-eltorito-alt-boot",
+                         "-b", self.uefi_eltorito, "-no-emul-boot"]
+            misc_args = ["-N", "-l", "-R", "-U", "-allow-multidot",
+                         "-no-iso-translate", "-cache-inodes", "-d",
+                         "-D", "-volset", self.volsetid, "-V",
+                         self.partial_distro_name, self.pkg_img_path]
+            self.mkisofs_cmd = base_args + ctlg_args + bios_args
+            if self.uefi_eltorito is not None:
+                self.mkisofs_cmd.extend(uefi_args)
+            self.mkisofs_cmd.extend(misc_args)
         else:
             self.mkisofs_cmd = [cli.MKISOFS, "-quiet", "-o", self.dist_iso,
                                 "-G", bootblock, "-B", "...", "-N", "-l",
--- a/usr/src/cmd/distro_const/checkpoints/pre_pkg_img_mod.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/distro_const/checkpoints/pre_pkg_img_mod.py	Fri Mar 23 04:40:57 2012 -0700
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 """ pre_pkg_img_mod.py - Customizations to the package image area before
@@ -170,11 +170,14 @@
                                                       "etc/inet/hosts"))
 
     def save_menu_lst(self):
-        """ class method to save the original /boot/grub/menu.lst file
+        """ class method to save the original /boot/grub/menu.lst file if it
+            exists. It will not exist on GRUB2 based images so it is silently
+            ignored if not present.
         """
-        
+
         save_list = ["boot/grub/menu.lst"]
-        self.save_files_directories(save_list)
+        if os.path.exists(os.path.join(self.pkg_img_path, save_list[0])):
+            self.save_files_directories(save_list)
 
     def save_files_directories(self, save_list=None):
         """ class method for saving key files and directories for restoration
--- a/usr/src/cmd/install-tools/usbcopy	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/install-tools/usbcopy	Fri Mar 23 04:40:57 2012 -0700
@@ -19,8 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 # Make sure we get the right versions of the commands
@@ -30,6 +29,94 @@
 # formats on us
 export LC_ALL=C
 
+function cleanup {
+
+    {
+
+        # unmounting, and uninstalling the lofi'ed devices and
+        # cleanup temporary files.
+
+        for devs in $(mount -p | grep $usbdev | awk '{print $1}'); do
+            umount -f $devs
+        done
+
+        lofiadm "${img}" && \
+            lofiadm -d "${img}"
+
+        if [[ -d "${usbmntpt}" ]]; then
+            rmdir "${usbmntpt}"
+        fi
+
+        rm -f $fdi
+    } > /dev/null 2>&1
+
+}
+
+function error_handler {
+
+	print -u2 "\nError:\n"
+	print -u2 -r -- "${progname}: $*"
+	cleanup
+
+	exit 1
+
+}
+
+function setup_grub2 {
+
+    # There are pre-built MBR stage 1 and UEFI system images on the USB image.
+    # These need to be used instead of trying to construct it ourselves in order
+    # to avoid grub2 version mismatches.
+    mbrimg="$usbmntpt/boot/mbr.img"
+    uefiimg="$usbmntpt/boot/uefi.img"
+
+    # cat the (MBR) usb.img and (ESP) uefi.img to the USB device.
+    cat $mbrimg $uefiimg > $dev || \
+        error_handler "Failed to write ${mbrimg} and ${uefiimg} to ${dev}"
+
+    # Read the geometry of the physical device
+    while read pcyl ncyl acyl bcyl nhead nsect secsiz; do
+        devblksize=$secsiz
+        devblocks=$(($ncyl * $nhead * $nsect))
+    done < <(fdisk -G $dev | grep -v "*")
+
+    # Size the embedded MBR image
+    mbrnbytes=$(ls -lL $mbrimg | awk '{print $5}')
+    mbrnblocks=$(($mbrnbytes / $devblksize))
+
+    # Size the uefi.img
+    uefinbytes=$(ls -lL $uefiimg | awk '{print $5}')
+    uefinblocks=$(($uefinbytes / $devblksize))
+
+    # fdisk table should end up looking like this.
+    # The first line/partition for the ESP is already embedded in the mbr image
+    # that we just cat'd to the disk. So we need to add the solaris partition.
+    # id   active bhead bsect bcyl ehead esect ecyl rsect         numsect
+    # 239  0      0     0     0    0     0     0    mbrnblocks(a) uefinblocks(b)
+    # 191  0      0     0     0    0     0     0    (a)+(b):(c)   diskblocks-(c)
+    solstart=$(($mbrnblocks + $uefinblocks))
+    solnblocks=$(($devblocks - $solstart))
+    addsolpart="191:0:0:0:0:0:0:0:$solstart:$solnblocks"
+    fdisk -A $addsolpart $dev
+
+    # fdisk is stupid and overwrites the MBR boot code region with mboot so we
+    # have to rewrite the first 440 bytes of the MBR image to restore what it
+    # so helpfully overwrote.
+    dd if=$mbrimg bs=1 count=440 oseek=0 of=$bdev 2>/dev/null|| \
+        error_handler "Failed to restore MBR stage1 image from ${usbimg}"
+
+}
+
+
+function setup_legacy_grub {
+
+    # Install grub stages to usb. The USB image is already lofi mounted so no need
+    # to mount the USB device.
+    echo Installing grub to USB device $s0cdev
+    installgrub -mf $usbmntpt/boot/grub/stage1 $usbmntpt/boot/grub/stage2 $s0cdev > /dev/null
+
+}
+
 if [ `/usr/bin/id -u` != "0" ] ; then
 	echo "Must be root to run."
 	exit 1
@@ -46,7 +133,7 @@
 	exit 1
 fi
 # Image size (in MB)
-ibytes=$(ls -l $img | awk '{print $5}')
+ibytes=$(ls -lL $img | awk '{print $5}')
 isz=$((ibytes >> 20))
 
 #nawk script to output the details of plugged in USB drives
@@ -99,6 +186,7 @@
 done
 
 dev=${log[$choice]}
+bdev=${dev/rdsk/dsk}
 s0cdev=${dev/p0/s0}
 s0bdev=${s0cdev/rdsk/dsk}
 mountdev=${s0bdev/s0}
@@ -159,24 +247,41 @@
 # Drop in config file to ensure HAL ignores the disk, otherwise it will attempt
 # to mount it after the partition table is rewritten
 fdi=/etc/hal/fdi/preprobe/10osvendor/99-usbcopy.fdi
-trap "rm -f $fdi" SIGTERM EXIT
+trap cleanup SIGTERM EXIT
 
 cat >$fdi << EOF
 <?xml version="1.0" encoding="UTF-8"?>
 <deviceinfo version="0.2">
   <device>
-    <match key="block.device" string="$s0bdev">
+    <match key="block.device" string="$bdev">
       <merge key="info.ignore" type="bool">true</merge>
     </match>
   </device>
 </deviceinfo>
 EOF
 
-# Install fdisk table with Solaris using entire disk, default VTOC
-fdisk -B $dev
+# lofi mount the USB image file into a temp dir
+usbmntpt="$(mktemp -d)"
+usbdev="$(lofiadm -a "${img}")" || \
+    error_handler "Failed to lofiadm ${img}"
+mount -F ufs -o ro $usbdev $usbmntpt || \
+    error_handler "Failed to mount ${usbdev} on ${usbmntpt}" 
+
+haslegacygrub=0
+if [ -f $usbmntpt/boot/grub/grub.cfg ] ; then
+    # GRUB2
+    setup_grub2
+elif [ -f $usbmntpt/boot/grub/menu.lst ] ; then
+    # Legacy GRUB
+    # Install fdisk table with Solaris using entire disk, default VTOC
+    haslegacygrub=1
+    fdisk -B $dev
+else
+    error_handler "Failed to find boot loader configuration"
+fi
 
 # Now create root partition.  We want to find number of cylinders in backup
-# partition from label created by fdisk -B and then generate root partition
+# partition from label created by fdisk and then generate root partition
 # using whole disk minus cylinder 1
 acyls=$(prtvtoc $dev | awk '/accessible/{print $2}')
 cyls=$((acyls - 1))
@@ -245,16 +350,12 @@
 echo "Finished $isz MB in $SECONDS seconds ($speed)"
 echo "$retries block(s) re-written due to verification failure"
 
-# Mount image
-mnt=/tmp/usb.$$
-trap "{ umount -f $mnt; rmdir $mnt; rm -f $fdi; }" SIGTERM EXIT
+if [ $haslegacygrub -eq 1 ] ; then
+    # Legacy GRUB. Install boot loader to USB physical device
+    setup_legacy_grub
+fi
 
-mkdir $mnt
-mount -o ro $s0bdev $mnt
-
-# Install grub stages to usb
-echo Installing grub to USB device $s0cdev
-installgrub -mf $mnt/boot/grub/stage1 $mnt/boot/grub/stage2 $s0cdev > /dev/null
+cleanup
 
 echo "Completed copy to USB"
 exit 0
--- a/usr/src/cmd/install-tools/usbgen	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/install-tools/usbgen	Fri Mar 23 04:40:57 2012 -0700
@@ -20,8 +20,7 @@
 #
 # CDDL HEADER END
 #
-#
-# Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 #
@@ -337,18 +336,21 @@
 set +o errexit
 
 #
-# Remove "Hard Disk" GRUB entries from the USB image as they apply only to ISO.
+# If the ISO is a legacy GRUB based image remove "Hard Disk" GRUB entries from 
+# from the USB image as they apply only to ISO. Not applicable to GRUB2 menu.cfg
 #
-nawk '
-	BEGIN { inhard=0 }
-	/^title.*Hard Disk$/ { inhard=1 }
-	/^title/ { if (index($0,"Hard Disk") == 0) inhard=0 }
-	inhard == 0 {print}
-' ${usb_path}/boot/grub/menu.lst > ${usb_path}/boot/grub/menu2.lst
-if [[ $? == 0 ]] ; then
-	mv ${usb_path}/boot/grub/menu2.lst ${usb_path}/boot/grub/menu.lst
-else
-	print -u2 "Warning: Could not remove \"Hard Disk\" entry from boot menu"
+if [[ -f ${usb_path}/boot/grub/menu.lst ]] ; then
+    nawk '
+	    BEGIN { inhard=0 }
+	    /^title.*Hard Disk$/ { inhard=1 }
+	    /^title/ { if (index($0,"Hard Disk") == 0) inhard=0 }
+	    inhard == 0 {print}
+    ' ${usb_path}/boot/grub/menu.lst > ${usb_path}/boot/grub/menu2.lst
+    if [[ $? == 0 ]] ; then
+	    mv ${usb_path}/boot/grub/menu2.lst ${usb_path}/boot/grub/menu.lst
+    else
+	    print -u2 "Warning: Could not remove \"Hard Disk\" entry from boot menu"
+    fi
 fi
 
 #
--- a/usr/src/cmd/installadm/client_control.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/client_control.py	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 '''
 AI create-client / delete-client
@@ -27,7 +27,6 @@
 import logging
 import os
 import re
-import shutil
 import sys
 
 import osol_install.auto_install.AI_database as AIdb
@@ -36,6 +35,7 @@
 import osol_install.auto_install.grub as grub
 import osol_install.auto_install.service_config as config
 
+from osol_install.auto_install.grub import AIGrubCfg as grubcfg
 from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from osol_install.auto_install.service import AIService
 from solaris_install import force_delete
@@ -54,6 +54,9 @@
                _pxegrub_path(client_id)[1],            # x86 pxegrub symlink
                os.path.join(com.BOOT_DIR, client_id)]  # SPARC symlink
 
+    for boot_type in grub.NBP_TYPE.values():
+        cleanup.append(os.path.join(com.BOOT_DIR, client_id + '.' + boot_type))
+
     # Now add in any files passed by the caller
     cleanup.extend(more_files)
 
@@ -81,57 +84,87 @@
     return client_id, os.path.join(com.BOOT_DIR, client_id)
 
 
+def _bootfile_path(client_id, archtype):
+    return os.path.join(com.BOOT_DIR, client_id + '.' + archtype)
+
+
 _PXE_CLIENT_DHCP_CONFIG = """
 No local DHCP configuration found. If not already configured, the
 following should be added to the DHCP configuration:
     Boot server IP      : %s
-    Boot file           : %s
+    Boot file(s)        : %s
 """
 
 
-def setup_x86_client(service, mac_address, bootargs=''):
+def setup_x86_client(service, mac_address, bootargs='',
+                     suppress_dhcp_msgs=False):
     ''' Set up an x86 client
 
-    Creates a relative symlink from the <svcname>'s bootfile to
-        /etc/netboot/<client_id>
-    Creates /etc/netboot/menu.lst.<client_id> boot configuration file
+    Creates relative symlink(s) in /etc/netboot::
+        <client_id>.<archtype> -> ./<svcname>/<bootfile_path>
+        e.g., 01223344223344.bios -> ./mysvc/boot/grub/pxegrub
+    Creates /etc/netboot/<cfg_file>.<client_id> boot configuration file
     Adds client info to AI_SERVICE_DIR_PATH/<svcname>/.config file
 
     Arguments:
-              image_path - directory path to AI image
+              service - the AIService to associate with client
               mac_address - client MAC address (as formed by
                             MACAddress class, i.e., 'ABABABABABAB')
               bootargs = bootargs of client (x86)
+              suppress_dhcp_msgs - if True, suppresses output of DHCP
+                                   configuration messages
     Returns: Nothing
 
     '''
     # create a client-identifier (01 + MAC ADDRESS)
     client_id = "01" + mac_address
-
-    menulst = os.path.join(service.config_dir, grub.MENULST)
-    client_menulst = _menulst_path(client_id)
+    clientinfo = dict()
 
-    # copy service's menu.lst file to menu.lst.<client_id>
-    try:
-        shutil.copy(menulst, client_menulst)
-    except IOError as err:
-        print >> sys.stderr, cw(_("Unable to copy grub menu.lst file: %s") %
-           err.strerror)
-        return
+    svcgrub = grubcfg(service.name, path=service.image.path,
+                      config_dir=service.config_dir)
 
-    # create a symlink from the boot directory to the sevice's bootfile.
-    # note this must be relative from the boot directory.
-    bootfile, pxegrub_path = _pxegrub_path(client_id)
-    os.symlink("./" + service.dhcp_bootfile, pxegrub_path)
+    # Call setup_client - it will return netconfig_files, config_files,
+    # and tuples, e.g.:
+    # netconfig_files:  ['/etc/netboot/menu.lst.01234234234234']
+    # config_files:  ['/etc/netboot/menu.conf.01234234234234']
+    # boot_tuples:  [('00:00', 'bios', 'mysvc/boot/grub/pxegrub2'),
+    #                ('00:07', 'uefi', 'mysvc/boot/grub/grub2netx64.efi')]
+    # If the client specifies bootargs, use them. Otherwise, inherit
+    # the bootargs specified in the service.
+    netconfig_files, config_files, boot_tuples = \
+        svcgrub.setup_client(mac_address, bootargs=bootargs,
+                             service_bootargs=service.bootargs)
 
-    clientinfo = {config.FILES: [client_menulst, pxegrub_path]}
-
-    # if the client specifies bootargs, use them. Otherwise, inherit
-    # the bootargs specified in the service (do nothing)
+    # update the bootargs in the service .config file
     if bootargs:
-        grub.update_bootargs(client_menulst, service.bootargs, bootargs)
         clientinfo[config.BOOTARGS] = bootargs
 
+    # keep track of client files to delete when client is removed
+    client_files = list(netconfig_files)
+    client_files.extend(config_files)
+
+    # create symlink(s) from the boot directory to the sevice's bootfile(s).
+    # note these must be relative from the boot directory.
+    for arch, archtype, relpath in boot_tuples:
+        # name of symlink is /etc/netboot/<clientid>.<archtype>
+        bootfile_symlink = _bootfile_path(client_id, archtype)
+        dotpath = './' + relpath
+        logging.debug('creating symlink %s->%s', bootfile_symlink, dotpath)
+        os.symlink(dotpath, bootfile_symlink)
+        client_files.append(bootfile_symlink)
+
+        # if this is archtype bios, create <clientid> symlink to
+        # <clientid>.bios for backward compatibility with existing dhcp
+        # servers.
+        if archtype == 'bios':
+            clientid_path = os.path.join(com.BOOT_DIR, client_id)
+            dot_client_arch = './' + client_id + '.' + archtype
+            logging.debug('creating symlink %s->%s', clientid_path,
+                            dot_client_arch)
+            os.symlink(dot_client_arch, clientid_path)
+
+    logging.debug('adding client_files to .config: %s', client_files)
+    clientinfo[config.FILES] = client_files
     config.add_client_info(service.name, client_id, clientinfo)
 
     # Configure DHCP for this client if the configuration is local, otherwise
@@ -143,9 +176,11 @@
         # rather than the non-delimited string that 'mac_address' is.
         full_mac = AIdb.formatValue('mac', mac_address)
         try:
-            print cw(_("Adding host entry for %s to local DHCP configuration.")
-                       % full_mac)
-            server.add_host(full_mac, bootfile)
+            if not suppress_dhcp_msgs:
+                print cw(_("Adding host entry for %s to local DHCP "
+                           "configuration.") % full_mac)
+            server.add_option_arch()
+            server.add_host(full_mac, boot_tuples)
         except dhcp.DHCPServerError as err:
             print cw(_("Unable to add host (%s) to DHCP configuration: %s") %
                       (full_mac, err))
@@ -158,11 +193,11 @@
                 print >> sys.stderr, cw(_("\nUnable to restart the DHCP SMF "
                                           "service: %s\n" % err))
                 return
-        else:
+        elif not suppress_dhcp_msgs:
             print cw(_("\nLocal DHCP configuration complete, but the DHCP "
                        "server SMF service is offline. To enable the "
                        "changes made, enable: %s.\nPlease see svcadm(1M) "
-                       "for further information.\n") % 
+                       "for further information.\n") %
                        dhcp.DHCP_SERVER_IPV4_SVC)
     else:
         # No local DHCP, tell the user all about their boot configuration
@@ -170,12 +205,19 @@
         if valid_nets:
             server_ip = valid_nets[0]
 
-        print _(_PXE_CLIENT_DHCP_CONFIG % (server_ip, bootfile))
+        if not suppress_dhcp_msgs:
+            boofile_text = '\n'
+            for archval, archtype, relpath in boot_tuples:
+                bootfilename = client_id + '.' + archtype
+                boofile_text = (boofile_text +
+                                '\t' + archtype + ' clients (arch ' +
+                                archval + '):  ' + bootfilename + '\n')
+            print _(_PXE_CLIENT_DHCP_CONFIG % (server_ip, boofile_text))
 
-        if len(valid_nets) > 1:
-            print cw(_("\nNote: determined more than one IP address "
-                       "configured for use with AI. Please ensure the above "
-                       "'Boot server IP' is correct.\n"))
+            if len(valid_nets) > 1:
+                print cw(_("\nNote: determined more than one IP address "
+                           "configured for use with AI. Please ensure the "
+                           "above 'Boot server IP' is correct.\n"))
 
 
 def setup_sparc_client(service, mac_address):
@@ -201,34 +243,39 @@
     config.add_client_info(service.name, client_id, clientinfo)
 
 
-def remove_client(client_id):
+def remove_client(client_id, suppress_dhcp_msgs=False):
     ''' Remove client configuration
 
         If client configuration incomplete (e.g., dangling symlink),
-        cleanup anyway.
+        cleanup anyway. Optionally suppress dhcp informational messages.
 
      '''
-    logging.debug("Removing client config for %s", client_id)
+    logging.debug("Removing client config for %s, suppress_dhcp_msgs=%s",
+                  client_id, suppress_dhcp_msgs)
 
     (service, datadict) = config.find_client(client_id)
-    more_files = list()
+    if datadict:
+        more_files = datadict.get(config.FILES, list())
+    else:
+        more_files = list()
     if service:
         # remove client info from .config file
         config.remove_client_from_config(service, client_id)
         if AIService(service).arch == 'i386':
             # suggest dhcp unconfiguration
-            remove_client_dhcp_config(client_id)
+            remove_client_dhcp_config(client_id, suppress_dhcp_msgs)
 
     # remove client specific symlinks/files
+    logging.debug("Cleaning up files %s", more_files)
     _cleanup_files(client_id, more_files)
 
 
-def remove_client_dhcp_config(client_id):
+def remove_client_dhcp_config(client_id, suppress_dhcp_msgs=False):
     '''
     If a local DHCP server is running, remove any client configuration for
     this client from its configuration. If not, inform end-user that the
     client-service binding should no longer be referenced in the DHCP
-    configuration.
+    configuration. Suppress dhcp informational messages if indicated.
     '''
     server = dhcp.DHCPServer()
     if server.is_configured():
@@ -237,8 +284,9 @@
         mac_address = client_id[2:]
         mac_address = AIdb.formatValue('mac', mac_address)
         if server.host_is_configured(mac_address):
-            print cw(_("Removing host entry '%s' from local DHCP "
-                       "configuration.") % mac_address)
+            if not suppress_dhcp_msgs:
+                print cw(_("Removing host entry '%s' from local DHCP "
+                           "configuration.") % mac_address)
             server.remove_host(mac_address)
 
             if server.is_online():
@@ -251,6 +299,7 @@
     else:
         # No local DHCP configuration, inform user that it needs to be
         # unconfigured elsewhere.
-        print cw(_("No local DHCP configuration found. Unless it will be "
-                   "reused, the bootfile '%s' may be removed from the DHCP "
-                   "configuration\n" % client_id))
+        if not suppress_dhcp_msgs:
+            print cw(_("No local DHCP configuration found. Unless it will be "
+                       "reused, the bootfile(s) associated with '%s' may be "
+                       "removed from the DHCP configuration.\n" % client_id))
--- a/usr/src/cmd/installadm/create_client.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/create_client.py	Fri Mar 23 04:40:57 2012 -0700
@@ -36,6 +36,7 @@
 
 from optparse import OptionParser, OptionValueError
 
+from bootmgmt import BootmgmtError
 from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from solaris_install import Popen
 
@@ -80,7 +81,7 @@
                       help=_("Service to associate client with"), nargs=1)
     (options, args) = parser.parse_args(cmd_options)
 
-    if args: 
+    if args:
         parser.error(_("Unexpected argument(s): %s" % args))
 
     # check that we got a service name and mac address
@@ -91,7 +92,7 @@
         parser.error(_("MAC address is required (-e|--macaddr <macaddr>)."))
 
     # Verify that the server settings are not obviously broken.
-    # These checks cannot be complete, but check for things which 
+    # These checks cannot be complete, but check for things which
     # will definitely cause failure.
     logging.debug("Calling %s", com.CHECK_SETUP_SCRIPT)
     ret = Popen([com.CHECK_SETUP_SCRIPT]).wait()
@@ -130,7 +131,8 @@
     return options
 
 
-def create_new_client(arch, service, mac_address, bootargs=None):
+def create_new_client(arch, service, mac_address, bootargs=None,
+                      suppress_dhcp_msgs=False):
     '''Create a new client of a service and ensure the Automated
        Install SMF service is enabled.
 
@@ -138,15 +140,18 @@
               service - The AIService to attach to
               mac_address - mac address of client
               bootargs - boot arguments to insert in client menu.lst file (x86)
+              suppress_dhcp_msgs - if True, suppresses informational messages
+                                   about DHCP configuration
        Returns: Nothing
 
     '''
     logging.debug("creating new client for service %s, mac %s, "
-                  "arch %s, bootargs %s",
-                  service.name, mac_address, arch, bootargs)
+                  "arch %s, bootargs %s, suppress_dhcp_msgs=%s",
+                  service.name, mac_address, arch, bootargs,
+                  suppress_dhcp_msgs)
     if arch == 'i386':
-        clientctrl.setup_x86_client(service, mac_address,
-                                     bootargs=bootargs)
+        clientctrl.setup_x86_client(service, mac_address, bootargs=bootargs,
+                                    suppress_dhcp_msgs=suppress_dhcp_msgs)
     else:
         clientctrl.setup_sparc_client(service, mac_address)
 
@@ -175,7 +180,8 @@
         bootargs = ",".join(options.boot_args).lstrip().rstrip() + ","
         logging.debug('bootargs=%s', bootargs)
 
-    clientctrl.remove_client("01" + options.mac_address)
+    clientctrl.remove_client("01" + options.mac_address,
+                             suppress_dhcp_msgs=True)
 
     # wrap the whole program's execution to catch exceptions as we should not
     # throw them anywhere
@@ -183,11 +189,10 @@
     try:
         create_new_client(options.arch, service,
                           options.mac_address, bootargs)
-    except OSError as err:
-        raise SystemExit(err)
-    except (aismf.ServicesError, config.ServiceCfgError,
-            svc.MountError) as err:
-        raise SystemExit(err)
+    except (OSError, BootmgmtError, aismf.ServicesError,
+            config.ServiceCfgError, svc.MountError) as err:
+        raise SystemExit(_('\nError: Unable to create client, %s:\n%s') %
+                         (options.mac_address, err))
 
 
 if __name__ == "__main__":
--- a/usr/src/cmd/installadm/create_service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/create_service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -42,6 +42,9 @@
 import pkg.client.history
 
 from optparse import OptionParser, OptionValueError
+
+from bootmgmt import BootmgmtError
+from osol_install.auto_install.grub import GrubError
 from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from osol_install.auto_install.service import AIService, AIServiceError, \
     DEFAULT_ARCH, MountError, UnsupportedAliasError
@@ -388,6 +391,10 @@
         service = AIService.create(options.svcname, image,
                                    alias=options.aliasof,
                                    bootargs=options.bootargs)
+    except (GrubError, BootmgmtError) as err:
+        AIService(options.svcname).delete()
+        raise SystemExit(_('\nError: Unable to create alias, %s:\n%s') %
+                         (options.svcname, err))
     except AIServiceError as err:
         raise SystemExit(err)
 
@@ -587,6 +594,10 @@
         else:
             service = AIService.create(options.svcname, image,
                                        bootargs=options.bootargs)
+    except (GrubError, BootmgmtError) as err:
+        AIService(options.svcname).delete()
+        raise SystemExit(_('\nError: Unable to create service, %s:\n%s') %
+                         (options.svcname, err))
     except AIServiceError as err:
         raise SystemExit(err)
 
@@ -612,6 +623,10 @@
             defaultarchsvc = AIService.create(defaultarch, image,
                                               bootargs=options.bootargs,
                                               alias=options.svcname)
+        except (GrubError, BootmgmtError) as err:
+            AIService(defaultarch).delete()
+            raise SystemExit(_('\nError: Unable to create alias, %s:\n%s') %
+                             (defaultarch, err))
         except AIServiceError as err:
             raise SystemExit(err)
         except UnsupportedAliasError as err:
--- a/usr/src/cmd/installadm/delete_service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/delete_service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -35,14 +35,14 @@
 
 from optparse import OptionParser
 
-from osol_install.auto_install.installadm_common import _, cli_wrap as cw 
+from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from osol_install.auto_install.service import AIService, DEFAULT_ARCH
-    
+
 
 def get_usage():
     ''' get usage for delete-service'''
     return(_('delete-service [-r|--autoremove] [-y|--noprompt] <svcname>]'))
- 
+
 
 def parse_options(cmd_options=None):
     '''
@@ -50,7 +50,7 @@
     Args: None
     Returns: A tuple of a dictionary of service properties of service
              to delete and an options object
-    
+
     '''
     usage = '\n' + get_usage()
     parser = OptionParser(usage=usage)
@@ -65,15 +65,15 @@
                       'service deletion'))
 
     (options, args) = parser.parse_args(cmd_options)
-    
+
     # Confirm install service's name was passed in
     if not args:
         parser.error(_("Missing required argument, <svcname>"))
     elif len(args) > 1:
         parser.error(_("Too many arguments: %s") % args)
-    
+
     service_name = args[0]
-    
+
     # validate service name
     try:
         com.validate_service_name(service_name)
@@ -82,11 +82,11 @@
     if not config.is_service(service_name):
         raise SystemExit(_("\nError: The specified service does "
                            "not exist: %s\n") % service_name)
-    
+
     # add service_name to the options
     options.service_name = service_name
     logging.debug("options = %s", options)
-    
+
     return options
 
 
@@ -98,9 +98,15 @@
     inform the end-user that the DHCP configuration should not reference this
     bootfile any longer.
     '''
+    logging.debug("in remove_dhcp_configuration, service=%s", service.name)
+
+    # Skip SPARC services, since they share a global bootfile
+    if service.arch == 'sparc':
+        return
+
     server = dhcp.DHCPServer()
     if server.is_configured():
-        # Server is configured. Regardless of it's current state, check for
+        # Server is configured. Regardless of its current state, check for
         # this bootfile in the service's architecture class. If it is set as
         # the default for this architecture, unset it.
         try:
@@ -110,16 +116,29 @@
                                       "%s\n" % err))
             return
 
-        # Skip SPARC services, since they share a global bootfile
-        if (service.arch != 'sparc' and arch_class is not None and 
-            arch_class.bootfile == service.dhcp_bootfile):
+        if arch_class is None or arch_class.bootfile is None:
+            # nothing to do
+            return
+
+        logging.debug("arch_class.bootfile is %s", arch_class.bootfile)
+        if isinstance(arch_class.bootfile, list):
+            # The list consists of tuples: (archval, relpath to bootfile)
+            # e.g., [('00:00', '<svcname>/boot/grub/pxegrub2'),..]
+            # Using the first tuple, get the service name.
+            relpath = arch_class.bootfile[0][1]
+        else:
+            relpath = arch_class.bootfile
+        parts = relpath.partition('/')
+        arch_svcname = parts[0]
+
+        if arch_svcname == service.name:
             try:
-                print cw(_("Removing this service's bootfile from local DHCP "
-                           "configuration\n"))
+                print cw(_("Removing this service's bootfile(s) from local "
+                           "DHCP configuration\n"))
                 arch_class.unset_bootfile()
             except dhcp.DHCPServerError as err:
                 print >> sys.stderr, cw(_("\nUnable to unset this service's "
-                                          "bootfile in the DHCP "
+                                          "bootfile(s) in the DHCP "
                                           "configuration: %s\n" % err))
                 return
 
@@ -142,10 +161,8 @@
     logging.debug("delete_specified_service %s %s %s", service_name,
                   auto_remove, noprompt)
 
-    # get service properties
-    svcprops = config.get_service_props(service_name)
     service = AIService(service_name)
-    
+
     # If the '-r' option has not been specified, look for all
     # dependent aliases and clients
     all_aliases = config.get_aliased_services(service_name, recurse=True)
@@ -194,12 +211,12 @@
         logging.debug("recursively calling delete_specified_service for %s",
                        dependent)
         delete_specified_service(dependent, True, True)
-    
+
     clients = config.get_clients(service_name).keys()
     for dependent in clients:
         logging.debug("calling remove_client for %s", dependent)
         clientctrl.remove_client(dependent)
-    
+
     logging.debug("now deleting service %s", service_name)
 
     # remove DHCP bootfile configuration for this service, if set
@@ -227,7 +244,7 @@
     if os.geteuid() != 0:
         raise SystemExit(_("Error: Root privileges are required for this "
                            "command.\n"))
-    
+
     # parse server options
     options = parse_options(cmd_options)
     delete_specified_service(options.service_name, options.autoremove,
--- a/usr/src/cmd/installadm/dhcp.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/dhcp.py	Fri Mar 23 04:40:57 2012 -0700
@@ -20,7 +20,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 '''
 Classes, methods and related routines used in the configuration and management
@@ -36,7 +36,8 @@
 
 import osol_install.auto_install.installadm_common as com
 
-from osol_install.auto_install.installadm_common import _, cli_wrap as cw
+from osol_install.auto_install.installadm_common import _, cli_wrap as cw, \
+    MACAddress
 from osol_install.libaimdns import getifaddrs
 from solaris_install import Popen, CalledProcessError
 
@@ -53,6 +54,8 @@
 
 DHCP_SERVER_IPV4_SVC = "svc:/network/dhcp/server:ipv4"
 
+OPTIONARCH = "option arch code 93 = unsigned integer 16;"
+
 # Mappings for name services (DNS, NIS) and related properties
 _ns_map = {'dns': {'SVC_NAME': "svc:/network/dns/client",
                    'DOMAIN_PROP': "config/domain",
@@ -98,6 +101,9 @@
 # network, the authoritative directive should be uncommented.
 authoritative;
 
+# arch option for PXEClient
+%(optionarch)s
+
 # Set logging facility (accompanies setting in syslog.conf)
 log-facility local7;
 """
@@ -155,11 +161,11 @@
 # The following defines a block quote for an x86 class stanza. Class stanzas
 # set details for an entire architecture.
 # Optionally passed in the format list:
-#   bootfile - The bootfile for this architecture class
+#   bootfile_clause - The bootfile if/else clause for this architecture class
 CFGFILE_PXE_CLASS_STANZA = """
 class "PXEBoot" {
   match if (substring(option vendor-class-identifier, 0, 9) = "PXEClient");
-  filename "%(bootfile)s";
+  %(bootfile_clause)s
 }
 """
 
@@ -176,7 +182,10 @@
 """
 
 # This string can be used when adding a new bootfile to an existing class.
-CFGFILE_CLASS_BOOTFILE_STRING = """  filename "%(bootfile)s";
+CFGFILE_CLASS_BOOTFILE_STRING_SPARC = """  filename "%(bootfile)s";
+"""
+
+CFGFILE_CLASS_BOOTFILE_STRING_X86 = """  %(bootfile)s
 """
 
 # The following defines a block quote for an host-specific stanza. These
@@ -192,6 +201,16 @@
 }
 """
 
+CFGFILE_HOST_STANZA_IFCLAUSE = """
+host %(hostname)s {
+  hardware ethernet %(macaddr)s;
+  %(bootfile_clause)s
+}
+"""
+
+# This string is used to add the arch option to the config file if needed.
+CFGFILE_OPTIONARCH_STRING = "%s\n\n" % OPTIONARCH
+
 
 class DHCPServerError(StandardError):
     '''
@@ -226,6 +245,7 @@
     '''
     def __init__(self):
         self.block = CFGFILE_BASE
+        self.optionarch = OPTIONARCH
 
 
 class _DHCPConfigSubnet(_DHCPConfigStanza):
@@ -261,6 +281,9 @@
     def __init__(self, bootfile):
         self.block = CFGFILE_PXE_CLASS_STANZA
         self.bootfile = bootfile
+        self.bootfile_clause = ''.join(_create_bootfile_clause(bootfile))
+        logging.debug('_DHCPConfigPXEClass bootfile clause is\n%s ',
+                      self.bootfile_clause)
 
 
 class _DHCPConfigSPARCClass(_DHCPConfigStanza):
@@ -282,12 +305,23 @@
         hostname - Label for this stanza
         macaddr - Ethernet (mac) address of the client
         bootfile - Bootfile for this client
+                   bootfile is either a string OR
+                   a list of tuples from NetBootConfig in the form
+                     [(arch_string, type_string, rel_path_to_bootfile), ...]
     '''
     def __init__(self, hostname, macaddr, bootfile):
-        self.block = CFGFILE_HOST_STANZA
         self.hostname = hostname
         self.macaddr = macaddr
         self.bootfile = bootfile
+        if isinstance(bootfile, list):
+            clientid = '01' + str(MACAddress(macaddr))
+            self.bootfile_clause = \
+                ''.join(_create_bootfile_clause(bootfile, clientid=clientid))
+            logging.debug('_DHCPConfigHost bootfile clause is\n%s ',
+                          self.bootfile_clause)
+            self.block = CFGFILE_HOST_STANZA_IFCLAUSE
+        else:
+            self.block = CFGFILE_HOST_STANZA
 
 
 class DHCPData(object):
@@ -342,7 +376,13 @@
         arch - The architecture class being queried
     Attributes:
         arch - The architecture of this architecture class entry
-        bootfile - The bootfile for this architecture class
+        bootfile - The bootfile for this architecture class:
+                   a string for sparc:
+                      e.g.: http://192.168.56.1:5555/cgi-bin/wanboot-cgi
+                   or a list of tuples for x86)
+                         [('00:00', 'default-i386/boot/grub/pxegrub2'),
+                          ('00:07', 'default-i386/boot/grub/grub2netx64.efi')]
+
     '''
     def __init__(self, server, arch, bootfile=None):
         self.server = server
@@ -373,19 +413,28 @@
         # we find that it's for the architecture in question, then we can break
         # out of parsing once we're done reading it all in. If not, move on to
         # the next class, if there are any.
+        if_opt_arch_pattern = 'if\s+option\s+arch\s*=\s*(\d{1,2}.\d{1,2})\s*{'
         found = False
         bootfile = None
+        in_ifclause = False
         classlines = list()
         for line in server._current_config():
             if line.startswith("class"):
                 # New class stanza, init a new class list
                 classlines = list()
                 continue
-            if line.endswith("}") and found:
-                # We're at the end of the class we wanted. The classlines list
-                # now contains each line of the class stanza for this
-                # architecture.
-                break
+            if line.endswith("}"):
+                if in_ifclause:
+                    # We're at the end of the if clause, clear the flag.
+                    in_ifclause = False
+                elif found:
+                    # We're at the end of the class we wanted. The classlines
+                    # list now contains each line of the class stanza for this
+                    # architecture.
+                    break
+            if line.startswith("if") and line.endswith("{"):
+                # We're entering an if clause inside the stanza
+                in_ifclause = True
             m = regexp.search(line)
             if m is not None:
                 # We've found the class stanza for this architecture.
@@ -398,11 +447,21 @@
             matches = [m.group(1)
                 for m in filter(bool, map(regexp.match, classlines))]
             if matches:
-                bootfile = matches[0]
+                if arch == 'i386' and len(matches) > 1:
+                    regexp = re.compile(if_opt_arch_pattern)
+                    arches = [m.group(1)
+                        for m in filter(bool, map(regexp.search, classlines))]
+                    if arches:
+                        bootfile = zip(arches, matches)
+                    else:
+                        bootfile = matches[0]
+                else:
+                    bootfile = matches[0]
 
         # Finally, if we found the class, instantiate and return a
         # DHCPArchClass object.
         if found:
+            logging.debug('dhcp:get_arch, found class, bootfile=%s', bootfile)
             return cls(server, arch, bootfile)
         else:
             return None
@@ -456,6 +515,9 @@
         Note: If action is 'update' and bootfile is "None", the bootfile will
         be unset entirely.
         '''
+        logging.debug('\nin _edit_class_bootfile, action=%s, bootfile=%s',
+                      action, bootfile)
+
         if action not in ['set', 'update']:
             raise ValueError(_("invalid action: %s") % action)
 
@@ -472,19 +534,26 @@
         # use "not x86", since we only support the two architectures, and
         # there is no simple way to determine a SPARC client explicitly.
         if self.arch == 'i386':
+            cfgfile_class_bf_string = CFGFILE_CLASS_BOOTFILE_STRING_X86
             vci_re = re.compile(X86_VCI_PATTERN)
+            if bootfile:
+                # Create a dictionary for use in the format string below.
+                # For x86, bf will be a string consisting of an if/else
+                # clause to be inserted into the dhcp conf file.
+                bf = {'bootfile': ''.join(_create_bootfile_clause(bootfile))}
+                logging.debug('_edit_class_bootfile: setting bf to %s' % bf)
         elif self.arch == 'sparc':
             vci_re = re.compile(SPARC_VCI_PATTERN)
+            cfgfile_class_bf_string = CFGFILE_CLASS_BOOTFILE_STRING_SPARC
             bootfile = fixup_sparc_bootfile(bootfile, True)
+            # Create a dictionary for use in the format string below
+            bf = {'bootfile': bootfile}
         else:
             raise DHCPServerError(_("unsupported architecture: %s") % \
                 self.arch)
 
         bootfile_re = re.compile('filename\s+\S+;')
 
-        # Create a dictionary for use in the format string below
-        bf = {'bootfile': bootfile}
-
         current_cfgfile = self.server._properties['config_file']
         tmp_cfgfile = "%s~" % current_cfgfile
 
@@ -502,6 +571,9 @@
         in_class = False
         seek_bootfile = False
         edit_complete = False
+        in_ifclause = False
+        removing_ifclause = False
+        ifclause = list()
         # dhcpd server runs under dhcpserv user account and needs to be able
         # to read its config file
         orig_umask = os.umask(0022)
@@ -521,19 +593,38 @@
                         # class stanza and can perform accordingly.
                         in_class = True
                     if line.strip().endswith("}"):
-                        # We are out of this particular class, so unset our
-                        # flag and move on. Note we could assert that
-                        # seek_bootfile shouldn't be set here (see below), but
-                        # it could be possible to have more than one class
-                        # stanza for this architecture. So, just unset that
-                        # flag as well.
-                        in_class = False
-                        seek_bootfile = False
+                        if in_ifclause:
+                            if removing_ifclause:
+                                if bootfile is not None:
+                                    # we've been removing the old bootfile
+                                    # ifclause and now want to insert the
+                                    # new bootfile lines
+                                    tmp_cfg.write(cfgfile_class_bf_string % bf)
+                                # We're removing this if clause
+                                edit_complete = True
+                                continue
+
+                            # We are out of this if clause. Append saved
+                            # if clause lines to file
+                            in_ifclause = False
+                            for ifline in ifclause:
+                                tmp_cfg.write(ifline)
+                        else:
+                            # We are out of this particular class, so unset our
+                            # flag and move on. Note we could assert that
+                            # seek_bootfile shouldn't be set here (see below),
+                            # but it could be possible to have more than one
+                            # class stanza for this architecture. So, just
+                            # unset that flag as well.
+                            in_class = False
+                            seek_bootfile = False
                     if seek_bootfile:
                         if bootfile_re.search(line) is not None:
                             # We're at the bootfile line in the class for this
                             # architecture, and we want to update it (i.e. the
-                            # action is 'update'). If the bootfile argument is
+                            # action is 'update'). If we're in an if clause,
+                            # then keep processing until we're out of the
+                            # clause. Otherwise, if the bootfile argument is
                             # set, then we're updating to a new bootfile
                             # setting. Create a new entry and drop it in,
                             # leaving the old one behind. If a new bootfile
@@ -541,11 +632,23 @@
                             # altogether - just leave this line behind.  Either
                             # way,  set our 'done' flag. We'll then just copy
                             # the rest of the file and return.
-                            if bootfile is not None:
-                                tmp_cfg.write(CFGFILE_CLASS_BOOTFILE_STRING % \
-                                    bf)
+                            if in_ifclause:
+                                # we are unsetting or replacing the bootfile.
+                                # If in if clause, remove the entire clause.
+                                ifclause.append(line)
+                                removing_ifclause = True
+                                continue
+                            elif bootfile is not None:
+                                tmp_cfg.write(cfgfile_class_bf_string % bf)
                             edit_complete = True
                             continue
+                        if (line.strip().startswith('if') and
+                            line.strip().endswith("{")):
+                            in_ifclause = True
+                            ifclause = list()
+                        if in_ifclause:
+                            ifclause.append(line)
+                            continue
                     if in_class:
                         if vci_re.search(line) is not None:
                             # We've found the class for this architecture
@@ -557,7 +660,7 @@
                                 # 'done' flag. We'll then just copy the rest of
                                 # the file and return.
                                 tmp_cfg.write(line)
-                                tmp_cfg.write(CFGFILE_CLASS_BOOTFILE_STRING % \
+                                tmp_cfg.write(cfgfile_class_bf_string % \
                                     bf)
                                 edit_complete = True
                                 continue
@@ -849,6 +952,42 @@
                                            broadcast, router, nextserver)
             self._add_stanza_to_config_file(new_stanza)
 
+    def add_option_arch(self):
+        '''
+        Add the arch option to the config file if not already there
+        '''
+        current_cfgfile = self._properties['config_file']
+        tmp_cfgfile = "%s~" % current_cfgfile
+
+        # Get the full configuration file, not just this server's current
+        # config, as we'll be making a copy of the current file for editing.
+        current_lines = list()
+        if os.path.exists(current_cfgfile):
+            with open(current_cfgfile, "r") as current:
+                current_lines = current.readlines()
+
+        # if arch is already defined, nothing to do
+        for line in current_lines:
+            if line.strip().startswith(OPTIONARCH):
+                return
+
+        # Need to add option arch. Make the copy and add it.
+        written = False
+        with open(tmp_cfgfile, "w") as tmp_cfg:
+            for line in current_lines:
+                if line.strip().startswith('class') and not written:
+                    tmp_cfg.write(CFGFILE_OPTIONARCH_STRING)
+                    written = True
+                tmp_cfg.write(line)
+
+        if not written:
+            # there were no class stanzas in the file. Append to the end.
+            with open(tmp_cfgfile, 'a') as cfg:
+                cfg.write(CFGFILE_OPTIONARCH_STRING)
+
+        # Rename the temporary new file to the configfile
+        os.rename(tmp_cfgfile, current_cfgfile)
+
     def _add_range_to_subnet(self, subnet, loaddr, hiaddr):
         '''
         Add a new IP range to an already established subnet stanza. This can
@@ -921,7 +1060,9 @@
         Add a host stanza to the DHCP configuration.
         Arguments:
             macaddr - Hardware ethernet address of the client
-            bootfile - Bootfile to set for this client
+            bootfile - Bootfile to set for this client OR
+                       list of tuples from grub:setup_client in the form
+                       [(arch_string, type_string, rel_path_to_bootfile), ...]
             hostname - Label for this stanza (optional)
         '''
         logging.debug("dhcp.add_host: adding host [%s] bootfile '%s'",
@@ -956,6 +1097,7 @@
         regexp = re.compile(macaddr)
 
         found = False
+        in_ifclause = False
         in_host = False
         remove_finished = False
         new_cfg = list()
@@ -985,25 +1127,33 @@
                     hostlines = list()
                     hostlines.append(line)
                     continue
+                if in_host and line.strip().startswith("if ") and \
+                    line.strip().endswith("{"):
+                    # We're entering an if clause inside the host stanza
+                    in_ifclause = True
                 if in_host and line.strip().endswith("}"):
-                    # We're at the end of a host stanza, clear our flag.
-                    in_host = False
-
-                    # Add this final line to the hostlines list.
-                    hostlines.append(line)
+                    if in_ifclause:
+                        # We're at the end of the if clause, clear the flag.
+                        in_ifclause = False
+                    else:
+                        # We're at the end of a host stanza, clear our flag.
+                        in_host = False
 
-                    # If we've not found our host yet, then this host stanza
-                    # we've just hit the end of needs to be written out to the
-                    # new file; add these lines to 'new_cfg'.
-                    if not found:
-                        new_cfg.extend(hostlines)
+                        # Add this final line to the hostlines list.
+                        hostlines.append(line)
+
+                        # If we've not found our host yet, then this host
+                        # stanza we've just hit the end of needs to be written
+                        # out to the new file; add these lines to 'new_cfg'.
+                        if not found:
+                            new_cfg.extend(hostlines)
+                            continue
+
+                        # If we have found the host we're removing, set our
+                        # finished flag and just move on, leaving this host
+                        # stanza behind.
+                        remove_finished = True
                         continue
-
-                    # If we have found the host we're removing, set our
-                    # finished flag and just move on, leaving this host
-                    # stanza behind.
-                    remove_finished = True
-                    continue
                 if in_host and regexp.search(line):
                     # We've found the right host stanza
                     found = True
@@ -1046,7 +1196,7 @@
 
     def add_arch_class(self, arch, bootfile):
         '''
-        Add an x86 architecture class to the DHCP configuration.
+        Add an x86 or SPARC architecture class to the DHCP configuration.
         Arguments:
             arch - The architecture to set this bootfile for.
             bootfile - The bootfile
@@ -1059,6 +1209,9 @@
                                     "set in the DHCP configuration") % arch)
 
         if arch == 'i386':
+            self.add_option_arch()
+            logging.debug('creating _DHCPConfigPXEClass, bootfile=%s',
+                          bootfile)
             new_stanza = _DHCPConfigPXEClass(bootfile)
         elif arch == 'sparc':
             bootfile = fixup_sparc_bootfile(bootfile, True)
@@ -1372,12 +1525,12 @@
                          logger='', stderr_loglevel=logging.DEBUG)
 
     regexp = re.compile('^default\s+(%s)\s+' % IP_PATTERN)
-    for route in [m.group(1)
-        for m in filter(bool, map(regexp.match, p.stdout.splitlines()))]:
-            if _ip_is_in_network(route, subnet_ip, _get_mask(route)):
-                logging.debug("dhcp._get_default_route_for_subnet: found  %s",
-                              route)
-                return route
+    for route in [m.group(1) for m in
+                  filter(bool, map(regexp.match, p.stdout.splitlines()))]:
+        if _ip_is_in_network(route, subnet_ip, _get_mask(route)):
+            logging.debug("dhcp._get_default_route_for_subnet: found  %s",
+                          route)
+            return route
 
     print >> sys.stderr, cw(_("\nUnable to determine a route for network %s. "
                               "Setting the route temporarily to %s; this "
@@ -1428,3 +1581,31 @@
 
     logging.debug("dhcp.fixup_sparc_bootfile: setting IP address %s", ipaddr)
     return re.sub('\$serverIP', ipaddr, bootfile)
+
+
+def _create_bootfile_clause(boot_tuples, clientid=None):
+    '''
+    Create the bootfile if/else clause for the host or PXEClient stanzas.
+    Input: boot_tuples: List of tuples returned by NetBootConfig
+               Example:
+                [('00:00', 'bios', 'default-i386/boot/grub/pxegrub2'),
+                 ('00:07', 'uefi', 'default-i386/boot/grub/grub2netx64.efi')]
+           clientid: clientid if creating clause for host section in dhcp
+                     configuration file
+    Returns: list of lines to be used to create if/else bootfile clause
+    '''
+    bf_clause = list()
+    for arch, archtype, relpath in boot_tuples:
+        line = 'if option arch = %s {\n' % arch
+        if bf_clause:
+            line = '  } else ' + line
+        bf_clause.append(line)
+        if clientid:
+            bootfile = clientid + '.' + archtype
+        else:
+            bootfile = relpath
+        bootline = '    filename "%s";\n' % bootfile
+        bf_clause.append(bootline)
+    if bf_clause:
+        bf_clause.append('  }')
+    return bf_clause
--- a/usr/src/cmd/installadm/grub.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/grub.py	Fri Mar 23 04:40:57 2012 -0700
@@ -24,170 +24,598 @@
 # Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 '''
-functions supporting creating and modifying menu.lst
+Classes and functions supporting creating and modifying bootmgmt
+configuration files
 '''
-import fileinput
 import logging
 import os
-import sys
+from grp import getgrnam
+from pwd import getpwnam
+
+import osol_install.auto_install.installadm_common as com
 
+from bootmgmt import BootmgmtUnsupportedPropertyError
+from bootmgmt.bootconfig import BootConfig, NetBootConfig, \
+    SolarisNetBootInstance
+from bootmgmt.bootloader import BootLoader
 from osol_install.auto_install.installadm_common import XDEBUG
 
 MENULST = 'menu.lst'
 
+# NBP_TYPE values come from RFC 4578
+NBP_TYPE = {'0000': 'bios', '0007': 'uefi'}
 
-def update_bootargs(menulstpath, oldbootargs, bootargs):
-    '''Update bootargs in menu.lst
+
+class GrubError(StandardError):
+    '''Base class for errors unique to grub module'''
+    pass
+
+
+def update_boot_instance_svcname(boot_instance, oldname, newname):
+    '''Update the svcname/mountdir in a boot_instance
 
      Input:
-        menulstpath - path to menu.lst file to update
-        oldbootargs - bootargs to replace
-        bootargs - replacement bootargs
+        boot_instance - boot_instance to update
+        oldname - current svcname
+        newname - replacement svcname
+     Returns: updated boot_instance
 
     '''
-    logging.log(XDEBUG, 'in update_bootargs menu.lst=%s oldbootargs=%s '
-                'bootargs=%s', menulstpath, oldbootargs, bootargs)
+    logging.log(XDEBUG, 'in update_boot_instance_svcname %s %s',
+                oldname, newname)
+    kernel = boot_instance.kernel
+    boot_instance.kernel = update_kernel_ba_svcname(kernel, oldname, newname)
 
-    bootargs = bootargs.strip()
-    for line in fileinput.input(menulstpath, inplace=1):
-        newline = line
-        parts = line.partition(' -B')
-        if parts[1]:
-            ending = parts[2].lstrip()
-            # Need to check for oldbootargs because '' is
-            # a valid value
-            if oldbootargs and oldbootargs in ending:
-                ending = ending.replace(oldbootargs, bootargs)
-            else:
-                ending = bootargs + ending
-            newline = parts[0] + ' -B ' + ending
-        sys.stdout.write(newline)
+    boot_archive = boot_instance.boot_archive
+    boot_instance.boot_archive = update_kernel_ba_svcname(boot_archive,
+        oldname, newname)
+
+    kargs = boot_instance.kargs
+    boot_instance.kargs = update_kargs_install_service(kargs, oldname, newname)
+    return boot_instance
 
 
-def update_svcname(menulstpath, newsvcname, mountdir):
-    ''' Update the svcname/mountdir in menu.lst
+def update_kargs_install_service(current_kargs, oldsvcname, newsvcname):
+    '''Update install_service in kargs string
 
      Input:
-        menulstpath - path to menu.lst file to update
+        current_kargs - current value of kargs string to update
+        oldsvcname - current svcname
         newsvcname - replacement svcname
-        mountdir - mountdir to replace with new svcname
+     Returns: updated kargs string
+
+    '''
+    logging.log(XDEBUG, 'in update_kargs_install_service current_kargs=%s '
+                'oldsvcname=%s newsvcname=%s', current_kargs,
+                oldsvcname, newsvcname)
+
+    new_kargs = current_kargs
+    install_svc = 'install_service='
+    parts = current_kargs.partition(install_svc)
+    if parts[1]:
+        ending = parts[2].partition(',')[2]
+        new_kargs = parts[0] + install_svc + newsvcname + ',' + ending
+    return(new_kargs)
+
+
+def update_kernel_ba_svcname(current_string, oldsvcname, newsvcname):
+    '''Update the svcname/mountdir in kernel or boot_archive string
+
+     Input:
+        current_string - current value of string to update
+        oldsvcname - current svcname
+        newsvcname - replacement svcname
+     Returns: updated string
 
     '''
-    logging.log(XDEBUG, 'in update_menulst %s %s %s',
-                menulstpath, newsvcname, mountdir)
-    install_svc = 'install_service='
-    kernel_str = '\tkernel$ /'
-    module_str = '\tmodule$ /'
-    for line in fileinput.input(menulstpath, inplace=1):
-        newline = line
-        parts = newline.partition(kernel_str)
-        if parts[1]:
-            ending = parts[2].partition('/')[2]
-            newline = kernel_str + mountdir + '/' + ending
-            parts = newline.partition(install_svc)
-            if parts[1]:
-                ending = parts[2].partition(',')[2]
-                newline = parts[0] + install_svc + newsvcname + ',' + \
-                          ending
-        else:
-            parts = newline.partition(module_str)
-            if parts[1]:
-                ending = parts[2].partition('/')[2]
-                newline = module_str + mountdir + '/' + ending
-        sys.stdout.write(newline)
+    logging.log(XDEBUG, 'in update_kernel_ba_svcname %s %s %s',
+                current_string, oldsvcname, newsvcname)
+    new_string = current_string
+    parts = current_string.partition(oldsvcname)
+    if parts[1]:
+        new_string = parts[0] + newsvcname + parts[2]
+    return new_string
 
 
-def update_imagepath(menulstpath, oldpath, newpath):
-    ''' Update the imagepath in menu.lst
+def update_kargs_bootargs(current_kargs, oldbootargs, newbootargs):
+    '''Update bootargs in kargs string
 
      Input:
-        menulstpath - path to menu.lst file to update
+        current_kargs - current value of kargs string to update
+        oldbootargs - bootargs to replace
+        newbootargs - replacement bootargs
+     Returns: updated kargs string
+
+    '''
+    logging.log(XDEBUG, 'in update_kargs_bootargs current_kargs=%s '
+                'oldbootargs=%s newbootargs=%s', current_kargs,
+                oldbootargs, newbootargs)
+
+    newbootargs = newbootargs.strip()
+    parts = current_kargs.partition('-B')
+    if not parts[1]:
+        return current_kargs
+    ending = parts[2].lstrip()
+    # Need to check for oldbootargs because '' is
+    # a valid value
+    if oldbootargs and oldbootargs in ending:
+        ending = ending.replace(oldbootargs, newbootargs)
+    else:
+        ending = newbootargs + ending
+    return(parts[0] + '-B ' + ending)
+
+
+def update_kargs_imagepath(current_kargs, oldpath, newpath):
+    '''Update imagepath in kargs string
+
+     Input:
+        current_kargs - current value of kargs string to update
         oldpath - imagepath to replace
         newpath - new imagepath
+     Returns: updated kargs string
 
     '''
-    logging.log(XDEBUG, 'in update_imagepath %s %s %s',
-                menulstpath, oldpath, newpath)
+    logging.log(XDEBUG, 'in update_kargs_imagepath current_kargs=%s '
+                'oldpath=%s newpath=%s', current_kargs, oldpath, newpath)
+
     install_media = 'install_media='
-    for line in fileinput.input(menulstpath, inplace=1):
-        newline = line
-        parts = newline.partition(install_media)
-        # Example line (spaces added for readability):
-        #   kernel$ /mysvc/platform/i86pc/kernel/$ISADIR/unix -B
-        #      install_media=http://$serverIP:5555//export/auto_install/myimg,
-        #      install_service=mysvc,install_svc_address=$serverIP:5555
-        if parts[1]:
-            # The line contains 'install_media='.
-            # parts[2] is:
-            #   http://$serverIP:5555//export/auto_install/myimg,
-            #      install_service=mysvc,install_svc_address=$serverIP:5555
-            ending_parts = parts[2].partition(',')
-            media_str = ending_parts[0]
-            #   http://$serverIP:5555//export/auto_install/myimg
+    parts = current_kargs.partition(install_media)
+
+    # Example line (spaces added for readability):
+    #   -B install_media=http://$serverIP:5555//export/auto_install/myimg,
+    #      install_service=mysvc,install_svc_address=$serverIP:5555
+
+    if not parts[1]:
+        return current_kargs
+
+    # The line contains 'install_media='.
+    # parts[2] is:
+    #   http://$serverIP:5555//export/auto_install/myimg,
+    #      install_service=mysvc,install_svc_address=$serverIP:5555
+    ending_parts = parts[2].partition(',')
+    media_str = ending_parts[0]
+    #   http://$serverIP:5555//export/auto_install/myimg
 
-            rest_of_string = ending_parts[1] + ending_parts[2]
-            #   ,install_service=mysvc,install_svc_address=$serverIP:5555
+    rest_of_string = ending_parts[1] + ending_parts[2]
+    #   ,install_service=mysvc,install_svc_address=$serverIP:5555
 
-            new_media_str = media_str.replace(oldpath, newpath, 1)
-            #   http://$serverIP:5555//foo/newpath
+    new_media_str = media_str.replace(oldpath, newpath, 1)
+    #   http://$serverIP:5555//foo/newpath
+
+    new_kargs = parts[0] + install_media + new_media_str + rest_of_string
+    #   kernel$ /mysvc/platform/i86pc/kernel/$ISADIR/unix -B
+    #      install_media=http://$serverIP:5555//foo/newpath,
+    #      install_service=mysvc,install_svc_address=$serverIP:5555
 
-            newline = parts[0] + install_media + new_media_str + rest_of_string
-            #   kernel$ /mysvc/platform/i86pc/kernel/$ISADIR/unix -B
-            #      install_media=http://$serverIP:5555//foo/newpath,
-            #      install_service=mysvc,install_svc_address=$serverIP:5555
-        sys.stdout.write(newline)
+    return new_kargs
+
+
+def set_perms(filename, uid, gid, mode):
+    '''Update permissions of a file'''
+    logging.debug("Setting ownership of %s to: %s:%s", filename, uid, gid)
+    os.chown(filename, getpwnam(uid).pw_uid, getgrnam(gid).gr_gid)
+    if mode:
+        logging.debug("Setting permissions of %s to: %d", filename, mode)
+        os.chmod(filename, mode)
 
 
-def setup_grub(svc_name, image_path, image_info, srv_address, menu_path,
-               bootargs):
-    '''Configure GRUB for this AIService instance.
-
-    Input:
-        svc_name - service name
-        image_path - image path for service
-        image_info - dict of image info
-        srv_address - address from service.py
-        menu_path - where to put menu.lst file
-        bootargs - string of user specified bootargs or '' if none
-
+class AIGrubCfg(object):
+    '''AIGrubCfg interacts with the bootmgmt classes to create and
+    modify the AI boot related service configuration files.
     '''
-    # Move the install media's grub menu out of the way so we do not boot it.
-    media_grub = os.path.join(image_path, "boot/grub", MENULST)
-    if os.path.exists(media_grub):
-        os.remove(media_grub)
+
+    def __init__(self, name, path=None, image_info=None, srv_address=None,
+                 config_dir=None, bootargs=None):
+        '''Constructor for class
+        Input:
+            name - service name
+            path - image path for service
+            image_info - dict of image info
+            srv_address - srv_address from AIService
+            config_dir - service config dir (/var/ai/service/<svcname>)
+            bootargs - string of user specified bootargs or '' if none
+
+        '''
+        # Used for string token substitution of pybootgmmt return data
+        self.boot_tokens = dict()
+        self.netbc = None
+        self.svcname = name
+        self.img_path = path
+        self.image_info = image_info
+        self.srv_address = srv_address
+        self.svc_config_dir = config_dir
+        self.bootargs = bootargs
+        self.tftp_subdir = '/' + name
+        self.mac_address = None
+
+    def setup_grub(self):
+        '''Setup grub files'''
+        self.init_netboot_config()
+        self.build_default_entries()
+        netconfig_files, config_files, boot_tuples = self.handle_boot_config()
+
+        logging.debug('setup_grub returning:\n  netconfig_files: %s\n  '
+                      'config_files: %s\n  boot_tuples: %s',
+                      netconfig_files, config_files, boot_tuples)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def init_netboot_config(self):
+        '''Instantiates the bootmgmt.bootConfig.NetBootConfig subclass
+           object for this class
+        '''
+        logging.debug('in init_boot_config')
+        self.netbc = NetBootConfig([BootConfig.BCF_CREATE],
+            platform=('x86', None),
+            osimage_root=self.img_path,
+            data_root=self.svc_config_dir,
+            tftproot_subdir=self.tftp_subdir)
+
+        # Set props to disable graphical boot splash, apply boot timeout
+        # property to boot loader, and set min_mem64.
+        propdict = {BootLoader.PROP_CONSOLE: BootLoader.PROP_CONSOLE_TEXT}
+        if "grub_min_mem64" in self.image_info:
+            minmem64 = self.image_info["grub_min_mem64"]
+            propdict[BootLoader.PROP_MINMEM64] = minmem64
+        for key in propdict:
+            try:
+                self.netbc.boot_loader.setprop(key, propdict[key])
+            except BootmgmtUnsupportedPropertyError:
+                logging.debug("Boot loader type '%s' does not support the "
+                              "'%s' property. Ignoring.",
+                              self.netbc.boot_loader.name, key)
+
+    def build_default_entries(self):
+        '''Construct the default boot entries and insert them into the
+           NetBootConfig object's boot_instances list
+        '''
+        logging.debug('in build_default_entries')
+
+        # Create net boot instance for Text Installer
+        ti_title = self.image_info.get("no_install_grub_title",
+                                       "Text Installer and command line")
+        ti_nbi = SolarisNetBootInstance(None, platform='x86', title=ti_title)
+
+        # Modify kernel and boot_archive lines if 32 bit
+        unix_32bit = 'platform/i86pc/kernel/unix'
+        if os.path.exists(os.path.join(self.img_path, unix_32bit)):
+            ti_nbi.kernel = '/platform/i86pc/kernel/%(karch)s/unix'
+            ti_nbi.boot_archive = '/platform/i86pc/%(karch)s/boot_archive'
+
+        # prepend kernel and boot_archive lines with /<svcname>
+        ti_nbi.kernel = '/' + self.svcname + ti_nbi.kernel
+        ti_nbi.boot_archive = '/' + self.svcname + ti_nbi.boot_archive
+        ti_nbi.kargs = ("-B %sinstall_media=http://%s/%s,install_service=%s,"
+                        "install_svc_address=%s" %
+                        (self.bootargs, self.srv_address, self.img_path,
+                        self.svcname, self.srv_address))
+        self.netbc.add_boot_instance(ti_nbi)
+
+        # Create net boot instance for AI
+        ai_title = self.image_info.get("grub_title",
+            "Solaris " + self.svcname) + ' Automated Install'
+        ai_nbi = SolarisNetBootInstance(None, platform='x86', title=ai_title)
+        ai_nbi.kernel = ti_nbi.kernel
+        ai_nbi.boot_archive = ti_nbi.boot_archive
+        ai_nbi.kargs = ("-B %sinstall=true,install_media=http://%s/%s,"
+                        "install_service=%s,install_svc_address=%s" %
+                        (self.bootargs, self.srv_address, self.img_path,
+                        self.svcname, self.srv_address))
+        self.netbc.add_boot_instance(ai_nbi)
 
-    # Create a new menu.lst for the AI client to use during boot
-    # menu.lst should be created with right permissions
-    orig_umask = os.umask(0022)
-    with open(os.path.join(menu_path, MENULST), "w") as menu:
-        # First setup the the global environment variables
-        menu.write("default=0\n")
-        menu.write("timeout=30\n")
+    def handle_boot_config(self):
+        '''Call commit_boot_config and process the resulting tuple list'''
+        logging.debug('in handle_boot_config')
+        tuple_list = self.netbc.commit_boot_config()
+        netconfig_files, config_files, boot_tuples = \
+            self._handle_boot_tuple_list(tuple_list)
+
+        # Remove the install image's cfgfile so we do not boot it.
+        for file in netconfig_files + config_files:
+            cfgfile = os.path.basename(file)
+            media_grub = os.path.join(self.img_path, "boot/grub", cfgfile)
+            if os.path.exists(media_grub):
+                logging.debug('removing %s', media_grub)
+                os.remove(media_grub)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def _handle_boot_tuple_list(self, boot_config_list):
+        '''Process returned tuple list from commit_boot_config()
+           and handle entries appropriately.
+           Returns tuple containing:
+               netconfig_files: List of boot loader configuration files
+                            e.g. /etc/netboot/<svcname>/[menu.lst|grub.cfg]
+               config_files: List of other bootmgmt configuration files
+               boot_tuples: list of tuples consisting of:
+               (<type>, <archval>, <relative path of bootfile in tftproot>)
+                e.g., ('00:00', 'bios', '<svcname>/boot/grub/pxegrub2')
+        '''
+        logging.debug('in _handle_boot_tuple_list tuples are %s',
+                      boot_config_list)
+        # Add dictionary mappings for tokens that commit_boot_config() might
+        # return.
+        self.boot_tokens[NetBootConfig.TOKEN_TFTP_ROOT] = com.BOOT_DIR
+        boot_tuples = list()
+        netconfig_files = list()
+        config_files = list()
+        for boot_config in boot_config_list:
+            # boot-config elements can be either tuples or lists of tuples.
+            # Cast tuples into a list for consistent iteration loop below.
+            if not isinstance(boot_config, list):
+                boot_config = [boot_config]
+            for config_set in boot_config:
+                ftype = config_set[0]
+                if ftype == BootConfig.OUTPUT_TYPE_NETCONFIG:
+                    dest_file = self._handle_file_type(config_set)
+                    netconfig_files.append(dest_file)
+                elif ftype == BootConfig.OUTPUT_TYPE_FILE:
+                    dest_file = self._handle_file_type(config_set)
+                    config_files.append(dest_file)
+                elif ftype in [BootConfig.OUTPUT_TYPE_BIOS_NBP,
+                               BootConfig.OUTPUT_TYPE_UEFI64_NBP]:
+                    boot_entry = self._handle_nbp_type(config_set)
+                    boot_tuples.append(boot_entry)
+                else:
+                    raise GrubError("Unexpected boot loader file "
+                                    "type: %s" % ftype)
+
+        logging.debug('_handle_boot_tuple_list returning netconfig_files: %s',
+                      netconfig_files)
+        logging.debug('_handle_boot_tuple_list returning config_files: %s',
+                      config_files)
+        logging.debug('_handle_boot_tuple_list returning boot_tuples: %s',
+                      boot_tuples)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def _handle_file_type(self, config):
+        '''Method that handles tuple entry of type BootConfig.OUTPUT_TYPE_FILE
+           Returns 'dest' from tuple, after updating '%(tftproot)s' token
+           with appropriate value.
+
+        '''
+        ftype, src, objref, dest, uid, gid, mode = config
+
+        if src is None:
+            raise GrubError("Error: No source path defined for boot file.")
+        if dest is None:
+            raise GrubError("Error: No destination path defined for boot "
+                            "file.")
+        # handle ownership/permissions
+        if uid is None:
+            uid = -1
+        if gid is None:
+            gid = -1
+        set_perms(src, uid, gid, mode)
+
+        # for a client, use the src as the dest, because the file is
+        # created directly in tftproot
+        if self.mac_address is not None:
+            real_dest = src
+        else:
+            real_dest = dest % self.boot_tokens
+        return (real_dest)
+
+    def _handle_nbp_type(self, config):
+        '''Method that handles tuple entry of type
+           BootConfig.OUTPUT_TYPE_BIOS_NBP or OUTPUT_TYPE_UEFI64_NBP
+           Returns tuple:
+               (dhcparch, archtype, relpath to nbp)
+               ('00:00', 'bios', mysvc/boot/grub/pxegrub2')
+        '''
+        ftype, src, objref, dest, uid, gid, mode = config
+
+        if src is None:
+            raise GrubError("Error: No source path defined for nbp file.")
 
-        if "grub_min_mem64" in image_info:
-            menu.write("min_mem64=%s\n\n" % image_info["grub_min_mem64"])
+        # ftype will be 'nbp-platform-0xnnnn', such as 'nbp-platform-0x0007'
+        # strip off the '0x'
+        archval = ftype.rpartition('-0x')[2]
+        archtype = archval
+        if archval in NBP_TYPE:
+            archtype = (NBP_TYPE[archval])
+        dhcparch = archval[0:2] + ':' + archval[2:4]
+
+        # create the relative path to the nbp (from /etc/netboot)
+        relpath = self.svcname + src.partition(self.img_path)[2]
+        return (dhcparch, archtype, relpath)
+
+    def _read_current_config(self, mac_address=None):
+        '''Read in current config and set self.netbc'''
+        logging.debug('_read_current_config: mac_address is %s', mac_address)
+
+        if mac_address is None:
+            # Read in service's config
+            self.netbc = NetBootConfig([],
+                platform=('x86', None),
+                osimage_root=self.img_path,
+                data_root=self.svc_config_dir,
+                tftproot_subdir=self.tftp_subdir)
+        else:
+            # Read in clients's config
+            self.netbc = NetBootConfig([],
+                platform=('x86', None),
+                osimage_root=self.img_path,
+                data_root=com.BOOT_DIR,
+                client_id=mac_address,
+                tftproot_subdir=self.tftp_subdir)
+            self.mac_address = mac_address
+
+    def update_imagepath(self, oldpath, newpath, mac_address=None):
+        '''Update the imagepath in the boot configfiles
+
+        Arguments:
+            oldpath - imagepath to replace
+            newpath - new imagepath
+            mac_address - client MAC address
+        '''
+        logging.debug('update_imagepath: oldpath=%s newpath=%s '
+                      'mac_address=%s', oldpath, newpath, mac_address)
+
+        self._read_current_config(mac_address=mac_address)
+
+        # Update imagepath in service's boot_instance kargs
+        for boot_instance in self.netbc.boot_instances:
+            kargs = boot_instance.kargs
+            boot_instance.kargs = update_kargs_imagepath(kargs, oldpath,
+                                                         newpath)
+        tuple_list = self.netbc.commit_boot_config()
+        netconfig_files, config_files, boot_tuples = \
+            self._handle_boot_tuple_list(tuple_list)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def update_svcname(self, oldname, newname, mac_address=None):
+        '''Update the service name in the boot configfiles
+
+        Arguments:
+            oldpath - imagepath to replace
+            newpath - new imagepath
+            mac_address - client MAC address
+        '''
+        logging.debug('in update_svcname oldname=%s newname=%s',
+                      oldname, newname)
+        self._read_current_config(mac_address=mac_address)
+
+        # Update net_tftproot_subdir in NetBootConfig instance
+        self.netbc.net_tftproot_subdir = '/' + newname
+
+        # Update svcname in service's boot_instance kargs
+        for boot_instance in self.netbc.boot_instances:
+            boot_instance = update_boot_instance_svcname(boot_instance,
+                oldname, newname)
+
+        tuple_list = self.netbc.commit_boot_config()
+        netconfig_files, config_files, boot_tuples = \
+            self._handle_boot_tuple_list(tuple_list)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def get_mountpt(self, mac_address=None):
+        '''Get the mountpt'''
+        logging.debug('in get_mountpt')
+        self._read_current_config(mac_address=mac_address)
+        kernel = self.netbc.boot_instances[0].kernel.lstrip('/')
+        parts = kernel.partition('/')
+        mountpt = ''
+        if parts[1]:
+            mountpt = parts[0]
+        logging.debug('get_mountpt returning %s', mountpt)
+        return mountpt
+
+    def setup_client(self, mac_address, bootargs='', service_bootargs=''):
+        '''Setup a client's config files, using service's config info
+           but allowing for client specific bootargs
 
-        # Add the 'no install' entry. It may not have a title in
-        # the image_info, so set a default.
-        menu.write("title %s\n" %
-                   image_info.get("no_install_grub_title",
-                                  "Text Installer and command line"))
-        menu.write("\tkernel$ /%s/platform/i86pc/kernel/$ISADIR/unix -B "
-                   "%sinstall_media=http://%s/%s,install_service=%s,"
-                   "install_svc_address=%s\n" %
-                   (svc_name, bootargs, srv_address, image_path, svc_name,
-                    srv_address))
-        menu.write("\tmodule$ /%s/platform/i86pc/$ISADIR/boot_archive\n\n" %
-                   svc_name)
+        Arguments:
+            mac_address - client MAC address
+                          (either ABABABABABAB or AB:AB:AB:AB:AB:AB)
+            bootargs = bootargs of client
+            service_bootargs = bootargs of service
+        Returns:
+            Returns tuple (netconfig_files, config_files, boot_tuples):
+               netconfig_files: List of boot loader configuration files
+                            e.g. /etc/netboot/<svcname>/[menu.lst|grub.cfg]
+               config_files: List of other bootmgmt configuration files
+               boot_tuples: list of tuples consisting of:
+                (<type>, <archval>, <relative path of bootfile>)
+                [('00:00', 'bios', 'myservice/boot/grub/pxegrub2'),
+                 ('00:07', 'uefi', 'seth0126rename/boot/grub/grub2netx64.efi')]
+
+        '''
+        logging.debug('setup_client: mac_address=%s, bootargs=%s, '
+                      'service_bootargs=%s', mac_address, bootargs,
+                      service_bootargs)
+        self.mac_address = mac_address
+
+        # Read in service's config
+        svc_netbc = NetBootConfig([],
+            platform=('x86', None),
+            osimage_root=self.img_path,
+            data_root=self.svc_config_dir,
+            tftproot_subdir=self.tftp_subdir)
+
+        # create client boot config
+        client_netbc = NetBootConfig([BootConfig.BCF_CREATE],
+            platform=('x86', None),
+            osimage_root=self.img_path,
+            data_root=com.BOOT_DIR,
+            client_id=mac_address,
+            tftproot_subdir=self.tftp_subdir)
+
+        # Copy service boot instances and add to client (update bootargs
+        # first, if they were specified by user)
+        svc_boot_instances = svc_netbc.boot_instances
+        for boot_instance in svc_boot_instances:
+            client_bi = boot_instance.copy()
+            if bootargs:
+                client_bi.kargs = update_kargs_bootargs(client_bi.kargs,
+                    service_bootargs, bootargs)
+            client_netbc.add_boot_instance(client_bi)
+
+        # Set client boot loader props the same as in the service
+        # E.g., BootLoader.<PROP_CONSOLE/PROP_TIMEOUT/PROP_MINMEM64>
+        svc_bl = svc_netbc.boot_loader
+        for prop in svc_bl.SUPPORTED_PROPS:
+            client_netbc.boot_loader.setprop(prop, svc_bl.getprop(prop))
 
-        # Finally, add the 'install' entry
-        menu.write("title %s Automated Install\n" %
-                   image_info.get("grub_title", "Solaris " + svc_name))
-        menu.write("\tkernel$ /%s/platform/i86pc/kernel/$ISADIR/unix -B "
-                   "%sinstall=true,install_media=http://%s/%s,"
-                   "install_service=%s,install_svc_address=%s\n" %
-                   (svc_name, bootargs, srv_address, image_path, svc_name,
-                    srv_address))
-        menu.write("\tmodule$ /%s/platform/i86pc/$ISADIR/boot_archive\n" %
-                   svc_name)
-    os.umask(orig_umask)
+        # Commit the configuration. This will result in the creation of
+        # the <cfgfile>.<clientid> file (e.g., menu.lst.01223344223344
+        # or grub.cfg.01223344223344) in tftproot.
+        tuple_list = client_netbc.commit_boot_config()
+        netconfig_files, config_files, boot_tuples = \
+            self._handle_boot_tuple_list(tuple_list)
+        return (netconfig_files, config_files, boot_tuples)
+
+    def update_basesvc(self, newbasesvc_netbc, newbasesvc_bootargs='',
+                       alias_bootargs=''):
+        '''Update an alias's config files for a new basesvc
+
+        Arguments:
+            newbasesvc_netbc - AIGrubCfg object of new basesvc
+            bootargs = bootargs of client
+            service_bootargs = bootargs of service
+        Returns tuple (netconfig_files, config_files, boot_tuples):
+               netconfig_files: List of boot loader configuration files
+                            e.g. /etc/netboot/<svcname>/[menu.lst|grub.cfg]
+               config_files: List of other bootmgmt configuration files
+               boot_tuples: list of tuples consisting of:
+                (<type>, <archval>, <relative path of bootfile>)
+
+        '''
+        logging.debug('update_basesvc: newbasesvc_bootargs=%s, '
+                      'alias_bootargs=%s' %
+                      (newbasesvc_bootargs, alias_bootargs))
+
+        # Read in new baseservice's config
+        newbase_netbc = NetBootConfig([],
+            platform=('x86', None),
+            osimage_root=newbasesvc_netbc.img_path,
+            data_root=newbasesvc_netbc.svc_config_dir,
+            tftproot_subdir=newbasesvc_netbc.tftp_subdir)
+
+        # Create new alias config
+        alias_netbc = NetBootConfig([BootConfig.BCF_CREATE],
+            platform=('x86', None),
+            osimage_root=newbasesvc_netbc.img_path,
+            data_root=self.svc_config_dir,
+            tftproot_subdir=self.tftp_subdir)
+
+        # Copy new baseservice boot instances and use for alias, but
+        # use name and bootargs of alias
+        newbase_boot_instances = newbase_netbc.boot_instances
+        for boot_inst in newbase_boot_instances:
+            alias_bi = boot_inst.copy()
+            alias_bi = update_boot_instance_svcname(alias_bi,
+                newbasesvc_netbc.svcname, self.svcname)
+            alias_bi.kargs = update_kargs_bootargs(alias_bi.kargs,
+                newbasesvc_bootargs, alias_bootargs)
+            alias_netbc.add_boot_instance(alias_bi)
+
+        # Set alias boot loader props the same as in the base service
+        # E.g., BootLoader.<PROP_CONSOLE/PROP_TIMEOUT/PROP_MINMEM64>
+        newbase_bl = newbase_netbc.boot_loader
+        for prop in newbase_bl.SUPPORTED_PROPS:
+            alias_netbc.boot_loader.setprop(prop, newbase_bl.getprop(prop))
+
+        # Commit the configuration.
+        tuple_list = alias_netbc.commit_boot_config()
+        netconfig_files, config_files, boot_tuples = \
+            self._handle_boot_tuple_list(tuple_list)
+        return (netconfig_files, config_files, boot_tuples)
--- a/usr/src/cmd/installadm/installadm-convert.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/installadm-convert.py	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 """
 This file contains the code necessary to convert an AI server configuration
@@ -41,13 +41,13 @@
 import osol_install.auto_install.AI_database as AIdb
 import osol_install.auto_install.ai_smf_service as aismf
 import osol_install.auto_install.dhcp as dhcp
-import osol_install.auto_install.grub as grub
 import osol_install.auto_install.installadm_common as com
 import osol_install.auto_install.service as svc
 import osol_install.auto_install.service_config as config
 import osol_install.libaiscf as smf
 
 from osol_install.auto_install import create_client
+from osol_install.auto_install.grub import AIGrubCfg as grubcfg
 from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from osol_install.auto_install.service import AIService
 from solaris_install import Popen, CalledProcessError
@@ -145,7 +145,7 @@
         # Set up a regular expression to pull the last tuple from an IP
         regexp = re.compile("^(\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,3})$")
 
-        # Break the IP address into two string values 
+        # Break the IP address into two string values
         m = regexp.match(cur_ipaddr)
         if m is None:
             raise SUNDHCPData.DHCPError("generate_ip_ranges: bad IP "
@@ -154,7 +154,7 @@
         cur_network_octet = m.group(1)
         cur_host_octet = m.group(2)
 
-        # Break the IP address into two string values; 
+        # Break the IP address into two string values;
         m = regexp.match(last_ipaddr)
         if m is None:
             raise SUNDHCPData.DHCPError("generate_ip_ranges: bad IP "
@@ -1251,7 +1251,7 @@
 
     # Check to see if installadm-convert has been run before by
     # testing for the existence of install.conf.bak and install.conf
-    # existing as a sym link.  If this is the case then remove the 
+    # existing as a sym link.  If this is the case then remove the
     # link and copy install.conf.bak to install.conf.  This will restore
     # the file to its original contents and allow the convert to run
     # successfully.
@@ -1379,7 +1379,10 @@
 
                 try:
                     print _("     Update menu.lst format")
-                    grub.update_svcname(new_grub, ai_service, ai_service)
+                    svcgrub = grubcfg(ai_service, path=ipath,
+                                      config_dir=new_service_path)
+                    old_mountpt = svcgrub.get_mountpt()
+                    svcgrub.update_svcname(old_mountpt, ai_service)
                 except IOError as err:
                     raise ServiceConversionError(str(err))
 
@@ -1512,7 +1515,7 @@
                     svc.MountError) as err:
                 sys.stderr.write("Client conversion failed:\n")
                 sys.stderr.write(str(err) + "\n")
-                
+
 
 def copy_netboot_files(dry_run):
     """
--- a/usr/src/cmd/installadm/rename_service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/rename_service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 """
 AI rename-service
 """
@@ -34,6 +34,7 @@
 
 from optparse import OptionParser
 
+from bootmgmt import BootmgmtError
 from osol_install.auto_install.image import ImageError
 from osol_install.auto_install.installadm_common import _, \
     validate_service_name, cli_wrap as cw
@@ -81,7 +82,7 @@
 
 
 def do_rename_service(cmd_options=None):
-    '''Rename a service.
+    '''Rename a service and update all associated aliases and clients.
 
     Note: Errors that occur during the various rename stages
     are printed, but the other stages will continue, with the hopes
@@ -137,9 +138,12 @@
     # Remove clients whose base service has been renamed
     clients = config.get_clients(svcname)
     for clientid in clients.keys():
-        clientctrl.remove_client(clientid)
+        clientctrl.remove_client(clientid, suppress_dhcp_msgs=True)
 
-    oldservice.rename(newsvcname)
+    try:
+        oldservice.rename(newsvcname)
+    except BootmgmtError as err:
+        raise SystemExit(err)
 
     # Update aliases whose base service has been renamed
     aliases = config.get_aliased_services(svcname)
@@ -166,13 +170,16 @@
         bootargs = None
         if config.BOOTARGS in clients[clientid]:
             bootargs = clients[clientid][config.BOOTARGS]
-        create_client.create_new_client(arch, newservice, client,
-                                        bootargs=bootargs)
-
+        try:
+            create_client.create_new_client(arch, newservice, client,
+                bootargs=bootargs, suppress_dhcp_msgs=True)
+        except BootmgmtError as err:
+            failures.append(err)
+            print >> sys.stderr, (_('\nError: Unable to recreate client, '
+                                    '%s:\n%s') % (client, err))
     if failures:
         return 1
-    else:
-        return 0
+    return 0
 
 
 if __name__ == '__main__':
--- a/usr/src/cmd/installadm/service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -41,7 +41,6 @@
 import fileinput
 import logging
 import os
-import shutil
 import socket
 import sys
 
@@ -54,6 +53,7 @@
 
 from osol_install.auto_install.data_files import DataFiles, insert_SQL, \
     place_manifest, validate_file
+from osol_install.auto_install.grub import AIGrubCfg as grubcfg
 from osol_install.auto_install.image import InstalladmImage, ImageError
 from osol_install.auto_install.installadm_common import _, cli_wrap as cw
 from solaris_install import Popen, CalledProcessError, force_delete
@@ -262,7 +262,7 @@
             bootserver: If DHCP configuration is provided, optionally explicit
                 address for the bootfile server
             alias: An AIService object to which this service will be aliased
-            bootargs (x86 only): Additional bootargs to add to menu.lst
+            bootargs (x86 only): Additional bootargs to add to boot cfgfile
 
         '''
         service = cls(name, _check_version=False)
@@ -306,9 +306,6 @@
 
         service._create_default_manifest()
 
-        # Configure DHCP for this service
-        service._configure_dhcp(ip_start, ip_count, bootserver)
-
         # Supported client images will interpolate the bootserver's IP
         # address (as retrieved from the DHCP server) in place of the
         # '$serverIP' string. We choose to utilize this functionality when
@@ -347,6 +344,9 @@
         else:
             service._init_grub(srv_address)
 
+        # Configure DHCP for this service
+        service._configure_dhcp(ip_start, ip_count, bootserver)
+
         return service
 
     def __init__(self, name, image=None, image_class=InstalladmImage,
@@ -371,6 +371,8 @@
         self._alias = None
         self._bootargs = None
         self._image_class = image_class
+        self._boot_tuples = None
+        self._netcfgfile = None
 
         # _check_version should ONLY be used by AIService.create during service
         # creation. Bypassing the version check when the service has already
@@ -454,10 +456,7 @@
         return AIdb.DB(self.database_path)
 
     def rename(self, new_name):
-        '''Change this service's name to "new_name" updating all relevant
-        files, aliases and clients.
-
-        '''
+        '''Change this service's name to new_name '''
         self._update_name_in_service_props(new_name)
         new_svcdir = os.path.join(AI_SERVICE_DIR_PATH, new_name)
         if self.arch == 'sparc':
@@ -478,11 +477,18 @@
             #   system.conf  -> /etc/netboot/<defaultsvc>/system.conf
             # by calling: self.do_default_sparc_symlinks(new_name)
         else:
-            # Update the menu.lst file with the new service name
+            # Update the grub config files with the new service name
             # Note that the new svcname is assumed to be the mountpoint
             # in /etc/netboot
-            menulstpath = os.path.join(self.config_dir, MENULST)
-            grub.update_svcname(menulstpath, new_name, new_name)
+            svcgrub = grubcfg(self.name, path=self.image.path,
+                              config_dir=self.config_dir)
+            netconfig_files, config_files, self._boot_tuples = \
+                svcgrub.update_svcname(self.name, new_name)
+
+            # update boot_cfgfile in .config file
+            self._netcfgfile = netconfig_files[0]
+            props = {config.PROP_BOOT_CFGFILE: self._netcfgfile}
+            config.set_service_props(self.name, props)
 
         self._migrate_service_dir(new_svcdir)
         self._setup_manifest_dir(new_name=new_name)
@@ -600,41 +606,63 @@
 
         '''
         if self.arch == 'i386':
-            return os.path.join(self.mountpoint, MENULST)
+            mtpt = self.boot_cfgfile
+            if not mtpt:
+                # Fall back to menu.lst
+                mtpt = os.path.join(self.mountpoint, MENULST)
+            return mtpt
         else:
             return os.path.join(self.mountpoint, SYSTEMCONF)
 
     def all_bootmountpts(self):
-        return [os.path.join(self.mountpoint, MENULST),
-                os.path.join(self.mountpoint, SYSTEMCONF)]
+        mountpts = [os.path.join(self.mountpoint, MENULST),
+                    os.path.join(self.mountpoint, SYSTEMCONF)]
+        mountpts.append(self.bootmountpt)
+        return mountpts
 
     @property
     def bootsource(self):
         '''Returns the path to the file that should be mounted at
-        self.bootmountpt
-
+        self.bootmountpt (e.g., /var/ai/service/<svcname>/<grub_cfg_file>)
         '''
         if self.arch == 'i386':
-            return os.path.join(self.config_dir, MENULST)
+            mtpt = self.bootmountpt
+            cfgfile = os.path.basename(mtpt)
+            return os.path.join(self.config_dir, cfgfile)
         else:
             return os.path.join(self.config_dir, SYSTEMCONF)
 
     @property
-    def dhcp_bootfile(self):
-        '''Returns a string that represents what should be added to DHCP
-        as this service's bootfile
+    def boot_tuples(self):
+        '''Boot tuples created by bootmgmt'''
+        return self._boot_tuples
+
+    @property
+    def boot_cfgfile(self):
+        '''Look up and return the name of where to mount config file
+           created by bootmgmt (e.g., /etc/netboot/mysvc/<grub_cfg_file>)
 
         '''
-        if self.arch == 'sparc':
-            http_port = libaimdns.getinteger_property(com.SRVINST,
-                                                      com.PORTPROP)
-            # Always use $serverIP keyword as setup-dhcp will
-            # substitute in the correct IP addresses.
-            dhcpbfile = 'http://%s:%u/%s' % ("$serverIP", http_port,
-                                             com.WANBOOTCGI)
-        else:
-            abs_dhcpbfile = os.path.join(self.mountpoint, self.X86_BOOTFILE)
-            dhcpbfile = os.path.relpath(abs_dhcpbfile, self.MOUNT_ON)
+        props = config.get_service_props(self.name)
+        try:
+            return props[config.PROP_BOOT_CFGFILE]
+        except (KeyError, ValueError, TypeError):
+            return None
+
+    @property
+    def dhcp_bootfile_sparc(self):
+        '''Returns a string that represents what should be added to DHCP
+        as this sparc service's bootfile
+
+        '''
+        if self.arch != 'sparc':
+            return None
+
+        http_port = libaimdns.getinteger_property(com.SRVINST, com.PORTPROP)
+        # Always use $serverIP keyword as setup-dhcp will substitute in
+        # the correct IP addresses.
+        dhcpbfile = 'http://%s:%u/%s' % ("$serverIP", http_port,
+                                         com.WANBOOTCGI)
         return dhcpbfile
 
     def _lofs_mount(self, from_path, to_mountpoint):
@@ -671,8 +699,8 @@
             /etc/netboot/<svcname>/system.conf is an lofs mount of
                 AI_SERVICE_DIR_PATH/<svcname>/system.conf
           For x86:
-            /etc/netboot/<svcname>/menu.lst is an lofs mount of
-                AI_SERVICE_DIR_PATH/<svcname>/menu.lst
+            /etc/netboot/<svcname>/<cfgfile> is an lofs mount of
+                AI_SERVICE_DIR_PATH/<svcname>/<grub_cfg_file>
 
         '''
         # Verify the image and ensure the mountpoints exist
@@ -687,8 +715,12 @@
 
         # Mount service's image to /etc/netboot/<svcname>
         self._lofs_mount(self.image.path, self.mountpoint)
-        # Do second lofi mount of menu.lst(x86) or system.conf(sparc)
-        self._lofs_mount(self.bootsource, self.bootmountpt)
+        # Do second lofi mount of grub config file for x86 or
+        # system.conf (for sparc)
+        bootsource = self.bootsource
+        bootmount = self.bootmountpt
+        logging.debug('lofs mount of %s, %s', bootsource, bootmount)
+        self._lofs_mount(bootsource, bootmount)
 
     def unmount(self, force=False, arch_safe=False):
         '''Unmount the service, reversing the effects of AIService.mount()
@@ -795,26 +827,10 @@
             All:
             o update aliasof property in .config to newbasesvc_name
             o disable alias
-               x86 only:
-                  o Copy over new base services's menu.lst to alias' menu.lst
-                  o reinstate alias service name in alias' menu.lst
-                  o Replace base service bootargs with alias' bootargs in
-                    alias' menu.lst
-                  o For each dependent alias of alias:
-                    o Copy alias' revised menu.lst to dependent alias' menu.lst
-                    o reinstate dependent alias name in dependent alias'
-                      menu.lst
-                    o If bootargs were specified for dependent alias,
-                      replace alias bootargs with dependent alias bootargs
-                  o For each client of alias:
-                    o Copy over alias' revised menu.lst to menu.lst.<clientid>
-                    o reinstate alias service_name in menu.lst.<clientid>
-                    o If client bootargs were specified during create-client,
-                      replace alias bootargs with client bootargs in
-                      menu.lst.<clientid>. Otherwise, alias bootargs are
-                      inherited.
+            o x86 only:  Recreate grub config for alias and dependent aliases
             o revalidate manifests and profiles
             o enable alias
+        Dependent clients are handled in set_service.
 
         Input:   newbasesvc_name - name of new base service
 
@@ -842,26 +858,23 @@
             self.disable(force=True)
 
         if self.arch == 'i386':
-            self_menulstpath = os.path.join(self.config_dir, MENULST)
-
             all_aliases = config.get_aliased_services(self.name, recurse=True)
-            all_clients = config.get_clients(self.name).keys()
-            for alias in all_aliases:
-                all_clients.extend(config.get_clients(alias).keys())
 
-            # Copy over the new base service's menu.lst file for the alias
-            # to use. Replace the base service's name with the alias' name
-            # as well as the base service's bootargs with the alias' bootargs
-            # in the alias' menu.lst file.
-            newbasesvc_menulst = os.path.join(newbasesvc.config_dir, MENULST)
-            logging.debug("update_basesvc: copying new menu.lst from %s to %s",
-                          newbasesvc_menulst, self_menulstpath)
-            shutil.copy(newbasesvc_menulst, self_menulstpath)
-            grub.update_svcname(self_menulstpath, self.name, self.name)
-            grub.update_bootargs(self_menulstpath, newbasesvc.bootargs,
-                                 self.bootargs)
+            # Update the grub config with the new base service info.
+            newbasegrub = grubcfg(newbasesvc.name, path=newbasesvc.image.path,
+                                  config_dir=newbasesvc.config_dir)
+            aliasgrub = grubcfg(self.name, path=newbasesvc.image.path,
+                                config_dir=self.config_dir)
+            netconfig_files, config_files, self._boot_tuples = \
+                aliasgrub.update_basesvc(newbasegrub, newbasesvc.bootargs,
+                                         self.bootargs)
 
-            # Recreate menu.lst files of all aliases of this alias
+            # update boot_cfgfile in .config file
+            self._netcfgfile = netconfig_files[0]
+            props = {config.PROP_BOOT_CFGFILE: self._netcfgfile}
+            config.set_service_props(self.name, props)
+
+            # Recreate grub config files of all aliases of this alias
             # (and descendants). Use the sub alias' bootargs instead
             # of the ones specified in the base service.
             for alias in all_aliases:
@@ -871,35 +884,13 @@
                     sub_alias_was_mounted = True
                     aliassvc.disable(force=True)
 
-                sub_alias_menulst = os.path.join(aliassvc.config_dir, MENULST)
-                logging.debug("update_basesvc: copying new menu.lst from %s "
-                              "to %s", self_menulstpath, sub_alias_menulst)
-                shutil.copy(self_menulstpath, sub_alias_menulst)
-                grub.update_svcname(sub_alias_menulst, aliassvc.name,
-                                    aliassvc.name)
-                grub.update_bootargs(sub_alias_menulst, self.bootargs,
-                                     aliassvc.bootargs)
+                aliasgrub = grubcfg(alias, path=aliassvc.image.path,
+                                    config_dir=aliassvc.config_dir)
+                aliasgrub.update_basesvc(newbasegrub, newbasesvc.bootargs,
+                                         aliassvc.bootargs)
                 if sub_alias_was_mounted:
                     aliassvc.enable()
 
-            # recreate menu.lst files for clients
-            for clientid in all_clients:
-                (service, datadict) = config.find_client(clientid)
-                client_bootargs = datadict.get(config.BOOTARGS, '')
-                client_menulst = get_client_menulst(clientid)
-
-                # copy alias' menu.lst file to menu.lst.<clientid>
-                logging.debug("update_basesvc: copying new menu.lst from %s "
-                              "to %s", self_menulstpath, client_menulst)
-                shutil.copy(self_menulstpath, client_menulst)
-                grub.update_svcname(client_menulst, self.name, self.name)
-
-                # if the client has bootargs, use them. Otherwise, inherit
-                # the bootargs specified in the alias (do nothing)
-                if client_bootargs:
-                    grub.update_bootargs(client_menulst, self.bootargs,
-                                         client_bootargs)
-
         # Turning point: Function calls prior to this line will reference
         # the 'old' base service's image. Function calls after this line
         # will reference the *new* base service's image. Keep that in mind
@@ -918,6 +909,9 @@
         if was_mounted:
             self.enable()
 
+        # Configure DHCP for this service
+        self._configure_dhcp(None, None, None)
+
     @property
     def manifest_dir(self):
         '''Full path to the directory where this service's
@@ -1081,16 +1075,22 @@
     def _configure_dhcp(self, ip_start, ip_count, bootserver):
         '''Add DHCP configuration elements for this service.'''
 
-        logging.debug('Calling setup_dhcp_server %s %s %s %s %s',
-            ip_start, ip_count, bootserver, self.dhcp_bootfile, self.arch)
+        logging.debug('Calling setup_dhcp_server %s %s %s',
+                       ip_start, ip_count, bootserver)
 
         setup_dhcp_server(self, ip_start, ip_count, bootserver)
 
     def _init_grub(self, srv_address):
-        '''Initialize grub (menu.lst) for this service'''
-        grub.setup_grub(self.name, self.image.path,
-                        self.image.read_image_info(), srv_address,
-                        self.config_dir, self._bootargs)
+        '''Initialize grub for this service'''
+        svcgrub = grubcfg(self.name, path=self.image.path,
+                          image_info=self.image.read_image_info(),
+                          srv_address=srv_address, config_dir=self.config_dir,
+                          bootargs=self._bootargs)
+
+        netconfig_files, config_files, self._boot_tuples = svcgrub.setup_grub()
+        self._netcfgfile = netconfig_files[0]
+        props = {config.PROP_BOOT_CFGFILE: self._netcfgfile}
+        config.set_service_props(self.name, props)
 
     def _init_wanboot(self, srv_address):
         '''Create system.conf file in AI_SERVICE_DIR_PATH/<svcname>'''
@@ -1291,8 +1291,8 @@
         '''Change the location of a service's image:
              o disable the service and all dependent aliases
              o update wanboot.conf for the service (sparc)
-             o update menu.lst files of service and its clients and
-               aliases (x86)
+             o update grub config files of service and its clients
+               and aliases (x86)
              o update service .config file
              o move the image and update webserver symlink
              o enable service and dependent aliases
@@ -1314,28 +1314,31 @@
                 enabled_aliases.append(alias)
                 aliassvc.disable(force=True)
 
-        # For x86, update menu.lst files (of service, clients, and aliases)
-        # with the new imagepath.
+        # For x86, update grub config files of service, clients, and
+        # aliases with the new imagepath.
         # For sparc, update wanboot.conf with the new imagepath.
         # There is nothing to do for sparc aliases or clients because they
         # reference the service's wanboot.conf file.
         if self.arch == 'i386':
-            self_menulstpath = os.path.join(self.config_dir, MENULST)
-            grub.update_imagepath(self_menulstpath, self.image.path, new_path)
+            svcgrub = grubcfg(self.name, path=self.image.path,
+                              config_dir=self.config_dir)
+
+            svcgrub.update_imagepath(self.image.path, new_path)
 
             all_clients = config.get_clients(self.name).keys()
             for alias in all_aliases:
                 all_clients.extend(config.get_clients(alias).keys())
 
             for clientid in all_clients:
-                client_menulstpath = get_client_menulst(clientid)
-                grub.update_imagepath(client_menulstpath, self.image.path,
-                                      new_path)
+                clientgrub = grubcfg(self.name, path=self.image.path,
+                                     config_dir=self.config_dir)
+                clientgrub.update_imagepath(self.image.path, new_path,
+                                            mac_address=clientid[2:])
             for alias in all_aliases:
                 aliassvc = AIService(alias)
-                alias_menulstpath = os.path.join(aliassvc.config_dir, MENULST)
-                grub.update_imagepath(alias_menulstpath, aliassvc.image.path,
-                                      new_path)
+                aliasgrub = grubcfg(alias, path=aliassvc.image.path,
+                                    config_dir=aliassvc.config_dir)
+                aliasgrub.update_imagepath(aliassvc.image.path, new_path)
         elif self.arch == 'sparc':
             self.update_wanboot_imagepath(self.image.path, new_path)
 
@@ -1392,11 +1395,6 @@
     return port
 
 
-def get_client_menulst(clientid):
-    '''get path to client menu.lst.<clientid> file'''
-    return (os.path.join(NETBOOT, MENULST + '.' + clientid))
-
-
 _DHCP_MSG = """No local DHCP configuration found. This service is the default
 alias for all %s clients. If not already in place, the following should
 be added to the DHCP configuration:"""
@@ -1409,11 +1407,9 @@
                            "DHCP configuration has not been completed. Please "
                            "see dhcpd(8) for further information.\n"))
 
-    bootfile = service.dhcp_bootfile
     arch = service.arch
-
-    logging.debug("setup_dhcp_server: ip_start=%s, ip_count=%s, "
-                  "bootfile=%s, arch=%s", ip_start, ip_count, bootfile, arch)
+    logging.debug("setup_dhcp_server: ip_start=%s, ip_count=%s, arch=%s",
+                  ip_start, ip_count, arch)
 
     server = dhcp.DHCPServer()
     svc_cmd = False
@@ -1448,35 +1444,54 @@
     # creating a new default alias for this architecture, set this service's
     # bootfile as the default in the DHCP local server.
     if arch == 'sparc':
+        bootfile = service.dhcp_bootfile_sparc
         bfile = dhcp.fixup_sparc_bootfile(bootfile)
         client_type = 'SPARC'
     else:
-        bfile = bootfile
+        # create new tuple list containing only the arch val and relpath
+        # i.e., go from entries of (dhcparch, archtype, relpath to nbp) to
+        # (dhcparch, relpath to nbp)
+        bootfile = service.boot_tuples
+        bootfile_tuples = zip(*service.boot_tuples)
+        bootfile_tuples = zip(bootfile_tuples[0], bootfile_tuples[2])
+        bfile = ''
+        for archval, archtype, relpath in service.boot_tuples:
+            line = ('    ' + archtype + ' clients (arch ' + archval + '):  ' +
+                     relpath + '\n')
+            bfile = bfile + line
         client_type = 'PXE'
 
+    logging.debug("setup_dhcp_server: bootfile=%s, bfile=\n%s",
+                  bootfile, bfile)
+
     if server.is_configured():
         if service.is_default_arch_service():
             try:
+                if arch == 'i386':
+                    # add the arch option if needed
+                    server.add_option_arch()
                 if server.arch_class_is_set(arch):
                     # There is a class for this architecture already in place
                     cur_bootfile = server.get_bootfile_for_arch(arch)
+                    logging.debug('cur_bootfile is %s', cur_bootfile)
                     if cur_bootfile is None:
                         # No bootfile set for this arch
-                        print cw(_("Setting the default %s bootfile in the "
-                                   "local DHCP configuration to '%s'\n") %
-                                   (client_type, bfile))
+                        print cw(_("Setting the default %s bootfile(s) in "
+                                   "the local DHCP configuration to:"
+                                   "\n%s\n") % (client_type, bfile))
                         server.add_bootfile_to_arch(arch, bootfile)
-                    elif cur_bootfile != bfile:
+                    elif ((arch == 'sparc' and cur_bootfile != bfile) or
+                          (arch == 'i386' and sorted(bootfile_tuples) !=
+                           sorted(cur_bootfile))):
                         # Update the existing bootfile to our default
-                        print cw(_("Updating the default %s bootfile in the "
-                                   "local DHCP configuration from '%s' to "
-                                   "'%s'\n") %
-                                   (client_type, cur_bootfile, bfile))
+                        print cw(_("Updating the default %s bootfile(s) in "
+                                   "the local DHCP configuration to:\n%s\n") %
+                                   (client_type, bfile))
                         server.update_bootfile_for_arch(arch, bootfile)
                 else:
                     # Set up a whole new architecture class
-                    print cw(_("Setting the default %s bootfile in the local "
-                               "DHCP configuration to '%s'\n") %
+                    print cw(_("Setting the default %s bootfile(s) in the "
+                               "local DHCP configuration to:\n%s\n") %
                                (client_type, bfile))
                     server.add_arch_class(arch, bootfile)
 
@@ -1515,9 +1530,10 @@
 
         if arch == 'i386':
             # Boot server IP is not needed for SPARC clients
-            print _("\t%-20s : %s" % ("Boot server IP", server_ip))
-
-        print _("\t%-20s : %s\n" % ("Boot file", bfile))
+            print _("%s: %s" % ("Boot server IP", server_ip))
+            print _("%s:\n%s" % ("Boot file(s)", bfile))
+        else:
+            print _("%s: %s\n" % ("Boot file", bfile))
 
         if len(valid_nets) > 1 and arch == 'i386':
             print cw(_("\nNote: determined more than one IP address "
--- a/usr/src/cmd/installadm/service_config.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/service_config.py	Fri Mar 23 04:40:57 2012 -0700
@@ -44,6 +44,7 @@
 PROP_BOOT_ARGS = 'boot_args'
 PROP_BOOT_FILE = 'boot_file'
 PROP_BOOT_MENU = 'boot_menu'
+PROP_BOOT_CFGFILE = 'boot_cfgfile'
 PROP_DEFAULT_MANIFEST = 'default-manifest'
 PROP_GLOBAL_MENU = 'global_menu'
 PROP_IMAGE_PATH = 'image_path'
@@ -169,19 +170,19 @@
 
     '''
     logging.log(com.XDEBUG, '**** START service_config.get_service_props ****')
-    
+
     cfgp = _read_config_file(service_name)
     if cfgp is None:
         return None
-    
+
     props = dict()
     if cfgp.has_section(SERVICE):
         section_dict = dict(cfgp.items(SERVICE))
         props.update(section_dict)
-    
+
     for prop in props:
         logging.log(com.XDEBUG, '   property: %s=%s', prop, props[prop])
-    
+
     return props
 
 
@@ -482,18 +483,18 @@
     Raises:
         ServiceCfgError if service doesn't exist or if there is a
         problem with the txt_record.
-    
+
     '''
     # get the port from the service's txt_record
     try:
         txt_rec = get_service_props(svcname)[PROP_TXT_RECORD]
     except KeyError as err:
         raise ServiceCfgError(err)
-    
+
     # txt_record is of the form "aiwebserver=example:46503" so split
     # on ":" and take the trailing portion for the port number
     port = txt_rec.rsplit(':')[-1]
-    
+
     return port
 
 
@@ -623,7 +624,7 @@
 
     Input:
         smf_port - webserver port from smf
-    Return: 
+    Return:
         0 - New COMPATIBILITY_PORTS file has been put into place.
         1 - Existing COMPATIBILITY_PORTS file was not touched.
     '''
@@ -649,7 +650,7 @@
         # Work file, as a tempfile.TemporaryFile object,
         # is automatically deleted upon close
         work_file.close()
-    
+
     if os.path.exists(COMPATIBILITY_PORTS):
         with open(COMPATIBILITY_PORTS, 'r') as compat:
             compat_data = compat.read()
@@ -688,14 +689,14 @@
         for ip in valid_networks:
             listen = 'Listen %s:%s\n' % (ip, smf_port)
             work_file.write(listen)
-        
+
         work_file.seek(0)
         work_data = work_file.read()
     finally:
         # Work file, as a tempfile.TemporaryFile object,
         # is automatically deleted upon close
         work_file.close()
-    
+
     if os.path.exists(LISTEN_ADDRESSES):
         with open(LISTEN_ADDRESSES, 'r') as listen:
             listen_data = listen.read()
@@ -705,7 +706,7 @@
     if work_data == listen_data:
         # use existing configuration file
         return 1
-    
+
     # Truncate existing file, write new data
     with open(LISTEN_ADDRESSES, "w") as listen_file:
         listen_file.write(work_data)
@@ -801,7 +802,7 @@
     # Add version if new file
     if newfile and cfg.has_section(SERVICE):
         cfg.set(SERVICE, PROP_VERSION, CURRENT_VERSION)
-    
+
     if logging.root.isEnabledFor(com.XDEBUG):
         for section in cfg.sections():
             logging.log(com.XDEBUG, '%s %s items are: %s', service_name,
--- a/usr/src/cmd/installadm/set_service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/set_service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 """
 AI set-service
 """
@@ -29,11 +29,14 @@
 import sys
 
 import osol_install.auto_install.ai_smf_service as aismf
+import osol_install.auto_install.create_client as create_client
+import osol_install.auto_install.client_control as clientctrl
 import osol_install.auto_install.service as svc
 import osol_install.auto_install.service_config as config
 
 from optparse import OptionParser
 
+from bootmgmt import BootmgmtError
 from osol_install.auto_install.installadm_common import _, \
     validate_service_name, cli_wrap as cw
 
@@ -166,19 +169,52 @@
         raise SystemExit(cw(_("\nError: %s can not be made an alias of %s "
                               "because %s is dependent on %s\n") % (aliasname,
                               basesvcname, basesvcname, aliasname)))
+
+    # Remove clients of alias
+    clients = config.get_clients(aliasname)
+    for clientid in clients.keys():
+        clientctrl.remove_client(clientid, suppress_dhcp_msgs=True)
+
+    failures = list()
     try:
         aliassvc.update_basesvc(basesvcname)
-    except (OSError, config.ServiceCfgError) as err:
-        raise SystemExit(_("Failed to set 'aliasof' property of : %s") %
-                         aliasname)
+    except (OSError, config.ServiceCfgError, BootmgmtError) as err:
+        print >> sys.stderr, (_("Failed to set 'aliasof' property of : %s") %
+                                aliasname)
+        print >> sys.stderr, err
+        failures.append(err)
     except svc.MultipleUnmountError as err:
         print >> sys.stderr, _("Failed to disable alias")
-        raise SystemExit(err)
+        print >> sys.stderr, err
+        failures.append(err)
     except svc.MountError as err:
         print >> sys.stderr, _("Failed to enable alias")
-        raise SystemExit(err)
+        print >> sys.stderr, err
+        failures.append(err)
     except svc.UnsupportedAliasError as err:
-        raise SystemExit(err)
+        print >> sys.stderr, err
+        failures.append(err)
+
+    # Re-add clients to updated alias
+    arch = aliassvc_arch
+    for clientid in clients.keys():
+        # strip off leading '01'
+        client = clientid[2:]
+        bootargs = None
+        if config.BOOTARGS in clients[clientid]:
+            bootargs = clients[clientid][config.BOOTARGS]
+        # Don't suppress messages, because user may need to update
+        # DHCP configuration
+        try:
+            create_client.create_new_client(arch, aliassvc, client,
+                bootargs=bootargs, suppress_dhcp_msgs=False)
+        except BootmgmtError as err:
+            failures.append(err)
+            print >> sys.stderr, (_('\nError: Unable to recreate client, '
+                                    '%s:\n%s') % (client, err))
+    if failures:
+        return 1
+    return 0
 
 
 def set_imagepath(options):
@@ -207,7 +243,7 @@
     new_imagepath = new_imagepath.rstrip('/')
     try:
         service.relocate_imagedir(new_imagepath)
-    except (svc.MountError, aismf.ServicesError) as error:
+    except (svc.MountError, aismf.ServicesError, BootmgmtError) as error:
         raise SystemExit(error)
 
 
--- a/usr/src/cmd/installadm/test/test_create_service.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/test/test_create_service.py	Fri Mar 23 04:40:57 2012 -0700
@@ -29,14 +29,10 @@
 
 '''
 
-import os
+import unittest
+
 import osol_install.auto_install.create_service as create_service
-import osol_install.auto_install.dhcp as dhcp
-import osol_install.auto_install.grub as grub
 import osol_install.auto_install.installadm_common as com
-import shutil
-import tempfile
-import unittest
 
 
 class ParseOptions(unittest.TestCase):
@@ -124,42 +120,5 @@
                           "pkg:/install-image/solaris-auto-install")
 
 
-class CreateTestService(unittest.TestCase):
-    '''Tests for install service set up.'''
-
-    def setUp(self):
-        '''unit test set up'''
-        self.svc_name = 'my-test-service'
-        self.image_path = tempfile.mkdtemp(dir="/tmp")
-        self.image_info = {'image_version': '3.0', 'grub_min_mem64': '0',
-                           'service_name': 'solaris11u1-i386-05',
-                           'grub_do_safe_default': 'true',
-                           'grub_title': 'Oracle Solaris 11',
-                           'image_size': '744619',
-                           'no_install_grub_title': 'Oracle Solaris 11'}
-        self.srv_address = '$serverIP:5555'
-        self.menu_path = self.image_path
-        self.bootargs = ''
-
-    def tearDown(self):
-        '''teardown'''
-        if os.path.exists(self.image_path):
-            shutil.rmtree(self.image_path)
-
-    def test_menu_permissions(self):
-        '''Ensure that menu.lst is created with correct permissions'''
-
-        # save original umask
-        orig_umask = os.umask(0022)
-        # set too restrictive and too open umask
-        for mask in (0066, 0000):
-            os.umask(mask)
-            grub.setup_grub(self.svc_name, self.image_path, self.image_info,
-                self.srv_address, self.menu_path, self.bootargs)
-            mode = os.stat(self.menu_path + '/' + 'menu.lst').st_mode
-            self.assertEqual(mode, 0100644)
-        # return umask to the original value
-        os.umask(orig_umask)
-
 if __name__ == '__main__':
     unittest.main()
--- a/usr/src/cmd/installadm/test/test_dhcp.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/test/test_dhcp.py	Fri Mar 23 04:40:57 2012 -0700
@@ -97,13 +97,15 @@
         '''Test correct permissions of dhcpd4.conf'''
 
         dhcpsrv = dhcp.DHCPServer()
-        dhcpsrv.add_arch_class('i386', 'some-test-string')
+        dhcpsrv.add_arch_class('i386',
+            [('00:00', 'bios', 'some-test-string')])
         # save original umask
         orig_umask = os.umask(0022)
         # set too restrictive and too open umask
         for mask in (0066, 0000):
             os.umask(mask)
-            dhcpsrv.update_bootfile_for_arch('i386', 'some-other-test-string')
+            dhcpsrv.update_bootfile_for_arch('i386',
+                [('00:07', 'uefi', 'some-other-test-string')])
             mode = os.stat(self.dhcp_dir + '/' + 'dhcpd4.conf').st_mode
             self.assertEqual(mode, 0100644)
         # return umask to the original value
--- a/usr/src/cmd/installadm/test/test_grub.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/cmd/installadm/test/test_grub.py	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 '''
@@ -28,88 +28,127 @@
 must be rebuilt for these tests to pick up any changes in the tested code.
 '''
 
-import unittest
+
+import grp
 import os
+import pwd
 import tempfile
+import unittest
 import osol_install.auto_install.grub as grub
 
 
+class BootInstance(object):
+    '''Class for fake BootInstance (instead of SolarisNetBootInstance)'''
+    def __init__(self, name='mysvc', path='/export/myimage', bootargs=''):
+        self.svcname = name
+        self.path = path
+        self.bootargs = bootargs
+        self.kargs = ("-B %(args)s,install=true,"
+            "install_media=http://$serverIP:5555/%(path)s,"
+            "install_service=%(name)s,install_svc_address=$serverIP:5555\n" %
+             {'args': self.bootargs, 'path': self.path, 'name': self.svcname})
+
+        self.kernel = ("\tkernel$ /%(name)s/platform/i86pc/kernel/$ISADIR/unix"
+                        % {'name': self.svcname})
+
+        self.boot_archive = ("\tmodule$ /%(name)s/platform/i86pc/$ISADIR/"
+                             "boot_archive\n" % {'name': self.svcname})
+
+
 class TestGrub(unittest.TestCase):
     '''Tests for grub'''
 
     def setUp(self):
         '''unit test set up'''
-        self.path = '/foo/auto_install/x86iso'
+        self.path = '/foo/auto_install/x86path'
         self.svcname = 'myservice'
         self.bootargs = 'console=ttya'
 
-        # create original menu.lst file
-        self.menulst_txt = (
-            "default=0\n"
-            "timeout=30\n"
-            "min_mem64=0\n\n"
-
-            "title Oracle Solaris 11 11/11 Text Installer and command line\n"
-            "\tkernel$ /%(name)s/platform/i86pc/kernel/$ISADIR/unix -B "
-            "%(args)s,install_media=http://$serverIP:5555/%(path)s,"
-            "install_service=%(name)s,install_svc_address=$serverIP:5555\n"
-            "\tmodule$ /%(name)s/platform/i86pc/$ISADIR/boot_archive\n\n"
-
-            "title Oracle Solaris 11 11/11 Automated Install\n"
-            "\tkernel$ /%(name)s/platform/i86pc/kernel/$ISADIR/unix -B "
-            "%(args)s,install=true,"
+        # create original boot instance strings
+        self.kargs = ("-B %(args)s,install=true,"
             "install_media=http://$serverIP:5555/%(path)s,"
-            "install_service=%(name)s,install_svc_address=$serverIP:5555\n"
-            "\tmodule$ /%(name)s/platform/i86pc/$ISADIR/boot_archive\n") % \
-            {'args': self.bootargs, 'path': self.path, 'name': self.svcname}
-
-        (tfp, self.mymenulst) = tempfile.mkstemp()
-        os.write(tfp, self.menulst_txt)
-        os.close(tfp)
-
-    def tearDown(self):
-        '''unit test tear down'''
-        os.remove(self.mymenulst)
+            "install_service=%(name)s,install_svc_address=$serverIP:5555\n" %
+             {'args': self.bootargs, 'path': self.path, 'name': self.svcname})
+        self.kernel = ("\tkernel$ /%(name)s/platform/i86pc/kernel/$ISADIR/unix"
+                        % {'name': self.svcname})
+        self.boot_archive = ("\tmodule$ /%(name)s/platform/i86pc/$ISADIR/"
+                             "boot_archive\n" % {'name': self.svcname})
 
     def test_update_imagepath(self):
-        '''verify update_imagepath updates imagepath correctly'''
+        '''verify update_kargs_imagepath updates imagepath correctly'''
 
-        # update path in menu.lst file, read file back in
-        # and ensure file updated properly
         newpath = '/export/mydir/myimage'
-        grub.update_imagepath(self.mymenulst, self.path, newpath)
-        with open(self.mymenulst, 'r') as menulst_file:
-            newmenulst = menulst_file.read()
-        expected_text = self.menulst_txt.replace(self.path, newpath)
-        self.assertEqual(newmenulst, expected_text)
+        new_kargs = grub.update_kargs_imagepath(self.kargs, self.path, newpath)
+        expected_text = self.kargs.replace(self.path, newpath)
+        self.assertEqual(new_kargs, expected_text)
+
+    def test_update_boot_instance_svcname(self):
+        '''verify update_boot_instance_svcname updates svcname correctly'''
+        newname = 'newsvcname'
+        boot_inst = BootInstance(name=self.svcname, path=self.path,
+                                 bootargs=self.bootargs)
+        boot_inst = grub.update_boot_instance_svcname(boot_inst,
+            self.svcname, 'newsvcname')
+        boot_inst2 = BootInstance(name=newname, path=self.path,
+                                     bootargs=self.bootargs)
+        self.assertEqual(boot_inst.kargs, boot_inst2.kargs)
+        self.assertEqual(boot_inst.kernel, boot_inst2.kernel)
+        self.assertEqual(boot_inst.boot_archive, boot_inst2.boot_archive)
 
-    def test_update_svcname(self):
-        '''verify update_svcname updates svcname correctly'''
+    def test_update_svcname_functions(self):
+        '''verify individual update_svcname functions work correctly'''
 
-        # update svcname in menu.lst file, read file back in
-        # and ensure file updated properly
         newsvcname = 'new_service'
-        grub.update_svcname(self.mymenulst, newsvcname, newsvcname)
-        with open(self.mymenulst, 'r') as menulst_file:
-            newmenulst = menulst_file.read()
-        expected_text = self.menulst_txt.replace('/' + self.svcname + '/',
-                                                 '/' + newsvcname + '/')
-        expected_text = expected_text.replace(
-            'install_service=' + self.svcname,
-            'install_service=' + newsvcname)
-        self.assertEqual(newmenulst, expected_text)
+
+        # test using kernel string
+        new_kernel = grub.update_kernel_ba_svcname(self.kernel, self.svcname,
+                                                   newsvcname)
+        expected_text = self.kernel.replace('/' + self.svcname + '/',
+                                           '/' + newsvcname + '/')
+        self.assertEqual(new_kernel, expected_text)
+
+        # test using module string
+        new_boot_archive = grub.update_kernel_ba_svcname(self.boot_archive,
+            self.svcname, newsvcname)
+        expected_text = self.boot_archive.replace('/' + self.svcname + '/',
+                                                  '/' + newsvcname + '/')
+        self.assertEqual(new_boot_archive, expected_text)
+
+        new_kargs = grub.update_kargs_install_service(self.kargs,
+            self.svcname, newsvcname)
+        expected_text = self.kargs.replace('install_service=' + self.svcname,
+                                           'install_service=' + newsvcname)
+        self.assertEqual(new_kargs, expected_text)
 
     def test_update_bootargs(self):
-        '''verify update_bootargs updates bootargs correctly'''
+        '''verify update_kargs_bootargs updates bootargs correctly'''
 
         # update bootargs in menu.lst file, read file back in
         # and ensure file updated properly
         newbootargs = 'console=ttyb'
-        grub.update_bootargs(self.mymenulst, self.bootargs, newbootargs)
-        with open(self.mymenulst, 'r') as menulst_file:
-            newmenulst = menulst_file.read()
-        expected_text = self.menulst_txt.replace(self.bootargs, newbootargs)
-        self.assertEqual(newmenulst, expected_text)
+        new_kargs = grub.update_kargs_bootargs(self.kargs, self.bootargs,
+                                               newbootargs)
+        expected_text = self.kargs.replace(self.bootargs, newbootargs)
+        self.assertEqual(new_kargs, expected_text)
+
+    def test_set_perms(self):
+        '''Ensure that set_perms sets permissions properly'''
+
+        # save original umask
+        orig_umask = os.umask(0022)
+        # set too restrictive and too open umask
+        for mask in (0066, 0000):
+            tmpfile = tempfile.mktemp()
+            with open(tmpfile, 'w'):
+                pass
+            os.umask(mask)
+            grub.set_perms(tmpfile, pwd.getpwuid(os.getuid()).pw_name,
+                           grp.getgrgid(os.getgid()).gr_name, 420)
+            mode = os.stat(tmpfile).st_mode
+            self.assertEqual(mode, 0100644)
+            os.remove(tmpfile)
+        # return umask to the original value
+        os.umask(orig_umask)
 
 
 if __name__ == '__main__':
--- a/usr/src/lib/install_boot/boot.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/lib/install_boot/boot.py	Fri Mar 23 04:40:57 2012 -0700
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 """ boot.py -- Installs and configures boot loader and boot menu
     onto physical systems and installation images.
@@ -41,7 +41,8 @@
 from pwd import getpwnam
 from shutil import move, copyfile, rmtree
 
-from bootmgmt import bootconfig, BootmgmtUnsupportedPropertyError
+from bootmgmt import bootconfig, BootmgmtUnsupportedPlatformError, \
+    BootmgmtPropertyWriteError, BootmgmtUnsupportedPropertyError
 from bootmgmt.bootconfig import BootConfig, DiskBootConfig, ODDBootConfig, \
     SolarisDiskBootInstance, SolarisODDBootInstance, ChainDiskBootInstance
 from bootmgmt.bootloader import BootLoader
@@ -55,13 +56,24 @@
 from solaris_install.logger import INSTALL_LOGGER_NAME as ILN
 from solaris_install.target import Target
 from solaris_install.target.logical import be_list, BE, Zpool
-from solaris_install.target.physical import Disk, Slice
+from solaris_install.target.physical import Disk, FDISK, \
+    Partition, Slice
 from solaris_install.transfer.media_transfer import get_image_grub_title
 
 BOOT_ENV = "boot-env"
 # Character boot device paths eg. c0t0d0s0
 DEVS = "devs"
 
+# Stuff used to set up a USB media image with grub2
+GRUB_SETUP = "/usr/lib/grub2/bios/sbin/grub-setup"
+LOFIADM = "/usr/sbin/lofiadm"
+MBR_IMG_RPATH = "boot/mbr.img"
+
+# For USB we assume a 512b block size. Seems most reasonable for a USB stick
+BLOCKSIZE = 512
+KB = 1024
+MB = (KB * KB)
+
 
 class BootMenu(Checkpoint):
     """ Abstract class for BootMenu checkpoint
@@ -210,9 +222,9 @@
                 ftype = config_set[0]
                 if ftype == BootConfig.OUTPUT_TYPE_FILE:
                     self._handle_file_type(config_set, dry_run)
-                elif ftype in [BootConfig.OUTPUT_TYPE_BIOS_ELTORITO]:
-                # UEFI - add handlers for the following when supported:
-                #              BootConfig.OUTPUT_TYPE_UEFI_ELTORITO
+                elif ftype in [BootConfig.OUTPUT_TYPE_BIOS_ELTORITO,
+                               BootConfig.OUTPUT_TYPE_UEFI_ELTORITO]:
+                # XXX add handlers for the following when supported:
                 #              BootConfig.OUTPUT_TYPE_HSFS_BOOTBLK
                     self._handle_iso_boot_image_type(config_set, dry_run)
                 else:
@@ -922,9 +934,11 @@
         """ Constructor for class
         """
         super(ISOImageBootMenu, self).__init__(name)
+        self.ba_build = None
         self.dc_dict = dict()
         self.dc_pers_dict = dict()
         self.pkg_img_path = None
+        self.tmp_dir = None
 
     def init_boot_config(self, autogen=True):
         """ Instantiates the appropriate bootmgmt.bootConfig subclass object
@@ -934,18 +948,49 @@
                     bootconfig.BootConfig.BCF_ONESHOT)
         self.config = ODDBootConfig(bc_flags,
                                     oddimage_root=self.pkg_img_path)
-        # UEFI Note that we will have to specify additional firmware
-        # targets (uefi64 & SPARC OBP) when pybootmgmt supports them.
-        self.config.boot_loader.setprop('boot-targets', 'bios')
+
+        # Inspect boot loader to determine if it can support creation of a
+        # hybrid BIOS/UEFI bootable ISO image.
+        try:
+            self.config.boot_loader.setprop(BootLoader.PROP_BOOT_TARGS,
+                                            ['bios', 'uefi64'])
+        except BootmgmtUnsupportedPlatformError:
+            self.logger.warning("Boot loader '%s' does not support both " \
+                "BIOS and UEFI64 platforms." % (self.config.boot_loader.name))
+
+        fw_targs = self.config.boot_loader.getprop(BootLoader.PROP_BOOT_TARGS)
+        if isinstance(fw_targs, str):
+            fw_targs = [fw_targs]
+        self.logger.info("Creating ISO boot configuration for firmware " \
+                         "targets: " + ', '.join(fw_targs))
 
         # Apply boot timeout property to boot loader.
         try:
-            self.config.boot_loader.setprop('timeout',
+            self.config.boot_loader.setprop(BootLoader.PROP_TIMEOUT,
                                             self.boot_timeout)
         except BootmgmtUnsupportedPropertyError:
             self.logger.warning("Boot loader type %s does not support the \
-                                'timeout' property. Ignoring." \
-                                % self.config.boot_loader.name)
+                'timeout' property. Ignoring." \
+                % self.config.boot_loader.name)
+
+        # Apply ident file property if required by boot loader:
+        # GRUB2 requires this file, Legacy GRUB doesn't. No big deal
+        if BootLoader.PROP_IDENT_FILE in \
+            self.config.boot_loader.SUPPORTED_PROPS:
+            # read .volsetid and derive a unique ident file name from it
+            vpath = os.path.join(self.ba_build, ".volsetid")
+            with open(vpath, "r") as vpath_fh:
+                volsetid = vpath_fh.read().strip()
+
+            identfile = "." + volsetid
+            identpath = os.path.join(self.pkg_img_path, identfile)
+            self.logger.info("Boot loader unique ident file name: %s" \
+                             % identfile)
+            with open(identpath, "a+") as identf:
+                identf.write("# This file identifies the image root for GRUB2")
+
+            self.config.boot_loader.setprop(BootLoader.PROP_IDENT_FILE,
+                                            "/" + identfile)
 
     def _get_rel_file_path(self):
         """
@@ -997,7 +1042,9 @@
             not_found_is_err=True)[0].data_dict
 
         try:
+            self.ba_build = self.dc_dict["ba_build"]
             self.pkg_img_path = self.dc_dict["pkg_img_path"]
+            self.tmp_dir = self.dc_dict["tmp_dir"]
         except KeyError, msg:
             raise RuntimeError("Error retrieving a value from the DOC: " + \
                 str(msg))
@@ -1009,12 +1056,99 @@
         # parsing the BootMods tree of the DOC
         self.boot_title = self.rel_file_title
 
+    def _prepare_usb_boot(self, dry_run=False):
+        """ Creates an embeddable boot image for GRUB2 based USB boot media
+        """
+        # Return if this is not GRUB2
+        if self.config.boot_loader.name != "GRUB2":
+            return
+
+        self.logger.info("Preparing MBR embedded GRUB2 boot image for USB")
+
+        # Create a nice big 10MB lofi file.
+        imagesize = (10 * MB)
+        imagename = os.path.join(self.tmp_dir, "mbr_embedded_grub2")
+
+        with open(imagename, 'w') as imagefile:
+            imagefile.seek(imagesize - 1)
+            imagefile.write('\0')
+
+        if not dry_run:
+            # Create the lofi device
+            lofi_dev = None
+            lofi_rdev = None
+            cmd = [LOFIADM, "-a", imagename]
+            try:
+                p = run(cmd)
+                lofi_dev = p.stdout.strip()
+                lofi_rdev = lofi_dev.replace("lofi", "rlofi")
+
+                uefi_image_size = os.stat(self._uefi_image_name).st_size
+                self.logger.debug("EFI system partition image: %s",
+                                  self._uefi_image_name)
+
+                # write the uefi.img to the lofi character dev at 1MB offset
+                # in 1KB increments, instead of allocating one giant buffer.
+                self.logger.debug("Writing EFI system partition image to: %s",
+                                  lofi_rdev)
+                with open(self._uefi_image_name, 'r') as src:
+                    with open(lofi_rdev, 'w') as dst:
+                        dst.seek(MB)
+                        for i in range(uefi_image_size / KB):
+                            dst.write(src.read(KB))
+                        tail = uefi_image_size % KB
+                        if tail > 0:
+                            dst.write(src.read(tail))
+
+                # Write out an fdisk table. We don't use the standard install
+                # target modules because we want precise control over the
+                # geometry, plus it's not well suited to lofi devices.
+                # Layout:
+                # type ESP, at 1M offset, partition size = uefi_image size
+                esptype = Partition.name_to_num("EFI System")
+                startblock = MB / BLOCKSIZE
+                uefi_image_nblocks = uefi_image_size / BLOCKSIZE
+                fdiskentry = "%d:0:0:0:0:0:0:0:%d:%d" \
+                             % (esptype, startblock, uefi_image_nblocks)
+                self.logger.debug("Creating fdisk table on %s:\n\t%s",
+                                  lofi_rdev, fdiskentry)
+                cmd = [FDISK, "-A", fdiskentry, lofi_rdev]
+                run(cmd)
+
+                # Install the BIOS grub2 core image in the MBR of the lofi dev
+                self.logger.debug("Installing GRUB2 on %s", lofi_rdev)
+                bios_grub_dir = os.path.join(self.pkg_img_path,
+                                             "boot/grub/i386-pc")
+                cmd = [GRUB_SETUP, "-d", bios_grub_dir, "-M", lofi_rdev]
+                run(cmd)
+
+                # Write the first 1MB of the lofi image out to file and store
+                # it in the pkg_img area for inclusion in the ISO
+                usb_image_name = os.path.join(self.pkg_img_path,
+                                              MBR_IMG_RPATH)
+                self.logger.debug("Writing GRUB2 MBR image to: %s",
+                                  usb_image_name)
+                with open(lofi_rdev, 'r') as src:
+                    with open(usb_image_name, 'w') as dst:
+                        for i in range(KB):
+                            dst.write(src.read(KB))
+            finally:
+                # remove the lofi device
+                if lofi_dev is not None:
+                    cmd = [LOFIADM, "-d", lofi_dev]
+                    run(cmd)
+
+            # Remove the backing file for the lofi image
+            os.unlink(imagename)
+            self.logger.info("MBR embedded GRUB2 boot image complete")
+
     def execute(self, dry_run=False):
         """ Primary execution method used by the Checkpoint parent class
         """
         self.logger.info("=== Executing Boot Loader Setup Checkpoint ===")
         super(ISOImageBootMenu, self).execute(dry_run)
         self.update_img_info_path()
+        self._prepare_usb_boot(dry_run)
 
     def install_boot_loader(self, dry_run=False):
         """ Install the boot loader and associated boot configuration files.
@@ -1024,9 +1158,12 @@
                 tempfile.mkdtemp(dir="/tmp", prefix="iso_boot_config_")
         else:
             temp_dir = self.pkg_img_path
+
         # Add dictionary mappings for tokens that commit_boot_config() might
         # return.
         self.boot_tokens[BootConfig.TOKEN_SYSTEMROOT] = self.pkg_img_path
+        self.boot_tokens[ODDBootConfig.TOKEN_ODD_ROOT] = self.pkg_img_path
+
         self.logger.debug("Writing boot configuration to %s" % (temp_dir))
         boot_config_list = self.config.commit_boot_config(temp_dir, None)
         self._handle_boot_config_list(boot_config_list, dry_run)
@@ -1049,13 +1186,41 @@
         """ Method that copies ISO El Torito and HSFS bootblockimage file types
             to their appropriate targets.
         """
+
+        # XXX: Add in key/val pair to handle the SPARC HSFS BOOTBLK
+        # type here when pybootmgmt supports this firmware type.
+        # For now just it just does BIOS & UEFI ELTORITO.
+        eltorito_dict = {
+            BootConfig.OUTPUT_TYPE_BIOS_ELTORITO: \
+                ("bios-eltorito-img", "BIOS"),
+            BootConfig.OUTPUT_TYPE_UEFI_ELTORITO: \
+                ("uefi-eltorito-img", "UEFI")
+        }
+
         # Store the ISO boot image path in DC's dictionary for later use
         # when constructing the ISO image
         img_type = config[0]
         src = config[1]
+        if config[3] is None:
+            dst = src
+        else:
+            dst = config[3] % self.boot_tokens
+
+        img_val = eltorito_dict.get(img_type)
+        if img_val is None:
+            raise RuntimeError("Unrecognised ISO boot loader image type: %s" \
+                               % img_type)
+        else:
+            img_id, firmware_id = img_val
+
         if not os.path.exists(src):
             raise RuntimeError("Expected  boot image type \'%s\' does not " \
                                "exist at path: %s" % (img_type, src))
+
+        # Keep a reference to the full path name of the UEFI image. We need it
+        # later to set up USB boot.
+        if img_type == BootConfig.OUTPUT_TYPE_UEFI_ELTORITO:
+            self._uefi_image_name = dst
         if dry_run is True:
             self.logger.debug("Deleting El Torito image: %s" % src)
             os.unlink(src)
@@ -1063,40 +1228,24 @@
 
         if not os.path.abspath(src).startswith(self.pkg_img_path):
             raise RuntimeError("El Torito boot image \'%s\' mislocated "
-                                 "outside of image root: %s" \
-                                 % (src, self.pkg_img_path))
-        if img_type == BootConfig.OUTPUT_TYPE_BIOS_ELTORITO:
-            # bios-eltorito-img name is randomised by pybootmgmt to avoid
-            # name collisions. Rename it to something less conspicuous.
-            dst = os.path.join(self.pkg_img_path,
-                               'boot',
-                               '.bios-eltorito-img')
+                               "outside of image root: %s" \
+                               % (src, self.pkg_img_path))
+        if src != dst:
             os.rename(src, dst)
-            # bios-eltorito-img needs to live in the persistent section
-            # of the DOC to ensure pause/resume works correctly.
-            #
-            # Update the DC_PERS_LABEL DOC object with an entry for
-            # bios-eltorito-img
-            if self.dc_pers_dict:
-                self.doc.persistent.delete_children(name=DC_PERS_LABEL)
+        if self.dc_pers_dict:
+            self.doc.persistent.delete_children(name=DC_PERS_LABEL)
 
-            # Strip out the pkg_img_path prefix from dst. Otherwise
-            # mkisofs will choke because it requires a relative rather
-            # than an absolute path for the eltorito image argument
-            self.dc_pers_dict["bios-eltorito-img"] = \
-                dst.split(self.pkg_img_path + os.sep)[1]
-            self.doc.persistent.insert_children(
-                DataObjectDict(DC_PERS_LABEL,
-                self.dc_pers_dict, generate_xml=True))
-            self.logger.debug("BIOS El Torito boot image: %s" \
-                              % self.dc_pers_dict["bios-eltorito-img"])
-        # UEFI / Pybootmgmt.
-        # Add in blocks to handle UEFI ELTORIO and SPARC HSFS BOOTBLK
-        # types here when pybootmgmt supports these firmware types.
-        # For now just it just does BIOS ELTORITO above.
-        else:
-            raise RuntimeError("Unrecognised ISO boot loader image type: %s" \
-                               % img_type)
+        # Strip out the pkg_img_path prefix from dst. Otherwise
+        # mkisofs will choke because it requires a relative rather
+        # than an absolute path for the eltorito image argument
+        self.dc_pers_dict[img_id] = \
+            dst.split(self.pkg_img_path + os.sep)[1]
+        self.doc.persistent.insert_children(
+            DataObjectDict(DC_PERS_LABEL,
+            self.dc_pers_dict, generate_xml=True))
+
+        self.logger.info("%s El Torito boot image name: %s" \
+                         % (firmware_id, self.dc_pers_dict[img_id]))
 
 
 class AIISOImageBootMenu(ISOImageBootMenu):
@@ -1185,6 +1334,17 @@
         """
         super(LiveCDISOImageBootMenu, self).__init__(name)
 
+    def init_boot_config(self, autogen=True):
+        """ Instantiates the appropriate bootmgmt.bootConfig subclass object
+            for LiveCD
+        """
+        super(LiveCDISOImageBootMenu, self).init_boot_config(autogen)
+        # LiveCD specific global bootloader configuration:
+        # - Enable graphical boot splash
+        self.config.boot_loader.setprop(
+            BootLoader.PROP_CONSOLE,
+            BootLoader.PROP_CONSOLE_GFX)
+
     def build_default_entries(self):
         """ Constructs the default boot entries and inserts them into the
             bootConfig object's boot_instances list
--- a/usr/src/lib/install_boot/test/test_boot.py	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/lib/install_boot/test/test_boot.py	Fri Mar 23 04:40:57 2012 -0700
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 """ Boot checkpoint Unit Tests
@@ -35,7 +35,7 @@
 import pwd
 import grp
 
-from shutil import rmtree, copyfile
+from shutil import rmtree, copyfile, copytree
 from lxml import etree
 
 from bootmgmt import BootmgmtUnsupportedPropertyError
@@ -421,25 +421,37 @@
         rel_file = os.path.join(rel_file_dir, 'release')
         with open(rel_file, 'w') as file_handle:
             file_handle.write('Solaris Boot Test')
-        doc_dict = {"pkg_img_path": temp_dir}
+        # Under normal DC circumstances pkg_img_path and ba_build are
+        # different paths, but this is irrelevant to the boot checkpoint
+        doc_dict = {"pkg_img_path": temp_dir,
+                    "ba_build": temp_dir,
+                    "tmp_dir": temp_dir}
         self.temp_dir = temp_dir
+        with open(os.path.join(temp_dir, ".volsetid"), "w") as volsetid:
+            volsetid.write("boot_checkpoint_ISO_unit_test")
         self.doc.volatile.insert_children(DataObjectDict("DC specific",
                                           doc_dict,
                                           generate_xml=True))
-        # Create a dummy boot and grub menu.ls environment
-        # so that pybootmgmt can instantiate a legacyGrub
-        # boot loader under the bootconfig object
-        # UEFI - this needs updating for SPARC, GRUB2 & UEFI
-        # when support gets added to pybootmgmt
+        # Create a dummy boot and grub menu.lst environment so that pybootmgmt
+        # can instantiate a Legacy GRUB or GRUB2 boot loader under the
+        # bootconfig object.
+        # XXX - this needs updating for SPARC
         if platform.processor() == 'i386':
-            os.makedirs(os.path.join(temp_dir, 'boot/grub'))
-            copyfile('/boot/grub/menu.lst',
-                     os.path.join(temp_dir, 'boot/grub/menu.lst'))
-            copyfile('/boot/grub/stage2_eltorito',
-                     os.path.join(temp_dir, 'boot/grub/stage2_eltorito'))
-            os.makedirs(os.path.join(temp_dir, 'boot/solaris'))
-            copyfile('/boot/solaris/bootenv.rc',
-                     os.path.join(temp_dir, 'boot/solaris/bootenv.rc'))
+            # Create a dummy legacy GRUB environment if that's what the build
+            # host has
+            if os.path.exists('/boot/grub/stage2_eltorito'):
+                os.makedirs(os.path.join(temp_dir, 'boot/grub'))
+                copyfile('/boot/grub/menu.lst',
+                         os.path.join(temp_dir, 'boot/grub/menu.lst'))
+                copyfile('/boot/grub/stage2_eltorito',
+                         os.path.join(temp_dir, 'boot/grub/stage2_eltorito'))
+                os.makedirs(os.path.join(temp_dir, 'boot/solaris'))
+                copyfile('/boot/solaris/bootenv.rc',
+                         os.path.join(temp_dir, 'boot/solaris/bootenv.rc'))
+            # If it has GRUB 2, copy over the entire GRUB2 tree
+            if os.path.exists('/usr/lib/grub2'):
+                copytree('/usr/lib/grub2',
+                         os.path.join(temp_dir, 'usr/lib/grub2'))
 
     def tearDown(self):
         """ Cleans up after each unit test is executed
--- a/usr/src/pkg/manifests/install-distribution-constructor.mf	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/pkg/manifests/install-distribution-constructor.mf	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 # CDDL HEADER END
 #
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 set name=pkg.fmri value=pkg:/install/distribution-constructor@$(PKGVERS)
@@ -180,6 +180,9 @@
 depend type=require fmri=compress/p7zip
 # /usr/bin/mkisofs
 depend type=require fmri=media/cdrtools
+# Add manual boot-management version 13 dependency for UEFI/GRUB2 compat.
+depend type=require \
+    fmri=pkg:/system/library/[email protected],5.11-0.175.1.0.0.13
 # /usr/bin/rmformat
 depend type=require fmri=service/storage/media-volume-manager
 # /usr/bin/awk
--- a/usr/src/pkg/manifests/install-installadm.mf	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/pkg/manifests/install-installadm.mf	Fri Mar 23 04:40:57 2012 -0700
@@ -19,54 +19,27 @@
 # CDDL HEADER END
 #
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 set name=pkg.fmri value=pkg:/install/installadm@$(PKGVERS)
+set name=pkg.summary value="installadm utility"
 set name=pkg.description value="Automatic Installation Server Setup Tools"
-set name=pkg.summary value="installadm utility"
 set name=info.classification \
     value="org.opensolaris.category.2008:System/Administration and Configuration"
 set name=variant.arch value=$(ARCH)
 set name=variant.opensolaris.zone value=global value=nonglobal
-dir path=lib
-dir path=lib/svc
-dir path=lib/svc/manifest group=sys
-dir path=lib/svc/manifest/system group=sys
-dir path=lib/svc/manifest/system/install group=sys
-dir path=lib/svc/method
-dir path=usr group=sys
-dir path=usr/lib
-dir path=usr/lib/installadm
-dir path=usr/lib/python2.6
-dir path=usr/lib/python2.6/vendor-packages
-dir path=usr/lib/python2.6/vendor-packages/osol_install
-dir path=usr/lib/python2.6/vendor-packages/osol_install/auto_install
-dir path=usr/sbin
-dir path=usr/share group=sys
-dir path=usr/share/auto_install group=sys
-dir path=usr/share/man
-dir path=usr/share/man/ja_JP.UTF-8
-dir path=usr/share/man/ja_JP.UTF-8/man1m
-dir path=usr/share/man/man1m
-dir path=usr/share/man/zh_CN.UTF-8
-dir path=usr/share/man/zh_CN.UTF-8/man1m
-dir path=usr/share/xml
-dir path=var group=sys
-dir path=var/ai group=sys
-dir path=var/ai/image-server group=sys
-dir path=var/ai/image-server/cgi-bin group=sys
-dir path=var/ai/image-server/images group=sys
-dir path=var/ai/image-server/logs group=sys
-dir path=var/ai/profile group=webservd mode=0700 owner=webservd
-dir path=var/ai/service group=sys
-dir path=var/installadm group=sys
-dir path=var/installadm/ai-webserver group=sys
-dir path=var/installadm/ai-webserver/AI_data group=sys
-dir path=var/installadm/ai-webserver/AI_files group=sys
-dir path=var/installadm/ai-webserver/compatibility-configuration group=sys
+dir  path=lib
+dir  path=lib/svc
+dir  path=lib/svc/manifest group=sys
+dir  path=lib/svc/manifest/system group=sys
+dir  path=lib/svc/manifest/system/install group=sys
 file path=lib/svc/manifest/system/install/server.xml group=sys
+dir  path=lib/svc/method
 file path=lib/svc/method/svc-install-server mode=0555
+dir  path=usr group=sys
+dir  path=usr/lib
+dir  path=usr/lib/installadm
 file path=usr/lib/installadm/aimdns.py
 file path=usr/lib/installadm/aimdnsd.py
 file path=usr/lib/installadm/check-server-setup
@@ -74,6 +47,10 @@
 file path=usr/lib/installadm/setup-image
 file path=usr/lib/installadm/setup-service
 file path=usr/lib/installadm/setup-sparc
+dir  path=usr/lib/python2.6
+dir  path=usr/lib/python2.6/vendor-packages
+dir  path=usr/lib/python2.6/vendor-packages/osol_install
+dir  path=usr/lib/python2.6/vendor-packages/osol_install/auto_install
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/AI_database.py \
     group=sys
@@ -92,9 +69,11 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/common_profile.pyc \
     group=sys
+# Add bypass generate to allow manual boot-management version 13 dependency
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/create_client.py \
-    group=sys
+    group=sys \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/create_client.pyc \
     group=sys
@@ -104,9 +83,11 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/create_profile.pyc \
     group=sys
+# Add bypass generate to allow manual boot-management version 13 dependency
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/create_service.py \
-    group=sys
+    group=sys \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/create_service.pyc \
     group=sys
@@ -150,8 +131,10 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/export.pyc \
     group=sys
+# Add bypass generate to allow manual boot-management version 13 dependency
 file path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/grub.py \
-    group=sys
+    group=sys \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/grub.pyc \
     group=sys
 file path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/image.py \
@@ -169,9 +152,11 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/publish_manifest.pyc \
     group=sys
+# Add bypass generate to allow manual boot-management version 13 dependency
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/rename_service.py \
-    group=sys
+    group=sys \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/rename_service.pyc \
     group=sys
@@ -187,9 +172,11 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/set_criteria.pyc \
     group=sys
+# Add bypass generate to allow manual boot-management version 13 dependency
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/set_service.py \
-    group=sys
+    group=sys \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/set_service.pyc \
     group=sys
@@ -205,19 +192,46 @@
 file \
     path=usr/lib/python2.6/vendor-packages/osol_install/auto_install/verifyXML.pyc \
     group=sys
+dir  path=usr/sbin
 file path=usr/sbin/installadm mode=0555
 file path=usr/sbin/installadm-convert mode=0555
+dir  path=usr/share group=sys
+dir  path=usr/share/auto_install group=sys
 file path=usr/share/auto_install/criteria_schema.rng group=sys
+dir  path=usr/share/man
+dir  path=usr/share/man/ja_JP.UTF-8
+dir  path=usr/share/man/ja_JP.UTF-8/man1m
 file path=usr/share/man/ja_JP.UTF-8/man1m/installadm.1m mode=0444
+dir  path=usr/share/man/man1m
 file path=usr/share/man/man1m/installadm.1m mode=0444
+dir  path=usr/share/man/zh_CN.UTF-8
+dir  path=usr/share/man/zh_CN.UTF-8/man1m
 file path=usr/share/man/zh_CN.UTF-8/man1m/installadm.1m mode=0444
+dir  path=usr/share/xml
+dir  path=var group=sys
+dir  path=var/ai group=sys
+dir  path=var/ai/image-server group=sys
+dir  path=var/ai/image-server/cgi-bin group=sys
 file path=var/ai/image-server/cgi-bin/cgi_get_manifest.py mode=0555
+dir  path=var/ai/image-server/images group=sys
+dir  path=var/ai/image-server/logs group=sys
+dir  path=var/ai/profile owner=webservd group=webservd mode=0700
+dir  path=var/ai/service group=sys
+dir  path=var/installadm group=sys
+dir  path=var/installadm/ai-webserver group=sys
 file path=var/installadm/ai-webserver/AI.db
+dir  path=var/installadm/ai-webserver/AI_data group=sys
+dir  path=var/installadm/ai-webserver/AI_files group=sys
 file path=var/installadm/ai-webserver/ai-httpd.conf
+dir  path=var/installadm/ai-webserver/compatibility-configuration group=sys
 license cr_Sun license=cr_Sun
-depend fmri=database/sqlite-3 type=require
-depend fmri=service/network/dhcp/isc-dhcp type=require
-depend fmri=service/network/dns/mdns type=require
-depend fmri=service/network/tftp type=require
-depend fmri=system/install/locale type=require
-depend fmri=web/server/apache-22 type=require
+depend type=require fmri=database/sqlite-3
+# Add manual boot-management version 13 dependency for UEFI/GRUB2 compat.
+# Make sure to remove the pkg.depend.bypass-genererate actions if removing.
+depend type=require \
+    fmri=pkg:/system/library/[email protected],5.11-0.175.1.0.0.13
+depend type=require fmri=service/network/dhcp/isc-dhcp
+depend type=require fmri=service/network/dns/mdns
+depend type=require fmri=service/network/tftp
+depend type=require fmri=system/install/locale
+depend type=require fmri=web/server/apache-22
--- a/usr/src/pkg/manifests/system-library-install.mf	Thu Mar 22 00:08:34 2012 -0600
+++ b/usr/src/pkg/manifests/system-library-install.mf	Fri Mar 23 04:40:57 2012 -0700
@@ -19,7 +19,7 @@
 # CDDL HEADER END
 #
 #
-# Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
 #
 
 set name=pkg.fmri value=pkg:/system/library/install@$(PKGVERS)
@@ -47,13 +47,17 @@
 file path=usr/lib/python2.6/vendor-packages/osol_install/liberrsvc.py
 file path=usr/lib/python2.6/vendor-packages/osol_install/liberrsvc.pyc
 dir  path=usr/lib/python2.6/vendor-packages/solaris_install
-file path=usr/lib/python2.6/vendor-packages/solaris_install/__init__.py
+# Add bypass generate to allow manual boot-management version 13 dependency
+file path=usr/lib/python2.6/vendor-packages/solaris_install/__init__.py \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/solaris_install/__init__.pyc
 file path=usr/lib/python2.6/vendor-packages/solaris_install/_logger.so
 dir  path=usr/lib/python2.6/vendor-packages/solaris_install/boot
 file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/__init__.py
 file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/__init__.pyc
-file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/boot.py
+# Add bypass generate to allow manual boot-management version 13 dependency
+file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/boot.py \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/boot.pyc
 file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/boot_spec.py
 file path=usr/lib/python2.6/vendor-packages/solaris_install/boot/boot_spec.pyc
@@ -115,9 +119,15 @@
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/__init__.pyc
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/cgc.py
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/cgc.pyc
-file path=usr/lib/python2.6/vendor-packages/solaris_install/target/controller.py
+# Add bypass generate to allow manual boot-management version 13 dependency
+file \
+    path=usr/lib/python2.6/vendor-packages/solaris_install/target/controller.py \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/controller.pyc
-file path=usr/lib/python2.6/vendor-packages/solaris_install/target/discovery.py
+# Add bypass generate to allow manual boot-management version 13 dependency
+file \
+    path=usr/lib/python2.6/vendor-packages/solaris_install/target/discovery.py \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/discovery.pyc
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/instantiation.py
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/instantiation.pyc
@@ -180,7 +190,9 @@
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/libnvpair/nvl.pyc
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/logical.py
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/logical.pyc
-file path=usr/lib/python2.6/vendor-packages/solaris_install/target/physical.py
+# Add bypass generate to allow manual boot-management version 13 dependency
+file path=usr/lib/python2.6/vendor-packages/solaris_install/target/physical.py \
+    pkg.depend.bypass-generate=^usr/lib/python2.6/vendor-packages/bootmgmt/.*$
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/physical.pyc
 dir  path=usr/lib/python2.6/vendor-packages/solaris_install/target/shadow
 file path=usr/lib/python2.6/vendor-packages/solaris_install/target/shadow/__init__.py
@@ -236,4 +248,8 @@
 link path=usr/snadm/lib/libspmicommon.so target=libspmicommon.so.1
 file path=usr/snadm/lib/libspmicommon.so.1
 license cr_Sun license=cr_Sun
+# Add manual boot-management version 13 dependency for UEFI/GRUB2 compat.
+# Make sure to remove the pkg.depend.bypass-genererate actions if removing.
+depend type=require \
+    fmri=pkg:/system/library/[email protected],5.11-0.175.1.0.0.13
 depend type=require fmri=system/boot-environment-utilities