#!/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-2018 FusionDirectory project
#
#  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, write to the Free Software
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
#######################################################################

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;

use App::Daemon qw(daemonize);

# 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();

$App::Daemon::pidfile = "$piddir/$pidfile";
$App::Daemon::logfile = "$logdir/$logfile";
$App::Daemon::as_user = "root";

argonaut_create_dir($piddir);

daemonize();

$filesystem = {
  'default' => {
    'content' =>  "# generated by argonaut-fuse for host default with no IP\n".
                  "default argonaut-fuse generated\n\n".
                  "label argonaut-fuse generated\n\n".
                  "localboot 0\n"
  }
};

$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 {
  $pidfile->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"}; # Used by modules
  $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()) {
    eval {
      if(my $info = $module->get_module_info()) {
        $log->info("Loaded module $module ($info)\n");
        $known_modules->{$module} = $info;
      } else {
        $log->warn("$module is not a module\n");
      }
      1;
    } or do {
      $log->warn("Exception when trying to call ->get_module_info on $module - $@");
    }
  }
}

#===  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 = oct(100);
  my $bits = oct(644);
  my $size = 0;

  # if directory, set type to dir and mode to 0755
  if ($filename eq '/') {
    $type = oct(40);
    $bits = oct(755);
  } else {
    $filename =~ s|^.*/||; # Keep only filename
    if ($filename =~ /^([0-9a-f]{1,2}-){6}[0-9a-f]{1,2}$/i) {
      # Always generate a fresh config
      delete $filesystem->{$filename} if(exists($filesystem->{$filename}));

      # Process known Modules
      MODULE: foreach my $module (keys %{$known_modules}) {
        $log->info("Processing Module $module with argument ${filename}\n");
        eval {
          my $answer = $module->get_pxe_config($filename);
          if (exists($filesystem->{$filename})) {
            last MODULE;
          }
          1;
        } or do {
          $log->error("ERROR: Processing Module $module failed with $@\n");
          delete $filesystem->{$filename} if(exists($filesystem->{$filename}));
        }
      }
    }
    if (not exists($filesystem->{$filename})) {
      return -ENOENT();
    }
    $size = length( $filesystem->{$filename}->{'content'} ) if($filesystem->{$filename}->{'content'});
  }

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

  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 ($filename, $requestedsize, $offset) = @_;

  $filename =~ s|^.*/||; # Keep only filename

  if (defined $filesystem->{$filename}) {
    return $filesystem->{$filename}->{'content'};
  }

  return -ENOENT();
}

sub getdir {
  my ($filename) = @_;
  my @result = ('.', '..');

  if ($filename eq '/') {
    push @result, keys(%{$filesystem});
  }

  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->{$file}->{'type'}     = 'file';
  $filesystem->{$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://gitlab.fusiondirectory.org/argonaut/argonaut/issues/new>

=head1 LICENCE AND COPYRIGHT

This code is part of Argonaut Project <https://www.argonaut-project.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-2018 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
