blob: 073eee572bd317de73ec4d8554f035d100030839 [file] [log] [blame]
Greg Hartman76d05dc2016-11-23 15:51:27 -08001#!/usr/bin/perl
2#
3# Set PXELINUX hard-coded options
4#
5
6use Socket; # For gethostbyname
7use Fcntl;
8use bytes;
9
10%option_names = (
11 6 => 'domain-name-servers',
12 15 => 'domain-name',
13 54 => 'next-server',
14 209 => 'config-file',
15 210 => 'path-prefix',
16 211 => 'reboottime'
17 );
18
19@fmt_oneip = ("ip-address", \&parse_oneip, \&show_ip);
20@fmt_multiip = ("ip-address-list", \&parse_multiip, \&show_ip);
21@fmt_string = ("string", \&parse_string, \&show_string);
22@fmt_uint32 = ("uint32", \&parse_uint32, \&show_uint32);
23
24%option_format = (
25 6 => \@fmt_multiip,
26 15 => \@fmt_string,
27 54 => \@fmt_oneip,
28 67 => \@fmt_string,
29 209 => \@fmt_string,
30 210 => \@fmt_string,
31 211 => \@fmt_uint32
32 );
33
34sub parse_oneip($)
35{
36 my($s) = @_;
37 my($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($s);
38
39 return ($addrtype == AF_INET) ? $addrs[0] : undef;
40}
41
42sub parse_multiip($)
43{
44 my($l) = @_;
45 my $s;
46 my @a = ();
47 my $addr;
48 my $d = '';
49
50 foreach $s (split(/,/, $l)) {
51 my($name,$aliases,$addrtype,$length,@addrs)
52 = gethostbyname($s);
53 if ($addrtype == AF_INET) {
54 foreach $addr (@addrs) {
55 $d .= $addr;
56 }
57 }
58 }
59
60 return $d ne '' ? $d : undef;
61}
62
63sub show_ip($)
64{
65 my($l) = @_;
66
67 if (length($l) & 3) {
68 return undef;
69 } else {
70 my @h = ();
71 my $i;
72
73 for ($i = 0; $i < length($l); $i += 4) {
74 push(@h, inet_ntoa(substr($l, $i, 4)));
75 }
76
77 return join(',', @h);
78 }
79}
80
81sub parse_string($)
82{
83 return $_[0];
84}
85
86sub show_string($)
87{
88 my($s) = @_;
89 my $o, $i, $c;
90
91 $o = "\'";
92 for ($i = 0; $i < length($s); $i++) {
93 $c = substr($s, $i, 1);
94 if ($c eq "\'" || $c eq '!') {
95 $o .= "\'\\$c\'";
96 } else {
97 $o .= $c;
98 }
99 }
100 $o .= "\'";
101
102 return $o;
103}
104
105sub parse_uint32($)
106{
107 my($s) = @_;
108
109 if ($s =~ /^[0-9]+$/) {
110 return pack("N", $s);
111 } else {
112 return undef;
113 }
114}
115
116sub show_uint32($)
117{
118 my($l) = @_;
119
120 if (length($l) == 4) {
121 return unpack("N", $l);
122 } else {
123 return undef;
124 }
125}
126
127sub parse_generic($)
128{
129 my($s) = @_;
130
131 if ($s =~ /^[0-9a-f]{1,2}(:[0-9a-f]{1,2})*$/) {
132 my $h;
133 my @b = ();
134
135 foreach $h (split(/\:/, $s)) {
136 push(@b, hex $h);
137 }
138
139 return pack("C", @b);
140 } else {
141 return undef;
142 }
143}
144
145sub show_generic($)
146{
147 my($l) = @_;
148 my $i;
149 my @h;
150
151 for ($i = 0; $i < length($l); $i++) {
152 push(@h, sprintf("%02x", unpack("C", substr($l, $i, $1))));
153 }
154
155 return join(':', @h);
156}
157
158sub parse_option($$)
159{
160 my($opt, $arg) = @_;
161 my $v;
162
163 if (defined($option_format{$opt})) {
164 $v = $option_format{$opt}[1]($arg);
165 return $v if (defined($v));
166 }
167
168 return parse_generic($arg);
169}
170
171sub show_option($$)
172{
173 my($opt, $arg) = @_;
174 my $v;
175
176 if (defined($option_format{$opt})) {
177 $v = $option_format{$opt}[2]($arg);
178 return $v if (defined($v));
179 }
180
181 return show_generic($arg);
182}
183
184sub option_number($)
185{
186 my($n) = @_;
187
188 if (defined($option_rnames{$n})) {
189 return $option_rnames{$n};
190 } elsif ($n =~ /^[0-9]+$/ && $n >= 1 && $n <= 254) {
191 return $n+0;
192 } else {
193 return undef;
194 }
195}
196
197sub read_optsets($)
198{
199 my($file) = @_;
200 my $data, $bdata, $adata;
201 my $patch_start = (stat($file))[7];
202 my $hdroffset = 0; # 0 means non-deep-embedded
203 my $bufsize = 0;
204 my $junk;
205 my %hdr;
206
207 return undef unless (seek($file, 0, SEEK_SET));
208 return undef unless (read($file, $data, 48) == 48);
209
210 my($mzmagic, $junk, $magic, $len, $flags, $boff, $blen, $aoff, $alen)
211 = unpack("va[6]VVVVVVV", $data);
212
213 if ($mzmagic == 0x5a4d) {
214 # It is an EFI file... search for the magic number
215 $hdroffset = 48;
216 my $magic = pack("VVVV", 0x2a171ead, 0x0600e65e,
217 0x4025a4e4, 0x42388fc8);
218
219 while (1) {
220 return undef unless (read($file, $data, 16) == 16);
221 last if ($data eq $magic);
222
223 $hdroffset += 16;
224 }
225
226 return undef unless (read($file, $data, 16) == 16);
227 ($blen, $alen, $bufsize, $junk) = unpack("VVVV", $data);
228
229 $patch_start = $boff = $hdroffset + 32;
230 $aoff = $boff + $blen;
231
232 $hdr{'deep'} = 1;
233 $hdr{'bufsize'} = $bufsize;
234 $hdr{'hdroffset'} = $hdroffset;
235 } else {
236 # It is a BIOS PXE file
237
238 return undef if ($magic != 0x2983c8ac);
239 return undef if ($len < 7*4);
240
241 $hdr{'deep'} = 0;
242 }
243
244 if ($blen == 0) {
245 $bdata = '';
246 } else {
247 return undef unless (seek($file, $boff, SEEK_SET));
248 return undef unless (read($file, $bdata, $blen) == $blen);
249 $patch_start = $boff if ($boff < patch_start);
250 }
251
252 if ($alen == 0) {
253 $adata = '';
254 } else {
255 return undef unless (seek($file, $aoff, SEEK_SET));
256 return undef unless (read($file, $adata, $alen) == $alen);
257 $patch_start = $aoff if ($aoff < $patch_start);
258 }
259
260 $hdr{'patch_start'} = $patch_start;
261
262 return (\%hdr, $bdata, $adata);
263}
264
265sub write_optsets($$@)
266{
267 my($file, $hdr, $bdata, $adata) = @_;
268 my $boff = 0;
269 my $aoff = 0;
270 my $bufsize = 0;
271 my $patch_start = $hdr->{'patch_start'};
272 my $len;
273
274 $bdata .= "\xff" unless ($bdata eq '');
275 $adata .= "\xff" unless ($adata eq '');
276
277 $len = length($bdata) + length($adata);
278
279 if (defined($hdr->{'bufsize'})) {
280 return undef unless ($len <= $hdr->{'bufsize'});
281 }
282
283 return undef unless (seek($file, $patch_start, SEEK_SET));
284
285 if (length($bdata)) {
286 $boff = $patch_start;
287 return undef unless (print $file $bdata);
288 $patch_start += length($bdata);
289 }
290
291 if (length($adata)) {
292 $aoff = $patch_start;
293 return undef unless (print $file $adata);
294 $patch_start += length($adata);
295 }
296
297 if ($hdr->{'deep'}) {
298 return undef unless (print $file "\0" x ($hdr->{'bufsize'} - $len));
299 return undef unless (seek($file, $hdr->{'hdroffset'} + 16, SEEK_SET));
300 my $hdr = pack("VV", length($bdata), length($adata));
301 return undef unless (print $file $hdr);
302 } else {
303 my $hdr = pack("VVVV", $boff, length($bdata), $aoff, length($adata));
304
305 return undef unless (seek($file, 8+3*4, SEEK_SET));
306 return undef unless (print $file $hdr);
307
308 truncate($file, $patch_start);
309 }
310
311 return 1;
312}
313
314sub delete_option($$)
315{
316 my ($num, $block) = @_;
317 my $o, $l, $c, $x;
318
319 $x = 0;
320 while ($x < length($block)) {
321 ($o, $l) = unpack("CC", substr($block, $x, 2));
322 if ($o == $num) {
323 # Delete this option
324 substr($block, $x, $l+2) = '';
325 } elsif ($o == 0) {
326 # Delete a null option
327 substr($block, $x, 1) = '';
328 } elsif ($o == 255) {
329 # End marker - truncate block
330 $block = substr($block, 0, $x);
331 last;
332 } else {
333 # Skip to the next option
334 $x += $l+2;
335 }
336 }
337
338 return $block;
339}
340
341sub add_option($$$)
342{
343 my ($num, $data, $block) = @_;
344
345 $block = delete_option($num, $block);
346
347 if (length($data) == 0) {
348 return $block;
349 } elsif (length($data) > 255) {
350 die "$0: option $num has too much data (max 255 bytes)\n";
351 } else {
352 return $block . pack("CC", $num, length($data)) . $data;
353 }
354}
355
356sub list_options($$)
357{
358 my($pfx, $data) = @_;
359 my $x, $o, $l;
360
361 while ($x < length($data)) {
362 ($o, $l) = unpack("CC", substr($data, $x, 2));
363
364 if ($o == 0) {
365 $x++;
366 } elsif ($o == 255) {
367 last;
368 } else {
369 my $odata = substr($data, $x+2, $l);
370 last if (length($odata) != $l); # Incomplete option
371
372 printf "%s%-20s %s\n", $pfx,
373 $option_names{$o} || sprintf("%d", $o),
374 show_option($o, $odata);
375
376 $x += $l+2;
377 }
378 }
379}
380
381sub usage()
382{
383 my $i;
384
385 print STDERR "Usage: $0 options pxelinux.0\n";
386 print STDERR "Options:\n";
387 print STDERR "--before option value -b Add an option before DHCP data\n";
388 print STDERR "--after option value -a Add an option after DHCP data\n";
389 print STDERR "--delete option -d Delete an option\n";
390 print STDERR "--list -l List set options\n";
391 print STDERR "--dry-run -n Don't modify the target file\n";
392 print STDERR "--help -h Display this help text\n";
393 print STDERR "\n";
394 print STDERR "The following DHCP options are currently recognized:\n";
395 printf STDERR "%-23s %-3s %s\n", 'Name', 'Num', 'Value Format';
396
397 foreach $i (sort { $a <=> $b } keys(%option_names)) {
398 printf STDERR "%-23s %3d %s\n",
399 $option_names{$i}, $i, $option_format{$i}[0];
400 }
401}
402
403%option_rnames = ();
404foreach $opt (keys(%option_names)) {
405 $option_rnames{$option_names{$opt}} = $opt;
406}
407
408%before = ();
409%after = ();
410@clear = ();
411$usage = 0;
412$err = 0;
413$list = 0;
414$no_write = 0;
415undef $file;
416
417while (defined($opt = shift(@ARGV))) {
418 if ($opt !~ /^-/) {
419 if (defined($file)) {
420 $err = $usage = 1;
421 last;
422 }
423 $file = $opt;
424 } elsif ($opt eq '-b' || $opt eq '--before') {
425 $oname = shift(@ARGV);
426 $odata = shift(@ARGV);
427
428 if (!defined($odata)) {
429 $err = $usage = 1;
430 last;
431 }
432
433 $onum = option_number($oname);
434 if (!defined($onum)) {
435 print STDERR "$0: unknown option name: $oname\n";
436 $err = 1;
437 next;
438 }
439
440 $odata = parse_option($onum, $odata);
441 if (!defined($odata)) {
442 print STDERR "$0: unable to parse data for option $oname\n";
443 $err = 1;
444 next;
445 }
446
447 delete $after{$onum};
448 $before{$onum} = $odata;
449 push(@clear, $onum);
450 } elsif ($opt eq '-a' || $opt eq '--after') {
451 $oname = shift(@ARGV);
452 $odata = shift(@ARGV);
453
454 if (!defined($odata)) {
455 $err = $usage = 1;
456 last;
457 }
458
459 $onum = option_number($oname);
460 if (!defined($onum)) {
461 print STDERR "$0: unknown option name: $oname\n";
462 $err = 1;
463 next;
464 }
465
466 $odata = parse_option($onum, $odata);
467 if (!defined($odata)) {
468 print STDERR "$0: unable to parse data for option $oname\n";
469 $err = 1;
470 next;
471 }
472
473 delete $before{$onum};
474 $after{$onum} = $odata;
475 push(@clear, $onum);
476 } elsif ($opt eq '-d' || $opt eq '--delete') {
477 $oname = shift(@ARGV);
478
479 if (!defined($oname)) {
480 $err = $usage = 1;
481 last;
482 }
483
484 $onum = option_number($oname);
485 if (!defined($onum)) {
486 print STDERR "$0: unknown option name: $oname\n";
487 $err = 1;
488 next;
489 }
490
491 push(@clear, $onum);
492 delete $before{$onum};
493 delete $after{$onum};
494 } elsif ($opt eq '-n' || $opt eq '--no-write' || $opt eq '--dry-run') {
495 $no_write = 1;
496 } elsif ($opt eq '-l' || $opt eq '--list') {
497 $list = 1;
498 } elsif ($opt eq '-h' || $opt eq '--help') {
499 $usage = 1;
500 } else {
501 print STDERR "Invalid option: $opt\n";
502 $err = $usage = 1;
503 }
504}
505
506if (!defined($file) && !$usage) {
507 $err = $usage = 1;
508}
509if ($usage) {
510 usage();
511}
512if ($err || $usage) {
513 exit($err);
514}
515
516if (!scalar(@clear)) {
517 $no_write = 1; # No modifications requested
518}
519
520$mode = $no_write ? '<' : '+<';
521
522open(FILE, $mode, $file)
523 or die "$0: cannot open: $file: $!\n";
524($hdrinfo, @data) = read_optsets(\*FILE);
525if (!defined($hdrinfo)) {
526 die "$0: $file: patch block not found or file corrupt\n";
527}
528
529foreach $o (@clear) {
530 $data[0] = delete_option($o, $data[0]);
531 $data[1] = delete_option($o, $data[1]);
532}
533foreach $o (keys(%before)) {
534 $data[0] = add_option($o, $before{$o}, $data[0]);
535}
536foreach $o (keys(%after)) {
537 $data[1] = add_option($o, $after{$o}, $data[1]);
538}
539
540if ($list) {
541 list_options('-b ', $data[0]);
542 list_options('-a ', $data[1]);
543}
544
545if (!$no_write) {
546 if (!write_optsets(\*FILE, $hdrinfo, @data)) {
547 die "$0: $file: failed to write options: $!\n";
548 }
549}
550
551close(FILE);
552exit 0;