summaryrefslogblamecommitdiffstats
path: root/lib/OpenSLX/Basics.pm
blob: 230bf72bf77b3e4fd103fd080c13131ce0824359 (plain) (tree)
1
2
3
4
5
6
7
8
                                         
 

                                                                    
 

                                                                         
 


                                                                               
                                                             
                                                                               


                        


                              

             
                
                        

             







                                                     

  

                                                   
                                                   
                      

                 



                                                                                
                                                                             
                                




                                                                        
                    

                   



                       
                 


                                                                          
                                                                          
                  






























                                                                        

                                        

                                                                            
                          
 

                                              
 



                                                                   
 

                                                                               
 


                                                                            
 

                                            
 

                                                             
 

                                                         
 


                                                                              
 


                                                                       
 

                                                   
 

                                                          

  

                     


                              
                     
 


                                                                                







                                                          




                                                                                




                                               

                                                                         

                                                            




                                       

































                                                                             





                                                                 











                                                    




                                                                                



















































                                                                                   




                                                                                













                                             

 
                                                                                
                    
 

























                                                                            


                                                                                

                       
                         
 

                     
 



                                    
 

                


                                                                                

                      

                     
 

                                     




                                                                                
                     
 

                                    




                                                                                





                                                        


                                                                                

             












                                                                               


                                                                                

         

                                




                                                                                

                               


                                                                                

        

                               




                                                                                


                                  




                                                                                


                                


                                                                                

       


                              
 
 


                                                                                






































                                                                 

 






















                                                                                         
 
















































































                                                                                                             


                                                                                

                    



                            


                                






                                                                           




                                                                 

                                                             

                                                                      









                                                                                    

 





                                                                                
  
# Copyright (c) 2006, 2007 - OpenSLX GmbH
#
# This program is free software distributed under the GPL version 2.
# See http://openslx.org/COPYING
#
# If you have any feedback please consult http://openslx.org/feedback and
# send your suggestions, praise, or complaints to feedback@openslx.org
#
# General information about OpenSLX can be found at http://openslx.org/
# -----------------------------------------------------------------------------
# Basics.pm
#    - provides basic functionality of the OpenSLX config-db.
# -----------------------------------------------------------------------------
package OpenSLX::Basics;

use strict;
use warnings;

our (@ISA, @EXPORT, $VERSION);

use Exporter;
$VERSION = 1.01;
@ISA     = qw(Exporter);

@EXPORT = qw(
    &openslxInit %openslxConfig %cmdlineConfig
    &_tr &trInit
    &warn &die &croak &carp &confess &cluck
    &callInSubprocess &executeInSubprocess &slxsystem
    &vlog
    &checkParams
    &instantiateClass
    &addCleanupFunction &removeCleanupFunction
);

our (%openslxConfig, %cmdlineConfig, %openslxPath);

use sigtrap qw( die normal-signals error-signals );
use subs qw(die warn);

use open ':utf8';

################################################################################
### Module implementation
################################################################################
require Carp;       # do not import anything as we are going to overload carp
                    # and croak!
use Carp::Heavy;    # use it here to have it loaded immediately, not at
                    # the time when carp() is being invoked (which might
                    # be at a point in time where the script executes in
                    # a chrooted environment, such that the module can't
                    # be loaded anymore).
use Config::General;
use Encode;
require File::Glob;
use FindBin;
use Getopt::Long;
use POSIX qw(locale_h);

my $translations;

