private async Task <bool> CheckIfSnapshotIsEncrypted(string fullPath) { using (var zip = await GetZipArchive(fullPath)) { foreach (var zipEntry in zip.Entries) { if (string.Equals(zipEntry.FullName, RestoreSettings.SettingsFileName, StringComparison.OrdinalIgnoreCase)) { using (var entryStream = zipEntry.Open()) { var json = _context.Read(entryStream, "read database settings"); json.BlittableValidation(); RestoreSettings restoreSettings = JsonDeserializationServer.RestoreSettings(json); return(restoreSettings.DatabaseRecord.Encrypted); } } } } throw new InvalidOperationException("Can't find settings file in backup archive."); }
private static bool CheckIfSnapshotIsEncrypted(string filePath, TransactionOperationContext context) { using (var zip = ZipFile.Open(filePath, ZipArchiveMode.Read, System.Text.Encoding.UTF8)) { foreach (var zipEntry in zip.Entries) { if (string.Equals(zipEntry.FullName, RestoreSettings.SettingsFileName, StringComparison.OrdinalIgnoreCase)) { using (var entryStream = zipEntry.Open()) { var json = context.Read(entryStream, "read database settings"); json.BlittableValidation(); RestoreSettings restoreSettings = JsonDeserializationServer.RestoreSettings(json); return(restoreSettings.DatabaseRecord.Encrypted); } } } } throw new InvalidOperationException("Can't find settings file in backup archive."); }
protected async Task <RestoreSettings> SnapshotRestore(JsonOperationContext context, string backupPath, Action <IOperationProgress> onProgress, RestoreResult restoreResult) { Debug.Assert(onProgress != null); RestoreSettings restoreSettings = null; var fullBackupPath = GetBackupPath(backupPath); using (var zip = await GetZipArchiveForSnapshot(fullBackupPath)) { foreach (var zipEntries in zip.Entries.GroupBy(x => x.FullName.Substring(0, x.FullName.Length - x.Name.Length))) { var directory = zipEntries.Key; if (string.IsNullOrWhiteSpace(directory)) { foreach (var zipEntry in zipEntries) { if (string.Equals(zipEntry.Name, RestoreSettings.SettingsFileName, StringComparison.OrdinalIgnoreCase)) { using (var entryStream = zipEntry.Open()) { var snapshotEncryptionKey = RestoreFromConfiguration.EncryptionKey != null ? Convert.FromBase64String(RestoreFromConfiguration.EncryptionKey) : null; using (var stream = GetInputStream(entryStream, snapshotEncryptionKey)) { var json = context.Read(stream, "read database settings for restore"); json.BlittableValidation(); restoreSettings = JsonDeserializationServer.RestoreSettings(json); restoreSettings.DatabaseRecord.DatabaseName = RestoreFromConfiguration.DatabaseName; DatabaseHelper.Validate(RestoreFromConfiguration.DatabaseName, restoreSettings.DatabaseRecord, _serverStore.Configuration); if (restoreSettings.DatabaseRecord.Encrypted && _hasEncryptionKey == false) { throw new ArgumentException("Database snapshot is encrypted but the encryption key is missing!"); } if (restoreSettings.DatabaseRecord.Encrypted == false && _hasEncryptionKey) { throw new ArgumentException("Cannot encrypt a non encrypted snapshot backup during restore!"); } } } } } continue; } var voronDataDirectory = new VoronPathSetting(RestoreFromConfiguration.DataDirectory); var restoreDirectory = directory.StartsWith(Constants.Documents.PeriodicBackup.Folders.Documents, StringComparison.OrdinalIgnoreCase) ? voronDataDirectory : voronDataDirectory.Combine(directory); BackupMethods.Full.Restore( zipEntries, restoreDirectory, journalDir: null, onProgress: message => { restoreResult.AddInfo(message); restoreResult.SnapshotRestore.ReadCount++; onProgress.Invoke(restoreResult.Progress); }, cancellationToken: _operationCancelToken.Token); } } if (restoreSettings == null) { throw new InvalidDataException("Cannot restore the snapshot without the settings file!"); } return(restoreSettings); }
public async Task <IOperationResult> Execute(Action <IOperationProgress> onProgress) { var databaseName = RestoreFromConfiguration.DatabaseName; var result = new RestoreResult { DataDirectory = RestoreFromConfiguration.DataDirectory }; try { var filesToRestore = await GetOrderedFilesToRestore(); using (_serverStore.ContextPool.AllocateOperationContext(out JsonOperationContext serverContext)) { if (onProgress == null) { onProgress = _ => { } } ; Stopwatch sw = null; RestoreSettings restoreSettings = null; var firstFile = filesToRestore[0]; var extension = Path.GetExtension(firstFile); var snapshotRestore = false; if ((extension == Constants.Documents.PeriodicBackup.SnapshotExtension) || (extension == Constants.Documents.PeriodicBackup.EncryptedSnapshotExtension)) { onProgress.Invoke(result.Progress); snapshotRestore = true; sw = Stopwatch.StartNew(); if (extension == Constants.Documents.PeriodicBackup.EncryptedSnapshotExtension) { _hasEncryptionKey = RestoreFromConfiguration.EncryptionKey != null || RestoreFromConfiguration.BackupEncryptionSettings?.Key != null; } // restore the snapshot restoreSettings = await SnapshotRestore(serverContext, firstFile, onProgress, result); if (restoreSettings != null && RestoreFromConfiguration.SkipIndexes) { // remove all indexes from the database record restoreSettings.DatabaseRecord.AutoIndexes = null; restoreSettings.DatabaseRecord.Indexes = null; } // removing the snapshot from the list of files filesToRestore.RemoveAt(0); } else { result.SnapshotRestore.Skipped = true; result.SnapshotRestore.Processed = true; onProgress.Invoke(result.Progress); } if (restoreSettings == null) { restoreSettings = new RestoreSettings { DatabaseRecord = new DatabaseRecord(databaseName) { // we only have a smuggler restore // use the encryption key to encrypt the database Encrypted = _hasEncryptionKey } }; DatabaseHelper.Validate(databaseName, restoreSettings.DatabaseRecord, _serverStore.Configuration); } var databaseRecord = restoreSettings.DatabaseRecord; if (databaseRecord.Settings == null) { databaseRecord.Settings = new Dictionary <string, string>(); } var runInMemoryConfigurationKey = RavenConfiguration.GetKey(x => x.Core.RunInMemory); databaseRecord.Settings.Remove(runInMemoryConfigurationKey); if (_serverStore.Configuration.Core.RunInMemory) { databaseRecord.Settings[runInMemoryConfigurationKey] = "false"; } var dataDirectoryConfigurationKey = RavenConfiguration.GetKey(x => x.Core.DataDirectory); databaseRecord.Settings.Remove(dataDirectoryConfigurationKey); // removing because we want to restore to given location, not to serialized in backup one if (_restoringToDefaultDataDirectory == false) { databaseRecord.Settings[dataDirectoryConfigurationKey] = RestoreFromConfiguration.DataDirectory; } if (_hasEncryptionKey) { // save the encryption key so we'll be able to access the database _serverStore.PutSecretKey(RestoreFromConfiguration.EncryptionKey, databaseName, overwrite: false); } var addToInitLog = new Action <string>(txt => // init log is not save in mem during RestoreBackup { var msg = $"[RestoreBackup] {DateTime.UtcNow} :: Database '{databaseName}' : {txt}"; if (Logger.IsInfoEnabled) { Logger.Info(msg); } }); var configuration = _serverStore .DatabasesLandlord .CreateDatabaseConfiguration(databaseName, ignoreDisabledDatabase: true, ignoreBeenDeleted: true, ignoreNotRelevant: true, databaseRecord); using (var database = new DocumentDatabase(databaseName, configuration, _serverStore, addToInitLog)) { // smuggler needs an existing document database to operate var options = InitializeOptions.SkipLoadingDatabaseRecord; if (snapshotRestore) { options |= InitializeOptions.GenerateNewDatabaseId; } database.Initialize(options); databaseRecord.Topology = new DatabaseTopology(); // restoring to the current node only databaseRecord.Topology.Members.Add(_nodeTag); // we are currently restoring, shouldn't try to access it databaseRecord.DatabaseState = DatabaseStateStatus.RestoreInProgress; var(index, _) = await _serverStore.WriteDatabaseRecordAsync(databaseName, databaseRecord, null, RaftIdGenerator.NewId(), restoreSettings.DatabaseValues, isRestore : true); await _serverStore.Cluster.WaitForIndexNotification(index); using (database.DocumentsStorage.ContextPool.AllocateOperationContext(out DocumentsOperationContext context)) { if (snapshotRestore) { await RestoreFromSmugglerFile(onProgress, database, firstFile, context); await SmugglerRestore(database, filesToRestore, context, databaseRecord, onProgress, result); result.SnapshotRestore.Processed = true; var summary = database.GetDatabaseSummary(); result.Documents.ReadCount += summary.DocumentsCount; result.Documents.Attachments.ReadCount += summary.AttachmentsCount; result.Counters.ReadCount += summary.CounterEntriesCount; result.RevisionDocuments.ReadCount += summary.RevisionsCount; result.Conflicts.ReadCount += summary.ConflictsCount; result.Indexes.ReadCount += databaseRecord.GetIndexesCount(); result.CompareExchange.ReadCount += summary.CompareExchangeCount; result.CompareExchangeTombstones.ReadCount += summary.CompareExchangeTombstonesCount; result.Identities.ReadCount += summary.IdentitiesCount; result.AddInfo($"Successfully restored {result.SnapshotRestore.ReadCount} files during snapshot restore, took: {sw.ElapsedMilliseconds:#,#;;0}ms"); onProgress.Invoke(result.Progress); } else { await SmugglerRestore(database, filesToRestore, context, databaseRecord, onProgress, result); } DisableOngoingTasksIfNeeded(databaseRecord); result.DatabaseRecord.Processed = true; result.Documents.Processed = true; result.RevisionDocuments.Processed = true; result.Conflicts.Processed = true; result.Indexes.Processed = true; result.Counters.Processed = true; result.Identities.Processed = true; result.CompareExchange.Processed = true; result.Subscriptions.Processed = true; onProgress.Invoke(result.Progress); } } // after the db for restore is done, we can safely set the db state to normal and write the DatabaseRecord databaseRecord.DatabaseState = DatabaseStateStatus.Normal; var(updateIndex, _) = await _serverStore.WriteDatabaseRecordAsync(databaseName, databaseRecord, null, RaftIdGenerator.DontCareId, isRestore : true); await _serverStore.Cluster.WaitForIndexNotification(updateIndex); if (databaseRecord.Topology.RelevantFor(_serverStore.NodeTag)) { // we need to wait for the database record change to be propagated properly var db = await _serverStore.DatabasesLandlord.TryGetOrCreateResourceStore(databaseName); await db.RachisLogIndexNotifications.WaitForIndexNotification(updateIndex, _operationCancelToken.Token); } return(result); } } catch (Exception e) { if (Logger.IsOperationsEnabled) { Logger.Operations("Failed to restore database", e); } var alert = AlertRaised.Create( RestoreFromConfiguration.DatabaseName, "Failed to restore database", $"Could not restore database named {RestoreFromConfiguration.DatabaseName}", AlertType.RestoreError, NotificationSeverity.Error, details: new ExceptionDetails(e)); _serverStore.NotificationCenter.Add(alert); using (_serverStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context)) { bool databaseExists; using (context.OpenReadTransaction()) { databaseExists = _serverStore.Cluster.DatabaseExists(context, RestoreFromConfiguration.DatabaseName); } if (databaseExists == false) { // delete any files that we already created during the restore IOExtensions.DeleteDirectory(RestoreFromConfiguration.DataDirectory); } else { var deleteResult = await _serverStore.DeleteDatabaseAsync(RestoreFromConfiguration.DatabaseName, true, new[] { _serverStore.NodeTag }, RaftIdGenerator.DontCareId); await _serverStore.Cluster.WaitForIndexNotification(deleteResult.Index); } } result.AddError($"Error occurred during restore of database {databaseName}. Exception: {e.Message}"); onProgress.Invoke(result.Progress); throw; } finally { Dispose(); } }
public async Task <IOperationResult> Execute(Action <IOperationProgress> onProgress) { var databaseName = _restoreConfiguration.DatabaseName; var result = new RestoreResult { DataDirectory = _restoreConfiguration.DataDirectory }; try { if (onProgress == null) { onProgress = _ => { } } ; Stopwatch sw = null; RestoreSettings restoreSettings = null; var firstFile = _filesToRestore[0]; var lastFile = _filesToRestore.Last(); var extension = Path.GetExtension(firstFile); var snapshotRestore = false; if (extension == Constants.Documents.PeriodicBackup.SnapshotExtension) { onProgress.Invoke(result.Progress); snapshotRestore = true; sw = Stopwatch.StartNew(); // restore the snapshot restoreSettings = SnapshotRestore(firstFile, _restoreConfiguration.DataDirectory, onProgress, result); // removing the snapshot from the list of files _filesToRestore.RemoveAt(0); } else { result.SnapshotRestore.Skipped = true; result.SnapshotRestore.Processed = true; onProgress.Invoke(result.Progress); } if (restoreSettings == null) { restoreSettings = new RestoreSettings { DatabaseRecord = new DatabaseRecord(databaseName) { // we only have a smuggler restore // use the encryption key to encrypt the database Encrypted = _hasEncryptionKey } }; DatabaseHelper.Validate(databaseName, restoreSettings.DatabaseRecord, _serverStore.Configuration); } var databaseRecord = restoreSettings.DatabaseRecord; if (databaseRecord.Settings == null) { databaseRecord.Settings = new Dictionary <string, string>(); } var runInMemoryConfigurationKey = RavenConfiguration.GetKey(x => x.Core.RunInMemory); databaseRecord.Settings.Remove(runInMemoryConfigurationKey); if (_serverStore.Configuration.Core.RunInMemory) { databaseRecord.Settings[runInMemoryConfigurationKey] = "false"; } var dataDirectoryConfigurationKey = RavenConfiguration.GetKey(x => x.Core.DataDirectory); databaseRecord.Settings.Remove(dataDirectoryConfigurationKey); // removing because we want to restore to given location, not to serialized in backup one if (_restoringToDefaultDataDirectory == false) { databaseRecord.Settings[dataDirectoryConfigurationKey] = _restoreConfiguration.DataDirectory; } if (_hasEncryptionKey) { // save the encryption key so we'll be able to access the database _serverStore.PutSecretKey(_restoreConfiguration.EncryptionKey, databaseName, overwrite: false); } var addToInitLog = new Action <string>(txt => // init log is not save in mem during RestoreBackup { var msg = $"[RestoreBackup] {DateTime.UtcNow} :: Database '{databaseName}' : {txt}"; if (Logger.IsInfoEnabled) { Logger.Info(msg); } }); using (var database = new DocumentDatabase(databaseName, new RavenConfiguration(databaseName, ResourceType.Database) { Core = { DataDirectory = new PathSetting(_restoreConfiguration.DataDirectory), RunInMemory = false } }, _serverStore, addToInitLog)) { // smuggler needs an existing document database to operate var options = InitializeOptions.SkipLoadingDatabaseRecord; if (snapshotRestore) { options |= InitializeOptions.GenerateNewDatabaseId; } database.Initialize(options); if (snapshotRestore) { result.SnapshotRestore.Processed = true; var summary = database.GetDatabaseSummary(); result.Documents.ReadCount += summary.DocumentsCount; result.Documents.Attachments.ReadCount += summary.AttachmentsCount; result.Counters.ReadCount += summary.CountersCount; result.RevisionDocuments.ReadCount += summary.RevisionsCount; result.Conflicts.ReadCount += summary.ConflictsCount; result.Indexes.ReadCount += databaseRecord.GetIndexesCount(); result.AddInfo($"Successfully restored {result.SnapshotRestore.ReadCount} " + $"files during snapshot restore, took: {sw.ElapsedMilliseconds:#,#;;0}ms"); onProgress.Invoke(result.Progress); } using (database.DocumentsStorage.ContextPool.AllocateOperationContext(out DocumentsOperationContext context)) { SmugglerRestore(_restoreConfiguration.BackupLocation, database, context, databaseRecord, onProgress, result); result.DatabaseRecord.Processed = true; result.Documents.Processed = true; result.RevisionDocuments.Processed = true; result.Conflicts.Processed = true; result.Indexes.Processed = true; result.Counters.Processed = true; onProgress.Invoke(result.Progress); databaseRecord.Topology = new DatabaseTopology(); // restoring to the current node only databaseRecord.Topology.Members.Add(_nodeTag); databaseRecord.Disabled = true; // we are currently restoring, shouldn't try to access it _serverStore.EnsureNotPassive(); DisableOngoingTasksIfNeeded(databaseRecord); var(index, _) = await _serverStore.WriteDatabaseRecordAsync(databaseName, databaseRecord, null, restoreSettings.DatabaseValues, isRestore : true); await _serverStore.Cluster.WaitForIndexNotification(index); // restore identities & cmpXchg values RestoreFromLastFile(onProgress, database, lastFile, context, result); } } // after the db for restore is done, we can safely set the db status to active databaseRecord = _serverStore.LoadDatabaseRecord(databaseName, out _); databaseRecord.Disabled = false; var(updateIndex, _) = await _serverStore.WriteDatabaseRecordAsync(databaseName, databaseRecord, null); await _serverStore.Cluster.WaitForIndexNotification(updateIndex); return(result); } catch (Exception e) { if (Logger.IsOperationsEnabled) { Logger.Operations("Failed to restore database", e); } var alert = AlertRaised.Create( _restoreConfiguration.DatabaseName, "Failed to restore database", $"Could not restore database named {_restoreConfiguration.DatabaseName}", AlertType.RestoreError, NotificationSeverity.Error, details: new ExceptionDetails(e)); _serverStore.NotificationCenter.Add(alert); if (_serverStore.LoadDatabaseRecord(_restoreConfiguration.DatabaseName, out var _) == null) { // delete any files that we already created during the restore IOExtensions.DeleteDirectory(_restoreConfiguration.DataDirectory); } else { var deleteResult = await _serverStore.DeleteDatabaseAsync(_restoreConfiguration.DatabaseName, true, new[] { _serverStore.NodeTag }); await _serverStore.Cluster.WaitForIndexNotification(deleteResult.Index); } result.AddError($"Error occurred during restore of database {databaseName}. Exception: {e.Message}"); onProgress.Invoke(result.Progress); throw; } finally { _operationCancelToken.Dispose(); } }
private RestoreSettings SnapshotRestore( string backupPath, string dataDirectory, Action <IOperationProgress> onProgress, RestoreResult restoreResult) { Debug.Assert(onProgress != null); RestoreSettings restoreSettings = null; var voronBackupPath = new VoronPathSetting(backupPath); var voronDataDirectory = new VoronPathSetting(dataDirectory); using (var zip = ZipFile.Open(voronBackupPath.FullPath, ZipArchiveMode.Read, System.Text.Encoding.UTF8)) { foreach (var zipEntries in zip.Entries.GroupBy(x => x.FullName.Substring(0, x.FullName.Length - x.Name.Length))) { var directory = zipEntries.Key; if (string.IsNullOrWhiteSpace(directory)) { foreach (var zipEntry in zipEntries) { if (string.Equals(zipEntry.Name, RestoreSettings.SettingsFileName, StringComparison.OrdinalIgnoreCase)) { using (var entryStream = zipEntry.Open()) using (_serverStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context)) { var json = context.Read(entryStream, "read database settings for restore"); json.BlittableValidation(); restoreSettings = JsonDeserializationServer.RestoreSettings(json); restoreSettings.DatabaseRecord.DatabaseName = _restoreConfiguration.DatabaseName; DatabaseHelper.Validate(_restoreConfiguration.DatabaseName, restoreSettings.DatabaseRecord); if (restoreSettings.DatabaseRecord.Encrypted && _hasEncryptionKey == false) { throw new ArgumentException("Database snapshot is encrypted but the encryption key is missing!"); } if (restoreSettings.DatabaseRecord.Encrypted == false && _hasEncryptionKey) { throw new ArgumentException("Cannot encrypt a non encrypted snapshot backup during restore!"); } } } } continue; } var restoreDirectory = directory.StartsWith(Constants.Documents.PeriodicBackup.Folders.Documents, StringComparison.OrdinalIgnoreCase) ? voronDataDirectory : voronDataDirectory.Combine(directory); BackupMethods.Full.Restore( zipEntries, restoreDirectory, journalDir: null, onProgress: message => { restoreResult.AddInfo(message); restoreResult.SnapshotRestore.ReadCount++; onProgress.Invoke(restoreResult.Progress); }, cancellationToken: _operationCancelToken.Token); } } if (restoreSettings == null) { throw new InvalidDataException("Cannot restore the snapshot without the settings file!"); } return(restoreSettings); }
protected async Task <RestoreSettings> SnapshotRestore(JsonOperationContext context, string backupPath, Action <IOperationProgress> onProgress, RestoreResult restoreResult) { Debug.Assert(onProgress != null); RestoreSettings restoreSettings = null; var fullBackupPath = GetBackupPath(backupPath); using (var zip = await GetZipArchiveForSnapshot(fullBackupPath)) { var restorePath = new VoronPathSetting(RestoreFromConfiguration.DataDirectory); if (Directory.Exists(restorePath.FullPath) == false) { Directory.CreateDirectory(restorePath.FullPath); } // validate free space var snapshotSize = zip.Entries.Sum(entry => entry.Length); BackupHelper.AssertFreeSpaceForSnapshot(restorePath.FullPath, snapshotSize, "restore a backup", Logger); foreach (var zipEntries in zip.Entries.GroupBy(x => x.FullName.Substring(0, x.FullName.Length - x.Name.Length))) { var directory = zipEntries.Key; if (string.IsNullOrWhiteSpace(directory)) { foreach (var zipEntry in zipEntries) { if (string.Equals(zipEntry.Name, RestoreSettings.SettingsFileName, StringComparison.OrdinalIgnoreCase)) { await using (var entryStream = zipEntry.Open()) { var snapshotEncryptionKey = RestoreFromConfiguration.EncryptionKey != null ? Convert.FromBase64String(RestoreFromConfiguration.EncryptionKey) : null; await using (var stream = GetInputStream(entryStream, snapshotEncryptionKey)) { var json = await context.ReadForMemoryAsync(stream, "read database settings for restore"); json.BlittableValidation(); restoreSettings = JsonDeserializationServer.RestoreSettings(json); restoreSettings.DatabaseRecord.DatabaseName = RestoreFromConfiguration.DatabaseName; DatabaseHelper.Validate(RestoreFromConfiguration.DatabaseName, restoreSettings.DatabaseRecord, _serverStore.Configuration); if (restoreSettings.DatabaseRecord.Encrypted && _hasEncryptionKey == false) { throw new ArgumentException("Database snapshot is encrypted but the encryption key is missing!"); } if (restoreSettings.DatabaseRecord.Encrypted == false && _hasEncryptionKey) { throw new ArgumentException("Cannot encrypt a non encrypted snapshot backup during restore!"); } } } } } continue; } var restoreDirectory = directory.StartsWith(Constants.Documents.PeriodicBackup.Folders.Documents, StringComparison.OrdinalIgnoreCase) ? restorePath : restorePath.Combine(directory); var isSubDirectory = PathUtil.IsSubDirectory(restoreDirectory.FullPath, restorePath.FullPath); if (isSubDirectory == false) { var extensions = zipEntries .Select(x => Path.GetExtension(x.Name)) .Distinct() .ToArray(); if (extensions.Length != 1 || string.Equals(extensions[0], TableValueCompressor.CompressionRecoveryExtension, StringComparison.OrdinalIgnoreCase) == false) { throw new InvalidOperationException($"Encountered invalid directory '{directory}' in snapshot file with following file extensions: {string.Join(", ", extensions)}"); } // this enables backward compatibility of snapshot backups with compression recovery files before fix was made in RavenDB-17173 // the underlying issue was that we were putting full path when compression recovery files were backed up using snapshot // because of that the end restore directory was not a sub-directory of a restore path // which could result in a file already exists exception // since restoring of compression recovery files is not mandatory then it is safe to skip them continue; } BackupMethods.Full.Restore( zipEntries, restoreDirectory, journalDir: null, onProgress: message => { restoreResult.AddInfo(message); restoreResult.SnapshotRestore.ReadCount++; onProgress.Invoke(restoreResult.Progress); }, cancellationToken: _operationCancelToken.Token); } } if (restoreSettings == null) { throw new InvalidDataException("Cannot restore the snapshot without the settings file!"); } return(restoreSettings); }