aboutsummaryrefslogtreecommitdiffstats
path: root/gpxe/src/usr/pxemenu.c
blob: 793cb624da9b30374429870a9c56bb61c2ee18fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/*
 * Copyright (C) 2009 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <byteswap.h>
#include <curses.h>
#include <console.h>
#include <gpxe/dhcp.h>
#include <gpxe/keys.h>
#include <gpxe/timer.h>
#include <gpxe/process.h>
#include <usr/dhcpmgmt.h>
#include <usr/autoboot.h>

/** @file
 *
 * PXE Boot Menus
 *
 */

/* Colour pairs */
#define CPAIR_NORMAL	1
#define CPAIR_SELECT	2

/** A PXE boot menu item */
struct pxe_menu_item {
	/** Boot Server type */
	unsigned int type;
	/** Description */
	char *desc;
};

/**
 * A PXE boot menu
 *
 * This structure encapsulates the menu information provided via DHCP
 * options.
 */
struct pxe_menu {
	/** Prompt string (optional) */
	const char *prompt;
	/** Timeout (in seconds)
	 *
	 * Negative indicates no timeout (i.e. wait indefinitely)
	 */
	int timeout;
	/** Number of menu items */
	unsigned int num_items;
	/** Selected menu item */
	unsigned int selection;
	/** Menu items */
	struct pxe_menu_item items[0];
};

/**
 * Parse and allocate PXE boot menu
 *
 * @v menu		PXE boot menu to fill in
 * @ret rc		Return status code
 *
 * It is the callers responsibility to eventually free the allocated
 * boot menu.
 */
static int pxe_menu_parse ( struct pxe_menu **menu ) {
	struct setting pxe_boot_menu_prompt_setting =
		{ .tag = DHCP_PXE_BOOT_MENU_PROMPT };
	struct setting pxe_boot_menu_setting =
		{ .tag = DHCP_PXE_BOOT_MENU };
	uint8_t raw_menu[256];
	int raw_prompt_len;
	int raw_menu_len;
	struct dhcp_pxe_boot_menu *raw_menu_item;
	struct dhcp_pxe_boot_menu_prompt *raw_menu_prompt;
	void *raw_menu_end;
	unsigned int num_menu_items;
	unsigned int i;
	int rc;

	/* Fetch raw menu */
	memset ( raw_menu, 0, sizeof ( raw_menu ) );
	if ( ( raw_menu_len = fetch_setting ( NULL, &pxe_boot_menu_setting,
					      raw_menu,
					      sizeof ( raw_menu ) ) ) < 0 ) {
		rc = raw_menu_len;
		DBG ( "Could not retrieve raw PXE boot menu: %s\n",
		      strerror ( rc ) );
		return rc;
	}
	if ( raw_menu_len >= ( int ) sizeof ( raw_menu ) ) {
		DBG ( "Raw PXE boot menu too large for buffer\n" );
		return -ENOSPC;
	}
	raw_menu_end = ( raw_menu + raw_menu_len );

	/* Fetch raw prompt length */
	raw_prompt_len = fetch_setting_len ( NULL,
					     &pxe_boot_menu_prompt_setting );
	if ( raw_prompt_len < 0 )
		raw_prompt_len = 0;

	/* Count menu items */
	num_menu_items = 0;
	raw_menu_item = ( ( void * ) raw_menu );
	while ( 1 ) {
		if ( ( ( ( void * ) raw_menu_item ) +
		       sizeof ( *raw_menu_item ) ) > raw_menu_end )
			break;
		if ( ( ( ( void * ) raw_menu_item ) +
		       sizeof ( *raw_menu_item ) +
		       raw_menu_item->desc_len ) > raw_menu_end )
			break;
		num_menu_items++;
		raw_menu_item = ( ( ( void * ) raw_menu_item ) +
				  sizeof ( *raw_menu_item ) +
				  raw_menu_item->desc_len );
	}

	/* Allocate space for parsed menu */
	*menu = zalloc ( sizeof ( **menu ) +
			 ( num_menu_items * sizeof ( (*menu)->items[0] ) ) +
			 raw_menu_len + 1 /* NUL */ +
			 raw_prompt_len + 1 /* NUL */ );
	if ( ! *menu ) {
		DBG ( "Could not allocate PXE boot menu\n" );
		return -ENOMEM;
	}

	/* Fill in parsed menu */
	(*menu)->num_items = num_menu_items;
	raw_menu_item = ( ( ( void * ) (*menu) ) + sizeof ( **menu ) +
			  ( num_menu_items * sizeof ( (*menu)->items[0] ) ) );
	memcpy ( raw_menu_item, raw_menu, raw_menu_len );
	for ( i = 0 ; i < num_menu_items ; i++ ) {
		(*menu)->items[i].type = ntohs ( raw_menu_item->type );
		(*menu)->items[i].desc = raw_menu_item->desc;
		/* Set type to 0; this ensures that the description
		 * for the previous menu item is NUL-terminated.
		 * (Final item is NUL-terminated anyway.)
		 */
		raw_menu_item->type = 0;
		raw_menu_item = ( ( ( void * ) raw_menu_item ) +
				  sizeof ( *raw_menu_item ) +
				  raw_menu_item->desc_len );
	}
	if ( raw_prompt_len ) {
		raw_menu_prompt = ( ( ( void * ) raw_menu_item ) +
				    1 /* NUL */ );
		fetch_setting ( NULL, &pxe_boot_menu_prompt_setting,
				raw_menu_prompt, raw_prompt_len );
		(*menu)->timeout =
			( ( raw_menu_prompt->timeout == 0xff ) ?
			  -1 : raw_menu_prompt->timeout );
		(*menu)->prompt = raw_menu_prompt->prompt;
	} else {
		(*menu)->timeout = -1;
	}

	return 0;
}

