#!/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 <lange@informatik.uni-koeln.de>
# (c) 2005, Jens Nitschke <jens.nitschke@2int.de>
# (c) 2005, Jan-Marek Glogowski <glogow@fbihome.de>
# (c) 2005, Cajus Pollmeier <pollmeier@gonicus.de>
#
#*********************************************************************

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=<LDAPCONF>;
  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] <MAC>

-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
