[dhcp] Include support for PXE boot menus
authorMichael Brown <mcb30@etherboot.org>
Sun, 25 Jan 2009 21:16:47 +0000 (21:16 +0000)
committerMichael Brown <mcb30@etherboot.org>
Sun, 25 Jan 2009 21:16:47 +0000 (21:16 +0000)
PXE dictates a mechanism for boot menuing, involving prompting the
user with a variable message, waiting for a predefined keypress,
displaying a boot menu, and waiting for a selection.

This breaks the currently desirable abstraction that DHCP is a process
that can happen in the background without any user interaction.

src/include/gpxe/dhcp.h
src/net/udp/dhcp.c

index 66aa50a..d49ba7f 100644 (file)
@@ -88,6 +88,9 @@ struct dhcp_pxe_boot_menu_item;
 /** PXE boot menu */
 #define DHCP_PXE_BOOT_MENU DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 9 )
 
+/** PXE boot menu prompt */
+#define DHCP_PXE_BOOT_MENU_PROMPT DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 10 )
+
 /** PXE boot menu item */
 #define DHCP_PXE_BOOT_MENU_ITEM DHCP_ENCAP_OPT ( DHCP_VENDOR_ENCAP, 71 )
 
@@ -478,7 +481,7 @@ struct dhcphdr {
 #define DHCP_MIN_LEN 552
 
 /** Maximum time that we will wait for ProxyDHCP responses */
-#define PROXYDHCP_WAIT_TIME ( TICKS_PER_SEC * 1 )
+#define PROXYDHCP_WAIT_TIME ( 2 * TICKS_PER_SEC )
 
 /** Timeouts for sending DHCP packets */
 #define DHCP_MIN_TIMEOUT ( 1 * TICKS_PER_SEC )
index 7140367..c7e1f88 100644 (file)
 #include <string.h>
 #include <stdlib.h>
 #include <stdio.h>
+#include <ctype.h>
 #include <errno.h>
 #include <assert.h>
 #include <byteswap.h>
+#include <console.h>
 #include <gpxe/if_ether.h>
 #include <gpxe/netdevice.h>
 #include <gpxe/device.h>
@@ -39,6 +41,7 @@
 #include <gpxe/dhcpopts.h>
 #include <gpxe/dhcppkt.h>
 #include <gpxe/features.h>
+#include <gpxe/keys.h>
 
 /** @file
  *
@@ -125,6 +128,32 @@ struct dhcp_client_uuid {
 
 #define DHCP_CLIENT_UUID_TYPE 0
 
+/** DHCP PXE boot prompt */
+struct dhcp_pxe_boot_prompt {
+       /** Timeout
+        *
+        * A value of 0 means "time out immediately and select first
+        * boot item, without displaying the prompt".  A value of 255
+        * means "display menu immediately with no timeout".  Any
+        * other value means "display prompt, wait this many seconds
+        * for keypress, if key is F8, display menu, otherwise select
+        * first boot item".
+        */
+       uint8_t timeout;
+       /** Prompt to press F8 */
+       char prompt[0];
+} __attribute__ (( packed ));
+
+/** DHCP PXE boot menu item description */
+struct dhcp_pxe_boot_menu_item_desc {
+       /** "Type" */
+       uint16_t type;
+       /** Description length */
+       uint8_t desc_len;
+       /** Description */
+       char desc[0];
+} __attribute__ (( packed ));
+
 /** DHCP PXE boot menu item */
 struct dhcp_pxe_boot_menu_item {
        /** "Type"
@@ -140,6 +169,9 @@ struct dhcp_pxe_boot_menu_item {
        uint16_t layer;
 } __attribute__ (( packed ));
 
+/** Maximum allowed number of PXE boot menu items */
+#define PXE_BOOT_MENU_MAX_ITEMS 20
+
 /**
  * Name a DHCP packet type
  *
@@ -352,6 +384,9 @@ struct dhcp_session {
        struct dhcp_settings *pxedhcpack;
        /** BootServerDHCPACK obtained during BootServerDHCPREQUEST */
        struct dhcp_settings *bsdhcpack;
+       /** PXE boot menu item */
+       struct dhcp_pxe_boot_menu_item menu_item;
+
        /** Retransmission timer */
        struct retry_timer timer;
        /** Start time of the current state (in ticks) */
@@ -602,7 +637,6 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
        struct in_addr ciaddr = { 0 };
        struct in_addr server = { 0 };
        struct in_addr requested_ip = { 0 };
-       struct dhcp_pxe_boot_menu_item menu_item = { 0, 0 };
        unsigned int msgtype;
        int rc;
 
@@ -649,11 +683,7 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
                                DHCP_PXE_BOOT_SERVER_MCAST,
                                &dest.sin_addr, sizeof ( dest.sin_addr ) );
                meta.dest = ( struct sockaddr * ) &dest;
-               dhcppkt_fetch ( &dhcp->pxedhcpack->dhcppkt,
-                               DHCP_PXE_BOOT_MENU, &menu_item.type,
-                               sizeof ( menu_item.type ) );
                assert ( dest.sin_addr.s_addr );
-               assert ( menu_item.type );
                assert ( ciaddr.s_addr );
                break;
        default:
@@ -677,8 +707,10 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
        }
        if ( requested_ip.s_addr )
                DBGC ( dhcp, " for %s", inet_ntoa ( requested_ip ) );
