blob: 694a212ee4afd6ee3f3b19e6dc7a708a51c3d1c0 [file] [log] [blame]
Jason Hobbsafc8cd92011-06-29 11:25:18 -05001/*
2 * Copyright 2010-2011 Calxeda, Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License as published by the Free
6 * Software Foundation; either version 2 of the License, or (at your option)
7 * any later version.
8 *
9 * This program is distributed in the hope it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 * more details.
13 *
14 * You should have received a copy of the GNU General Public License along with
15 * this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17#include <common.h>
18#include <command.h>
19#include <malloc.h>
20#include <linux/string.h>
21#include <linux/ctype.h>
22#include <errno.h>
23#include <linux/list.h>
24
25#include "menu.h"
26
27#define MAX_TFTP_PATH_LEN 127
28
29static char *from_env(char *envvar)
30{
31 char *ret;
32
33 ret = getenv(envvar);
34
35 if (!ret)
36 printf("missing environment variable: %s\n", envvar);
37
38 return ret;
39}
40
41/*
42 * Returns the ethaddr environment variable formated with -'s instead of :'s
43 */
44static void format_mac_pxecfg(char **outbuf)
45{
46 char *p, *ethaddr;
47
48 *outbuf = NULL;
49
50 ethaddr = from_env("ethaddr");
51
52 if (!ethaddr)
John Rigbya7dbc122011-07-13 23:05:19 -060053 ethaddr = from_env("usbethaddr");
54
55 if (!ethaddr)
Jason Hobbsafc8cd92011-06-29 11:25:18 -050056 return;
57
58 *outbuf = strdup(ethaddr);
59
60 if (*outbuf == NULL)
61 return;
62
63 for (p = *outbuf; *p; p++) {
64 if (*p == ':')
65 *p = '-';
66 }
67}
68
69/*
70 * Returns the directory the file specified in the bootfile env variable is
71 * in.
72 */
73static char *get_bootfile_path(void)
74{
75 char *bootfile, *bootfile_path, *last_slash;
76 size_t path_len;
77
78 bootfile = from_env("bootfile");
79
80 if (!bootfile)
81 return NULL;
82
83 last_slash = strrchr(bootfile, '/');
84
85 if (last_slash == NULL)
86 return NULL;
87
88 path_len = (last_slash - bootfile) + 1;
89
90 bootfile_path = malloc(path_len + 1);
91
92 if (bootfile_path == NULL) {
93 printf("out of memory\n");
94 return NULL;
95 }
96
97 strncpy(bootfile_path, bootfile, path_len);
98
99 bootfile_path[path_len] = '\0';
100
101 return bootfile_path;
102}
103
104/*
105 * As in pxelinux, paths to files in pxecfg files are relative to the location
106 * of bootfile. get_relfile takes a path from a pxecfg file and joins it with
107 * the bootfile path to get the full path to the target file.
108 */
109static int get_relfile(char *file_path, void *file_addr)
110{
111 char *bootfile_path;
112 size_t path_len;
113 char relfile[MAX_TFTP_PATH_LEN+1];
114 char addr_buf[10];
115 char *tftp_argv[] = {"tftp", NULL, NULL, NULL};
116
117 bootfile_path = get_bootfile_path();
118
119 path_len = strlen(file_path);
120
121 if (bootfile_path)
122 path_len += strlen(bootfile_path);
123
124 if (path_len > MAX_TFTP_PATH_LEN) {
125 printf("Base path too long (%s%s)\n",
126 bootfile_path ? bootfile_path : "",
127 file_path);
128
129 if (bootfile_path)
130 free(bootfile_path);
131
132 return -ENAMETOOLONG;
133 }
134
135 sprintf(relfile, "%s%s",
136 bootfile_path ? bootfile_path : "",
137 file_path);
138
139 if (bootfile_path)
140 free(bootfile_path);
141
142 printf("Retreiving file: %s\n", relfile);
143
144 sprintf(addr_buf, "%p", file_addr);
145
146 tftp_argv[1] = addr_buf;
147 tftp_argv[2] = relfile;
148
149 if (do_tftpb(NULL, 0, 3, tftp_argv)) {
150 printf("File not found\n");
151 return -ENOENT;
152 }
153
154 return 1;
155}
156
157static int get_pxecfg_file(char *file_path, void *file_addr)
158{
159 unsigned long config_file_size;
160 int err;
161
162 err = get_relfile(file_path, file_addr);
163
164 if (err < 0)
165 return err;
166
167 config_file_size = simple_strtoul(getenv("filesize"), NULL, 16);
168 *(char *)(file_addr + config_file_size) = '\0';
169
170 return 1;
171}
172
173static int get_pxelinux_path(char *file, void *pxecfg_addr)
174{
175 size_t base_len = strlen("pxelinux.cfg/");
176 char path[MAX_TFTP_PATH_LEN+1];
177
178 if (base_len + strlen(file) > MAX_TFTP_PATH_LEN) {
179 printf("path too long, skipping\n");
180 return -ENAMETOOLONG;
181 }
182
183 sprintf(path, "pxelinux.cfg/%s", file);
184
185 return get_pxecfg_file(path, pxecfg_addr);
186}
187
188static int pxecfg_uuid_path(void *pxecfg_addr)
189{
190 char *uuid_str;
191
192 uuid_str = from_env("pxeuuid");
193
194 if (!uuid_str)
195 return -ENOENT;
196
197 return get_pxelinux_path(uuid_str, pxecfg_addr);
198}
199
200static int pxecfg_mac_path(void *pxecfg_addr)
201{
202 char *mac_str = NULL;
203
204 format_mac_pxecfg(&mac_str);
205
206 if (!mac_str)
207 return -ENOENT;
208
209 return get_pxelinux_path(mac_str, pxecfg_addr);
210}
211
212static int pxecfg_ipaddr_paths(void *pxecfg_addr)
213{
214 char ip_addr[9];
215 int mask_pos, err;
216
217 sprintf(ip_addr, "%08X", ntohl(NetOurIP));
218
219 for (mask_pos = 7; mask_pos >= 0; mask_pos--) {
220 err = get_pxelinux_path(ip_addr, pxecfg_addr);
221
222 if (err > 0)
223 return err;
224
225 ip_addr[mask_pos] = '\0';
226 }
227
228 return -ENOENT;
229}
230
231/*
232 * Follows pxelinux's rules to download a pxecfg file from a tftp server. The
233 * file is stored at the location given by the pxecfg_addr environment
234 * variable, which must be set.
235 *
236 * UUID comes from pxeuuid env variable, if defined
237 * MAC addr comes from ethaddr env variable, if defined
238 * IP
239 *
240 * see http://syslinux.zytor.com/wiki/index.php/PXELINUX
241 */
242static int get_pxecfg(int argc, char * const argv[])
243{
244 char *pxecfg_ram;
245 void *pxecfg_addr;
246
247 pxecfg_ram = from_env("pxecfg_ram");
248
249 if (!pxecfg_ram)
250 return 1;
251
252 pxecfg_addr = (void *)simple_strtoul(pxecfg_ram, NULL, 16);
253
254 if (pxecfg_uuid_path(pxecfg_addr) > 0
255 || pxecfg_mac_path(pxecfg_addr) > 0
256 || pxecfg_ipaddr_paths(pxecfg_addr) > 0
257 || get_pxelinux_path("default", pxecfg_addr) > 0) {
258
259 printf("Config file found\n");
260
261 return 1;
262 }
263
264 printf("Config file not found\n");
265
266 return 0;
267}
268
269static int get_relfile_envaddr(char *file_path, char *envaddr_name)
270{
271 void *file_addr;
272 char *envaddr;
273
274 envaddr = from_env(envaddr_name);
275
276 if (!envaddr)
277 return -ENOENT;
278
279 file_addr = (void *)simple_strtoul(envaddr, NULL, 16);
280
281 return get_relfile(file_path, file_addr);
282}
283
284struct pxecfg_label {
285 char *name;
286 char *kernel;
287 char *append;
288 char *initrd;
289 int attempted;
290 int localboot;
291 struct list_head list;
292};
293
294struct pxecfg {
295 char *title;
296 char *default_label;
297 int timeout;
298 int prompt;
299 struct list_head labels;
300};
301
302struct pxecfg_label *label_create(void)
303{
304 struct pxecfg_label *label;
305
306 label = malloc(sizeof *label);
307
308 if (!label)
309 return NULL;
310
311 label->name = NULL;
312 label->kernel = NULL;
313 label->append = NULL;
314 label->initrd = NULL;
315 label->localboot = 0;
316 label->attempted = 0;
317
318 return label;
319}
320
321static void label_destroy(struct pxecfg_label *label)
322{
323 if (label->name)
324 free(label->name);
325
326 if (label->kernel)
327 free(label->kernel);
328
329 if (label->append)
330 free(label->append);
331
332 if (label->initrd)
333 free(label->initrd);
334
335 free(label);
336}
337
338static void label_print(void *data)
339{
340 struct pxecfg_label *label = data;
341
342 printf("Label: %s\n", label->name);
343
344 if (label->kernel)
345 printf("\tkernel: %s\n", label->kernel);
346
347 if (label->append)
348 printf("\tappend: %s\n", label->append);
349
350 if (label->initrd)
351 printf("\tinitrd: %s\n", label->initrd);
352}
353
354static int label_localboot(struct pxecfg_label *label)
355{
356 char *localcmd, *dupcmd;
357 int ret;
358
359 localcmd = from_env("localcmd");
360
361 if (!localcmd)
362 return -ENOENT;
363
364 /*
365 * dup the command to avoid any issues with the version of it existing
366 * in the environment changing during the execution of the command.
367 */
368 dupcmd = strdup(localcmd);
369
370 if (!dupcmd) {
371 printf("out of memory\n");
372 return -ENOMEM;
373 }
374
375 if (label->append)
376 setenv("bootargs", label->append);
377
378 printf("running: %s\n", dupcmd);
379
380 ret = run_command2(dupcmd, 0);
381
382 free(dupcmd);
383
384 return ret;
385}
386
387/*
388 * Do what it takes to boot a chosen label.
389 *
390 * Retreive the kernel and initrd, and prepare bootargs.
391 */
392static void label_boot(struct pxecfg_label *label)
393{
394 char *bootm_argv[] = { "bootm", NULL, NULL, NULL, NULL };
395
396 label_print(label);
397
398 label->attempted = 1;
399
400 if (label->localboot) {
401 label_localboot(label);
402 return;
403 }
404
405 if (label->kernel == NULL) {
406 printf("No kernel given, skipping label\n");
407 return;
408 }
409
410 if (label->initrd) {
411 if (get_relfile_envaddr(label->initrd, "initrd_ram") < 0) {
412 printf("Skipping label\n");
413 return;
414 }
415
416 bootm_argv[2] = getenv("initrd_ram");
417 } else {
418 bootm_argv[2] = "-";
419 }
420
421 if (get_relfile_envaddr(label->kernel, "kernel_ram") < 0) {
422 printf("Skipping label\n");
423 return;
424 }
425
426 if (label->append)
427 setenv("bootargs", label->append);
428
429 bootm_argv[1] = getenv("kernel_ram");
430
431 /*
432 * fdt usage is optional - if unset, this stays NULL.
433 */
434 bootm_argv[3] = getenv("fdtaddr");
435
Ricardo Salveti de Araujo2f84e8e2011-07-11 01:37:45 -0300436 if (bootm_argv[3])
437 do_bootm(NULL, 0, 4, bootm_argv);
438 else
439 do_bootm(NULL, 0, 3, bootm_argv);
Jason Hobbsafc8cd92011-06-29 11:25:18 -0500440}
441
442enum token_type {
443 T_EOL,
444 T_STRING,
445 T_EOF,
446 T_MENU,
447 T_TITLE,
448 T_TIMEOUT,
449 T_LABEL,
450 T_KERNEL,
451 T_APPEND,
452 T_INITRD,
453 T_LOCALBOOT,
454 T_DEFAULT,
455 T_PROMPT,
456 T_INCLUDE,
457 T_INVALID
458};
459
460struct token {
461 char *val;
462 enum token_type type;
463};
464
465enum lex_state {
466 L_NORMAL = 0,
467 L_KEYWORD,
468 L_SLITERAL
469};
470
471static const struct token keywords[] = {
472 {"menu", T_MENU},
473 {"title", T_TITLE},
474 {"timeout", T_TIMEOUT},
475 {"default", T_DEFAULT},
476 {"prompt", T_PROMPT},
477 {"label", T_LABEL},
478 {"kernel", T_KERNEL},
479 {"localboot", T_LOCALBOOT},
480 {"append", T_APPEND},
481 {"initrd", T_INITRD},
482 {"include", T_INCLUDE},
483 {NULL, T_INVALID}
484};
485
486static char *get_string(char **p, struct token *t, char delim, int lower)
487{
488 char *b, *e;
489 size_t len, i;
490
491 b = e = *p;
492
493 while (*e) {
494 if ((delim == ' ' && isspace(*e)) || delim == *e)
495 break;
496 e++;
497 }
498
499 len = e - b;
500
501 t->val = malloc(len + 1);
502 if (!t->val) {
503 printf("out of memory\n");
504 return NULL;
505 }
506
507 for (i = 0; i < len; i++, b++) {
508 if (lower)
509 t->val[i] = tolower(*b);
510 else
511 t->val[i] = *b;
512 }
513
514 t->val[len] = '\0';
515
516 *p = e;
517
518 t->type = T_STRING;
519
520 return t->val;
521}
522
523static void get_keyword(struct token *t)
524{
525 int i;
526
527 for (i = 0; keywords[i].val; i++) {
528 if (!strcmp(t->val, keywords[i].val)) {
529 t->type = keywords[i].type;
530 break;
531 }
532 }
533}
534
535static void get_token(char **p, struct token *t, enum lex_state state)
536{
537 char *c = *p;
538
539 t->type = T_INVALID;
540
541 /* eat non EOL whitespace */
542 while (*c == ' ' || *c == '\t')
543 c++;
544
545 /* eat comments */
546 if (*c == '#') {
547 while (*c && *c != '\n')
548 c++;
549 }
550
551 if (*c == '\n') {
552 t->type = T_EOL;
553 c++;
554 } else if (*c == '\0') {
555 t->type = T_EOF;
556 c++;
557 } else if (state == L_SLITERAL) {
558 get_string(&c, t, '\n', 0);
559 } else if (state == L_KEYWORD) {
560 get_string(&c, t, ' ', 1);
561 get_keyword(t);
562 }
563
564 *p = c;
565}
566
567static void eol_or_eof(char **c)
568{
569 while (**c && **c != '\n')
570 (*c)++;
571}
572
573static int parse_sliteral(char **c, char **dst)
574{
575 struct token t;
576 char *s = *c;
577
578 get_token(c, &t, L_SLITERAL);
579
580 if (t.type != T_STRING) {
581 printf("Expected string literal: %.*s\n", (int)(*c - s), s);
582 return -EINVAL;
583 }
584
585 *dst = t.val;
586
587 return 1;
588}
589
590static int parse_integer(char **c, int *dst)
591{
592 struct token t;
593 char *s = *c;
594
595 get_token(c, &t, L_SLITERAL);
596
597 if (t.type != T_STRING) {
598 printf("Expected string: %.*s\n", (int)(*c - s), s);
599 return -EINVAL;
600 }
601
602 *dst = (int)simple_strtoul(t.val, NULL, 10);
603
604 free(t.val);
605
606 return 1;
607}
608
609static int parse_pxecfg_top(char *p, struct pxecfg *cfg, int nest_level);
610
611static int handle_include(char **c, char *base,
612 struct pxecfg *cfg, int nest_level)
613{
614 char *include_path;
615 int err;
616
617 err = parse_sliteral(c, &include_path);
618
619 if (err < 0) {
620 printf("Expected include path\n");
621 return err;
622 }
623
624 err = get_pxecfg_file(include_path, base);
625
626 if (err < 0) {
627 printf("Couldn't get %s\n", include_path);
628 return err;
629 }
630
631 return parse_pxecfg_top(base, cfg, nest_level);
632}
633
634static int parse_menu(char **c, struct pxecfg *cfg, char *b, int nest_level)
635{
636 struct token t;
637 char *s = *c;
638 int err;
639
640 get_token(c, &t, L_KEYWORD);
641
642 switch (t.type) {
643 case T_TITLE:
644 err = parse_sliteral(c, &cfg->title);
645
646 break;
647
648 case T_INCLUDE:
649 err = handle_include(c, b + strlen(b) + 1, cfg,
650 nest_level + 1);
651 break;
652
653 default:
654 printf("Ignoring malformed menu command: %.*s\n",
655 (int)(*c - s), s);
656 }
657
658 if (err < 0)
659 return err;
660
661 eol_or_eof(c);
662
663 return 1;
664}
665
666static int parse_label_menu(char **c, struct pxecfg *cfg,
667 struct pxecfg_label *label)
668{
669 struct token t;
670 char *s;
671
672 s = *c;
673
674 get_token(c, &t, L_KEYWORD);
675
676 switch (t.type) {
677 case T_DEFAULT:
678 if (cfg->default_label)
679 free(cfg->default_label);
680
681 cfg->default_label = strdup(label->name);
682
683 if (!cfg->default_label)
684 return -ENOMEM;
685
686 break;
687 default:
688 printf("Ignoring malformed menu command: %.*s\n",
689 (int)(*c - s), s);
690 }
691
692 eol_or_eof(c);
693
694 return 0;
695}
696
697static int parse_label(char **c, struct pxecfg *cfg)
698{
699 struct token t;
700 char *s;
701 struct pxecfg_label *label;
702 int err;
703
704 label = label_create();
705 if (!label)
706 return -ENOMEM;
707
708 err = parse_sliteral(c, &label->name);
709 if (err < 0) {
710 printf("Expected label name\n");
711 label_destroy(label);
712 return -EINVAL;
713 }
714
715 list_add_tail(&label->list, &cfg->labels);
716
717 while (1) {
718 s = *c;
719 get_token(c, &t, L_KEYWORD);
720
721 err = 0;
722 switch (t.type) {
723 case T_MENU:
724 err = parse_label_menu(c, cfg, label);
725 break;
726
727 case T_KERNEL:
728 err = parse_sliteral(c, &label->kernel);
729 break;
730
731 case T_APPEND:
732 err = parse_sliteral(c, &label->append);
733 break;
734
735 case T_INITRD:
736 err = parse_sliteral(c, &label->initrd);
737 break;
738
739 case T_LOCALBOOT:
740 err = parse_integer(c, &label->localboot);
741 break;
742
743 case T_EOL:
744 break;
745
746 /*
747 * A label ends when we either get to the end of a file, or
748 * get some input we otherwise don't have a handler defined
749 * for.
750 */
751 default:
752 /* put it back */
753 *c = s;
754 return 1;
755 }
756
757 if (err < 0)
758 return err;
759 }
760}
761
762#define MAX_NEST_LEVEL 16
763
764static int parse_pxecfg_top(char *p, struct pxecfg *cfg, int nest_level)
765{
766 struct token t;
767 char *s, *b, *label_name;
768 int err;
769
770 b = p;
771
772 if (nest_level > MAX_NEST_LEVEL) {
773 printf("Maximum nesting exceeded\n");
774 return -EMLINK;
775 }
776
777 while (1) {
778 s = p;
779
780 get_token(&p, &t, L_KEYWORD);
781
782 err = 0;
783 switch (t.type) {
784 case T_MENU:
785 err = parse_menu(&p, cfg, b, nest_level);
786 break;
787
788 case T_TIMEOUT:
789 err = parse_integer(&p, &cfg->timeout);
790 break;
791
792 case T_LABEL:
793 err = parse_label(&p, cfg);
794 break;
795
796 case T_DEFAULT:
797 err = parse_sliteral(&p, &label_name);
798
799 if (label_name) {
800 if (cfg->default_label)
801 free(cfg->default_label);
802
803 cfg->default_label = label_name;
804 }
805
806 break;
807
808 case T_PROMPT:
809 err = parse_integer(&p, &cfg->prompt);
810 break;
811
812 case T_EOL:
813 break;
814
815 case T_EOF:
816 return 1;
817
818 default:
819 printf("Ignoring unknown command: %.*s\n",
820 (int)(p - s), s);
821 eol_or_eof(&p);
822 }
823
824 if (err < 0)
825 return err;
826 }
827}
828
829static void destroy_pxecfg(struct pxecfg *cfg)
830{
831 struct list_head *pos, *n;
832 struct pxecfg_label *label;
833
834 if (cfg->title)
835 free(cfg->title);
836
837 if (cfg->default_label)
838 free(cfg->default_label);
839
840 list_for_each_safe(pos, n, &cfg->labels) {
841 label = list_entry(pos, struct pxecfg_label, list);
842
843 label_destroy(label);
844 }
845
846 free(cfg);
847}
848
849static struct pxecfg *parse_pxecfg(char *menucfg)
850{
851 struct pxecfg *cfg;
852
853 cfg = malloc(sizeof(*cfg));
854
855 if (!cfg) {
856 printf("Out of memory\n");
857 return NULL;
858 }
859
860 cfg->timeout = 0;
861 cfg->prompt = 0;
862 cfg->default_label = NULL;
863 cfg->title = NULL;
864
865 INIT_LIST_HEAD(&cfg->labels);
866
867 if (parse_pxecfg_top(menucfg, cfg, 1) < 0) {
868 destroy_pxecfg(cfg);
869 return NULL;
870 }
871
872 return cfg;
873}
874
875static struct menu *pxecfg_to_menu(struct pxecfg *cfg)
876{
877 struct pxecfg_label *label;
878 struct list_head *pos;
879 struct menu *m;
880 int err;
881
882 m = menu_create(cfg->title, cfg->timeout, cfg->prompt, label_print);
883
884 if (!m)
885 return NULL;
886
887 list_for_each(pos, &cfg->labels) {
888 label = list_entry(pos, struct pxecfg_label, list);
889
890 if (menu_item_add(m, label->name, label) != 1) {
891 menu_destroy(m);
892 return NULL;
893 }
894 }
895
896 if (cfg->default_label) {
897 err = menu_default_set(m, cfg->default_label);
898 if (err != 1) {
899 if (err != -ENOENT) {
900 menu_destroy(m);
901 return NULL;
902 }
903
904 printf("Missing default: %s\n", cfg->default_label);
905 }
906 }
907
908 return m;
909}
910
911static void boot_unattempted_labels(struct pxecfg *cfg)
912{
913 struct list_head *pos;
914 struct pxecfg_label *label;
915
916 list_for_each(pos, &cfg->labels) {
917 label = list_entry(pos, struct pxecfg_label, list);
918
919 if (!label->attempted)
920 label_boot(label);
921 }
922}
923
924static void handle_pxecfg(struct pxecfg *cfg)
925{
926 void *choice;
927 struct menu *m;
928
929 m = pxecfg_to_menu(cfg);
930 if (!m)
931 return;
932
933 if (menu_get_choice(m, &choice) == 1)
934 label_boot(choice);
935
936 menu_destroy(m);
937
938 boot_unattempted_labels(cfg);
939}
940
941static int boot_pxecfg(int argc, char * const argv[])
942{
943 unsigned long pxecfg_addr;
944 struct pxecfg *cfg;
945 char *pxecfg_ram;
946
947 if (argc == 2) {
948 pxecfg_ram = from_env("pxecfg_ram");
949 if (!pxecfg_ram)
950 return 1;
951
952 pxecfg_addr = simple_strtoul(pxecfg_ram, NULL, 16);
953 } else if (argc == 3) {
954 pxecfg_addr = simple_strtoul(argv[2], NULL, 16);
955 } else {
956 printf("Invalid number of arguments\n");
957 return 1;
958 }
959
960 cfg = parse_pxecfg((char *)(pxecfg_addr));
961
962 if (cfg == NULL) {
963 printf("Error parsing config file\n");
964 return 1;
965 }
966
967 handle_pxecfg(cfg);
968
969 destroy_pxecfg(cfg);
970
971 return 0;
972}
973
974int do_pxecfg(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
975{
976 if (argc < 2) {
977 printf("pxecfg requires at least one argument\n");
978 return EINVAL;
979 }
980
981 if (!strcmp(argv[1], "get"))
982 return get_pxecfg(argc, argv);
983
984 if (!strcmp(argv[1], "boot"))
985 return boot_pxecfg(argc, argv);
986
987 printf("Invalid pxecfg command: %s\n", argv[1]);
988
989 return EINVAL;
990}
991
992U_BOOT_CMD(
993 pxecfg, 2, 1, do_pxecfg,
994 "commands to get and boot from pxecfg files",
995 "get - try to retrieve a pxecfg file using tftp\npxecfg "
996 "boot [pxecfg_addr] - boot from the pxecfg file at pxecfg_addr\n"
997);