/**
 * Draw PXE boot menu item
 *
 * @v menu		PXE boot menu
 * @v index		Index of item to draw
 * @v selected		Item is selected
 */
static void pxe_menu_draw_item ( struct pxe_menu *menu,
				 unsigned int index, int selected ) {
	char buf[COLS+1];
	size_t len;
	unsigned int row;

	/* Prepare space-padded row content */
	len = snprintf ( buf, sizeof ( buf ), " %c. %s",
			 ( 'A' + index ), menu->items[index].desc );
	while ( len < ( sizeof ( buf ) - 1 ) )
		buf[len++] = ' ';
	buf[ sizeof ( buf ) - 1 ] = '\0';

	/* Draw row */
	row = ( LINES - menu->num_items + index );
	color_set ( ( selected ? CPAIR_SELECT : CPAIR_NORMAL ), NULL );
	mvprintw ( row, 0, "%s", buf );
	move ( row, 1 );
}

/**
 * Make selection from PXE boot menu
 *
 * @v menu		PXE boot menu
 * @ret rc		Return status code
 */
static int pxe_menu_select ( struct pxe_menu *menu ) {
	int key;
	unsigned int key_selection;
	unsigned int i;
	int rc = 0;

	/* Initialise UI */
	initscr();
	start_color();
	init_pair ( CPAIR_NORMAL, COLOR_WHITE, COLOR_BLACK );
	init_pair ( CPAIR_SELECT, COLOR_BLACK, COLOR_WHITE );
	color_set ( CPAIR_NORMAL, NULL );

	/* Draw initial menu */
	for ( i = 0 ; i < menu->num_items ; i++ )
		printf ( "\n" );
	for ( i = 0 ; i < menu->num_items ; i++ )
		pxe_menu_draw_item ( menu, ( menu->num_items - i - 1 ), 0 );

	while ( 1 ) {

		/* Highlight currently selected item */
		pxe_menu_draw_item ( menu, menu->selection, 1 );

		/* Wait for keyboard input */
		while ( ! iskey() )
			step();
		key = getkey();

		/* Unhighlight currently selected item */
		pxe_menu_draw_item ( menu, menu->selection, 0 );

		/* Act upon key */
		if ( ( key == CR ) || ( key == LF ) ) {
			pxe_menu_draw_item ( menu, menu->selection, 1 );
			break;
		} else if ( ( key == CTRL_C ) || ( key == ESC ) ) {
			rc = -ECANCELED;
			break;
		} else if ( key == KEY_UP ) {
			if ( menu->selection > 0 )
				menu->selection--;
		} else if ( key == KEY_DOWN ) {
			if ( menu->selection < ( menu->num_items - 1 ) )
				menu->selection++;
		} else if ( ( key < KEY_MIN ) &&
			    ( ( key_selection = ( toupper ( key ) - 'A' ) )
			      < menu->num_items ) ) {
			menu->selection = key_selection;
			pxe_menu_draw_item ( menu, menu->selection, 1 );
			break;
		}
	}

	/* Shut down UI */
	endwin();

	return rc;
}