-       if ( menu_item.type )
-               DBGC ( dhcp, " for item %04x", ntohs ( menu_item.type ) );
+       if ( dhcp->menu_item.type ) {
+               DBGC ( dhcp, " for item %04x",
+                      ntohs ( dhcp->menu_item.type ) );
+       }
        DBGC ( dhcp, "\n" );
 
        /* Allocate buffer for packet */
@@ -689,7 +721,7 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
        /* Create DHCP packet in temporary buffer */
        if ( ( rc = dhcp_create_request ( &dhcppkt, dhcp->netdev, msgtype,
                                          ciaddr, server, requested_ip,
-                                         &menu_item, iobuf->data,
+                                         &dhcp->menu_item, iobuf->data,
                                          iob_tailroom ( iobuf ) ) ) != 0 ) {
                DBGC ( dhcp, "DHCP %p could not construct DHCP request: %s\n",
                       dhcp, strerror ( rc ) );
@@ -717,6 +749,154 @@ static int dhcp_tx ( struct dhcp_session *dhcp ) {
        return rc;
 }
 
+/**
+ * Prompt for PXE boot menu selection
+ *
+ * @v pxedhcpack       PXEDHCPACK packet
+ * @ret rc             Return status code
+ *
+ * Note that a success return status indicates that the PXE boot menu
+ * should be displayed.
+ */
+static int dhcp_pxe_boot_menu_prompt ( struct dhcp_packet *pxedhcpack ) {
+       union {
+               uint8_t buf[80];
+               struct dhcp_pxe_boot_prompt prompt;
+       } u;
+       ssize_t slen;
+       unsigned long start;
+       int key;
+
+       /* Parse menu prompt */
+       memset ( &u, 0, sizeof ( u ) );
+       if ( ( slen = dhcppkt_fetch ( pxedhcpack, DHCP_PXE_BOOT_MENU_PROMPT,
+                                     &u, sizeof ( u ) ) ) <= 0 ) {
+               /* If prompt is not present, we should always display
+                * the menu.
+                */
+               return 0;
+       }
+
+       /* Display prompt, if applicable */
+       if ( u.prompt.timeout )
+               printf ( "\n%s\n", u.prompt.prompt );
+
+       /* Timeout==0xff means display menu immediately */
+       if ( u.prompt.timeout == 0xff )
+               return 0;
+
+       /* Wait for F8 or other key press */
+       start = currticks();
+       while ( ( currticks() - start ) <
+               ( u.prompt.timeout * TICKS_PER_SEC ) ) {
+               if ( iskey() ) {
+                       key = getkey();
+                       return ( ( key == KEY_F8 ) ? 0 : -ECANCELED );
+               }
+       }
+
+       return -ECANCELED;
+}
+
+/**
+ * Perform PXE boot menu selection
+ *
+ * @v pxedhcpack       PXEDHCPACK packet
+ * @v menu_item                PXE boot menu item to fill in
+ * @ret rc             Return status code
+ *
+ * Note that a success return status indicates that a PXE boot menu
+ * item has been selected, and that the DHCP session should perform a
+ * boot server request/ack.
+ */
+static int dhcp_pxe_boot_menu ( struct dhcp_packet *pxedhcpack,
+                               struct dhcp_pxe_boot_menu_item *menu_item ) {
+       uint8_t buf[256];
+       ssize_t slen;
+       size_t menu_len;
+       struct dhcp_pxe_boot_menu_item_desc *menu_item_desc;
+       size_t menu_item_desc_len;
+       struct {
+               uint16_t type;
+               char *desc;
+       } menu[PXE_BOOT_MENU_MAX_ITEMS];
+       size_t offset = 0;
+       unsigned int num_menu_items = 0;
+       unsigned int i;
+       unsigned int selected_menu_item;
+       int key;
+       int rc;
+
+       /* Check for boot menu */
+       memset ( &buf, 0, sizeof ( buf ) );
+       if ( ( slen = dhcppkt_fetch ( pxedhcpack, DHCP_PXE_BOOT_MENU,
+                                     &buf, sizeof ( buf ) ) ) <= 0 ) {
+               DBGC2 ( pxedhcpack, "PXEDHCPACK %p has no boot menu\n",
+                       pxedhcpack );
+               return slen;
+       }
+       menu_len = slen;
+
+       /* Parse boot menu */
+       while ( offset < menu_len ) {
+               menu_item_desc = ( ( void * ) ( buf + offset ) );
+               menu_item_desc_len = ( sizeof ( *menu_item_desc ) +
+                                      menu_item_desc->desc_len );
+               if ( ( offset + menu_item_desc_len ) > menu_len ) {
+                       DBGC ( pxedhcpack, "PXEDHCPACK %p has malformed "
+                              "boot menu\n", pxedhcpack );
+                       return -EINVAL;
+               }
+               menu[num_menu_items].type = menu_item_desc->type;
+               menu[num_menu_items].desc = menu_item_desc->desc;
+               /* Set type to 0; this ensures that the description
+                * for the previous menu item is NUL-terminated.
+                * (Final item is NUL-terminated anyway.)
+                */
+               menu_item_desc->type = 0;
+               offset += menu_item_desc_len;
+               num_menu_items++;
+               if ( num_menu_items == ( sizeof ( menu ) /
+                                        sizeof ( menu[0] ) ) ) {
+                       DBGC ( pxedhcpack, "PXEDHCPACK %p has too many "
+                              "menu items\n", pxedhcpack );
+                       /* Silently ignore remaining items */
+                       break;
+               }
+       }
+       if ( ! num_menu_items ) {
+               DBGC ( pxedhcpack, "PXEDHCPACK %p has no menu items\n",
+                      pxedhcpack );
+               return -EINVAL;
+       }
+
+       /* Default to first menu item */
+       menu_item->type = menu[0].type;
+
+       /* Prompt for menu, if necessary */
+       if ( ( rc = dhcp_pxe_boot_menu_prompt ( pxedhcpack ) ) != 0 ) {
+               /* Failure to display menu means we should just
+                * continue with the boot.
+                */
+               return 0;
+       }
+
+       /* Display menu */
+       for ( i = 0 ; i < num_menu_items ; i++ ) {
+               printf ( "%c. %s\n", ( 'A' + i ), menu[i].desc );
+       }
+
+       /* Obtain selection */
+       while ( 1 ) {
+               key = getkey();
+               selected_menu_item = ( toupper ( key ) - 'A' );
+               if ( selected_menu_item < num_menu_items ) {
+                       menu_item->type = menu[selected_menu_item].type;
+                       return 0;
+               }
+       }
+}
+
 /**
  * Transition to new DHCP session state
  *
@@ -739,7 +919,8 @@ static void dhcp_set_state ( struct dhcp_session *dhcp,
  * @v dhcp             DHCP session
  */
 static void dhcp_next_state ( struct dhcp_session *dhcp ) {
-       struct in_addr bs_mcast = { 0 };
+
+       stop_timer ( &dhcp->timer );
 
        switch ( dhcp->state ) {
        case DHCP_STATE_DISCOVER:
@@ -760,10 +941,9 @@ static void dhcp_next_state ( struct dhcp_session *dhcp ) {
                /* Fall through */
        case DHCP_STATE_PROXYREQUEST:
                if ( dhcp->pxedhcpack ) {
-                       dhcppkt_fetch ( &dhcp->pxedhcpack->dhcppkt,
-                                       DHCP_PXE_BOOT_SERVER_MCAST,
-                                       &bs_mcast, sizeof ( bs_mcast ) );
-                       if ( bs_mcast.s_addr ) {
+                       dhcp_pxe_boot_menu ( &dhcp->pxedhcpack->dhcppkt,
+                                            &dhcp->menu_item );
+                       if ( dhcp->menu_item.type ) {
                                dhcp_set_state ( dhcp, DHCP_STATE_BSREQUEST );
                                break;
                        }