appshot 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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