001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2008 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 */ 017package org.opends.server.tasks; 018 019import static org.opends.messages.TaskMessages.*; 020import static org.opends.messages.ToolMessages.*; 021import static org.opends.server.config.ConfigConstants.*; 022import static org.opends.server.core.DirectoryServer.*; 023import static org.opends.server.util.ServerConstants.*; 024import static org.opends.server.util.StaticUtils.*; 025 026import java.io.File; 027import java.text.SimpleDateFormat; 028import java.util.ArrayList; 029import java.util.Date; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.TimeZone; 034 035import org.forgerock.i18n.LocalizableMessage; 036import org.forgerock.i18n.slf4j.LocalizedLogger; 037import org.forgerock.opendj.config.server.ConfigException; 038import org.forgerock.opendj.ldap.ResultCode; 039import org.forgerock.opendj.ldap.schema.AttributeType; 040import org.opends.messages.Severity; 041import org.opends.messages.TaskMessages; 042import org.forgerock.opendj.server.config.server.BackendCfg; 043import org.opends.server.api.Backend; 044import org.opends.server.api.Backend.BackendOperation; 045import org.opends.server.api.ClientConnection; 046import org.opends.server.backends.task.Task; 047import org.opends.server.backends.task.TaskState; 048import org.opends.server.core.DirectoryServer; 049import org.opends.server.core.LockFileManager; 050import org.opends.server.types.BackupConfig; 051import org.opends.server.types.BackupDirectory; 052import org.opends.server.types.DirectoryException; 053import org.opends.server.types.Entry; 054import org.opends.server.types.Operation; 055import org.opends.server.types.Privilege; 056 057/** 058 * This class provides an implementation of a Directory Server task that may be 059 * used to back up a Directory Server backend in a binary form that may be 060 * quickly archived and restored. 061 */ 062public class BackupTask extends Task 063{ 064 065 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 066 067 068 069 /** Stores mapping between configuration attribute name and its label. */ 070 private static Map<String,LocalizableMessage> argDisplayMap = new HashMap<>(); 071 static { 072 argDisplayMap.put(ATTR_TASK_BACKUP_ALL, INFO_BACKUP_ARG_BACKUPALL.get()); 073 argDisplayMap.put(ATTR_TASK_BACKUP_COMPRESS, INFO_BACKUP_ARG_COMPRESS.get()); 074 argDisplayMap.put(ATTR_TASK_BACKUP_ENCRYPT, INFO_BACKUP_ARG_ENCRYPT.get()); 075 argDisplayMap.put(ATTR_TASK_BACKUP_HASH, INFO_BACKUP_ARG_HASH.get()); 076 argDisplayMap.put(ATTR_TASK_BACKUP_INCREMENTAL, INFO_BACKUP_ARG_INCREMENTAL.get()); 077 argDisplayMap.put(ATTR_TASK_BACKUP_SIGN_HASH, INFO_BACKUP_ARG_SIGN_HASH.get()); 078 argDisplayMap.put(ATTR_TASK_BACKUP_BACKEND_ID, INFO_BACKUP_ARG_BACKEND_IDS.get()); 079 argDisplayMap.put(ATTR_BACKUP_ID, INFO_BACKUP_ARG_BACKUP_ID.get()); 080 argDisplayMap.put(ATTR_BACKUP_DIRECTORY_PATH, INFO_BACKUP_ARG_BACKUP_DIR.get()); 081 argDisplayMap.put(ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID, INFO_BACKUP_ARG_INC_BASE_ID.get()); 082 } 083 084 085 // The task arguments. 086 private boolean backUpAll; 087 private boolean compress; 088 private boolean encrypt; 089 private boolean hash; 090 private boolean incremental; 091 private boolean signHash; 092 private List<String> backendIDList; 093 private String backupID; 094 private File backupDirectory; 095 private String incrementalBase; 096 097 private BackupConfig backupConfig; 098 099 /** 100 * All the backend configuration entries defined in the server mapped 101 * by their backend ID. 102 */ 103 private Map<String,Entry> configEntries; 104 105 private ArrayList<Backend<?>> backendsToArchive; 106 107 /** {@inheritDoc} */ 108 @Override 109 public LocalizableMessage getDisplayName() { 110 return INFO_TASK_BACKUP_NAME.get(); 111 } 112 113 /** {@inheritDoc} */ 114 @Override 115 public LocalizableMessage getAttributeDisplayName(String attrName) { 116 return argDisplayMap.get(attrName); 117 } 118 119 /** {@inheritDoc} */ 120 @Override 121 public void initializeTask() throws DirectoryException 122 { 123 // If the client connection is available, then make sure the associated 124 // client has the BACKEND_BACKUP privilege. 125 Operation operation = getOperation(); 126 if (operation != null) 127 { 128 ClientConnection clientConnection = operation.getClientConnection(); 129 if (! clientConnection.hasPrivilege(Privilege.BACKEND_BACKUP, operation)) 130 { 131 LocalizableMessage message = ERR_TASK_BACKUP_INSUFFICIENT_PRIVILEGES.get(); 132 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 133 message); 134 } 135 } 136 137 138 Entry taskEntry = getTaskEntry(); 139 140 AttributeType typeBackupAll = getSchema().getAttributeType(ATTR_TASK_BACKUP_ALL); 141 AttributeType typeCompress = getSchema().getAttributeType(ATTR_TASK_BACKUP_COMPRESS); 142 AttributeType typeEncrypt = getSchema().getAttributeType(ATTR_TASK_BACKUP_ENCRYPT); 143 AttributeType typeHash = getSchema().getAttributeType(ATTR_TASK_BACKUP_HASH); 144 AttributeType typeIncremental = getSchema().getAttributeType(ATTR_TASK_BACKUP_INCREMENTAL); 145 AttributeType typeSignHash = getSchema().getAttributeType(ATTR_TASK_BACKUP_SIGN_HASH); 146 AttributeType typeBackendID = getSchema().getAttributeType(ATTR_TASK_BACKUP_BACKEND_ID); 147 AttributeType typeBackupID = getSchema().getAttributeType(ATTR_BACKUP_ID); 148 AttributeType typeBackupDirectory = getSchema().getAttributeType(ATTR_BACKUP_DIRECTORY_PATH); 149 AttributeType typeIncrementalBaseID = getSchema().getAttributeType(ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID); 150 151 backUpAll = TaskUtils.getBoolean(taskEntry.getAttribute(typeBackupAll), false); 152 compress = TaskUtils.getBoolean(taskEntry.getAttribute(typeCompress), false); 153 encrypt = TaskUtils.getBoolean(taskEntry.getAttribute(typeEncrypt), false); 154 hash = TaskUtils.getBoolean(taskEntry.getAttribute(typeHash), false); 155 incremental = TaskUtils.getBoolean(taskEntry.getAttribute(typeIncremental), false); 156 signHash = TaskUtils.getBoolean(taskEntry.getAttribute(typeSignHash), false); 157 backendIDList = TaskUtils.getMultiValueString(taskEntry.getAttribute(typeBackendID)); 158 backupID = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeBackupID)); 159 160 String backupDirectoryPath = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeBackupDirectory)); 161 backupDirectory = new File(backupDirectoryPath); 162 if (! backupDirectory.isAbsolute()) 163 { 164 backupDirectory = new File(DirectoryServer.getInstanceRoot(), backupDirectoryPath); 165 } 166 167 incrementalBase = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeIncrementalBaseID)); 168 169 configEntries = TaskUtils.getBackendConfigEntries(); 170 } 171 172 173 /** 174 * Validate the task arguments and construct the list of backends to be 175 * archived. 176 * @return true if the task arguments are valid. 177 */ 178 private boolean argumentsAreValid() 179 { 180 // Make sure that either the backUpAll argument was provided or at least one 181 // backend ID was given. They are mutually exclusive. 182 if (backUpAll) 183 { 184 if (!backendIDList.isEmpty()) 185 { 186 logger.error(ERR_BACKUPDB_CANNOT_MIX_BACKUP_ALL_AND_BACKEND_ID, 187 ATTR_TASK_BACKUP_ALL, ATTR_TASK_BACKUP_BACKEND_ID); 188 return false; 189 } 190 } 191 else if (backendIDList.isEmpty()) 192 { 193 logger.error(ERR_BACKUPDB_NEED_BACKUP_ALL_OR_BACKEND_ID, 194 ATTR_TASK_BACKUP_ALL, ATTR_TASK_BACKUP_BACKEND_ID); 195 return false; 196 } 197 198 199 // Use task id for backup id in case of recurring task. 200 if (super.isRecurring()) { 201 backupID = super.getTaskID(); 202 } 203 204 205 // If no backup ID was provided, then create one with the current timestamp. 206 if (backupID == null) 207 { 208 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME); 209 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 210 backupID = dateFormat.format(new Date()); 211 } 212 213 214 // If the incremental base ID was specified, then make sure it is an 215 // incremental backup. 216 if (incrementalBase != null && ! incremental) 217 { 218 logger.error(ERR_BACKUPDB_INCREMENTAL_BASE_REQUIRES_INCREMENTAL, ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID, 219 ATTR_TASK_BACKUP_INCREMENTAL); 220 return false; 221 } 222 223 224 // If the signHash option was provided, then make sure that the hash option 225 // was given. 226 if (signHash && !hash) 227 { 228 logger.error(ERR_BACKUPDB_SIGN_REQUIRES_HASH, ATTR_TASK_BACKUP_SIGN_HASH, ATTR_TASK_BACKUP_HASH); 229 return false; 230 } 231 232 233 // Make sure that the backup directory exists. If not, then create it. 234 if (! backupDirectory.exists()) 235 { 236 try 237 { 238 backupDirectory.mkdirs(); 239 } 240 catch (Exception e) 241 { 242 LocalizableMessage message = ERR_BACKUPDB_CANNOT_CREATE_BACKUP_DIR.get( 243 backupDirectory.getPath(), getExceptionMessage(e)); 244 System.err.println(message); 245 return false; 246 } 247 } 248 249 int numBackends = configEntries.size(); 250 251 252 backendsToArchive = new ArrayList<>(numBackends); 253 254 if (backUpAll) 255 { 256 for (Map.Entry<String,Entry> mapEntry : configEntries.entrySet()) 257 { 258 Backend<?> b = DirectoryServer.getBackend(mapEntry.getKey()); 259 if (b != null && b.supports(BackendOperation.BACKUP)) 260 { 261 backendsToArchive.add(b); 262 } 263 } 264 } 265 else 266 { 267 // Iterate through the set of requested backends and make sure they can 268 // be used. 269 for (String id : backendIDList) 270 { 271 Backend<?> b = DirectoryServer.getBackend(id); 272 if (b == null || configEntries.get(id) == null) 273 { 274 logger.error(ERR_BACKUPDB_NO_BACKENDS_FOR_ID, id); 275 } 276 else if (!b.supports(BackendOperation.BACKUP)) 277 { 278 logger.warn(WARN_BACKUPDB_BACKUP_NOT_SUPPORTED, b.getBackendID()); 279 } 280 else 281 { 282 backendsToArchive.add(b); 283 } 284 } 285 286 // It is an error if any of the requested backends could not be used. 287 if (backendsToArchive.size() != backendIDList.size()) 288 { 289 return false; 290 } 291 } 292 293 294 // If there are no backends to archive, then print an error and exit. 295 if (backendsToArchive.isEmpty()) 296 { 297 logger.warn(WARN_BACKUPDB_NO_BACKENDS_TO_ARCHIVE); 298 return false; 299 } 300 301 302 return true; 303 } 304 305 306 /** 307 * Archive a single backend, where the backend is known to support backups. 308 * @param b The backend to be archived. 309 * @param backupLocation The backup directory. 310 * @return true if the backend was successfully archived. 311 */ 312 private boolean backupBackend(Backend<?> b, File backupLocation) 313 { 314 // Get the config entry for this backend. 315 BackendCfg cfg = TaskUtils.getConfigEntry(b); 316 317 318 // If the directory doesn't exist, then create it. If it does exist, then 319 // see if it has a backup descriptor file. 320 BackupDirectory backupDir; 321 if (backupLocation.exists()) 322 { 323 String descriptorPath = backupLocation.getPath() + File.separator + 324 BACKUP_DIRECTORY_DESCRIPTOR_FILE; 325 File descriptorFile = new File(descriptorPath); 326 if (descriptorFile.exists()) 327 { 328 try 329 { 330 backupDir = BackupDirectory.readBackupDirectoryDescriptor( 331 backupLocation.getPath()); 332 333 // Check the current backup directory corresponds to the provided 334 // backend 335 if (! backupDir.getConfigEntryDN().equals(cfg.dn())) 336 { 337 logger.error(ERR_BACKUPDB_CANNOT_BACKUP_IN_DIRECTORY, b.getBackendID(), backupLocation.getPath(), 338 backupDir.getConfigEntryDN().rdn().getFirstAVA().getAttributeValue()); 339 return false ; 340 } 341 } 342 catch (ConfigException ce) 343 { 344 logger.error(ERR_BACKUPDB_CANNOT_PARSE_BACKUP_DESCRIPTOR, descriptorPath, ce.getMessage()); 345 return false; 346 } 347 catch (Exception e) 348 { 349 logger.error(ERR_BACKUPDB_CANNOT_PARSE_BACKUP_DESCRIPTOR, descriptorPath, getExceptionMessage(e)); 350 return false; 351 } 352 } 353 else 354 { 355 backupDir = new BackupDirectory(backupLocation.getPath(), cfg.dn()); 356 } 357 } 358 else 359 { 360 try 361 { 362 backupLocation.mkdirs(); 363 } 364 catch (Exception e) 365 { 366 logger.error(ERR_BACKUPDB_CANNOT_CREATE_BACKUP_DIR, backupLocation.getPath(), getExceptionMessage(e)); 367 return false; 368 } 369 370 backupDir = new BackupDirectory(backupLocation.getPath(), 371 cfg.dn()); 372 } 373 374 375 // Create a backup configuration. 376 backupConfig = new BackupConfig(backupDir, backupID, 377 incremental); 378 backupConfig.setCompressData(compress); 379 backupConfig.setEncryptData(encrypt); 380 backupConfig.setHashData(hash); 381 backupConfig.setSignHash(signHash); 382 backupConfig.setIncrementalBaseID(incrementalBase); 383 384 385 // Perform the backup. 386 try 387 { 388 DirectoryServer.notifyBackupBeginning(b, backupConfig); 389 b.createBackup(backupConfig); 390 DirectoryServer.notifyBackupEnded(b, backupConfig, true); 391 } 392 catch (DirectoryException de) 393 { 394 DirectoryServer.notifyBackupEnded(b, backupConfig, false); 395 logger.error(ERR_BACKUPDB_ERROR_DURING_BACKUP, b.getBackendID(), de.getMessageObject()); 396 return false; 397 } 398 catch (Exception e) 399 { 400 DirectoryServer.notifyBackupEnded(b, backupConfig, false); 401 logger.error(ERR_BACKUPDB_ERROR_DURING_BACKUP, b.getBackendID(), getExceptionMessage(e)); 402 return false; 403 } 404 405 return true; 406 } 407 408 /** 409 * Acquire a shared lock on a backend. 410 * @param b The backend on which the lock is to be acquired. 411 * @return true if the lock was successfully acquired. 412 */ 413 private boolean lockBackend(Backend<?> b) 414 { 415 try 416 { 417 String lockFile = LockFileManager.getBackendLockFileName(b); 418 StringBuilder failureReason = new StringBuilder(); 419 if (! LockFileManager.acquireSharedLock(lockFile, failureReason)) 420 { 421 logger.error(ERR_BACKUPDB_CANNOT_LOCK_BACKEND, b.getBackendID(), failureReason); 422 return false; 423 } 424 } 425 catch (Exception e) 426 { 427 logger.error(ERR_BACKUPDB_CANNOT_LOCK_BACKEND, b.getBackendID(), getExceptionMessage(e)); 428 return false; 429 } 430 431 return true; 432 } 433 434 /** 435 * Release a lock on a backend. 436 * @param b The backend on which the lock is held. 437 * @return true if the lock was successfully released. 438 */ 439 private boolean unlockBackend(Backend<?> b) 440 { 441 try 442 { 443 String lockFile = LockFileManager.getBackendLockFileName(b); 444 StringBuilder failureReason = new StringBuilder(); 445 if (! LockFileManager.releaseLock(lockFile, failureReason)) 446 { 447 logger.warn(WARN_BACKUPDB_CANNOT_UNLOCK_BACKEND, b.getBackendID(), failureReason); 448 return false; 449 } 450 } 451 catch (Exception e) 452 { 453 logger.warn(WARN_BACKUPDB_CANNOT_UNLOCK_BACKEND, b.getBackendID(), getExceptionMessage(e)); 454 return false; 455 } 456 457 return true; 458 } 459 460 461 /** {@inheritDoc} */ 462 @Override 463 public void interruptTask(TaskState interruptState, LocalizableMessage interruptReason) 464 { 465 if (TaskState.STOPPED_BY_ADMINISTRATOR.equals(interruptState) && 466 backupConfig != null) 467 { 468 addLogMessage(Severity.INFORMATION, TaskMessages.INFO_TASK_STOPPED_BY_ADMIN.get( 469 interruptReason)); 470 setTaskInterruptState(interruptState); 471 backupConfig.cancel(); 472 } 473 } 474 475 476 /** {@inheritDoc} */ 477 @Override 478 public boolean isInterruptable() { 479 return true; 480 } 481 482 483 /** {@inheritDoc} */ 484 @Override 485 protected TaskState runTask() 486 { 487 if (!argumentsAreValid()) 488 { 489 return TaskState.STOPPED_BY_ERROR; 490 } 491 492 boolean multiple; 493 if (backUpAll) 494 { 495 // We'll proceed as if we're backing up multiple backends in this case 496 // even if there's just one. 497 multiple = true; 498 } 499 else 500 { 501 // See if there are multiple backends to archive. 502 multiple = backendsToArchive.size() > 1; 503 } 504 505 506 // Iterate through the backends to archive and back them up individually. 507 boolean errorsEncountered = false; 508 for (Backend<?> b : backendsToArchive) 509 { 510 if (isCancelled()) 511 { 512 break; 513 } 514 515 // Acquire a shared lock for this backend. 516 if (!lockBackend(b)) 517 { 518 errorsEncountered = true; 519 continue; 520 } 521 522 523 try 524 { 525 logger.info(NOTE_BACKUPDB_STARTING_BACKUP, b.getBackendID()); 526 527 528 // Get the path to the directory to use for this backup. If we will be 529 // backing up multiple backends (or if we are backing up all backends, 530 // even if there's only one of them), then create a subdirectory for 531 // each 532 // backend. 533 File backupLocation; 534 if (multiple) 535 { 536 backupLocation = new File(backupDirectory, b.getBackendID()); 537 } 538 else 539 { 540 backupLocation = backupDirectory; 541 } 542 543 544 if (!backupBackend(b, backupLocation)) 545 { 546 errorsEncountered = true; 547 } 548 } 549 finally 550 { 551 // Release the shared lock for the backend. 552 if (!unlockBackend(b)) 553 { 554 errorsEncountered = true; 555 } 556 } 557 } 558 559 560 // Print a final completed message, indicating whether there were any errors 561 // in the process. In this case it means that the backup could not be 562 // completed at least for one of the backends. 563 if (errorsEncountered) 564 { 565 logger.info(NOTE_BACKUPDB_COMPLETED_WITH_ERRORS); 566 return TaskState.STOPPED_BY_ERROR; 567 } 568 else if (isCancelled()) 569 { 570 logger.info(NOTE_BACKUPDB_CANCELLED); 571 return getTaskInterruptState(); 572 } 573 else 574 { 575 logger.info(NOTE_BACKUPDB_COMPLETED_SUCCESSFULLY); 576 return TaskState.COMPLETED_SUCCESSFULLY; 577 } 578 } 579 580 581}