diff --git a/managed/src/main/java/api/v2/controllers/ContinuousBackupApiControllerImp.java b/managed/src/main/java/api/v2/controllers/ContinuousBackupApiControllerImp.java index b60fb79b6301..57d751b05b53 100644 --- a/managed/src/main/java/api/v2/controllers/ContinuousBackupApiControllerImp.java +++ b/managed/src/main/java/api/v2/controllers/ContinuousBackupApiControllerImp.java @@ -4,7 +4,7 @@ import api.v2.handlers.ContinuousBackupHandler; import api.v2.models.ContinuousBackup; -import api.v2.models.ContinuousBackupCreateSpec; +import api.v2.models.ContinuousBackupSpec; import api.v2.models.ContinuousRestoreSpec; import api.v2.models.YBATask; import com.google.inject.Inject; @@ -16,7 +16,7 @@ public class ContinuousBackupApiControllerImp extends ContinuousBackupApiControl @Override public ContinuousBackup createContinuousBackup( - Http.Request request, UUID cUUID, ContinuousBackupCreateSpec continuousBackupCreateSpec) + Http.Request request, UUID cUUID, ContinuousBackupSpec continuousBackupCreateSpec) throws Exception { return cbHandler.createContinuousBackup(request, cUUID, continuousBackupCreateSpec); } @@ -29,10 +29,7 @@ public ContinuousBackup deleteContinuousBackup(Http.Request request, UUID cUUID, @Override public ContinuousBackup editContinuousBackup( - Http.Request request, - UUID cUUID, - UUID bUUID, - ContinuousBackupCreateSpec continuousBackupCreateSpec) + Http.Request request, UUID cUUID, UUID bUUID, ContinuousBackupSpec continuousBackupCreateSpec) throws Exception { return cbHandler.editContinuousBackup(request, cUUID, bUUID, continuousBackupCreateSpec); } diff --git a/managed/src/main/java/api/v2/handlers/ContinuousBackupHandler.java b/managed/src/main/java/api/v2/handlers/ContinuousBackupHandler.java index 8e6bc0b25fd7..ef3af303ac26 100644 --- a/managed/src/main/java/api/v2/handlers/ContinuousBackupHandler.java +++ b/managed/src/main/java/api/v2/handlers/ContinuousBackupHandler.java @@ -5,17 +5,19 @@ import api.v2.mappers.ContinuousBackupMapper; import api.v2.models.ContinuousBackup; -import api.v2.models.ContinuousBackupCreateSpec; +import api.v2.models.ContinuousBackupSpec; import api.v2.models.ContinuousRestoreSpec; import api.v2.models.YBATask; import api.v2.utils.ApiControllerUtils; import com.google.inject.Inject; +import com.yugabyte.yw.commissioner.tasks.CreateYbaBackup; import com.yugabyte.yw.commissioner.tasks.RestoreContinuousBackup; import com.yugabyte.yw.common.PlatformServiceException; import com.yugabyte.yw.models.ContinuousBackupConfig; import com.yugabyte.yw.models.Customer; +import com.yugabyte.yw.models.Schedule; +import com.yugabyte.yw.models.helpers.TaskType; import com.yugabyte.yw.models.helpers.TimeUnit; -import java.util.List; import java.util.Optional; import java.util.UUID; import play.mvc.Http; @@ -25,9 +27,13 @@ public class ContinuousBackupHandler extends ApiControllerUtils { @Inject private YbaBackupHandler ybaBackupHandler; public ContinuousBackup createContinuousBackup( - Http.Request request, UUID cUUID, ContinuousBackupCreateSpec continuousBackupCreateSpec) + Http.Request request, UUID cUUID, ContinuousBackupSpec continuousBackupCreateSpec) throws Exception { + // Check if there is an existing config + if (ContinuousBackupConfig.get().isPresent()) { + throw new PlatformServiceException(BAD_REQUEST, "Continuous backup config already exists."); + } ContinuousBackupConfig cbConfig = ContinuousBackupConfig.create( continuousBackupCreateSpec.getStorageConfigUuid(), @@ -35,6 +41,20 @@ public ContinuousBackup createContinuousBackup( TimeUnit.valueOf(continuousBackupCreateSpec.getFrequencyTimeUnit().name()), continuousBackupCreateSpec.getNumBackups(), continuousBackupCreateSpec.getBackupDir()); + CreateYbaBackup.Params taskParams = new CreateYbaBackup.Params(); + taskParams.storageConfigUUID = cbConfig.getStorageConfigUUID(); + taskParams.dirName = cbConfig.getBackupDir(); + // TODO: list of components? + Schedule schedule = + Schedule.create( + cUUID, + cbConfig.getUuid(), + taskParams, + TaskType.CreateYbaBackup, + cbConfig.getFrequency(), + null, + cbConfig.getFrequencyTimeUnit(), + null); return ContinuousBackupMapper.INSTANCE.toContinuousBackup(cbConfig); } @@ -49,14 +69,12 @@ public ContinuousBackup deleteContinuousBackup(Http.Request request, UUID cUUID, } public ContinuousBackup editContinuousBackup( - Http.Request request, - UUID cUUID, - UUID bUUID, - ContinuousBackupCreateSpec continuousBackupCreateSpec) + Http.Request request, UUID cUUID, UUID bUUID, ContinuousBackupSpec continuousBackupCreateSpec) throws Exception { Optional optional = ContinuousBackupConfig.get(bUUID); if (!optional.isPresent()) { - throw new PlatformServiceException(BAD_REQUEST, "no continous backup config found with UUID"); + throw new PlatformServiceException( + BAD_REQUEST, "No continous backup config found with UUID " + bUUID); } ContinuousBackupConfig cbConfig = optional.get(); // TODO: Actual edit work @@ -64,11 +82,11 @@ public ContinuousBackup editContinuousBackup( } public ContinuousBackup getContinuousBackup(Http.Request request, UUID cUUID) throws Exception { - List cbConfigs = ContinuousBackupConfig.getAll(); - if (cbConfigs.size() < 1) { + Optional cbConfigOpt = ContinuousBackupConfig.get(); + if (!cbConfigOpt.isPresent()) { throw new PlatformServiceException(NOT_FOUND, "No continuous backup config found."); } - ContinuousBackupConfig cbConfig = cbConfigs.get(0); + ContinuousBackupConfig cbConfig = cbConfigOpt.get(); return ContinuousBackupMapper.INSTANCE.toContinuousBackup(cbConfig); } diff --git a/managed/src/main/java/api/v2/handlers/IsolatedBackupHandler.java b/managed/src/main/java/api/v2/handlers/IsolatedBackupHandler.java index 33da075f9195..ce07baec9cc6 100644 --- a/managed/src/main/java/api/v2/handlers/IsolatedBackupHandler.java +++ b/managed/src/main/java/api/v2/handlers/IsolatedBackupHandler.java @@ -19,7 +19,7 @@ public YBATask createYbaBackup(Http.Request request, UUID cUUID, IsolatedBackupC throws Exception { Customer customer = Customer.getOrBadRequest(cUUID); CreateYbaBackup.Params taskParams = new CreateYbaBackup.Params(); - taskParams.localDir = spec.getLocalDir(); + taskParams.dirName = spec.getLocalDir(); taskParams.components = spec.getComponents(); UUID taskUUID = ybaBackupHandler.createBackup(customer, taskParams); return new YBATask().taskUuid(taskUUID); diff --git a/managed/src/main/java/api/v2/mappers/ContinuousBackupMapper.java b/managed/src/main/java/api/v2/mappers/ContinuousBackupMapper.java index 68d161e8da96..894dd021e2e8 100644 --- a/managed/src/main/java/api/v2/mappers/ContinuousBackupMapper.java +++ b/managed/src/main/java/api/v2/mappers/ContinuousBackupMapper.java @@ -2,6 +2,7 @@ import api.v2.models.ContinuousBackup; import api.v2.models.ContinuousBackupInfo; +import api.v2.models.ContinuousBackupSpec; import api.v2.models.TimeUnitType; import com.yugabyte.yw.models.ContinuousBackupConfig; import java.time.OffsetDateTime; @@ -15,14 +16,17 @@ public interface ContinuousBackupMapper { default ContinuousBackup toContinuousBackup(ContinuousBackupConfig cbConfig) { ContinuousBackup v2ContinuousBackup = new ContinuousBackup(); ContinuousBackupInfo v2ContinuousBackupInfo = new ContinuousBackupInfo(); + ContinuousBackupSpec v2ContinuousBackupSpec = new ContinuousBackupSpec(); v2ContinuousBackupInfo.setUuid(cbConfig.getUuid()); - v2ContinuousBackupInfo.setFrequency(cbConfig.getFrequency()); - v2ContinuousBackupInfo.setFrequencyTimeUnit( + v2ContinuousBackupSpec.setStorageConfigUuid(cbConfig.getStorageConfigUUID()); + v2ContinuousBackupSpec.setFrequency(cbConfig.getFrequency()); + v2ContinuousBackupSpec.setFrequencyTimeUnit( TimeUnitType.valueOf(cbConfig.getFrequencyTimeUnit().name())); // TODO: compute from actual cbConfig v2ContinuousBackupInfo.setStorageLocation("s3://backup_bucket/YBA.1.2.3.4/"); v2ContinuousBackupInfo.setLastBackup(OffsetDateTime.parse("2024-08-19T10:30:45-04:00")); v2ContinuousBackup.setInfo(v2ContinuousBackupInfo); + v2ContinuousBackup.setSpec(v2ContinuousBackupSpec); return v2ContinuousBackup; } } diff --git a/managed/src/main/java/com/yugabyte/yw/commissioner/tasks/CreateYbaBackup.java b/managed/src/main/java/com/yugabyte/yw/commissioner/tasks/CreateYbaBackup.java index b61ad71ce583..0cb75f02fd6e 100644 --- a/managed/src/main/java/com/yugabyte/yw/commissioner/tasks/CreateYbaBackup.java +++ b/managed/src/main/java/com/yugabyte/yw/commissioner/tasks/CreateYbaBackup.java @@ -10,24 +10,56 @@ package com.yugabyte.yw.commissioner.tasks; +import static com.yugabyte.yw.common.Util.NULL_UUID; +import static play.mvc.Http.Status.INTERNAL_SERVER_ERROR; + import api.v2.models.YbaComponent; import com.google.inject.Inject; import com.yugabyte.yw.commissioner.AbstractTaskBase; import com.yugabyte.yw.commissioner.BaseTaskDependencies; +import com.yugabyte.yw.commissioner.Commissioner; +import com.yugabyte.yw.common.CloudUtil; +import com.yugabyte.yw.common.CloudUtilFactory; +import com.yugabyte.yw.common.PlatformServiceException; +import com.yugabyte.yw.common.ShellResponse; +import com.yugabyte.yw.common.ha.PlatformReplicationHelper; +import com.yugabyte.yw.common.ha.PlatformReplicationManager; import com.yugabyte.yw.forms.AbstractTaskParams; +import com.yugabyte.yw.models.Customer; +import com.yugabyte.yw.models.CustomerTask; +import com.yugabyte.yw.models.Schedule; +import com.yugabyte.yw.models.ScheduleTask; +import com.yugabyte.yw.models.configs.CustomerConfig; +import com.yugabyte.yw.models.helpers.TaskType; +import java.io.File; import java.util.List; +import java.util.Optional; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import play.libs.Json; @Slf4j public class CreateYbaBackup extends AbstractTaskBase { + private final CloudUtilFactory cloudUtilFactory; + private final PlatformReplicationHelper replicationHelper; + private final PlatformReplicationManager replicationManager; + @Inject - protected CreateYbaBackup(BaseTaskDependencies baseTaskDependencies) { + protected CreateYbaBackup( + BaseTaskDependencies baseTaskDependencies, + PlatformReplicationHelper replicationHelper, + PlatformReplicationManager replicationManager, + CloudUtilFactory cloudUtilFactory) { super(baseTaskDependencies); + this.replicationHelper = replicationHelper; + this.replicationManager = replicationManager; + this.cloudUtilFactory = cloudUtilFactory; } public static class Params extends AbstractTaskParams { - public String localDir; + public UUID storageConfigUUID; + public String dirName; public List components; } @@ -36,9 +68,76 @@ protected Params taskParams() { return (Params) taskParams; } + public void runScheduledBackup( + Schedule schedule, Commissioner commissioner, boolean alreadyRunning) { + log.info("Execution of scheduled YBA backup"); + if (alreadyRunning) { + log.info("Continuous backup already running, skipping."); + return; + } + UUID customerUUID = schedule.getCustomerUUID(); + Customer customer = Customer.get(customerUUID); + CreateYbaBackup.Params taskParams = + Json.fromJson(schedule.getTaskParams(), CreateYbaBackup.Params.class); + + if (schedule.isBacklogStatus()) { + schedule.updateBacklogStatus(false); + } + + UUID taskUUID = commissioner.submit(TaskType.CreateYbaBackup, taskParams); + ScheduleTask.create(taskUUID, schedule.getScheduleUUID()); + CustomerTask.create( + customer, + NULL_UUID, + taskUUID, + CustomerTask.TargetType.Yba, + CustomerTask.TaskType.CreateYbaBackup, + // TODO: Actually get platform IP + "platform_ip"); + log.info("Submitted continuous yba backup creation with task uuid = {}.", taskUUID); + } + @Override public void run() { - log.info("Dummy exeuction of CreateYbaBackup"); + log.info("Execution of CreateYbaBackup"); + CreateYbaBackup.Params taskParams = taskParams(); + if (taskParams.storageConfigUUID != null) { + log.debug("Creating platform backup..."); + ShellResponse response = + replicationHelper.runCommand(replicationManager.new CreatePlatformBackupParams()); + + if (response.code != 0) { + log.error("Backup failed: " + response.message); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "Backup failed: " + response.message); + } + Optional backupOpt = replicationHelper.getMostRecentBackup(); + if (!backupOpt.isPresent()) { + throw new PlatformServiceException(INTERNAL_SERVER_ERROR, "could not find backup file"); + } + File backup = backupOpt.get(); + CustomerConfig customerConfig = CustomerConfig.get(taskParams.storageConfigUUID); + if (customerConfig == null) { + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, + "Could not find customer config with provided storage config UUID."); + } + CloudUtil cloudUtil = cloudUtilFactory.getCloudUtil(customerConfig.getName()); + if (!cloudUtil.uploadYbaBackup(customerConfig.getDataObject(), backup, taskParams.dirName)) { + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "Could not upload YBA backup to cloud storage."); + } + + if (!cloudUtil.cleanupUploadedBackups(customerConfig.getDataObject(), taskParams.dirName)) { + log.warn( + "Error cleaning up uploaded backups to cloud storage, please delete manually to avoid" + + " incurring unexpected costs."); + } + + // Cleanup backups + replicationHelper.cleanupCreatedBackups(); + log.info(backup.getAbsolutePath()); + } return; } } diff --git a/managed/src/main/java/com/yugabyte/yw/common/AWSUtil.java b/managed/src/main/java/com/yugabyte/yw/common/AWSUtil.java index 58004e768f67..87c10007d062 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/AWSUtil.java +++ b/managed/src/main/java/com/yugabyte/yw/common/AWSUtil.java @@ -42,6 +42,7 @@ import com.amazonaws.services.s3.model.GetBucketLocationRequest; import com.amazonaws.services.s3.model.ListObjectsV2Request; import com.amazonaws.services.s3.model.ListObjectsV2Result; +import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.model.S3ObjectSummary; import com.google.inject.Inject; @@ -63,10 +64,12 @@ import com.yugabyte.yw.models.configs.data.CustomerConfigStorageS3Data; import com.yugabyte.yw.models.configs.data.CustomerConfigStorageS3Data.RegionLocations; import com.yugabyte.yw.models.helpers.NodeDetails; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -77,6 +80,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -251,6 +255,111 @@ public void checkConfigTypeAndBackupLocationSame(String backupLocation) { } } + @Override + public boolean uploadYbaBackup(CustomerConfigData configData, File backup, String backupDir) { + try { + maybeDisableCertVerification(); + CustomerConfigStorageS3Data s3Data = (CustomerConfigStorageS3Data) configData; + AmazonS3 client = createS3Client(s3Data); + CloudLocationInfo cLInfo = + getCloudLocationInfo(YbcBackupUtil.DEFAULT_REGION_STRING, s3Data, ""); + // Construct full path to upload like (s3://bucket/)cloudPath/backupDir/backupName.tar.gz + String keyName = + Stream.of(cLInfo.cloudPath, backupDir, backup.getName()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining("/")); + + PutObjectRequest request = new PutObjectRequest(cLInfo.bucket, keyName, backup); + client.putObject(request); + } catch (Exception e) { + log.error("Error uploading backup: {}", e); + return false; + } finally { + maybeEnableCertVerification(); + } + return true; + } + + @Override + public boolean cleanupUploadedBackups(CustomerConfigData configData, String backupDir) { + try { + maybeDisableCertVerification(); + CustomerConfigStorageS3Data s3Data = (CustomerConfigStorageS3Data) configData; + AmazonS3 client = createS3Client(s3Data); + CloudLocationInfo cLInfo = + getCloudLocationInfo(YbcBackupUtil.DEFAULT_REGION_STRING, s3Data, ""); + log.info("Cleaning up uploaded backups in S3 location s3://{}/{}", cLInfo.bucket, backupDir); + + // Get all the backups in the specified bucket/directory + List allBackups = new ArrayList<>(); + String nextContinuationToken = null; + do { + ListObjectsV2Result listObjectsResult; + ListObjectsV2Request request = + new ListObjectsV2Request().withBucketName(cLInfo.bucket).withPrefix(backupDir); + if (StringUtils.isNotBlank(nextContinuationToken)) { + request.withContinuationToken(nextContinuationToken); + } + listObjectsResult = client.listObjectsV2(request); + + if (listObjectsResult.getKeyCount() == 0) { + break; + } + nextContinuationToken = null; + if (listObjectsResult.isTruncated()) { + nextContinuationToken = listObjectsResult.getNextContinuationToken(); + } + allBackups.addAll(listObjectsResult.getObjectSummaries()); + } while (nextContinuationToken != null); + + // Sort backups by last modified date (most recent first) + List sortedBackups = + allBackups.stream() + .sorted((o1, o2) -> o2.getLastModified().compareTo(o1.getLastModified())) + .collect(Collectors.toList()); + + // Only keep the n most recent backups + int numKeepBackups = + runtimeConfGetter.getGlobalConf(GlobalConfKeys.numCloudYbaBackupsRetention); + if (sortedBackups.size() <= numKeepBackups) { + log.info( + "No backups to delete, only {} backups in s3://{}/{} less than limit {}", + sortedBackups.size(), + cLInfo.bucket, + backupDir, + numKeepBackups); + return true; + } + List backupsToDelete = + sortedBackups.subList(numKeepBackups, sortedBackups.size()); + // Prepare the delete request + DeleteObjectsRequest deleteRequest = + new DeleteObjectsRequest(cLInfo.bucket) + .withKeys( + backupsToDelete.stream() + .map(o -> new KeyVersion(o.getKey())) + .collect(Collectors.toList())); + + // Delete the old backups + client.deleteObjects(deleteRequest); + log.info( + "Deleted {} old backups from s3://{}/{}", + backupsToDelete.size(), + cLInfo.bucket, + backupDir); + } catch (AmazonS3Exception e) { + log.warn("Error occured while deleted objects in S3: {}", e.getErrorMessage()); + return false; + } catch (Exception e) { + log.warn( + "Unexpected exception while attempting to cleanup S3 YBA backup: {}", e.getMessage()); + return false; + } finally { + maybeEnableCertVerification(); + } + return true; + } + @Override public CloudLocationInfo getCloudLocationInfo( String region, CustomerConfigData configData, @Nullable String backupLocation) { diff --git a/managed/src/main/java/com/yugabyte/yw/common/AZUtil.java b/managed/src/main/java/com/yugabyte/yw/common/AZUtil.java index 6902a7950044..8fb989d96292 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/AZUtil.java +++ b/managed/src/main/java/com/yugabyte/yw/common/AZUtil.java @@ -39,6 +39,7 @@ import com.yugabyte.yw.models.helpers.NodeDetails; import com.yugabyte.yw.models.helpers.provider.AzureCloudInfo; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -281,6 +282,11 @@ public BlobContainerClient createBlobContainerClient(String sasToken, String loc return createBlobContainerClient(azureUrl, sasToken, container); } + @Override + public boolean uploadYbaBackup(CustomerConfigData configData, File backup, String backupDir) { + return false; + } + @Override public boolean deleteStorage( CustomerConfigData configData, Map> backupRegionLocationsMap) { diff --git a/managed/src/main/java/com/yugabyte/yw/common/CloudUtil.java b/managed/src/main/java/com/yugabyte/yw/common/CloudUtil.java index 4928d648e026..2a82a5847fe4 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/CloudUtil.java +++ b/managed/src/main/java/com/yugabyte/yw/common/CloudUtil.java @@ -11,6 +11,7 @@ import com.yugabyte.yw.models.configs.data.CustomerConfigData; import com.yugabyte.yw.models.helpers.NodeDetails; import com.yugabyte.yw.models.helpers.ProxyConfig; +import java.io.File; import java.io.InputStream; import java.nio.file.Path; import java.util.Collections; @@ -111,6 +112,15 @@ default UUID getRandomUUID() { return UUID.randomUUID(); } + public default boolean uploadYbaBackup( + CustomerConfigData configData, File backup, String backupDir) { + return false; + } + + public default boolean cleanupUploadedBackups(CustomerConfigData configData, String backupDir) { + return false; + } + public default RestorePreflightResponse generateYBBackupRestorePreflightResponseWithoutBackupObject( AdvancedRestorePreflightParams preflightParams, CustomerConfigData configData) { diff --git a/managed/src/main/java/com/yugabyte/yw/common/GCPUtil.java b/managed/src/main/java/com/yugabyte/yw/common/GCPUtil.java index a93f3be93a91..9242b2fe4152 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/GCPUtil.java +++ b/managed/src/main/java/com/yugabyte/yw/common/GCPUtil.java @@ -45,6 +45,7 @@ import com.yugabyte.yw.models.helpers.provider.GCPCloudInfo; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -165,6 +166,10 @@ public static Storage getStorageService(InputStream is, RetrySettings retrySetti return storageOptions.build().getService(); } + public boolean uploadYbaBackup(CustomerConfigData configData, File backup, String backupDir) { + return false; + } + @Override public boolean deleteKeyIfExists(CustomerConfigData configData, String defaultBackupLocation) { CustomerConfigStorageGCSData gcsData = (CustomerConfigStorageGCSData) configData; diff --git a/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java b/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java index 3f32c11ff495..0e4feb3e3369 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java +++ b/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java @@ -1501,4 +1501,13 @@ public class GlobalConfKeys extends RuntimeConfigKeysModule { + " service per Namespace", ConfDataType.StringType, ImmutableList.of(ConfKeyTags.BETA)); + public static final ConfKeyInfo numCloudYbaBackupsRetention = + new ConfKeyInfo<>( + "yb.auto_yba_backups.num_cloud_retention", + ScopeType.GLOBAL, + "Number of cloud YBA backups to retain", + "When continuous backups feature is enabled only the most recent n backups will be" + + " retained in the storage bucket", + ConfDataType.IntegerType, + ImmutableList.of(ConfKeyTags.PUBLIC)); } diff --git a/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationHelper.java b/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationHelper.java index 88a1cbd3a75c..1c962e00fba7 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationHelper.java +++ b/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationHelper.java @@ -36,6 +36,7 @@ import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -439,9 +440,10 @@ void cleanupBackups(List backups, int numToRetain) { backups.subList(0, numBackups - numToRetain).forEach(File::delete); } - Optional getMostRecentBackup() { + public Optional getMostRecentBackup() { try { - return Optional.of(FileUtils.listFiles(this.getBackupDir(), BACKUP_FILE_PATTERN).get(0)); + return FileUtils.listFiles(this.getBackupDir(), BACKUP_FILE_PATTERN).stream() + .max(Comparator.comparingLong(File::lastModified)); } catch (Exception exception) { LOG.error("Could not locate recent backup", exception); } @@ -449,10 +451,11 @@ Optional getMostRecentBackup() { return Optional.empty(); } - void cleanupCreatedBackups() { + public void cleanupCreatedBackups() { try { List backups = FileUtils.listFiles(this.getBackupDir(), BACKUP_FILE_PATTERN); - this.cleanupBackups(backups, 0); + // Keep 3 most recent backups to avoid interference between continuous backups and HA + this.cleanupBackups(backups, 3); } catch (IOException ioException) { LOG.warn("Failed to list or delete backups"); } @@ -528,7 +531,7 @@ Optional processImportedInstance(PlatformInstance i) { return Optional.empty(); } - synchronized ShellResponse runCommand(T params) { + public synchronized ShellResponse runCommand(T params) { List commandArgs = params.getCommandArgs(); Map extraVars = params.getExtraVars(); diff --git a/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationManager.java b/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationManager.java index fb00fd4ddcec..f4ef8708db79 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationManager.java +++ b/managed/src/main/java/com/yugabyte/yw/common/ha/PlatformReplicationManager.java @@ -508,7 +508,7 @@ List getYbaInstallerArgs() { } } - private class CreatePlatformBackupParams extends PlatformBackupParams { + public class CreatePlatformBackupParams extends PlatformBackupParams { // Whether to exclude prometheus metric data from the backup or not. private final boolean excludePrometheus; @@ -517,7 +517,7 @@ private class CreatePlatformBackupParams extends PlatformBackupParams { // Where to output the platform backup private final String outputDirectory; - CreatePlatformBackupParams() { + public CreatePlatformBackupParams() { this.excludePrometheus = true; this.excludeReleases = true; this.outputDirectory = replicationHelper.getBackupDir().toString(); @@ -550,12 +550,12 @@ protected List getCommandSpecificArgs() { } } - private class RestorePlatformBackupParams extends PlatformBackupParams { + public class RestorePlatformBackupParams extends PlatformBackupParams { // Where to input a previously taken platform backup from. private final File input; - RestorePlatformBackupParams(File input) { + public RestorePlatformBackupParams(File input) { this.input = input; } diff --git a/managed/src/main/java/com/yugabyte/yw/models/ContinuousBackupConfig.java b/managed/src/main/java/com/yugabyte/yw/models/ContinuousBackupConfig.java index 49414e0a4a8f..3590ae5bde44 100644 --- a/managed/src/main/java/com/yugabyte/yw/models/ContinuousBackupConfig.java +++ b/managed/src/main/java/com/yugabyte/yw/models/ContinuousBackupConfig.java @@ -69,6 +69,10 @@ public static ContinuousBackupConfig create( return cbConfig; } + public static Optional get() { + return find.query().where().findOneOrEmpty(); + } + public static Optional get(UUID uuid) { return Optional.ofNullable(find.byId(uuid)); } diff --git a/managed/src/main/java/com/yugabyte/yw/scheduler/Scheduler.java b/managed/src/main/java/com/yugabyte/yw/scheduler/Scheduler.java index b332fd4f66d1..4fa6af0b9807 100644 --- a/managed/src/main/java/com/yugabyte/yw/scheduler/Scheduler.java +++ b/managed/src/main/java/com/yugabyte/yw/scheduler/Scheduler.java @@ -21,6 +21,7 @@ import com.yugabyte.yw.commissioner.Commissioner; import com.yugabyte.yw.commissioner.tasks.BackupUniverse; import com.yugabyte.yw.commissioner.tasks.CreateBackup; +import com.yugabyte.yw.commissioner.tasks.CreateYbaBackup; import com.yugabyte.yw.commissioner.tasks.MultiTableBackup; import com.yugabyte.yw.commissioner.tasks.params.ScheduledAccessKeyRotateParams; import com.yugabyte.yw.commissioner.tasks.subtasks.RunExternalScript; @@ -273,6 +274,8 @@ void scheduleRunner() { break; case CreateAndRotateAccessKey: this.runAccessKeyRotation(schedule, alreadyRunning); + case CreateYbaBackup: + this.runCreateYbaBackupTask(schedule, alreadyRunning); default: log.error( "Cannot schedule task {} for scheduler {}", @@ -334,6 +337,11 @@ private void runCreateBackupTask(Schedule schedule, boolean alreadyRunning, UUID createBackup.runScheduledBackup(schedule, commissioner, alreadyRunning, baseBackupUUID); } + private void runCreateYbaBackupTask(Schedule schedule, boolean alreadyRunning) { + CreateYbaBackup createYbaBackup = AbstractTaskBase.createTask(CreateYbaBackup.class); + createYbaBackup.runScheduledBackup(schedule, commissioner, alreadyRunning); + } + private void runExternalScriptTask(Schedule schedule, boolean alreadyRunning) { JsonNode params = schedule.getTaskParams(); RunExternalScript.Params taskParams = Json.fromJson(params, RunExternalScript.Params.class); diff --git a/managed/src/main/resources/openapi/components/requestBodies/ContinuousBackupCreateReq.yaml b/managed/src/main/resources/openapi/components/requestBodies/ContinuousBackupCreateReq.yaml index 805676fbdb09..cddfde68d343 100644 --- a/managed/src/main/resources/openapi/components/requestBodies/ContinuousBackupCreateReq.yaml +++ b/managed/src/main/resources/openapi/components/requestBodies/ContinuousBackupCreateReq.yaml @@ -2,4 +2,4 @@ required: true content: application/json: schema: - $ref: "../schemas/ContinuousBackupCreateSpec.yaml" + $ref: "../schemas/ContinuousBackupSpec.yaml" diff --git a/managed/src/main/resources/openapi/components/schemas/ContinuousBackup.yaml b/managed/src/main/resources/openapi/components/schemas/ContinuousBackup.yaml index 66765920c62c..6515d07a322d 100644 --- a/managed/src/main/resources/openapi/components/schemas/ContinuousBackup.yaml +++ b/managed/src/main/resources/openapi/components/schemas/ContinuousBackup.yaml @@ -1,6 +1,11 @@ title: ContinuousBackup -description: Continuos Backup object which contains the info for a configuration. +description: | + ContinuousBackup + + Continuous Backup object which contains the info for a configuration. type: object properties: info: $ref: "./ContinuousBackupInfo.yaml" + spec: + $ref: "./ContinuousBackupSpec.yaml" diff --git a/managed/src/main/resources/openapi/components/schemas/ContinuousBackupInfo.yaml b/managed/src/main/resources/openapi/components/schemas/ContinuousBackupInfo.yaml index 40262c69d3e3..a65d1763e106 100644 --- a/managed/src/main/resources/openapi/components/schemas/ContinuousBackupInfo.yaml +++ b/managed/src/main/resources/openapi/components/schemas/ContinuousBackupInfo.yaml @@ -11,13 +11,6 @@ properties: format: uuid example: f33e3c9b-75ab-4c30-80ad-cba85646ea39 readOnly: true - frequency: - description: Interval between two backups. - type: integer - format: int64 - frequency_time_unit: - description: The time unit for the interval between backups. - $ref: "./TimeUnitType.yaml" storage_location: description: bucket or directory where backups are stored type: string diff --git a/managed/src/main/resources/openapi/components/schemas/ContinuousBackupCreateSpec.yaml b/managed/src/main/resources/openapi/components/schemas/ContinuousBackupSpec.yaml similarity index 83% rename from managed/src/main/resources/openapi/components/schemas/ContinuousBackupCreateSpec.yaml rename to managed/src/main/resources/openapi/components/schemas/ContinuousBackupSpec.yaml index 38f458d908c2..478e554d7de6 100644 --- a/managed/src/main/resources/openapi/components/schemas/ContinuousBackupCreateSpec.yaml +++ b/managed/src/main/resources/openapi/components/schemas/ContinuousBackupSpec.yaml @@ -1,8 +1,8 @@ -title: ContinuousBackupCreateSpec +title: ContinuousBackupSpec description: | - ContinuousBackupCreateSpec + ContinuousBackupSpec - Continuous Backup create time properties. Used to create a continuous backup configuration. + User specification for a continuous backup of YBA. All these properties can be edited. type: object required: - backup_dir diff --git a/managed/src/main/resources/reference.conf b/managed/src/main/resources/reference.conf index de45bd865adf..7a39252f458b 100644 --- a/managed/src/main/resources/reference.conf +++ b/managed/src/main/resources/reference.conf @@ -1153,6 +1153,10 @@ yb { enabled = false } + auto_yba_backups { + num_cloud_retention = 5 + } + job_scheduler { poller_interval = 2m instance_record_ttl = 20m