# BEGIN COPYRIGHT BLOCK # 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; version 2 of the License. # # 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., 59 Temple # Place, Suite 330, Boston, MA 02111-1307 USA. # # In addition, as a special exception, Red Hat, Inc. gives You the additional # right to link the code of this Program with code not covered under the GNU # General Public License ("Non-GPL Code") and to distribute linked combinations # including the two, subject to the limitations in this paragraph. Non-GPL Code # permitted under this exception must only link to the code of this Program # through those well defined interfaces identified in the file named EXCEPTION # found in the source code files (the "Approved Interfaces"). The files of # Non-GPL Code may instantiate templates or use macros or inline functions from # the Approved Interfaces without causing the resulting work to be covered by # the GNU General Public License. Only Red Hat, Inc. may make changes or # additions to the list of Approved Interfaces. You must obey the GNU General # Public License in all respects for all of the Program code and other code used # in conjunction with the Program except the Non-GPL Code covered by this # exception. If you modify this file, you may extend this exception to your # version of the file, but you are not obligated to do so. If you do not wish to # provide this exception without modification, you must delete this exception # statement from your version and license this file solely under the GPL without # exception. # # # Copyright (C) 2009 Red Hat, Inc. # All rights reserved. # END COPYRIGHT BLOCK # ########################### # # This perl module provides code to update/upgrade directory # server shared files/config and instance specific files/config # ########################## package DSUpdate; use Util; use Inf; use FileConn; use DSCreate qw(setDefaults createInstanceScripts); use File::Basename qw(basename dirname); # load perldap use Mozilla::LDAP::Conn; use Mozilla::LDAP::Utils qw(normalizeDN); use Mozilla::LDAP::API qw(ldap_explode_dn); use Mozilla::LDAP::LDIF; use Exporter; @ISA = qw(Exporter); @EXPORT = qw(updateDS); @EXPORT_OK = qw(updateDS); use strict; use SetupLog; # the default location of the updates - this is a subdir # of the directory server data dir (e.g. /usr/share/dirsrv) # the default directory is read-only - if you need to provide # additional updates, pass in additional update directories # to updateDS my $DS_UPDATE_PATH = "@updatedir@"; my $PRE_STAGE = "pre"; my $PREINST_STAGE = "preinst"; my $RUNINST_STAGE = "runinst"; my $POSTINST_STAGE = "postinst"; my $POST_STAGE = "post"; my @STAGES = ($PRE_STAGE, $PREINST_STAGE, $RUNINST_STAGE, $POSTINST_STAGE, $POST_STAGE); my @INSTSTAGES = ($PREINST_STAGE, $RUNINST_STAGE, $POSTINST_STAGE); # used to create unique package names for loading updates # from perl scriptlets my $pkgname = "Package00000000000"; # generate and return a unique package name that is a # subpackage of our current package sub get_pkgname { return __PACKAGE__ . "::" . $pkgname++; } sub loadUpdates { my $errs = shift; my $dirs = shift; my $mapinfo = shift || {}; my @updates; # a list of hash refs, sorted in execution order for my $dir (@{$dirs}) { for my $file (glob("$dir/*")) { my $name = basename($file); next if $name !~ /^\d\d/; # we only consider files that begin with two digits # print "name = $name\n"; my $href = { path => $file, name => $name }; if ($file =~ /\.(pl|pm)$/) { # a perl file my $fullpkg = get_pkgname(); # get a unique package name for the file # this will import the update functions from the given file # each file is given its own private namespace via the package # directive below # we have to use the eval because package takes a "bareword" - # you cannot pass a dynamically constructed string to package eval "package $fullpkg; require q($file)"; # "import" it if ($@) { if ($@ =~ /did not return a true value/) { # this usually means the file did not end with 1; - just use it anyway debug(3, "notice: $file does not return a true value - using anyway\n"); } else { # probably a syntax or other compilation error in the file # we can't safely use it, so log it and skip it push @{$errs}, ['error_loading_update', $file, $@]; debug(0, "Error: not applying update $file. Error: $@\n"); next; # skip this one } } # grab the hook functions from the update for my $fn (@STAGES) { # this is some deep perl magic - see the perl Symbol Table # documentation for the gory details # We're trying to find if the file defined a symbol called # pre, run, post, etc. and if so, if that symbol is code no strict 'refs'; # turn off strict refs to use magic if (*{$fullpkg . "::" . $fn}{CODE}) { debug(5, "$file $fn is defined\n"); # store the "function pointer" in the href for this update $href->{$fn} = \&{$fullpkg . "::" . $fn}; } else { debug(5, "$file $fn is not defined or not a subroutine\n"); } } } else { # some other type of file $href->{file} = 1; } if ($mapinfo->{$file}) { $href->{mapper} = $mapinfo->{$file}->{mapper}; $href->{infary} = $mapinfo->{$file}->{infary}; } push @updates, $href; } } # we have all the updates now - sort by the name @updates = sort { $a->{name} cmp $b->{name} } @updates; return @updates; } sub applyLDIFUpdate { my ($upd, $conn, $inf) = @_; my @errs; my $path = ref($upd) ? $upd->{path} : $upd; my $mapper; my @infary; # caller can set mapper to use and additional inf to use if (ref($upd)) { if ($upd->{mapper}) { $mapper = new Inf($upd->{mapper}); } if ($upd->{infary}) { @infary = @{$upd->{infary}}; } } if (!$mapper) { $mapper = new Inf("$inf->{General}->{prefix}@infdir@/dsupdate.map"); } my $dsinf = new Inf("$inf->{General}->{prefix}@infdir@/slapd.inf"); $mapper = process_maptbl($mapper, \@errs, $inf, $dsinf, @infary); if (!$mapper or @errs) { return @errs; } getMappedEntries($mapper, [$path], \@errs, \&check_and_add_entry, [$conn]); return @errs; } # process an update from an ldif file or executable # LDIF files only apply to instance updates, so ignore # LDIF files when not processing updates for instances sub processUpdate { my ($upd, $inf, $configdir, $stage, $inst, $dseldif, $conn) = @_; my @errs; # $upd is either a hashref or a simple path name my $path = ref($upd) ? $upd->{path} : $upd; if ($path =~ /\.ldif$/) { # ldif files are only processed during the runinst stage if ($stage eq $RUNINST_STAGE) { @errs = applyLDIFUpdate($upd, $conn, $inf); } } elsif (-x $path) { # setup environment $ENV{DS_UPDATE_STAGE} = $stage; $ENV{DS_UPDATE_DIR} = $configdir; $ENV{DS_UPDATE_INST} = $inst; # empty if not instance specific $ENV{DS_UPDATE_DSELDIF} = $dseldif; # empty if not instance specific $? = 0; # clear error condition my $output = `$path 2>&1`; if ($?) { @errs = ('error_executing_update', $path, $?, $output); } debug(1, $output); } else { @errs = ('error_unknown_update', $path); } return @errs; } # sub updateDS { # get base configdir, instances from setup my $setup = shift; # get other info from inf my $inf = $setup->{inf}; # directories containing updates to apply my $dirs = shift || []; my $mapinfo = shift; # the default directory server update path if ($inf->{slapd}->{updatedir}) { push @{$dirs}, $inf->{General}->{prefix} . $inf->{slapd}->{updatedir}; } else { push @{$dirs}, $inf->{General}->{prefix} . $DS_UPDATE_PATH; } my @errs; my $force = $setup->{force}; my @updates = loadUpdates(\@errs, $dirs, $mapinfo); if (@errs and !$force) { return @errs; } if (!@updates) { # nothing to do? debug(0, "No updates to apply in @{$dirs}\n"); return @errs; } # run pre-update hooks for my $upd (@updates) { my @localerrs; if ($upd->{$PRE_STAGE}) { debug(1, "Running stage $PRE_STAGE update ", $upd->{path}, "\n"); @localerrs = &{$upd->{$PRE_STAGE}}($inf, $setup->{configdir}); } elsif ($upd->{file}) { debug(1, "Running stage $PRE_STAGE update ", $upd->{path}, "\n"); @localerrs = processUpdate($upd, $inf, $setup->{configdir}, $PRE_STAGE); } if (@localerrs) { push @errs, @localerrs; if (!$force) { return @errs; } } } # update each instance for my $inst ($setup->getDirServers()) { my @localerrs = updateDSInstance($inst, $inf, $setup->{configdir}, \@updates, $force); if (@localerrs) { # push array here because localerrs will likely be an array of # array refs already push @errs, @localerrs; if (!$force) { return @errs; } } } # run post-update hooks for my $upd (@updates) { my @localerrs; if ($upd->{$POST_STAGE}) { debug(1, "Running stage $POST_STAGE update ", $upd->{path}, "\n"); @localerrs = &{$upd->{$POST_STAGE}}($inf, $setup->{configdir}); } elsif ($upd->{file}) { debug(1, "Running stage $POST_STAGE update ", $upd->{path}, "\n"); @localerrs = processUpdate($upd, $inf, $setup->{configdir}, $POST_STAGE); } if (@localerrs) { push @errs, @localerrs; if (!$force) { return @errs; } } } return @errs; } sub updateDSInstance { my ($inst, $inf, $configdir, $updates, $force) = @_; my @errs; my $dseldif = "$configdir/$inst/dse.ldif"; # get the information we need from the instance delete $inf->{slapd}; # delete old data, if any if (@errs = initInfFromInst($inf, $dseldif, $configdir, $inst)) { return @errs; } # upgrade instance scripts if (@errs = createInstanceScripts($inf, 1)) { return @errs; } my $conn; if ($inf->{General}->{UpdateMode} eq 'online') { # open a connection to the directory server to upgrade my $host = $inf->{General}->{FullMachineName}; my $port = $inf->{slapd}->{ServerPort}; # this says RootDN and password, but it can be any administrative DN # such as the one used by the console my $binddn = $inf->{$inst}->{RootDN} || $inf->{slapd}->{RootDN}; my $bindpw = $inf->{$inst}->{RootDNPwd}; my $certdir = $inf->{$inst}->{cert_dir} || $inf->{$inst}->{config_dir} || $inf->{slapd}->{cert_dir}; $conn = new Mozilla::LDAP::Conn({ host => $host, port => $port, bind => $binddn, pswd => $bindpw, cert => $certdir, starttls => 1 }); if (!$conn) { debug(0, "Could not open TLS connection to $host:$port - trying regular connection\n"); $conn = new Mozilla::LDAP::Conn({ host => $host, port => $port, bind => $binddn, pswd => $bindpw }); } if (!$conn) { debug(0, "Could not open a connection to $host:$port\n"); return ('error_online_update', $host, $port, $binddn); } } else { $conn = new FileConn($dseldif); if (!$conn) { debug(0, "Could not open a connection to $dseldif: $!\n"); return ('error_offline_update', $dseldif, $!); } } # run pre-instance hooks first, then runinst hooks, then postinst hooks # the DS_UPDATE_STAGE for my $stage (@INSTSTAGES) { # always process these first in the runinst stage - we don't really have any # other good way to process conditional features during update if ($stage eq $RUNINST_STAGE) { my @ldiffiles; if ("@enable_pam_passthru@") { push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-pampta.ldif"; } if ("@enable_bitwise@") { push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-bitwise.ldif"; } if ("@enable_dna@") { push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-dnaplugin.ldif"; push @ldiffiles, $inf->{General}->{prefix} . $DS_UPDATE_PATH . "/dnaplugindepends.ldif"; } for my $ldiffile (@ldiffiles) { my @localerrs = processUpdate($ldiffile, $inf, $configdir, $stage, $inst, $dseldif, $conn); if (@localerrs) { push @errs, @localerrs; if (!$force) { $conn->close(); return @errs; } } } } for my $upd (@{$updates}) { my @localerrs; if ($upd->{$stage}) { debug(1, "Running stage $stage update ", $upd->{path}, "\n"); @localerrs = &{$upd->{$stage}}($inf, $inst, $dseldif, $conn); } elsif ($upd->{file}) { debug(1, "Running stage $stage update ", $upd->{path}, "\n"); @localerrs = processUpdate($upd, $inf, $configdir, $stage, $inst, $dseldif, $conn); } if (@localerrs) { push @errs, @localerrs; if (!$force) { $conn->close(); return @errs; } } } } $conn->close(); return @errs; } # populate the fields in the inf we need to perform upgrade # tasks from the information in the instance dse.ldif and # other config sub initInfFromInst { my ($inf, $dseldif, $configdir, $inst) = @_; my $conn = new FileConn($dseldif, 1); if (!$conn) { debug(1, "Error: Could not open config file $dseldif: Error $!\n"); return ('error_opening_dseldif', $dseldif, $!); } my $dn = "cn=config"; my $entry = $conn->search($dn, "base", "(cn=*)", 0); if (!$entry) { $conn->close(); debug(1, "Error: Search $dn in $dseldif failed: ".$conn->getErrorString()."\n"); return ('error_finding_config_entry', $dn, $dseldif, $conn->getErrorString()); } my $servid = $inst; $servid =~ s/slapd-//; $inf->{General}->{FullMachineName} = $entry->getValue("nsslapd-localhost"); $inf->{General}->{SuiteSpotUserID} = $entry->getValue("nsslapd-localuser"); $inf->{slapd}->{ServerPort} = $entry->getValue("nsslapd-port"); $inf->{slapd}->{ldapifilepath} = $entry->getValue("nsslapd-ldapifilepath"); if (!$inf->{$inst}->{RootDN}) { $inf->{$inst}->{RootDN} || $entry->getValue('nsslapd-rootdn'); } # we don't use this password - we either use {$inst} password or # none at all $inf->{slapd}->{RootDNPwd} = '{SSHA}dummy'; if (!$inf->{$inst}->{cert_dir}) { $inf->{$inst}->{cert_dir} = $entry->getValue('nsslapd-certdir'); } $inf->{slapd}->{cert_dir} = $inf->{$inst}->{cert_dir}; if (!$inf->{slapd}->{ldif_dir}) { $inf->{slapd}->{ldif_dir} = $entry->getValue('nsslapd-ldifdir'); } if (!$inf->{slapd}->{ServerIdentifier}) { $inf->{slapd}->{ServerIdentifier} = $servid; } if (!$inf->{slapd}->{bak_dir}) { $inf->{slapd}->{bak_dir} = $entry->getValue('nsslapd-bakdir'); } if (!$inf->{slapd}->{config_dir}) { $inf->{slapd}->{config_dir} = $configdir; } if (!$inf->{slapd}->{inst_dir}) { $inf->{slapd}->{inst_dir} = $entry->getValue('nsslapd-instancedir'); } if (!$inf->{slapd}->{run_dir}) { $inf->{slapd}->{run_dir} = $entry->getValue('nsslapd-rundir'); } if (!$inf->{slapd}->{schema_dir}) { $inf->{slapd}->{schema_dir} = $entry->getValue('nsslapd-schemadir'); } if (!$inf->{slapd}->{lock_dir}) { $inf->{slapd}->{lock_dir} = $entry->getValue('nsslapd-lockdir'); } if (!$inf->{slapd}->{log_dir}) { # use the errorlog dir my $logfile = $entry->getValue('nsslapd-errorlog'); if ($logfile) { $inf->{slapd}->{log_dir} = dirname($logfile); } } if (!$inf->{slapd}->{sasl_path}) { $inf->{slapd}->{sasl_path} = $entry->getValue('nsslapd-saslpath'); } # dn: cn=config,cn=ldbm database,cn=plugins,cn=config $dn = "cn=config,cn=ldbm database,cn=plugins,cn=config"; $entry = $conn->search($dn, "base", "(cn=*)", 0); if (!$entry) { $conn->close(); debug(1, "Error: Search $dn in $dseldif failed: ".$conn->getErrorString()."\n"); return ('error_finding_config_entry', $dn, $dseldif, $conn->getErrorString()); } if (!$inf->{slapd}->{db_dir}) { $inf->{slapd}->{db_dir} = $entry->getValue('nsslapd-directory'); } $conn->close(); # don't need this anymore # set defaults for things we don't know how to find, after setting the values # we do know how to find return setDefaults($inf); } 1; # emacs settings # Local Variables: # mode:perl # indent-tabs-mode: nil # tab-width: 4 # End: