appshot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/Gemfile +19 -0
- data/Guardfile +19 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/README.rdoc +20 -0
- data/Rakefile +44 -0
- data/appshot.conf.example +7 -0
- data/appshot.gemspec +32 -0
- data/bin/appshot +32 -0
- data/features/appshot.feature +55 -0
- data/features/step_definitions/appshot_steps.rb +3 -0
- data/features/support/env.rb +18 -0
- data/lib/appshot/app/mysql.rb +43 -0
- data/lib/appshot/app.rb +1 -0
- data/lib/appshot/filesystem/dm.rb +53 -0
- data/lib/appshot/filesystem.rb +1 -0
- data/lib/appshot/version.rb +3 -0
- data/lib/appshot/volume/ebs_prune.rb +38 -0
- data/lib/appshot/volume/ebs_snapshot.rb +34 -0
- data/lib/appshot/volume/ebs_volume.rb +65 -0
- data/lib/appshot/volume.rb +7 -0
- data/lib/appshot.rb +93 -0
- data/rvmrc.example +52 -0
- data/spec/lib/appshot/app/mysql_spec.rb +37 -0
- data/spec/lib/appshot/filesystem/dm_spec.rb +39 -0
- data/spec/lib/appshot/volume/ebs_prune_spec.rb +39 -0
- data/spec/lib/appshot/volume/ebs_snapshot_spec.rb +43 -0
- data/spec/lib/appshot/volume/ebs_volume_spec.rb +125 -0
- data/spec/lib/appshot_spec.rb +64 -0
- data/spec/spec_helper.rb +7 -0
- metadata +304 -0
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.DS_Store
|
4
|
+
.bundle
|
5
|
+
.config
|
6
|
+
.rvmrc
|
7
|
+
.yardoc
|
8
|
+
Gemfile.lock
|
9
|
+
InstalledFiles
|
10
|
+
_yardoc
|
11
|
+
coverage
|
12
|
+
doc/
|
13
|
+
html
|
14
|
+
lib/bundler/man
|
15
|
+
pkg
|
16
|
+
rdoc
|
17
|
+
results.html
|
18
|
+
spec/reports
|
19
|
+
test/tmp
|
20
|
+
test/version_tmp
|
21
|
+
tmp
|
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in appshot.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem "awesome_print"
|
8
|
+
gem "fivemat"
|
9
|
+
gem "growl"
|
10
|
+
gem "guard"
|
11
|
+
gem "guard-bundler"
|
12
|
+
gem "guard-rspec"
|
13
|
+
gem "guard-cucumber"
|
14
|
+
gem "rb-fsevent"
|
15
|
+
gem "ruby_gntp"
|
16
|
+
gem "rb-readline"
|
17
|
+
gem "debugger", :platforms => :mri_19 if RUBY_VERSION >= "1.9.3"
|
18
|
+
gem "yard"
|
19
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
watch(/^.+\.gemspec/)
|
7
|
+
end
|
8
|
+
|
9
|
+
guard 'rspec', :version => 2, :cli=> "--format Fivemat --debugger" do
|
10
|
+
watch(%r{^spec/.+_spec\.rb$})
|
11
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
12
|
+
watch('spec/spec_helper.rb') { "spec" }
|
13
|
+
end
|
14
|
+
|
15
|
+
guard 'cucumber' do
|
16
|
+
watch(%r{^features/.+\.feature$})
|
17
|
+
watch(%r{^features/support/.+$}) { 'features' }
|
18
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
|
19
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Morgan Nelson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Appshot
|
2
|
+
|
3
|
+
AppShot takes consistent snapshots of your server volumes using pluggable modules for applications, filesystems, and storage providers. One issue with taking snapshots of storage volumes is that without fundamental orchestration at both the application and filesystem level, you are almost certainly not going to get a consistent snapshot. In-flight data in application and kernel caches will likely be missed, and while you think you are making good backups of your data, when the time comes, you realize that it doesn't work.
|
4
|
+
|
5
|
+
The goal for AppShot is to provide a framework for creating pluggable providers for application pausing/restarting, filesystem freezeing/thawing and volume management, whether local (LVM, et. al.) or cloud (Amazon EBS, et. al.). The software isn't quite there yet, but progress is being made.
|
6
|
+
|
7
|
+
Currently only supports some Device Mapper filesystems for freezing (ext4, XFS), Mysql as an application, and Amazon EBS as the volume store. More on the way.
|
8
|
+
|
9
|
+
Support for ext3, ReiserFS and JFS in filesystem freezing should be a trivial method alias, but I haven't had time to test them. Filesystem freezing is unnecessary when using LMV2's volume snapshot capability (the underlying device-mapper provides this functionality for free), but I haven't implemented LVM2 snapshots yet.
|
10
|
+
|
11
|
+
As always, patches welcome. I don't have much access or experience with anything but linux or OS X, so patches for any flavor of BSD or (Open)Solaris gratefully accepted.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
$ gem install appshot
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
Setup your appshot.conf file and run appshot.
|
20
|
+
|
21
|
+
$ appshot
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/README.rdoc
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
= appshot - DESCRIBE YOUR GEM
|
2
|
+
|
3
|
+
Author:: Morgan Nelson (morgan\.nelson@gmail.com)
|
4
|
+
Copyright:: Copyright (c) 2012 Morgan Nelson
|
5
|
+
|
6
|
+
|
7
|
+
License:: mit, see LICENSE.txt
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
Appshot coordinates application, filesystem, and storage volume operations to give you consistent snapshots.
|
12
|
+
|
13
|
+
== Links
|
14
|
+
|
15
|
+
* {Source on Github}[http://github.com/korishev/appshot]
|
16
|
+
|
17
|
+
== Install
|
18
|
+
|
19
|
+
gem install appshot
|
20
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/clean'
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
require 'cucumber'
|
7
|
+
require 'cucumber/rake/task'
|
8
|
+
gem 'rdoc' # we need the installed RDoc gem, not the system one
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
include Rake::DSL
|
12
|
+
|
13
|
+
Bundler::GemHelper.install_tasks
|
14
|
+
|
15
|
+
|
16
|
+
RSpec::Core::RakeTask.new do |t|
|
17
|
+
# Put spec opts in a file named .rspec in root
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
CUKE_RESULTS = 'results.html'
|
22
|
+
CLEAN << CUKE_RESULTS
|
23
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
24
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x"
|
25
|
+
t.fork = false
|
26
|
+
end
|
27
|
+
|
28
|
+
Rake::RDocTask.new do |rd|
|
29
|
+
|
30
|
+
rd.main = "README.rdoc"
|
31
|
+
|
32
|
+
rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
33
|
+
end
|
34
|
+
|
35
|
+
task :default => [:spec,:features]
|
36
|
+
|
37
|
+
|
38
|
+
desc "Open an irb session preloaded with Appshot"
|
39
|
+
|
40
|
+
task :console do
|
41
|
+
sh "irb -rrubygems -I lib -r appshot.rb"
|
42
|
+
end
|
43
|
+
|
44
|
+
task :c => :console
|
@@ -0,0 +1,7 @@
|
|
1
|
+
appshot "mysql_userdb" do
|
2
|
+
mysql name: "userdb", port: 1536, username: "pooky", password: "bear"
|
3
|
+
xfs mount_point: "/mnt/untitled"
|
4
|
+
ebs_snapshot volume_id: "vol-some_id", aws_access_key_id: "BOOM", aws_secret_access_key: "KEEP_IT_SECRET"
|
5
|
+
ebs_prune volume_id: "vol-some_id", snapshots_to_keep: 3, minimum_retention_days: 0, aws_access_key_id: "BOOM", aws_secret_access_key: "KEEP_IT_SECRET"
|
6
|
+
end
|
7
|
+
|
data/appshot.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/appshot/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Morgan Nelson"]
|
6
|
+
gem.email = ["mnelson@steele-forge.com"]
|
7
|
+
gem.description = %q{AppShot takes an application aware snapshot of your drive volume (one day) using pluggable modules representing actions to be taken concerning applications, filesystems, cloud providers, etc.}
|
8
|
+
gem.summary = %q{Appshot takes consistent snapshots of drive volumes.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "appshot"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Appshot::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency "fog", "~> 1.4.0"
|
19
|
+
gem.add_runtime_dependency "methadone", "~> 1.2.1"
|
20
|
+
gem.add_runtime_dependency "open4", "~> 1.3.0"
|
21
|
+
gem.add_runtime_dependency "mysql2", "~> 0.3.11"
|
22
|
+
|
23
|
+
gem.add_development_dependency "aruba"
|
24
|
+
gem.add_development_dependency "bundler", "~> 1.0"
|
25
|
+
gem.add_development_dependency "fabrication", "~> 2.0.1"
|
26
|
+
gem.add_development_dependency "pry"
|
27
|
+
gem.add_development_dependency "rake", "~> 0.9.0"
|
28
|
+
gem.add_development_dependency "rake", "~> 0.9.2"
|
29
|
+
gem.add_development_dependency "rdoc"
|
30
|
+
gem.add_development_dependency "rspec", "~> 2.6"
|
31
|
+
gem.add_development_dependency "timecop", "~> 0.3.5"
|
32
|
+
end
|
data/bin/appshot
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'methadone'
|
5
|
+
require 'appshot'
|
6
|
+
|
7
|
+
class App
|
8
|
+
include Methadone::Main
|
9
|
+
include Methadone::CLILogging
|
10
|
+
|
11
|
+
main do |*appshot_names|
|
12
|
+
config_file_name = options["config_file"] || "/etc/appshot/appshot.cfg"
|
13
|
+
args = { :appshot_names => appshot_names }
|
14
|
+
|
15
|
+
appshot = Appshot.new(File.read(config_file_name))
|
16
|
+
|
17
|
+
appshot.run_pass(options, args)
|
18
|
+
end
|
19
|
+
|
20
|
+
version Appshot::VERSION
|
21
|
+
description "Use Appshot to take consistent snapshots of your data volumes"
|
22
|
+
|
23
|
+
use_log_level_option
|
24
|
+
|
25
|
+
on("-c CONFIG_FILE","--config_file", "Configuration File", /\S+/)
|
26
|
+
on("-l", "--list_appshots", "--list-appshots", "List known appshots", /\S+/)
|
27
|
+
on("-p", "Pretend mode", /\S+/)
|
28
|
+
|
29
|
+
arg :appshot_names, :any, "The name of one or more appshots as defined in your config file"
|
30
|
+
|
31
|
+
go!
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
Feature: Appshot CLI mode
|
2
|
+
In order to take consistent snapshots of my data volumes
|
3
|
+
I want to check my application parameters
|
4
|
+
So I have the options I need
|
5
|
+
|
6
|
+
Scenario: App displays help when asked
|
7
|
+
When I get help for "appshot"
|
8
|
+
Then the exit status should be 0
|
9
|
+
And the banner should be present
|
10
|
+
And the banner should document that this app takes options
|
11
|
+
And the following options should be documented:
|
12
|
+
|--version |
|
13
|
+
|-c |
|
14
|
+
|--config_file |
|
15
|
+
|-l |
|
16
|
+
|--list_appshots|
|
17
|
+
|-p |
|
18
|
+
And the banner should document that this app's arguments are:
|
19
|
+
|appshot_names...| optional|
|
20
|
+
|
21
|
+
Scenario: User asks for list of appshots from empty file
|
22
|
+
Given an empty file named "/tmp/empty.cfg"
|
23
|
+
When I successfully run `appshot -c /tmp/empty.cfg --list`
|
24
|
+
Then the output should contain "There are no appshots configured"
|
25
|
+
|
26
|
+
Scenario: User asks for list of appshots from file with one appshot
|
27
|
+
Given a file named "/tmp/one_appshot.cfg" with:
|
28
|
+
"""
|
29
|
+
appshot "mysql_userdb" do
|
30
|
+
mysql name: "userdb", port: 1536, username: "pooky"
|
31
|
+
ebs_snapshot volume_id: "vol-4ed40599", aws_access_key_id: "BOO", aws_secret_access_key: "GAH"
|
32
|
+
ebs_prune snapshots_to_keep: 15, minimum_retention_days: 5
|
33
|
+
end
|
34
|
+
"""
|
35
|
+
When I successfully run `appshot -c /tmp/one_appshot.cfg --list`
|
36
|
+
Then the output should contain "There is one appshot configured"
|
37
|
+
|
38
|
+
Scenario: User asks for list of appshots from file with multiple appshots
|
39
|
+
Given a file named "/tmp/one_appshot.cfg" with:
|
40
|
+
"""
|
41
|
+
appshot "mysql_userdb" do
|
42
|
+
mysql name: "userdb", port: 1536, username: "pooky"
|
43
|
+
ebs_snapshot volume_id: "vol-4ed40599", aws_access_key_id: "BOO", aws_secret_access_key: "GAH"
|
44
|
+
ebs_prune snapshots_to_keep: 15, minimum_retention_days: 5
|
45
|
+
end
|
46
|
+
|
47
|
+
appshot "mysql_datadb" do
|
48
|
+
mysql name: "datadb", port: 1536, username: "pooky"
|
49
|
+
ebs_snapshot volume_id: "vol-ded06738", aws_access_key_id: "BOO", aws_secret_access_key: "GAH"
|
50
|
+
ebs_prune snapshots_to_keep: 15, minimum_retention_days: 5
|
51
|
+
end
|
52
|
+
"""
|
53
|
+
When I successfully run `appshot -c /tmp/one_appshot.cfg --list`
|
54
|
+
Then the output should contain "There are 2 appshots configured"
|
55
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'methadone/cucumber'
|
3
|
+
require 'debugger'
|
4
|
+
require 'pry'
|
5
|
+
|
6
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
7
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
8
|
+
|
9
|
+
Before do
|
10
|
+
# Using "announce" causes massive warnings on 1.9.2
|
11
|
+
@puts = true
|
12
|
+
@original_rubylib = ENV['RUBYLIB']
|
13
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
After do
|
17
|
+
ENV['RUBYLIB'] = @original_rubylib
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
require 'methadone'
|
3
|
+
|
4
|
+
class Appshot
|
5
|
+
class Mysql
|
6
|
+
include Methadone::CLILogging
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
@host = opts[:hostname] || "localhost"
|
10
|
+
@name = opts[:name] || "mysql"
|
11
|
+
@port = opts[:port] || 3306
|
12
|
+
@user = opts[:username] || "mysql"
|
13
|
+
@password = opts[:password]
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(call_chain)
|
17
|
+
next_action = call_chain.shift
|
18
|
+
@client = Mysql2::Client.new(username: @user, hostname: @host, password: @password, port: @port, database: @name)
|
19
|
+
|
20
|
+
lock
|
21
|
+
next_action.call(call_chain) unless next_action.nil?
|
22
|
+
unlock
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def lock
|
28
|
+
begin
|
29
|
+
Timeout::timeout(20) do
|
30
|
+
@client.query("FLUSH TABLES WITH READ LOCK")
|
31
|
+
end
|
32
|
+
rescue
|
33
|
+
error "Could not get MySQL tables flushed and locked before timeout."
|
34
|
+
@client.query("UNLOCK TABLES")
|
35
|
+
@client.close
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def unlock
|
40
|
+
@client.query("UNLOCK TABLES")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/appshot/app.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'app/mysql'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class Appshot
|
4
|
+
class DM
|
5
|
+
def initialize(opts={})
|
6
|
+
@mount_point = Pathname.new(opts[:mount_point] || "")
|
7
|
+
validate_mount_point(@mount_point)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(call_chain=[])
|
11
|
+
next_action = call_chain.shift
|
12
|
+
|
13
|
+
freeze
|
14
|
+
next_action.call(call_chain) unless next_action.nil?
|
15
|
+
unfreeze
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate_mount_point(mount_point)
|
21
|
+
raise "Your mount point: '#{mount_point}' is not a mount point!" unless @mount_point.mountpoint?
|
22
|
+
raise "We cannot currently unfreeze the root filesystem, leaving your system unusable. Aborting." if @mount_point.to_s == "/"
|
23
|
+
end
|
24
|
+
|
25
|
+
def mount_point
|
26
|
+
@mount_point.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def freeze_command
|
30
|
+
%x{ /sbin/fsfreeze -f #{mount_point} }
|
31
|
+
end
|
32
|
+
|
33
|
+
def unfreeze_command
|
34
|
+
%x{ /sbin/fsfreeze -u #{mount_point} }
|
35
|
+
end
|
36
|
+
|
37
|
+
def freeze
|
38
|
+
begin
|
39
|
+
freeze_command
|
40
|
+
rescue
|
41
|
+
puts "There was a problem freezing your mount point: #{mount_point}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def unfreeze
|
46
|
+
unfreeze_command
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_mount_point
|
50
|
+
mounts = %x{ df }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'filesystem/dm'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Appshot
|
2
|
+
class EBS_Prune
|
3
|
+
def initialize(opts={})
|
4
|
+
@volume_id = opts[:volume_id] || ""
|
5
|
+
@snapshots_to_keep = opts[:snapshots_to_keep] || 5
|
6
|
+
@minimum_retention_days = opts[:minimum_retention_days] || 3
|
7
|
+
@region = opts[:region] unless opts[:region].nil?
|
8
|
+
@aws_secret_access_key = opts[:aws_secret_access_key] || ""
|
9
|
+
@aws_access_key_id = opts[:aws_access_key_id] || ""
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(call_chain=[])
|
13
|
+
next_action = call_chain.shift
|
14
|
+
prune_snapshots if valid?
|
15
|
+
next_action.call(call_chain) unless next_action.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid?
|
19
|
+
raise ArgumentError.new "volume_id must be specified for an ebs_prune" if @volume_id.empty?
|
20
|
+
raise ArgumentError.new "aws_access_key_id must be specified for an ebs_prune" if @aws_access_key_id.empty?
|
21
|
+
raise ArgumentError.new "aws_secret_access_key must be specified for an ebs_prune" if @aws_secret_access_key.empty?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def ebs_volume
|
28
|
+
options = { aws_access_key_id: @aws_access_key_id, aws_secret_access_key: @aws_secret_access_key }
|
29
|
+
options.update( { region: @region } ) if @region
|
30
|
+
Appshot::EBS_Volume.new(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def prune_snapshots
|
34
|
+
not_after_time = Time.now - (@minimum_retention_days * 86400)
|
35
|
+
ebs_volume.prune_snapshots(@volume_id, snapshots_to_keep: @snapshots_to_keep, not_after_time: not_after_time )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'ebs_volume'
|
2
|
+
|
3
|
+
class Appshot
|
4
|
+
class EBS_Snapshot
|
5
|
+
def initialize(opts={})
|
6
|
+
@volume_id = opts[:volume_id] || ""
|
7
|
+
@region = opts[:region] unless opts[:region].nil?
|
8
|
+
@aws_secret_access_key = opts[:aws_secret_access_key] || ""
|
9
|
+
@aws_access_key_id = opts[:aws_access_key_id] || ""
|
10
|
+
@description = opts[:description] || "Standard Snapshot #{Time.now.utc}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(call_chain=[])
|
14
|
+
next_action = call_chain.shift
|
15
|
+
ebs_volume.snap(@volume_id, @description) if valid?
|
16
|
+
next_action.call(call_chain) unless next_action.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
raise ArgumentError.new "volume_id must be specified for an ebs_snapshot" if @volume_id.empty?
|
21
|
+
raise ArgumentError.new "aws_access_key_id must be specified for an ebs_snapshot" if @aws_access_key_id.empty?
|
22
|
+
raise ArgumentError.new "aws_secret_access_key must be specified for an ebs_snapshot" if @aws_secret_access_key.empty?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ebs_volume
|
29
|
+
options = { aws_access_key_id: @aws_access_key_id, aws_secret_access_key: @aws_secret_access_key }
|
30
|
+
options.update( { region: @region } ) if @region
|
31
|
+
Appshot::EBS_Volume.new(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'awesome_print'
|
3
|
+
|
4
|
+
class Appshot
|
5
|
+
class EBS_Volume
|
6
|
+
include Fog
|
7
|
+
|
8
|
+
attr_accessor :volume_id, :region
|
9
|
+
attr_writer :aws_access_key_id, :aws_secret_access_key
|
10
|
+
attr_reader :provider
|
11
|
+
|
12
|
+
|
13
|
+
def initialize(opts = {})
|
14
|
+
@provider = opts[:provider] || 'AWS'
|
15
|
+
@region = opts[:region] || "us-east-1"
|
16
|
+
@aws_access_key_id = opts[:aws_access_key_id] unless opts[:aws_access_key_id].nil?
|
17
|
+
@aws_secret_access_key = opts[:aws_secret_access_key] unless opts[:aws_secret_access_key].nil?
|
18
|
+
|
19
|
+
@fog = Fog::Compute.new(:provider => @provider,
|
20
|
+
:region => @region,
|
21
|
+
:aws_access_key_id => @aws_access_key_id,
|
22
|
+
:aws_secret_access_key => @aws_secret_access_key,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def volumes
|
27
|
+
@fog.volumes.all
|
28
|
+
end
|
29
|
+
|
30
|
+
def snap(volume_id, description = "", options = {})
|
31
|
+
snap = @fog.snapshots.create({:volume_id => volume_id, :description => description}.merge(options))
|
32
|
+
while snap.state != "completed" do
|
33
|
+
sleep 0.5
|
34
|
+
snap.reload
|
35
|
+
end
|
36
|
+
snap
|
37
|
+
end
|
38
|
+
|
39
|
+
def snapshots_for(volume_id)
|
40
|
+
# for more info on Amazon EC2 filters (like "volume-id" below) please
|
41
|
+
# refer to http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeVolumes.html
|
42
|
+
@fog.snapshots.all("volume-id" => volume_id)
|
43
|
+
end
|
44
|
+
|
45
|
+
#def prune_snapshots(volume_id, snapshots_to_keep, not_after_time = Time.now)
|
46
|
+
def prune_snapshots(volume_id, options = {})
|
47
|
+
snapshots_to_keep = options[:snapshots_to_keep] || 3
|
48
|
+
not_after_time = options[:not_after_time] || Time.now
|
49
|
+
snapshots = snapshots_for(volume_id)
|
50
|
+
|
51
|
+
(snapshots.count - snapshots_to_keep).times do
|
52
|
+
snapshots.first.destroy if snapshots.first.created_at < not_after_time
|
53
|
+
snapshots.reload
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def day_offset(days)
|
58
|
+
return days if days.nil?
|
59
|
+
Time.now - (days * 86400)
|
60
|
+
end
|
61
|
+
def days_to_seconds(days)
|
62
|
+
days * 86400
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|