├── .dockerignore ├── gui ├── vue.config.js ├── babel.config.js ├── src │ ├── api │ │ └── index.js │ ├── assets │ │ ├── custom.scss │ │ └── logo_scalar.svg │ ├── main.js │ ├── components │ │ ├── ErrorNotification.vue │ │ ├── ClusterSummary.vue │ │ ├── Clusters.vue │ │ ├── Restores.vue │ │ ├── RestoreCluster.vue │ │ ├── ConfirmRestore.vue │ │ ├── RegisterCluster.vue │ │ ├── CreateBackup.vue │ │ └── Backups.vue │ ├── router │ │ └── index.js │ ├── views │ │ ├── ViewClusters.vue │ │ └── ViewRestores.vue │ └── App.vue ├── .gitignore ├── public │ └── index.html ├── package.json └── README.md ├── src ├── test │ ├── resources │ │ └── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ └── java │ │ └── com │ │ └── scalar │ │ └── cassy │ │ └── transferer │ │ └── AwsS3FileDownloaderTest.java ├── main │ ├── java │ │ └── com │ │ │ └── scalar │ │ │ └── cassy │ │ │ ├── config │ │ │ ├── StorageType.java │ │ │ ├── RestoreType.java │ │ │ ├── BackupType.java │ │ │ ├── BackupConfig.java │ │ │ ├── RestoreConfig.java │ │ │ ├── BaseConfig.java │ │ │ └── CassyServerConfig.java │ │ │ ├── service │ │ │ ├── ApplicationPauseClient.java │ │ │ ├── AzureBlobRestoreModule.java │ │ │ ├── RestoreService.java │ │ │ ├── AzureBlobContainerClientModule.java │ │ │ ├── AwsS3RestoreModule.java │ │ │ ├── AzureBlobBackupModule.java │ │ │ ├── GrpcApplicationPauseClient.java │ │ │ ├── AbstractServiceMaster.java │ │ │ ├── BackupService.java │ │ │ ├── BackupKey.java │ │ │ ├── AwsS3BackupModule.java │ │ │ ├── MetadataDbBackupService.java │ │ │ ├── RestoreServiceMaster.java │ │ │ ├── ApplicationPauser.java │ │ │ └── BackupServiceMaster.java │ │ │ ├── transferer │ │ │ ├── FileDownloader.java │ │ │ ├── FileUploader.java │ │ │ ├── BackupPath.java │ │ │ ├── AwsS3FileDownloader.java │ │ │ ├── AzureBlobFileDownloader.java │ │ │ ├── AwsS3FileUploader.java │ │ │ └── AzureBlobFileUploader.java │ │ │ ├── exception │ │ │ ├── FileTraversalException.java │ │ │ ├── JmxException.java │ │ │ ├── PauseException.java │ │ │ ├── BackupException.java │ │ │ ├── FileIOException.java │ │ │ ├── TimeoutException.java │ │ │ ├── DatabaseException.java │ │ │ ├── PlacementException.java │ │ │ ├── ExecutionException.java │ │ │ ├── FileTransferException.java │ │ │ └── RemoteExecutionException.java │ │ │ ├── rpc │ │ │ ├── PauseRequestOrBuilder.java │ │ │ ├── ClusterRegistrationRequestOrBuilder.java │ │ │ ├── ClusterListingRequestOrBuilder.java │ │ │ ├── StatsResponseOrBuilder.java │ │ │ ├── BackupListingRequestOrBuilder.java │ │ │ ├── BackupListingResponseOrBuilder.java │ │ │ ├── ClusterListingResponseOrBuilder.java │ │ │ ├── RestoreStatusListingRequestOrBuilder.java │ │ │ ├── RestoreStatusListingResponseOrBuilder.java │ │ │ ├── BackupResponseOrBuilder.java │ │ │ ├── RestoreResponseOrBuilder.java │ │ │ ├── BackupRequestOrBuilder.java │ │ │ ├── RestoreRequestOrBuilder.java │ │ │ ├── ClusterRegistrationResponseOrBuilder.java │ │ │ └── OperationStatus.java │ │ │ ├── remotecommand │ │ │ ├── RemoteCommandResult.java │ │ │ ├── RemoteCommandContext.java │ │ │ ├── RemoteCommandFuture.java │ │ │ ├── RemoteCommandExecutor.java │ │ │ └── RemoteCommand.java │ │ │ ├── util │ │ │ ├── ConnectionUtil.java │ │ │ └── AzureUtil.java │ │ │ ├── traverser │ │ │ ├── IncrementalBackupTraverser.java │ │ │ ├── SnapshotTraverser.java │ │ │ └── FileTraverser.java │ │ │ ├── placer │ │ │ └── Placer.java │ │ │ ├── command │ │ │ ├── AbstractCommand.java │ │ │ ├── BackupCommand.java │ │ │ └── RestoreCommand.java │ │ │ ├── server │ │ │ ├── CassyServerModule.java │ │ │ ├── RemoteCommandHandler.java │ │ │ └── CassyServer.java │ │ │ └── db │ │ │ ├── BackupHistoryRecord.java │ │ │ ├── RestoreHistoryRecord.java │ │ │ ├── ClusterInfoRecord.java │ │ │ └── ClusterInfo.java │ ├── resources │ │ └── logback.xml │ └── proto │ │ └── cassy.proto └── integration-test │ └── java │ └── com │ └── scalar │ └── cassy │ └── traverser │ ├── IncrementalBackupTraverserTest.java │ └── SnapshotTraverserTest.java ├── docs └── images │ └── cassy.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── docker ├── entrypoint.sh └── logback.xml ├── go.mod ├── conf └── cassy.properties ├── .gitignore ├── scripts ├── generate_gateway.sh └── db.schema ├── Dockerfile ├── .circleci └── config.yml ├── entrypoint.go ├── gradlew.bat └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | build 3 | -------------------------------------------------------------------------------- /gui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/' 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /docs/images/cassy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalar-labs/cassy/HEAD/docs/images/cassy.png -------------------------------------------------------------------------------- /gui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalar-labs/cassy/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gui/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://127.0.0.1:8080/v1', 5 | }); 6 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/StorageType.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | public enum StorageType { 4 | AWS_S3, AZURE_BLOB 5 | } 6 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f /cassy/data/cassy.db ]; then 4 | sqlite3 /cassy/data/cassy.db < /cassy/scripts/db.schema 5 | fi 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/ApplicationPauseClient.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | public interface ApplicationPauseClient extends AutoCloseable { 4 | 5 | void pause(); 6 | 7 | void unpause(); 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/FileDownloader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.scalar.cassy.config.RestoreConfig; 4 | 5 | public interface FileDownloader extends AutoCloseable { 6 | 7 | void download(RestoreConfig config); 8 | } 9 | -------------------------------------------------------------------------------- /gui/src/assets/custom.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | &.disabled, 3 | &:disabled { 4 | background-color: #6c757d; 5 | border-color: #6c757d; 6 | } 7 | } 8 | .no-data { 9 | display: none; 10 | text-align: center; 11 | } 12 | .table { 13 | border-left: none; 14 | border-right: none; 15 | } 16 | -------------------------------------------------------------------------------- /gui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/FileTraversalException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class FileTraversalException extends RuntimeException { 4 | 5 | public FileTraversalException(String message) { 6 | super(message); 7 | } 8 | 9 | public FileTraversalException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scalar-labs/cassy/entrypoint 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/golang/protobuf v1.4.2 8 | github.com/grpc-ecosystem/grpc-gateway v1.14.6 9 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 10 | google.golang.org/genproto v0.0.0-20200715011427-11fb19a81f2c 11 | google.golang.org/grpc v1.30.0 12 | ) 13 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/JmxException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class JmxException extends RuntimeException { 4 | 5 | public JmxException(String message) { 6 | super(message); 7 | } 8 | 9 | public JmxException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public JmxException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/FileUploader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.scalar.cassy.config.BackupConfig; 4 | import java.nio.file.Path; 5 | import java.util.List; 6 | import java.util.concurrent.Future; 7 | 8 | public interface FileUploader extends AutoCloseable { 9 | 10 | Future upload(Path file, String key); 11 | 12 | void upload(List files, BackupConfig config); 13 | } 14 | -------------------------------------------------------------------------------- /docker/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/PauseException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class PauseException extends RuntimeException { 4 | 5 | public PauseException(String message) { 6 | super(message); 7 | } 8 | 9 | public PauseException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public PauseException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/BackupException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class BackupException extends RuntimeException { 4 | 5 | public BackupException(String message) { 6 | super(message); 7 | } 8 | 9 | public BackupException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public BackupException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/FileIOException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class FileIOException extends RuntimeException { 4 | 5 | public FileIOException(String message) { 6 | super(message); 7 | } 8 | 9 | public FileIOException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public FileIOException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/TimeoutException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class TimeoutException extends RuntimeException { 4 | 5 | public TimeoutException(String message) { 6 | super(message); 7 | } 8 | 9 | public TimeoutException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public TimeoutException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/DatabaseException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class DatabaseException extends RuntimeException { 4 | 5 | public DatabaseException(String message) { 6 | super(message); 7 | } 8 | 9 | public DatabaseException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public DatabaseException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/PlacementException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class PlacementException extends RuntimeException { 4 | 5 | public PlacementException(String message) { 6 | super(message); 7 | } 8 | 9 | public PlacementException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public PlacementException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/ExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class ExecutionException extends RuntimeException { 4 | 5 | public ExecutionException(String message) { 6 | super(message); 7 | } 8 | 9 | public ExecutionException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public ExecutionException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /conf/cassy.properties: -------------------------------------------------------------------------------- 1 | #scalar.cassy.server.port= 2 | #scalar.cassy.server.jmx_port= 3 | scalar.cassy.server.ssh_user= 4 | scalar.cassy.server.ssh_private_key_path=/path/to/key 5 | scalar.cassy.server.slave_command_path=/path/to/bin 6 | scalar.cassy.server.storage_base_uri=s3:// 7 | scalar.cassy.server.storage_type=aws_s3 8 | scalar.cassy.server.metadata_db_url=jdbc:sqlite:/cassy/data/cassy.db 9 | scalar.cassy.server.srv_service_url= 10 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/FileTransferException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class FileTransferException extends RuntimeException { 4 | 5 | public FileTransferException(String message) { 6 | super(message); 7 | } 8 | 9 | public FileTransferException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public FileTransferException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/PauseRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface PauseRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.PauseRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * bool wait_outstanding = 1; 12 | */ 13 | boolean getWaitOutstanding(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/exception/RemoteExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.exception; 2 | 3 | public class RemoteExecutionException extends RuntimeException { 4 | 5 | public RemoteExecutionException(String message) { 6 | super(message); 7 | } 8 | 9 | public RemoteExecutionException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public RemoteExecutionException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gui/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { api } from './api' 3 | import App from './App.vue' 4 | import router from './router' 5 | import BootstrapVue from 'bootstrap-vue' 6 | import 'bootstrap' 7 | import 'bootstrap/dist/css/bootstrap.css' 8 | import 'bootstrap-vue/dist/bootstrap-vue.css' 9 | import '@/assets/custom.scss' 10 | Vue.prototype.$api = api; 11 | Vue.config.productionTip = false; 12 | 13 | Vue.use(require('vue-moment')); 14 | Vue.use(BootstrapVue); 15 | new Vue({ 16 | router, 17 | render: h => h(App) 18 | }).$mount('#app') 19 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/RestoreType.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | public enum RestoreType { 4 | CLUSTER(1), 5 | NODE(2); 6 | 7 | private final int type; 8 | 9 | RestoreType(int type) { 10 | this.type = type; 11 | } 12 | 13 | public int get() { 14 | return this.type; 15 | } 16 | 17 | public static RestoreType getByType(int type) { 18 | for (RestoreType t : RestoreType.values()) { 19 | if (t.type == type) { 20 | return t; 21 | } 22 | } 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/BackupType.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | public enum BackupType { 4 | CLUSTER_SNAPSHOT(1), 5 | NODE_SNAPSHOT(2), 6 | NODE_INCREMENTAL(3); 7 | 8 | private final int type; 9 | 10 | BackupType(int type) { 11 | this.type = type; 12 | } 13 | 14 | public int get() { 15 | return this.type; 16 | } 17 | 18 | public static BackupType getByType(int type) { 19 | for (BackupType t : BackupType.values()) { 20 | if (t.type == type) { 21 | return t; 22 | } 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AzureBlobRestoreModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.inject.Singleton; 4 | import com.scalar.cassy.transferer.AzureBlobFileDownloader; 5 | import com.scalar.cassy.transferer.FileDownloader; 6 | 7 | public class AzureBlobRestoreModule extends AzureBlobContainerClientModule { 8 | 9 | public AzureBlobRestoreModule(String storeBaseUri) { 10 | super(storeBaseUri); 11 | } 12 | 13 | @Override 14 | protected void configure() { 15 | bind(FileDownloader.class).to(AzureBlobFileDownloader.class).in(Singleton.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .gradle 26 | .idea 27 | build 28 | *.iml 29 | *.db 30 | 31 | # out files 32 | out/ 33 | 34 | # executables 35 | entrypoint 36 | api 37 | 38 | # dev files 39 | tmp/ 40 | conf/test-cassy.properties 41 | 42 | # executables 43 | entrypoint 44 | -------------------------------------------------------------------------------- /scripts/generate_gateway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | protoc -I/usr/local/include -I. \ 4 | -I$GOPATH/src \ 5 | -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 6 | --plugin=protoc-gen-go=$GOPATH/bin/protoc-gen-go \ 7 | --go_out=plugins=grpc:. \ 8 | ./src/main/proto/cassy.proto 9 | 10 | protoc -I/usr/local/include -I. \ 11 | -I$GOPATH/src \ 12 | -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 13 | --plugin=protoc-gen-grpc-gateway=$GOPATH/bin/protoc-gen-grpc-gateway \ 14 | --grpc-gateway_out=logtostderr=true:. \ 15 | src/main/proto/cassy.proto 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u212-jre-slim-stretch 2 | 3 | WORKDIR /cassy 4 | 5 | # The Docker context is created in build/docker by `./gradlew docker` 6 | COPY ./cassy.tar . 7 | RUN tar xf cassy.tar -C / && rm -f cassy.tar 8 | 9 | COPY scripts ./scripts 10 | COPY logback.xml entrypoint.sh ./ 11 | 12 | RUN apt-get update && apt-get install -y \ 13 | sqlite3 \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | RUN mkdir -p /cassy/data /cassy/conf 17 | 18 | ENV JAVA_OPTS -Dlogback.configurationFile=/cassy/logback.xml 19 | ENTRYPOINT ["/cassy/entrypoint.sh"] 20 | CMD ["/cassy/bin/cassy-server", "--config", "/cassy/conf/cassy.properties"] 21 | 22 | EXPOSE 20051 23 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/ClusterRegistrationRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface ClusterRegistrationRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.ClusterRegistrationRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cassandra_host = 1; 12 | */ 13 | java.lang.String getCassandraHost(); 14 | /** 15 | * string cassandra_host = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getCassandraHostBytes(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/remotecommand/RemoteCommandResult.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.remotecommand; 2 | 3 | import com.palantir.giraffe.command.CommandResult; 4 | import javax.annotation.concurrent.Immutable; 5 | 6 | @Immutable 7 | public class RemoteCommandResult { 8 | private final CommandResult result; 9 | 10 | public RemoteCommandResult(CommandResult result) { 11 | this.result = result; 12 | } 13 | 14 | public int getExitStatus() { 15 | return result.getExitStatus(); 16 | } 17 | 18 | public String getStdOut() { 19 | return result.getStdOut(); 20 | } 21 | 22 | public String getStdErr() { 23 | return result.getStdErr(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/ClusterListingRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface ClusterListingRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.ClusterListingRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | * int32 limit = 2; 22 | */ 23 | int getLimit(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/StatsResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface StatsResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.StatsResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | *
12 |    * json-formatted
13 |    * 
14 | * 15 | * string stats = 1; 16 | */ 17 | java.lang.String getStats(); 18 | /** 19 | *
20 |    * json-formatted
21 |    * 
22 | * 23 | * string stats = 1; 24 | */ 25 | com.google.protobuf.ByteString 26 | getStatsBytes(); 27 | } 28 | -------------------------------------------------------------------------------- /gui/src/components/ErrorNotification.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /gui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import ViewClusters from '../views/ViewClusters.vue'; 4 | import ViewBackups from '../views/ViewBackups'; 5 | import ViewRestores from '../views/ViewRestores'; 6 | 7 | Vue.use(VueRouter); 8 | 9 | const routes = [ 10 | { 11 | path: '/', 12 | name: 'ViewClusters', 13 | component: ViewClusters, 14 | }, 15 | { 16 | path: '/clusters/:cluster_id/backups', 17 | name: 'ViewBackups', 18 | component: ViewBackups, 19 | }, 20 | { 21 | path: '/clusters/:cluster_id/data/', 22 | name: 'ViewRestores', 23 | component: ViewRestores, 24 | } 25 | ]; 26 | 27 | const router = new VueRouter({ 28 | routes, 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/remotecommand/RemoteCommandContext.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.remotecommand; 2 | 3 | import com.scalar.cassy.service.BackupKey; 4 | import javax.annotation.concurrent.Immutable; 5 | 6 | @Immutable 7 | public class RemoteCommandContext { 8 | private final RemoteCommand command; 9 | private final BackupKey backupKey; 10 | private final RemoteCommandFuture future; 11 | 12 | public RemoteCommandContext( 13 | RemoteCommand command, BackupKey backupKey, RemoteCommandFuture future) { 14 | this.command = command; 15 | this.backupKey = backupKey; 16 | this.future = future; 17 | } 18 | 19 | public RemoteCommand getCommand() { 20 | return command; 21 | } 22 | 23 | public BackupKey getBackupKey() { 24 | return backupKey; 25 | } 26 | 27 | public RemoteCommandFuture getFuture() { 28 | return future; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/util/ConnectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.util; 2 | 3 | import java.sql.Connection; 4 | import java.sql.DriverManager; 5 | import java.sql.SQLException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class ConnectionUtil { 10 | private static final Logger logger = LoggerFactory.getLogger(ConnectionUtil.class); 11 | 12 | public static Connection create(String dbUrl) throws SQLException { 13 | Connection connection = DriverManager.getConnection(dbUrl); 14 | connection.setAutoCommit(true); 15 | return connection; 16 | } 17 | 18 | public static void close(Connection connection) { 19 | try { 20 | if (connection != null) { 21 | connection.close(); 22 | } 23 | } catch (SQLException e) { 24 | logger.warn("failed to close the connection", e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/RestoreService.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.inject.Inject; 4 | import com.scalar.cassy.config.RestoreConfig; 5 | import com.scalar.cassy.placer.Placer; 6 | import com.scalar.cassy.transferer.FileDownloader; 7 | import javax.annotation.concurrent.Immutable; 8 | 9 | @Immutable 10 | public class RestoreService implements AutoCloseable { 11 | private final FileDownloader downloader; 12 | private final Placer placer; 13 | 14 | @Inject 15 | public RestoreService(FileDownloader downloader, Placer placer) { 16 | this.downloader = downloader; 17 | this.placer = placer; 18 | } 19 | 20 | public void restore(RestoreConfig config) { 21 | downloader.download(config); 22 | placer.place(config); 23 | } 24 | 25 | @Override 26 | public void close() throws Exception { 27 | downloader.close(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/traverser/IncrementalBackupTraverser.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.traverser; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import java.util.stream.Stream; 6 | 7 | public class IncrementalBackupTraverser extends FileTraverser { 8 | static final String BACKUP_DIRNAME = "backups"; 9 | public static final int DIR_TO_FILE_DISTANCE = 1; 10 | 11 | public IncrementalBackupTraverser(Path dataDir) { 12 | super(dataDir); 13 | } 14 | 15 | @Override 16 | public List traverse(String keyspace) { 17 | return traverse(keyspace, null); 18 | } 19 | 20 | @Override 21 | public List traverse(String keyspace, String table) { 22 | return traverse(keyspace, table, stream -> traverseBackup(stream)); 23 | } 24 | 25 | private List traverseBackup(Stream stream) { 26 | return traverseFile(stream, BACKUP_DIRNAME, DIR_TO_FILE_DISTANCE); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AzureBlobContainerClientModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.azure.storage.blob.BlobContainerAsyncClient; 4 | import com.azure.storage.blob.BlobContainerClient; 5 | import com.google.inject.AbstractModule; 6 | import com.google.inject.Provides; 7 | import com.google.inject.Singleton; 8 | import com.scalar.cassy.util.AzureUtil; 9 | 10 | public abstract class AzureBlobContainerClientModule extends AbstractModule { 11 | private final String storeBaseUri; 12 | 13 | public AzureBlobContainerClientModule(String storeBaseUri) { 14 | this.storeBaseUri = storeBaseUri; 15 | } 16 | 17 | @Provides 18 | @Singleton 19 | public BlobContainerAsyncClient provideBlobContainerAsyncClient() { 20 | return AzureUtil.getBlobContainerAsyncClient(storeBaseUri); 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | public BlobContainerClient provideBlobContainerClient() { 26 | return AzureUtil.getBlobContainerClient(storeBaseUri); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | INFO 8 | ACCEPT 9 | DENY 10 | 11 | 12 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 13 | 14 | 15 | 16 | 17 | /var/log/scalar/cassy.log 18 | true 19 | 20 | %d{yyyy-MM-dd} %d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /gui/src/components/ClusterSummary.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AwsS3RestoreModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.amazonaws.services.s3.AmazonS3; 4 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 5 | import com.amazonaws.services.s3.transfer.TransferManager; 6 | import com.amazonaws.services.s3.transfer.TransferManagerBuilder; 7 | import com.google.inject.AbstractModule; 8 | import com.google.inject.Provides; 9 | import com.google.inject.Singleton; 10 | import com.scalar.cassy.transferer.AwsS3FileDownloader; 11 | import com.scalar.cassy.transferer.FileDownloader; 12 | 13 | public class AwsS3RestoreModule extends AbstractModule { 14 | 15 | @Override 16 | protected void configure() { 17 | bind(FileDownloader.class).to(AwsS3FileDownloader.class).in(Singleton.class); 18 | } 19 | 20 | @Provides 21 | @Singleton 22 | TransferManager provideTransferManager() { 23 | return TransferManagerBuilder.standard().build(); 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | AmazonS3 provideAmazonS3() { 29 | return AmazonS3ClientBuilder.defaultClient(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/BackupListingRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface BackupListingRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.BackupListingRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | * string target_ip = 2; 22 | */ 23 | java.lang.String getTargetIp(); 24 | /** 25 | * string target_ip = 2; 26 | */ 27 | com.google.protobuf.ByteString 28 | getTargetIpBytes(); 29 | 30 | /** 31 | * int32 limit = 3; 32 | */ 33 | int getLimit(); 34 | 35 | /** 36 | * string snapshot_id = 4; 37 | */ 38 | java.lang.String getSnapshotId(); 39 | /** 40 | * string snapshot_id = 4; 41 | */ 42 | com.google.protobuf.ByteString 43 | getSnapshotIdBytes(); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/traverser/SnapshotTraverser.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.traverser; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | import javax.annotation.Nullable; 8 | 9 | public class SnapshotTraverser extends FileTraverser { 10 | static final String SNAPSHOT_DIRNAME = "snapshots"; 11 | public static final int DIR_TO_FILE_DISTANCE = 2; 12 | private final String snapshotId; 13 | 14 | public SnapshotTraverser(Path dataDir, String snapshotId) { 15 | super(dataDir); 16 | this.snapshotId = snapshotId; 17 | } 18 | 19 | @Override 20 | public List traverse(String keyspace) { 21 | return traverse(keyspace, null); 22 | } 23 | 24 | @Override 25 | public List traverse(String keyspace, @Nullable String table) { 26 | return traverse(keyspace, table, stream -> traverseSnapshot(stream)); 27 | } 28 | 29 | private List traverseSnapshot(Stream stream) { 30 | return traverseFile(stream, SNAPSHOT_DIRNAME, DIR_TO_FILE_DISTANCE).stream() 31 | .filter(f -> f.toString().contains(snapshotId)) 32 | .collect(Collectors.toList()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/BackupConfig.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | import javax.annotation.concurrent.Immutable; 10 | 11 | @Immutable 12 | public class BackupConfig extends BaseConfig { 13 | public static final String BACKUP_TYPE = BaseConfig.PREFIX + "backup_type"; 14 | private BackupType backupType; 15 | 16 | public BackupConfig(File propertiesFile) throws IOException { 17 | super(propertiesFile); 18 | load(); 19 | } 20 | 21 | public BackupConfig(InputStream stream) throws IOException { 22 | super(stream); 23 | load(); 24 | } 25 | 26 | public BackupConfig(Properties properties) { 27 | super(properties); 28 | load(); 29 | } 30 | 31 | public BackupType getBackupType() { 32 | return backupType; 33 | } 34 | 35 | private void load() { 36 | Properties props = getProperties(); 37 | checkArgument(props.getProperty(BACKUP_TYPE) != null); 38 | backupType = BackupType.getByType(Integer.parseInt(props.getProperty(BACKUP_TYPE))); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/BackupListingResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface BackupListingResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.BackupListingResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * repeated .rpc.BackupListingResponse.Entry entries = 1; 12 | */ 13 | java.util.List 14 | getEntriesList(); 15 | /** 16 | * repeated .rpc.BackupListingResponse.Entry entries = 1; 17 | */ 18 | com.scalar.cassy.rpc.BackupListingResponse.Entry getEntries(int index); 19 | /** 20 | * repeated .rpc.BackupListingResponse.Entry entries = 1; 21 | */ 22 | int getEntriesCount(); 23 | /** 24 | * repeated .rpc.BackupListingResponse.Entry entries = 1; 25 | */ 26 | java.util.List 27 | getEntriesOrBuilderList(); 28 | /** 29 | * repeated .rpc.BackupListingResponse.Entry entries = 1; 30 | */ 31 | com.scalar.cassy.rpc.BackupListingResponse.EntryOrBuilder getEntriesOrBuilder( 32 | int index); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/ClusterListingResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface ClusterListingResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.ClusterListingResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * repeated .rpc.ClusterListingResponse.Entry entries = 1; 12 | */ 13 | java.util.List 14 | getEntriesList(); 15 | /** 16 | * repeated .rpc.ClusterListingResponse.Entry entries = 1; 17 | */ 18 | com.scalar.cassy.rpc.ClusterListingResponse.Entry getEntries(int index); 19 | /** 20 | * repeated .rpc.ClusterListingResponse.Entry entries = 1; 21 | */ 22 | int getEntriesCount(); 23 | /** 24 | * repeated .rpc.ClusterListingResponse.Entry entries = 1; 25 | */ 26 | java.util.List 27 | getEntriesOrBuilderList(); 28 | /** 29 | * repeated .rpc.ClusterListingResponse.Entry entries = 1; 30 | */ 31 | com.scalar.cassy.rpc.ClusterListingResponse.EntryOrBuilder getEntriesOrBuilder( 32 | int index); 33 | } 34 | -------------------------------------------------------------------------------- /scripts/db.schema: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS backup_history ( 2 | cluster_id VARCHAR(255) NOT NULL, 3 | target_ip VARCHAR(255) NOT NULL, 4 | snapshot_id VARCHAR(255) NOT NULL, 5 | created_at BIGINT NOT NULL, 6 | updated_at BIGINT, 7 | backup_type TINYINT NOT NULL, 8 | status TINYINT NOT NULL, 9 | PRIMARY KEY (cluster_id, target_ip, snapshot_id, created_at) 10 | ); 11 | CREATE INDEX IF NOT EXISTS idx_snapshot_created_backup ON backup_history (snapshot_id, created_at); 12 | 13 | CREATE TABLE IF NOT EXISTS restore_history ( 14 | cluster_id VARCHAR(255) NOT NULL, 15 | target_ip VARCHAR(255) NOT NULL, 16 | snapshot_id VARCHAR(255) NOT NULL, 17 | created_at BIGINT NOT NULL, 18 | updated_at BIGINT, 19 | restore_type TINYINT NOT NULL, 20 | status TINYINT NOT NULL, 21 | PRIMARY KEY (cluster_id, target_ip, snapshot_id, created_at) 22 | ); 23 | CREATE INDEX IF NOT EXISTS idx_snapshot_created_restore ON restore_history (snapshot_id, created_at); 24 | 25 | CREATE TABLE IF NOT EXISTS cluster_info ( 26 | cluster_id VARCHAR(255) NOT NULL, 27 | cluster_name VARCHAR(255) NOT NULL, 28 | target_ips TEXT NOT NULL, 29 | keyspaces TEXT NOT NULL, 30 | data_dir TEXT NOT NULL, 31 | created_at BIGINT NOT NULL, 32 | updated_at BIGINT, 33 | PRIMARY KEY (cluster_id) 34 | ); 35 | 36 | -------------------------------------------------------------------------------- /gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "bootstrap": "^4.4.1", 13 | "bootstrap-vue": "^2.15.0", 14 | "core-js": "^3.6.4", 15 | "jquery": "^3.5.0", 16 | "popper.js": "^1.16.1", 17 | "sass-loader": "^8.0.2", 18 | "vue": "^2.6.11", 19 | "vue-moment": "^4.1.0", 20 | "vue-router": "^3.1.5" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "~4.2.0", 24 | "@vue/cli-plugin-eslint": "~4.2.0", 25 | "@vue/cli-plugin-router": "~4.2.0", 26 | "@vue/cli-service": "~4.2.0", 27 | "babel-eslint": "^10.0.3", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-vue": "^6.1.2", 30 | "node-sass": "^4.14.1", 31 | "vue-template-compiler": "^2.6.11" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | }, 45 | "rules": {} 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/RestoreStatusListingRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface RestoreStatusListingRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.RestoreStatusListingRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | *
22 |    * optional
23 |    * 
24 | * 25 | * string target_ip = 2; 26 | */ 27 | java.lang.String getTargetIp(); 28 | /** 29 | *
30 |    * optional
31 |    * 
32 | * 33 | * string target_ip = 2; 34 | */ 35 | com.google.protobuf.ByteString 36 | getTargetIpBytes(); 37 | 38 | /** 39 | * string snapshot_id = 3; 40 | */ 41 | java.lang.String getSnapshotId(); 42 | /** 43 | * string snapshot_id = 3; 44 | */ 45 | com.google.protobuf.ByteString 46 | getSnapshotIdBytes(); 47 | 48 | /** 49 | * int32 limit = 4; 50 | */ 51 | int getLimit(); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/BackupPath.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.scalar.cassy.config.BackupConfig; 4 | import com.scalar.cassy.config.BackupType; 5 | import com.scalar.cassy.config.BaseConfig; 6 | import com.scalar.cassy.config.RestoreConfig; 7 | import com.scalar.cassy.config.RestoreType; 8 | import java.nio.file.Paths; 9 | 10 | public class BackupPath { 11 | private static final String CLUSTER_BACKUP_KEY = "cluster-backup"; 12 | private static final String NODE_BACKUP_KEY = "node-backup"; 13 | 14 | public static String create(BaseConfig config, String path) { 15 | return create(getType(config), config, path); 16 | } 17 | 18 | public static String getType(BaseConfig config) { 19 | String type = NODE_BACKUP_KEY; 20 | if ((config instanceof RestoreConfig 21 | && ((RestoreConfig) config).getRestoreType().equals(RestoreType.CLUSTER)) 22 | || (config instanceof BackupConfig 23 | && ((BackupConfig) config).getBackupType().equals(BackupType.CLUSTER_SNAPSHOT))) { 24 | type = CLUSTER_BACKUP_KEY; 25 | } 26 | return type; 27 | } 28 | 29 | private static String create(String type, BaseConfig config, String path) { 30 | return Paths.get( 31 | type, config.getClusterId(), config.getSnapshotId(), config.getTargetIp(), path) 32 | .toString(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AzureBlobBackupModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.inject.Provides; 4 | import com.google.inject.Singleton; 5 | import com.scalar.cassy.config.BackupType; 6 | import com.scalar.cassy.transferer.AzureBlobFileUploader; 7 | import com.scalar.cassy.transferer.FileUploader; 8 | import com.scalar.cassy.traverser.FileTraverser; 9 | import com.scalar.cassy.traverser.IncrementalBackupTraverser; 10 | import com.scalar.cassy.traverser.SnapshotTraverser; 11 | import java.nio.file.Paths; 12 | 13 | public class AzureBlobBackupModule extends AzureBlobContainerClientModule { 14 | 15 | private final BackupType type; 16 | private final String dataDir; 17 | private final String snapshotId; 18 | 19 | public AzureBlobBackupModule( 20 | BackupType type, String dataDir, String snapshotId, String storeBaseUri) { 21 | super(storeBaseUri); 22 | this.type = type; 23 | this.dataDir = dataDir; 24 | this.snapshotId = snapshotId; 25 | } 26 | 27 | @Override 28 | protected void configure() { 29 | bind(FileUploader.class).to(AzureBlobFileUploader.class).in(Singleton.class); 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | FileTraverser provideFileTraverser() { 35 | if (type.equals(BackupType.NODE_INCREMENTAL)) { 36 | return new IncrementalBackupTraverser(Paths.get(dataDir)); 37 | } 38 | return new SnapshotTraverser(Paths.get(dataDir), snapshotId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/RestoreConfig.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | import javax.annotation.concurrent.Immutable; 10 | 11 | @Immutable 12 | public class RestoreConfig extends BaseConfig { 13 | public static final String RESTORE_TYPE = BaseConfig.PREFIX + "restore_type"; 14 | public static final String SNAPSHOT_ONLY = BaseConfig.PREFIX + "snapshot_only"; 15 | private RestoreType restoreType; 16 | private boolean snapshotOnly = false; 17 | 18 | public RestoreConfig(File propertiesFile) throws IOException { 19 | super(propertiesFile); 20 | load(); 21 | } 22 | 23 | public RestoreConfig(InputStream stream) throws IOException { 24 | super(stream); 25 | load(); 26 | } 27 | 28 | public RestoreConfig(Properties properties) { 29 | super(properties); 30 | load(); 31 | } 32 | 33 | public RestoreType getRestoreType() { 34 | return restoreType; 35 | } 36 | 37 | public boolean isSnapshotOnly() { 38 | return snapshotOnly; 39 | } 40 | 41 | private void load() { 42 | Properties props = getProperties(); 43 | checkArgument(props.getProperty(RESTORE_TYPE) != null); 44 | restoreType = RestoreType.getByType(Integer.parseInt(props.getProperty(RESTORE_TYPE))); 45 | if (props.getProperty(SNAPSHOT_ONLY) != null) { 46 | snapshotOnly = Boolean.parseBoolean(props.getProperty(SNAPSHOT_ONLY)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/RestoreStatusListingResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface RestoreStatusListingResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.RestoreStatusListingResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | * repeated .rpc.RestoreStatusListingResponse.Entry entries = 3; 22 | */ 23 | java.util.List 24 | getEntriesList(); 25 | /** 26 | * repeated .rpc.RestoreStatusListingResponse.Entry entries = 3; 27 | */ 28 | com.scalar.cassy.rpc.RestoreStatusListingResponse.Entry getEntries(int index); 29 | /** 30 | * repeated .rpc.RestoreStatusListingResponse.Entry entries = 3; 31 | */ 32 | int getEntriesCount(); 33 | /** 34 | * repeated .rpc.RestoreStatusListingResponse.Entry entries = 3; 35 | */ 36 | java.util.List 37 | getEntriesOrBuilderList(); 38 | /** 39 | * repeated .rpc.RestoreStatusListingResponse.Entry entries = 3; 40 | */ 41 | com.scalar.cassy.rpc.RestoreStatusListingResponse.EntryOrBuilder getEntriesOrBuilder( 42 | int index); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/AwsS3FileDownloader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.amazonaws.services.s3.AmazonS3URI; 4 | import com.amazonaws.services.s3.transfer.MultipleFileDownload; 5 | import com.amazonaws.services.s3.transfer.TransferManager; 6 | import com.google.inject.Inject; 7 | import com.scalar.cassy.config.RestoreConfig; 8 | import com.scalar.cassy.exception.FileTransferException; 9 | import java.nio.file.Paths; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | public class AwsS3FileDownloader implements FileDownloader { 14 | private static final Logger logger = LoggerFactory.getLogger(AwsS3FileDownloader.class); 15 | private final TransferManager manager; 16 | 17 | @Inject 18 | public AwsS3FileDownloader(TransferManager manager) { 19 | this.manager = manager; 20 | } 21 | 22 | @Override 23 | public void download(RestoreConfig config) { 24 | String key = BackupPath.create(config, config.getKeyspace()); 25 | AmazonS3URI s3Uri = new AmazonS3URI(config.getStoreBaseUri()); 26 | 27 | try { 28 | logger.info("Downloading " + s3Uri.getBucket() + "/" + key); 29 | MultipleFileDownload download = 30 | manager.downloadDirectory( 31 | s3Uri.getBucket(), key, Paths.get(config.getDataDir()).toFile()); 32 | download.waitForCompletion(); 33 | logger.info(download.getDescription() + " - " + download.getState().name()); 34 | } catch (InterruptedException | RuntimeException e) { 35 | throw new FileTransferException(e); 36 | } 37 | } 38 | 39 | @Override 40 | public void close() { 41 | manager.shutdownNow(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/remotecommand/RemoteCommandFuture.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.remotecommand; 2 | 3 | import com.palantir.giraffe.command.CommandResult; 4 | import com.palantir.giraffe.host.HostControlSystem; 5 | import java.io.IOException; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.Future; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.TimeoutException; 10 | import javax.annotation.concurrent.Immutable; 11 | 12 | @Immutable 13 | public class RemoteCommandFuture implements Future { 14 | private final HostControlSystem system; 15 | private final Future future; 16 | 17 | public RemoteCommandFuture(HostControlSystem system, Future future) { 18 | this.system = system; 19 | this.future = future; 20 | } 21 | 22 | public void close() throws IOException { 23 | system.close(); 24 | } 25 | 26 | @Override 27 | public boolean cancel(boolean mayInterruptIfRunning) { 28 | return future.cancel(mayInterruptIfRunning); 29 | } 30 | 31 | @Override 32 | public boolean isCancelled() { 33 | return future.isCancelled(); 34 | } 35 | 36 | @Override 37 | public boolean isDone() { 38 | return future.isDone(); 39 | } 40 | 41 | @Override 42 | public RemoteCommandResult get() throws InterruptedException, ExecutionException { 43 | return new RemoteCommandResult((CommandResult) future.get()); 44 | } 45 | 46 | @Override 47 | public RemoteCommandResult get(long timeout, TimeUnit unit) 48 | throws InterruptedException, ExecutionException, TimeoutException { 49 | return new RemoteCommandResult((CommandResult) future.get(timeout, unit)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gui/src/views/ViewClusters.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 58 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/BackupResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface BackupResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.BackupResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * .rpc.OperationStatus status = 1; 12 | */ 13 | int getStatusValue(); 14 | /** 15 | * .rpc.OperationStatus status = 1; 16 | */ 17 | com.scalar.cassy.rpc.OperationStatus getStatus(); 18 | 19 | /** 20 | * string cluster_id = 2; 21 | */ 22 | java.lang.String getClusterId(); 23 | /** 24 | * string cluster_id = 2; 25 | */ 26 | com.google.protobuf.ByteString 27 | getClusterIdBytes(); 28 | 29 | /** 30 | * repeated string target_ips = 3; 31 | */ 32 | java.util.List 33 | getTargetIpsList(); 34 | /** 35 | * repeated string target_ips = 3; 36 | */ 37 | int getTargetIpsCount(); 38 | /** 39 | * repeated string target_ips = 3; 40 | */ 41 | java.lang.String getTargetIps(int index); 42 | /** 43 | * repeated string target_ips = 3; 44 | */ 45 | com.google.protobuf.ByteString 46 | getTargetIpsBytes(int index); 47 | 48 | /** 49 | * string snapshot_id = 4; 50 | */ 51 | java.lang.String getSnapshotId(); 52 | /** 53 | * string snapshot_id = 4; 54 | */ 55 | com.google.protobuf.ByteString 56 | getSnapshotIdBytes(); 57 | 58 | /** 59 | * uint64 created_at = 5; 60 | */ 61 | long getCreatedAt(); 62 | 63 | /** 64 | * uint32 backup_type = 6; 65 | */ 66 | int getBackupType(); 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/RestoreResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface RestoreResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.RestoreResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * .rpc.OperationStatus status = 1; 12 | */ 13 | int getStatusValue(); 14 | /** 15 | * .rpc.OperationStatus status = 1; 16 | */ 17 | com.scalar.cassy.rpc.OperationStatus getStatus(); 18 | 19 | /** 20 | * string cluster_id = 2; 21 | */ 22 | java.lang.String getClusterId(); 23 | /** 24 | * string cluster_id = 2; 25 | */ 26 | com.google.protobuf.ByteString 27 | getClusterIdBytes(); 28 | 29 | /** 30 | * repeated string target_ips = 3; 31 | */ 32 | java.util.List 33 | getTargetIpsList(); 34 | /** 35 | * repeated string target_ips = 3; 36 | */ 37 | int getTargetIpsCount(); 38 | /** 39 | * repeated string target_ips = 3; 40 | */ 41 | java.lang.String getTargetIps(int index); 42 | /** 43 | * repeated string target_ips = 3; 44 | */ 45 | com.google.protobuf.ByteString 46 | getTargetIpsBytes(int index); 47 | 48 | /** 49 | * string snapshot_id = 4; 50 | */ 51 | java.lang.String getSnapshotId(); 52 | /** 53 | * string snapshot_id = 4; 54 | */ 55 | com.google.protobuf.ByteString 56 | getSnapshotIdBytes(); 57 | 58 | /** 59 | * uint32 restore_type = 5; 60 | */ 61 | int getRestoreType(); 62 | 63 | /** 64 | * bool snapshot_only = 6; 65 | */ 66 | boolean getSnapshotOnly(); 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/BackupRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface BackupRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.BackupRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | *
22 |    * optional
23 |    * 
24 | * 25 | * repeated string target_ips = 2; 26 | */ 27 | java.util.List 28 | getTargetIpsList(); 29 | /** 30 | *
31 |    * optional
32 |    * 
33 | * 34 | * repeated string target_ips = 2; 35 | */ 36 | int getTargetIpsCount(); 37 | /** 38 | *
39 |    * optional
40 |    * 
41 | * 42 | * repeated string target_ips = 2; 43 | */ 44 | java.lang.String getTargetIps(int index); 45 | /** 46 | *
47 |    * optional
48 |    * 
49 | * 50 | * repeated string target_ips = 2; 51 | */ 52 | com.google.protobuf.ByteString 53 | getTargetIpsBytes(int index); 54 | 55 | /** 56 | *
57 |    * optional
58 |    * 
59 | * 60 | * string snapshot_id = 3; 61 | */ 62 | java.lang.String getSnapshotId(); 63 | /** 64 | *
65 |    * optional
66 |    * 
67 | * 68 | * string snapshot_id = 3; 69 | */ 70 | com.google.protobuf.ByteString 71 | getSnapshotIdBytes(); 72 | 73 | /** 74 | * uint32 backup_type = 4; 75 | */ 76 | int getBackupType(); 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/RestoreRequestOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface RestoreRequestOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.RestoreRequest) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | *
22 |    * optional
23 |    * 
24 | * 25 | * repeated string target_ips = 2; 26 | */ 27 | java.util.List 28 | getTargetIpsList(); 29 | /** 30 | *
31 |    * optional
32 |    * 
33 | * 34 | * repeated string target_ips = 2; 35 | */ 36 | int getTargetIpsCount(); 37 | /** 38 | *
39 |    * optional
40 |    * 
41 | * 42 | * repeated string target_ips = 2; 43 | */ 44 | java.lang.String getTargetIps(int index); 45 | /** 46 | *
47 |    * optional
48 |    * 
49 | * 50 | * repeated string target_ips = 2; 51 | */ 52 | com.google.protobuf.ByteString 53 | getTargetIpsBytes(int index); 54 | 55 | /** 56 | * string snapshot_id = 3; 57 | */ 58 | java.lang.String getSnapshotId(); 59 | /** 60 | * string snapshot_id = 3; 61 | */ 62 | com.google.protobuf.ByteString 63 | getSnapshotIdBytes(); 64 | 65 | /** 66 | * uint32 restore_type = 4; 67 | */ 68 | int getRestoreType(); 69 | 70 | /** 71 | *
72 |    * optional (default: false)
73 |    * 
74 | * 75 | * bool snapshot_only = 5; 76 | */ 77 | boolean getSnapshotOnly(); 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/GrpcApplicationPauseClient.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.protobuf.Empty; 4 | import com.scalar.cassy.exception.PauseException; 5 | import com.scalar.cassy.rpc.AdminGrpc; 6 | import com.scalar.cassy.rpc.PauseRequest; 7 | import io.grpc.ManagedChannel; 8 | import io.grpc.ManagedChannelBuilder; 9 | import java.util.concurrent.TimeUnit; 10 | import javax.annotation.concurrent.Immutable; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | @Immutable 15 | public class GrpcApplicationPauseClient implements ApplicationPauseClient { 16 | private static final Logger logger = LoggerFactory.getLogger(GrpcApplicationPauseClient.class); 17 | private final ManagedChannel channel; 18 | private final AdminGrpc.AdminBlockingStub blockingStub; 19 | 20 | public GrpcApplicationPauseClient(String host, int port) { 21 | this.channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); 22 | this.blockingStub = AdminGrpc.newBlockingStub(channel); 23 | } 24 | 25 | @Override 26 | public void pause() { 27 | PauseRequest request = PauseRequest.newBuilder().setWaitOutstanding(true).build(); 28 | try { 29 | blockingStub.pause(request); 30 | } catch (Exception e) { 31 | logger.error(e.getMessage(), e); 32 | throw new PauseException(e); 33 | } 34 | } 35 | 36 | @Override 37 | public void unpause() { 38 | try { 39 | blockingStub.unpause(Empty.newBuilder().build()); 40 | } catch (Exception e) { 41 | logger.error(e.getMessage(), e); 42 | throw new PauseException(e); 43 | } 44 | } 45 | 46 | @Override 47 | public void close() { 48 | try { 49 | channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); 50 | } catch (InterruptedException e) { 51 | logger.error(e.getMessage(), e); 52 | throw new PauseException(e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Gradle CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/openjdk:8-jdk 11 | environment: 12 | MAX_HEAP_SIZE: 2048m 13 | HEAP_NEWSIZE: 512m 14 | 15 | working_directory: ~/repo 16 | 17 | environment: 18 | # Customize the JVM maximum heap limit 19 | JVM_OPTS: -Xmx3200m 20 | TERM: dumb 21 | 22 | steps: 23 | - checkout 24 | 25 | # Download and cache dependencies 26 | - restore_cache: 27 | keys: 28 | - v1-dependencies-{{ checksum "build.gradle" }} 29 | # fallback to using the latest cache if no exact match is found 30 | - v1-dependencies- 31 | 32 | - run: gradle dependencies 33 | 34 | - save_cache: 35 | paths: 36 | - ~/.gradle 37 | key: v1-dependencies-{{ checksum "build.gradle" }} 38 | 39 | # run tests! 40 | - run: gradle test 41 | - run: gradle integrationTest 42 | 43 | - run: 44 | name: Save Gradle test reports 45 | command: | 46 | mkdir -p /tmp/gradle_test_reports 47 | cp -a build/reports/tests/test /tmp/gradle_test_reports/ 48 | when: always 49 | 50 | - run: 51 | name: Save Gradle integration test reports 52 | command: | 53 | mkdir -p /tmp/gradle_integration_test_reports 54 | cp -a build/reports/tests/integrationTest /tmp/gradle_integration_test_reports/ 55 | when: always 56 | 57 | - store_artifacts: 58 | path: /tmp/gradle_test_reports 59 | destination: gradle_test_reports 60 | 61 | - store_artifacts: 62 | path: /tmp/gradle_integration_test_reports 63 | destination: gradle_integration_test_reports 64 | -------------------------------------------------------------------------------- /gui/README.md: -------------------------------------------------------------------------------- 1 | # Cassy GUI 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### To run the GUI with Cassy 9 | See the [Getting Started](../docs/getting-started.md) to set up your Cassy configuration and run the server. 10 | 11 | Once you have started Cassy, go to the root folder and run the following commands to build and start the HTTP/1.1 server. 12 | ``` 13 | go build entrypoint.go 14 | ``` 15 | ``` 16 | ./entrypoint 17 | ``` 18 | 19 | In your browser, you can go to [localhost:8080](http://localhost:8080) to view the gui. 20 | 21 | ### Running the GUI in development mode 22 | You can run the GUI on a development server to gain the benefits of hot reload. To do this, first navigate to the [router file](src/api/index.js). 23 | This file contains the configuration for Axios, which is used for talking with the backend server. 24 | 25 | When running in production mode, the GUI is run using a file server, so it can exist on the same port as the backend (:8080). In development mode, the dev server will conflict with this, so we need to change the `baseUrl` to port 8090. 26 | 27 | ```javascript 28 | export const api = axios.create({ 29 | baseURL: 'http://localhost:8090/v1', // by default the port is set to 8080. 30 | }); 31 | ``` 32 | 33 | After making this change, we can start the Cassy server as usual, and then run `entrypoint` in dev mode. 34 | ``` 35 | ./entrypoint -mode=dev 36 | ``` 37 | This will start the HTTP server at port 8090, and allow CORS. 38 | 39 | Finally, we can start the GUI dev server by navigating back to the `gui` folder and running: 40 | ``` 41 | npm run serve 42 | ``` 43 | 44 | You should now be able to edit the GUI code and see live changes in your browser at `http://localhost:8080`. 45 | 46 | #### Making a production build 47 | After making your desired changes to the GUI, you can create a static webpage by running `npm run build`, which will create the files in `gui/dist`. 48 | Make sure to go back to the `src/api/index.js` file and change the port back to `8080` before doing this. 49 | -------------------------------------------------------------------------------- /gui/src/views/ViewRestores.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AbstractServiceMaster.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.scalar.cassy.config.CassyServerConfig; 5 | import com.scalar.cassy.db.ClusterInfoRecord; 6 | import com.scalar.cassy.exception.BackupException; 7 | import com.scalar.cassy.exception.TimeoutException; 8 | import com.scalar.cassy.jmx.JmxManager; 9 | import com.scalar.cassy.remotecommand.RemoteCommandExecutor; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public abstract class AbstractServiceMaster { 14 | protected final String CLUSTER_ID_OPTION = "--cluster-id="; 15 | protected final String SNAPSHOT_ID_OPTION = "--snapshot-id="; 16 | protected final String TARGET_IP_OPTION = "--target-ip="; 17 | protected final String DATA_DIR_OPTION = "--data-dir="; 18 | protected final String STORE_BASE_URI_OPTION = "--store-base-uri="; 19 | protected final String STORE_TYPE_OPTION = "--store-type="; 20 | protected final String KEYSPACES_OPTION = "--keyspaces="; 21 | protected final CassyServerConfig config; 22 | protected final ClusterInfoRecord clusterInfo; 23 | protected final RemoteCommandExecutor executor; 24 | 25 | public AbstractServiceMaster( 26 | CassyServerConfig config, ClusterInfoRecord clusterInfo, RemoteCommandExecutor executor) { 27 | this.config = config; 28 | this.clusterInfo = clusterInfo; 29 | this.executor = executor; 30 | } 31 | 32 | protected void awaitTermination(ExecutorService executor, String tag) { 33 | executor.shutdown(); 34 | try { 35 | boolean terminated = executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS); 36 | if (!terminated) { 37 | throw new TimeoutException("timeout occurred in " + tag); 38 | } 39 | } catch (InterruptedException e) { 40 | throw new BackupException(e); 41 | } 42 | } 43 | 44 | @VisibleForTesting 45 | JmxManager getJmx(String ip, int port) { 46 | return new JmxManager(ip, port); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/placer/Placer.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.placer; 2 | 3 | import com.scalar.cassy.config.RestoreConfig; 4 | import com.scalar.cassy.exception.PlacementException; 5 | import com.scalar.cassy.transferer.BackupPath; 6 | import com.scalar.cassy.traverser.IncrementalBackupTraverser; 7 | import com.scalar.cassy.traverser.SnapshotTraverser; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.List; 13 | 14 | public class Placer { 15 | 16 | public void place(RestoreConfig config) { 17 | String key = BackupPath.create(config, ""); 18 | Path fromDir = Paths.get(config.getDataDir(), key); 19 | Path toDir = Paths.get(config.getDataDir()); 20 | 21 | place( 22 | new SnapshotTraverser(fromDir, config.getSnapshotId()).traverse(config.getKeyspace()), 23 | fromDir, 24 | toDir, 25 | SnapshotTraverser.DIR_TO_FILE_DISTANCE); 26 | 27 | if (config.isSnapshotOnly()) { 28 | return; 29 | } 30 | 31 | place( 32 | new IncrementalBackupTraverser(fromDir).traverse(config.getKeyspace()), 33 | fromDir, 34 | toDir, 35 | IncrementalBackupTraverser.DIR_TO_FILE_DISTANCE); 36 | } 37 | 38 | private void place(List files, Path fromDir, Path toDir, int dirToFileDistance) { 39 | files.forEach( 40 | f -> { 41 | Path relative = fromDir.relativize(f); 42 | try { 43 | Path from = Paths.get(fromDir.toString(), relative.toString()); 44 | Path tmp = Paths.get(toDir.toString(), relative.getParent().toString()); 45 | for (int i = 0; i < dirToFileDistance; ++i) { 46 | tmp = tmp.getParent(); 47 | } 48 | Path to = Paths.get(tmp.toString(), relative.getFileName().toString()); 49 | Files.createDirectories(to.getParent()); 50 | Files.move(from, to); 51 | } catch (IOException e) { 52 | throw new PlacementException(e); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/BackupService.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.inject.Inject; 4 | import com.scalar.cassy.config.BackupConfig; 5 | import com.scalar.cassy.config.BackupType; 6 | import com.scalar.cassy.transferer.FileUploader; 7 | import com.scalar.cassy.traverser.FileTraverser; 8 | import com.scalar.cassy.traverser.IncrementalBackupTraverser; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.List; 14 | import javax.annotation.concurrent.Immutable; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | @Immutable 19 | public class BackupService implements AutoCloseable { 20 | private static final Logger logger = LoggerFactory.getLogger(BackupService.class); 21 | private final FileTraverser traverser; 22 | private final FileUploader uploader; 23 | 24 | @Inject 25 | public BackupService(FileTraverser traverser, FileUploader uploader) { 26 | this.traverser = traverser; 27 | this.uploader = uploader; 28 | } 29 | 30 | public void backup(BackupConfig config) { 31 | // Assumes that another incremental backups are not created after the last snapshot is taken. 32 | removeIncrementalBackups(config); 33 | 34 | List files = traverser.traverse(config.getKeyspace()); 35 | uploader.upload(files, config); 36 | } 37 | 38 | @Override 39 | public void close() throws Exception { 40 | uploader.close(); 41 | } 42 | 43 | private void removeIncrementalBackups(BackupConfig config) { 44 | if (config.getBackupType() == BackupType.NODE_INCREMENTAL) { 45 | return; 46 | } 47 | new IncrementalBackupTraverser(Paths.get(config.getDataDir())) 48 | .traverse(config.getKeyspace()) 49 | .forEach( 50 | p -> { 51 | try { 52 | Files.delete(p); 53 | } catch (IOException e) { 54 | logger.warn("removing incremental backup file " + p + " failed.", e); 55 | } 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/command/AbstractCommand.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.command; 2 | 3 | import com.scalar.cassy.config.StorageType; 4 | import java.util.concurrent.Callable; 5 | import picocli.CommandLine; 6 | 7 | public abstract class AbstractCommand implements Callable { 8 | 9 | @CommandLine.Option( 10 | names = {"--cluster-id"}, 11 | required = true, 12 | paramLabel = "CLUSTER_ID", 13 | description = "A cluster id of a Cassandra cluster") 14 | protected String clusterId; 15 | 16 | @CommandLine.Option( 17 | names = {"--snapshot-id"}, 18 | required = true, 19 | paramLabel = "SNAPSHOT_ID", 20 | description = "An ID of a snapshot to use") 21 | protected String snapshotId; 22 | 23 | @CommandLine.Option( 24 | names = {"--target-ip"}, 25 | required = true, 26 | paramLabel = "TARGET_IP", 27 | description = "An ip of a node to operate") 28 | protected String targetIp; 29 | 30 | @CommandLine.Option( 31 | names = {"--data-dir"}, 32 | required = true, 33 | paramLabel = "DATA_DIR", 34 | description = "A data directory to take backups from or restore backup to") 35 | protected String dataDir; 36 | 37 | @CommandLine.Option( 38 | names = {"--store-type"}, 39 | required = true, 40 | paramLabel = "STORE_TYPE", 41 | description = "The store type [AWS_S3, AZURE_STORAGE_BLOB]") 42 | protected StorageType storeType; 43 | 44 | @CommandLine.Option( 45 | names = {"--store-base-uri"}, 46 | required = true, 47 | paramLabel = "STORE_BASE_URI", 48 | description = "A URI of a store to save backup files") 49 | protected String storeBaseUri; 50 | 51 | @CommandLine.Option( 52 | names = {"--keyspaces"}, 53 | required = true, 54 | paramLabel = "KEYSPACES", 55 | description = "A comma-separated list of keyspaces to operate") 56 | protected String keyspaces; 57 | 58 | @CommandLine.Option( 59 | names = {"-h", "--help"}, 60 | usageHelp = true, 61 | description = "display a help message") 62 | protected boolean helpRequested; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/remotecommand/RemoteCommandExecutor.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.remotecommand; 2 | 3 | import com.palantir.giraffe.command.Command; 4 | import com.palantir.giraffe.command.Commands; 5 | import com.palantir.giraffe.host.Host; 6 | import com.palantir.giraffe.host.HostControlSystem; 7 | import com.palantir.giraffe.ssh.PublicKeySshCredential; 8 | import com.palantir.giraffe.ssh.SshCredential; 9 | import com.palantir.giraffe.ssh.SshHostAccessor; 10 | import com.scalar.cassy.exception.RemoteExecutionException; 11 | import java.io.IOException; 12 | import java.util.concurrent.Future; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | public class RemoteCommandExecutor { 17 | private static final Logger logger = 18 | LoggerFactory.getLogger(RemoteCommandExecutor.class.getName()); 19 | 20 | public RemoteCommandFuture execute(RemoteCommand command) { 21 | SshCredential credential = null; 22 | try { 23 | credential = 24 | PublicKeySshCredential.fromFile(command.getUsername(), command.getPrivateKeyFile()); 25 | } catch (IOException e) { 26 | logger.error(e.getMessage(), e); 27 | throw new RemoteExecutionException(e); 28 | } 29 | SshHostAccessor ssh = 30 | SshHostAccessor.forCredential(Host.fromHostname(command.getIp()), credential); 31 | 32 | HostControlSystem hcs = null; 33 | try { 34 | hcs = ssh.open(); 35 | Command remoteCommand = 36 | hcs.getExecutionSystem() 37 | .getCommandBuilder(command.getCommand()) 38 | .addArguments(command.getArguments()) 39 | .build(); 40 | logger.info("executing " + remoteCommand + " in " + command.getIp()); 41 | 42 | Future future = Commands.executeAsync(remoteCommand); 43 | return new RemoteCommandFuture(hcs, future); 44 | } catch (IOException e) { 45 | try { 46 | if (hcs != null) { 47 | hcs.close(); 48 | } 49 | } catch (IOException e1) { 50 | logger.error(e.getMessage(), e1); 51 | } 52 | logger.error(e.getMessage(), e); 53 | throw new RemoteExecutionException(e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/ClusterRegistrationResponseOrBuilder.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | public interface ClusterRegistrationResponseOrBuilder extends 7 | // @@protoc_insertion_point(interface_extends:rpc.ClusterRegistrationResponse) 8 | com.google.protobuf.MessageOrBuilder { 9 | 10 | /** 11 | * string cluster_id = 1; 12 | */ 13 | java.lang.String getClusterId(); 14 | /** 15 | * string cluster_id = 1; 16 | */ 17 | com.google.protobuf.ByteString 18 | getClusterIdBytes(); 19 | 20 | /** 21 | * string cluster_name = 2; 22 | */ 23 | java.lang.String getClusterName(); 24 | /** 25 | * string cluster_name = 2; 26 | */ 27 | com.google.protobuf.ByteString 28 | getClusterNameBytes(); 29 | 30 | /** 31 | * repeated string target_ips = 3; 32 | */ 33 | java.util.List 34 | getTargetIpsList(); 35 | /** 36 | * repeated string target_ips = 3; 37 | */ 38 | int getTargetIpsCount(); 39 | /** 40 | * repeated string target_ips = 3; 41 | */ 42 | java.lang.String getTargetIps(int index); 43 | /** 44 | * repeated string target_ips = 3; 45 | */ 46 | com.google.protobuf.ByteString 47 | getTargetIpsBytes(int index); 48 | 49 | /** 50 | * repeated string keyspaces = 4; 51 | */ 52 | java.util.List 53 | getKeyspacesList(); 54 | /** 55 | * repeated string keyspaces = 4; 56 | */ 57 | int getKeyspacesCount(); 58 | /** 59 | * repeated string keyspaces = 4; 60 | */ 61 | java.lang.String getKeyspaces(int index); 62 | /** 63 | * repeated string keyspaces = 4; 64 | */ 65 | com.google.protobuf.ByteString 66 | getKeyspacesBytes(int index); 67 | 68 | /** 69 | * string data_dir = 5; 70 | */ 71 | java.lang.String getDataDir(); 72 | /** 73 | * string data_dir = 5; 74 | */ 75 | com.google.protobuf.ByteString 76 | getDataDirBytes(); 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/BackupKey.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import javax.annotation.concurrent.Immutable; 6 | 7 | @Immutable 8 | public class BackupKey { 9 | private final String snapshotId; 10 | private final String clusterId; 11 | private final String targetIp; 12 | private final long createdAt; 13 | 14 | private BackupKey(Builder builder) { 15 | this.snapshotId = builder.snapshotId; 16 | this.clusterId = builder.clusterId; 17 | this.targetIp = builder.targetIp; 18 | this.createdAt = builder.createdAt; 19 | } 20 | 21 | public String getSnapshotId() { 22 | return snapshotId; 23 | } 24 | 25 | public String getClusterId() { 26 | return clusterId; 27 | } 28 | 29 | public String getTargetIp() { 30 | return targetIp; 31 | } 32 | 33 | public long getCreatedAt() { 34 | return createdAt; 35 | } 36 | 37 | public static Builder newBuilder() { 38 | return new Builder(); 39 | } 40 | 41 | public static final class Builder { 42 | private String snapshotId; 43 | private String clusterId; 44 | private String targetIp; 45 | private long createdAt; 46 | 47 | public Builder snapshotId(String snapshotId) { 48 | checkArgument(snapshotId != null); 49 | this.snapshotId = snapshotId; 50 | return this; 51 | } 52 | 53 | public Builder clusterId(String clusterId) { 54 | checkArgument(clusterId != null); 55 | this.clusterId = clusterId; 56 | return this; 57 | } 58 | 59 | public Builder targetIp(String targetIp) { 60 | checkArgument(targetIp != null); 61 | this.targetIp = targetIp; 62 | return this; 63 | } 64 | 65 | public Builder createdAt(long createdAt) { 66 | checkArgument(createdAt > 0); 67 | this.createdAt = createdAt; 68 | return this; 69 | } 70 | 71 | public BackupKey build() { 72 | if (snapshotId == null || clusterId == null || targetIp == null || createdAt == 0) { 73 | throw new IllegalArgumentException("Required fields are not given."); 74 | } 75 | return new BackupKey(this); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/AwsS3BackupModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.amazonaws.services.s3.AmazonS3; 4 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 5 | import com.amazonaws.services.s3.AmazonS3URI; 6 | import com.amazonaws.services.s3.transfer.TransferManager; 7 | import com.amazonaws.services.s3.transfer.TransferManagerBuilder; 8 | import com.google.inject.AbstractModule; 9 | import com.google.inject.Provides; 10 | import com.google.inject.Singleton; 11 | import com.scalar.cassy.config.BackupType; 12 | import com.scalar.cassy.transferer.AwsS3FileUploader; 13 | import com.scalar.cassy.transferer.FileUploader; 14 | import com.scalar.cassy.traverser.FileTraverser; 15 | import com.scalar.cassy.traverser.IncrementalBackupTraverser; 16 | import com.scalar.cassy.traverser.SnapshotTraverser; 17 | import java.nio.file.Paths; 18 | 19 | public class AwsS3BackupModule extends AbstractModule { 20 | private final BackupType type; 21 | private final String dataDir; 22 | private final String snapshotId; 23 | private final String storeBaseUri; 24 | 25 | public AwsS3BackupModule( 26 | BackupType type, String dataDir, String snapshotId, String storeBaseUri) { 27 | this.type = type; 28 | this.dataDir = dataDir; 29 | this.snapshotId = snapshotId; 30 | this.storeBaseUri = storeBaseUri; 31 | } 32 | 33 | @Override 34 | protected void configure() { 35 | bind(FileUploader.class).to(AwsS3FileUploader.class).in(Singleton.class); 36 | } 37 | 38 | @Provides 39 | @Singleton 40 | FileTraverser provideFileTraverser() { 41 | if (type.equals(BackupType.NODE_INCREMENTAL)) { 42 | return new IncrementalBackupTraverser(Paths.get(dataDir)); 43 | } 44 | return new SnapshotTraverser(Paths.get(dataDir), snapshotId); 45 | } 46 | 47 | @Provides 48 | @Singleton 49 | TransferManager provideTransferManager() { 50 | return TransferManagerBuilder.standard().build(); 51 | } 52 | 53 | @Provides 54 | @Singleton 55 | AmazonS3 provideAmazonS3() { 56 | return AmazonS3ClientBuilder.defaultClient(); 57 | } 58 | 59 | @Provides 60 | @Singleton 61 | AmazonS3URI provideAmazonS3URI() { 62 | return new AmazonS3URI(storeBaseUri); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/command/BackupCommand.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.command; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import com.scalar.cassy.config.BackupConfig; 6 | import com.scalar.cassy.config.BackupType; 7 | import com.scalar.cassy.service.AwsS3BackupModule; 8 | import com.scalar.cassy.service.AzureBlobBackupModule; 9 | import com.scalar.cassy.service.BackupService; 10 | import java.util.Arrays; 11 | import java.util.Properties; 12 | import picocli.CommandLine; 13 | 14 | public class BackupCommand extends AbstractCommand { 15 | 16 | @CommandLine.Option( 17 | names = {"--backup-type"}, 18 | required = true, 19 | paramLabel = "BACKUP_TYPE", 20 | description = "The type of backup to take") 21 | private int backupType; 22 | 23 | public static void main(String[] args) { 24 | int exitCode = new CommandLine(new BackupCommand()).execute(args); 25 | System.exit(exitCode); 26 | } 27 | 28 | @Override 29 | public Void call() throws Exception { 30 | Properties props = new Properties(); 31 | props.setProperty(BackupConfig.CLUSTER_ID, clusterId); 32 | props.setProperty(BackupConfig.SNAPSHOT_ID, snapshotId); 33 | props.setProperty(BackupConfig.TARGET_IP, targetIp); 34 | props.setProperty(BackupConfig.DATA_DIR, dataDir); 35 | props.setProperty(BackupConfig.STORE_BASE_URI, storeBaseUri); 36 | props.setProperty(BackupConfig.BACKUP_TYPE, Integer.toString(backupType)); 37 | 38 | BackupType type = BackupType.getByType(backupType); 39 | 40 | Injector injector; 41 | switch (storeType) { 42 | case AWS_S3: 43 | injector = 44 | Guice.createInjector(new AwsS3BackupModule(type, dataDir, snapshotId, storeBaseUri)); 45 | break; 46 | case AZURE_BLOB: 47 | injector = 48 | Guice.createInjector( 49 | new AzureBlobBackupModule(type, dataDir, snapshotId, storeBaseUri)); 50 | break; 51 | default: 52 | throw new UnsupportedOperationException( 53 | "The storage type " + storeType + " is not implemented"); 54 | } 55 | 56 | try (BackupService service = injector.getInstance(BackupService.class)) { 57 | Arrays.asList(keyspaces.split(",")) 58 | .forEach( 59 | k -> { 60 | props.setProperty(BackupConfig.KEYSPACE, k); 61 | service.backup(new BackupConfig(props)); 62 | }); 63 | } 64 | 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /entrypoint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/golang/glog" 9 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | 13 | gw "github.com/scalar-labs/cassy/entrypoint/src/main/proto" 14 | ) 15 | 16 | var ( 17 | endpoint = flag.String("cassy server endpoint", "localhost:20051", "endpoint of Cassy server") 18 | mode = flag.String("mode", "prod", 19 | "prod: runs the HTTP server on port 8080. Recommended for most users.\n " + 20 | "dev: runs the HTTP server on port 8090 to avoid conflict with dev servers.\n") 21 | ) 22 | 23 | func run() error { 24 | ctx := context.Background() 25 | ctx, cancel := context.WithCancel(ctx) 26 | defer cancel() 27 | 28 | mux := runtime.NewServeMux() 29 | httpMux := http.NewServeMux() 30 | prefix := "/v1/" 31 | httpMux.Handle(prefix, mux) 32 | fs := http.FileServer(http.Dir("./gui/dist")) 33 | httpMux.Handle("/", fs) 34 | 35 | 36 | opts := []grpc.DialOption{grpc.WithInsecure()} 37 | err := gw.RegisterCassyHandlerFromEndpoint(ctx, mux, *endpoint, opts) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if *mode == "dev" { 43 | return http.ListenAndServe(":8090", allowCORS(mux)) 44 | } else if *mode == "prod" { 45 | return http.ListenAndServe(":8080", httpMux) 46 | } else { 47 | glog.Fatal("Please select a valid mode") 48 | } 49 | return nil 50 | } 51 | 52 | func allowCORS(h http.Handler) http.Handler { 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | w.Header().Set("Access-Control-Allow-Origin", "*") 55 | if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { 56 | preflightHandler(w, r) 57 | return 58 | } 59 | h.ServeHTTP(w, r) 60 | }) 61 | } 62 | 63 | // We need to implement a handler for OPTIONS requests that are done in preflight, 64 | // because grpc-gateway cannot handle complex requests from cross origin 65 | func preflightHandler(w http.ResponseWriter, r *http.Request) { 66 | headers := []string{"Content-Type", "Accept", "Authorization"} 67 | w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) 68 | methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"} 69 | w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) 70 | glog.Infof("preflight request for %s", r.URL.Path) 71 | } 72 | 73 | func main() { 74 | flag.Parse() 75 | defer glog.Flush() 76 | 77 | if err := run(); err != nil { 78 | glog.Fatal(err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/util/AzureUtil.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.util; 2 | 3 | import com.azure.identity.DefaultAzureCredentialBuilder; 4 | import com.azure.storage.blob.BlobContainerAsyncClient; 5 | import com.azure.storage.blob.BlobServiceAsyncClient; 6 | import com.azure.storage.blob.BlobContainerClient; 7 | import com.azure.storage.blob.BlobServiceClient; 8 | import com.azure.storage.blob.BlobServiceClientBuilder; 9 | 10 | public class AzureUtil { 11 | private static final String CONNECTION_STRING = "AZURE_STORAGE_CONNECTION_STRING"; 12 | 13 | public static BlobContainerAsyncClient getBlobContainerAsyncClient(String storageBaseUri) { 14 | BlobServiceClientBuilder builder = getBlobServiceClientBuilder(storageBaseUri); 15 | BlobServiceAsyncClient asyncClient = builder.buildAsyncClient(); 16 | 17 | validateUrl(storageBaseUri, asyncClient.getAccountUrl()); 18 | String containerName = storageBaseUri.replace(asyncClient.getAccountUrl() + "/", ""); 19 | return asyncClient.getBlobContainerAsyncClient(containerName); 20 | } 21 | 22 | public static BlobContainerClient getBlobContainerClient(String storageBaseUri) { 23 | BlobServiceClientBuilder builder = getBlobServiceClientBuilder(storageBaseUri); 24 | BlobServiceClient client = builder.buildClient(); 25 | 26 | validateUrl(storageBaseUri, client.getAccountUrl()); 27 | String containerName = storageBaseUri.replace(client.getAccountUrl() + "/", ""); 28 | return client.getBlobContainerClient(containerName); 29 | } 30 | 31 | private static BlobServiceClientBuilder getBlobServiceClientBuilder(String storageBaseUri) { 32 | BlobServiceClientBuilder builder = new BlobServiceClientBuilder(); 33 | 34 | String connectionString = System.getenv(CONNECTION_STRING); 35 | if (connectionString == null) { 36 | // Use service principal or managed identity when a connection string is not given 37 | String accountUrl = storageBaseUri.substring(0, storageBaseUri.lastIndexOf('/')); 38 | builder.endpoint(accountUrl).credential(new DefaultAzureCredentialBuilder().build()); 39 | } else { 40 | // Use a specified connection string when given 41 | builder.connectionString(connectionString); 42 | } 43 | return builder; 44 | } 45 | 46 | private static void validateUrl(String storageBaseUri, String accountUrl) { 47 | if (!storageBaseUri.startsWith(accountUrl)) { 48 | throw new IllegalArgumentException( 49 | "The given credential can not be used for the specified container."); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/server/CassyServerModule.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.server; 2 | 3 | import com.amazonaws.services.s3.AmazonS3; 4 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 5 | import com.amazonaws.services.s3.AmazonS3URI; 6 | import com.amazonaws.services.s3.transfer.TransferManager; 7 | import com.amazonaws.services.s3.transfer.TransferManagerBuilder; 8 | import com.azure.storage.blob.BlobContainerAsyncClient; 9 | import com.google.inject.AbstractModule; 10 | import com.google.inject.Provides; 11 | import com.google.inject.Singleton; 12 | import com.scalar.cassy.config.CassyServerConfig; 13 | import com.scalar.cassy.config.StorageType; 14 | import com.scalar.cassy.remotecommand.RemoteCommandContext; 15 | import com.scalar.cassy.transferer.AwsS3FileUploader; 16 | import com.scalar.cassy.transferer.AzureBlobFileUploader; 17 | import com.scalar.cassy.transferer.FileUploader; 18 | import com.scalar.cassy.util.AzureUtil; 19 | import java.util.concurrent.BlockingQueue; 20 | import java.util.concurrent.LinkedBlockingQueue; 21 | 22 | public class CassyServerModule extends AbstractModule { 23 | private final CassyServerConfig config; 24 | 25 | public CassyServerModule(CassyServerConfig config) { 26 | this.config = config; 27 | } 28 | 29 | @Override 30 | protected void configure() { 31 | if (config.getStorageType().equals(StorageType.AWS_S3)) { 32 | bind(FileUploader.class).to(AwsS3FileUploader.class).in(Singleton.class); 33 | } else if (config.getStorageType().equals(StorageType.AZURE_BLOB)) { 34 | bind(FileUploader.class).to(AzureBlobFileUploader.class).in(Singleton.class); 35 | } else { 36 | throw new UnsupportedOperationException( 37 | "The storage type " + config.getStorageType() + " is not implemented"); 38 | } 39 | } 40 | 41 | @Provides 42 | @Singleton 43 | CassyServerConfig provideCassyServerConfig() { 44 | return config; 45 | } 46 | 47 | @Provides 48 | @Singleton 49 | BlockingQueue provideRemoteCommandQueue() { 50 | return new LinkedBlockingQueue<>(); 51 | } 52 | 53 | @Provides 54 | @Singleton 55 | TransferManager provideTransferManager() { 56 | return TransferManagerBuilder.standard().build(); 57 | } 58 | 59 | @Provides 60 | @Singleton 61 | AmazonS3 provideAmazonS3() { 62 | return AmazonS3ClientBuilder.defaultClient(); 63 | } 64 | 65 | @Provides 66 | @Singleton 67 | AmazonS3URI provideAmazonS3URI() { 68 | return new AmazonS3URI(config.getStorageBaseUri()); 69 | } 70 | 71 | @Provides 72 | @Singleton 73 | public BlobContainerAsyncClient provideBlobContainerAsyncClient() { 74 | return AzureUtil.getBlobContainerAsyncClient(config.getStorageBaseUri()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gui/src/App.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 60 | 61 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cassy: A simple and integrated backup tool for Apache Cassandra 2 | 3 | [![CircleCI](https://circleci.com/gh/scalar-labs/cassy.svg?style=svg&circle-token=4f293f3061b353a7f5bd8c7d9544bae8817449af)](https://circleci.com/gh/scalar-labs/cassy) 4 | 5 | Cassy is a simple and integrated backup tool for Apache Cassandra. 6 | 7 | You can do the following things with Cassy from an easy to use gRPC APIs or HTTP/1.1 REST APIs: 8 | * Take snapshots, and upload snapshots and incremental backups to a blob store or filesystem of your choice from your cluster (AWS S3 is the only supported blob store at the current version, but other blob stores or filesystems will be supported shortly) 9 | * Download backups, and restore a node or a cluster from the backups 10 | * Manage statuses and histories of backups and restore 11 | 12 | You can NOT do the followings things with the current version of Cassy: 13 | * Backup commitlogs 14 | * Select keyspaces to take/restore backups 15 | * Operate easily with GUI 16 | 17 | ## Background 18 | 19 | The existing Cassandra backup features such as snapshot and incremental backups are great building blocks for doing backup and restore for Cassandra data. However, they are not necessarily easy to use because they are not fully integrated. Moreover, the current backup feature and the existing backup tools are problematic when used with [Scalar DB](https://github.com/scalar-labs/scalardb/) transactions that update multiple records in a transactional (atomic) way as they do not handle transactional consistency of multiple records. 20 | In order to overcome these problems we created a new backup tool, which makes it easy to do backup and restore operations, and makes it possible to do cluster-wide transactionally consistent backups. 21 | 22 | ## System Overview 23 | 24 |

25 | 26 |

27 | 28 | ## Docs 29 | * [Getting started](docs/getting-started.md) 30 | 31 | ## Contributing 32 | This library is mainly maintained by the Scalar Engineering Team, but of course we appreciate any help. 33 | 34 | * For asking questions, finding answers and helping other users, please go to [stackoverflow](https://stackoverflow.com/) and use [cassy](https://stackoverflow.com/questions/tagged/cassy) tag. 35 | * For filing bugs, suggesting improvements, or requesting new features, help us out by opening an issue. 36 | 37 | ## License 38 | Cassy is dual-licensed under both the Apache 2.0 License (found in the LICENSE file in the root directory) and a commercial license. You may select, at your option, one of the above-listed licenses. Regarding the commercial license, please [contact us](https://scalar-labs.com/contact_us/) for more information. 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/BaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Properties; 10 | import javax.annotation.concurrent.Immutable; 11 | 12 | @Immutable 13 | public class BaseConfig { 14 | private final Properties props; 15 | protected static final String PREFIX = "scalar.cassy."; 16 | public static final String CLUSTER_ID = PREFIX + "cluster_id"; 17 | public static final String SNAPSHOT_ID = PREFIX + "snapshot_id"; 18 | public static final String TARGET_IP = PREFIX + "target_ip"; 19 | public static final String DATA_DIR = PREFIX + "data_dir"; 20 | public static final String STORE_BASE_URI = PREFIX + "store_base_uri"; 21 | public static final String KEYSPACE = PREFIX + "keyspace"; 22 | private String clusterId; 23 | private String snapshotId; 24 | private String targetIp; 25 | private String dataDir; 26 | private String storeBaseUri; 27 | private String keyspace; 28 | 29 | public BaseConfig(File propertiesFile) throws IOException { 30 | this(new FileInputStream(propertiesFile)); 31 | } 32 | 33 | public BaseConfig(InputStream stream) throws IOException { 34 | props = new Properties(); 35 | props.load(stream); 36 | load(); 37 | } 38 | 39 | public BaseConfig(Properties properties) { 40 | props = new Properties(properties); 41 | load(); 42 | } 43 | 44 | public Properties getProperties() { 45 | return props; 46 | } 47 | 48 | public String getClusterId() { 49 | return clusterId; 50 | } 51 | 52 | public String getSnapshotId() { 53 | return snapshotId; 54 | } 55 | 56 | public String getTargetIp() { 57 | return targetIp; 58 | } 59 | 60 | public String getDataDir() { 61 | return dataDir; 62 | } 63 | 64 | public String getStoreBaseUri() { 65 | return storeBaseUri; 66 | } 67 | 68 | public String getKeyspace() { 69 | return keyspace; 70 | } 71 | 72 | private void load() { 73 | checkArgument(props.getProperty(CLUSTER_ID) != null); 74 | clusterId = props.getProperty(CLUSTER_ID); 75 | checkArgument(props.getProperty(SNAPSHOT_ID) != null); 76 | snapshotId = props.getProperty(SNAPSHOT_ID); 77 | checkArgument(props.getProperty(TARGET_IP) != null); 78 | targetIp = props.getProperty(TARGET_IP); 79 | checkArgument(props.getProperty(DATA_DIR) != null); 80 | dataDir = props.getProperty(DATA_DIR); 81 | checkArgument(props.getProperty(STORE_BASE_URI) != null); 82 | storeBaseUri = props.getProperty(STORE_BASE_URI); 83 | checkArgument(props.getProperty(KEYSPACE) != null); 84 | keyspace = props.getProperty(KEYSPACE); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/traverser/FileTraverser.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.traverser; 2 | 3 | import com.scalar.cassy.exception.FileTraversalException; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | import javax.annotation.Nullable; 14 | 15 | public abstract class FileTraverser { 16 | private final Path dataDir; 17 | 18 | public FileTraverser(Path dataDir) { 19 | this.dataDir = dataDir; 20 | } 21 | 22 | public abstract List traverse(String keyspace); 23 | 24 | public abstract List traverse(String keyspace, @Nullable String table); 25 | 26 | protected List traverse( 27 | String keyspace, @Nullable String table, Function, List> traverser) { 28 | Path keyspacePath = Paths.get(dataDir.toString(), keyspace); 29 | 30 | List tablePaths = traverseTable(keyspacePath, table); 31 | 32 | List filePaths = new ArrayList<>(); 33 | tablePaths.forEach( 34 | t -> { 35 | try (Stream walk = Files.walk(t, 1)) { 36 | filePaths.addAll(traverser.apply(walk)); 37 | } catch (IOException e) { 38 | throw new FileTraversalException(e.getMessage(), e); 39 | } 40 | }); 41 | 42 | return filePaths; 43 | } 44 | 45 | private List traverseTable(Path keyspace, String table) { 46 | List tables = new ArrayList<>(); 47 | try (Stream walk = Files.walk(keyspace, 1)) { 48 | tables.addAll(filterTable(walk.filter(dir -> !dir.endsWith(keyspace)), table)); 49 | } catch (IOException e) { 50 | throw new FileTraversalException(e.getMessage(), e); 51 | } 52 | return tables; 53 | } 54 | 55 | private List filterTable(Stream stream, String table) { 56 | return stream 57 | .filter( 58 | dir -> { 59 | if (table != null) { 60 | return dir.getFileName().toString().startsWith(table); 61 | } 62 | return true; 63 | }) 64 | .collect(Collectors.toList()); 65 | } 66 | 67 | protected List traverseFile(Stream stream, String directory, int depth) { 68 | List paths = new ArrayList<>(); 69 | stream 70 | .filter(dir -> dir.endsWith(directory)) 71 | .forEach( 72 | dir -> { 73 | try (Stream walk = Files.walk(dir, depth)) { 74 | paths.addAll(walk.filter(Files::isRegularFile).collect(Collectors.toList())); 75 | } catch (IOException e) { 76 | throw new FileTraversalException(e.getMessage(), e); 77 | } 78 | }); 79 | return paths; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gui/src/components/Clusters.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 86 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/MetadataDbBackupService.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.common.base.Splitter; 4 | import com.google.inject.Inject; 5 | import com.palantir.giraffe.command.Command; 6 | import com.palantir.giraffe.command.CommandResult; 7 | import com.palantir.giraffe.command.Commands; 8 | import com.palantir.giraffe.file.MoreFiles; 9 | import com.scalar.cassy.config.CassyServerConfig; 10 | import com.scalar.cassy.exception.ExecutionException; 11 | import com.scalar.cassy.transferer.FileUploader; 12 | import java.nio.charset.StandardCharsets; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.text.SimpleDateFormat; 16 | import java.util.Date; 17 | import java.util.List; 18 | import java.util.concurrent.Future; 19 | import javax.annotation.concurrent.Immutable; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | @Immutable 24 | public class MetadataDbBackupService { 25 | private static final Logger logger = LoggerFactory.getLogger(MetadataDbBackupService.class); 26 | private static final String METADATA_BACKUP_KEY = "metadata-backup"; 27 | // assumes that sqlite3 is in PATH 28 | private static final String SQLITE_COMMAND = "sqlite3"; 29 | private static final String BACKUP_TMP_DIR = "/tmp/"; 30 | private final CassyServerConfig config; 31 | private final FileUploader uploader; 32 | 33 | @Inject 34 | public MetadataDbBackupService(CassyServerConfig config, FileUploader uploader) { 35 | this.config = config; 36 | this.uploader = uploader; 37 | } 38 | 39 | public Future backup() { 40 | List urls = Splitter.on(':').splitToList(config.getMetadataDbUrl()); 41 | if (urls.get(1).equalsIgnoreCase("sqlite")) { 42 | Path dumpFile = sqliteBackup(Paths.get(urls.get(2))); 43 | return uploader.upload(dumpFile, getKey(dumpFile).toString()); 44 | } else { 45 | throw new IllegalArgumentException( 46 | "metadata backup for " + urls.get(1) + " is not supported."); 47 | } 48 | } 49 | 50 | private Path sqliteBackup(Path file) { 51 | // uses SQLite .dump command 52 | Path dumpFile = Paths.get(BACKUP_TMP_DIR + file.getFileName() + ".dump"); 53 | Command removeDumpFile = Commands.get("rm", "-f", dumpFile); 54 | Command sqliteDump = Commands.get(SQLITE_COMMAND, file, ".dump"); 55 | try { 56 | Commands.execute(removeDumpFile); 57 | CommandResult result = Commands.execute(sqliteDump); 58 | MoreFiles.write(dumpFile, result.getStdOut(), StandardCharsets.UTF_8); 59 | } catch (Exception e) { 60 | throw new ExecutionException(e); 61 | } 62 | return dumpFile; 63 | } 64 | 65 | private Path getKey(Path file) { 66 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 67 | String datetime = simpleDateFormat.format(new Date()); 68 | return Paths.get(METADATA_BACKUP_KEY, datetime, file.getFileName().toString()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gui/src/components/Restores.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 93 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/remotecommand/RemoteCommand.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.remotecommand; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import com.google.common.collect.ImmutableList; 6 | import java.nio.file.Path; 7 | import java.util.List; 8 | import javax.annotation.concurrent.Immutable; 9 | 10 | @Immutable 11 | public final class RemoteCommand { 12 | private final String ip; 13 | private final String username; 14 | private final Path privateKeyFile; 15 | private final String command; 16 | private final List arguments; 17 | private final String name; 18 | 19 | private RemoteCommand(Builder builder) { 20 | this.ip = builder.ip; 21 | this.username = builder.username; 22 | this.privateKeyFile = builder.privateKeyFile; 23 | this.command = builder.command; 24 | this.arguments = builder.arguments; 25 | this.name = builder.name; 26 | } 27 | 28 | public String getIp() { 29 | return ip; 30 | } 31 | 32 | public String getUsername() { 33 | return username; 34 | } 35 | 36 | public Path getPrivateKeyFile() { 37 | return privateKeyFile; 38 | } 39 | 40 | public String getCommand() { 41 | return command; 42 | } 43 | 44 | public List getArguments() { 45 | return ImmutableList.copyOf(arguments); 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public static Builder newBuilder() { 53 | return new Builder(); 54 | } 55 | 56 | public static final class Builder { 57 | private String ip; 58 | private String username; 59 | private Path privateKeyFile; 60 | private String command; 61 | private List arguments; 62 | private String name; 63 | 64 | public Builder ip(String ip) { 65 | checkArgument(ip != null); 66 | this.ip = ip; 67 | return this; 68 | } 69 | 70 | public Builder username(String username) { 71 | checkArgument(username != null); 72 | this.username = username; 73 | return this; 74 | } 75 | 76 | public Builder privateKeyFile(Path privateKeyFile) { 77 | checkArgument(privateKeyFile != null); 78 | this.privateKeyFile = privateKeyFile; 79 | return this; 80 | } 81 | 82 | public Builder command(String command) { 83 | checkArgument(command != null); 84 | this.command = command; 85 | return this; 86 | } 87 | 88 | public Builder arguments(List arguments) { 89 | checkArgument(arguments != null); 90 | this.arguments = arguments; 91 | return this; 92 | } 93 | 94 | public Builder name(String name) { 95 | checkArgument(name != null); 96 | this.name = name; 97 | return this; 98 | } 99 | 100 | public RemoteCommand build() { 101 | if (ip == null 102 | || username == null 103 | || privateKeyFile == null 104 | || command == null 105 | || arguments == null 106 | || name == null) { 107 | throw new IllegalArgumentException("Required fields are not given."); 108 | } 109 | return new RemoteCommand(this); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /gui/src/components/RestoreCluster.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 86 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/server/RemoteCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.server; 2 | 3 | import com.google.common.util.concurrent.Uninterruptibles; 4 | import com.google.inject.Inject; 5 | import com.scalar.cassy.config.CassyServerConfig; 6 | import com.scalar.cassy.db.BackupHistory; 7 | import com.scalar.cassy.db.RestoreHistory; 8 | import com.scalar.cassy.exception.RemoteExecutionException; 9 | import com.scalar.cassy.remotecommand.RemoteCommandContext; 10 | import com.scalar.cassy.remotecommand.RemoteCommandResult; 11 | import com.scalar.cassy.rpc.OperationStatus; 12 | import com.scalar.cassy.service.BackupServiceMaster; 13 | import com.scalar.cassy.util.ConnectionUtil; 14 | import java.io.IOException; 15 | import java.sql.Connection; 16 | import java.util.concurrent.BlockingQueue; 17 | import java.util.concurrent.TimeUnit; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | public class RemoteCommandHandler implements Runnable { 22 | private static final Logger logger = LoggerFactory.getLogger(RemoteCommandHandler.class); 23 | private final CassyServerConfig config; 24 | private final BlockingQueue futures; 25 | private volatile boolean isActive = true; 26 | 27 | @Inject 28 | public RemoteCommandHandler( 29 | CassyServerConfig config, BlockingQueue futures) { 30 | this.config = config; 31 | this.futures = futures; 32 | } 33 | 34 | @Override 35 | public void run() { 36 | while (isActive) { 37 | RemoteCommandContext future = null; 38 | try { 39 | future = futures.peek(); 40 | if (future == null) { 41 | Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); 42 | continue; 43 | } 44 | RemoteCommandResult result = Uninterruptibles.getUninterruptibly(future.getFuture()); 45 | if (result.getExitStatus() != 0) { 46 | throw new RemoteExecutionException( 47 | future.getCommand().getCommand() + " failed for some reason"); 48 | } 49 | updateStatus(future, OperationStatus.COMPLETED); 50 | } catch (Exception e) { 51 | logger.warn(e.getMessage(), e); 52 | if (future != null) { 53 | updateStatus(future, OperationStatus.FAILED); 54 | } 55 | } finally { 56 | if (future != null) { 57 | try { 58 | future.getFuture().close(); 59 | } catch (IOException e) { 60 | // ignore 61 | } 62 | futures.remove(); 63 | } 64 | } 65 | } 66 | } 67 | 68 | public void stop() { 69 | isActive = false; 70 | } 71 | 72 | private void updateStatus(RemoteCommandContext future, OperationStatus status) { 73 | Connection connection = null; 74 | try { 75 | connection = ConnectionUtil.create(config.getMetadataDbUrl()); 76 | if (future.getCommand().getName().equals(BackupServiceMaster.BACKUP_COMMAND)) { 77 | new BackupHistory(connection).update(future.getBackupKey(), status); 78 | } else { 79 | new RestoreHistory(connection).update(future.getBackupKey(), status); 80 | } 81 | } catch (Exception e) { 82 | logger.error( 83 | "Writing status " + status + " for " + future.getCommand().getName() + " failed.", e); 84 | } finally { 85 | ConnectionUtil.close(connection); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /gui/src/components/ConfirmRestore.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 91 | -------------------------------------------------------------------------------- /gui/src/assets/logo_scalar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/command/RestoreCommand.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.command; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import com.palantir.giraffe.file.MoreFiles; 6 | import com.scalar.cassy.config.RestoreConfig; 7 | import com.scalar.cassy.service.AwsS3RestoreModule; 8 | import com.scalar.cassy.service.AzureBlobRestoreModule; 9 | import com.scalar.cassy.service.RestoreService; 10 | import com.scalar.cassy.transferer.BackupPath; 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.Arrays; 15 | import java.util.Properties; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import picocli.CommandLine; 19 | 20 | public class RestoreCommand extends AbstractCommand { 21 | private static final Logger logger = LoggerFactory.getLogger(RestoreCommand.class); 22 | 23 | @CommandLine.Option( 24 | names = {"--restore-type"}, 25 | required = true, 26 | paramLabel = "RESTORE_TYPE", 27 | description = "The type of restore to perform") 28 | private int restoreType; 29 | 30 | @CommandLine.Option( 31 | names = {"--snapshot-only"}, 32 | description = "A flag to specify restoring snapshots only") 33 | private boolean snapshotOnly = false; 34 | 35 | public static void main(String[] args) { 36 | int exitCode = new CommandLine(new RestoreCommand()).execute(args); 37 | System.exit(exitCode); 38 | } 39 | 40 | @Override 41 | public Void call() throws Exception { 42 | Properties props = new Properties(); 43 | props.setProperty(RestoreConfig.CLUSTER_ID, clusterId); 44 | props.setProperty(RestoreConfig.SNAPSHOT_ID, snapshotId); 45 | props.setProperty(RestoreConfig.TARGET_IP, targetIp); 46 | props.setProperty(RestoreConfig.DATA_DIR, dataDir); 47 | props.setProperty(RestoreConfig.STORE_BASE_URI, storeBaseUri); 48 | props.setProperty(RestoreConfig.RESTORE_TYPE, Integer.toString(restoreType)); 49 | props.setProperty(RestoreConfig.SNAPSHOT_ONLY, Boolean.toString(snapshotOnly)); 50 | 51 | Injector injector; 52 | switch (storeType) { 53 | case AWS_S3: 54 | injector = Guice.createInjector(new AwsS3RestoreModule()); 55 | break; 56 | case AZURE_BLOB: 57 | injector = Guice.createInjector(new AzureBlobRestoreModule(storeBaseUri)); 58 | break; 59 | default: 60 | throw new UnsupportedOperationException( 61 | "The storage type " + storeType + " is not implemented"); 62 | } 63 | 64 | try (RestoreService service = injector.getInstance(RestoreService.class)) { 65 | Arrays.asList(keyspaces.split(",")) 66 | .forEach( 67 | k -> { 68 | props.setProperty(RestoreConfig.KEYSPACE, k); 69 | service.restore(new RestoreConfig(props)); 70 | // TODO reporting 71 | }); 72 | } finally { 73 | deleteStagingDirectories(props); 74 | } 75 | 76 | return null; 77 | } 78 | 79 | private void deleteStagingDirectories(Properties props) throws IOException { 80 | RestoreConfig config = new RestoreConfig(props); 81 | String stagingDirectoryName = BackupPath.getType(config); 82 | Path stagingDirectory = Paths.get(config.getDataDir(), stagingDirectoryName); 83 | logger.debug("Delete staging directories used for restoration : " + stagingDirectory); 84 | MoreFiles.deleteRecursive(stagingDirectory); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /gui/src/components/RegisterCluster.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 90 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/db/BackupHistoryRecord.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.db; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import com.scalar.cassy.config.BackupType; 6 | import com.scalar.cassy.rpc.OperationStatus; 7 | import javax.annotation.concurrent.Immutable; 8 | 9 | @Immutable 10 | public final class BackupHistoryRecord { 11 | private final String snapshotId; 12 | private final String clusterId; 13 | private final String targetIp; 14 | private final BackupType backupType; 15 | private final long createdAt; 16 | private final long updatedAt; 17 | private final OperationStatus status; 18 | 19 | private BackupHistoryRecord(Builder builder) { 20 | this.snapshotId = builder.snapshotId; 21 | this.clusterId = builder.clusterId; 22 | this.targetIp = builder.targetIp; 23 | this.backupType = builder.backupType; 24 | this.createdAt = builder.createdAt; 25 | this.updatedAt = builder.updatedAt; 26 | this.status = builder.status; 27 | } 28 | 29 | public String getSnapshotId() { 30 | return snapshotId; 31 | } 32 | 33 | public String getClusterId() { 34 | return clusterId; 35 | } 36 | 37 | public String getTargetIp() { 38 | return targetIp; 39 | } 40 | 41 | public BackupType getBackupType() { 42 | return backupType; 43 | } 44 | 45 | public long getCreatedAt() { 46 | return createdAt; 47 | } 48 | 49 | public long getUpdatedAt() { 50 | return updatedAt; 51 | } 52 | 53 | public OperationStatus getStatus() { 54 | return status; 55 | } 56 | 57 | public static Builder newBuilder() { 58 | return new Builder(); 59 | } 60 | 61 | public static final class Builder { 62 | private String snapshotId; 63 | private String clusterId; 64 | private String targetIp; 65 | private BackupType backupType; 66 | private long createdAt; 67 | private long updatedAt; 68 | private OperationStatus status; 69 | 70 | public Builder snapshotId(String snapshotId) { 71 | checkArgument(snapshotId != null); 72 | this.snapshotId = snapshotId; 73 | return this; 74 | } 75 | 76 | public Builder clusterId(String clusterId) { 77 | checkArgument(clusterId != null); 78 | this.clusterId = clusterId; 79 | return this; 80 | } 81 | 82 | public Builder targetIp(String targetIp) { 83 | checkArgument(targetIp != null); 84 | this.targetIp = targetIp; 85 | return this; 86 | } 87 | 88 | public Builder backupType(BackupType backupType) { 89 | checkArgument(backupType != null); 90 | this.backupType = backupType; 91 | return this; 92 | } 93 | 94 | public Builder createdAt(long createdAt) { 95 | checkArgument(createdAt != 0L); 96 | this.createdAt = createdAt; 97 | return this; 98 | } 99 | 100 | public Builder updatedAt(long updatedAt) { 101 | checkArgument(updatedAt != 0L); 102 | this.updatedAt = updatedAt; 103 | return this; 104 | } 105 | 106 | public Builder status(OperationStatus status) { 107 | checkArgument(status != null); 108 | this.status = status; 109 | return this; 110 | } 111 | 112 | public BackupHistoryRecord build() { 113 | if (snapshotId == null 114 | || clusterId == null 115 | || targetIp == null 116 | || backupType == null 117 | || createdAt == 0L 118 | || updatedAt == 0L 119 | || status == null) { 120 | throw new IllegalArgumentException("Required fields are not given."); 121 | } 122 | return new BackupHistoryRecord(this); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/db/RestoreHistoryRecord.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.db; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import com.scalar.cassy.config.RestoreType; 6 | import com.scalar.cassy.rpc.OperationStatus; 7 | import javax.annotation.concurrent.Immutable; 8 | 9 | @Immutable 10 | public final class RestoreHistoryRecord { 11 | private final String snapshotId; 12 | private final String clusterId; 13 | private final String targetIp; 14 | private final RestoreType restoreType; 15 | private final long createdAt; 16 | private final long updatedAt; 17 | private final OperationStatus status; 18 | 19 | private RestoreHistoryRecord(Builder builder) { 20 | this.snapshotId = builder.snapshotId; 21 | this.clusterId = builder.clusterId; 22 | this.targetIp = builder.targetIp; 23 | this.restoreType = builder.restoreType; 24 | this.createdAt = builder.createdAt; 25 | this.updatedAt = builder.updatedAt; 26 | this.status = builder.status; 27 | } 28 | 29 | public String getSnapshotId() { 30 | return snapshotId; 31 | } 32 | 33 | public String getClusterId() { 34 | return clusterId; 35 | } 36 | 37 | public String getTargetIp() { 38 | return targetIp; 39 | } 40 | 41 | public RestoreType getRestoreType() { 42 | return restoreType; 43 | } 44 | 45 | public long getCreatedAt() { 46 | return createdAt; 47 | } 48 | 49 | public long getUpdatedAt() { 50 | return updatedAt; 51 | } 52 | 53 | public OperationStatus getStatus() { 54 | return status; 55 | } 56 | 57 | public static Builder newBuilder() { 58 | return new Builder(); 59 | } 60 | 61 | public static final class Builder { 62 | private String snapshotId; 63 | private String clusterId; 64 | private String targetIp; 65 | private RestoreType restoreType; 66 | private long createdAt; 67 | private long updatedAt; 68 | private OperationStatus status; 69 | 70 | public Builder snapshotId(String snapshotId) { 71 | checkArgument(snapshotId != null); 72 | this.snapshotId = snapshotId; 73 | return this; 74 | } 75 | 76 | public Builder clusterId(String clusterId) { 77 | checkArgument(clusterId != null); 78 | this.clusterId = clusterId; 79 | return this; 80 | } 81 | 82 | public Builder targetIp(String targetIp) { 83 | checkArgument(targetIp != null); 84 | this.targetIp = targetIp; 85 | return this; 86 | } 87 | 88 | public Builder restoreType(RestoreType restoreType) { 89 | checkArgument(restoreType != null); 90 | this.restoreType = restoreType; 91 | return this; 92 | } 93 | 94 | public Builder createdAt(long createdAt) { 95 | checkArgument(createdAt != 0L); 96 | this.createdAt = createdAt; 97 | return this; 98 | } 99 | 100 | public Builder updatedAt(long updatedAt) { 101 | checkArgument(updatedAt != 0L); 102 | this.updatedAt = updatedAt; 103 | return this; 104 | } 105 | 106 | public Builder status(OperationStatus status) { 107 | checkArgument(status != null); 108 | this.status = status; 109 | return this; 110 | } 111 | 112 | public RestoreHistoryRecord build() { 113 | if (snapshotId == null 114 | || clusterId == null 115 | || targetIp == null 116 | || restoreType == null 117 | || createdAt == 0L 118 | || updatedAt == 0L 119 | || status == null) { 120 | throw new IllegalArgumentException("Required fields are not given."); 121 | } 122 | return new RestoreHistoryRecord(this); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/RestoreServiceMaster.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.scalar.cassy.config.CassyServerConfig; 5 | import com.scalar.cassy.config.RestoreType; 6 | import com.scalar.cassy.db.ClusterInfoRecord; 7 | import com.scalar.cassy.remotecommand.RemoteCommand; 8 | import com.scalar.cassy.remotecommand.RemoteCommandContext; 9 | import com.scalar.cassy.remotecommand.RemoteCommandExecutor; 10 | import com.scalar.cassy.remotecommand.RemoteCommandFuture; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | public class RestoreServiceMaster extends AbstractServiceMaster { 18 | private static final String RESTORE_TYPE_OPTION = "--restore-type="; 19 | private static final String SNAPSHOT_ONLY_OPTION = "--snapshot-only"; 20 | public static final String RESTORE_COMMAND = "cassy-restore"; 21 | 22 | public RestoreServiceMaster( 23 | CassyServerConfig config, ClusterInfoRecord clusterInfo, RemoteCommandExecutor executor) { 24 | super(config, clusterInfo, executor); 25 | } 26 | 27 | public List restoreBackup(List backupKeys, RestoreType type) { 28 | return restoreBackup(backupKeys, type, false); 29 | } 30 | 31 | public List restoreBackup( 32 | List backupKeys, RestoreType type, boolean snapshotOnly) { 33 | if (type != RestoreType.CLUSTER && type != RestoreType.NODE) { 34 | throw new IllegalArgumentException("Unsupported restore type."); 35 | } 36 | 37 | return downloadNodesBackups(backupKeys, type, snapshotOnly); 38 | } 39 | 40 | private List downloadNodesBackups( 41 | List backupKeys, RestoreType type, boolean snapshotOnly) { 42 | List futures = new ArrayList<>(); 43 | 44 | ExecutorService executor = Executors.newCachedThreadPool(); 45 | backupKeys.forEach( 46 | backupKey -> 47 | executor.submit( 48 | () -> { 49 | futures.add(downloadNodeBackups(backupKey, type, snapshotOnly)); 50 | })); 51 | awaitTermination(executor, "downloadNodesBackups"); 52 | return futures; 53 | } 54 | 55 | @VisibleForTesting 56 | RemoteCommandContext downloadNodeBackups( 57 | BackupKey backupKey, RestoreType type, boolean snapshotOnly) { 58 | List arguments = new ArrayList<>(); 59 | arguments.add(CLUSTER_ID_OPTION + backupKey.getClusterId()); 60 | arguments.add(SNAPSHOT_ID_OPTION + backupKey.getSnapshotId()); 61 | arguments.add(TARGET_IP_OPTION + backupKey.getTargetIp()); 62 | arguments.add(DATA_DIR_OPTION + clusterInfo.getDataDir()); 63 | arguments.add(STORE_BASE_URI_OPTION + config.getStorageBaseUri()); 64 | arguments.add(STORE_TYPE_OPTION + config.getStorageType()); 65 | arguments.add(KEYSPACES_OPTION + String.join(",", clusterInfo.getKeyspaces())); 66 | arguments.add(RESTORE_TYPE_OPTION + type.get()); 67 | if (snapshotOnly) { 68 | arguments.add(SNAPSHOT_ONLY_OPTION); 69 | } 70 | 71 | RemoteCommand command = 72 | RemoteCommand.newBuilder() 73 | .ip(backupKey.getTargetIp()) 74 | .username(config.getSshUser()) 75 | .privateKeyFile(Paths.get(config.getSshPrivateKeyPath())) 76 | .name(RESTORE_COMMAND) 77 | .command(config.getSlaveCommandPath() + "/" + RESTORE_COMMAND) 78 | .arguments(arguments) 79 | .build(); 80 | 81 | RemoteCommandFuture future = executor.execute(command); 82 | return new RemoteCommandContext(command, backupKey, future); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/db/ClusterInfoRecord.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.db; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import com.google.common.collect.ImmutableList; 6 | import java.util.List; 7 | import javax.annotation.concurrent.Immutable; 8 | 9 | // Not final for mocking 10 | @Immutable 11 | public class ClusterInfoRecord { 12 | private final String clusterId; 13 | private final String clusterName; 14 | private final List targetIps; 15 | private final List keyspaces; 16 | private final String dataDir; 17 | private final long createdAt; 18 | private final long updatedAt; 19 | 20 | private ClusterInfoRecord(Builder builder) { 21 | this.clusterId = builder.clusterId; 22 | this.clusterName = builder.clusterName; 23 | this.targetIps = builder.targetIps; 24 | this.keyspaces = builder.keyspaces; 25 | this.dataDir = builder.dataDir; 26 | this.createdAt = builder.createdAt; 27 | this.updatedAt = builder.updatedAt; 28 | } 29 | 30 | public String getClusterId() { 31 | return clusterId; 32 | } 33 | 34 | public String getClusterName() { 35 | return clusterName; 36 | } 37 | 38 | public List getTargetIps() { 39 | return ImmutableList.copyOf(targetIps); 40 | } 41 | 42 | public List getKeyspaces() { 43 | return ImmutableList.copyOf(keyspaces); 44 | } 45 | 46 | public String getDataDir() { 47 | return dataDir; 48 | } 49 | 50 | public long getCreatedAt() { 51 | return createdAt; 52 | } 53 | 54 | public long getUpdatedAt() { 55 | return updatedAt; 56 | } 57 | 58 | public static Builder newBuilder() { 59 | return new Builder(); 60 | } 61 | 62 | public static final class Builder { 63 | private String clusterId; 64 | private String clusterName; 65 | private List targetIps; 66 | private List keyspaces; 67 | private String dataDir; 68 | private long createdAt; 69 | private long updatedAt; 70 | 71 | public Builder clusterId(String clusterId) { 72 | checkArgument(clusterId != null); 73 | this.clusterId = clusterId; 74 | return this; 75 | } 76 | 77 | public Builder clusterName(String clusterName) { 78 | checkArgument(clusterName != null); 79 | this.clusterName = clusterName; 80 | return this; 81 | } 82 | 83 | public Builder targetIps(List targetIps) { 84 | checkArgument(targetIps != null); 85 | this.targetIps = targetIps; 86 | return this; 87 | } 88 | 89 | public Builder keyspaces(List keyspaces) { 90 | checkArgument(keyspaces != null); 91 | this.keyspaces = keyspaces; 92 | return this; 93 | } 94 | 95 | public Builder dataDir(String dataDir) { 96 | checkArgument(dataDir != null); 97 | this.dataDir = dataDir; 98 | return this; 99 | } 100 | 101 | public Builder createdAt(long createdAt) { 102 | checkArgument(createdAt != 0L); 103 | this.createdAt = createdAt; 104 | return this; 105 | } 106 | 107 | public Builder updatedAt(long updatedAt) { 108 | checkArgument(updatedAt != 0L); 109 | this.updatedAt = updatedAt; 110 | return this; 111 | } 112 | 113 | public ClusterInfoRecord build() { 114 | if (clusterId == null 115 | || clusterName == null 116 | || targetIps == null 117 | || keyspaces == null 118 | || dataDir == null 119 | || createdAt == 0L 120 | || updatedAt == 0L) { 121 | throw new IllegalArgumentException("Required fields are not given."); 122 | } 123 | return new ClusterInfoRecord(this); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/rpc/OperationStatus.java: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: cassy.proto 3 | 4 | package com.scalar.cassy.rpc; 5 | 6 | /** 7 | * Protobuf enum {@code rpc.OperationStatus} 8 | */ 9 | public enum OperationStatus 10 | implements com.google.protobuf.ProtocolMessageEnum { 11 | /** 12 | * UNKNOWN = 0; 13 | */ 14 | UNKNOWN(0), 15 | /** 16 | * INITIALIZED = 1; 17 | */ 18 | INITIALIZED(1), 19 | /** 20 | * STARTED = 2; 21 | */ 22 | STARTED(2), 23 | /** 24 | * COMPLETED = 3; 25 | */ 26 | COMPLETED(3), 27 | /** 28 | * FAILED = 4; 29 | */ 30 | FAILED(4), 31 | UNRECOGNIZED(-1), 32 | ; 33 | 34 | /** 35 | * UNKNOWN = 0; 36 | */ 37 | public static final int UNKNOWN_VALUE = 0; 38 | /** 39 | * INITIALIZED = 1; 40 | */ 41 | public static final int INITIALIZED_VALUE = 1; 42 | /** 43 | * STARTED = 2; 44 | */ 45 | public static final int STARTED_VALUE = 2; 46 | /** 47 | * COMPLETED = 3; 48 | */ 49 | public static final int COMPLETED_VALUE = 3; 50 | /** 51 | * FAILED = 4; 52 | */ 53 | public static final int FAILED_VALUE = 4; 54 | 55 | 56 | public final int getNumber() { 57 | if (this == UNRECOGNIZED) { 58 | throw new java.lang.IllegalArgumentException( 59 | "Can't get the number of an unknown enum value."); 60 | } 61 | return value; 62 | } 63 | 64 | /** 65 | * @deprecated Use {@link #forNumber(int)} instead. 66 | */ 67 | @java.lang.Deprecated 68 | public static OperationStatus valueOf(int value) { 69 | return forNumber(value); 70 | } 71 | 72 | public static OperationStatus forNumber(int value) { 73 | switch (value) { 74 | case 0: return UNKNOWN; 75 | case 1: return INITIALIZED; 76 | case 2: return STARTED; 77 | case 3: return COMPLETED; 78 | case 4: return FAILED; 79 | default: return null; 80 | } 81 | } 82 | 83 | public static com.google.protobuf.Internal.EnumLiteMap 84 | internalGetValueMap() { 85 | return internalValueMap; 86 | } 87 | private static final com.google.protobuf.Internal.EnumLiteMap< 88 | OperationStatus> internalValueMap = 89 | new com.google.protobuf.Internal.EnumLiteMap() { 90 | public OperationStatus findValueByNumber(int number) { 91 | return OperationStatus.forNumber(number); 92 | } 93 | }; 94 | 95 | public final com.google.protobuf.Descriptors.EnumValueDescriptor 96 | getValueDescriptor() { 97 | return getDescriptor().getValues().get(ordinal()); 98 | } 99 | public final com.google.protobuf.Descriptors.EnumDescriptor 100 | getDescriptorForType() { 101 | return getDescriptor(); 102 | } 103 | public static final com.google.protobuf.Descriptors.EnumDescriptor 104 | getDescriptor() { 105 | return com.scalar.cassy.rpc.CassyProto.getDescriptor().getEnumTypes().get(0); 106 | } 107 | 108 | private static final OperationStatus[] VALUES = values(); 109 | 110 | public static OperationStatus valueOf( 111 | com.google.protobuf.Descriptors.EnumValueDescriptor desc) { 112 | if (desc.getType() != getDescriptor()) { 113 | throw new java.lang.IllegalArgumentException( 114 | "EnumValueDescriptor is not for this type."); 115 | } 116 | if (desc.getIndex() == -1) { 117 | return UNRECOGNIZED; 118 | } 119 | return VALUES[desc.getIndex()]; 120 | } 121 | 122 | private final int value; 123 | 124 | private OperationStatus(int value) { 125 | this.value = value; 126 | } 127 | 128 | // @@protoc_insertion_point(enum_scope:rpc.OperationStatus) 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/ApplicationPauser.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.scalar.cassy.exception.PauseException; 5 | import com.scalar.cassy.exception.TimeoutException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.Future; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.function.Consumer; 15 | import javax.annotation.concurrent.Immutable; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.xbill.DNS.Lookup; 19 | import org.xbill.DNS.Record; 20 | import org.xbill.DNS.SRVRecord; 21 | import org.xbill.DNS.TextParseException; 22 | import org.xbill.DNS.Type; 23 | 24 | @Immutable 25 | public class ApplicationPauser { 26 | private static final Logger logger = LoggerFactory.getLogger(ApplicationPauser.class); 27 | private final Optional srvServiceUrl; 28 | 29 | public ApplicationPauser(Optional srvServiceUrl) { 30 | this.srvServiceUrl = srvServiceUrl; 31 | } 32 | 33 | public void pause() { 34 | run(client -> client.pause()); 35 | } 36 | 37 | public void unpause() { 38 | run(client -> client.unpause()); 39 | } 40 | 41 | private void run(Consumer consumer) { 42 | String serviceUrl = 43 | srvServiceUrl.orElseThrow( 44 | () -> new PauseException("Please configure srv_service_url to pause a cluster. ")); 45 | 46 | // Assume that the list of addresses for unpause is the same as the one for pause. 47 | List records = getApplicationIps(serviceUrl); 48 | 49 | List> futures = new ArrayList<>(); 50 | ExecutorService executor = Executors.newCachedThreadPool(); 51 | records.forEach( 52 | record -> { 53 | // use Callable to propagate exceptions 54 | Callable task = 55 | () -> { 56 | try (ApplicationPauseClient client = 57 | getClient(record.getTarget().toString(true), record.getPort())) { 58 | consumer.accept(client); 59 | } catch (Exception e) { 60 | logger.error(e.getMessage(), e); 61 | throw new PauseException(e); 62 | } 63 | return null; 64 | }; 65 | futures.add(executor.submit(task)); 66 | }); 67 | 68 | executor.shutdown(); 69 | try { 70 | for (Future f : futures) { 71 | f.get(); 72 | } 73 | boolean terminated = executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS); 74 | if (!terminated) { 75 | throw new TimeoutException("timeout occurred in pause or unpause."); 76 | } 77 | } catch (Exception e) { 78 | logger.error(e.getMessage(), e); 79 | throw new PauseException(e); 80 | } 81 | } 82 | 83 | @VisibleForTesting 84 | List getApplicationIps(String srvServiceUrl) { 85 | Record[] records; 86 | try { 87 | records = new Lookup(srvServiceUrl, Type.SRV).run(); 88 | if (records == null) { 89 | throw new PauseException("Can't get SRV records from " + srvServiceUrl); 90 | } 91 | } catch (TextParseException e) { 92 | logger.error(e.getMessage(), e); 93 | throw new PauseException(e); 94 | } 95 | 96 | List srvRecords = new ArrayList<>(); 97 | for (int i = 0; i < records.length; i++) { 98 | srvRecords.add((SRVRecord) records[i]); 99 | } 100 | return srvRecords; 101 | } 102 | 103 | @VisibleForTesting 104 | ApplicationPauseClient getClient(String host, int port) { 105 | return new GrpcApplicationPauseClient(host, port); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/AzureBlobFileDownloader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.azure.storage.blob.BlobContainerClient; 4 | import com.azure.storage.blob.models.BlobItem; 5 | import com.azure.storage.blob.models.ListBlobsOptions; 6 | import com.google.common.annotations.VisibleForTesting; 7 | import com.google.inject.Inject; 8 | import com.scalar.cassy.config.RestoreConfig; 9 | import com.scalar.cassy.exception.FileTransferException; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.concurrent.ExecutionException; 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.Future; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | public class AzureBlobFileDownloader implements FileDownloader { 26 | private static final Logger logger = LoggerFactory.getLogger(AzureBlobFileDownloader.class); 27 | private static final int NUM_THREADS = 3; 28 | private static final int ASYNC_FILE_DOWNLOAD_LIMIT = 20; 29 | private final ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS); 30 | private final BlobContainerClient blobContainerClient; 31 | 32 | @Inject 33 | public AzureBlobFileDownloader(BlobContainerClient blobContainerClient) { 34 | this.blobContainerClient = blobContainerClient; 35 | } 36 | 37 | @Override 38 | public void download(RestoreConfig config) { 39 | List> downloadFuture = new ArrayList<>(); 40 | String key = BackupPath.create(config, config.getKeyspace()); 41 | 42 | logger.info("Downloading " + blobContainerClient.getBlobContainerName() + "/" + key); 43 | for (BlobItem blob : listBlobs(key)) { 44 | Path destFile = Paths.get(config.getDataDir(), blob.getName()); 45 | try { 46 | Files.createDirectories(destFile.getParent()); 47 | } catch (IOException e) { 48 | throw new FileTransferException(e); 49 | } 50 | downloadFuture.add( 51 | executorService.submit( 52 | () -> { 53 | try (OutputStream outputStream = writeStream(destFile)) { 54 | blobContainerClient.getBlobClient(blob.getName()).download(outputStream); 55 | } catch (IOException e) { 56 | throw new FileTransferException(e); 57 | } 58 | logger.info("Download file succeeded : " + destFile); 59 | return null; 60 | })); 61 | 62 | if (downloadFuture.size() >= ASYNC_FILE_DOWNLOAD_LIMIT) { 63 | waitForAsyncFileDownload(downloadFuture); 64 | downloadFuture.clear(); 65 | } 66 | } 67 | 68 | if (downloadFuture.size() > 0) { 69 | waitForAsyncFileDownload(downloadFuture); 70 | } 71 | } 72 | 73 | private void waitForAsyncFileDownload(List> downloadFuture) { 74 | downloadFuture.forEach( 75 | d -> { 76 | try { 77 | // Start download files asynchronously and wait for them to complete 78 | d.get(); 79 | } catch (InterruptedException | ExecutionException e) { 80 | throw new FileTransferException(e); 81 | } 82 | }); 83 | } 84 | 85 | @Override 86 | public void close() {} 87 | 88 | @VisibleForTesting 89 | OutputStream writeStream(Path path) { 90 | try { 91 | return new FileOutputStream(path.toString()); 92 | } catch (IOException e) { 93 | throw new FileTransferException(e); 94 | } 95 | } 96 | 97 | @VisibleForTesting 98 | Iterable listBlobs(String key) { 99 | return blobContainerClient.listBlobs(new ListBlobsOptions().setPrefix(key + "/"), null); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/config/CassyServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.config; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Optional; 10 | import java.util.Properties; 11 | import javax.annotation.concurrent.Immutable; 12 | 13 | @Immutable 14 | public class CassyServerConfig { 15 | protected static final String PREFIX = "scalar.cassy.server."; 16 | public static final String PORT = PREFIX + "port"; 17 | public static final String JMX_PORT = PREFIX + "jmx_port"; 18 | public static final String SSH_USER = PREFIX + "ssh_user"; 19 | public static final String SSH_PRIVATE_KEY_PATH = PREFIX + "ssh_private_key_path"; 20 | public static final String SLAVE_COMMAND_PATH = PREFIX + "slave_command_path"; 21 | public static final String STORAGE_BASE_URI = PREFIX + "storage_base_uri"; 22 | public static final String STORAGE_TYPE = PREFIX + "storage_type"; 23 | public static final String METADATA_DB_URL = PREFIX + "metadata_db_url"; 24 | public static final String SRV_SERVICE_URL = PREFIX + "srv_service_url"; 25 | private final Properties props; 26 | private int port = 20051; 27 | private int jmxPort = 7199; 28 | private String sshUser; 29 | private String sshPrivateKeyPath; 30 | private String slaveCommandPath; 31 | private String storageBaseUri; 32 | private StorageType storageType; 33 | private String metadataDbUrl; 34 | private Optional srvServiceUrl; 35 | 36 | public CassyServerConfig(File propertiesFile) throws IOException { 37 | this(new FileInputStream(propertiesFile)); 38 | } 39 | 40 | public CassyServerConfig(InputStream stream) throws IOException { 41 | props = new Properties(); 42 | props.load(stream); 43 | load(); 44 | } 45 | 46 | public CassyServerConfig(Properties properties) { 47 | props = new Properties(properties); 48 | load(); 49 | } 50 | 51 | public Properties getProperties() { 52 | return props; 53 | } 54 | 55 | public int getPort() { 56 | return port; 57 | } 58 | 59 | public int getJmxPort() { 60 | return jmxPort; 61 | } 62 | 63 | public String getSshUser() { 64 | return sshUser; 65 | } 66 | 67 | public String getSshPrivateKeyPath() { 68 | return sshPrivateKeyPath; 69 | } 70 | 71 | public String getSlaveCommandPath() { 72 | return slaveCommandPath; 73 | } 74 | 75 | public String getStorageBaseUri() { 76 | return storageBaseUri; 77 | } 78 | 79 | public StorageType getStorageType() { return storageType; } 80 | 81 | public String getMetadataDbUrl() { 82 | return metadataDbUrl; 83 | } 84 | 85 | public Optional getSrvServiceUrl() { 86 | return srvServiceUrl; 87 | } 88 | 89 | private void load() { 90 | if (props.getProperty(PORT) != null) { 91 | port = Integer.parseInt(props.getProperty(PORT)); 92 | } 93 | if (props.getProperty(JMX_PORT) != null) { 94 | jmxPort = Integer.parseInt(props.getProperty(JMX_PORT)); 95 | } 96 | checkArgument(props.getProperty(SSH_USER) != null); 97 | sshUser = props.getProperty(SSH_USER); 98 | checkArgument(props.getProperty(SSH_PRIVATE_KEY_PATH) != null); 99 | sshPrivateKeyPath = props.getProperty(SSH_PRIVATE_KEY_PATH); 100 | checkArgument(props.getProperty(SLAVE_COMMAND_PATH) != null); 101 | slaveCommandPath = props.getProperty(SLAVE_COMMAND_PATH); 102 | checkArgument(props.getProperty(STORAGE_TYPE) != null); 103 | storageType = StorageType.valueOf(props.getProperty(STORAGE_TYPE).toUpperCase()); 104 | checkArgument(props.getProperty(STORAGE_BASE_URI) != null); 105 | storageBaseUri = props.getProperty(STORAGE_BASE_URI); 106 | checkArgument(props.getProperty(METADATA_DB_URL) != null); 107 | metadataDbUrl = props.getProperty(METADATA_DB_URL); 108 | srvServiceUrl = Optional.ofNullable(props.getProperty(SRV_SERVICE_URL)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/server/CassyServer.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.server; 2 | 3 | import com.google.inject.Guice; 4 | import com.google.inject.Injector; 5 | import com.scalar.cassy.config.CassyServerConfig; 6 | import com.scalar.cassy.rpc.CassyGrpc.CassyImplBase; 7 | import io.grpc.ServerBuilder; 8 | import io.grpc.protobuf.services.ProtoReflectionService; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | import java.util.concurrent.TimeUnit; 14 | import javax.annotation.concurrent.Immutable; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | @Immutable 19 | public final class CassyServer extends CassyImplBase { 20 | private static final Logger logger = LoggerFactory.getLogger(CassyServer.class); 21 | private final CassyServerConfig config; 22 | private io.grpc.Server server; 23 | private Injector injector; 24 | private RemoteCommandHandler remoteCommandHandler; 25 | private ExecutorService handlerService; 26 | 27 | public CassyServer(CassyServerConfig config) { 28 | this.config = config; 29 | } 30 | 31 | private void start() throws IOException { 32 | injector = Guice.createInjector(new CassyServerModule(config)); 33 | remoteCommandHandler = injector.getInstance(RemoteCommandHandler.class); 34 | handlerService = Executors.newFixedThreadPool(1); 35 | handlerService.submit(remoteCommandHandler); 36 | 37 | ServerBuilder builder = 38 | ServerBuilder.forPort(config.getPort()) 39 | .addService(injector.getInstance(CassyServerController.class)) 40 | .addService(ProtoReflectionService.newInstance()); 41 | 42 | server = builder.build().start(); 43 | logger.info("Server started, listening on " + config.getPort()); 44 | 45 | Runtime.getRuntime() 46 | .addShutdownHook( 47 | new Thread( 48 | () -> { 49 | // Use stderr here since the logger may have been reset by its JVM shutdown hook. 50 | System.err.println("*** shutting down gRPC server since JVM is shutting down"); 51 | CassyServer.this.stop(); 52 | System.err.println("*** server shut down"); 53 | })); 54 | } 55 | 56 | private void stop() { 57 | if (server != null) { 58 | try { 59 | server.shutdown().awaitTermination(10, TimeUnit.SECONDS); 60 | } catch (InterruptedException e) { 61 | Thread.interrupted(); 62 | logger.warn("CassyServer shutdown is interrupted.", e); 63 | } 64 | } 65 | if (handlerService != null) { 66 | remoteCommandHandler.stop(); 67 | handlerService.shutdown(); 68 | try { 69 | handlerService.awaitTermination(10, TimeUnit.SECONDS); 70 | } catch (InterruptedException e) { 71 | Thread.interrupted(); 72 | logger.warn("RemoteCommandHandler shutdown is interrupted.", e); 73 | } 74 | } 75 | } 76 | 77 | /** Await termination on the main thread since the grpc library uses daemon threads. */ 78 | private void blockUntilShutdown() throws InterruptedException { 79 | if (server != null) { 80 | server.awaitTermination(); 81 | handlerService.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS); 82 | } 83 | } 84 | 85 | public static void main(String[] args) throws IOException, InterruptedException { 86 | CassyServerConfig config = null; 87 | for (int i = 0; i < args.length; ++i) { 88 | if ("--config".equals(args[i])) { 89 | config = new CassyServerConfig(new File(args[++i])); 90 | } else if ("-help".equals(args[i])) { 91 | printUsageAndExit(); 92 | } 93 | } 94 | if (config == null) { 95 | printUsageAndExit(); 96 | } 97 | 98 | final CassyServer server = new CassyServer(config); 99 | server.start(); 100 | server.blockUntilShutdown(); 101 | } 102 | 103 | private static void printUsageAndExit() { 104 | System.err.println("CassyServer --config backup-server.properties"); 105 | System.exit(1); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/AwsS3FileUploader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.amazonaws.AmazonServiceException; 4 | import com.amazonaws.services.s3.AmazonS3; 5 | import com.amazonaws.services.s3.AmazonS3URI; 6 | import com.amazonaws.services.s3.transfer.TransferManager; 7 | import com.amazonaws.services.s3.transfer.Upload; 8 | import com.google.common.annotations.VisibleForTesting; 9 | import com.google.inject.Inject; 10 | import com.scalar.cassy.config.BackupConfig; 11 | import com.scalar.cassy.exception.FileIOException; 12 | import com.scalar.cassy.exception.FileTransferException; 13 | import java.io.IOException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.Future; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | public class AwsS3FileUploader implements FileUploader { 27 | private static final Logger logger = LoggerFactory.getLogger(AwsS3FileUploader.class); 28 | private static final int NUM_THREADS = 3; 29 | private final ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS); 30 | private final TransferManager manager; 31 | private final AmazonS3 s3; 32 | private final AmazonS3URI s3Uri; 33 | 34 | @Inject 35 | public AwsS3FileUploader(TransferManager manager, AmazonS3 s3, AmazonS3URI s3Uri) { 36 | this.manager = manager; 37 | this.s3 = s3; 38 | this.s3Uri = s3Uri; 39 | } 40 | 41 | @Override 42 | public Future upload(Path file, String key) { 43 | if (!requiresUpload(s3Uri.getBucket(), key, file)) { 44 | logger.info(file + " has been already uploaded."); 45 | return CompletableFuture.completedFuture(null); 46 | } 47 | 48 | logger.info("Uploading " + file); 49 | return executorService.submit( 50 | () -> { 51 | manager.upload(s3Uri.getBucket(), key, file.toFile()).waitForCompletion(); 52 | return null; 53 | }); 54 | } 55 | 56 | @Override 57 | public void upload(List files, BackupConfig config) { 58 | Path dataDir = Paths.get(config.getDataDir()); 59 | 60 | List uploads = new ArrayList<>(); 61 | files.forEach( 62 | p -> { 63 | Path relative = dataDir.relativize(p); 64 | String key = BackupPath.create(config, relative.toString()); 65 | if (requiresUpload(s3Uri.getBucket(), key, p)) { 66 | logger.info("Uploading " + p); 67 | try { 68 | uploads.add(manager.upload(s3Uri.getBucket(), key, p.toFile())); 69 | } catch (RuntimeException e) { 70 | throw new FileTransferException(e); 71 | } 72 | } else { 73 | logger.info(p + " has been already uploaded."); 74 | } 75 | }); 76 | 77 | uploads.forEach( 78 | u -> { 79 | try { 80 | u.waitForCompletion(); 81 | logger.info(u.getDescription() + " - " + u.getState().name()); 82 | } catch (InterruptedException e) { 83 | Thread.currentThread().interrupt(); 84 | throw new FileTransferException(e); 85 | } 86 | }); 87 | } 88 | 89 | @Override 90 | public void close() { 91 | manager.shutdownNow(); 92 | } 93 | 94 | @VisibleForTesting 95 | boolean requiresUpload(String bucket, String key, Path file) { 96 | try { 97 | if (s3.getObjectMetadata(bucket, key).getContentLength() == Files.size(file)) { 98 | return false; 99 | } 100 | } catch (IOException e) { 101 | throw new FileIOException(e); 102 | } catch (AmazonServiceException e) { 103 | // if the file doesn't exist in the bucket, it will be uploaded 104 | if (e.getStatusCode() == 404) { 105 | return true; 106 | } 107 | throw new FileTransferException(e); 108 | } 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/com/scalar/cassy/transferer/AwsS3FileDownloaderTest.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.Mockito.never; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import com.amazonaws.services.s3.AmazonS3URI; 11 | import com.amazonaws.services.s3.transfer.MultipleFileDownload; 12 | import com.amazonaws.services.s3.transfer.Transfer; 13 | import com.amazonaws.services.s3.transfer.TransferManager; 14 | import com.scalar.cassy.config.BackupType; 15 | import com.scalar.cassy.config.RestoreConfig; 16 | import com.scalar.cassy.exception.FileTransferException; 17 | import java.io.File; 18 | import java.nio.file.Paths; 19 | import java.util.Properties; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.mockito.InjectMocks; 23 | import org.mockito.Mock; 24 | import org.mockito.Mockito; 25 | import org.mockito.MockitoAnnotations; 26 | 27 | public class AwsS3FileDownloaderTest { 28 | private static final String KEYSPACE_DIR = "keyspace1"; 29 | private static final String ANY_CLUSTER_ID = "cluster_id"; 30 | private static final String ANY_SNAPSHOT_ID = "snapshot_id"; 31 | private static final String ANY_TARGET_IP = "target_ip"; 32 | private static final String ANY_TMP_DATA_DIR = "tmp_data_dir"; 33 | private static final String ANY_S3_URI = "s3://scalar"; 34 | private AmazonS3URI s3Uri; 35 | @Mock private TransferManager manager; 36 | @Mock private MultipleFileDownload download; 37 | @InjectMocks private AwsS3FileDownloader downloader; 38 | 39 | @Before 40 | public void setUp() { 41 | MockitoAnnotations.initMocks(this); 42 | this.s3Uri = new AmazonS3URI(ANY_S3_URI); 43 | } 44 | 45 | public Properties getProperties(BackupType type, String dataDir) { 46 | Properties props = new Properties(); 47 | props.setProperty(RestoreConfig.CLUSTER_ID, ANY_CLUSTER_ID); 48 | props.setProperty(RestoreConfig.SNAPSHOT_ID, ANY_SNAPSHOT_ID); 49 | props.setProperty(RestoreConfig.RESTORE_TYPE, Integer.toString(type.get())); 50 | props.setProperty(RestoreConfig.TARGET_IP, ANY_TARGET_IP); 51 | props.setProperty(RestoreConfig.DATA_DIR, dataDir); 52 | props.setProperty(RestoreConfig.STORE_BASE_URI, ANY_S3_URI); 53 | props.setProperty(RestoreConfig.KEYSPACE, KEYSPACE_DIR); 54 | return props; 55 | } 56 | 57 | @Test 58 | public void download_ConfigGiven_ShouldDownloadProperly() throws InterruptedException { 59 | // Arrange 60 | RestoreConfig config = 61 | new RestoreConfig(getProperties(BackupType.NODE_SNAPSHOT, ANY_TMP_DATA_DIR)); 62 | when(download.getDescription()).thenReturn("anything"); 63 | when(download.getState()).thenReturn(Transfer.TransferState.Completed); 64 | when(manager.downloadDirectory(anyString(), anyString(), any(File.class))).thenReturn(download); 65 | 66 | // Act 67 | downloader.download(config); 68 | 69 | // Assert 70 | verify(manager) 71 | .downloadDirectory( 72 | s3Uri.getBucket(), 73 | BackupPath.create(config, config.getKeyspace()), 74 | Paths.get(config.getDataDir()).toFile()); 75 | verify(download).waitForCompletion(); 76 | } 77 | 78 | @Test 79 | public void download_RuntimeExceptionThrown_ShouldThrowFileTransferException() 80 | throws InterruptedException { 81 | // Arrange 82 | RestoreConfig config = 83 | new RestoreConfig(getProperties(BackupType.NODE_SNAPSHOT, ANY_TMP_DATA_DIR)); 84 | RuntimeException toThrow = Mockito.mock(RuntimeException.class); 85 | when(manager.downloadDirectory(anyString(), anyString(), any(File.class))).thenThrow(toThrow); 86 | 87 | // Act 88 | assertThatThrownBy(() -> downloader.download(config)) 89 | .isInstanceOf(FileTransferException.class) 90 | .hasCause(toThrow); 91 | 92 | // Assert 93 | verify(manager) 94 | .downloadDirectory( 95 | s3Uri.getBucket(), 96 | BackupPath.create(config, config.getKeyspace()), 97 | Paths.get(config.getDataDir()).toFile()); 98 | verify(download, never()).waitForCompletion(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/integration-test/java/com/scalar/cassy/traverser/IncrementalBackupTraverserTest.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.traverser; 2 | 3 | import static org.assertj.core.api.Java6Assertions.assertThat; 4 | 5 | import com.google.common.base.Joiner; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.Arrays; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.UUID; 17 | import org.junit.AfterClass; 18 | import org.junit.Before; 19 | import org.junit.BeforeClass; 20 | import org.junit.Test; 21 | 22 | public class IncrementalBackupTraverserTest { 23 | private static final String DATA_DIR = "/tmp/" + UUID.randomUUID(); 24 | private static final String KEYSPACE_DIR_1 = "keyspace1"; 25 | private static final String KEYSPACE_DIR_2 = "keyspace2"; 26 | private static final String TABLE_DIR_1 = "standard1-xxx"; 27 | private static final String TABLE_DIR_2 = "standard2-xxx"; 28 | private static final String BACKUP_DIR = IncrementalBackupTraverser.BACKUP_DIRNAME; 29 | private static final String FILE1 = "file1"; 30 | private static final String FILE2 = "file2"; 31 | private static final Joiner joiner = Joiner.on("/").skipNulls(); 32 | private static final FileSystem fs = FileSystems.getDefault(); 33 | private IncrementalBackupTraverser traverser; 34 | 35 | @BeforeClass 36 | public static void setUpBeforeClass() { 37 | getListOfFiles() 38 | .forEach( 39 | p -> { 40 | try { 41 | Files.createDirectories(p.getParent()); 42 | Files.createFile(p); 43 | } catch (IOException e) { 44 | throw new RuntimeException(e); 45 | } 46 | }); 47 | } 48 | 49 | @AfterClass 50 | public static void tearDownAfterClass() throws IOException { 51 | Files.walk(Paths.get(DATA_DIR)) 52 | .map(Path::toFile) 53 | .sorted(Comparator.reverseOrder()) 54 | .forEach(File::delete); 55 | } 56 | 57 | private static List getListOfFiles() { 58 | return Arrays.asList( 59 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, BACKUP_DIR, FILE1)), 60 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, BACKUP_DIR, FILE2)), 61 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE1)), 62 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE2)), 63 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_1, BACKUP_DIR, FILE1)), 64 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_1, BACKUP_DIR, FILE2)), 65 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_2, BACKUP_DIR, FILE1)), 66 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_2, BACKUP_DIR, FILE2))); 67 | } 68 | 69 | @Before 70 | public void setUp() { 71 | this.traverser = new IncrementalBackupTraverser(FileSystems.getDefault().getPath(DATA_DIR)); 72 | } 73 | 74 | @Test 75 | public void traverse_KeyspaceGiven_ReturnPathsWithTheKeyspace() { 76 | // Arrange 77 | 78 | // Act 79 | List files = traverser.traverse(KEYSPACE_DIR_1); 80 | 81 | // Assert 82 | assertThat(files) 83 | .containsOnly( 84 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, BACKUP_DIR, FILE1)), 85 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, BACKUP_DIR, FILE2)), 86 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE1)), 87 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE2))); 88 | } 89 | 90 | @Test 91 | public void traverse_KeyspaceAndTableGiven_ReturnPathsWithTheKeyspaceAndTheTable() { 92 | // Arrange 93 | 94 | // Act 95 | List files = traverser.traverse(KEYSPACE_DIR_1, TABLE_DIR_2); 96 | 97 | // Assert 98 | assertThat(files) 99 | .containsOnly( 100 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE1)), 101 | fs.getPath(joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, BACKUP_DIR, FILE2))); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/transferer/AzureBlobFileUploader.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.transferer; 2 | 3 | import com.azure.storage.blob.BlobClient; 4 | import com.azure.storage.blob.BlobContainerClient; 5 | import com.azure.storage.blob.models.BlobProperties; 6 | import com.azure.storage.blob.models.BlobStorageException; 7 | import com.google.common.annotations.VisibleForTesting; 8 | import com.google.inject.Inject; 9 | import com.scalar.cassy.config.BackupConfig; 10 | import com.scalar.cassy.exception.FileIOException; 11 | import com.scalar.cassy.exception.FileTransferException; 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.nio.file.Paths; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.concurrent.Future; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | public class AzureBlobFileUploader implements FileUploader { 30 | private static final Logger logger = LoggerFactory.getLogger(AzureBlobFileUploader.class); 31 | private static final int NUM_THREADS = 3; 32 | private static final int ASYNC_FILE_UPLOAD_LIMIT = 20; 33 | private final ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS); 34 | private final BlobContainerClient blobContainerClient; 35 | 36 | @Inject 37 | public AzureBlobFileUploader(BlobContainerClient blobContainerClient) { 38 | this.blobContainerClient = blobContainerClient; 39 | } 40 | 41 | @Override 42 | public Future upload(Path file, String key) { 43 | if (!requiresUpload(key, file)) { 44 | logger.info(file + " has been already uploaded."); 45 | return CompletableFuture.completedFuture(null); 46 | } 47 | 48 | logger.info("Uploading " + file); 49 | return executorService.submit( 50 | () -> { 51 | try (InputStream inputStream = readStream(file)) { 52 | blobContainerClient 53 | .getBlobClient(key) 54 | .upload(inputStream, new File(file.toString()).length(), true); 55 | } catch (IOException e) { 56 | throw new FileTransferException(e); 57 | } 58 | 59 | logger.info("Upload file succeeded : " + file); 60 | return null; 61 | }); 62 | } 63 | 64 | @Override 65 | public void upload(List files, BackupConfig config) { 66 | Path dataDir = Paths.get(config.getDataDir()); 67 | List> uploads = new ArrayList<>(); 68 | for (Path filePath : files) { 69 | Path relative = dataDir.relativize(filePath); 70 | String key = BackupPath.create(config, relative.toString()); 71 | uploads.add(upload(filePath, key)); 72 | 73 | if (uploads.size() >= ASYNC_FILE_UPLOAD_LIMIT) { 74 | waitForAsyncFileUpload(uploads); 75 | uploads.clear(); 76 | } 77 | } 78 | 79 | if (uploads.size() > 0) { 80 | waitForAsyncFileUpload(uploads); 81 | } 82 | } 83 | 84 | private void waitForAsyncFileUpload(List> uploads) { 85 | uploads.forEach( 86 | u -> { 87 | try { 88 | // Start upload files asynchronously and wait for them to complete 89 | u.get(); 90 | } catch (InterruptedException | ExecutionException e) { 91 | throw new FileTransferException(e); 92 | } 93 | }); 94 | } 95 | 96 | @Override 97 | public void close() {} 98 | 99 | @VisibleForTesting 100 | boolean requiresUpload(String key, Path file) { 101 | try { 102 | BlobClient client = blobContainerClient.getBlobClient(key); 103 | BlobProperties blobProperties = client.getProperties(); 104 | 105 | if (blobProperties.getBlobSize() == Files.size(file)) { 106 | return false; 107 | } 108 | } catch (IOException e) { 109 | throw new FileIOException(e); 110 | } catch (BlobStorageException e) { 111 | if (e.getStatusCode() == 404) { 112 | return true; 113 | } 114 | throw new FileTransferException(e); 115 | } 116 | return true; 117 | } 118 | 119 | @VisibleForTesting 120 | InputStream readStream(Path path) { 121 | try { 122 | return new FileInputStream(path.toString()); 123 | } catch (IOException e) { 124 | throw new FileTransferException(e); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/db/ClusterInfo.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.db; 2 | 3 | import com.scalar.cassy.exception.DatabaseException; 4 | import java.sql.Connection; 5 | import java.sql.PreparedStatement; 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import javax.annotation.concurrent.NotThreadSafe; 13 | 14 | @NotThreadSafe 15 | public class ClusterInfo { 16 | private static final String DELIMITER = ","; 17 | static final String INSERT = 18 | "INSERT INTO cluster_info " 19 | + "(cluster_id, cluster_name, target_ips, keyspaces, data_dir, created_at, updated_at) " 20 | + "VALUES (?, ?, ?, ?, ?, ?, ?)"; 21 | static final String SELECT_BY_CLUSTER = "SELECT * FROM cluster_info WHERE cluster_id = ?"; 22 | static final String SELECT_RECENT = "SELECT * FROM cluster_info ORDER BY created_at DESC limit ?"; 23 | private final Connection connection; 24 | private final PreparedStatement insert; 25 | private final PreparedStatement selectByCluster; 26 | private final PreparedStatement selectRecent; 27 | 28 | public ClusterInfo(Connection connection) { 29 | this.connection = connection; 30 | try { 31 | insert = connection.prepareStatement(INSERT); 32 | insert.setQueryTimeout(30); 33 | selectByCluster = connection.prepareStatement(SELECT_BY_CLUSTER); 34 | selectByCluster.setQueryTimeout(30); 35 | selectRecent = connection.prepareStatement(SELECT_RECENT); 36 | selectRecent.setQueryTimeout(30); 37 | } catch (SQLException e) { 38 | throw new DatabaseException(e); 39 | } 40 | } 41 | 42 | public void insert( 43 | String clusterId, 44 | String clusterName, 45 | List targetIps, 46 | List keyspaces, 47 | String dataDir) { 48 | try { 49 | insertImpl(clusterId, clusterName, targetIps, keyspaces, dataDir); 50 | } catch (SQLException e) { 51 | throw new DatabaseException(e); 52 | } 53 | } 54 | 55 | public Optional selectByClusterId(String clusterId) { 56 | List records; 57 | try { 58 | selectByCluster.setString(1, clusterId); 59 | ResultSet resultSet = selectByCluster.executeQuery(); 60 | records = traverseResults(resultSet); 61 | selectByCluster.clearParameters(); 62 | } catch (SQLException e) { 63 | throw new DatabaseException(e); 64 | } 65 | 66 | if (records.isEmpty()) { 67 | return Optional.empty(); 68 | } 69 | return Optional.of(records.get(0)); 70 | } 71 | 72 | public List selectRecent(int n) { 73 | List records; 74 | try { 75 | selectRecent.setInt(1, n); 76 | ResultSet resultSet = selectRecent.executeQuery(); 77 | records = traverseResults(resultSet); 78 | selectRecent.clearParameters(); 79 | } catch (SQLException e) { 80 | throw new DatabaseException(e); 81 | } 82 | 83 | return records; 84 | } 85 | 86 | private void insertImpl( 87 | String clusterId, 88 | String clusterName, 89 | List targetIps, 90 | List keyspaces, 91 | String dataDir) 92 | throws SQLException { 93 | insert.clearParameters(); 94 | long currentTimestamp = System.currentTimeMillis(); 95 | insert.setString(1, clusterId); 96 | insert.setString(2, clusterName); 97 | insert.setString(3, String.join(DELIMITER, targetIps)); 98 | insert.setString(4, String.join(DELIMITER, keyspaces)); 99 | insert.setString(5, dataDir); 100 | insert.setLong(6, currentTimestamp); 101 | insert.setLong(7, currentTimestamp); 102 | if (insert.executeUpdate() != 1) { 103 | throw new SQLException("Inserting the record failed."); 104 | } 105 | } 106 | 107 | private List traverseResults(ResultSet resultSet) { 108 | List records = new ArrayList<>(); 109 | try { 110 | while (resultSet.next()) { 111 | ClusterInfoRecord.Builder builder = ClusterInfoRecord.newBuilder(); 112 | builder.clusterId(resultSet.getString("cluster_id")); 113 | builder.clusterName(resultSet.getString("cluster_name")); 114 | builder.targetIps(Arrays.asList(resultSet.getString("target_ips").split(DELIMITER))); 115 | builder.keyspaces(Arrays.asList(resultSet.getString("keyspaces").split(DELIMITER))); 116 | builder.dataDir(resultSet.getString("data_dir")); 117 | builder.createdAt(resultSet.getLong("created_at")); 118 | builder.updatedAt(resultSet.getLong("updated_at")); 119 | records.add(builder.build()); 120 | } 121 | } catch (SQLException e) { 122 | throw new DatabaseException(e); 123 | } 124 | return records; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /gui/src/components/CreateBackup.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 113 | -------------------------------------------------------------------------------- /src/integration-test/java/com/scalar/cassy/traverser/SnapshotTraverserTest.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.traverser; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.google.common.base.Joiner; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.Arrays; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.UUID; 17 | import org.junit.AfterClass; 18 | import org.junit.Before; 19 | import org.junit.BeforeClass; 20 | import org.junit.Test; 21 | 22 | public class SnapshotTraverserTest { 23 | private static final String DATA_DIR = "/tmp/" + UUID.randomUUID(); 24 | private static final String KEYSPACE_DIR_1 = "keyspace1"; 25 | private static final String KEYSPACE_DIR_2 = "keyspace2"; 26 | private static final String TABLE_DIR_1 = "standard1-xxx"; 27 | private static final String TABLE_DIR_2 = "standard2-xxx"; 28 | private static final String SNAPSHOT_DIR = SnapshotTraverser.SNAPSHOT_DIRNAME; 29 | private static final String SNAPSHOT_ID = "unique-snapshot-id"; 30 | private static final String FILE1 = "file1"; 31 | private static final String FILE2 = "file2"; 32 | private static final Joiner joiner = Joiner.on("/").skipNulls(); 33 | private static final FileSystem fs = FileSystems.getDefault(); 34 | private SnapshotTraverser traverser; 35 | 36 | @BeforeClass 37 | public static void setUpBeforeClass() { 38 | getListOfFiles() 39 | .forEach( 40 | p -> { 41 | try { 42 | Files.createDirectories(p.getParent()); 43 | Files.createFile(p); 44 | } catch (IOException e) { 45 | throw new RuntimeException(e); 46 | } 47 | }); 48 | } 49 | 50 | @AfterClass 51 | public static void tearDownAfterClass() throws IOException { 52 | Files.walk(Paths.get(DATA_DIR)) 53 | .map(Path::toFile) 54 | .sorted(Comparator.reverseOrder()) 55 | .forEach(File::delete); 56 | } 57 | 58 | private static List getListOfFiles() { 59 | return Arrays.asList( 60 | fs.getPath( 61 | joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 62 | fs.getPath( 63 | joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2)), 64 | fs.getPath( 65 | joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 66 | fs.getPath( 67 | joiner.join(DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2)), 68 | fs.getPath( 69 | joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 70 | fs.getPath( 71 | joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2)), 72 | fs.getPath( 73 | joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 74 | fs.getPath( 75 | joiner.join(DATA_DIR, KEYSPACE_DIR_2, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2))); 76 | } 77 | 78 | @Before 79 | public void setUp() { 80 | this.traverser = new SnapshotTraverser(FileSystems.getDefault().getPath(DATA_DIR), SNAPSHOT_ID); 81 | } 82 | 83 | @Test 84 | public void traverse_KeyspaceGiven_ReturnPathsWithTheKeyspace() { 85 | // Arrange 86 | 87 | // Act 88 | List files = traverser.traverse(KEYSPACE_DIR_1); 89 | 90 | // Assert 91 | assertThat(files) 92 | .containsOnly( 93 | fs.getPath( 94 | joiner.join( 95 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 96 | fs.getPath( 97 | joiner.join( 98 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_1, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2)), 99 | fs.getPath( 100 | joiner.join( 101 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 102 | fs.getPath( 103 | joiner.join( 104 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2))); 105 | } 106 | 107 | @Test 108 | public void traverse_KeyspaceAndTableGiven_ReturnPathsWithTheKeyspaceAndTheTable() { 109 | // Arrange 110 | 111 | // Act 112 | List files = traverser.traverse(KEYSPACE_DIR_1, TABLE_DIR_2); 113 | 114 | // Assert 115 | assertThat(files) 116 | .containsOnly( 117 | fs.getPath( 118 | joiner.join( 119 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE1)), 120 | fs.getPath( 121 | joiner.join( 122 | DATA_DIR, KEYSPACE_DIR_1, TABLE_DIR_2, SNAPSHOT_DIR, SNAPSHOT_ID, FILE2))); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/proto/cassy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/api/annotations.proto"; 4 | import "google/protobuf/empty.proto"; 5 | 6 | option java_multiple_files = true; 7 | option java_package = "com.scalar.cassy.rpc"; 8 | option java_outer_classname = "CassyProto"; 9 | 10 | package rpc; 11 | 12 | service Cassy { 13 | rpc RegisterCluster (ClusterRegistrationRequest) returns (ClusterRegistrationResponse) { 14 | option (google.api.http) = { 15 | post: "/v1/clusters" 16 | body: "*" 17 | }; 18 | } 19 | rpc ListClusters (ClusterListingRequest) returns (ClusterListingResponse) { 20 | option (google.api.http) = { 21 | get: "/v1/clusters/{cluster_id}" 22 | }; 23 | } 24 | rpc ListBackups (BackupListingRequest) returns (BackupListingResponse) { 25 | option (google.api.http) = { 26 | get: "/v1/clusters/{cluster_id}/backups" 27 | }; 28 | } 29 | rpc TakeBackup (BackupRequest) returns (BackupResponse) { 30 | option (google.api.http) = { 31 | post: "/v1/clusters/{cluster_id}/backups" 32 | body: "*" 33 | }; 34 | } 35 | rpc RestoreBackup (RestoreRequest) returns (RestoreResponse) { 36 | option (google.api.http) = { 37 | put: "/v1/clusters/{cluster_id}/data/{snapshot_id}" 38 | body: "*" 39 | }; 40 | } 41 | rpc ListRestoreStatuses (RestoreStatusListingRequest) returns (RestoreStatusListingResponse) { 42 | option (google.api.http) = { 43 | get: "/v1/clusters/{cluster_id}/data/{snapshot_id}" 44 | }; 45 | } 46 | } 47 | 48 | message ClusterRegistrationRequest { 49 | string cassandra_host = 1; 50 | } 51 | 52 | message ClusterRegistrationResponse { 53 | string cluster_id = 1; 54 | string cluster_name = 2; 55 | repeated string target_ips = 3; 56 | repeated string keyspaces = 4; 57 | string data_dir = 5; 58 | } 59 | 60 | message ClusterListingRequest { 61 | string cluster_id = 1; 62 | int32 limit = 2; 63 | } 64 | 65 | message ClusterListingResponse { 66 | message Entry { 67 | string cluster_id = 1; 68 | string cluster_name = 2; 69 | repeated string target_ips = 3; 70 | repeated string keyspaces = 4; 71 | string data_dir = 5; 72 | uint64 created_at = 6; 73 | uint64 updated_at = 7; 74 | } 75 | repeated Entry entries = 1; 76 | } 77 | 78 | message BackupListingRequest { 79 | string cluster_id = 1; 80 | string target_ip = 2; 81 | int32 limit = 3; 82 | string snapshot_id = 4; 83 | } 84 | 85 | message BackupListingResponse { 86 | message Entry { 87 | string cluster_id = 1; 88 | string target_ip = 2; 89 | string snapshot_id = 3; 90 | uint64 created_at = 4; 91 | uint64 updated_at = 5; 92 | uint32 backup_type = 6; 93 | OperationStatus status = 7; 94 | } 95 | repeated Entry entries = 1; 96 | } 97 | 98 | enum OperationStatus { 99 | UNKNOWN = 0; 100 | INITIALIZED = 1; 101 | STARTED = 2; 102 | COMPLETED = 3; 103 | FAILED = 4; 104 | } 105 | 106 | message BackupRequest { 107 | string cluster_id = 1; 108 | repeated string target_ips = 2; // optional 109 | string snapshot_id = 3; // optional 110 | uint32 backup_type = 4; 111 | } 112 | 113 | message BackupResponse { 114 | OperationStatus status = 1; 115 | string cluster_id = 2; 116 | repeated string target_ips = 3; 117 | string snapshot_id = 4; 118 | uint64 created_at = 5; 119 | uint32 backup_type = 6; 120 | } 121 | 122 | message RestoreRequest { 123 | string cluster_id = 1; 124 | repeated string target_ips = 2; // optional 125 | string snapshot_id = 3; 126 | uint32 restore_type = 4; 127 | bool snapshot_only = 5; // optional (default: false) 128 | } 129 | 130 | message RestoreResponse { 131 | OperationStatus status = 1; 132 | string cluster_id = 2; 133 | repeated string target_ips = 3; 134 | string snapshot_id = 4; 135 | uint32 restore_type = 5; 136 | bool snapshot_only = 6; 137 | } 138 | 139 | message RestoreStatusListingRequest { 140 | string cluster_id = 1; 141 | string target_ip = 2; // optional 142 | string snapshot_id = 3; 143 | int32 limit = 4; 144 | } 145 | 146 | message RestoreStatusListingResponse { 147 | message Entry { 148 | string target_ip = 1; 149 | string snapshot_id = 2; 150 | uint64 created_at = 3; 151 | uint64 updated_at = 4; 152 | uint32 restore_type = 5; 153 | OperationStatus status = 6; 154 | } 155 | string cluster_id = 1; 156 | repeated Entry entries = 3; 157 | } 158 | 159 | service Admin { 160 | rpc Pause (PauseRequest) returns (google.protobuf.Empty) { 161 | } 162 | rpc Unpause (google.protobuf.Empty) returns (google.protobuf.Empty) { 163 | } 164 | rpc Stats (google.protobuf.Empty) returns (StatsResponse) { 165 | } 166 | } 167 | 168 | message PauseRequest { 169 | bool wait_outstanding = 1; 170 | } 171 | 172 | message StatsResponse { 173 | string stats = 1; // json-formatted 174 | } 175 | -------------------------------------------------------------------------------- /gui/src/components/Backups.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 134 | 141 | -------------------------------------------------------------------------------- /src/main/java/com/scalar/cassy/service/BackupServiceMaster.java: -------------------------------------------------------------------------------- 1 | package com.scalar.cassy.service; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.scalar.cassy.config.BackupType; 5 | import com.scalar.cassy.config.CassyServerConfig; 6 | import com.scalar.cassy.db.ClusterInfoRecord; 7 | import com.scalar.cassy.jmx.JmxManager; 8 | import com.scalar.cassy.remotecommand.RemoteCommand; 9 | import com.scalar.cassy.remotecommand.RemoteCommandContext; 10 | import com.scalar.cassy.remotecommand.RemoteCommandExecutor; 11 | import com.scalar.cassy.remotecommand.RemoteCommandFuture; 12 | import java.nio.file.Paths; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | public class BackupServiceMaster extends AbstractServiceMaster { 22 | private static final Logger logger = LoggerFactory.getLogger(BackupServiceMaster.class); 23 | private static final String BACKUP_TYPE_OPTION = "--backup-type="; 24 | public static final String BACKUP_COMMAND = "cassy-backup"; 25 | private ApplicationPauser pauser; 26 | 27 | public BackupServiceMaster( 28 | CassyServerConfig config, 29 | ClusterInfoRecord clusterInfo, 30 | RemoteCommandExecutor executor, 31 | ApplicationPauser pauser) { 32 | super(config, clusterInfo, executor); 33 | this.pauser = pauser; 34 | } 35 | 36 | public List takeBackup(List backupKeys, BackupType type) { 37 | switch (type) { 38 | case CLUSTER_SNAPSHOT: 39 | return takeClusterSnapshots(backupKeys, type); 40 | case NODE_SNAPSHOT: 41 | case NODE_INCREMENTAL: 42 | return takeNodesBackups(backupKeys, type); 43 | default: 44 | throw new IllegalArgumentException("Unsupported backup type."); 45 | } 46 | } 47 | 48 | private List takeClusterSnapshots( 49 | List backupKeys, BackupType type) { 50 | // 1. pause C* applications 51 | pauser.pause(); 52 | 53 | // 2. take snapshots of all the nodes in a C* cluster 54 | takeNodesSnapshots(backupKeys); 55 | 56 | // 3. unpause C* applications 57 | pauser.unpause(); 58 | 59 | // 4. copy snapshots in parallel 60 | return uploadNodesBackups(backupKeys, type); 61 | } 62 | 63 | private List takeNodesBackups(List backupKeys, BackupType type) { 64 | // 1. take snapshots of the specified nodes if NODE_SNAPSHOT 65 | if (type == BackupType.NODE_SNAPSHOT) { 66 | takeNodesSnapshots(backupKeys); 67 | } 68 | 69 | // 2. copy backups in parallel 70 | return uploadNodesBackups(backupKeys, type); 71 | } 72 | 73 | private void takeNodesSnapshots(List backupKeys) { 74 | String[] keyspaces = clusterInfo.getKeyspaces().toArray(new String[0]); 75 | 76 | ExecutorService executor = Executors.newCachedThreadPool(); 77 | backupKeys.forEach( 78 | backupKey -> 79 | executor.submit( 80 | () -> { 81 | JmxManager eachJmx = getJmx(backupKey.getTargetIp(), config.getJmxPort()); 82 | eachJmx.clearSnapshot(null, keyspaces); 83 | eachJmx.takeSnapshot(backupKey.getSnapshotId(), keyspaces); 84 | })); 85 | awaitTermination(executor, "takeNodesSnapshots"); 86 | } 87 | 88 | private List uploadNodesBackups( 89 | List backupKeys, BackupType type) { 90 | List futures = new ArrayList<>(); 91 | 92 | // Parallel upload for now. It will be adjusted based on workload 93 | ExecutorService executor = Executors.newCachedThreadPool(); 94 | backupKeys.forEach( 95 | backupKey -> executor.submit(() -> futures.add(uploadNodeBackups(backupKey, type)))); 96 | awaitTermination(executor, "copyNodesBackups"); 97 | return futures; 98 | } 99 | 100 | @VisibleForTesting 101 | RemoteCommandContext uploadNodeBackups(BackupKey backupKey, BackupType type) { 102 | List arguments = 103 | Arrays.asList( 104 | CLUSTER_ID_OPTION + clusterInfo.getClusterId(), 105 | SNAPSHOT_ID_OPTION + backupKey.getSnapshotId(), 106 | TARGET_IP_OPTION + backupKey.getTargetIp(), 107 | DATA_DIR_OPTION + clusterInfo.getDataDir(), 108 | STORE_BASE_URI_OPTION + config.getStorageBaseUri(), 109 | STORE_TYPE_OPTION + config.getStorageType(), 110 | KEYSPACES_OPTION + String.join(",", clusterInfo.getKeyspaces()), 111 | BACKUP_TYPE_OPTION + type.get()); 112 | 113 | RemoteCommand command = 114 | RemoteCommand.newBuilder() 115 | .ip(backupKey.getTargetIp()) 116 | .username(config.getSshUser()) 117 | .privateKeyFile(Paths.get(config.getSshPrivateKeyPath())) 118 | .name(BACKUP_COMMAND) 119 | .command(config.getSlaveCommandPath() + "/" + BACKUP_COMMAND) 120 | .arguments(arguments) 121 | .build(); 122 | 123 | RemoteCommandFuture future = executor.execute(command); 124 | return new RemoteCommandContext(command, backupKey, future); 125 | } 126 | } 127 | --------------------------------------------------------------------------------