/**
 * Prompt for (and make selection from) PXE boot menu
 *
 * @v menu		PXE boot menu
 * @ret rc		Return status code
 */
static int pxe_menu_prompt_and_select ( struct pxe_menu *menu ) {
	unsigned long start = currticks();
	unsigned long now;
	unsigned long elapsed;
	size_t len = 0;
	int key;
	int rc = 0;

	/* Display menu immediately, if specified to do so */
	if ( menu->timeout < 0 ) {
		if ( menu->prompt )
			printf ( "%s\n", menu->prompt );
		return pxe_menu_select ( menu );
	}

	/* Display prompt, if specified */
	if ( menu->prompt )
		printf ( "%s", menu->prompt );

	/* Wait for timeout, if specified */
	while ( menu->timeout > 0 ) {
		if ( ! len )
			len = printf ( " (%d)", menu->timeout );
		if ( iskey() ) {
			key = getkey();
			if ( key == KEY_F8 ) {
				/* Display menu */
				printf ( "\n" );
				return pxe_menu_select ( menu );
			} else if ( ( key == CTRL_C ) || ( key == ESC ) ) {
				/* Abort */
				rc = -ECANCELED;
				break;
			} else {
				/* Stop waiting */
				break;
			}
		}
		now = currticks();
		elapsed = ( now - start );
		if ( elapsed >= TICKS_PER_SEC ) {
			menu->timeout -= 1;
			do {
				printf ( "\b \b" );
			} while ( --len );
			start = now;
		}
	}

	/* Return with default option selected */
	printf ( "\n" );
	return rc;
}

/**
 * Boot using PXE boot menu
 *
 * @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.
 */
int pxe_menu_boot ( struct net_device *netdev ) {
	struct pxe_menu *menu;
	unsigned int pxe_type;
	struct settings *pxebs_settings;
	struct in_addr next_server;
	char filename[256];
	int rc;

	/* Parse and allocate boot menu */
	if ( ( rc = pxe_menu_parse ( &menu ) ) != 0 )
		return rc;

	/* Make selection from boot menu */
	if ( ( rc = pxe_menu_prompt_and_select ( menu ) ) != 0 ) {
		free ( menu );
		return rc;
	}
	pxe_type = menu->items[menu->selection].type;

	/* Free boot menu */
	free ( menu );

	/* Return immediately if local boot selected */
	if ( ! pxe_type )
		return 0;

	/* Attempt PXE Boot Server Discovery */
	if ( ( rc = pxebs ( netdev, pxe_type ) ) != 0 )
		return rc;

	/* Attempt boot */
	pxebs_settings = find_settings ( PXEBS_SETTINGS_NAME );
	assert ( pxebs_settings );
	fetch_ipv4_setting ( pxebs_settings, &next_server_setting,
			     &next_server );
	fetch_string_setting ( pxebs_settings, &filename_setting,
			       filename, sizeof ( filename ) );
	return boot_next_server_and_filename ( next_server, filename );
}