# this hash will hold the active openslx configuration,
# the initial content is based on environment variables or default values.
# Each value may be overridden from config files and/or cmdline arguments.
%openslxConfig = (
    'db-name' => $ENV{SLX_DB_NAME} || 'openslx',
    'db-spec' => $ENV{SLX_DB_SPEC},
    'db-type' => $ENV{SLX_DB_TYPE} || 'SQLite',
    'locale'         => setlocale(LC_MESSAGES),
    'locale-charmap' => `locale charmap`,
    'base-path'      => $ENV{SLX_BASE_PATH} || '/opt/openslx',
    'config-path'    => $ENV{SLX_CONFIG_PATH} || '/etc/opt/openslx',
    'private-path'   => $ENV{SLX_PRIVATE_PATH} || '/var/opt/openslx',
    'public-path'    => $ENV{SLX_PUBLIC_PATH} || '/srv/openslx',
    'temp-path'      => $ENV{SLX_TEMP_PATH} || '/tmp',
    'verbose-level'  => $ENV{SLX_VERBOSE_LEVEL} || '0',

    #
    # options useful during development only:
    #
    'debug-confess' => '0',

    #
    # extended settings follow, which are only supported by slxsettings,
    # but not by any other script:
    #
    'db-user'                            => undef,
    'db-passwd'                          => undef,
    'default-shell'                      => 'bash',
    'default-timezone'                   => 'Europe/Berlin',
    'mirrors-preferred-top-level-domain' => undef,
    'mirrors-to-try-count'               => '20',
    'mirrors-to-use-count'               => '5',
    'ossetup-max-try-count'              => '5',
    'pxe-theme'                          => undef,
    'pxe-theme-menu-margin'              => '9',
);
chomp($openslxConfig{'locale-charmap'});

# specification of cmdline arguments that are shared by all openslx-scripts:
my %openslxCmdlineArgs = (

    # name of database, defaults to 'openslx'
    'db-name=s' => \$cmdlineConfig{'db-name'},

    # full specification of database, a special string defining the
    # precise database to connect to (the contents of this string
    # depend on db-type)
    'db-spec=s' => \$cmdlineConfig{'db-spec'},

    # type of database to connect to (SQLite, mysql, ...), defaults to 'SQLite'
    'db-type=s' => \$cmdlineConfig{'db-type'},

    # activates debug mode, this will show the lines where any error occured
    # (followed by a stacktrace):
    'debug-confess' => \$cmdlineConfig{'debug-confess'},

    # locale to use for translations
    'locale=s' => \$cmdlineConfig{'locale'},

    # locale-charmap to use for I/O (iso-8859-1, utf-8, etc.)
    'locale-charmap=s' => \$cmdlineConfig{'locale-charmap'},

    # file to write logging output to, defaults to STDERR
    'logfile=s' => \$cmdlineConfig{'locale'},

    # path to private data (which is *not* accesible by clients and contains
    # database, vendorOSes and all local extensions [system specific scripts])
    'private-path=s' => \$cmdlineConfig{'private-path'},

    # path to public data (which is accesible by clients and contains
    # PXE-configurations, kernels, initramfs and client configurations)
    'public-path=s' => \$cmdlineConfig{'public-path'},

    # path to temporary data (used during demuxing)
    'temp-path=s' => \$cmdlineConfig{'temp-path'},

    # level of logging verbosity (0-3)
    'verbose-level=i' => \$cmdlineConfig{'verbose-level'},
);

my %cleanupFunctions;

# filehandle used for logging:
my $openslxLog = *STDERR;

$Carp::CarpLevel = 1;

# ------------------------------------------------------------------------------
sub vlog
{
    my $minLevel = shift;
    return if $minLevel > $openslxConfig{'verbose-level'};
    my $str = join("", '-' x $minLevel, @_);
    if (substr($str, -1, 1) ne "\n") {
        $str .= "\n";
    }
    print $openslxLog $str;
    return;
}

