#!/usr/bin/perl # $Id$ #********************************************************************* # # ldap2fai -- read FAI config from LDAP and create config space # # This script is part of FAI (Fully Automatic Installation) # (c) 2005, Thomas Lange # (c) 2005, Jens Nitschke # (c) 2005, Jan-Marek Glogowski # (c) 2005, Cajus Pollmeier # #********************************************************************* use strict; use Net::LDAP; use MIME::Base64; use Getopt::Std; use File::Path; use File::Copy; use vars qw/ %opt /; my $base; my $ldapuri; my $ldapdir = "/etc/ldap/ldap.conf"; my $outdir = "/fai"; my $verbose = 0; my $opt_string = 'c:d:hv'; my $hostname; getopts( "$opt_string", \%opt ) or usage("Hello"); usage("Help") if $opt{h}; $verbose = $opt{v} ? 1 : 0; $outdir = $opt{d} ? $opt{d} : $outdir; $ldapdir = $opt{c} ? $opt{c} : $ldapdir; # Get MAC from cmdline my $mac = shift @ARGV; $mac eq '' && usage("MAC address not specified."); # Is outdir a directory -d "$outdir" || usage("'$outdir' is not a directory.\n"); my @classes=(); # the classes a host belongs to # initialize ldap setup(); my $ldap = Net::LDAP->new("$ldapuri") or die "$@"; my $mesg = $ldap->bind; # create class hooks debconf disk_config package_config scripts files my @dirs= qw/class hooks debconf disk_config package_config scripts files/; foreach (@dirs) { -d "$outdir/$_" || mkpath "$outdir/$_" || warn "WARNING: Can't create subdir $outdir/$_ $!\n"; } @classes= get_classes($mac); prt_scripts(); prt_package_list(); prt_debconf(); prt_templates(); prt_var(); prt_hooks(); prt_disk_config(); # create sources list if (!$hostname) { -d "${outdir}/files/etc/apt/sources.list" || mkpath "${outdir}/files/etc/apt/sources.list"; copy ("${outdir}/tmp/apt-sources.list", "${outdir}/files/etc/apt/sources.list/$hostname") ; } $mesg = $ldap->unbind; # take down session exit 0; # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub setup { # Read LDAP open (LDAPCONF,"${ldapdir}") || usage("Can't open LDAP configuration$!\n"); my @content=; close(LDAPCONF); # Scan LDAP config foreach my $line (@content) { $line =~ /^\s*(#|$)/ && next; chomp($line); if ($line =~ /^BASE\s+(.*)$/) { $base= $1; next; } if ($line =~ m#^URI\s+ldaps?://([^/:]+).*$#) { $ldapuri= $1; next; } } } sub usage { (@_) && print STDERR "\n@_\n\n"; print STDERR << "EOF"; usage: $0 [-hv] [-c config] [-d outdir] -h : this (help) message -c : LDAP config file (default: ${ldapdir}) -d : output dir (default: ${outdir}) -v : be verbose EOF exit -1; } #----------------------------------------------------------------------------------- sub write_file { my @opts = @_; my $len = scalar @_; ($len < 2) && return; my $filename = shift; my $data = shift; open (SCRIPT,">${filename}") || warn "Can't create ${filename}. $!\n"; print SCRIPT $data; close(SCRIPT); ($opts[2] ne "") && chmod oct($opts[2]),${filename}; ($opts[3] ne "") && chown_files(${filename}, $opts[3]); } #----------------------------------------------------------------------------------- sub chown_files { my @owner = split('.',@_[1]); my $filename = @_[0]; my ($uid,$gid); $uid = getpwnam(@owner[0]); $gid = getgrnam(@owner[1]); chown $uid, $gid, $filename; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_classes { # return list of FAI classes defined for host my $mac = shift; my (@classes,$mesg,$entry); $mesg = $ldap->search( base => "ou=systems,$base", filter => "(&(macAddress=$mac)(objectClass=gotoWorkstation))", attrs => [ 'FAIclass', 'cn']); $mesg->code && die $mesg->error; # normally, only one value should be returned if ($mesg->count != 1) { die "LDAP search for client failed. ".$mesg->count." entries have been returned\n"; } # this assigns the last value to @classes $entry= ($mesg->entries)[0]; @classes= split /\s+/,$entry->get_value('FAIclass'); # get hostname my $hname= $entry->get_value('cn'); my $dn= $entry->dn; $hostname= $hname; # Search for object groups containing this client $mesg = $ldap->search( base => "ou=groups,$base", filter => "(&(objectClass=gosaGroupOfNames)(objectClass=FAIobject)(member=$dn))", attrs => [ 'FAIclass' ]); $mesg->code && die $mesg->error; foreach my $m ($mesg->entries) { push @classes, split /\s+/,$m->get_value('FAIclass'); } # print all classes to the file with hostname open (FAICLASS,">$outdir/class/$hname") || warn "Can't create $outdir/class/$hname. $!\n"; my @newclasses; foreach my $class (@classes) { # We need to walk through the list of classes and watch out for # a profile which is named like the class. Replace the profile # name by the names of the included classes. $mesg = $ldap->search( base => "ou=systems,$base", filter => "(&(objectClass=FAIprofile)(cn=$class))", attrs => [ 'FAIclass' ]); $mesg->code && die $mesg->error; if ($mesg->count > 0){ foreach my $m ($mesg->entries) { foreach my $tc (split /\s+/,$m->get_value('FAIclass')){ print FAICLASS "$tc\n"; push @newclasses, $tc; } } } else { print FAICLASS "$class\n"; push @newclasses, $class; } } close(FAICLASS); print "Host $hname belongs to FAI classes: ",join ' ',@newclasses,"\n" if $verbose; return @newclasses; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_variables { # gets all variables defined for a class # returns a list of lines in bourne shell syntax my $class = shift; my ($mesg,$var_base,$entry,$line,@vars); $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIvariable))", attrs => [ 'cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $var_base=$entry->dn; $mesg = $ldap->search( base => "$var_base", filter => "(objectClass=FAIvariableEntry)", attrs => ['cn', 'FAIvariableContent']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; foreach $entry ($mesg->entries) { $line= sprintf "%s=\'%s\'\n", $entry->get_value('cn'), $entry->get_value('FAIvariableContent'); push @vars,$line; } return @vars; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_var { my (@lines, $hname); foreach my $class (@classes) { @lines = get_variables($class); next until @lines; # do not create .var file if no variables are defined open (FAIVAR,">$outdir/class/${class}.var") || warn "Can't create $outdir/class/$hname.var.$!\n"; print FAIVAR @lines; close(FAIVAR); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_disk_config { my $class = shift; my ($mesg,$entry,$line,@diskconfig,$partition_base,$dn,%diskline,$xxmesg); # Search for partition schema for the specified class $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIpartitionTable))" ); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $partition_base= $entry->dn; # Search for disks $mesg = $ldap->search( base => "$partition_base", filter => "(objectClass=FAIpartitionDisk)" ); return if ($mesg->code == 32); # skip if no such object exists $mesg->code && die $mesg->error; foreach $entry ($mesg->entries) { my $logic_count= 4; my $primary_count= 0; my $dn=$entry->dn; my $disk=$entry->get_value('cn'); my $part; undef %diskline; $diskline{0} = "disk_config $disk\n"; $xxmesg = $ldap->search( base => "$dn", filter => "objectClass=FAIpartitionEntry" ); $xxmesg->code && die $xxmesg->error; foreach my $dl ($xxmesg->entries) { if ($dl->get_value('FAIpartitionType') eq 'primary'){ $primary_count++; } else { $logic_count++; } if ($dl->get_value('FAIpartitionFlags') eq 'preserve'){ if ($dl->get_value('FAIpartitionType') eq 'primary'){ $part= 'preserve'.$primary_count; } else { $part= 'preserve'.$logic_count; } $line= sprintf "%-7s %-12s %-12s %-10s ; %s\n", $dl->get_value('FAIpartitionType'), $dl->get_value('FAImountPoint'), $part, $dl->get_value('FAImountOptions') eq '' ? 'rw' : $dl->get_value('FAImountOptions'), $dl->get_value('FAIfsOptions'); } elsif ($dl->get_value('FAIfsType') eq 'swap'){ $line= sprintf "%-7s %-12s %-12s %-10s\n", $dl->get_value('FAIpartitionType'), $dl->get_value('FAImountPoint'), $dl->get_value('FAIpartitionSize'), $dl->get_value('FAImountOptions') eq '' ? 'rw' : $dl->get_value('FAImountOptions'); } else { $line= sprintf "%-7s %-12s %-12s %-10s ; %s %s\n", $dl->get_value('FAIpartitionType'), $dl->get_value('FAImountPoint'), $dl->get_value('FAIpartitionSize'), $dl->get_value('FAImountOptions') eq '' ? 'rw' : $dl->get_value('FAImountOptions'), $dl->get_value('FAIfsOptions'), $dl->get_value('FAIfsType'); } $diskline{$dl->get_value('FAIpartitionNr')}=$line; } foreach my $l (sort {$a <=> $b} keys %diskline) { push @diskconfig, $diskline{$l}; } } return @diskconfig; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_disk_config { # create one disk_config file my ($class,@lines); foreach $class (reverse @classes) { @lines=get_disk_config($class); next until @lines; # skip if nothing is defined for this class print "Generating partition layout for class '${class}'\n." if $verbose; open (FAIVAR,">${outdir}/disk_config/${class}") || warn "Can't create $outdir/disk_config/$class. $!\n"; print FAIVAR join '',@lines; close(FAIVAR); last; # finish when one config file is created } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_packages { # gets list of packages defined for a class my $class = shift; my ($mesg,$entry,$line,$method,%packlist); -d "${outdir}/tmp" || mkpath "${outdir}/tmp" || warn "Can't create ${outdir}/tmp. $!\n"; print "Generate sources.list for install\n" if $verbose; open (SOURCES,">>${outdir}/tmp/apt-sources.list") || warn "Can't create ${outdir}/tmp/apt-sources.list. $!\n"; $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIpackageList))" , attrs => [ 'FAIpackage', 'FAIinstallMethod', 'FAIdebianMirror', 'FAIdebianRelease', 'FAIdebianSection']); $mesg->code && die $mesg->error; # should also return only one value undef %packlist; foreach $entry ($mesg->entries) { $method=$entry->get_value('FAIinstallMethod'); push @{$packlist{$method}}, $entry->get_value('FAIpackage'); print SOURCES "deb ".$entry->get_value('FAIdebianMirror')." ".$entry->get_value('FAIdebianRelease')." "; my $section; foreach $section ($entry->get_value('FAIdebianSection')){ print SOURCES "$section "; } print SOURCES "\n"; } close (SOURCES); # return a ref to the hash of arrays (key of the hash is the method), # the value is the array of package names for this method return \%packlist; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_package_list { my (@lines,$plist,$method,$value); foreach my $class (@classes) { $plist=get_packages($class); # test if hash contains any keys or values unless (keys %{$plist}) { next; } print "Generate package list for class '$class'.\n" if $verbose; open (PACKAGES,">$outdir/package_config/$class") || warn "Can't create $outdir/package_config/$class. $!\n"; while (($method, $value) = each %{$plist}) { print PACKAGES "PACKAGES $method\n"; print PACKAGES join "\n",@{$value}; print PACKAGES "\n"; } close(PACKAGES); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_templates { # get list of template-files defined for a class my $class = shift; my ($mesg,$entry,$str,$pfad,$name,$owner,$mode,$template_base,@template); $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAItemplate))", attrs => ['cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $template_base=$entry->dn; $mesg = $ldap->search( base => "$template_base", filter => "(objectClass=FAItemplateEntry)", attrs => ['FAItemplateFile', 'FAItemplatePath', 'FAIowner', 'FAImode' ,'cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; foreach $entry ($mesg->entries) { $name = $entry->get_value('cn'); $owner = $entry->get_value('FAIowner'); $owner = $entry->get_value('FAImode'); $pfad = $entry->get_value('FAItemplatePath'); chomp($pfad); -d "${outdir}/files/${pfad}" || mkpath "${outdir}/files/${pfad}" || warn "WARNING: Can't create subdir ${outdir}/files/${pfad} !$\n"; print "Generate template '$pfad' ($name) for class '$class'.\n" if $verbose; write_file( "${outdir}/files/${pfad}/${class}", $entry->get_value('FAItemplateFile'),$entry->get_value('FAImode'),$entry->get_value('FAIowner')); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_templates { my ($class); foreach $class (reverse @classes) { get_templates($class); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_debconf { # gets list of packages defined for a class my $class = shift; my ($mesg,$entry,$str,$debconf_base,@debconf); $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIpackageList))", attrs => ['cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $debconf_base=$entry->dn; $mesg = $ldap->search( base => "$debconf_base", filter => "(objectClass=FAIdebconfInfo)" , attrs => [ 'FAIpackage', 'FAIvariable', 'FAIvariableType','FAIvariableContent']); $mesg->code && die $mesg->error; # undef @debconf; foreach $entry ($mesg->entries) { $str = sprintf "%s %s %s %s\n", $entry->get_value('FAIpackage'), $entry->get_value('FAIvariable'), $entry->get_value('FAIvariableType'), $entry->get_value('FAIvariableContent'); push @debconf, $str; } return @debconf; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_debconf { my @lines; my $class; foreach $class (@classes) { @lines = get_debconf($class); next until @lines; print "Generate DebConf for class '$class'.\n" if $verbose; open (DEBCONF,">${outdir}/debconf/${class}") || warn "Can't create $outdir/debconf/$class. $!\n"; print DEBCONF @lines; close(DEBCONF); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_scripts { my ($class,@lines); foreach $class (@classes) { get_scripts($class); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_scripts { # gets list of packages defined for a class my $class = shift; my ($mesg,$entry,$str,$script_base,$prio,$name,$script); $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIscript))", attrs => ['cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $script_base= $entry->dn; $mesg = $ldap->search( base => "$script_base", filter => "(objectClass=FAIscriptEntry)", attrs => ['FAIpriority', 'FAIscript', 'cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; foreach $entry ($mesg->entries) { $name = $entry->get_value('cn'); $prio = $entry->get_value('FAIpriority'); $script= sprintf('%02d-%s', $prio, $name); -d "$outdir/scripts/$class" || mkpath "$outdir/scripts/$class" || warn "WARNING: Can't create subdir $outdir/scripts/$class !$\n"; write_file("${outdir}/scripts/${class}/${script}", $entry->get_value('FAIscript'), "0700"); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub prt_hooks { my ($class,@lines); foreach $class (reverse @classes) { get_hooks($class); } } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub get_hooks { # gets list of packages defined for a class my $class = shift; my ($mesg,$entry,$str,$hook_base,$prio,$task,$hook,$name); $mesg = $ldap->search( base => "$base", filter => "(&(cn=$class)(objectClass=FAIhook))", attrs => ['cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; $entry=($mesg->entries)[0]; $hook_base= $entry->dn; $mesg = $ldap->search( base => "$hook_base", filter => "(objectClass=FAIhookEntry)", attrs => ['FAItask', 'FAIscript', 'cn']); return if ($mesg->count() == 0); # skip if no such object exists $mesg->code && die $mesg->error; foreach $entry ($mesg->entries) { $name = $entry->get_value('cn'); $task = $entry->get_value('FAItask'); $prio = $entry->get_value('FAIpriority'); $hook = sprintf('%s.%s', ${task}, ${class}); write_file("${outdir}/hooks/${hook}", $entry->get_value('FAIscript'), "0700"); } } # vim:ts=2:sw=2:expandtab:shiftwidth=2:syntax:paste