#!/usr/bin/perl -w

#######################################################################
#
# argonaut-fuse -- fuse-supplicant which allows one to create pxelinux
#                  configurations for different types of clients using
#                  external modules.
#
# Copyright (c) 2005,2006,2007 by Jan-Marek Glogowski <glogow@fbihome.de>
# Copyright (c) 2008 by Cajus Pollmeier <pollmeier@gonicus.de>
# Copyright (c) 2008,2009, 2010 by Jan Wenzel <wenzel@gonicus.de>
# Copyright (C) 2011-2014 FusionDirectory project <contact@fusiondirectory.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>
#
#######################################################################

use strict;
use warnings;

use 5.008;

use POSIX;
use FindBin;
use Socket;
use Fuse;

use Data::Dumper;
use File::Pid;
use Storable qw(freeze thaw);
use Time::HiRes qw(gettimeofday usleep);
use Net::LDAP;
use Log::Handler;

use Argonaut::Libraries::Common qw(:ldap :config :file);

use constant USEC => 1000000;

# Predefined variables for config
our ($default_mode, $tftp_root);
our ($config, $ldap_base, $ldap_handle, $ldap_uris, $ldapinfos);

our $filesystem;
our $last_attred_file;
our ($known_modules);

my $configfile = "/etc/argonaut/argonaut.conf";
my $logfile = "argonaut-fuse.log";
my $piddir = "/var/run/argonaut";
my $pidfile = "argonaut-fuse.pid";
my $logdir;

$SIG{TERM}=\&sig_int_handler;

$SIG{INT}=\&sig_int_handler;

readConfig();

argonaut_create_dir($logdir);

my $log = Log::Handler->create_logger("argonaut-fuse");

$log->add(
    file => {
        filename => "$logdir/$logfile",
        maxlevel => "debug",
        minlevel => "emergency",
        newline  => 1,
    }
);

($ldap_handle,$ldap_base,$ldapinfos) = argonaut_ldap_handle($config);

$ldap_uris = $ldapinfos->{'URIS'};

$log->info("Argonaut-Fuse Started\n");

# Scan for modules
use Module::Pluggable search_path => 'Argonaut::Fuse::Modules', sub_name => 'modules', require => 1;
import_modules();

argonaut_create_dir($piddir);

my $pid = File::Pid->new({
    file => "$piddir/$pidfile",
  });

my $pidrunning = $pid->running;

if ($pidrunning) {
  die "Argonaut Server is already running $pidrunning\n";
}

$pid->pid($$);
$pid->write;

$filesystem = {
  root => {
    content => {}
  }
};

$log->info("Argonaut-Fuse Mounting $tftp_root\n");

# Mount FUSE Filesystem
eval {
  Fuse::main(
    mountpoint  => $tftp_root,
    mountopts   => "nonempty,allow_other",
    getattr     => \&getattr,
    read        => \&read,
    getdir      => \&getdir,
    debug       => 0,
    threaded    => 0,
  );
};
if ($@) {
  $log->error("Fuse error: $@\n");
  die $@;
}

exit 0;

sub sig_int_handler {
  $pid->remove;
  exit(0);
}

#===  FUNCTION  ================================================================
#         NAME:  readConfig
#   PARAMETERS:  none
#      RETURNS:  nothing
#  DESCRIPTION:  read the config file and put everything needed into the
#                corresponding variables
#===============================================================================

sub readConfig
{
    $config = argonaut_read_config;

    my $settings = argonaut_get_fuse_settings($config,$config->{'client_ip'});

    $default_mode           =   $settings->{"default_mode"};
    $tftp_root              =   $settings->{"pxelinux_cfg"};

    $logdir                 =   $settings->{"logdir"};
}

#===  FUNCTION  ================================================================
#         NAME:  import_modules
#   PARAMETERS:  none
#      RETURNS:  nothing
#  DESCRIPTION:  Import modules from Argonaut::Fuse namespace,
#                store their get_module_info result
#===============================================================================

sub import_modules {
  foreach my $module (modules()) {
    my $info = eval($module.'::get_module_info()');
    # Only load module if get_module_info() returns a non-null object
    if ($info) {
      $log->info("Loaded module $module ($info)\n");
      $known_modules->{$module} = $info;
    } else {
      $log->info("$module is not a module\n");
    }
  }
}

#===  FUNCTION  ================================================================
#         NAME:  getattr
#   PARAMETERS:  $filename - string -
#      RETURNS:  Returns a list, very similar to the 'stat' function (see perlfunc).
#                On error, simply return a single numeric scalar value
#                (e.g. "return -ENOENT();").
#  DESCRIPTION:  get attributes
#===============================================================================

