cassandra backup
Change-Id: I1641915700b91c87770f34d4a6786f3951b28f0b
diff --git a/README.rst b/README.rst
index 5baa12d..08ba87f 100644
--- a/README.rst
+++ b/README.rst
@@ -17,6 +17,88 @@
enabled: true
version: icehouse
+Backup client with ssh/rsync remote host
+
+.. code-block:: yaml
+
+ cassandra:
+ backup:
+ client:
+ enabled: true
+ full_backups_to_keep: 3
+ hours_before_full: 24
+ target:
+ host: cfg01
+
+ .. note:: full_backups_to_keep param states how many backup will be stored locally on cassandra client.
+ More options to relocate local backups can be done using salt-formula-backupninja.
+
+
+Backup client with local backup only
+
+.. code-block:: yaml
+
+ cassandra:
+ backup:
+ client:
+ enabled: true
+ full_backups_to_keep: 3
+ hours_before_full: 24
+
+ .. note:: full_backups_to_keep param states how many backup will be stored locally on cassandra client
+
+
+Backup server rsync
+
+.. code-block:: yaml
+
+ cassandra:
+ backup:
+ server:
+ enabled: true
+ hours_before_full: 24
+ full_backups_to_keep: 5
+ key:
+ cassandra_pub_key:
+ enabled: true
+ key: ssh_rsa
+
+Client restore from local backup:
+
+.. code-block:: yaml
+
+ cassandra:
+ backup:
+ client:
+ enabled: true
+ full_backups_to_keep: 3
+ hours_before_full: 24
+ target:
+ host: cfg01
+ restore_latest: 1
+ restore_from: local
+
+ .. note:: restore_latest param with a value of 1 means to restore db from the last full backup. 2 would mean to restore second latest full backup.
+
+Client restore from remote backup:
+
+.. code-block:: yaml
+
+ cassandra:
+ backup:
+ client:
+ enabled: true
+ full_backups_to_keep: 3
+ hours_before_full: 24
+ target:
+ host: cfg01
+ restore_latest: 1
+ restore_from: remote
+
+ .. note:: restore_latest param with a value of 1 means to restore db from the last full backup. 2 would mean to restore second latest full backup.
+
+
+
Read more
=========
diff --git a/cassandra/backup.sls b/cassandra/backup.sls
new file mode 100644
index 0000000..9b0ecf8
--- /dev/null
+++ b/cassandra/backup.sls
@@ -0,0 +1,162 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+
+{%- if backup.client is defined %}
+
+{%- if backup.client.enabled %}
+
+cassandra_backup_client_packages:
+ pkg.installed:
+ - names: {{ backup.pkgs }}
+
+cassandra_backup_runner_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-backup-runner.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-client-runner.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - pkg: cassandra_backup_client_packages
+
+cassandra_call_backup_runner_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-backup-runner-call.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-client-runner-call.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - pkg: cassandra_backup_client_packages
+
+cassandra_backup_dir:
+ file.directory:
+ - name: {{ backup.backup_dir }}/full
+ - user: root
+ - group: root
+ - makedirs: true
+
+cassandra_backup_runner_cron:
+ cron.present:
+ - name: /usr/local/bin/cassandra-backup-runner-call.sh
+ - user: root
+{%- if not backup.cron %}
+ - commented: True
+{%- endif %}
+ - minute: 0
+{%- if backup.client.hours_before_full is defined %}
+{%- if backup.client.hours_before_full <= 23 and backup.client.hours_before_full > 1 %}
+ - hour: '*/{{ backup.client.hours_before_full }}'
+{%- elif not backup.client.hours_before_full <= 1 %}
+ - hour: 2
+{%- endif %}
+{%- else %}
+ - hour: 2
+{%- endif %}
+ - require:
+ - file: cassandra_backup_runner_script
+ - file: cassandra_call_backup_runner_script
+
+
+{%- if backup.client.restore_latest is defined %}
+
+cassandra_backup_restore_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-backup-restore.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-client-restore.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - pkg: cassandra_backup_client_packages
+
+cassandra_backup_call_restore_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-backup-restore-call.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-client-restore-call.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - file: cassandra_backup_restore_script
+
+cassandra_run_restore:
+ cmd.run:
+ - name: /usr/local/bin/cassandra-backup-restore-call.sh
+ - unless: "[ -e {{ backup.backup_dir }}/dbrestored ]"
+ - require:
+ - file: cassandra_backup_call_restore_script
+
+{%- endif %}
+
+{%- endif %}
+
+{%- endif %}
+
+{%- if backup.server is defined %}
+
+{%- if backup.server.enabled %}
+
+cassandra_backup_server_packages:
+ pkg.installed:
+ - names: {{ backup.pkgs }}
+
+cassandra_user:
+ user.present:
+ - name: cassandra
+ - system: true
+ - home: {{ backup.backup_dir }}
+
+{{ backup.backup_dir }}/full:
+ file.directory:
+ - mode: 755
+ - user: cassandra
+ - group: cassandra
+ - makedirs: true
+ - require:
+ - user: cassandra_user
+ - pkg: cassandra_backup_server_packages
+
+{%- for key_name, key in backup.server.key.iteritems() %}
+
+{%- if key.get('enabled', False) %}
+
+cassandra_key_{{ key.key }}:
+ ssh_auth.present:
+ - user: cassandra
+ - name: {{ key.key }}
+ - require:
+ - file: {{ backup.backup_dir }}/full
+
+{%- endif %}
+
+{%- endfor %}
+
+cassandra_server_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-backup-runner.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-server-runner.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - pkg: cassandra_backup_server_packages
+
+cassandra_server_cron:
+ cron.present:
+ - name: /usr/local/bin/cassandra-backup-runner.sh
+ - user: cassandra
+{%- if not backup.cron %}
+ - commented: True
+{%- endif %}
+ - minute: 0
+ - hour: 2
+ - require:
+ - file: cassandra_server_script
+
+cassandra_server_call_restore_script:
+ file.managed:
+ - name: /usr/local/bin/cassandra-restore-call.sh
+ - source: salt://cassandra/files/backup/cassandra-backup-server-restore-call.sh
+ - template: jinja
+ - mode: 655
+ - require:
+ - pkg: cassandra_backup_server_packages
+
+{%- endif %}
+
+{%- endif %}
diff --git a/cassandra/files/backup/cassandra-backup-client-restore-call.sh b/cassandra/files/backup/cassandra-backup-client-restore-call.sh
new file mode 100644
index 0000000..292e707
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-client-restore-call.sh
@@ -0,0 +1,87 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+#!/bin/bash
+
+# Script is to locally prepare appropriate backup to restore from local or remote location and call client-restore script in for loop with every keyspace
+
+# Configuration
+# -------------
+ PROGNAME="getSnapshot"
+ PROGVER="1.0.1"
+ ASFCFG="/etc/cassandra"
+ DSECFG="/etc/dse/cassandra"
+ BACKUPDIR="{{ backup.backup_dir }}/full"
+ TMPDIR="$( pwd )/${PROGNAME}.tmp${RANDOM}"
+ CLITMPFILE="${TMPDIR}/cqlschema"
+ CASIP="127.0.0.1"
+ JMXIP="127.0.0.1"
+ HOSTNAME="$( hostname )"
+ SNAPCREATE=false
+ KEYSPFILE="cassandra.keyspace"
+ SNAPSFILE="cassandra.snapshot"
+ HOSTSFILE="cassandra.hostname"
+ DATESFILE="cassandra.snapdate"
+ APPENDTIMESTAMP="yes"
+ SCRIPTDIR="/usr/local/bin"
+ DBALREADYRESTORED="{{ backup.backup_dir }}/dbrestored"
+ LOGDIR="/var/log/backups"
+ LOGFILE="/var/log/backups/cassandra-restore.log"
+ SCPLOG="/var/log/backups/cassandra-restore-scp.log"
+
+
+if [ -e $DBALREADYRESTORED ]; then
+ error "Databases already restored. If you want to restore again delete $DBALREADYRESTORED file and run the script again."
+fi
+
+# Create backup directory.
+if [ ! -d "$LOGDIR" ] && [ ! -e "$LOGDIR" ]; then
+ mkdir -p "$LOGDIR"
+fi
+
+{%- if backup.client.restore_from == 'remote' %}
+
+echo "Adding ssh-key of remote host to known_hosts"
+ssh-keygen -R {{ backup.client.target.host }} 2>&1 | > $SCPLOG
+ssh-keyscan {{ backup.client.target.host }} >> ~/.ssh/known_hosts 2>&1 | >> $SCPLOG
+REMOTEBACKUPPATH=`ssh cassandra@{{ backup.client.target.host }} "/usr/local/bin/cassandra-restore-call.sh {{ backup.client.restore_latest }}"`
+
+#get files from remote and change variables to local restore dir
+
+LOCALRESTOREDIR=/var/backups/restoreCassandra
+FULLBACKUPDIR=$LOCALRESTOREDIR/full
+
+mkdir -p $LOCALRESTOREDIR
+rm -rf $LOCALRESTOREDIR/*
+
+echo "SCP getting full backup files"
+FULL=`basename $REMOTEBACKUPPATH`
+mkdir -p $FULLBACKUPDIR
+`scp -rp cassandra@{{ backup.client.target.host }}:$REMOTEBACKUPPATH $FULLBACKUPDIR/$FULL/ >> $SCPLOG 2>&1`
+
+# Check if the scp succeeded or failed
+if ! grep -q "No such file or directory" $SCPLOG; then
+ echo "SCP from remote host completed OK"
+else
+ echo "SCP from remote host FAILED"
+ exit 1
+fi
+
+echo "Restoring db from $FULLBACKUPDIR/$FULL/"
+for filename in $FULLBACKUPDIR/$FULL/*; do $SCRIPTDIR/cassandra-backup-restore.sh -f $filename; done
+RC=$?
+if [ $RC -eq 0 ]; then
+ nodetool repair
+ touch $DBALREADYRESTORED
+fi
+
+{%- else %}
+
+FULL=`find $BACKUPDIR -mindepth 1 -maxdepth 1 -type d -printf "%P\n" | sort -nr | head -{{ backup.client.restore_latest }} | tail -1`
+echo "Restoring db from $BACKUPDIR/$FULL/"
+for filename in $BACKUPDIR/$FULL/*; do $SCRIPTDIR/cassandra-backup-restore.sh -f $filename; done
+RC=$?
+if [ $RC -eq 0 ]; then
+ nodetool repair
+ touch $DBALREADYRESTORED
+fi
+
+{%- endif %}
diff --git a/cassandra/files/backup/cassandra-backup-client-restore.sh b/cassandra/files/backup/cassandra-backup-client-restore.sh
new file mode 100644
index 0000000..ac260d1
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-client-restore.sh
@@ -0,0 +1,317 @@
+#!/bin/bash
+# Script to restore Cassandra schema and keyspaces from snapshot one by one
+
+# Configuration
+# -------------
+ PROGNAME="putSnapshot"
+ PROGVER="1.0.1"
+ ASFCFG="/etc/cassandra"
+ DSECFG="/etc/dse/cassandra"
+ TEMPDIR="$( pwd )/${PROGNAME}.tmp${RANDOM}"
+ CLITMPFILE="${TEMPDIR}/cqlkeyspace"
+ KEYSPFILE="cassandra.keyspace"
+ SNAPSFILE="cassandra.snapshot"
+ HOSTSFILE="cassandra.hostname"
+ DATESFILE="cassandra.snapdate"
+
+# Functions
+# ---------
+ function check_dependencies() {
+ # Function to iterate through a list of required executables to ensure
+ # they are installed and executable by the current user.
+ DEPS="awk cat cqlsh cut echo find getopt grep hostname "
+ DEPS+="mkdir rm sed sstableloader tar tr "
+ for bin in $DEPS; do
+ $( which $bin >/dev/null 2>&1 ) || NOTFOUND+="$bin "
+ done
+
+ if [ ! -z "$NOTFOUND" ]; then
+ printf "Error finding required executables: ${NOTFOUND}\n" >&2
+ exit 1
+ fi
+ }
+
+ function parse_yaml() {
+ # Basic (as in imperfect) parsing of a given YAML file. Parameters
+ # are stored as environment variables.
+ local prefix=$2
+ local s
+ local w
+ local fs
+ s='[[:space:]]*'
+ w='[a-zA-Z0-9_]*'
+ fs="$(echo @|tr @ '\034')"
+ sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
+ -e "s|^\($s\)\($w\)$s[:-]$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" |
+ awk -F"$fs" '{
+ indent = length($1)/2;
+ if (length($2) == 0) { conj[indent]="+";} else {conj[indent]="";}
+ vname[indent] = $2;
+ for (i in vname) {if (i > indent) {delete vname[i]}}
+ if (length($3) > 0) {
+ vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
+ printf("%s%s%s%s=(\"%s\")\n", "'"$prefix"'",vn, $2, conj[indent-1],$3);
+ }
+ }' | sed 's/_=/+=/g'
+ }
+
+ function usage() {
+ printf "Usage: $0 -h\n"
+ printf " $0 -f <snapshot file> [-n <node address>] [-k <new ks name>] [-d <new dc name>] [-r <new rf>] [-y <cassandra.yaml file>]\n"
+ printf " -h,--help Print usage and exit\n"
+ printf " -v,--version Print version information and exit\n"
+ printf " -f,--file <snapshot file> REQUIRED: The snapshot file name (created using the\n"
+ printf " getSnapshot utility\n"
+ printf " -n,--node <node address> Destination Cassandra node IP (defaults to the local\n"
+ printf " Cassandra IP if run on a Cassandra node, otherwise\n"
+ printf " required in order to connect to Cassandra. Will take\n"
+ printf " precedence if provided and run on a Cassandra node\n"
+ printf " -k,--keyspace <new ks name> Override the destination keyspace name (defaults to\n"
+ printf " the source keyspace name)\n"
+ printf " -d,--datacenter <new dc name> Override the destination datacenter name (defaults\n"
+ printf " to the sourcen datacenter name)\n"
+ printf " -r,--replication <new rf> Override the destination replication factor (defaults\n"
+ printf " to source replication factor)\n"
+ printf " -y,--yaml <cassandra.yaml file> Alternate cassandra.yaml file\n"
+ exit 0
+ }
+
+ function version() {
+ printf "$PROGNAME version $PROGVER\n"
+ printf "Cassandra snapshot loader utility\n\n"
+ printf "Copyright 2016 Applied Infrastructure, LLC\n\n"
+ printf "Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+ printf "you may not use this file except in compliance with the License.\n"
+ printf "You may obtain a copy of the License at\n\n"
+ printf " http://www.apache.org/licenses/LICENSE-2.0\n\n"
+ printf "Unless required by applicable law or agreed to in writing, software\n"
+ printf "distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+ printf "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+ printf "See the License for the specific language governing permissions and\n"
+ printf "limitations under the License.\n"
+ exit 0
+ }
+
+# Validate Input/Environment
+# --------------------------
+ # Great sample getopt implementation by Cosimo Streppone
+ # https://gist.github.com/cosimo/3760587#file-parse-options-sh
+ SHORT='hvd:f:n:k:r:y:'
+ LONG='help,version,datacenter:,file:,node:,keyspace:,replication:,yaml:'
+ OPTS=$( getopt -o $SHORT --long $LONG -n "$0" -- "$@" )
+
+ if [ $? -gt 0 ]; then
+ # Exit early if argument parsing failed
+ printf "Error parsing command arguments\n" >&2
+ exit 1
+ fi
+
+ eval set -- "$OPTS"
+ while true; do
+ case "$1" in
+ -h|--help) usage;;
+ -v|--version) version;;
+ -f|--file) SNAPPKG="$2"; shift 2;;
+ -n|--node) IPINPUT="$2"; shift 2;;
+ -k|--keyspace) INPKEYSPACE="$2"; shift 2;;
+ -d|--datacenter) DATACENTER="$2"; shift 2;;
+ -r|--replication) RFACTOR="$2"; shift 2;;
+ -y|--yaml) INPYAML="$2"; shift 2;;
+ --) shift; break;;
+ *) printf "Error processing command arguments\n" >&2; exit 1;;
+ esac
+ done
+
+ # Verify required binaries at this point
+ check_dependencies
+
+ # Only a snapshot file is required
+ if [ ! -r "$SNAPPKG" ]; then
+ printf "You must provide the location of a snapshot package\n"
+ exit 1
+ fi
+
+ # Need write access to local directory to create dump file
+ if [ ! -w $( pwd ) ]; then
+ printf "You must have write access to the current directory $( pwd )\n"
+ exit 1
+ fi
+
+ # Attempt to locate a local Cassandra install and YAML file
+ YAMLLIST="${INPYAML:-$( find "$DSECFG" "$ASFCFG" -type f -name cassandra.yaml 2>/dev/null ) }"
+
+ for yaml in $YAMLLIST; do
+ if [ -r "$yaml" ]; then
+ # Cassandra YAML found - load it (assume a local Cassandra)
+ eval $( parse_yaml "$yaml" )
+ YAMLFILE="$yaml"
+
+ if [ -z $listen_address ]; then
+ CASIP=$( hostname )
+ elif [ "$listen_address" == "0.0.0.0" ]; then
+ CASIP=127.0.0.1
+ else
+ CASIP=$listen_address
+ fi
+ break
+ fi
+ done
+
+ # Determine IP to use to connect to Cassandra. If an IP is provided via
+ # -n,--node argument, prefer it. If not, and a Cassandra IP cannot be
+ # discovered via YAML (i.e. this is not a Cassandra node), then return
+ # an error and exit.
+ if [ ! -z $IPINPUT ]; then
+ CASIP="$IPINPUT"
+ elif [ -z $CASIP ]; then
+ printf "Cassandra IP not provided and not discoverable locally\n"
+ exit 1
+ fi
+
+ # Check if a new keyspace name is provided, and validate input
+ if [ -z $INPKEYSPACE ]; then
+ printf "New keyspace name not provided, using original keyspace name\n"
+ elif [[ ! "$INPKEYSPACE" =~ ^[_a-zA-Z0-9]*$ ]]; then
+ printf "Cassandra keyspace names can only contain alpha-numerics and underscore (_)\n"
+ exit 1
+ else
+ KEYSPACE="$INPKEYSPACE"
+ printf "New keyspace name $KEYSPACE to be used\n"
+ fi
+
+ # Let the user know which datacenter and replication factor values being used
+ if [ -z $DATACENTER ]; then
+ printf "New datacenter name not provided, using original datacenter name\n"
+ fi
+ if [ -z $RFACTOR ]; then
+ printf "New replication factor not provided, using original replication factor\n"
+ fi
+
+# Preparation
+# -----------
+ # Remove local temp directory
+ [ "$TEMPDIR" != "/" ] && rm -rf "$TEMPDIR"
+
+ # Verify/Extract Snapshot Package
+ tar -tvf "$SNAPPKG" 2>&1 | grep "$KEYSPFILE" 2>&1 >/dev/null
+ RC=$?
+
+ if [ $RC -gt 0 ]; then
+ printf "\nSnapshot package $SNAPPKG appears invalid or corrupt\n"
+ exit 1
+ else
+ # Create temporary working directory. Yes, deliberately avoiding mktemp
+ if [ ! -d "$TEMPDIR" ] && [ ! -e "$TEMPDIR" ]; then
+ mkdir -p "$TEMPDIR"
+ else
+ printf "\nError creating temporary directory $TEMPDIR"
+ exit 1
+ fi
+
+ # Extract snapshot package
+ tar -xf "$SNAPPKG" --directory "$TEMPDIR"
+ fi
+
+# Prepare Snapshot
+# ----------------
+ FILEKSNAME=$( cat "${TEMPDIR}/${KEYSPFILE}" )
+ FILEHOSTNAME=$( cat "${TEMPDIR}/${HOSTSFILE}" )
+ FILESNAPDATE=$( cat "${TEMPDIR}/${DATESFILE}" )
+ SCHEMAFILE=$( ls "${TEMPDIR}"/schema-${FILEKSNAME}-*.cdl 2>/dev/null )
+
+ # Place schema on single line and extract replication setup
+ FILEREPL=$( sed ':a;N;$!ba;s/\n/ /g' "$SCHEMAFILE" | \
+ grep -Eo 'replication ?= ?{[^}]*}' | \
+ tr -dc "[:alnum:],:=" | \
+ cut -d, -f 2 )
+ FILEDCNAME=$( cut -d: -f 1 <<< ${FILEREPL} )
+ FILERFACTOR=$( cut -d: -f 2 <<< ${FILEREPL} )
+
+ if [ ! -z $KEYSPACE ]; then
+ # Update keyspace names in snapshot
+ sed -i 's/'$FILEKSNAME'/'$KEYSPACE'/g' "$SCHEMAFILE"
+ mv "${TEMPDIR}/${FILEKSNAME}" "${TEMPDIR}/${KEYSPACE}"
+ for dbfile in $( find "${TEMPDIR}/${KEYSPACE}" -type f ); do
+ if grep "${FILEKSNAME}" <<< "${dbfile}" >/dev/null; then
+ mv "$dbfile" "${dbfile//$FILEKSNAME/$KEYSPACE}"
+ fi
+ done
+ NEWKSNAME=$KEYSPACE
+ else
+ NEWKSNAME=$FILEKSNAME
+ fi
+
+ if [ ! -z $DATACENTER ]; then
+ # Update datacenter name in snapshot
+ sed -i 's/'$FILEDCNAME'/'$DATACENTER'/g' "$SCHEMAFILE"
+ DCNAME=$DATACENTER
+ else
+ DCNAME=$FILEDCNAME
+ fi
+
+ if [ ! -z $RFACTOR ]; then
+ # Update replication factor in snapshot
+ sed -i 's/\(\d039'$DCNAME'\d039[^:]*:[^\d039]*\d039\)'$FILERFACTOR'\(\d039\)/\1'$RFACTOR'\2/' "$SCHEMAFILE"
+ NEWRFACTOR="$RFACTOR"
+ else
+ NEWRFACTOR="$FILERFACTOR"
+ fi
+
+# Load Snapshot
+# -------------
+ # Check for keyspace name conflict
+ while true; do
+ echo "describe keyspace $NEWKSNAME;" > "$CLITMPFILE"
+
+ # Use CQL version in environment, if available
+ [ -z $CQLVER ] && CQLSHVER="" || CQLSHVER="--cqlversion=$CQLVER"
+ OUTPUT=$( cqlsh -f $CLITMPFILE $CQLSHVER $CASIP 2>&1 )
+ RC=$?
+
+ if [ $RC -eq 0 ] && grep -qi 'CREATE KEYSPACE '$NEWKSNAME' ' <<< $OUTPUT; then
+ printf "ERROR: Keyspace name $NEWKSNAME conflicts with existing keyspace name\n"
+ [ "$TEMPDIR" != "/" ] && rm -rf "$TEMPDIR"
+ exit 1
+ elif grep -qi 'version .\?[.0-9]*.\? is not' <<< $OUTPUT; then
+ ERRORCQLVER=$( grep -o 'version .\?[0-9.]*.\? is not' <<< $OUTPUT | tr -dc ' 0-9.' )
+ SUPPORTED=($( grep -Eo 'upported( versions)?: .*[ 0-9.,]*' <<< $OUTPUT | tr -dc ' 0-9.' ))
+{% raw %}
+ CQLVER=${SUPPORTED[$((${#SUPPORTED[@]}-1))]}
+{% endraw %}
+ printf "Default CQL version $ERRORCQLVER not supported by Cassandra at ${CASIP}.\n"
+ printf "Reported versions are ${SUPPORTED[@]}. Attempting with ${CQLVER}.\n"
+ continue
+ elif grep -qi 'connection error\|not connect' <<< $OUTPUT; then
+ printf "ERROR: Unable to connect to Cassandra at ${CASIP}.\n"
+ [ "$TEMPDIR" != "/" ] && rm -rf "$TEMPDIR"
+ exit 1
+ elif [ $RC -eq 1 ]; then
+ printf "ERROR: Error executing cqlsh command:\n ${OUTPUT}\n"
+ [ "$TEMPDIR" != "/" ] && rm -rf "$TEMPDIR"
+ exit 1
+ else
+ printf "Performing Import:\n"
+ printf " Original cassandra host: $FILEHOSTNAME\n"
+ printf " Original keyspace name: $FILEKSNAME\n"
+ printf " Original snapshot date: $FILESNAPDATE\n\n"
+ printf " Importing schema into keyspace $NEWKSNAME\n"
+ printf " Using datacenter $DCNAME and replication factor $NEWRFACTOR\n"
+
+ # Wait - give the user a chance to panic and CTRL-C out
+ sleep 5
+
+ # Create schema for the new keyspace
+ cqlsh -f "$SCHEMAFILE" $CQLSHVER $CASIP
+
+ printf " Loading snapshot into keyspace $NEWKSNAME\n"
+ for columnfamily in `ls "${TEMPDIR}/${NEWKSNAME}"`; do
+ sstableloader -d $CASIP "${TEMPDIR}/${NEWKSNAME}/${columnfamily}"
+ done
+
+ printf "\n\nImport operation complete - check output for errors\n"
+ [ "$TEMPDIR" != "/" ] && rm -rf "$TEMPDIR"
+ exit 0
+ fi
+ done
+
+# Fin.
diff --git a/cassandra/files/backup/cassandra-backup-client-runner-call.sh b/cassandra/files/backup/cassandra-backup-client-runner-call.sh
new file mode 100644
index 0000000..a848f1c
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-client-runner-call.sh
@@ -0,0 +1,105 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+#!/bin/bash
+# Script to call cassandra-backup-runner.sh in for loop to backup all keyspaces.
+# This script is also able to rsync backed up data to remote host and perform clean up on historical backups
+
+# Configuration
+# -------------
+ PROGNAME="getSnapshot"
+ PROGVER="1.0.1"
+ ASFCFG="/etc/cassandra"
+ DSECFG="/etc/dse/cassandra"
+ BACKUPDIR="{{ backup.backup_dir }}/full"
+ TMPDIR="$( pwd )/${PROGNAME}.tmp${RANDOM}"
+ CLITMPFILE="${TMPDIR}/cqlschema"
+ CASIP="127.0.0.1"
+ JMXIP="127.0.0.1"
+ HOSTNAME="$( hostname )"
+ SNAPCREATE=false
+ KEYSPFILE="cassandra.keyspace"
+ SNAPSFILE="cassandra.snapshot"
+ HOSTSFILE="cassandra.hostname"
+ DATESFILE="cassandra.snapdate"
+ APPENDTIMESTAMP="yes"
+ SCRIPTDIR="/usr/local/bin"
+ KEEP={{ backup.client.full_backups_to_keep }}
+ HOURSFULLBACKUPLIFE={{ backup.client.hours_before_full }} # Lifetime of the latest full backup in seconds
+ RSYNCLOG=/var/log/backups/cassandra-rsync.log
+
+
+ if [ $HOURSFULLBACKUPLIFE -gt 24 ]; then
+ FULLBACKUPLIFE=$(( 24 * 60 * 60 ))
+ else
+ FULLBACKUPLIFE=$(( $HOURSFULLBACKUPLIFE * 60 * 60 ))
+ fi
+
+# Functions
+# ---------
+ function check_dependencies() {
+ # Function to iterate through a list of required executables to ensure
+ # they are installed and executable by the current user.
+ DEPS="awk basename cp cqlsh date dirname echo find "
+ DEPS+="getopt grep hostname mkdir rm sed tail tar "
+ for bin in $DEPS; do
+ $( which $bin >/dev/null 2>&1 ) || NOTFOUND+="$bin "
+ done
+
+ if [ ! -z "$NOTFOUND" ]; then
+ printf "Error finding required executables: ${NOTFOUND}\n" >&2
+ exit 1
+ fi
+ }
+
+
+ # Need write access to local directory to create dump file
+ if [ ! -w $( pwd ) ]; then
+ printf "You must have write access to the current directory $( pwd )\n"
+ exit 1
+ fi
+
+ if [ ! -d "$RSYNCLOG" ] && [ ! -e "$RSYNCLOG" ]; then
+ mkdir -p "$RSYNCLOG"
+ fi
+
+ # Get local Cassandra listen address. Should be loaded via the selected
+ # cassandra.yaml file above.
+ if [ -z $listen_address ]; then
+ CASIP=$( hostname )
+ elif [ "$listen_address" == "0.0.0.0" ]; then
+ CASIP=127.0.0.1
+ else
+ CASIP=$listen_address
+ fi
+
+ TIMESTAMP=$( date +"%Y%m%d%H%M%S" )
+ DATESTRING=$( date )
+
+ cqlsh $CASIP -e "DESC KEYSPACES" |perl -pe 's/\e([^\[\]]|\[.*?[a-zA-Z]|\].*?\a)//g' | sed '/^$/d' > Keyspace_name_schema.cql
+ sed 's/\"//g' Keyspace_name_schema.cql > KEYSPACES_LIST
+ for i in `cat KEYSPACES_LIST`; do $SCRIPTDIR/cassandra-backup-runner.sh -k $i -t $TIMESTAMP -d $DATESTRING; done
+
+# rsync just the new or modified backup files
+# ---------
+
+ {%- if backup.client.target is defined %}
+ echo "Adding ssh-key of remote host to known_hosts"
+ ssh-keygen -R {{ backup.client.target.host }} 2>&1 | > $RSYNCLOG
+ ssh-keyscan {{ backup.client.target.host }} >> ~/.ssh/known_hosts 2>&1 | >> $RSYNCLOG
+ echo "Rsyncing files to remote host"
+ /usr/bin/rsync -rhtPv --rsync-path=rsync --progress $BACKUPDIR/* -e ssh cassandra@{{ backup.client.target.host }}:$BACKUPDIR >> $RSYNCLOG
+
+ # Check if the rsync succeeded or failed
+ if [ -s $RSYNCLOG ] && ! grep -q "rsync error: " $RSYNCLOG; then
+ echo "Rsync to remote host completed OK"
+ else
+ echo "Rsync to remote host FAILED"
+ exit 1
+ fi
+ {%- endif %}
+
+# Cleanup
+# ---------
+ echo "Cleanup. Keeping only $KEEP full backups"
+ AGE=$(($FULLBACKUPLIFE * $KEEP / 60))
+ find $BACKUPDIR -maxdepth 1 -type d -mmin +$AGE -execdir echo "removing: "$BACKUPDIR/{} \; -execdir rm -rf $BACKUPDIR/{} \;
+
diff --git a/cassandra/files/backup/cassandra-backup-client-runner.sh b/cassandra/files/backup/cassandra-backup-client-runner.sh
new file mode 100644
index 0000000..50d0ed0
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-client-runner.sh
@@ -0,0 +1,288 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+#!/bin/bash
+# Script to backup Cassandra schema and create snapshot of keyspaces
+
+# Configuration
+# -------------
+ PROGNAME="getSnapshot"
+ PROGVER="1.0.1"
+ ASFCFG="/etc/cassandra"
+ DSECFG="/etc/dse/cassandra"
+ BACKUPDIR="{{ backup.backup_dir }}/full"
+ TMPDIR="$( pwd )/${PROGNAME}.tmp${RANDOM}"
+ CLITMPFILE="${TMPDIR}/cqlschema"
+ CASIP="127.0.0.1"
+ JMXIP="127.0.0.1"
+ HOSTNAME="$( hostname )"
+ SNAPCREATE=false
+ KEYSPFILE="cassandra.keyspace"
+ SNAPSFILE="cassandra.snapshot"
+ HOSTSFILE="cassandra.hostname"
+ DATESFILE="cassandra.snapdate"
+ APPENDTIMESTAMP="yes"
+
+# Functions
+# ---------
+ function check_dependencies() {
+ # Function to iterate through a list of required executables to ensure
+ # they are installed and executable by the current user.
+ DEPS="awk basename cp cqlsh date dirname echo find "
+ DEPS+="getopt grep hostname mkdir rm sed tail tar "
+ for bin in $DEPS; do
+ $( which $bin >/dev/null 2>&1 ) || NOTFOUND+="$bin "
+ done
+
+ if [ ! -z "$NOTFOUND" ]; then
+ printf "Error finding required executables: ${NOTFOUND}\n" >&2
+ exit 1
+ fi
+ }
+
+ function parse_yaml() {
+ # Basic (as in imperfect) parsing of a given YAML file. Parameters
+ # are stored as environment variables.
+ local prefix=$2
+ local s
+ local w
+ local fs
+ s='[[:space:]]*'
+ w='[a-zA-Z0-9_]*'
+ fs="$(echo @|tr @ '\034')"
+ sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
+ -e "s|^\($s\)\($w\)$s[:-]$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" |
+ awk -F"$fs" '{
+ indent = length($1)/2;
+ if (length($2) == 0) { conj[indent]="+";} else {conj[indent]="";}
+ vname[indent] = $2;
+ for (i in vname) {if (i > indent) {delete vname[i]}}
+ if (length($3) > 0) {
+ vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
+ printf("%s%s%s%s=(\"%s\")\n", "'"$prefix"'",vn, $2, conj[indent-1],$3);
+ }
+ }' | sed 's/_=/+=/g'
+ }
+
+ function usage() {
+ printf "Usage: $0 -h\n"
+ printf " $0 -k <keyspace name> [-s <snapshot name>] [-y <cassandra.yaml file>] [--no-timestamp]\n"
+ printf " -h,--help Print usage and exit\n"
+ printf " -v,--version Print version information and exit\n"
+ printf " -k,--keyspace <keyspace name> REQUIRED: The name of the keyspace to snapshot\n"
+ printf " -s,--snapshot <snapshot name> The name of an existing snapshot to package\n"
+ printf " -y,--yaml <cassandra.yaml file> Alternate cassandra.yaml file\n"
+ printf " -t,--timestamp timestamp\n"
+ printf " -d,--datestring datestring\n"
+ printf " --no-timestamp Don't include a timestamp in the resulting filename\n"
+ exit 0
+ }
+
+ function version() {
+ printf "$PROGNAME version $PROGVER\n"
+ printf "Cassandra snapshot packaging utility\n\n"
+ printf "Copyright 2016 Applied Infrastructure, LLC\n\n"
+ printf "Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+ printf "you may not use this file except in compliance with the License.\n"
+ printf "You may obtain a copy of the License at\n\n"
+ printf " http://www.apache.org/licenses/LICENSE-2.0\n\n"
+ printf "Unless required by applicable law or agreed to in writing, software\n"
+ printf "distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+ printf "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+ printf "See the License for the specific language governing permissions and\n"
+ printf "limitations under the License.\n"
+ exit 0
+ }
+
+# Validate Input/Environment
+# --------------------------
+ # Great sample getopt implementation by Cosimo Streppone
+ # https://gist.github.com/cosimo/3760587#file-parse-options-sh
+ SHORT='hvk:s:y:t:d:'
+ LONG='help,version,keyspace:,snapshot:,yaml:,timestamp:,datestring:,no-timestamp'
+ OPTS=$( getopt -o $SHORT --long $LONG -n "$0" -- "$@" )
+
+ if [ $? -gt 0 ]; then
+ # Exit early if argument parsing failed
+ printf "Error parsing command arguments\n" >&2
+ exit 1
+ fi
+
+ eval set -- "$OPTS"
+ while true; do
+ case "$1" in
+ -h|--help) usage;;
+ -v|--version) version;;
+ -k|--keyspace) KEYSPACE="$2"; shift 2;;
+ -s|--snapshot) SNAPSHOT="$2"; shift 2;;
+ -y|--yaml) INPYAML="$2"; shift 2;;
+ -t|--timestamp) TIMESTAMP="$2"; shift 2;;
+ -d|--datestring) DATESTRING="$2"; shift 2;;
+ --no-timestamp) APPENDTIMESTAMP="no"; shift;;
+ --) shift; break;;
+ *) printf "Error processing command arguments\n" >&2; exit 1;;
+ esac
+ done
+
+ # Verify required binaries at this point
+ check_dependencies
+
+ # Only KEYSPACE is absolutely required
+ if [ "$KEYSPACE" == "" ]; then
+ printf "You must provide a keyspace name\n"
+ exit 1
+ fi
+
+ # Need write access to local directory to create dump file
+ if [ ! -w $( pwd ) ]; then
+ printf "You must have write access to the current directory $( pwd )\n"
+ exit 1
+ fi
+
+ # Attempt to locate data directory and keyspace files
+ YAMLLIST="${INPYAML:-$( find "$DSECFG" "$ASFCFG" -type f -name cassandra.yaml 2>/dev/null ) }"
+
+ for yaml in $YAMLLIST; do
+ if [ -r "$yaml" ]; then
+ eval $( parse_yaml "$yaml" )
+ # Search each data directory in the YAML
+ for directory in ${data_file_directories_[@]}; do
+ if [ -d "$directory/$KEYSPACE" ]; then
+ # Use the YAML that references the keyspace
+ DATADIR="$directory"
+ YAMLFILE="$yaml"
+ break
+ fi
+ # Used only when the keyspace can't be found
+ TESTED="$TESTED $directory"
+ done
+ fi
+ done
+
+ if [ -z "$TESTED" ] && [ -z "$DATADIR" ]; then
+ printf "No data directories, or no cassandra.yaml file found\n" >&2
+ exit 1
+ elif [ -z "$DATADIR" ] || [ -z "$YAMLFILE" ]; then
+ printf "Keyspace data directory could not be found in:\n"
+ for dir in $TESTED; do
+ printf " $dir/$KEYSPACE\n"
+ done
+ exit 1
+ fi
+
+# Preparation
+# -----------
+ eval $( parse_yaml "$YAMLFILE" )
+
+ # Create temporary working directory. Yes, deliberately avoiding mktemp
+ if [ ! -d "$TMPDIR" ] && [ ! -e "$TMPDIR" ]; then
+ mkdir -p "$TMPDIR"
+ else
+ printf "Error creating temporary directory $TMPDIR"
+ exit 1
+ fi
+
+ # Create backup directory.
+ if [ ! -d "$BACKUPDIR" ] && [ ! -e "$BACKUPDIR" ]; then
+ mkdir -p "$BACKUPDIR"
+ fi
+
+ # Write temp command file for Cassandra CLI
+ printf "desc keyspace $KEYSPACE;\n" > $CLITMPFILE
+
+ # Get local Cassandra listen address. Should be loaded via the selected
+ # cassandra.yaml file above.
+ if [ -z $listen_address ]; then
+ CASIP=$( hostname )
+ elif [ "$listen_address" == "0.0.0.0" ]; then
+ CASIP=127.0.0.1
+ else
+ CASIP=$listen_address
+ fi
+
+ # Get local Cassandra JMX address
+ # Cheating for now - this is *usually* right, but may be set to a real IP
+ # in cassandra-env.sh in some environments.
+ JMXIP=127.0.0.1
+
+# Create/Pull Snapshot
+# --------------------
+ if [ -z "$SNAPSHOT" ]; then
+ # Create a new snapshot if a snapshot name was not provided
+ printf "Creating new snapshot $KEYSPACE\n"
+
+ OUTPUT=$( nodetool -h $JMXIP snapshot $KEYSPACE 2>&1 )
+ SNAPSHOT=$( grep -Eo '[0-9]{10}[0-9]+' <<< "$OUTPUT" | tail -1 )
+
+ # Check if the snapshot process failed
+ if [ -z "$SNAPSHOT" ]; then
+ printf "Problem creating snapshot for keyspace $KEYSPACE\n\n"
+ printf "$OUTPUT\n"
+ [ "$TMPDIR" != "/" ] && rm -rf "$TMPDIR"
+ exit 1
+ fi
+ else
+ # If a snapshot name was provided, check if it exists
+ SEARCH=$( find "${DATADIR}/${KEYSPACE}" -type d -name "${SNAPSHOT}" )
+
+ if [ -z "$SEARCH" ]; then
+ printf "No snapshots found with name ${SNAPSHOT}\n"
+ [ "$TMPDIR" != "/" ] && rm -rf "$TMPDIR"
+ exit 1
+ else
+ printf "Using provided snapshot name ${SNAPSHOT}\n"
+ fi
+ fi
+
+ # Pull new/existing snapshot
+ SNAPDIR="snapshots/$SNAPSHOT"
+ SCHEMA="schema-$KEYSPACE-$TIMESTAMP.cdl"
+
+ for dir in $( find "$DATADIR" -regex ".*/$SNAPDIR/[^\.]*.db" ); do
+ NEWDIR=$( sed "s|${DATADIR}||" <<< $( dirname $dir ) | \
+ awk -F / '{print "/"$2"/"$3}' )
+
+ mkdir -p "$TMPDIR/$NEWDIR"
+ cp $dir "$TMPDIR/$NEWDIR/"
+ done
+
+# Backup the schema and create tar archive
+# ----------------------------------------
+ printf "$KEYSPACE" > "$TMPDIR/$KEYSPFILE"
+ printf "$SNAPSHOT" > "$TMPDIR/$SNAPSFILE"
+ printf "$HOSTNAME" > "$TMPDIR/$HOSTSFILE"
+ printf "$DATESTRING" > "$TMPDIR/$DATESFILE"
+ cqlsh $CASIP -k $KEYSPACE -f $CLITMPFILE | tail -n +2 > "$TMPDIR/$SCHEMA"
+ RC=$?
+
+ mkdir -p "$BACKUPDIR/$TIMESTAMP"
+
+ if [ $? -gt 0 ] && [ ! -s "$TMPDIR/$SCHEMA" ]; then
+ printf "Schema backup failed\n"
+ [ "$TMPDIR" != "/" ] && rm -rf "$TMPDIR"
+ exit 1
+ else
+ # Include the timestamp in the filename or not (i.e. --no-timestamp)
+ [ "$APPENDTIMESTAMP" == "no" ] && FILENAME="$BACKUPDIR/$TIMESTAMP/$KEYSPACE.tar.gz" \
+ || FILENAME="$BACKUPDIR/$TIMESTAMP/$KEYSPACE-$TIMESTAMP.tar.gz"
+
+ tar --directory "$TMPDIR" \
+ -zcvf $FILENAME \
+ $KEYSPACE \
+ $SCHEMA \
+ $KEYSPFILE \
+ $SNAPSFILE \
+ $HOSTSFILE \
+ $DATESFILE >/dev/null 2>&1
+ RC=$?
+
+ if [ $RC -gt 0 ]; then
+ printf "Error generating tar archive. Because keyspace $KEYSPACE probably due to not containing any .db files.\n"
+ [ "$TMPDIR" != "/" ] && rm -rf "$TMPDIR"
+ exit 1
+ else
+ printf "Successfully created snapshot package $KEYSPACE\n"
+ [ "$TMPDIR" != "/" ] && rm -rf "$TMPDIR"
+ exit 0
+ fi
+ fi
+
+# Fin.
diff --git a/cassandra/files/backup/cassandra-backup-server-restore-call.sh b/cassandra/files/backup/cassandra-backup-server-restore-call.sh
new file mode 100644
index 0000000..1afc60d
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-server-restore-call.sh
@@ -0,0 +1,20 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+#!/bin/sh
+
+# This script is called remotely by Cassandra 'client role' node and returns appropriate backup that client will restore
+
+if [ $# -eq 0 ]; then
+ echo "No arguments provided"
+ exit 1
+fi
+
+# if arg is not an integer
+case $1 in
+ ''|*[!0-9]*) echo "Argument must be integer"; exit 1 ;;
+ *) ;;
+esac
+
+BACKUPDIR="{{ backup.backup_dir }}/full/"
+FULL=`find $BACKUPDIR -mindepth 1 -maxdepth 1 -type d -printf "%P\n" | sort -nr | head -$1 | tail -1`
+
+echo "$BACKUPDIR/$FULL/"
diff --git a/cassandra/files/backup/cassandra-backup-server-runner.sh b/cassandra/files/backup/cassandra-backup-server-runner.sh
new file mode 100644
index 0000000..63405c0
--- /dev/null
+++ b/cassandra/files/backup/cassandra-backup-server-runner.sh
@@ -0,0 +1,21 @@
+{%- from "cassandra/map.jinja" import backup with context %}
+#!/bin/bash
+
+# Script to erase old backups on Cassandra 'server role' node.
+# ---------
+
+ BACKUPDIR="{{ backup.backup_dir }}/full"
+ KEEP={{ backup.server.full_backups_to_keep }}
+ HOURSFULLBACKUPLIFE={{ backup.server.hours_before_full }} # Lifetime of the latest full backup in seconds
+
+ if [ $HOURSFULLBACKUPLIFE -gt 24 ]; then
+ FULLBACKUPLIFE=$(( 24 * 60 * 60 ))
+ else
+ FULLBACKUPLIFE=$(( $HOURSFULLBACKUPLIFE * 60 * 60 ))
+ fi
+
+# Cleanup
+# ---------
+ echo "Cleanup. Keeping only $KEEP full backups"
+ AGE=$(($FULLBACKUPLIFE * $KEEP / 60))
+ find $BACKUPDIR -maxdepth 1 -type d -mmin +$AGE -execdir echo "removing: "$BACKUPDIR/{} \; -execdir rm -rf $BACKUPDIR/{} \;
diff --git a/cassandra/init.sls b/cassandra/init.sls
index 072d389..58fd270 100644
--- a/cassandra/init.sls
+++ b/cassandra/init.sls
@@ -3,4 +3,7 @@
{%- if pillar.cassandra.server is defined %}
- cassandra.server
{%- endif %}
+{%- if pillar.cassandra.backup is defined %}
+- cassandra.backup
+{%- endif %}
{%- endif %}
diff --git a/cassandra/map.jinja b/cassandra/map.jinja
index 51d7ce4..a2405b0 100644
--- a/cassandra/map.jinja
+++ b/cassandra/map.jinja
@@ -16,6 +16,21 @@
base: /etc/cassandra
services:
- cassandra
+
+backup:
+ Debian:
+ pkgs:
+ - rsync
+ backup_dir: '/var/backups/cassandra'
+ cron: True
+ RedHat:
+ pkgs:
+ - rsync
+ backup_dir: '/var/backups/cassandra'
+ cron: True
+
{%- endload %}
-{%- set server = salt['grains.filter_by'](base_defaults, merge=salt['pillar.get']('cassandra:server')) %}
\ No newline at end of file
+{%- set server = salt['grains.filter_by'](base_defaults, merge=salt['pillar.get']('cassandra:server')) %}
+
+{% set backup = salt['grains.filter_by'](base_defaults['backup'], merge=salt['pillar.get']('cassandra:backup', {}), base='backup') %}
diff --git a/tests/pillar/backup_client.sls b/tests/pillar/backup_client.sls
new file mode 100644
index 0000000..c1b423f
--- /dev/null
+++ b/tests/pillar/backup_client.sls
@@ -0,0 +1,10 @@
+cassandra:
+ backup:
+ client:
+ enabled: true
+ full_backups_to_keep: 3
+ hours_before_full: 24
+ target:
+ host: cfg01
+ restore_latest: 1
+ restore_from: local
\ No newline at end of file
diff --git a/tests/pillar/backup_server.sls b/tests/pillar/backup_server.sls
new file mode 100644
index 0000000..2a3c5f6
--- /dev/null
+++ b/tests/pillar/backup_server.sls
@@ -0,0 +1,10 @@
+cassandra:
+ backup:
+ server:
+ enabled: true
+ hours_before_full: 24
+ full_backups_to_keep: 5
+ key:
+ cassandra_pub_key:
+ enabled: true
+ key: ssh_rsa
\ No newline at end of file