DSUpdate.pm.in 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. # BEGIN COPYRIGHT BLOCK
  2. # Copyright (C) 2009 Red Hat, Inc.
  3. # All rights reserved.
  4. #
  5. # License: GPL (version 3 or any later version).
  6. # See LICENSE for details.
  7. # END COPYRIGHT BLOCK
  8. #
  9. ###########################
  10. #
  11. # This perl module provides code to update/upgrade directory
  12. # server shared files/config and instance specific files/config
  13. #
  14. ##########################
  15. package DSUpdate;
  16. use DSUtil;
  17. use Inf;
  18. use FileConn;
  19. use DSCreate qw(setDefaults createInstanceScripts makeOtherConfigFiles
  20. makeDSDirs updateSelinuxPolicy updateTmpfilesDotD updateSystemD);
  21. use File::Basename qw(basename dirname);
  22. # load perldap
  23. use Mozilla::LDAP::Conn;
  24. use Mozilla::LDAP::Utils qw(normalizeDN);
  25. use Mozilla::LDAP::API qw(ldap_explode_dn);
  26. use Mozilla::LDAP::LDIF;
  27. use Exporter;
  28. @ISA = qw(Exporter);
  29. @EXPORT = qw(updateDS isOffline);
  30. @EXPORT_OK = qw(updateDS isOffline);
  31. use strict;
  32. use SetupLog;
  33. # the default location of the updates - this is a subdir
  34. # of the directory server data dir (e.g. /usr/share/dirsrv)
  35. # the default directory is read-only - if you need to provide
  36. # additional updates, pass in additional update directories
  37. # to updateDS
  38. my $DS_UPDATE_PATH = "@updatedir@";
  39. my $PRE_STAGE = "pre";
  40. my $PREINST_STAGE = "preinst";
  41. my $RUNINST_STAGE = "runinst";
  42. my $POSTINST_STAGE = "postinst";
  43. my $POST_STAGE = "post";
  44. my @STAGES = ($PRE_STAGE, $PREINST_STAGE, $RUNINST_STAGE, $POSTINST_STAGE, $POST_STAGE);
  45. my @INSTSTAGES = ($PREINST_STAGE, $RUNINST_STAGE, $POSTINST_STAGE);
  46. # used to create unique package names for loading updates
  47. # from perl scriptlets
  48. my $pkgname = "Package00000000000";
  49. # generate and return a unique package name that is a
  50. # subpackage of our current package
  51. sub get_pkgname {
  52. return __PACKAGE__ . "::" . $pkgname++;
  53. }
  54. sub loadUpdates {
  55. my $errs = shift;
  56. my $dirs = shift;
  57. my $mapinfo = shift || {};
  58. my @updates; # a list of hash refs, sorted in execution order
  59. for my $dir (@{$dirs}) {
  60. for my $file (glob("$dir/*")) {
  61. my $name = basename($file);
  62. next if $name !~ /^\d\d/; # we only consider files that begin with two digits
  63. # print "name = $name\n";
  64. my $href = { path => $file, name => $name };
  65. if ($file =~ /\.(pl|pm)$/) { # a perl file
  66. my $fullpkg = get_pkgname(); # get a unique package name for the file
  67. # this will import the update functions from the given file
  68. # each file is given its own private namespace via the package
  69. # directive below
  70. # we have to use the eval because package takes a "bareword" -
  71. # you cannot pass a dynamically constructed string to package
  72. eval "package $fullpkg; require q($file)"; # "import" it
  73. if ($@) {
  74. if ($@ =~ /did not return a true value/) {
  75. # this usually means the file did not end with 1; - just use it anyway
  76. debug(3, "notice: $file does not return a true value - using anyway\n");
  77. } else {
  78. # probably a syntax or other compilation error in the file
  79. # we can't safely use it, so log it and skip it
  80. push @{$errs}, ['error_loading_update', $file, $@];
  81. debug(0, "Error: not applying update $file. Error: $@\n");
  82. next; # skip this one
  83. }
  84. }
  85. # grab the hook functions from the update
  86. for my $fn (@STAGES) {
  87. # this is some deep perl magic - see the perl Symbol Table
  88. # documentation for the gory details
  89. # We're trying to find if the file defined a symbol called
  90. # pre, run, post, etc. and if so, if that symbol is code
  91. no strict 'refs'; # turn off strict refs to use magic
  92. if (*{$fullpkg . "::" . $fn}{CODE}) {
  93. debug(5, "$file $fn is defined\n");
  94. # store the "function pointer" in the href for this update
  95. $href->{$fn} = \&{$fullpkg . "::" . $fn};
  96. } else {
  97. debug(5, "$file $fn is not defined or not a subroutine\n");
  98. }
  99. }
  100. } else { # some other type of file
  101. $href->{file} = 1;
  102. }
  103. if ($mapinfo->{$file}) {
  104. $href->{mapper} = $mapinfo->{$file}->{mapper};
  105. $href->{infary} = $mapinfo->{$file}->{infary};
  106. }
  107. push @updates, $href;
  108. }
  109. }
  110. # we have all the updates now - sort by the name
  111. @updates = sort { $a->{name} cmp $b->{name} } @updates;
  112. return @updates;
  113. }
  114. sub applyLDIFUpdate {
  115. my ($upd, $conn, $inf) = @_;
  116. my @errs;
  117. my $path = ref($upd) ? $upd->{path} : $upd;
  118. my $mapper;
  119. my @infary;
  120. # caller can set mapper to use and additional inf to use
  121. if (ref($upd)) {
  122. if ($upd->{mapper}) {
  123. $mapper = new Inf($upd->{mapper});
  124. }
  125. if ($upd->{infary}) {
  126. @infary = @{$upd->{infary}};
  127. }
  128. }
  129. if (!$mapper) {
  130. $mapper = new Inf("$inf->{General}->{prefix}@infdir@/dsupdate.map");
  131. }
  132. my $dsinf = new Inf("$inf->{General}->{prefix}@infdir@/slapd.inf");
  133. $mapper = process_maptbl($mapper, \@errs, $inf, $dsinf, @infary);
  134. if (!$mapper or @errs) {
  135. return @errs;
  136. }
  137. getMappedEntries($mapper, [$path], \@errs, \&check_and_add_entry,
  138. [$conn]);
  139. return @errs;
  140. }
  141. # process an update from an ldif file or executable
  142. # LDIF files only apply to instance updates, so ignore
  143. # LDIF files when not processing updates for instances
  144. sub processUpdate {
  145. my ($upd, $inf, $configdir, $stage, $inst, $dseldif, $conn) = @_;
  146. my @errs;
  147. # $upd is either a hashref or a simple path name
  148. my $path = ref($upd) ? $upd->{path} : $upd;
  149. if ($path =~ /\.ldif$/) {
  150. # ldif files are only processed during the runinst stage
  151. if ($stage eq $RUNINST_STAGE) {
  152. @errs = applyLDIFUpdate($upd, $conn, $inf);
  153. }
  154. } elsif (-x $path) {
  155. # setup environment
  156. $ENV{DS_UPDATE_STAGE} = $stage;
  157. $ENV{DS_UPDATE_DIR} = $configdir;
  158. $ENV{DS_UPDATE_INST} = $inst; # empty if not instance specific
  159. $ENV{DS_UPDATE_DSELDIF} = $dseldif; # empty if not instance specific
  160. $? = 0; # clear error condition
  161. my $output = `$path 2>&1`;
  162. if ($?) {
  163. @errs = ('error_executing_update', $path, $?, $output);
  164. }
  165. debug(1, $output);
  166. } else {
  167. @errs = ('error_unknown_update', $path);
  168. }
  169. return @errs;
  170. }
  171. #
  172. sub updateDS {
  173. # get base configdir, instances from setup
  174. my $setup = shift;
  175. # get other info from inf
  176. my $inf = $setup->{inf};
  177. # directories containing updates to apply
  178. my $dirs = shift || [];
  179. my $mapinfo = shift;
  180. # the default directory server update path
  181. if ($inf->{slapd}->{updatedir}) {
  182. push @{$dirs}, $inf->{General}->{prefix} . $inf->{slapd}->{updatedir};
  183. } else {
  184. push @{$dirs}, $inf->{General}->{prefix} . $DS_UPDATE_PATH;
  185. }
  186. my @errs;
  187. my $force = $setup->{force};
  188. my @updates = loadUpdates(\@errs, $dirs, $mapinfo);
  189. if (@errs and !$force) {
  190. return @errs;
  191. }
  192. if (!@updates) {
  193. # nothing to do?
  194. debug(0, "No updates to apply in @{$dirs}\n");
  195. return @errs;
  196. }
  197. # run pre-update hooks
  198. for my $upd (@updates) {
  199. my @localerrs;
  200. if ($upd->{$PRE_STAGE}) {
  201. debug(1, "Running stage $PRE_STAGE update ", $upd->{path}, "\n");
  202. @localerrs = &{$upd->{$PRE_STAGE}}($inf, $setup->{configdir});
  203. } elsif ($upd->{file}) {
  204. debug(1, "Running stage $PRE_STAGE update ", $upd->{path}, "\n");
  205. @localerrs = processUpdate($upd, $inf, $setup->{configdir}, $PRE_STAGE);
  206. }
  207. if (@localerrs) {
  208. push @errs, @localerrs;
  209. if (!$force) {
  210. return @errs;
  211. }
  212. }
  213. }
  214. # update each instance
  215. my @instances = $setup->getDirServers();
  216. my $inst_count = @instances;
  217. my @failed_instances = ();
  218. my $failed_count = 0;
  219. for my $inst (@instances) {
  220. debug(0, "Updating instance ($inst)...\n");
  221. my @localerrs = updateDSInstance($inst, $inf, $setup->{configdir}, \@updates, $force);
  222. if (@localerrs) {
  223. # push array here because localerrs will likely be an array of
  224. # array refs already
  225. $failed_count++;
  226. if (!$force || $inst_count == 1) {
  227. push @errs, @localerrs;
  228. return @errs;
  229. }
  230. push @failed_instances, $inst;
  231. debug(0, "Failed to update instance ($inst):\n---> @localerrs\n");
  232. } else {
  233. debug(0, "Successfully updated instance ($inst).\n");
  234. }
  235. }
  236. if($failed_count && $failed_count == $inst_count){
  237. push @errs, ('error_update_all');
  238. return @errs;
  239. }
  240. if (@failed_instances){
  241. # list all the instances that were not updated
  242. debug(0, "The following instances were not updated: (@failed_instances). ");
  243. debug(0, "After fixing the problems you will need to rerun the setup script\n");
  244. }
  245. # run post-update hooks
  246. for my $upd (@updates) {
  247. my @localerrs;
  248. if ($upd->{$POST_STAGE}) {
  249. debug(1, "Running stage $POST_STAGE update ", $upd->{path}, "\n");
  250. @localerrs = &{$upd->{$POST_STAGE}}($inf, $setup->{configdir});
  251. } elsif ($upd->{file}) {
  252. debug(1, "Running stage $POST_STAGE update ", $upd->{path}, "\n");
  253. @localerrs = processUpdate($upd, $inf, $setup->{configdir}, $POST_STAGE);
  254. }
  255. if (@localerrs) {
  256. push @errs, @localerrs;
  257. if (!$force) {
  258. return @errs;
  259. }
  260. }
  261. }
  262. return @errs;
  263. }
  264. sub updateDSInstance {
  265. my ($inst, $inf, $configdir, $updates, $force) = @_;
  266. my @errs;
  267. my $dseldif = "$configdir/$inst/dse.ldif";
  268. # get the information we need from the instance
  269. delete $inf->{slapd}; # delete old data, if any
  270. if (@errs = initInfFromInst($inf, $dseldif, $configdir, $inst)) {
  271. return @errs;
  272. }
  273. # create dirs if missing e.g. cross platform upgrade
  274. if (@errs = makeDSDirs($inf)) {
  275. return @errs;
  276. }
  277. # upgrade instance scripts
  278. if (@errs = createInstanceScripts($inf, 0)) {
  279. return @errs;
  280. }
  281. # add new or missing config files
  282. if (@errs = makeOtherConfigFiles($inf, 1)) {
  283. return @errs;
  284. }
  285. my $conn;
  286. if ($inf->{General}->{UpdateMode} eq 'online') {
  287. # open a connection to the directory server to upgrade
  288. my $host = $inf->{General}->{FullMachineName};
  289. my $port = $inf->{slapd}->{ServerPort};
  290. # this says RootDN and password, but it can be any administrative DN
  291. # such as the one used by the console
  292. my $binddn = $inf->{$inst}->{RootDN} || $inf->{slapd}->{RootDN};
  293. my $bindpw = $inf->{$inst}->{RootDNPwd};
  294. my $certdir = $inf->{$inst}->{cert_dir} || $inf->{$inst}->{config_dir} || $inf->{slapd}->{cert_dir};
  295. $conn = new Mozilla::LDAP::Conn({ host => $host, port => $port, bind => $binddn,
  296. pswd => $bindpw, cert => $certdir, starttls => 1 });
  297. if (!$conn) {
  298. debug(1, "Could not open TLS connection to $host:$port - trying regular connection\n");
  299. $conn = new Mozilla::LDAP::Conn({ host => $host, port => $port, bind => $binddn,
  300. pswd => $bindpw });
  301. }
  302. if (!$conn) {
  303. debug(0, "Could not open a connection to $host:$port\n");
  304. return ('error_online_update', $host, $port, $binddn);
  305. }
  306. } else {
  307. $conn = new FileConn($dseldif);
  308. if (!$conn) {
  309. debug(0, "Could not open a connection to $dseldif: $!\n");
  310. return ('error_offline_update', $dseldif, $!);
  311. }
  312. }
  313. # run pre-instance hooks first, then runinst hooks, then postinst hooks
  314. # the DS_UPDATE_STAGE
  315. for my $stage (@INSTSTAGES) {
  316. # always process these first in the runinst stage - we don't really have any
  317. # other good way to process conditional features during update
  318. if ($stage eq $RUNINST_STAGE) {
  319. my @ldiffiles;
  320. if ("@enable_pam_passthru@") {
  321. push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-pampta.ldif";
  322. }
  323. if ("@enable_bitwise@") {
  324. push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-bitwise.ldif";
  325. }
  326. if ("@enable_dna@") {
  327. push @ldiffiles, "$inf->{General}->{prefix}@templatedir@/template-dnaplugin.ldif";
  328. push @ldiffiles, $inf->{General}->{prefix} . $DS_UPDATE_PATH . "/dnaplugindepends.ldif";
  329. }
  330. push @ldiffiles, $inf->{General}->{prefix} . $DS_UPDATE_PATH . "/50updateconfig.ldif";
  331. for my $ldiffile (@ldiffiles) {
  332. my @localerrs = processUpdate($ldiffile, $inf, $configdir, $stage,
  333. $inst, $dseldif, $conn);
  334. if (@localerrs) {
  335. push @errs, @localerrs;
  336. if (!$force) {
  337. $conn->close();
  338. return @errs;
  339. }
  340. }
  341. }
  342. }
  343. for my $upd (@{$updates}) {
  344. my @localerrs;
  345. if ($upd->{$stage}) {
  346. debug(1, "Running stage $stage update ", $upd->{path}, "\n");
  347. @localerrs = &{$upd->{$stage}}($inf, $inst, $dseldif, $conn);
  348. } elsif ($upd->{file}) {
  349. debug(1, "Running stage $stage update ", $upd->{path}, "\n");
  350. @localerrs = processUpdate($upd, $inf, $configdir, $stage,
  351. $inst, $dseldif, $conn);
  352. }
  353. if (@localerrs) {
  354. push @errs, @localerrs;
  355. if (!$force) {
  356. $conn->close();
  357. return @errs;
  358. }
  359. }
  360. }
  361. }
  362. $conn->close();
  363. updateSelinuxPolicy($inf);
  364. push @errs, updateTmpfilesDotD($inf);
  365. push @errs, updateSystemD(1, $inf);
  366. return @errs;
  367. }
  368. # populate the fields in the inf we need to perform upgrade
  369. # tasks from the information in the instance dse.ldif and
  370. # other config
  371. sub initInfFromInst {
  372. my ($inf, $dseldif, $configdir, $inst) = @_;
  373. my $conn = new FileConn($dseldif, 1);
  374. if (!$conn) {
  375. debug(1, "Error: Could not open config file $dseldif: Error $!\n");
  376. return ('error_opening_dseldif', $dseldif, $!);
  377. }
  378. my $dn = "cn=config";
  379. my $entry = $conn->search($dn, "base", "(cn=*)", 0);
  380. if (!$entry) {
  381. $conn->close();
  382. debug(1, "Error: Search $dn in $dseldif failed: ".$conn->getErrorString()."\n");
  383. return ('error_finding_config_entry', $dn, $dseldif, $conn->getErrorString());
  384. }
  385. my $servid = $inst;
  386. $servid =~ s/slapd-//;
  387. $inf->{General}->{FullMachineName} = $entry->getValue("nsslapd-localhost");
  388. $inf->{General}->{SuiteSpotUserID} = $entry->getValue("nsslapd-localuser");
  389. $inf->{slapd}->{ServerPort} = $entry->getValue("nsslapd-port");
  390. $inf->{slapd}->{ldapifilepath} = $entry->getValue("nsslapd-ldapifilepath");
  391. if (!$inf->{$inst}->{RootDN}) {
  392. $inf->{$inst}->{RootDN} || $entry->getValue('nsslapd-rootdn');
  393. }
  394. # we don't use this password - we either use {$inst} password or
  395. # none at all
  396. $inf->{slapd}->{RootDNPwd} = '{SSHA}dummy';
  397. if (!$inf->{$inst}->{cert_dir}) {
  398. $inf->{$inst}->{cert_dir} = $entry->getValue('nsslapd-certdir');
  399. }
  400. $inf->{slapd}->{cert_dir} = $inf->{$inst}->{cert_dir};
  401. if (!$inf->{slapd}->{ldif_dir}) {
  402. $inf->{slapd}->{ldif_dir} = $entry->getValue('nsslapd-ldifdir');
  403. }
  404. if (!$inf->{slapd}->{ServerIdentifier}) {
  405. $inf->{slapd}->{ServerIdentifier} = $servid;
  406. }
  407. if (!$inf->{slapd}->{bak_dir}) {
  408. $inf->{slapd}->{bak_dir} = $entry->getValue('nsslapd-bakdir');
  409. }
  410. if (!$inf->{slapd}->{config_dir}) {
  411. $inf->{slapd}->{config_dir} = $configdir."/".$inst;
  412. }
  413. if (!$inf->{slapd}->{inst_dir}) {
  414. $inf->{slapd}->{inst_dir} = $entry->getValue('nsslapd-instancedir');
  415. }
  416. if (!$inf->{slapd}->{run_dir}) {
  417. $inf->{slapd}->{run_dir} = $entry->getValue('nsslapd-rundir');
  418. }
  419. if (!$inf->{slapd}->{schema_dir}) {
  420. $inf->{slapd}->{schema_dir} = $entry->getValue('nsslapd-schemadir');
  421. }
  422. if (!$inf->{slapd}->{lock_dir}) {
  423. $inf->{slapd}->{lock_dir} = $entry->getValue('nsslapd-lockdir');
  424. }
  425. if (!$inf->{slapd}->{log_dir}) {
  426. # use the errorlog dir
  427. my $logfile = $entry->getValue('nsslapd-errorlog');
  428. if ($logfile) {
  429. $inf->{slapd}->{log_dir} = dirname($logfile);
  430. }
  431. }
  432. if (!$inf->{slapd}->{sasl_path}) {
  433. $inf->{slapd}->{sasl_path} = $entry->getValue('nsslapd-saslpath');
  434. }
  435. # dn: cn=config,cn=ldbm database,cn=plugins,cn=config
  436. $dn = "cn=config,cn=ldbm database,cn=plugins,cn=config";
  437. $entry = $conn->search($dn, "base", "(cn=*)", 0);
  438. if (!$entry) {
  439. $conn->close();
  440. debug(1, "Error: Search $dn in $dseldif failed: ".$conn->getErrorString()."\n");
  441. return ('error_finding_config_entry', $dn, $dseldif, $conn->getErrorString());
  442. }
  443. if (!$inf->{slapd}->{db_dir}) {
  444. $inf->{slapd}->{db_dir} = $entry->getValue('nsslapd-directory');
  445. }
  446. $conn->close(); # don't need this anymore
  447. # set defaults for things we don't know how to find, after setting the values
  448. # we do know how to find
  449. return setDefaults($inf);
  450. }
  451. # check to see if the user has chosen offline mode and the server is really offline
  452. sub isOffline {
  453. my ($inf, $inst, $conn) = @_;
  454. if ($inf->{General}->{UpdateMode} !~ /offline/i) {
  455. debug(3, "UpdateMode " . $inf->{General}->{UpdateMode} . " is not offline\n");
  456. return 0;
  457. }
  458. # mode is offline - see if server is really offline
  459. my $config = $conn->search("cn=config", "base", "(objectclass=*)");
  460. if (!$config) {
  461. return 0, ['error_finding_config_entry', 'cn=config',
  462. $conn->getErrorString()];
  463. }
  464. my $rundir = $config->getValues('nsslapd-rundir');
  465. if (serverIsRunning($rundir, $inst)) {
  466. return 0, ['error_update_not_offline', $inst];
  467. }
  468. return 1; # server is offline
  469. }
  470. 1;
  471. # emacs settings
  472. # Local Variables:
  473. # mode:perl
  474. # indent-tabs-mode: nil
  475. # tab-width: 4
  476. # End: