appshot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ When 'I debug' do
2
+ binding.pry
3
+ end
@@ -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
@@ -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,3 @@
1
+ class Appshot
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require_relative 'volume/ebs_volume'
2
+ require_relative 'volume/ebs_snapshot'
3
+ require_relative 'volume/ebs_prune'
4
+
5
+ class Volume
6
+
7
+ end