appshot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/appshot.rb ADDED
@@ -0,0 +1,93 @@
1
+ require 'appshot/version'
2
+ require 'appshot/volume'
3
+ require 'appshot/app'
4
+ require 'appshot/filesystem'
5
+ require 'awesome_print'
6
+
7
+ class Appshot
8
+ def initialize(config)
9
+ @appshots = {}
10
+ @callables = []
11
+ instance_eval(config)
12
+ end
13
+
14
+ def empty?
15
+ true
16
+ end
17
+
18
+ def appshot_count
19
+ @appshots.size
20
+ end
21
+
22
+ def appshot_names
23
+ @appshots ? @appshots.keys : []
24
+ end
25
+
26
+ ##############
27
+ # DSL Keywords
28
+ ##############
29
+ def appshot(appshot_name, &block)
30
+ if block_given?
31
+ @callables = []
32
+ @appshots[appshot_name.to_s] = instance_eval(&block)
33
+ end
34
+ end
35
+
36
+ def comment(arg)
37
+ @callables
38
+ end
39
+
40
+ def xfs(args={})
41
+ @callables << Appshot::DM.new(args)
42
+ end
43
+
44
+ def ext4(args={})
45
+ @callables << Appshot::DM.new(args)
46
+ end
47
+
48
+ def ebs_snapshot(args={})
49
+ @callables << Appshot::EBS_Snapshot.new(args)
50
+ end
51
+
52
+ def mysql(args={})
53
+ @callables << Appshot::Mysql.new(args)
54
+ end
55
+
56
+ def ebs_prune(args={})
57
+ @callables << Appshot::EBS_Prune.new(args)
58
+ end
59
+
60
+ ###############
61
+ # Internals
62
+ ###############
63
+
64
+ def appshots
65
+ @appshots
66
+ end
67
+
68
+ def execute_callables
69
+ @appshots.each do |appshot, callable|
70
+ first_call = callable.shift
71
+ first_call.call(callable) unless first_call.nil?
72
+ end
73
+ end
74
+
75
+ def run_pass(options, args)
76
+ if options["list-appshots"]
77
+ puts list_appshots if options["list-appshots"]
78
+ else
79
+ execute_callables unless options["trial-run"]
80
+ end
81
+ end
82
+
83
+ def list_appshots
84
+ case @appshots.count
85
+ when 0
86
+ "There are no appshots configured"
87
+ when 1
88
+ "There is one appshot configured: #{@appshots.keys.first.to_s}"
89
+ else
90
+ "There are #{@appshots.count} appshots configured: #{@appshots.keys.join(', ')}"
91
+ end
92
+ end
93
+ end
data/rvmrc.example ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
+ # Only full ruby name is supported here, for short names use:
8
+ # echo "rvm use 1.9.3" > .rvmrc
9
+ environment_id="ruby-1.9.3"
10
+
11
+ # Uncomment the following lines if you want to verify rvm version per project
12
+ # rvmrc_rvm_version="1.10.2" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+
18
+ # First we attempt to load the desired environment directly from the environment
19
+ # file. This is very fast and efficient compared to running through the entire
20
+ # CLI and selector. If you want feedback on which environment was used then
21
+ # insert the word 'use' after --create as this triggers verbose mode.
22
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
23
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
24
+ then
25
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
26
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
27
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
28
+ if [[ $- == *i* ]] # check for interactive shells
29
+ then echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
30
+ else echo "Using: $GEM_HOME" # don't use colors in non-interactive shells
31
+ fi
32
+ else
33
+ # If the environment file has not yet been created, use the RVM CLI to select.
34
+ rvm --create use "$environment_id" || {
35
+ echo "Failed to create RVM environment '${environment_id}'."
36
+ return 1
37
+ }
38
+ fi
39
+
40
+ # If you use bundler, this might be useful to you:
41
+ # if [[ -s Gemfile ]] && {
42
+ # ! builtin command -v bundle >/dev/null ||
43
+ # builtin command -v bundle | grep $rvm_path/bin/bundle >/dev/null
44
+ # }
45
+ # then
46
+ # printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
47
+ # gem install bundler
48
+ # fi
49
+ # if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
50
+ # then
51
+ # bundle install | grep -vE '^Using|Your bundle is complete'
52
+ # fi
@@ -0,0 +1,37 @@
1
+ require 'appshot/app/mysql'
2
+ require 'awesome_print'
3
+
4
+ describe Appshot::Mysql do
5
+ subject { Appshot::Mysql.new }
6
+ it { should be_a_kind_of Appshot::Mysql }
7
+ it { subject.respond_to?(:call).should be_true }
8
+
9
+ describe "#call" do
10
+ before do
11
+ Mysql2::Client.stub(:new).and_return(mysql)
12
+ end
13
+ let(:mysql) { double(:mysql2, query: true, close: true) }
14
+
15
+ it "should require an argument to #call" do
16
+ lambda { subject.call }.should raise_error(ArgumentError)
17
+ end
18
+
19
+ it "should invoke #call on the next object in the call_chain" do
20
+ items = [double(:appshot1), double(:appshot2)]
21
+ items.first.should_receive(:call).with(items).and_return(true)
22
+ subject.call(items)
23
+ end
24
+
25
+ it "should run #lock before call is invoked" do
26
+ items = [Appshot::Mysql.new, Appshot::Mysql.new]
27
+ items.first.should_receive(:lock)
28
+ subject.call(items)
29
+ end
30
+
31
+ it "should run #unlock after call is invoked" do
32
+ items = [Appshot::Mysql.new, Appshot::Mysql.new]
33
+ items.first.should_receive(:unlock)
34
+ subject.call(items)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+ require 'appshot/filesystem/dm'
2
+
3
+ describe Appshot::DM do
4
+ subject {
5
+ Appshot::DM.any_instance.stub(validate_mount_point: true)
6
+ Appshot::DM.new(mount_point: "/")
7
+ }
8
+
9
+ it { should be_a_kind_of Appshot::DM }
10
+ it { subject.respond_to?(:call).should be_true }
11
+
12
+ describe "#call" do
13
+ it "should invoke #call on the next object in the call_chain" do
14
+ Appshot::DM.any_instance.stub(:freeze).and_return(true)
15
+ Appshot::DM.any_instance.stub(:unfreeze).and_return(true)
16
+ items = [double(:appshot1), double(:appshot2)]
17
+ items.first.should_receive(:call).with(items).and_return(true)
18
+ subject.call(items)
19
+ end
20
+
21
+ it "should run #freeze and #unfreeze around a call being invoked" do
22
+ Appshot::DM.any_instance.stub(validate_mount_point: true)
23
+ first_dm = Appshot::DM.new(mount_point: "/tmp/fake_mount")
24
+ first_dm.stub(:freeze).and_return(true)
25
+ first_dm.stub(:unfreeze).and_return(true)
26
+ items = [double(:appshot_dm)]
27
+ items.first.should_receive(:call).with([]).and_return(true)
28
+ first_dm.call(items)
29
+ end
30
+
31
+ it "should not allow root paths" do
32
+ lambda{ Appshot::DM.new(mount_point: "/") }.should raise_error "We cannot currently unfreeze the root filesystem, leaving your system unusable. Aborting."
33
+ end
34
+
35
+ it "should verify that the given mount point exists and is actually a mount point" do
36
+ lambda{ Appshot::DM.new(mount_point: "/tmp/fake_mount_for_appshot_testing") }.should raise_error "Your mount point: '/tmp/fake_mount_for_appshot_testing' is not a mount point!"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ require "appshot/volume/ebs_prune"
2
+
3
+ describe Appshot::EBS_Prune do
4
+ subject { Appshot::EBS_Prune.new }
5
+
6
+ it { should be_a_kind_of Appshot::EBS_Prune }
7
+ it { subject.respond_to?(:call).should be_true }
8
+
9
+ it "should invoke #call on the next object in the call_chain" do
10
+ next_item = [double(:appshot)]
11
+ subject.should_receive(:call).with(next_item).and_return(true)
12
+ subject.call(next_item)
13
+ end
14
+ describe "validating arguments" do
15
+ let(:opts) { { volume_id: "vol-eba11eee", snapshots_to_keep: 10, minimum_retention_days: 5, aws_access_key_id: "BOO", aws_secret_access_key: "YAH" } }
16
+ subject { Appshot::EBS_Prune.new(final_opts) }
17
+
18
+ context "with no volume_id" do
19
+ let(:final_opts) { opts.delete_if { |k,v| k == :volume_id } }
20
+ it "requires a volume_id argument" do
21
+ lambda { subject.valid? }.should raise_error ArgumentError, "volume_id must be specified for an ebs_prune"
22
+ end
23
+ end
24
+
25
+ context "with no aws_access_key_id" do
26
+ let(:final_opts) { opts.delete_if { |k,v| k == :aws_access_key_id } }
27
+ it "requires a aws_access_key_id argument" do
28
+ lambda { subject.valid? }.should raise_error ArgumentError, "aws_access_key_id must be specified for an ebs_prune"
29
+ end
30
+ end
31
+
32
+ context "with no aws_secret_access_key" do
33
+ let(:final_opts) { opts.delete_if { |k,v| k == :aws_secret_access_key } }
34
+ it "requires a aws_secret_access_key argument" do
35
+ lambda { subject.valid? }.should raise_error ArgumentError, "aws_secret_access_key must be specified for an ebs_prune"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ require 'appshot/volume/ebs_snapshot'
2
+
3
+ describe Appshot::EBS_Snapshot do
4
+
5
+ subject { Appshot::EBS_Snapshot.new }
6
+
7
+ it { should be_a_kind_of Appshot::EBS_Snapshot }
8
+ it { subject.respond_to?(:call).should be_true }
9
+
10
+ it "should invoke #call on the next object in the call_chain" do
11
+ next_item = [double(:appshot)]
12
+ subject.should_receive(:call).with(next_item).and_return(true)
13
+ subject.call(next_item)
14
+ end
15
+
16
+ describe "validating arguments" do
17
+ let(:opts) { { volume_id: "vol-12341234", aws_access_key_id: "BOO", aws_secret_access_key: "YAH" } }
18
+ subject { Appshot::EBS_Snapshot.new(final_opts) }
19
+
20
+ context "with no volume_id" do
21
+ let(:final_opts) { opts.delete_if { |k,v| k == :volume_id } }
22
+ it "requires a volume_id argument" do
23
+ lambda { subject.valid? }.should raise_error ArgumentError, "volume_id must be specified for an ebs_snapshot"
24
+ end
25
+ end
26
+
27
+ context "with no aws_access_key_id" do
28
+ let(:final_opts) { opts.delete_if { |k,v| k == :aws_access_key_id } }
29
+ it "requires an aws_access_key_id argument" do
30
+ lambda { subject.valid? }.should raise_error ArgumentError, "aws_access_key_id must be specified for an ebs_snapshot"
31
+ end
32
+ end
33
+
34
+ context "with no aws_secret_access_key" do
35
+ let(:final_opts) { opts.delete_if { |k,v| k == :aws_secret_access_key } }
36
+ it "requires an aws_secret_access_key argument" do
37
+ lambda { subject.valid? }.should raise_error ArgumentError, "aws_secret_access_key must be specified for an ebs_snapshot"
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+ require "appshot"
3
+ require "appshot/volume"
4
+ require "appshot/volume/ebs_volume"
5
+ require "timecop"
6
+
7
+ describe Appshot::EBS_Volume do
8
+ before do
9
+ Fog::Compute::AWS::Mock.reset
10
+ Fog.mock!
11
+ end
12
+
13
+ let(:ebs) { described_class.new(:provider => 'AWS',
14
+ :region => region,
15
+ :aws_access_key_id => aws_access_key_id,
16
+ :aws_secret_access_key => aws_secret_access_key,
17
+ ) }
18
+ let(:fog_compute) { ebs.instance_variable_get(:@fog) }
19
+ let(:region) { "us-east-1" }
20
+ let(:aws_access_key_id) { "access_key_shhh" }
21
+ let(:aws_secret_access_key) { "secret_key_shhh" }
22
+
23
+ describe "basic setup" do
24
+ it "should be a class" do
25
+ described_class.should be_a_kind_of Class
26
+ end
27
+
28
+ it "should have a volume_id" do
29
+ ebs.respond_to?(:volume_id).should be_true
30
+ end
31
+ end
32
+
33
+ describe "given a volume_id" do
34
+ let(:snapshot_count) { 2 }
35
+ let(:volume_id) { volumes.first.id }
36
+
37
+ let(:volumes) do
38
+ with_no_mock_delay do
39
+ [ create_volume("us-east-1a"),
40
+ create_volume("us-east-1b"), ]
41
+ end
42
+ end
43
+
44
+ let(:snaps) do
45
+ with_no_mock_delay do
46
+ snapshot_count.times do |i|
47
+ ebs.snap(volume_id, "This is test snapshot ##{i}")
48
+ end
49
+ ebs.snapshots_for(volume_id)
50
+ end
51
+ end
52
+
53
+ it "should create a snap id with a valid format" do
54
+ snaps.first.id.should =~ /^snap-*/
55
+ end
56
+
57
+ it "must finish a snapshot of the volume" do
58
+ snaps.first.state.should == "completed"
59
+ end
60
+
61
+ it "should know how many snapshots there are for a given volume" do
62
+ snaps
63
+ ebs.snapshots_for(volumes.first.id).count.should == 2
64
+ ebs.snapshots_for(volumes.last.id).count.should == 0
65
+ end
66
+
67
+ describe "pruning snapshots" do
68
+ context "with a minimum age" do
69
+ let(:snapshot_count) { 5 }
70
+
71
+ it "should not prune snapshots younger than the minimum age" do
72
+ with_no_mock_delay do
73
+ Timecop.freeze(Time.now - (3 * 86400))
74
+ ebs.snap(volume_id, "This is test snapshot #2")
75
+ Timecop.freeze(Time.now + 86400)
76
+ ebs.snap(volume_id, "This is test snapshot #3")
77
+ Timecop.freeze(Time.now + 86400)
78
+ ebs.snap(volume_id, "This is test snapshot #4")
79
+ Timecop.return
80
+ end
81
+ ebs.snapshots_for(volumes.first.id).count.should == 3
82
+ ebs.prune_snapshots(volumes.first.id, snapshots_to_keep: 1, not_after_time: Time.now - (3 * 86400))
83
+ ebs.snapshots_for(volumes.first.id).count.should == 2
84
+ end
85
+ end
86
+
87
+ context "without a minimum age" do
88
+ let(:snapshot_count) { 3 }
89
+
90
+ it "should prune snapshots for a given volume to a snapshot count" do
91
+ snaps
92
+ ebs.snapshots_for(volumes.first.id).count.should == 3
93
+ ebs.prune_snapshots(volumes.first.id, snapshots_to_keep: 1)
94
+ ebs.snapshots_for(volumes.first.id).count.should == 1
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def create_server(zone = "us-east-1" + %w(a b c d).sample)
104
+ with_no_mock_delay do
105
+ new_server = fog_compute.servers.create
106
+ new_server.reload.id
107
+ end
108
+ end
109
+
110
+ def create_volume(zone = "us-east-1" + %w(a b c d).sample, size = 20)
111
+ with_no_mock_delay do
112
+ new_volume = fog_compute.volumes.create(:availability_zone => zone, :size => size)
113
+ new_volume.reload
114
+ end
115
+ end
116
+
117
+ def with_no_mock_delay
118
+ delay = Fog::Mock.delay
119
+ Fog::Mock.delay = 0
120
+ return_object = yield
121
+ Fog::Mock.delay = delay
122
+ return_object
123
+ end
124
+
125
+
@@ -0,0 +1,64 @@
1
+ require "appshot"
2
+ require 'awesome_print'
3
+
4
+ describe Appshot do
5
+
6
+ let(:app) { Appshot.new("") }
7
+
8
+ it "is a class" do
9
+ described_class.should be_a_kind_of Class
10
+ end
11
+
12
+ it "has a method #appshot" do
13
+ app.respond_to?(:appshot).should be_true
14
+ end
15
+
16
+ describe "#appshot" do
17
+ it "should not add an appshot with an empty block" do
18
+ app.appshot_count.should == 0
19
+ app.appshot("my_account")
20
+ app.appshot_count.should == 0
21
+ end
22
+
23
+ it "should add an appshot to the store" do
24
+ app.appshot_count.should == 0
25
+ app.appshot("my_account") {}
26
+ app.appshot_count.should == 1
27
+ end
28
+
29
+ it "should add an appshot with the right name" do
30
+ app.appshot("my_account") {}
31
+ app.appshot_names.include?("my_account").should be_true
32
+ end
33
+
34
+ it "should accept a block" do
35
+ app.appshot("my_account") do
36
+ comment "my_account snapshot run"
37
+ end
38
+ app.appshots["my_account"].should_not be_nil
39
+ end
40
+ end
41
+
42
+ describe "#list_appshots" do
43
+ context "with no appshots in config file" do
44
+ it "should give a 'no appshots' message" do
45
+ app.list_appshots.should == "There are no appshots configured"
46
+ end
47
+ end
48
+ context "with one appshot in config file" do
49
+ it "should give a 'one appshot' message" do
50
+ app.appshot("one") {}
51
+ app.list_appshots.should == "There is one appshot configured: one"
52
+ end
53
+ end
54
+ end
55
+
56
+ describe "running an appshot" do
57
+ it "should run appshots" do
58
+ app.appshot("my_account") do
59
+ comment "my account snapshot execute"
60
+ end
61
+ app.execute_callables
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ require 'awesome_print'
2
+ require 'pry'
3
+
4
+ RSpec.configure do |config|
5
+ config.mock_framework = :rspec
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true # default in RSpec 3
7
+ end