rds-rotate-db-snapshots 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +43 -0
- data/.github/workflows/codeql.yml +76 -0
- data/.rspec +1 -0
- data/Gemfile +10 -11
- data/README.md +82 -0
- data/Rakefile +0 -15
- data/VERSION +1 -1
- data/bin/rds-rotate-db-snapshots +18 -247
- data/lib/rds_rotate_db_snapshots/action_wrappers.rb +23 -0
- data/lib/rds_rotate_db_snapshots/actions.rb +105 -0
- data/lib/rds_rotate_db_snapshots/options_parser.rb +110 -0
- data/lib/rds_rotate_db_snapshots/rds_client.rb +20 -0
- data/lib/rds_rotate_db_snapshots.rb +58 -0
- data/rds-rotate-db-snapshots.gemspec +22 -13
- data/spec/helper.rb +81 -0
- data/spec/rds_rotate_db_snapshots_spec.rb +62 -0
- metadata +32 -11
- data/.travis.yml +0 -9
- data/README.rdoc +0 -65
- data/lib/.empty +0 -1
- data/test/helper.rb +0 -18
- data/test/test_rds-rotate-db-snapshots.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ddfaa42b87e8ad4f9cabafcea086ce82d0f351068d05db7da97b53fa5080a0f
|
4
|
+
data.tar.gz: dac01f0570c16a5a5a205253f0ca3e6cd26d7f7ed22377da3f9f1bad2e99d87b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d5d586b5ff033df7c7a4d131259d7f47b1f30e2592b616fc0e6644fcbd4999d0e24815dba96c9d335e8d6e35446293bee2b59f83ab286eda4a32a3f39cbaf38
|
7
|
+
data.tar.gz: 5f9339752c7a963981f1d6be97ed321a0897f8028d52f632b6dae0fb81e2e495327e127f7dec3d4040b35610ad3a7c56d9206b8220a376485c87f4372fe88470
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
3
|
+
# Please see the documentation for all configuration options:
|
4
|
+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
5
|
+
|
6
|
+
version: 2
|
7
|
+
updates:
|
8
|
+
- package-ecosystem: "bundler" # See documentation for possible values
|
9
|
+
directory: "/" # Location of package manifests
|
10
|
+
schedule:
|
11
|
+
interval: "weekly"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
name: "CI"
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: ["main"]
|
6
|
+
pull_request:
|
7
|
+
branches: ["main"]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: ubuntu-20.04
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby_version: [2.7, 3.0, 3.1]
|
15
|
+
steps:
|
16
|
+
- name: Checkout code
|
17
|
+
uses: actions/checkout@v3
|
18
|
+
- name: Install Ruby and gems
|
19
|
+
uses: ruby/setup-ruby@03b78bdda287ae04217ee12e4b64996630a03542 #v1.131.0
|
20
|
+
with:
|
21
|
+
bundler-cache: true
|
22
|
+
ruby-version: ${{ matrix.ruby_version }}
|
23
|
+
- name: Install Bundler
|
24
|
+
run: gem install bundler
|
25
|
+
- name: Bundle Install
|
26
|
+
run: bundle install
|
27
|
+
- name: Test
|
28
|
+
run: bundle exec rspec
|
29
|
+
- name: Coveralls Parallel
|
30
|
+
uses: coverallsapp/github-action@master
|
31
|
+
with:
|
32
|
+
github-token: ${{ secrets.github_token }}
|
33
|
+
flag-name: run-${{ matrix.ruby_version }}
|
34
|
+
parallel: true
|
35
|
+
finish:
|
36
|
+
needs: test
|
37
|
+
runs-on: ubuntu-latest
|
38
|
+
steps:
|
39
|
+
- name: Coveralls Finished
|
40
|
+
uses: coverallsapp/github-action@master
|
41
|
+
with:
|
42
|
+
github-token: ${{ secrets.github_token }}
|
43
|
+
parallel-finished: true
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# For most projects, this workflow file will not need changing; you simply need
|
2
|
+
# to commit it to your repository.
|
3
|
+
#
|
4
|
+
# You may wish to alter this file to override the set of languages analyzed,
|
5
|
+
# or to provide custom queries or build logic.
|
6
|
+
#
|
7
|
+
# ******** NOTE ********
|
8
|
+
# We have attempted to detect the languages in your repository. Please check
|
9
|
+
# the `language` matrix defined below to confirm you have the correct set of
|
10
|
+
# supported CodeQL languages.
|
11
|
+
#
|
12
|
+
name: "CodeQL"
|
13
|
+
|
14
|
+
on:
|
15
|
+
push:
|
16
|
+
branches: [ "main" ]
|
17
|
+
pull_request:
|
18
|
+
# The branches below must be a subset of the branches above
|
19
|
+
branches: [ "main" ]
|
20
|
+
schedule:
|
21
|
+
- cron: '22 4 * * 2'
|
22
|
+
|
23
|
+
jobs:
|
24
|
+
analyze:
|
25
|
+
name: Analyze
|
26
|
+
runs-on: ubuntu-latest
|
27
|
+
permissions:
|
28
|
+
actions: read
|
29
|
+
contents: read
|
30
|
+
security-events: write
|
31
|
+
|
32
|
+
strategy:
|
33
|
+
fail-fast: false
|
34
|
+
matrix:
|
35
|
+
language: [ 'ruby' ]
|
36
|
+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
37
|
+
# Use only 'java' to analyze code written in Java, Kotlin or both
|
38
|
+
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
39
|
+
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
40
|
+
|
41
|
+
steps:
|
42
|
+
- name: Checkout repository
|
43
|
+
uses: actions/checkout@v3
|
44
|
+
|
45
|
+
# Initializes the CodeQL tools for scanning.
|
46
|
+
- name: Initialize CodeQL
|
47
|
+
uses: github/codeql-action/init@v2
|
48
|
+
with:
|
49
|
+
languages: ${{ matrix.language }}
|
50
|
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
51
|
+
# By default, queries listed here will override any specified in a config file.
|
52
|
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
53
|
+
|
54
|
+
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
55
|
+
# queries: security-extended,security-and-quality
|
56
|
+
|
57
|
+
|
58
|
+
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
59
|
+
# If this step fails, then you should remove it and run the build manually (see below)
|
60
|
+
- name: Autobuild
|
61
|
+
uses: github/codeql-action/autobuild@v2
|
62
|
+
|
63
|
+
# ℹ️ Command-line programs to run using the OS shell.
|
64
|
+
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
65
|
+
|
66
|
+
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
67
|
+
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
68
|
+
|
69
|
+
# - run: |
|
70
|
+
# echo "Run, Build Application using script"
|
71
|
+
# ./location_of_script_within_repo/buildscript.sh
|
72
|
+
|
73
|
+
- name: Perform CodeQL Analysis
|
74
|
+
uses: github/codeql-action/analyze@v2
|
75
|
+
with:
|
76
|
+
category: "/language:${{matrix.language}}"
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-w --color
|
data/Gemfile
CHANGED
@@ -1,20 +1,19 @@
|
|
1
|
-
source "
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
3
|
gem 'aws-sdk-rds', '~> 1'
|
4
|
-
|
5
|
-
# Add dependencies to develop your gem here.
|
6
|
-
# Include everything needed to run rake, tests, features, etc.
|
7
|
-
group :development, :test do
|
8
|
-
gem 'bundler'
|
9
|
-
gem 'simplecov'
|
10
|
-
end
|
4
|
+
gem "rake"
|
11
5
|
|
12
6
|
group :development do
|
13
7
|
gem 'juwelier'
|
8
|
+
gem "pry"
|
9
|
+
gem "pry-byebug"
|
14
10
|
end
|
15
11
|
|
16
12
|
group :test do
|
17
|
-
gem
|
18
|
-
gem
|
19
|
-
gem
|
13
|
+
gem "rspec", ">= 3.2"
|
14
|
+
gem "rspec-mocks", ">= 3"
|
15
|
+
gem "rubocop", "~> 0.50.0"
|
16
|
+
gem "simplecov", ">= 0.13"
|
17
|
+
gem 'simplecov-lcov', '~> 0.8.0'
|
18
|
+
gem "webmock"
|
20
19
|
end
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# rds-rotate-db-snapshots
|
2
|
+
|
3
|
+
[<img src="https://badge.fury.io/rb/rds-rotate-db-snapshots.svg" alt="Gem
|
4
|
+
Version" />](https://badge.fury.io/rb/rds-rotate-db-snapshots) [![CI](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml/badge.svg?query=branch%3Amain+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml?query=branch%3Amain+event%3Apush) [![CodeQL](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml/badge.svg?query=branch%3Amain+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml?query=branch%3Amain+event%3Apush)
|
5
|
+
|
6
|
+
Provides a simple way to rotate db snapshots in Amazon Relational Database
|
7
|
+
Service (RDS).
|
8
|
+
|
9
|
+
## Tested on Rubies
|
10
|
+
|
11
|
+
- 2.7
|
12
|
+
- 3.1
|
13
|
+
- 3.2
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Gem installation:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install rds-rotate-db-snapshots
|
21
|
+
```
|
22
|
+
|
23
|
+
Usage:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
rds-rotate-db-snapshots [options] <db_indentifier>
|
27
|
+
```
|
28
|
+
|
29
|
+
Add this script to CRON (let's say it will run this script every X hours) and it will do the job well
|
30
|
+
|
31
|
+
```bash
|
32
|
+
#/usr/bin/bash
|
33
|
+
AWS_ACCESS_KEY='xxxxxxxxxxxxxxxxxxxx'
|
34
|
+
AWS_SECRET_ACCESS_KEY='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
|
35
|
+
AWS_REGION='eu-west-1'
|
36
|
+
DESCRIPTION_PREFIX='automatic-backup-'
|
37
|
+
RDS_ROTATOR=/here/is/the/path/to/rds-rotate-db-snapshots
|
38
|
+
DB_NAME='db_name_here'
|
39
|
+
|
40
|
+
$RDS_ROTATOR --aws-region $AWS_REGION --aws-access-key $AWS_ACCESS_KEY --aws-secret-access-key $AWS_SECRET_ACCESS_KEY --pattern $DESCRIPTION_PREFIX --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 1 --keep-yearly 0 --create-snapshot $DESCRIPTION_PREFIX$DB_NAME $DB_NAME
|
41
|
+
```
|
42
|
+
|
43
|
+
## Options
|
44
|
+
|
45
|
+
- `--aws-access-key ACCESS_KEY` "AWS Access Key"
|
46
|
+
- `--aws-secret-access-key SECRET_KEY` "AWS Secret Access Key"
|
47
|
+
- `--aws-region REGION` "AWS Region"
|
48
|
+
- `--pattern STRING` "Snapshots without this string in the description will be ignored"
|
49
|
+
- `--by-tags TAG=VALUE,TAG=VALUE` "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs."
|
50
|
+
- `--backoff-limit INTEGER` "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15"
|
51
|
+
- `--create-snapshot STRING` "Use this option if you want to create a snapshot"
|
52
|
+
- `--keep-hourly INTEGER` "Number of hourly snapshots to keep"
|
53
|
+
- `--keep-daily INTEGER` "Number of daily snapshots to keep"
|
54
|
+
- `--keep-weekly INTEGER` "Number of weekly snapshots to keep"
|
55
|
+
- `--keep-last` "Keep the most recent snapshot, regardless of time-based policy"
|
56
|
+
- `--dry-run` "Shows what would happen without doing anything"
|
57
|
+
|
58
|
+
## Tips
|
59
|
+
|
60
|
+
If you are not sure what happen - add option `--dry-run`.
|
61
|
+
|
62
|
+
In that case the script will not destroy/create anything in RDS, it will just
|
63
|
+
show the messages.
|
64
|
+
|
65
|
+
## Contributing to rds-rotate-db-snapshots
|
66
|
+
|
67
|
+
- Check out the latest main to make sure the feature hasn't been
|
68
|
+
implemented or the bug hasn't been fixed yet
|
69
|
+
- Check out the issue tracker to make sure someone already hasn't requested
|
70
|
+
it and/or contributed it
|
71
|
+
- Fork the project
|
72
|
+
- Start a feature/bugfix branch
|
73
|
+
- Commit and push until you are happy with your contribution
|
74
|
+
- Make sure to add tests for it. This is important so I don't break it in a
|
75
|
+
future version unintentionally.
|
76
|
+
- Please try not to mess with the Rakefile, version, or history. If you want
|
77
|
+
to have your own version, or is otherwise necessary, that is fine, but
|
78
|
+
please isolate to its own commit so I can cherry-pick around it.
|
79
|
+
|
80
|
+
## Copyright
|
81
|
+
|
82
|
+
Copyright (c) 2014 Siarhei Kavaliou. See LICENSE.txt for further details.
|
data/Rakefile
CHANGED
@@ -22,21 +22,6 @@ Juwelier::Tasks.new do |gem|
|
|
22
22
|
end
|
23
23
|
Juwelier::RubygemsDotOrgTasks.new
|
24
24
|
|
25
|
-
require 'rake/testtask'
|
26
|
-
Rake::TestTask.new(:test) do |test|
|
27
|
-
test.pattern = 'test/**/test_*.rb'
|
28
|
-
test.verbose = true
|
29
|
-
end
|
30
|
-
|
31
|
-
# require 'simplecov'
|
32
|
-
# Rcov::RcovTask.new do |test|
|
33
|
-
# test.libs << 'test'
|
34
|
-
# test.pattern = 'test/**/test_*.rb'
|
35
|
-
# test.verbose = true
|
36
|
-
# end
|
37
|
-
|
38
|
-
task :default => :test
|
39
|
-
|
40
25
|
require 'rdoc/task'
|
41
26
|
Rake::RDocTask.new do |rdoc|
|
42
27
|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/bin/rds-rotate-db-snapshots
CHANGED
@@ -1,200 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require 'aws-sdk-rds'
|
5
|
-
require 'optparse'
|
3
|
+
require_relative '../lib/rds_rotate_db_snapshots'
|
6
4
|
|
7
|
-
|
8
|
-
:aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
|
9
|
-
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
10
|
-
:aws_session_token => ENV["AWS_SESSION_TOKEN"],
|
11
|
-
:aws_region => ENV["AWS_REGION"],
|
12
|
-
:pattern => nil,
|
13
|
-
:by_tags => nil,
|
14
|
-
:dry_run => false,
|
15
|
-
:backoff_limit => 15,
|
16
|
-
:create_snapshot => nil
|
17
|
-
}
|
5
|
+
rrds = RdsRotateDbSnapshots.new(script_name: File.basename($0), cli: true)
|
18
6
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
:weekly => { :seconds => 7 * 24 * 60 * 60, :format => '%Y-%W', :keep => 0, :keeping => {} },
|
23
|
-
:monthly => { :seconds => 30 * 24 * 60 * 60, :format => '%Y-%m', :keep => 0, :keeping => {} },
|
24
|
-
:yearly => { :seconds => 12 * 30 * 24 * 60 * 60, :format => '%Y', :keep => 0, :keeping => {} },
|
25
|
-
}
|
26
|
-
def backoff()
|
27
|
-
$backoffed = $backoffed + 1
|
28
|
-
|
29
|
-
if $opts[:backoff_limit] > 0 && $opts[:backoff_limit] < $backoffed
|
30
|
-
puts "Too many backoff attempts. Sorry it didn't work out."
|
31
|
-
exit 2
|
32
|
-
end
|
33
|
-
|
34
|
-
naptime = rand(60) * $backoffed
|
35
|
-
puts "Backing off for #{naptime} seconds..."
|
36
|
-
sleep naptime
|
37
|
-
end
|
38
|
-
|
39
|
-
def rotate_em(snapshots)
|
40
|
-
# poor man's way to get a deep copy of our time_periods definition hash
|
41
|
-
periods = Marshal.load(Marshal.dump($time_periods))
|
42
|
-
|
43
|
-
snapshots.each do |snapshot|
|
44
|
-
time = snapshot[:snapshot_create_time]
|
45
|
-
db_id = snapshot[:db_instance_identifier]
|
46
|
-
snapshot_id = snapshot[:db_snapshot_identifier]
|
47
|
-
description = snapshot_id
|
48
|
-
keep_reason = nil
|
49
|
-
|
50
|
-
if $opts[:pattern] && description !~ /#{$opts[:pattern]}/
|
51
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Skipping snapshot with description #{description}"
|
52
|
-
next
|
53
|
-
end
|
54
|
-
|
55
|
-
periods.keys.sort { |a, b| periods[a][:seconds] <=> periods[b][:seconds] }.each do |period|
|
56
|
-
period_info = periods[period]
|
57
|
-
keep = period_info[:keep]
|
58
|
-
keeping = period_info[:keeping]
|
59
|
-
|
60
|
-
time_string = time.strftime period_info[:format]
|
61
|
-
if Time.now - time < keep * period_info[:seconds]
|
62
|
-
if !keeping.key?(time_string) && keeping.length < keep
|
63
|
-
keep_reason = period
|
64
|
-
keeping[time_string] = snapshot
|
65
|
-
end
|
66
|
-
break
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
if keep_reason.nil? && snapshot == snapshots.last && $opts[:keep_last]
|
71
|
-
keep_reason = 'last snapshot'
|
72
|
-
end
|
73
|
-
|
74
|
-
if !keep_reason.nil?
|
75
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Keeping for #{keep_reason}"
|
76
|
-
else
|
77
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Deleting"
|
78
|
-
begin
|
79
|
-
$rds.delete_db_snapshot(db_snapshot_identifier: snapshot_id) unless $opts[:dry_run]
|
80
|
-
rescue Aws::RDS::Errors => e
|
81
|
-
backoff()
|
82
|
-
retry
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def create_snapshot(name, db_indentifier_ids)
|
89
|
-
if !!name
|
90
|
-
name = name.gsub(/[^a-zA-Z0-9\-]/, '')
|
91
|
-
if name.size > 0
|
92
|
-
name = "#{name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
93
|
-
db_indentifier_ids.each do |db_id|
|
94
|
-
begin
|
95
|
-
$rds.create_db_snapshot(db_snapshot_identifier: name, db_instance_identifier: db_id) unless $opts[:dry_run]
|
96
|
-
puts " #{Time.now.strftime '%Y-%m-%d %H:%M:%S'} Creation snapshot #{name} is pending (db: #{db_id})"
|
97
|
-
rescue Aws::RDS::Errors::InvalidDBInstanceStateFault => e
|
98
|
-
backoff()
|
99
|
-
retry
|
100
|
-
end
|
101
|
-
end
|
102
|
-
else
|
103
|
-
puts "invalid snapshot name format - #{name}"
|
104
|
-
exit 1
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def split_tag(hash,v)
|
110
|
-
v.split(',').each do |pair|
|
111
|
-
tag, value = pair.split('=',2)
|
112
|
-
if value.nil?
|
113
|
-
puts "invalid tag=value format"
|
114
|
-
exit 1
|
115
|
-
end
|
116
|
-
hash[tag] = value
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def get_db_snapshots(options)
|
121
|
-
snapshots = []
|
122
|
-
response = $rds.describe_db_snapshots(options)
|
123
|
-
while true do
|
124
|
-
snapshots += response.db_snapshots
|
125
|
-
break unless response[:marker]
|
126
|
-
|
127
|
-
response = $rds.describe_db_snapshots(options.merge(marker: response[:marker]))
|
128
|
-
end
|
129
|
-
snapshots
|
7
|
+
if rrds.options[:aws_access_key].nil? || rrds.options[:aws_secret_access_key].nil?
|
8
|
+
puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key and --aws-session-token"
|
9
|
+
exit 1
|
130
10
|
end
|
131
11
|
|
132
|
-
|
133
|
-
|
134
|
-
o.banner = "Usage: #{script_name} [options] <db_indentifier>\nUsage: #{script_name} --by-tags <tag=value,...> [other options]"
|
135
|
-
o.separator ""
|
136
|
-
|
137
|
-
o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
|
138
|
-
$opts[:aws_access_key] = v
|
139
|
-
end
|
140
|
-
|
141
|
-
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
142
|
-
$opts[:aws_secret_access_key] = v
|
143
|
-
end
|
144
|
-
|
145
|
-
o.on("--aws-region REGION", "AWS Region") do |v|
|
146
|
-
$opts[:aws_region] = v
|
147
|
-
end
|
148
|
-
|
149
|
-
o.on("--aws-session-token SESSION_TOKEN", "AWS session token") do |v|
|
150
|
-
$opts[:aws_session_token] = v
|
151
|
-
end
|
152
|
-
|
153
|
-
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
154
|
-
$opts[:pattern] = v
|
155
|
-
end
|
156
|
-
|
157
|
-
o.on("--by-tags TAG=VALUE,TAG=VALUE", "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs.") do |v|
|
158
|
-
$opts[:by_tags] = {}
|
159
|
-
puts 'Hey! It\'s not implemented in RDS yet. Who knows, maybe they will add Tagging in RDS later.'
|
160
|
-
exit 0
|
161
|
-
split_tag($opts[:by_tags],v)
|
162
|
-
end
|
163
|
-
|
164
|
-
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15") do |v|
|
165
|
-
$opts[:backoff_limit] = v
|
166
|
-
end
|
167
|
-
|
168
|
-
o.on("--create-snapshot STRING", "Use this option if you want to create a snapshot") do |v|
|
169
|
-
$opts[:create_snapshot] = v
|
170
|
-
end
|
171
|
-
|
172
|
-
$time_periods.keys.sort { |a, b| $time_periods[a][:seconds] <=> $time_periods[b][:seconds] }.each do |period|
|
173
|
-
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
174
|
-
$time_periods[period][:keep] = v
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
179
|
-
$opts[:keep_last] = true
|
180
|
-
end
|
181
|
-
|
182
|
-
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
183
|
-
$opts[:dry_run] = true
|
184
|
-
end
|
185
|
-
end.parse!
|
186
|
-
|
187
|
-
if $opts[:aws_access_key].nil? || $opts[:aws_secret_access_key].nil?
|
188
|
-
puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key"
|
12
|
+
if rrds.options[:aws_region].nil?
|
13
|
+
puts "You must specify your AWS Region via --aws-region"
|
189
14
|
exit 1
|
190
15
|
end
|
191
16
|
|
192
|
-
if ARGV.empty? and
|
17
|
+
if ARGV.empty? and rrds.options[:by_tags].nil?
|
193
18
|
puts "You must provide at least one DB Indentifier when not rotating by tags"
|
194
19
|
exit 1
|
195
20
|
end
|
196
21
|
|
197
|
-
if
|
22
|
+
if rrds.options[:by_tags].nil?
|
198
23
|
db_indentifier_ids = ARGV
|
199
24
|
|
200
25
|
db_indentifier_ids.each do |db_id|
|
@@ -208,83 +33,29 @@ else
|
|
208
33
|
if !ARGV.empty?
|
209
34
|
puts "Ignoring supplied db_indentifier_ids because we're rotating by tags."
|
210
35
|
end
|
211
|
-
if
|
36
|
+
if rrds.options[:by_tags].length == 0
|
212
37
|
puts "Rotating by tags but no tags specified? Refusing to rotate all snapshots!"
|
213
38
|
exit 1
|
214
39
|
end
|
215
40
|
end
|
216
41
|
|
217
|
-
if
|
42
|
+
if rrds.options[:backoff_limit] < 0
|
218
43
|
puts "A negative backoff limit doesn't make much sense."
|
219
44
|
exit 1
|
220
45
|
end
|
221
46
|
|
222
|
-
|
223
|
-
|
224
|
-
Aws.config.update(
|
225
|
-
access_key_id: $opts[:aws_access_key],
|
226
|
-
secret_access_key: $opts[:aws_secret_access_key],
|
227
|
-
region: $opts[:aws_region],
|
228
|
-
session_token: $opts[:aws_session_token]
|
229
|
-
)
|
230
|
-
$rds = Aws::RDS::Client.new
|
231
|
-
rescue Aws::RDS::Errors => e
|
232
|
-
backoff()
|
233
|
-
retry
|
234
|
-
end
|
235
|
-
|
236
|
-
if $opts[:create_snapshot]
|
237
|
-
create_snapshot($opts[:create_snapshot], db_indentifier_ids)
|
47
|
+
if rrds.options[:create_snapshot]
|
48
|
+
rrds.create_snapshot(rrds.options[:create_snapshot], db_indentifier_ids)
|
238
49
|
end
|
239
50
|
|
240
51
|
all_snapshots = []
|
241
|
-
if
|
242
|
-
|
243
|
-
begin
|
244
|
-
these_snapshots = $rds.describe_tags(snapshot_type: 'manual', filters: {'resource-type'=>"snapshot", 'key'=>tag, 'value'=>value}).
|
245
|
-
delete_if{ |e| e.status != 'available' }
|
246
|
-
rescue Aws::RDS::Errors => e
|
247
|
-
backoff()
|
248
|
-
retry
|
249
|
-
end
|
250
|
-
if these_snapshots.length == 0
|
251
|
-
puts "(tag,value)=(#{tag},#{value}) found no snapshots; nothing to rotate!"
|
252
|
-
exit 0
|
253
|
-
end
|
254
|
-
if all_snapshots.length == 0
|
255
|
-
remaining_snapshots = these_snapshots
|
256
|
-
else
|
257
|
-
remaining_snapshots = all_snapshots & these_snapshots
|
258
|
-
end
|
259
|
-
if remaining_snapshots.length == 0
|
260
|
-
puts "No remaining snapshots after applying (tag,value)=(#{tag},#{value}) filter; nothing to rotate!"
|
261
|
-
exit 0
|
262
|
-
end
|
263
|
-
all_snapshots = remaining_snapshots
|
264
|
-
end
|
265
|
-
|
266
|
-
begin
|
267
|
-
rotate_these = get_db_snapshots(db_instance_identifier: all_snapshots.map(&:db_instance_identifier).uniq).
|
268
|
-
delete_if{ |e| !all_snapshots.include?(e.db_snapshot_identifier) }.
|
269
|
-
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
270
|
-
rescue Aws::RDS::Errors => e
|
271
|
-
backoff()
|
272
|
-
retry
|
273
|
-
end
|
274
|
-
|
275
|
-
rotate_em(rotate_these)
|
52
|
+
if rrds.options[:by_tags]
|
53
|
+
rrds.rotate_by_tags
|
276
54
|
else
|
277
|
-
|
278
|
-
all_snapshots = get_db_snapshots(snapshot_type: 'manual').
|
279
|
-
delete_if{ |e| e[:status] != 'available' }
|
280
|
-
rescue Aws::RDS::Errors => e
|
281
|
-
backoff()
|
282
|
-
retry
|
283
|
-
end
|
284
|
-
|
55
|
+
snapshots = rrds.get_db_snapshots(snapshot_type: 'manual').delete_if{ |e| e[:status] != 'available' }
|
285
56
|
db_indentifier_ids.each do |db_id|
|
286
|
-
rotate_em(
|
287
|
-
|
57
|
+
rrds.rotate_em(
|
58
|
+
snapshots.select {|ss| ss[:db_instance_identifier] == db_id }.
|
288
59
|
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
289
60
|
)
|
290
61
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'aws-sdk-rds'
|
2
|
+
|
3
|
+
class RdsRotateDbSnapshots
|
4
|
+
module ActionWrappers
|
5
|
+
def with_backoff(*method_names)
|
6
|
+
method_names.each do |m|
|
7
|
+
wrapper = Module.new do
|
8
|
+
define_method(m) do |*args|
|
9
|
+
reset_backoff
|
10
|
+
begin
|
11
|
+
super *args
|
12
|
+
rescue Aws::RDS::Errors => e
|
13
|
+
backoff
|
14
|
+
retry
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
self.prepend wrapper
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class RdsRotateDbSnapshots
|
2
|
+
module Actions
|
3
|
+
def rotate_em(snapshots)
|
4
|
+
# poor man's way to get a deep copy of our time_periods definition hash
|
5
|
+
periods = Marshal.load(Marshal.dump(time_periods))
|
6
|
+
|
7
|
+
snapshots.each do |snapshot|
|
8
|
+
time = snapshot[:snapshot_create_time]
|
9
|
+
db_id = snapshot[:db_instance_identifier]
|
10
|
+
snapshot_id = snapshot[:db_snapshot_identifier]
|
11
|
+
description = snapshot_id
|
12
|
+
keep_reason = nil
|
13
|
+
|
14
|
+
if options[:pattern] && description !~ /#{options[:pattern]}/
|
15
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Skipping snapshot with description #{description}"
|
16
|
+
next
|
17
|
+
end
|
18
|
+
|
19
|
+
periods.keys.sort { |a, b| periods[a][:seconds] <=> periods[b][:seconds] }.each do |period|
|
20
|
+
period_info = periods[period]
|
21
|
+
keep = period_info[:keep]
|
22
|
+
keeping = period_info[:keeping]
|
23
|
+
|
24
|
+
time_string = time.strftime period_info[:format]
|
25
|
+
if Time.now - time < keep * period_info[:seconds]
|
26
|
+
if !keeping.key?(time_string) && keeping.length < keep
|
27
|
+
keep_reason = period
|
28
|
+
keeping[time_string] = snapshot
|
29
|
+
end
|
30
|
+
break
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if keep_reason.nil? && snapshot == snapshots.last && options[:keep_last]
|
35
|
+
keep_reason = 'last snapshot'
|
36
|
+
end
|
37
|
+
|
38
|
+
if !keep_reason.nil?
|
39
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Keeping for #{keep_reason}"
|
40
|
+
else
|
41
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Deleting"
|
42
|
+
begin
|
43
|
+
client.delete_db_snapshot(db_snapshot_identifier: snapshot_id) unless options[:dry_run]
|
44
|
+
rescue Aws::RDS::Errors => e
|
45
|
+
backoff
|
46
|
+
retry
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_snapshot(name, db_indentifier_ids)
|
53
|
+
if !!name
|
54
|
+
name = name.gsub(/[^a-zA-Z0-9\-]/, '')
|
55
|
+
if name.size > 0
|
56
|
+
name = "#{name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
57
|
+
db_indentifier_ids.each do |db_id|
|
58
|
+
begin
|
59
|
+
client.create_db_snapshot(db_snapshot_identifier: name, db_instance_identifier: db_id) unless options[:dry_run]
|
60
|
+
puts " #{Time.now.strftime '%Y-%m-%d %H:%M:%S'} Creation snapshot #{name} is pending (db: #{db_id})"
|
61
|
+
rescue Aws::RDS::Errors::InvalidDBInstanceStateFault => e
|
62
|
+
backoff
|
63
|
+
retry
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
puts "invalid snapshot name format - #{name}"
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_db_snapshots(options)
|
74
|
+
snapshots = []
|
75
|
+
response = client.describe_db_snapshots(options)
|
76
|
+
while true do
|
77
|
+
snapshots += response.db_snapshots
|
78
|
+
break unless response[:marker]
|
79
|
+
|
80
|
+
response = client.describe_db_snapshots(options.merge(marker: response[:marker]))
|
81
|
+
end
|
82
|
+
snapshots
|
83
|
+
end
|
84
|
+
|
85
|
+
def rotate_by_tags
|
86
|
+
snapshots = []
|
87
|
+
options[:by_tags].each do |tag, value|
|
88
|
+
snapshots = rrds.client.describe_tags(
|
89
|
+
snapshot_type: 'manual', filters: {'resource-type'=>"snapshot", 'key'=>tag, 'value'=>value}
|
90
|
+
).delete_if{ |e| e.status != 'available' }
|
91
|
+
# TODO: re-work
|
92
|
+
if snapshots.length == 0
|
93
|
+
puts "(tag,value)=(#{tag},#{value}) found no snapshots; nothing to rotate!"
|
94
|
+
exit 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
snapshots = get_db_snapshots(db_instance_identifier: snapshots.map(&:db_instance_identifier).uniq).
|
99
|
+
delete_if{ |e| !snapshots.include?(e.db_snapshot_identifier) }.
|
100
|
+
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
101
|
+
|
102
|
+
rotate_em snapshots
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class RdsRotateDbSnapshots
|
4
|
+
class OptionsParser
|
5
|
+
class NotImplementedError < StandardError; end
|
6
|
+
class InvalidArgument < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :options, :script_name, :time_periods
|
9
|
+
|
10
|
+
def initialize(script_name: nil, cli: false)
|
11
|
+
@script_name = script_name
|
12
|
+
@options = {
|
13
|
+
:aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
|
14
|
+
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
15
|
+
:aws_session_token => ENV["AWS_SESSION_TOKEN"],
|
16
|
+
:aws_region => ENV["AWS_REGION"],
|
17
|
+
:pattern => nil,
|
18
|
+
:by_tags => nil,
|
19
|
+
:dry_run => false,
|
20
|
+
:backoff_limit => 15,
|
21
|
+
:create_snapshot => nil
|
22
|
+
}
|
23
|
+
@time_periods = {
|
24
|
+
:hourly => { :seconds => 60 * 60, :format => '%Y-%m-%d-%H', :keep => 0, :keeping => {} },
|
25
|
+
:daily => { :seconds => 24 * 60 * 60, :format => '%Y-%m-%d', :keep => 0, :keeping => {} },
|
26
|
+
:weekly => { :seconds => 7 * 24 * 60 * 60, :format => '%Y-%W', :keep => 0, :keeping => {} },
|
27
|
+
:monthly => { :seconds => 30 * 24 * 60 * 60, :format => '%Y-%m', :keep => 0, :keeping => {} },
|
28
|
+
:yearly => { :seconds => 12 * 30 * 24 * 60 * 60, :format => '%Y', :keep => 0, :keeping => {} },
|
29
|
+
}
|
30
|
+
@cli = cli
|
31
|
+
init_cli_parser if cli?
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse!
|
35
|
+
@parser.parse!
|
36
|
+
@options.merge(time_periods: @time_periods)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def cli?
|
42
|
+
!!@cli
|
43
|
+
end
|
44
|
+
|
45
|
+
def init_cli_parser
|
46
|
+
@parser ||= OptionParser.new do |o|
|
47
|
+
o.banner = "Usage: #{script_name} [options] <db_indentifier>\nUsage: #{script_name} --by-tags <tag=value,...> [other options]"
|
48
|
+
o.separator ""
|
49
|
+
|
50
|
+
o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
|
51
|
+
@options[:aws_access_key] = v
|
52
|
+
end
|
53
|
+
|
54
|
+
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
55
|
+
@options[:aws_secret_access_key] = v
|
56
|
+
end
|
57
|
+
|
58
|
+
o.on("--aws-region REGION", "AWS Region") do |v|
|
59
|
+
@options[:aws_region] = v
|
60
|
+
end
|
61
|
+
|
62
|
+
o.on("--aws-session-token SESSION_TOKEN", "AWS session token") do |v|
|
63
|
+
@options[:aws_session_token] = v
|
64
|
+
end
|
65
|
+
|
66
|
+
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
67
|
+
@options[:pattern] = v
|
68
|
+
end
|
69
|
+
|
70
|
+
o.on("--by-tags TAG=VALUE,TAG=VALUE", "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs.") do |v|
|
71
|
+
@options[:by_tags] = {}
|
72
|
+
raise NotImplementedError, 'Hey! It\'s not implemented in RDS yet. Who knows, maybe they will add Tagging in RDS later.'
|
73
|
+
split_tag(@options[:by_tags],v)
|
74
|
+
end
|
75
|
+
|
76
|
+
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15") do |v|
|
77
|
+
@options[:backoff_limit] = v
|
78
|
+
end
|
79
|
+
|
80
|
+
o.on("--create-snapshot STRING", "Use this option if you want to create a snapshot") do |v|
|
81
|
+
@options[:create_snapshot] = v
|
82
|
+
end
|
83
|
+
|
84
|
+
@time_periods.keys.sort { |a, b| @time_periods[a][:seconds] <=> @time_periods[b][:seconds] }.each do |period|
|
85
|
+
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
86
|
+
@time_periods[period][:keep] = v
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
91
|
+
@options[:keep_last] = true
|
92
|
+
end
|
93
|
+
|
94
|
+
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
95
|
+
@options[:dry_run] = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def split_tag(hash,v)
|
100
|
+
v.split(',').each do |pair|
|
101
|
+
tag, value = pair.split('=',2)
|
102
|
+
if value.nil?
|
103
|
+
raise InvalidArgument, "invalid tag=value format"
|
104
|
+
end
|
105
|
+
hash[tag] = value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'aws-sdk-rds'
|
3
|
+
|
4
|
+
class RdsRotateDbSnapshots
|
5
|
+
class RdsClient
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@client, :describe_db_snapshots, :create_db_snapshot, :delete_db_snapshot
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
Aws.config.update(
|
12
|
+
access_key_id: options[:aws_access_key],
|
13
|
+
secret_access_key: options[:aws_secret_access_key],
|
14
|
+
region: options[:aws_region],
|
15
|
+
session_token: options[:aws_session_token]
|
16
|
+
)
|
17
|
+
@client = Aws::RDS::Client.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require_relative 'rds_rotate_db_snapshots/actions'
|
3
|
+
require_relative 'rds_rotate_db_snapshots/action_wrappers'
|
4
|
+
require_relative 'rds_rotate_db_snapshots/options_parser'
|
5
|
+
require_relative 'rds_rotate_db_snapshots/rds_client'
|
6
|
+
|
7
|
+
class RdsRotateDbSnapshots
|
8
|
+
extend RdsRotateDbSnapshots::ActionWrappers
|
9
|
+
include RdsRotateDbSnapshots::Actions
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
with_backoff :get_db_snapshots, :create_snapshot, :rotate_em
|
13
|
+
|
14
|
+
def initialize(script_name: nil, cli: false, options: {})
|
15
|
+
@script_name = script_name
|
16
|
+
@options = options
|
17
|
+
@cli = cli
|
18
|
+
parse_options if cli?
|
19
|
+
@backoff_counter = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def rds_client
|
23
|
+
@rds_client ||= RdsRotateDbSnapshots::RdsClient.new(@options)
|
24
|
+
end
|
25
|
+
alias_method :client, :rds_client
|
26
|
+
|
27
|
+
def reset_backoff
|
28
|
+
@backoff_counter = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def time_periods
|
32
|
+
@options[:time_periods]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def cli?
|
38
|
+
!!@cli
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_options
|
42
|
+
@options = RdsRotateDbSnapshots::OptionsParser.new(script_name: @script_name, cli: @cli).parse!
|
43
|
+
end
|
44
|
+
|
45
|
+
def backoff
|
46
|
+
@backoff_counter = @backoff_counter + 1
|
47
|
+
|
48
|
+
# TODO: re-work
|
49
|
+
if options && options[:backoff_limit] > 0 && options[:backoff_limit] < @backoff_counter
|
50
|
+
puts "Too many backoff attempts. Sorry it didn't work out."
|
51
|
+
exit 2
|
52
|
+
end
|
53
|
+
|
54
|
+
naptime = rand(60) * @backoff_counter
|
55
|
+
puts "Backing off for #{naptime} seconds..."
|
56
|
+
sleep naptime
|
57
|
+
end
|
58
|
+
end
|
@@ -2,36 +2,43 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: rds-rotate-db-snapshots 0.
|
5
|
+
# stub: rds-rotate-db-snapshots 0.5.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "rds-rotate-db-snapshots".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.5.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Siarhei Kavaliou".freeze]
|
14
|
-
s.date = "
|
14
|
+
s.date = "2023-01-10"
|
15
15
|
s.description = "Provides a simple way to rotate RDS DB snapshots with configurable retention periods.".freeze
|
16
16
|
s.email = "kovserg@gmail.com".freeze
|
17
17
|
s.executables = ["rds-rotate-db-snapshots".freeze]
|
18
18
|
s.extra_rdoc_files = [
|
19
19
|
"LICENSE.txt",
|
20
|
-
"README.
|
20
|
+
"README.md"
|
21
21
|
]
|
22
22
|
s.files = [
|
23
23
|
".document",
|
24
|
-
".
|
24
|
+
".github/dependabot.yml",
|
25
|
+
".github/workflows/ci.yml",
|
26
|
+
".github/workflows/codeql.yml",
|
27
|
+
".rspec",
|
25
28
|
"Gemfile",
|
26
29
|
"LICENSE.txt",
|
27
|
-
"README.
|
30
|
+
"README.md",
|
28
31
|
"Rakefile",
|
29
32
|
"VERSION",
|
30
33
|
"bin/rds-rotate-db-snapshots",
|
31
|
-
"lib
|
34
|
+
"lib/rds_rotate_db_snapshots.rb",
|
35
|
+
"lib/rds_rotate_db_snapshots/action_wrappers.rb",
|
36
|
+
"lib/rds_rotate_db_snapshots/actions.rb",
|
37
|
+
"lib/rds_rotate_db_snapshots/options_parser.rb",
|
38
|
+
"lib/rds_rotate_db_snapshots/rds_client.rb",
|
32
39
|
"rds-rotate-db-snapshots.gemspec",
|
33
|
-
"
|
34
|
-
"
|
40
|
+
"spec/helper.rb",
|
41
|
+
"spec/rds_rotate_db_snapshots_spec.rb"
|
35
42
|
]
|
36
43
|
s.homepage = "http://github.com/serg-kovalev/rds-rotate-db-snapshots".freeze
|
37
44
|
s.licenses = ["MIT".freeze]
|
@@ -44,14 +51,16 @@ Gem::Specification.new do |s|
|
|
44
51
|
|
45
52
|
if s.respond_to? :add_runtime_dependency then
|
46
53
|
s.add_runtime_dependency(%q<aws-sdk-rds>.freeze, ["~> 1"])
|
47
|
-
s.
|
48
|
-
s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
|
54
|
+
s.add_runtime_dependency(%q<rake>.freeze, [">= 0"])
|
49
55
|
s.add_development_dependency(%q<juwelier>.freeze, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<pry>.freeze, [">= 0"])
|
57
|
+
s.add_development_dependency(%q<pry-byebug>.freeze, [">= 0"])
|
50
58
|
else
|
51
59
|
s.add_dependency(%q<aws-sdk-rds>.freeze, ["~> 1"])
|
52
|
-
s.add_dependency(%q<
|
53
|
-
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
60
|
+
s.add_dependency(%q<rake>.freeze, [">= 0"])
|
54
61
|
s.add_dependency(%q<juwelier>.freeze, [">= 0"])
|
62
|
+
s.add_dependency(%q<pry>.freeze, [">= 0"])
|
63
|
+
s.add_dependency(%q<pry-byebug>.freeze, [">= 0"])
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
$TESTING = true
|
2
|
+
|
3
|
+
require "simplecov"
|
4
|
+
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter "/spec"
|
7
|
+
# minimum_coverage(70)
|
8
|
+
|
9
|
+
if ENV['CI']
|
10
|
+
require 'simplecov-lcov'
|
11
|
+
|
12
|
+
SimpleCov::Formatter::LcovFormatter.config do |c|
|
13
|
+
c.report_with_single_file = true
|
14
|
+
c.single_report_path = 'coverage/lcov.info'
|
15
|
+
end
|
16
|
+
|
17
|
+
formatter SimpleCov::Formatter::LcovFormatter
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
22
|
+
require "rds_rotate_db_snapshots"
|
23
|
+
|
24
|
+
require "rdoc"
|
25
|
+
require "rspec"
|
26
|
+
require "diff/lcs" # You need diff/lcs installed to run specs.
|
27
|
+
# require 'stringio'
|
28
|
+
require "webmock/rspec"
|
29
|
+
|
30
|
+
WebMock.disable_net_connect!(:allow => "coveralls.io")
|
31
|
+
|
32
|
+
$0 = "rds_rotate_db_snapshots"
|
33
|
+
ARGV.clear
|
34
|
+
|
35
|
+
RSpec.configure do |config|
|
36
|
+
config.before do
|
37
|
+
ARGV.replace []
|
38
|
+
end
|
39
|
+
|
40
|
+
config.expect_with :rspec do |c|
|
41
|
+
c.syntax = :expect
|
42
|
+
end
|
43
|
+
|
44
|
+
# def capture(stream)
|
45
|
+
# begin
|
46
|
+
# stream = stream.to_s
|
47
|
+
# eval "$#{stream} = StringIO.new"
|
48
|
+
# yield
|
49
|
+
# result = eval("$#{stream}").string
|
50
|
+
# ensure
|
51
|
+
# eval("$#{stream} = #{stream.upcase}")
|
52
|
+
# end
|
53
|
+
|
54
|
+
# result
|
55
|
+
# end
|
56
|
+
|
57
|
+
def source_root
|
58
|
+
File.join(File.dirname(__FILE__), "fixtures")
|
59
|
+
end
|
60
|
+
|
61
|
+
def destination_root
|
62
|
+
File.join(File.dirname(__FILE__), "sandbox")
|
63
|
+
end
|
64
|
+
|
65
|
+
def silence_warnings
|
66
|
+
old_verbose = $VERBOSE
|
67
|
+
$VERBOSE = nil
|
68
|
+
yield
|
69
|
+
ensure
|
70
|
+
$VERBOSE = old_verbose
|
71
|
+
end
|
72
|
+
|
73
|
+
# true if running on windows, used for conditional spec skips
|
74
|
+
#
|
75
|
+
# @return [TrueClass/FalseClass]
|
76
|
+
def windows?
|
77
|
+
Gem.win_platform?
|
78
|
+
end
|
79
|
+
|
80
|
+
# alias silence capture
|
81
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe RdsRotateDbSnapshots do
|
4
|
+
subject { described_class.new(script_name: script_name, cli: cli) }
|
5
|
+
|
6
|
+
let(:script_name) { "test" }
|
7
|
+
let(:cli) { true }
|
8
|
+
before do
|
9
|
+
allow(Aws::RDS::Client).to receive(:new)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "on include" do
|
13
|
+
it "adds action methods to the base class" do
|
14
|
+
expect(described_class.instance_methods).to include(:rotate_em)
|
15
|
+
expect(described_class.instance_methods).to include(:create_snapshot)
|
16
|
+
expect(described_class.instance_methods).to include(:get_db_snapshots)
|
17
|
+
expect(described_class.instance_methods).to include(:rotate_by_tags)
|
18
|
+
expect(described_class.instance_methods).to include(:client)
|
19
|
+
expect(described_class.instance_methods).to include(:time_periods)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#client" do
|
24
|
+
it "returns an RdsClient" do
|
25
|
+
expect(subject.client).to be_a(RdsRotateDbSnapshots::RdsClient)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#rds_client" do
|
30
|
+
it "returns an RdsClient" do
|
31
|
+
expect(subject.rds_client).to be_a(RdsRotateDbSnapshots::RdsClient)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#reset_backoff" do
|
36
|
+
it "resets backoff counter" do
|
37
|
+
subject.instance_variable_set(:@backoff_counter, 1)
|
38
|
+
subject.reset_backoff
|
39
|
+
expect(subject.instance_variable_get(:@backoff_counter)).to eq(0)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#time_periods" do
|
44
|
+
it "returns time periods" do
|
45
|
+
expect(subject.time_periods).to eq(
|
46
|
+
:daily=>{:format=>"%Y-%m-%d", :keep=>0, :keeping=>{}, :seconds=>86400},
|
47
|
+
:hourly => {:format=>"%Y-%m-%d-%H", :keep=>0, :keeping=>{}, :seconds=>3600},
|
48
|
+
:monthly => {:format=>"%Y-%m", :keep=>0, :keeping=>{}, :seconds=>2592000},
|
49
|
+
:weekly => {:format=>"%Y-%W", :keep=>0, :keeping=>{}, :seconds=>604800},
|
50
|
+
:yearly=>{:format=>"%Y", :keep=>0, :keeping=>{}, :seconds=>31104000}
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#backoff" do
|
56
|
+
it "backs off" do
|
57
|
+
subject.instance_variable_set(:@backoff_counter, 1)
|
58
|
+
expect(subject).to receive(:sleep)
|
59
|
+
subject.send(:backoff)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rds-rotate-db-snapshots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Siarhei Kavaliou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-rds
|
@@ -25,7 +25,21 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: juwelier
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
@@ -39,7 +53,7 @@ dependencies:
|
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
56
|
+
name: pry
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - ">="
|
@@ -53,7 +67,7 @@ dependencies:
|
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: pry-byebug
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - ">="
|
@@ -74,20 +88,27 @@ executables:
|
|
74
88
|
extensions: []
|
75
89
|
extra_rdoc_files:
|
76
90
|
- LICENSE.txt
|
77
|
-
- README.
|
91
|
+
- README.md
|
78
92
|
files:
|
79
93
|
- ".document"
|
80
|
-
- ".
|
94
|
+
- ".github/dependabot.yml"
|
95
|
+
- ".github/workflows/ci.yml"
|
96
|
+
- ".github/workflows/codeql.yml"
|
97
|
+
- ".rspec"
|
81
98
|
- Gemfile
|
82
99
|
- LICENSE.txt
|
83
|
-
- README.
|
100
|
+
- README.md
|
84
101
|
- Rakefile
|
85
102
|
- VERSION
|
86
103
|
- bin/rds-rotate-db-snapshots
|
87
|
-
- lib
|
104
|
+
- lib/rds_rotate_db_snapshots.rb
|
105
|
+
- lib/rds_rotate_db_snapshots/action_wrappers.rb
|
106
|
+
- lib/rds_rotate_db_snapshots/actions.rb
|
107
|
+
- lib/rds_rotate_db_snapshots/options_parser.rb
|
108
|
+
- lib/rds_rotate_db_snapshots/rds_client.rb
|
88
109
|
- rds-rotate-db-snapshots.gemspec
|
89
|
-
-
|
90
|
-
-
|
110
|
+
- spec/helper.rb
|
111
|
+
- spec/rds_rotate_db_snapshots_spec.rb
|
91
112
|
homepage: http://github.com/serg-kovalev/rds-rotate-db-snapshots
|
92
113
|
licenses:
|
93
114
|
- MIT
|
data/.travis.yml
DELETED
data/README.rdoc
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
= rds-rotate-db-snapshots
|
2
|
-
{<img src="https://badge.fury.io/rb/rds-rotate-db-snapshots.svg" alt="Gem Version" />}[http://badge.fury.io/rb/rds-rotate-db-snapshots] {<img src="https://travis-ci.org/serg-kovalev/rds-rotate-db-snapshots.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/serg-kovalev/rds-rotate-db-snapshots] {<img src="https://hakiri.io/github/serg-kovalev/rds-rotate-db-snapshots/master.svg" alt="security" />}[https://hakiri.io/github/serg-kovalev/rds-rotate-db-snapshots/master]
|
3
|
-
|
4
|
-
Provides a simple way to rotate db snapshots in Amazon Relational Database Service (RDS).
|
5
|
-
|
6
|
-
== Tested on Rubies
|
7
|
-
- 1.9.3
|
8
|
-
- 2.1
|
9
|
-
- 2.2
|
10
|
-
- jruby
|
11
|
-
- rbx-2
|
12
|
-
|
13
|
-
== Usage
|
14
|
-
|
15
|
-
[Gem installation]
|
16
|
-
gem install rds-rotate-db-snapshots
|
17
|
-
|
18
|
-
[Usage]
|
19
|
-
rds-rotate-db-snapshots [options] <db_indentifier>
|
20
|
-
|
21
|
-
Add this script to CRON (let's say it will run this script every X hours) and it will do the job well
|
22
|
-
#/usr/bin/bash
|
23
|
-
AWS_ACCESS_KEY='xxxxxxxxxxxxxxxxxxxx'
|
24
|
-
AWS_SECRET_ACCESS_KEY='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
|
25
|
-
AWS_REGION='eu-west-1'
|
26
|
-
DESCRIPTION_PREFIX='automatic-backup-'
|
27
|
-
RDS_ROTATOR=/here/is/the/path/to/rds-rotate-db-snapshots
|
28
|
-
DB_NAME='db_name_here'
|
29
|
-
|
30
|
-
$RDS_ROTATOR --aws-region $AWS_REGION --aws-access-key $AWS_ACCESS_KEY --aws-secret-access-key $AWS_SECRET_ACCESS_KEY --pattern $DESCRIPTION_PREFIX --keep-hourly 24 --keep-daily 7 --keep-weekly 4 --keep-monthly 1 --keep-yearly 0 --create-snapshot $DESCRIPTION_PREFIX$DB_NAME $DB_NAME
|
31
|
-
|
32
|
-
== Options
|
33
|
-
|
34
|
-
[--aws-access-key ACCESS_KEY] "AWS Access Key"
|
35
|
-
[--aws-secret-access-key SECRET_KEY] "AWS Secret Access Key"
|
36
|
-
[--aws-region REGION] "AWS Region"
|
37
|
-
[--pattern STRING] "Snapshots without this string in the description will be ignored"
|
38
|
-
[--by-tags TAG=VALUE,TAG=VALUE] "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs."
|
39
|
-
[--backoff-limit INTEGER] "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15"
|
40
|
-
[--create-snapshot STRING] "Use this option if you want to create a snapshot"
|
41
|
-
[--keep-hourly INTEGER] "Number of hourly snapshots to keep"
|
42
|
-
[--keep-daily INTEGER] "Number of daily snapshots to keep"
|
43
|
-
[--keep-weekly INTEGER] "Number of weekly snapshots to keep"
|
44
|
-
[--keep-last] "Keep the most recent snapshot, regardless of time-based policy"
|
45
|
-
[--dry-run] "Shows what would happen without doing anything"
|
46
|
-
|
47
|
-
== Tips
|
48
|
-
|
49
|
-
If you are not sure what happen - add option "dry-run".
|
50
|
-
|
51
|
-
In that case the script will not destroy/create anything in RDS, it will just show the messages.
|
52
|
-
|
53
|
-
== Contributing to rds-rotate-db-snapshots
|
54
|
-
|
55
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
56
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
57
|
-
* Fork the project
|
58
|
-
* Start a feature/bugfix branch
|
59
|
-
* Commit and push until you are happy with your contribution
|
60
|
-
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
61
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
62
|
-
|
63
|
-
== Copyright
|
64
|
-
|
65
|
-
Copyright (c) 2014 Siarhei Kavaliou. See LICENSE.txt for further details.
|
data/lib/.empty
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Juwelier doesn't like me not having a lib/ directory.
|
data/test/helper.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'bundler'
|
3
|
-
require 'minitest'
|
4
|
-
require 'shoulda'
|
5
|
-
|
6
|
-
begin
|
7
|
-
Bundler.setup(:default, :development)
|
8
|
-
rescue Bundler::BundlerError => e
|
9
|
-
$stderr.puts e.message
|
10
|
-
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
-
exit e.status_code
|
12
|
-
end
|
13
|
-
|
14
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
15
|
-
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
|
-
|
17
|
-
class Minitest::Test
|
18
|
-
end
|