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