# ------------------------------------------------------------------------------
sub openslxInit
{
    # evaluate cmdline arguments:
    Getopt::Long::Configure('no_pass_through');
    GetOptions(%openslxCmdlineArgs);

    # try to read and evaluate config files:
    my $configPath 
        = $cmdlineConfig{'config-path'} || $openslxConfig{'config-path'};
    my $sharePath = "$openslxConfig{'base-path'}/share";
    my $verboseLevel = $cmdlineConfig{'verbose-level'} || 0;
    foreach my $f (
        "$sharePath/settings.default", 
        "$configPath/settings",
        "$ENV{HOME}/.openslx/settings"
    ) {
        next unless -e $f;
        if ($verboseLevel >= 2) {
            vlog(0, "reading config-file $f...");
        }
        my $configObject = Config::General->new(
            -AutoTrue       => 1, 
            -ConfigFile     => $f, 
            -LowerCaseNames => 1,
            -SplitPolicy    => 'equalsign',
        );
        my %config = $configObject->getall();
        foreach my $key (keys %config) {
            # N.B.: these config files are used by shell-scripts, too, so in
            # order to comply with shell-style, the config files use shell
            # syntax and an uppercase, underline-as-separator format.
            # Internally, we use lowercase, minus-as-separator format, so we
            # need to convert the environment variable names to our own
            # internal style here (e.g. 'SLX_BASE_PATH' to 'base-path'):
            my $ourKey = $key;
            $ourKey =~ s[^slx_][];
            $ourKey =~ tr/_/-/;
            $openslxConfig{$ourKey} = $config{$key};
        }
    }

    # push any cmdline argument into our config hash, possibly overriding any
    # setting from the config files:
    while (my ($key, $val) = each(%cmdlineConfig)) {
        next unless defined $val;
        $openslxConfig{$key} = $val;
    }

    if (defined $openslxConfig{'logfile'}) {
        open($openslxLog, '>>', $openslxConfig{'logfile'})
            or croak(
                _tr(
                    "unable to append to logfile '%s'! (%s)",    
                    $openslxConfig{'logfile'}, $!
                )
            );
    }
    if ($openslxConfig{'verbose-level'} >= 2) {
        foreach my $key (sort keys %openslxConfig) {
            my $val = $openslxConfig{$key} || '';
            vlog(2, "config-dump: $key = $val");
        }
    }

    # setup translation "engine":
    trInit();

    return 1;
}

# ------------------------------------------------------------------------------
sub trInit
{
    # activate automatic charset conversion on all the standard I/O streams,
    # just to give *some* support to shells in other charsets:
    binmode(STDIN,  ":encoding($openslxConfig{'locale-charmap'})");
    binmode(STDOUT, ":encoding($openslxConfig{'locale-charmap'})");
    binmode(STDERR, ":encoding($openslxConfig{'locale-charmap'})");

    my $locale = $openslxConfig{'locale'};
    if (lc($locale) eq 'c') {
        # treat locale 'c' as equivalent for 'posix':
        $locale = 'posix';
    }

    if (lc($locale) ne 'posix') {
        # parse locale and canonicalize it (e.g. to 'de_DE') and generate
        # two filenames from it (language+country and language only):
        if ($locale !~ m{^\s*([^_]+)(?:_(\w+))?}) {
            die "locale $locale has unknown format!?!";
        }
        my @locales;
        if (defined $2) {
            push @locales, lc($1) . '_' . uc($2);
        }
        push @locales, lc($1);

        # try to load any of the Translation modules (starting with the more
        # specific one [language+country]):
        my $loadedTranslationModule;
        foreach my $trName (@locales) {
            vlog(2,    "trying to load translation module $trName...");
            my $trModule = "OpenSLX/Translations/$trName.pm";
            my $trModuleSpec = "OpenSLX::Translations::$trName";
            if (eval { require $trModule } ) {
                # copy the translations available in the given locale into our 
                # hash:
                $translations = $trModuleSpec->getAllTranslations();
                $loadedTranslationModule = $trModule;
                vlog(
                    1,
                    _tr(
                        "translations module %s loaded successfully", $trModule
                    )
                );
                last;
            }
        }
        if (!defined $loadedTranslationModule) {
            vlog(1,
                "unable to load any translations module for locale '$locale' ($!)."
            );
        }
    }
    return;
}

# ------------------------------------------------------------------------------
sub _tr
{
    my $trOrig = shift;

    my $trKey = $trOrig;
    $trKey =~ s[\n][\\n]g;
    $trKey =~ s[\t][\\t]g;

    my $formatStr;
    if (defined $translations) {
        $formatStr = $translations->{$trKey};
    }
    if (!defined $formatStr) {
        $formatStr = $trOrig;
    }
    return sprintf($formatStr, @_);
}

# ------------------------------------------------------------------------------
sub callInSubprocess
{
    my $childFunc = shift;

    my $pid = fork();
    if (!$pid) {

        # child...
        # ...execute the given function and exit:
        my $ok = eval { $childFunc->(); 1 };
        if (!$ok) {
            print STDERR "*** $@";
            exit 5;
        }
        exit 0;
    }

    # parent...
    # ...pass on interrupt- and terminate-signals to child...
    local $SIG{INT}  = sub { kill 'INT',  $pid; waitpid($pid, 0); exit $? };
    local $SIG{TERM} = sub { kill 'TERM', $pid; waitpid($pid, 0); exit $? };

    # ...and wait for child to do its work:
    waitpid($pid, 0);
    if ($?) {
        exit $?;
    }
    return;
}

# ------------------------------------------------------------------------------
sub executeInSubprocess
{
    my @cmdlineArgs = @_;

    my $pid = fork();
    if (!$pid) {

        # child...
        # ...exec the given cmdline:
        exec(@cmdlineArgs);
    }

    # parent...
    return $pid;
}

# ------------------------------------------------------------------------------
sub addCleanupFunction
{
    my $name = shift;
    my $func = shift;

    $cleanupFunctions{$name} = $func;
    return;
}

# ------------------------------------------------------------------------------
sub removeCleanupFunction
{
    my $name = shift;

    delete $cleanupFunctions{$name};
    return;
}

# ------------------------------------------------------------------------------
sub invokeCleanupFunctions
{
    my @funcNames = keys %cleanupFunctions;
    foreach my $name (@funcNames) {
        vlog(2, "invoking cleanup function '$name'...");
        $cleanupFunctions{$name}->();
    }
    return;
}

# ------------------------------------------------------------------------------
sub slxsystem
{
    vlog(2, _tr("executing: %s", join ' ', @_));
    my $res = system(@_);
    if ($res > 0) {
        # check if child got killed, if so we stop, too (unless the signal is
        # SIGPIPE, which we ignore in order to loop over failed FTP connections
        # and the like):
        my $signalNo = $res & 127;
        if ($signalNo > 0 && $signalNo != 13) {
            die _tr("child-process reveived signal '%s', parent stops!",
                $signalNo);
        }
    }
    return $res;
}

# ------------------------------------------------------------------------------
sub cluck
{
    _doThrowOrWarn('cluck', @_);
    return;
}

# ------------------------------------------------------------------------------
sub carp
{
    _doThrowOrWarn('carp', @_);
    return;
}

# ------------------------------------------------------------------------------
sub warn
{
    _doThrowOrWarn('warn', @_);
    return;
}

# ------------------------------------------------------------------------------
sub confess
{
    invokeCleanupFunctions();
    _doThrowOrWarn('confess', @_);
    return;
}

# ------------------------------------------------------------------------------
sub croak
{
    invokeCleanupFunctions();
    _doThrowOrWarn('croak', @_);
    return;
}

# ------------------------------------------------------------------------------
sub die
{
    invokeCleanupFunctions();
    _doThrowOrWarn('die', @_);
    return;
}

# ------------------------------------------------------------------------------
sub _doThrowOrWarn
{
    my $type = shift;
    my $msg = shift;
    
    # use '°°°' for warnings and '***' for errors
    if ($type eq 'carp' || $type eq 'warn' || $type eq 'cluck') {
        $msg =~ s[^°°° ][]igms;
        $msg =~ s[^][°°° ]igms;
    }
    else {
        $msg =~ s[^\*\*\* ][]igms;
        $msg =~ s[^][*** ]igms;
    }

    if ($openslxConfig{'debug-confess'}) {
        my %functionFor = (
            'carp' => sub { Carp::cluck @_ },
            'cluck' => sub { Carp::cluck @_ },
            'confess' => sub { Carp::confess @_ },
            'croak' => sub { Carp::confess @_ },
            'die' => sub { Carp::confess @_ },
            'warn' => sub { Carp::cluck @_ },
        );
        my $func = $functionFor{$type};
        $func->($msg);
    }
    else {
        chomp $msg;
        my %functionFor = (
            'carp' => sub { Carp::carp @_ },
            'cluck' => sub { Carp::cluck @_ },
            'confess' => sub { Carp::confess @_ },
            'croak' => sub { Carp::croak @_ },
            'die' => sub { CORE::die @_},
            'warn' => sub { CORE::warn @_ },
        );
        my $func = $functionFor{$type};
        $func->("$msg\n");
    }
    return;
}

=item checkParams()

Utility function that can be used by any method that accepts param-hashes
to check if the given parameters actually match the expectations.

Each individual parameter has a specification that describes the expectation
that the calling function has towards this param. The following specifications
are supported:

* '!'          - the parameter is required
* '?'          - the parameter is optional
* 'm{regex}'   - the parameter must match the given regex
* '!class=...' - the parameter is required and must be an object of the given class
* '?class=...' - if the parameter has been given, it must be an object of the given class

