-
Notifications
You must be signed in to change notification settings - Fork 0
/
snap.sh
executable file
·554 lines (462 loc) · 13.6 KB
/
snap.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
#!/usr/bin/env bash
if [[ -z "${AWS_CONFIG_PATH}" ]]; then
AWS_CONFIG_PATH="$(cd -P -- "$(dirname -- "$0")" && pwd -P)"
fi
if [[ -z "${AWS_CONFIG}" ]]; then
AWS_CONFIG="${AWS_CONFIG_PATH}/.config"
fi
if [[ -f "${AWS_CONFIG}" ]]; then
. "${AWS_CONFIG}"
fi
# Ensure everything has a nice, pretty default
DEFAULT_TAGS="${DEFAULT_TAGS-CreatedBy=AutomatedBackup}"
EXTRA_TAG_LIST="${DEFAULT_TAGS}"
VOLUMES="${VOLUMES:-}"
INSTANCES="${INSTANCES:-}"
TAGLIST="${TAGLIST:-}"
TAGMAP="${TAGMAP:-}"
SNAPSHOT_FILTERS="${SNAPSHOT_FILTERS:-}"
MAX_AGE=${MAX_AGE:-'- 7 days'}
MAX_DATE="${MAX_DATE:-}"
DEBUG="${DEBUG:-1}"
MAX_RUNNING_SNAPSHOTS="${MAX_RUNNING_SNAPSHOTS:-0}"
# Convert stuff to arrays.
# At some point, we can make all this sh-compatible...
VOLUMES=($VOLUMES)
INSTANCES=($INSTANCES)
TAGLIST=($TAGLIST)
TAGMAP=($TAGMAP)
SNAPSHOT_FILTERS=($SNAPSHOT_FILTERS)
export AWS_DEFAULT_OUTPUT=text
declare -A VOLUME_TAGS
declare -A SNAPSHOT_TAGS
__parse_commandline() {
POSITIONAL=()
while [[ "$1" != "" ]]; do
case $1 in
--help | -h)
usage
exit
;;
--volume | --volumes | -v)
VOLUMES+=("$2")
shift
;;
--no-max-date)
MAX_DATE=0
;;
--instance | -i)
INSTANCES+=("$2")
shift
;;
--tag-list | --tags)
TAGLIST+=("$2")
shift
;;
--snapshot-tag-filter)
SNAPSHOT_FILTERS+=("$2")
shift
;;
--tag-map)
TAGMAP+=("$2")
shift
;;
--extra-tags)
EXTRA_TAG_LIST="${DEFAULT_TAGS} $2"
shift
;;
--max-age)
MAX_AGE="$2"
shift
;;
--max-date)
MAX_DATE="$2"
shift
;;
--dry-run | -d)
DRY_RUN='--dry-run'
;;
--debug)
DEBUG=1
;;
--max-running-snapshots)
MAX_RUNNING_SNAPSHOTS="$2"
shift
;;
* )
POSITIONAL+=("$1")
;;
esac
shift
done
}
####
# Parses "MAX_AGE" or "MAX_DATE" into the min epoch time to keep
__parse_retention_date() {
# If max-date is set, just convert it to epoch
# Otherwise, we need to parse from the "max age"
# We want to accept: 6 6D '6 days'
if [[ ! -z "${MAX_DATE}" ]]; then
MAX_DATE=$(date +%s --date "${MAX_DATE}")
elif [[ "${MAX_AGE}" =~ ^[0-9]+$ ]]; then
MAX_DATE=$(date +%s --date "${MAX_AGE} days ago")
elif [[ "${MAX_AGE}" =~ ^[0-9]+[dD]$ ]]; then
MAX_AGE="${MAX_AGE//[^0-9]/}"
MAX_DATE=$(date +%s --date "${MAX_AGE} days ago")
elif [[ "${MAX_AGE}" =~ ^[0-9]+[wW]$ ]]; then
MAX_AGE="${MAX_AGE//[^0-9]/}"
MAX_DATE=$(date +%s --date "${MAX_AGE} weeks ago")
elif [[ "${MAX_AGE}" =~ ^[0-9]+[mM]$ ]]; then
MAX_AGE="${MAX_AGE//[^0-9]/}"
MAX_DATE=$(date +%s --date "${MAX_AGE} months ago")
elif [[ "${MAX_AGE}" =~ ^[0-9]+[yY]$ ]]; then
MAX_AGE="${MAX_AGE//[^0-9]/}"
MAX_DATE=$(date +%s --date "${MAX_AGE} years ago")
else
# Well, hopefully 'date' can get-er-done
MAX_DATE=$(date +%s --date "${MAX_AGE}")
fi
AWS_FORMATTED_DATETIME=$(date --utc -d @"${MAX_DATE}" +'%Y-%m-%dT%H:%M:%SZ')
debug "MAX_DATE is '${MAX_DATE}' ($(date -d @"${MAX_DATE}" +'%Y-%m-%d %H:%M:%S'))"
}
###
# Simple logging
log() {
>&2 echo "[$(date +"%Y-%m-%d"+"%T")]: $*"
}
###
# Log only if debug is set
debug() {
if [[ ! -z "${DEBUG}" ]]; then
log "$*"
fi
}
trace() {
if [[ -n "${TRACE}" ]]; then
log "$*"
fi
}
__join() {
local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}";
}
###
# Checks to see if the machine the script is running on is an EC2 instance
__is_ec2() {
if command wget --timeout=0.1 --tries=1 -q http://169.254.169.254/latest/meta-data/instance-id > /dev/null; then
debug "Current machine is an EC2 instance"
return 0
else
debug "Current machine is not an EC2 instance"
return 1
fi
}
__get_snapshot_statuses() {
volume_id="$1"
aws ec2 describe-snapshots --filters "Name=volume-id,Values='${volume_id}'" --query 'Snapshots[].[SnapshotId,State]' --output text
}
###
# Get a list of instances from AWS if not explicitly specified
__get_instance_list() {
if [[ "${#INSTANCES[@]}" -eq "0" && "${#VOLUMES[@]}" -eq "0" ]]; then
instance_list=$(aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId')
INSTANCES=($instance_list)
fi
}
###
# Given an instance, retrieve all known volumes
__get_volumes_for_instance() {
instance="$1"
aws ec2 describe-volumes --filters "Name=attachment.instance-id,Values='${instance}'" --query 'Volumes[].VolumeId' --output text
}
###
# Get a list of volumes from AWS if not explicitly specified
__get_volumes_for_instances() {
for instance in "${INSTANCES[@]}"; do
debug "Getting volumes for '${instance}'"
volume_list=$(__get_volumes_for_instance "${instance}")
volume_list=($volume_list)
for volume in "${volume_list[@]}"; do
debug "Found volume ${volume} for ${instance}"
done
VOLUMES+=( "${volume_list[@]}" )
done
}
###
# Tries to read
__read_volume_tags() {
volume="$1"
VOLUME_TAGS=()
tags=$(aws ec2 describe-volumes --volume-ids "${volume}" --query 'Volumes[0].Tags' --output text)
while read -r key value; do
VOLUME_TAGS[$key]=$value
done <<< "${tags}"
}
__read_snapshot_tags() {
snapshot_id="$1"
SNAPSHOT_TAGS=()
tags=$(aws ec2 describe-snapshots --snapshot-ids "${snapshot_id}" --query 'Snapshots[0].Tags' --output text)
while read -r key value; do
SNAPSHOT_TAGS[$key]=$value
done <<< "${tags}"
}
###
# Given a list of volumes, filter out any not matching tags given on the command line
__filter_volume_by_tag() {
volume="$1"
# We're good if we're not filtering by tags
if [[ "${#TAGLIST[@]}" -eq "0" ]]; then
return 1
fi
for tag in "${TAGLIST[@]}"; do
IFS='=' read -r key value <<< "${tag}"
if [[ "${VOLUME_TAGS[$key]}" ]]; then
if [[ -z "${value}" ]] || [[ "${VOLUME_TAGS[$key]}" == "${value}" ]]; then
return 1
fi
fi
done
return 0
}
__filter_volume_by_snapshot_status() {
volume="$1"
if [[ "${MAX_RUNNING_SNAPSHOTS}" == "0" ]]; then
return 1
fi
pending_count=$(__get_snapshot_statuses ${volume} | grep pending | wc -l)
if (( "${pending_count}" < "${MAX_RUNNING_SNAPSHOTS}" )); then
return 1
else
return 0
fi
}
###
# Given a list of snapshots, filter out any not matching tags given on the command line
__filter_snapshot_by_tags() {
snapshot="$1"
# We're good if we're not filtering by tags
if [[ "${#SNAPSHOT_FILTERS[@]}" -eq "0" ]]; then
return 1
fi
for tag in "${SNAPSHOT_FILTERS[@]}"; do
IFS='=' read -r key value <<< "${tag}"
if [[ "${SNAPSHOT_TAGS[$key]}" ]]; then
if [[ -z "${value}" ]] || [[ "${SNAPSHOT_TAGS[$key]}" == "${value}" ]]; then
trace "Found a matching tag for ${SNAPSHOT_TAGS[$key]}"
return 1
fi
fi
done
return 0
}
###
# During cleanup, snapshots must have -all- default tags.
__filter_snapshot_by_required_tags() {
for tag in $EXTRA_TAG_LIST; do
IFS='=' read -r key value <<< "${tag}"
if [[ -z "${SNAPSHOT_TAGS[$key]}" ]]; then
return 0
fi
done
return 1
}
__get_volume_data() {
volume="$1"
aws ec2 describe-volumes --output=text --volume-ids "${volume}" --query 'Volumes[0].Attachments[0].[InstanceId,Device]'
}
__build_name_for_volume() {
instance_name="$1"
device_name="$2"
snapshot_tag="[${instance_name}]-[${device_name}]-backup-[$(date +'%Y-%m-%d %H:%M:%S')]"
echo "${snapshot_tag}"
}
__add_tag_to_snapshot() {
snapshot_id="$1"
tag_name="$2"
tag_value="$3"
if [[ -n "${DRY_RUN}" ]]; then
debug "Dry-run enabled: pretending to add '${tag_value}' as '${tag_name}' to '${snapshot_id}'"
else
aws ec2 create-tags --resource "${snapshot_id}" --tags "Key='${tag_name}',Value='${tag_value}'"
fi
}
###
# tagmaps copy "volume:tag" to "snapshop:tag"
# The values copied match "volume_tag=snapshot_tag" or "tag_to_copy"
__add_tagmap_to_snapshot() {
volume="$1"
snapshot_id="$2"
for tag in "${TAGMAP[@]}"; do
IFS='=' read -r key value <<< "${tag}"
tag_key="${value:-$key}"
tag_value="${VOLUME_TAGS[$key]}"
if [[ -z "${tag_value}" ]]; then
log "Not copying empty value from tag '${volume}:${tag_key}'"
continue
fi
debug "Adding tag '${tag_value}' as '${tag_key}' to ${snapshot_id} "
__add_tag_to_snapshot "${snapshot_id}" "${tag_key}" "${tag_value}"
done
}
__create_snapshot() {
name="$1"
volume="$2"
if [[ -n "${DRY_RUN}" ]]; then
snapshot_name="fake-snapshot-${name}-${volume}"
debug "dry-run enabled - fake snapshot name is ${snapshot_name}"
echo "${snapshot_name}"
else
aws ec2 create-snapshot --output=text --description "${name}" --volume-id "${volume}" --query 'SnapshotId'
fi
}
__add_extra_tags_to_snapshot() {
snapshot="$1"
for tag in $EXTRA_TAG_LIST; do
IFS='=' read -r key value <<< "${tag}"
__add_tag_to_snapshot "${snapshot}" "${key}" "${value}"
done
}
__make_snapshot_for_volume() {
volume="$1"
volume_data=$(__get_volume_data "${volume}")
read -r instance_name device_name <<< "${volume_data}"
name=$(__build_name_for_volume "${instance_name}" "${device_name}")
snapshot_id=$(__create_snapshot "${name}" "${volume}")
__add_extra_tags_to_snapshot "${snapshot_id}"
__add_tagmap_to_snapshot "${volume}" "${snapshot_id}"
}
###
# Creates snapshots of all known volumes
__snapshot_volumes() {
for volume in "${VOLUMES[@]}"; do
log "Discovered volume $volume"
__read_volume_tags "${volume}"
if __filter_volume_by_tag "${volume}"; then
debug "${volume} was discovered but filtered via tag"
continue
fi
if __filter_volume_by_snapshot_status "${volume}"; then
debug "${volume} was found but has too many pending snapshots"
continue
fi
log "Snapshotting ${volume}"
__make_snapshot_for_volume "${volume}"
done
}
__get_date_for_snapshot() {
snapshot="$1"
aws ec2 describe-snapshots --output=text --snapshot-ids "${snapshot}" --query Snapshots[].StartTime
}
####
# Check to see if the given snapshot is older than the MAX_DATE
__filter_snapshot_by_date() {
snapshot="$1"
snapshot_date=$(__get_date_for_snapshot "${snapshot}")
snapshot_date_epoch=$(date -d "${snapshot_date}" +'%s')
if (( "${snapshot_date_epoch}" <= "${MAX_DATE}" )); then
return 1;
else
return 0;
fi
}
####
# Wrapper for ec2 snapshot removal
__remove_snapshot() {
snapshot="$1"
if [[ -n "${DRY_RUN}" ]]; then
debug "... skipping actual delete of '${snapshot}' due to dry-run"
else
aws ec2 delete-snapshot --snapshot-id "${snapshot}"
fi
}
__cleanup_snapshots_for_volume() {
volume_id="$1"
debug "Beginning check of volume: '${volume_id}'"
snapshot_list=$(__get_snapshots_for_volume "${volume_id}" "StartTime<='${AWS_FORMATTED_DATETIME}'")
for snapshot in $snapshot_list; do
debug "Beginning check of ${snapshot}"
__read_snapshot_tags "${snapshot}"
if __filter_snapshot_by_required_tags "${snapshot}"; then
debug "'${snapshot}' was discovered but is missing a required tag"
continue
fi
if __filter_snapshot_by_tags "${snapshot}"; then
debug "'${snapshot}' was discovered but filtered by tag"
continue
fi
if __filter_snapshot_by_date "${snapshot}"; then
debug "'${snapshot}' was discovered but filtered by date"
continue
fi
debug "Will remove snapshot '${snapshot}'"
__remove_snapshot "${snapshot}"
done
}
__get_snapshots_for_volume() {
volume_id="$1"; shift
if [[ ! -z "$@" ]]; then
local additional_filters=$(printf '?'; __join ' && ' "$@")
log "additional_filters: $additional_filters"
fi
# ${snapshot_filter}
aws ec2 describe-snapshots --output=text --filters "Name=volume-id,Values='${volume_id}'" --query "Snapshots[${additional_filters}].SnapshotId"
}
__cleanup_volumes() {
for volume in "${VOLUMES[@]}"; do
__cleanup_snapshots_for_volume "${volume}"
done
}
usage() {
cat <<HELP_USAGE
$0 [options] ... command
Commands:
backup Takes snapshots of volumes matching the given filters
cleanup Cleans up old snapshots
cycle Performs snapshots, then runs cleanup
[all]
--help Shows the help message
--volume An explicit list of volumes to operate on
--instance List of instances to check for volumes
--tag-list List of tags to check volumes for snapshotting
--dry-run Don't actually do anything!
--debug Show debug output
[backup]
Takes a snapshot of a list of volumes. The list of volumes can be given via --volume,
or found by searching AWS volumes.
--tag-map List of tags to clone from volume to snapshot
[cleanup]
Cleans the snapshot list, removing snapshots older than the given age or date (default: 7 days)
--snapshot-tag-filter List of tags to filter on volumes during snapshot cleanup
--max-age Maxiumum age of snapshots to keep
--max-date Maximum date of snapshot to keep
--no-max-date Do not filter by dates at all (equal to --max-date=0)
HELP_USAGE
}
__snapshot() {
__get_instance_list
__get_volumes_for_instances
__snapshot_volumes
}
__cleanup() {
__parse_retention_date
__get_instance_list
__get_volumes_for_instances
__cleanup_volumes
}
__cycle() {
__parse_retention_date
__get_instance_list
__get_volumes_for_instances
__snapshot_volumes
__cleanup_volumes
}
__parse_commandline "$@"
COMMAND="${POSITIONAL[0]}"
if [[ "${COMMAND}" == "backup" ]]; then
__snapshot
elif [[ "${COMMAND}" == "cleanup" ]]; then
__cleanup
elif [[ "${COMMAND}" == "cycle" ]]; then
__cycle
else
echo "Unknown command: must give one of [backup,cleanup,cycle]"
fi