From 416ab8a37f1b07dc9f6c0fb3ff1a8ff2036510b5 Mon Sep 17 00:00:00 2001 From: Sebastian Schmelzer Date: Thu, 2 Sep 2010 17:50:49 +0200 Subject: change dir structure --- src/lib/OpenSLX/Basics.pm | 856 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 856 insertions(+) create mode 100644 src/lib/OpenSLX/Basics.pm (limited to 'src/lib/OpenSLX/Basics.pm') diff --git a/src/lib/OpenSLX/Basics.pm b/src/lib/OpenSLX/Basics.pm new file mode 100644 index 00000000..4ac40166 --- /dev/null +++ b/src/lib/OpenSLX/Basics.pm @@ -0,0 +1,856 @@ +# 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/ +# ----------------------------------------------------------------------------- +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 + &warn &die &croak &carp &confess &cluck + &callInSubprocess &executeInSubprocess &slxsystem + &vlog + &checkParams + &instantiateClass &loadDistroModule +); + +=head1 NAME + +OpenSLX::Basics - implements basic functionality for OpenSLX. + +=head1 DESCRIPTION + +This module exports basic functions, which are expected to be used all across +OpenSLX. + +=cut + +our (%openslxConfig, %cmdlineConfig, %openslxPath); + +use subs qw(die warn); + +use open ':utf8'; + +require Carp; # do not import anything as we are going to overload carp + # and croak! +use Config::General; +use Encode; +use FindBin; +use Getopt::Long; +use POSIX qw(locale_h); + +my $translations; + +=head1 PUBLIC VARIABLES + +=over + +=item B<%openslxConfig> + +This hash holds the active openslx configuration. + +The initial content is based on environment variables or default values. Calling +C will read the configuration files and/or cmdline arguments +and modify this hash accordingly. + +The individual entries of this hash are documented in the manual of the +I-script, so please look there if you'd like to know more. + +=cut + +%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', + 'log-level' => $ENV{SLX_VERBOSE_LEVEL} || '0', + 'private-path' => $ENV{SLX_PRIVATE_PATH} || '/var/opt/openslx', + 'public-path' => $ENV{SLX_PUBLIC_PATH} || '/srv/openslx', + 'temp-path' => $ENV{SLX_TEMP_PATH} || '/tmp', + + # + # options useful during development only: + # + 'debug-confess' => '0', + # + # only settable programmatically: + # + 'log-pids' => '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-default-menu-entry' => undef, + 'pxe-passwd' => 'secret', + 'pxe-timeout' => '100', + 'pxe-title' => 'Welcome to OpenSLX', + 'pxe-totaltimeout' => '600', + 'syslinux-theme' => 'openslx', +); +chomp($openslxConfig{'locale-charmap'}); + +=item B<%cmdlineConfig> + +This hash holds the config items that were specified via cmdline. This can be +useful if you need to find out which settings have been specified via cmdline +and which ones have come from a config file. + +Currently, only the slxsettings script and some tests make use of this hash. + +=cut + +# 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'}, + + # level of logging verbosity (0-3) + 'log-level=i' => \$cmdlineConfig{'log-level'}, + + # 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'}, +); + +# filehandle used for logging: +my $openslxLog = *STDERR; + +$Carp::CarpLevel = 1; + +=back + +=head1 PUBLIC FUNCTIONS + +=over + +=item B + +Initializes OpenSLX environment - every script should invoke this function +before it invokes any other. + +Basically, this function reads in the configuration and sets up logging +and translation backends. + +Returns 1 upon success and dies in case of a problem. + +=cut + +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{'log-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{'log-level'} >= 2) { + foreach my $key (sort keys %openslxConfig) { + my $val = $openslxConfig{$key} || ''; + vlog(2, "config-dump: $key = $val"); + } + } + + # setup translation "engine": + _trInit(); + + return 1; +} + +=item B + +Logs the given I<$message> if the current log level is equal or greater than +the given I<$level>. + +=cut + +sub vlog +{ + my $minLevel = shift; + return if $minLevel > $openslxConfig{'log-level'}; + my $str = join("", '-' x $minLevel, @_); + if (substr($str, -1, 1) ne "\n") { + $str .= "\n"; + } + if ($openslxConfig{'log-pids'}) { + print $openslxLog "$$: $str"; + } else { + print $openslxLog $str; + } + return; +} + +=item B<_tr($originalMsg, @msgParams)> + +Translates the english text given in I<$originalMsg> to the currently selected +language, passing on any given additional I<$msgParams> to the translation +process (as printf arguments). + +N.B.: although it starts with an underscore, this is still a public function! + +=cut + +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, @_); +} + +=item B + +Forks the current process and invokes the code given in I<$childFunc> in the +child process. The parent blocks until the child has executed that function. + +If an error occured during execution of I<$childFunc>, the parent process will +cleanup the child and then pass back that error with an invocation of die(). + +If the process of executing I<$childFunc> is being interrupted by a signal, +the parent will cleanup and then exit with an appropriate exit code. + +=cut + +sub callInSubprocess +{ + my $childFunc = shift; + + my $pid = fork(); + if (!$pid) { + # child -> execute the given function and exit: + if (! eval { $childFunc->(); 1 }) { + $@ = "*** $@" unless substr( $@, 0, 4) eq '*** '; + print STDERR "$@\n"; + } + exit 0; + } + + # parent -> pass on interrupt- and terminate-signals to child ... + $SIG{INT} = sub { kill 'INT', $pid; }; + $SIG{TERM} = sub { kill 'TERM', $pid; }; + + # ... and wait until child has done its work + waitpid($pid, 0); + exit $? if $?; + + return; +} + +=item B + +Forks the current process and executes the program given in I<@cmdlineArgs> in +the child process. + +The parent process returns immediately after having spawned the new process, +returning the process-ID of the child. + +=cut + +sub executeInSubprocess +{ + my @cmdlineArgs = @_; + + my $pid = fork(); + if (!$pid) { + + # child... + # ...exec the given cmdline: + exec(@cmdlineArgs); + } + + # parent... + return $pid; +} + +=item B + +Executes a new program specified by I<@cmdlineArgs> and waits until it is done. + +Returns the exit code of the execution (usually 0 if everything is ok). + +If any signal (other than SIGPIPE) interrupts the execution, this function +dies with an appropriate error message. SIGPIPE is being ignored in order +to ignore any failed FTP connections and the like (we just return the +error code instead). + +=cut + +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 received signal '%s', parent stops!", $signalNo + ); + } + } + return $res; +} + +=item B, B, B, B, B, B + +Overrides of the respective functions in I or I that mark +any warnings with '°°°' and any errors with '***' in order to make them +more visible in the output. + +=cut + +sub cluck +{ + _doThrowOrWarn('cluck', @_); + return; +} + +sub carp +{ + _doThrowOrWarn('carp', @_); + return; +} + +sub warn +{ + _doThrowOrWarn('warn', @_); + return; +} + +sub confess +{ + _doThrowOrWarn('confess', @_); + return; +} + +sub croak +{ + _doThrowOrWarn('croak', @_); + return; +} + +sub die +{ + _doThrowOrWarn('die', @_); + return; +} + +=item B + +Utility function that can be used by any function that accepts param-hashes +to check if the parameters given in I<$params> actually match the expectations +specified in I<$paramsSpec>. + +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. + +=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; +} + +=item B + +Loads the required module and instantiates an object of the class given in +I<$class>. + +The following flags can be specified via I<$flags>-hashref: + +=over + +=item acceptMissing [optional] + +Usually, this function will die if the corresponding module could not be found +(acceptMissing == 0). Pass in acceptMissing => 1 if you want this function +to return undef instead. + +=item pathToClass [optional] + +Sometimes, the module specified in I<$class> lives relative to another path. +If so, you can specify the base path of that module via this flag. + +=item incPaths [optional] + +Some modules live outside of the standard perl search paths. If you'd like to +load such a module, you can specify one (or more) paths that will be added +to @INC while trying to load the module. + +=item version [optional] + +If you require a specific version of the module, you can specify the version +number via the I<$version> flag. + +=back + +=cut + +sub instantiateClass +{ + my $class = shift; + my $flags = shift || {}; + + checkParams($flags, { + 'acceptMissing' => '?', + 'pathToClass' => '?', + 'incPaths' => '?', + 'version' => '?', + }); + my $pathToClass = $flags->{pathToClass}; + my $requestedVersion = $flags->{version}; + my $incPaths = $flags->{incPaths} || []; + + my $moduleName = defined $pathToClass ? "$pathToClass/$class" : $class; + $moduleName =~ s[::][/]g; + $moduleName .= '.pm'; + + vlog(3, "trying to load $moduleName..."); + local @INC = @INC; + foreach my $incPath (@$incPaths) { + next if grep { $_ eq $incPath } @INC; + unshift @INC, $incPath; + } + if (!eval { require $moduleName; 1 } ) { + # check if module does not exists anywhere in search path + if ($! == 2) { + 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; +} + +=item B + +Tries to determine the most appropriate distro module for the context specified +via the given I<$params>. + +During that process, this function will try to load several different modules, +working its way from the most specific down to a generic fallback. + +For example: when given I as distroName, this function would +try the following modules: + +=over + +=item I + +=item I + +=item I + +=item I + +=item I (or whatever has been given as fallback name) + +=back + +The I<$params>-hashref supports the following entries: + +=over + +=item distroName + +Specifies the name of the distro as it was retrieved from the vendor-OS +(e.g. 'suse-10.2' or 'ubuntu-8.04_amd64'). + +=item distroScope + +Specifies the scope of the required distro class (e.g. +'OpenSLX::OSSetup::Distro' or 'vmware::OpenSLX::Distro'). + +=item fallbackName [optional] + +Instead of the default 'Base', you can specify the name of a different fallback +class that will be tried if no module matching the given distro name could be +found. + +=item pathToClass [optional] + +If you require the distro modules to be loaded relative to a specific path, +you can specify that base path via the I<$pathToClass> param. + +=back + +=cut + +sub loadDistroModule +{ + my $params = shift; + + checkParams($params, { + 'distroName' => '!', + 'distroScope' => '!', + 'fallbackName' => '?', + 'pathToClass' => '?', + }); + my $distroName = ucfirst(lc($params->{distroName})); + my $distroScope = $params->{distroScope}; + my $fallbackName = $params->{fallbackName} || 'Base'; + my $pathToClass = $params->{pathToClass}; + + vlog(1, "finding a ${distroScope} module for $distroName ..."); + + # try to load the distro module starting with the given name and then + # working the way upwards (from most specific to generic). + $distroName =~ tr{.-}{__}; + my @distroModules; + my $blockRX = qr{ + ^(.+?)_ # everything before the last block (the rest is dropped) + (?:x86_)? # takes care to treat 'x86_64' as one block + [^_]*$ # the last _-block + }x; + while($distroName =~ m{$blockRX}) { + push @distroModules, $distroName; + $distroName = $1; + } + push @distroModules, $distroName; + push @distroModules, $fallbackName; + + my $pluginBasePath = "$openslxConfig{'base-path'}/lib/plugins"; + + my $distro; + for my $distroModule (@distroModules) { + my $loaded = eval { + vlog(1, "trying ${distroScope}::$distroModule ..."); + my $flags = { acceptMissing => 1 }; + if ($pathToClass) { + $flags->{incPaths} = [ $pathToClass ]; + } + $distro = instantiateClass("${distroScope}::$distroModule", $flags); + return 0 if !$distro; # module does not exist, try next + vlog(1, "ok - using ${distroScope}::$distroModule."); + 1; + }; + last if $loaded; + if (!defined $loaded) { + die _tr( + "Error when trying to load distro module '%s':\n%s", + $distroModule, $@ + ); + } + } + + return $distro; +} + +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 _doThrowOrWarn +{ + my $type = shift; + my $msg = shift; + + # use '! ' for warnings and '***' for errors + $msg =~ s[^(! |\*\*\*) ][]gms; + if ($type eq 'carp' || $type eq 'warn' || $type eq 'cluck') { + $msg =~ s[^][! ]gms; + } + else { + $msg =~ s[^][*** ]gms; + } + + 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; +} + + +=back + +=cut + +1; -- cgit v1.2.3-55-g7522