The function will confess for any unknown, missing, or non-matching param.

If accepted as useful, this function could be moved to a utility module of
the framework in order to be available to all other OTRS-modules.

=cut

sub checkParams
{
    my $params     = shift or confess('need to pass in params-hashref!');
    my $paramsSpec = shift or confess('need to pass in params-spec-hashref!');

    # print a warning for any unknown parameters that have been given:
    my @unknownParams
        =   grep { !exists $paramsSpec->{$_}; }
            keys %$params;
    if (@unknownParams) {
        my $unknownParamsStr = join ',', @unknownParams;
        confess("Enocuntered unknown params: '$unknownParamsStr'!\n");
    }

    # check if all required params have been specified:
    foreach my $param (keys %$paramsSpec) {
        my $spec = $paramsSpec->{$param};
        if (ref($spec) eq 'HASH') {
            # Handle nested specs by recursion:
            my $subParams = $params->{$param};
            if (!defined $subParams) {
                confess("Required param '$param' is missing!");
            }
            checkParams($subParams, $spec);
        }
        elsif (ref($spec) eq 'ARRAY') {
            # Handle nested spec arrays by looped recursion:
            my $subParams = $params->{$param};
            if (!defined $subParams) {
                confess("Required param '$param' is missing!");
            }
            elsif (ref($subParams) ne 'ARRAY') {
                confess("Value for param '$param' must be an array-ref!");
            }
            foreach my $subParam (@$subParams) {
                checkParams($subParam, $spec->[0]);
            }
        }
        elsif ($spec eq '!') {
            # required parameter:
            if (!exists $params->{$param}) {
                confess("Required param '$param' is missing!");
            }
        }
        elsif ($spec =~ m{^\!class=(.+)$}i) {
            my $class = $1;
            # required parameter ...
            if (!exists $params->{$param}) {
                confess("Required param '$param' is missing!");
            }
            # ... of specific class
            if (!$params->{$param}->isa($class)) {
                confess("Param '$param' is not a '$class', but that is required!");
            }
        }
        elsif ($spec eq '?') {
            # optional parameter - nothing to do
        }
        elsif ($spec =~ m{^\?class=(.+)$}i) {
            my $class = $1;
            # optional parameter ...
            if (exists $params->{$param}) {
                # ... has been given, so it must match specific class
                if (!$params->{$param}->isa($class)) {
                    confess("Param '$param' is not a '$class', but that is required!");
                }
            }
        }
        elsif ($spec =~ m{^m{(.+)}$}) {
            # try to match given regex:
            my $regex = $1;
            my $value = $params->{$param};
            if ($value !~ m{$regex}) {
                confess("Required param '$param' isn't matching regex '$regex' (given value was '$value')!");
            }
        }
        else {
            # complain about unknown spec:
            confess("Unknown param-spec '$spec' encountered!");
        }
    }

    return scalar 1;
}

# ------------------------------------------------------------------------------
sub instantiateClass
{
    my $class = shift;
    my $flags = shift || {};

    checkParams($flags, { 
        'acceptMissing' => '?', 
        'pathToClass'   => '?', 
        'version'       => '?',
    });
    my $pathToClass      = $flags->{pathToClass};
    my $requestedVersion = $flags->{version};

    my $moduleName = defined $pathToClass ? "$pathToClass/$class" : $class;
    $moduleName =~ s[::][/]g;
    $moduleName .= '.pm';

    if (!eval { require $moduleName } ) {
        # check if module does not exists anywhere in search path
        if (!-e $moduleName) {
            return if $flags->{acceptMissing};
            die _tr("Module '%s' not found!\n", $moduleName);
        }
        # some other error (probably compilation problems)
        die _tr("Unable to load module '%s' (%s)\n", $moduleName, $@);
    }
    if (defined $requestedVersion) {
        my $classVersion = $class->VERSION;
        if ($classVersion < $requestedVersion) {
            die _tr(
                'Could not load class <%s> (Version <%s> required, but <%s> found)',
                $class, $requestedVersion, $classVersion);
        }
    }
    return $class->new;
}

# ------------------------------------------------------------------------------
END 
{
    invokeCleanupFunctions() if %cleanupFunctions;
}

1;