sub getattr {
  my ($filename) = @_;

  # regular file
  my $type = 0100;
  my $bits = 0644;

  my $current = $filesystem->{'root'}->{'content'};
  my @path_elements = split('/',$filename);
  my $current_type = 'file';

  if ( @path_elements > 1 ) {
    foreach my $path_element (@path_elements[1..$#path_elements]) {
      if ($path_element =~ /^([0-9a-f]{1,2}-){6}[0-9a-f]{1,2}$/i) {
        # Always generate a fresh config
        delete $current->{$path_element} if(exists($current->{$path_element}));

        # Process known Modules
        MODULE: foreach my $module (keys %{$known_modules}) {
          $log->info("Processing Module $module with argument ${path_element}\n");
          my $answer = eval($module.'::get_pxe_config("'.$path_element.'")');
          if (exists($current->{$path_element})) {
            last MODULE;
          }
          if ($@) {
            $log->error("ERROR: Processing Module $module failed with $@\n");
          }
        }
      }
      $current_type = $current->{$path_element}->{'type'};
      $last_attred_file = $current->{$path_element} if ($current_type eq 'file');
      $current = $current->{$path_element}->{'content'} if ($current_type eq 'dir');
    }
  }

  # if directory, set type to dir and mode to 0755
  if ($filename eq '/' || $current_type eq 'dir') {
    $type = 0040;
    $bits = 0755;
  }

  my $mode = $type << 9 | $bits;
  my $nlink = 1;
  my $uid = $<;
  my ($gid) = split / /, $(;
  my $rdev = 0;
  my $atime = time;
  my $size = 0;

  if ($current_type eq 'file') {
    $size = length( $last_attred_file->{'content'} ) if($last_attred_file->{'content'});
  }

  my $mtime = $atime;
  my $ctime = $atime;

  my $blksize = 1024;
  my $blocks = 1;

  my $dev = 0;
  my $ino = 0;

  return ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks);
}

sub read {
  my ($pathname, $requestedsize, $offset) = @_;
  my $current= $filesystem->{'root'}->{'content'};
  my @path_elements= split('/', $pathname);

  if (@path_elements > 1) {
    foreach my $path_element (@path_elements[1..$#path_elements]) {
      $current = $current->{$path_element}->{'content'};
    }
  }

  return $current;
}

sub getdir {
  my ($filename) = @_;
  my $current = $filesystem->{'root'}->{'content'};
  my @path_elements= split('/', $filename);
  my @result= ('.', '..');

  if (@path_elements > 1) {
    foreach my $path_element (@path_elements[1..$#path_elements]) {
      return(-1*ENOENT) if(not defined($current->{$path_element}));
      $current= $current->{$path_element}->{'content'};
    }
  }

  push @result, keys(%{$current});

  push @result, 0;
  return @result;
}

#===  FUNCTION  ================================================================
#         NAME:  write_pxe_config_file
#   PARAMETERS:  $host
#                $file
#                $kernel
#                $append
#      RETURNS:  0
#  DESCRIPTION:  create the pxelinux.cfg file for a host
#===============================================================================

sub write_pxe_config_file {
  my ($host,$file,$kernel,$append) = @_;

  my $file_content  =   "# Generated by argonaut-fuse for host $host\n";
  $file_content     .=  "default argonaut-fuse-generated\n\n";
  $file_content     .=  "label argonaut-fuse-generated\n";
  $file_content     .=  "$kernel\n";
  if ($append) {
    $file_content .= "append $append\n";
  }

  # store in hash
  $filesystem->{'root'}->{'content'}->{$file}->{'type'}     = 'file';
  $filesystem->{'root'}->{'content'}->{$file}->{'content'}  = $file_content;

  return 0;
}

1;

__END__


=head1 NAME

argonaut-fuse - FUSE/TFTP supplicant targeted to work with LDAP entries written by FusionDirectory

=head1 SYNOPSIS

argonaut-fuse

=head1 DESCRIPTION

B<argonaut-fuse> is a modular fuse-tftp-supplicant written in perl which allows one to create pxelinux configurations for different types of clients using external modules.

=head1 BUGS

Please report any bugs, or post any suggestions, to the fusiondirectory mailing list fusiondirectory-users or to
<https://forge.fusiondirectory.org/projects/argonaut-agents/issues/new>

=head1 LICENCE AND COPYRIGHT

This code is part of FusionDirectory <http://www.fusiondirectory.org>

This code was Based on ctftpd

=over 5

=item Copyright (c) 2005,2006,2007 by Jan-Marek Glogowski <glogow@fbihome.de>

=item Copyright (c) 2008 by Cajus Pollmeier <pollmeier@gonicus.de>

=item Copyright (c) 2008,2009 by Jan Wenzel <wenzel@gonicus.de>

=item Copyright (C) 2010 by Jan Wenzel <wenzel@gonicus.de>

=item Copyright (C) 2011-2014 FusionDirectory project

=back

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

=cut

# vim:ts=2:sw=2:expandtab:shiftwidth=2:syntax:paste
