1 #!/usr/bin/perl
2 # $Id$
3 #*********************************************************************
4 #
5 # ldap2fai -- read FAI config from LDAP and create config space
6 #
7 # This script is part of FAI (Fully Automatic Installation)
8 # (c) 2005, Thomas Lange <lange@informatik.uni-koeln.de>
9 # (c) 2005, Jens Nitschke <jens.nitschke@2int.de>
10 # (c) 2005, Jan-Marek Glogowski <glogow@fbihome.de>
11 # (c) 2005, Cajus Pollmeier <pollmeier@gonicus.de>
12 #
13 #*********************************************************************
15 use strict;
16 use Net::LDAP;
17 use MIME::Base64;
18 use Getopt::Std;
19 use File::Path;
20 use File::Copy;
21 use vars qw/ %opt /;
23 my $base;
24 my $ldapuri;
25 my $ldapdir = "/etc/ldap/ldap.conf";
26 my $outdir = "/fai";
27 my $verbose = 0;
28 my $opt_string = 'c:d:hv';
29 my $hostname;
31 getopts( "$opt_string", \%opt ) or usage("Hello");
32 usage("Help") if $opt{h};
34 $verbose = $opt{v} ? 1 : 0;
35 $outdir = $opt{d} ? $opt{d} : $outdir;
36 $ldapdir = $opt{c} ? $opt{c} : $ldapdir;
38 # Get MAC from cmdline
39 my $mac = shift @ARGV;
40 $mac eq '' && usage("MAC address not specified.");
42 # Is outdir a directory
43 -d "$outdir" || usage("'$outdir' is not a directory.\n");
45 my @classes=(); # the classes a host belongs to
47 # initialize ldap
48 setup();
49 my $ldap = Net::LDAP->new("$ldapuri") or die "$@";
50 my $mesg = $ldap->bind;
52 # create class hooks debconf disk_config package_config scripts files
53 my @dirs= qw/class hooks debconf disk_config package_config scripts files/;
54 foreach (@dirs) {
55 -d "$outdir/$_" || mkpath "$outdir/$_"
56 || warn "WARNING: Can't create subdir $outdir/$_ $!\n";
57 }
59 @classes= get_classes($mac);
60 prt_scripts();
61 prt_package_list();
62 prt_debconf();
63 prt_templates();
64 prt_var();
65 prt_hooks();
66 prt_disk_config();
68 # create sources list
69 if (!$hostname) {
70 -d "${outdir}/files/etc/apt/sources.list"
71 || mkpath "${outdir}/files/etc/apt/sources.list";
72 copy ("${outdir}/tmp/apt-sources.list",
73 "${outdir}/files/etc/apt/sources.list/$hostname") ;
74 }
76 $mesg = $ldap->unbind; # take down session
77 exit 0;
79 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
80 sub setup
81 {
82 # Read LDAP
83 open (LDAPCONF,"${ldapdir}")
84 || usage("Can't open LDAP configuration$!\n");
85 my @content=<LDAPCONF>;
86 close(LDAPCONF);
88 # Scan LDAP config
89 foreach my $line (@content) {
90 $line =~ /^\s*(#|$)/ && next;
91 chomp($line);
93 if ($line =~ /^BASE\s+(.*)$/) {
94 $base= $1;
95 next;
96 }
97 if ($line =~ m#^URI\s+ldaps?://([^/:]+).*$#) {
98 $ldapuri= $1;
99 next;
100 }
101 }
102 }
104 sub usage
105 {
106 (@_) && print STDERR "\n@_\n\n";
108 print STDERR << "EOF";
109 usage: $0 [-hv] [-c config] [-d outdir] <MAC>
111 -h : this (help) message
112 -c : LDAP config file (default: ${ldapdir})
113 -d : output dir (default: ${outdir})
114 -v : be verbose
115 EOF
116 exit -1;
117 }
118 #-----------------------------------------------------------------------------------
120 sub write_file {
122 my @opts = @_;
123 my $len = scalar @_;
124 ($len < 2) && return;
126 my $filename = shift;
127 my $data = shift;
129 open (SCRIPT,">${filename}") || warn "Can't create ${filename}. $!\n";
130 print SCRIPT $data;
131 close(SCRIPT);
133 ($opts[2] ne "") && chmod oct($opts[2]),${filename};
134 ($opts[3] ne "") && chown_files(${filename}, $opts[3]);
135 }
137 #-----------------------------------------------------------------------------------
139 sub chown_files
140 {
141 my @owner = split('.',@_[1]);
142 my $filename = @_[0];
143 my ($uid,$gid);
144 $uid = getpwnam(@owner[0]);
145 $gid = getgrnam(@owner[1]);
147 chown $uid, $gid, $filename;
148 }
150 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
151 sub get_classes {
153 # return list of FAI classes defined for host
154 my $mac = shift;
155 my (@classes,$mesg,$entry);
157 $mesg = $ldap->search(
158 base => "ou=systems,$base",
159 filter => "(&(macAddress=$mac)(objectClass=gotoWorkstation))",
160 attrs => [ 'FAIclass', 'cn']);
161 $mesg->code && die $mesg->error;
162 # normally, only one value should be returned
163 if ($mesg->count != 1) {
164 die "LDAP search for client failed. ".$mesg->count." entries have been returned\n";
165 }
167 # this assigns the last value to @classes
168 $entry= ($mesg->entries)[0];
169 @classes= split /\s+/,$entry->get_value('FAIclass');
171 # get hostname
172 my $hname= $entry->get_value('cn');
173 my $dn= $entry->dn;
174 $hostname= $hname;
176 # Search for object groups containing this client
177 $mesg = $ldap->search(
178 base => "ou=groups,$base",
179 filter => "(&(objectClass=gosaGroupOfNames)(objectClass=FAIobject)(member=$dn))",
180 attrs => [ 'FAIclass' ]);
181 $mesg->code && die $mesg->error;
182 foreach my $m ($mesg->entries) {
183 push @classes, split /\s+/,$m->get_value('FAIclass');
184 }
186 # print all classes to the file with hostname
187 open (FAICLASS,">$outdir/class/$hname") || warn "Can't create $outdir/class/$hname. $!\n";
188 my @newclasses;
189 foreach my $class (@classes) {
191 # We need to walk through the list of classes and watch out for
192 # a profile which is named like the class. Replace the profile
193 # name by the names of the included classes.
194 $mesg = $ldap->search(
195 base => "ou=systems,$base",
196 filter => "(&(objectClass=FAIprofile)(cn=$class))",
197 attrs => [ 'FAIclass' ]);
198 $mesg->code && die $mesg->error;
200 if ($mesg->count > 0){
201 foreach my $m ($mesg->entries) {
202 foreach my $tc (split /\s+/,$m->get_value('FAIclass')){
203 print FAICLASS "$tc\n";
204 push @newclasses, $tc;
205 }
206 }
207 } else {
208 print FAICLASS "$class\n";
209 push @newclasses, $class;
210 }
211 }
212 close(FAICLASS);
213 print "Host $hname belongs to FAI classes: ",join ' ',@newclasses,"\n" if $verbose;
214 return @newclasses;
215 }
217 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
218 sub get_variables {
219 # gets all variables defined for a class
220 # returns a list of lines in bourne shell syntax
222 my $class = shift;
223 my ($mesg,$var_base,$entry,$line,@vars);
225 $mesg = $ldap->search(
226 base => "$base",
227 filter => "(&(cn=$class)(objectClass=FAIvariable))",
228 attrs => [ 'cn']);
229 return if ($mesg->count() == 0); # skip if no such object exists
230 $mesg->code && die $mesg->error;
232 $entry=($mesg->entries)[0];
233 $var_base=$entry->dn;
235 $mesg = $ldap->search(
236 base => "$var_base",
237 filter => "(objectClass=FAIvariableEntry)",
238 attrs => ['cn', 'FAIvariableContent']);
239 return if ($mesg->count() == 0); # skip if no such object exists
240 $mesg->code && die $mesg->error;
243 foreach $entry ($mesg->entries) {
244 $line= sprintf "%s=\'%s\'\n", $entry->get_value('cn'),
245 $entry->get_value('FAIvariableContent');
246 push @vars,$line;
247 }
248 return @vars;
249 }
251 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
252 sub prt_var {
254 my (@lines, $hname);
256 foreach my $class (@classes) {
257 @lines = get_variables($class);
258 next until @lines; # do not create .var file if no variables are defined
259 open (FAIVAR,">$outdir/class/${class}.var")
260 || warn "Can't create $outdir/class/$hname.var.$!\n";
261 print FAIVAR @lines;
262 close(FAIVAR);
263 }
264 }
266 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
267 sub get_disk_config {
269 my $class = shift;
270 my ($mesg,$entry,$line,@diskconfig,$partition_base,$dn,%diskline,$xxmesg);
272 # Search for partition schema for the specified class
273 $mesg = $ldap->search(
274 base => "$base",
275 filter => "(&(cn=$class)(objectClass=FAIpartitionTable))" );
277 return if ($mesg->count() == 0); # skip if no such object exists
278 $mesg->code && die $mesg->error;
280 $entry=($mesg->entries)[0];
281 $partition_base= $entry->dn;
283 # Search for disks
284 $mesg = $ldap->search(
285 base => "$partition_base",
286 filter => "(objectClass=FAIpartitionDisk)" );
288 return if ($mesg->code == 32); # skip if no such object exists
289 $mesg->code && die $mesg->error;
291 foreach $entry ($mesg->entries) {
292 my $logic_count= 4;
293 my $primary_count= 0;
294 my $dn=$entry->dn;
295 my $disk=$entry->get_value('cn');
296 my $part;
297 undef %diskline;
298 $diskline{0} = "disk_config $disk\n";
299 $xxmesg = $ldap->search(
300 base => "$dn",
301 filter => "objectClass=FAIpartitionEntry" );
302 $xxmesg->code && die $xxmesg->error;
303 foreach my $dl ($xxmesg->entries) {
304 if ($dl->get_value('FAIpartitionType') eq 'primary'){
305 $primary_count++;
306 } else {
307 $logic_count++;
308 }
309 if ($dl->get_value('FAIpartitionFlags') eq 'preserve'){
310 if ($dl->get_value('FAIpartitionType') eq 'primary'){
311 $part= 'preserve'.$primary_count;
312 } else {
313 $part= 'preserve'.$logic_count;
314 }
315 $line= sprintf "%-7s %-12s %-12s %-10s ; %s\n",
316 $dl->get_value('FAIpartitionType'),
317 $dl->get_value('FAImountPoint'),
318 $part,
319 $dl->get_value('FAImountOptions') eq ''
320 ? 'rw' : $dl->get_value('FAImountOptions'),
321 $dl->get_value('FAIfsOptions');
322 }
323 elsif ($dl->get_value('FAIfsType') eq 'swap'){
324 $line= sprintf "%-7s %-12s %-12s %-10s\n",
325 $dl->get_value('FAIpartitionType'),
326 $dl->get_value('FAImountPoint'),
327 $dl->get_value('FAIpartitionSize'),
328 $dl->get_value('FAImountOptions') eq ''
329 ? 'rw' : $dl->get_value('FAImountOptions');
330 }
331 else {
332 $line= sprintf "%-7s %-12s %-12s %-10s ; %s %s\n",
333 $dl->get_value('FAIpartitionType'),
334 $dl->get_value('FAImountPoint'),
335 $dl->get_value('FAIpartitionSize'),
336 $dl->get_value('FAImountOptions') eq ''
337 ? 'rw' : $dl->get_value('FAImountOptions'),
338 $dl->get_value('FAIfsOptions'),
339 $dl->get_value('FAIfsType');
340 }
342 $diskline{$dl->get_value('FAIpartitionNr')}=$line;
343 }
344 foreach my $l (sort {$a <=> $b} keys %diskline) {
345 push @diskconfig, $diskline{$l};
346 }
347 }
348 return @diskconfig;
349 }
351 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
352 sub prt_disk_config {
354 # create one disk_config file
356 my ($class,@lines);
358 foreach $class (reverse @classes) {
359 @lines=get_disk_config($class);
360 next until @lines; # skip if nothing is defined for this class
362 print "Generating partition layout for class '${class}'\n." if $verbose;
363 open (FAIVAR,">${outdir}/disk_config/${class}")
364 || warn "Can't create $outdir/disk_config/$class. $!\n";
365 print FAIVAR join '',@lines;
366 close(FAIVAR);
367 last; # finish when one config file is created
368 }
369 }
371 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
372 sub get_packages {
374 # gets list of packages defined for a class
376 my $class = shift;
377 my ($mesg,$entry,$line,$method,%packlist);
379 -d "${outdir}/tmp" || mkpath "${outdir}/tmp"
380 || warn "Can't create ${outdir}/tmp. $!\n";
381 print "Generate sources.list for install\n" if $verbose;
382 open (SOURCES,">>${outdir}/tmp/apt-sources.list")
383 || warn "Can't create ${outdir}/tmp/apt-sources.list. $!\n";
385 $mesg = $ldap->search(
386 base => "$base",
387 filter => "(&(cn=$class)(objectClass=FAIpackageList))" ,
388 attrs => [ 'FAIpackage', 'FAIinstallMethod',
389 'FAIdebianMirror', 'FAIdebianRelease', 'FAIdebianSection']);
391 $mesg->code && die $mesg->error;
392 # should also return only one value
394 undef %packlist;
395 foreach $entry ($mesg->entries) {
396 $method=$entry->get_value('FAIinstallMethod');
397 push @{$packlist{$method}}, $entry->get_value('FAIpackage');
399 print SOURCES "deb ".$entry->get_value('FAIdebianMirror')." ".$entry->get_value('FAIdebianRelease')." ";
400 my $section;
401 foreach $section ($entry->get_value('FAIdebianSection')){
402 print SOURCES "$section ";
403 }
404 print SOURCES "\n";
405 }
407 close (SOURCES);
409 # return a ref to the hash of arrays (key of the hash is the method),
410 # the value is the array of package names for this method
411 return \%packlist;
412 }
414 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
415 sub prt_package_list {
417 my (@lines,$plist,$method,$value);
419 foreach my $class (@classes) {
420 $plist=get_packages($class);
421 # test if hash contains any keys or values
422 unless (keys %{$plist}) {
423 next;
424 }
426 print "Generate package list for class '$class'.\n" if $verbose;
427 open (PACKAGES,">$outdir/package_config/$class")
428 || warn "Can't create $outdir/package_config/$class. $!\n";
429 while (($method, $value) = each %{$plist}) {
430 print PACKAGES "PACKAGES $method\n";
431 print PACKAGES join "\n",@{$value};
432 print PACKAGES "\n";
433 }
434 close(PACKAGES);
435 }
436 }
438 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
439 sub get_templates {
441 # get list of template-files defined for a class
442 my $class = shift;
443 my ($mesg,$entry,$str,$pfad,$name,$owner,$mode,$template_base,@template);
445 $mesg = $ldap->search(
446 base => "$base",
447 filter => "(&(cn=$class)(objectClass=FAItemplate))",
448 attrs => ['cn']);
449 return if ($mesg->count() == 0); # skip if no such object exists
450 $mesg->code && die $mesg->error;
452 $entry=($mesg->entries)[0];
453 $template_base=$entry->dn;
455 $mesg = $ldap->search(
456 base => "$template_base",
457 filter => "(objectClass=FAItemplateEntry)",
458 attrs => ['FAItemplateFile', 'FAItemplatePath', 'FAIowner', 'FAImode' ,'cn']);
459 return if ($mesg->count() == 0); # skip if no such object exists
460 $mesg->code && die $mesg->error;
462 foreach $entry ($mesg->entries) {
463 $name = $entry->get_value('cn');
464 $owner = $entry->get_value('FAIowner');
465 $owner = $entry->get_value('FAImode');
466 $pfad = $entry->get_value('FAItemplatePath');
467 chomp($pfad);
468 -d "${outdir}/files/${pfad}" || mkpath "${outdir}/files/${pfad}"
469 || warn "WARNING: Can't create subdir ${outdir}/files/${pfad} !$\n";
470 print "Generate template '$pfad' ($name) for class '$class'.\n" if $verbose;
471 write_file( "${outdir}/files/${pfad}/${class}",
472 $entry->get_value('FAItemplateFile'),$entry->get_value('FAImode'),$entry->get_value('FAIowner'));
473 }
474 }
476 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
477 sub prt_templates {
478 my ($class);
480 foreach $class (reverse @classes) {
481 get_templates($class);
482 }
483 }
485 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
486 sub get_debconf {
488 # gets list of packages defined for a class
490 my $class = shift;
491 my ($mesg,$entry,$str,$debconf_base,@debconf);
493 $mesg = $ldap->search(
494 base => "$base",
495 filter => "(&(cn=$class)(objectClass=FAIpackageList))",
496 attrs => ['cn']);
497 return if ($mesg->count() == 0); # skip if no such object exists
498 $mesg->code && die $mesg->error;
500 $entry=($mesg->entries)[0];
501 $debconf_base=$entry->dn;
503 $mesg = $ldap->search(
504 base => "$debconf_base",
505 filter => "(objectClass=FAIdebconfInfo)" ,
506 attrs => [ 'FAIpackage', 'FAIvariable',
507 'FAIvariableType','FAIvariableContent']);
508 $mesg->code && die $mesg->error;
510 # undef @debconf;
511 foreach $entry ($mesg->entries) {
512 $str = sprintf "%s %s %s %s\n",
513 $entry->get_value('FAIpackage'),
514 $entry->get_value('FAIvariable'),
515 $entry->get_value('FAIvariableType'),
516 $entry->get_value('FAIvariableContent');
517 push @debconf, $str;
518 }
519 return @debconf;
520 }
522 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
523 sub prt_debconf {
525 my @lines;
526 my $class;
528 foreach $class (@classes) {
529 @lines = get_debconf($class);
530 next until @lines;
531 print "Generate DebConf for class '$class'.\n" if $verbose;
532 open (DEBCONF,">${outdir}/debconf/${class}") || warn "Can't create $outdir/debconf/$class. $!\n";
533 print DEBCONF @lines;
534 close(DEBCONF);
535 }
536 }
538 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
539 sub prt_scripts {
540 my ($class,@lines);
542 foreach $class (@classes) {
543 get_scripts($class);
544 }
545 }
547 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
548 sub get_scripts {
550 # gets list of packages defined for a class
552 my $class = shift;
553 my ($mesg,$entry,$str,$script_base,$prio,$name,$script);
555 $mesg = $ldap->search(
556 base => "$base",
557 filter => "(&(cn=$class)(objectClass=FAIscript))",
558 attrs => ['cn']);
559 return if ($mesg->count() == 0); # skip if no such object exists
560 $mesg->code && die $mesg->error;
562 $entry=($mesg->entries)[0];
563 $script_base= $entry->dn;
565 $mesg = $ldap->search(
566 base => "$script_base",
567 filter => "(objectClass=FAIscriptEntry)",
568 attrs => ['FAIpriority', 'FAIscript', 'cn']);
569 return if ($mesg->count() == 0); # skip if no such object exists
570 $mesg->code && die $mesg->error;
572 foreach $entry ($mesg->entries) {
573 $name = $entry->get_value('cn');
574 $prio = $entry->get_value('FAIpriority');
575 $script= sprintf('%02d-%s', $prio, $name);
577 -d "$outdir/scripts/$class" || mkpath "$outdir/scripts/$class" ||
578 warn "WARNING: Can't create subdir $outdir/scripts/$class !$\n";
580 write_file("${outdir}/scripts/${class}/${script}",
581 $entry->get_value('FAIscript'), "0700");
582 }
583 }
585 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
586 sub prt_hooks {
587 my ($class,@lines);
589 foreach $class (reverse @classes) {
590 get_hooks($class);
591 }
592 }
594 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
595 sub get_hooks {
597 # gets list of packages defined for a class
599 my $class = shift;
600 my ($mesg,$entry,$str,$hook_base,$prio,$task,$hook,$name);
602 $mesg = $ldap->search(
603 base => "$base",
604 filter => "(&(cn=$class)(objectClass=FAIhook))",
605 attrs => ['cn']);
606 return if ($mesg->count() == 0); # skip if no such object exists
607 $mesg->code && die $mesg->error;
609 $entry=($mesg->entries)[0];
610 $hook_base= $entry->dn;
612 $mesg = $ldap->search(
613 base => "$hook_base",
614 filter => "(objectClass=FAIhookEntry)",
615 attrs => ['FAItask', 'FAIscript', 'cn']);
616 return if ($mesg->count() == 0); # skip if no such object exists
617 $mesg->code && die $mesg->error;
619 foreach $entry ($mesg->entries) {
620 $name = $entry->get_value('cn');
621 $task = $entry->get_value('FAItask');
622 $prio = $entry->get_value('FAIpriority');
623 $hook = sprintf('%s.%s', ${task}, ${class});
625 write_file("${outdir}/hooks/${hook}",
626 $entry->get_value('FAIscript'), "0700");
627 }
628 }
630 # vim:ts=2:sw=2:expandtab:shiftwidth=2:syntax:paste