vgh 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,112 @@
1
+ module VGH
2
+
3
+ # Creates the snapshots
4
+ def snap_and_tag(*args)
5
+ EC2::Snapshot.new(*args)
6
+ end
7
+
8
+ module EC2
9
+
10
+ # Creates a snapshot of the specified volume.
11
+ #
12
+ # == Usage
13
+ #
14
+ # Snapshot.new(volume_id, description, tags)
15
+ #
16
+ class Snapshot
17
+
18
+ # Create and tag snapshot, and also purge expired ones
19
+ # @param [String] volume_id The ID of the volume to snapshot
20
+ # @param [String] description The description for the new snapshot
21
+ # @param [Hash] tags A Hash containing the names and values of the tags
22
+ # @return [Snapshot] The Snapshot object.
23
+ def initialize(volume_id, description, tags)
24
+ @volume_id = volume_id
25
+ @description = description
26
+ @tags = tags
27
+ create_snapshot
28
+ tag_snapshot
29
+ purge_backups
30
+ return snapshot
31
+ end
32
+
33
+ # @return [String] The Volume ID
34
+ attr_reader :volume_id
35
+
36
+ # @return [String] The description of the snapshot
37
+ attr_reader :description
38
+
39
+ # @return [Hash] The tags hash
40
+ attr_reader :tags
41
+
42
+ # @return [Object] The Snapshot object
43
+ attr_reader :snapshot
44
+
45
+ # Creates a snapshot for the specified volume
46
+ # @return [Object] The newly created snapshot object
47
+ def create_snapshot
48
+ @snapshot = ec2.volumes[volume_id].
49
+ create_snapshot("#{description}")
50
+ message.info "Created snapshot \"#{snapshot.id}\""
51
+ return @snapshot
52
+ end
53
+
54
+ # Tags a Snapshot
55
+ def tag_snapshot
56
+ snap = snapshot
57
+ message.info "Tagging snapshot \"#{snap.id}\""
58
+ tags.map {|key, value|
59
+ ec2.tags.create(snap, key, {:value => value})
60
+ }
61
+ end
62
+
63
+ # Purges expired snapshots
64
+ def purge_backups
65
+ expired_backups.each do |snap|
66
+ message.info "Deleting expired snapshot (#{snap.id})"
67
+ snap.delete
68
+ end
69
+ end
70
+
71
+ # Creates a list of expired snapshots according to the expiration time
72
+ # specified in the app's configuration file
73
+ # @return [Array] An array of expired snapshot objects
74
+ def expired_backups
75
+ @expired_backups = []
76
+ all_backups.each {|snap|
77
+ if snap.start_time < (Time.now - backup_expiration*24*60*60)
78
+ @expired_backups.push snap
79
+ end
80
+ }
81
+ return @expired_backups
82
+ end
83
+
84
+ # Check for a an expiration period in the configuration file
85
+ def backup_expiration
86
+ expiration = config[:expiration]
87
+ if expiration.nil?
88
+ @backup_expiration = 7
89
+ else
90
+ @backup_expiration = expiration
91
+ end
92
+ return @backup_expiration.to_i
93
+ end
94
+
95
+ # Returns a list of snapshots that are named the same with the current FQDN.
96
+ def all_backups
97
+ @all ||= ec2.snapshots.
98
+ with_owner('self').
99
+ tagged('Name').tagged_values(fqdn)
100
+ end
101
+
102
+ end # class Snapshot
103
+
104
+ end # module EC2
105
+ end # module VGH
106
+
107
+ require 'vgh/system'
108
+ require 'vgh/output'
109
+ require 'vgh/configuration'
110
+ require 'vgh/ec2'
111
+ require 'vgh/ec2/metadata'
112
+ require 'vgh/ec2/volume'
@@ -0,0 +1,77 @@
1
+ module VGH
2
+ module EC2
3
+
4
+ # Collects information about the EBS volumes attached to the current instance.
5
+ # == Usage
6
+ # volumes = Volume.new
7
+ # puts volumes.list
8
+ # puts volumes.list_tagged('MyTag')
9
+ #
10
+ class Volume
11
+
12
+ # Creates an array with the IDs of all the volumes attached to the current
13
+ # instance
14
+ # @return [Array]
15
+ def list
16
+ @list = []
17
+ mappings.map {|device, info| @list.push(info.volume.id)}
18
+ return @list
19
+ end
20
+
21
+ # Creates an array with the IDs of all the volumes attached to the current
22
+ # instance, that contain a specific tag key
23
+ # @param [String] tag_key The Tag to look for
24
+ # @return [Array]
25
+ def list_tagged(tag_key)
26
+ @list_tagged = []
27
+ list.each {|vid|
28
+ volume_tags(vid).map {|volume_tag|
29
+ @list_tagged.push(vid) if volume_tag.key == tag_key
30
+ }
31
+ }
32
+ return @list_tagged
33
+ end
34
+
35
+ # Returns a Hash containing the block device mappings of the current instance.
36
+ # @return [Hash]
37
+ def mappings
38
+ message.info "Creating a list of volumes..."
39
+ @mappings ||= instance.block_device_mappings
40
+ end
41
+
42
+ # The current instance object
43
+ # @return [Instance] An instance object
44
+ def instance
45
+ @instance ||= ec2.instances[instance_id]
46
+ end
47
+
48
+ # Get volume's Name tag
49
+ # @param [String] volume_id The ID of the volume
50
+ # @return [String] The tag of the volume or (NOTAG) if a tag does not exists.
51
+ def name_tag(volume_id)
52
+ @name_tag = 'NOTAG'
53
+ volume_tags(volume_id).each {|tag|
54
+ @name_tag = tag.value if tag.key == 'Name'
55
+ }
56
+ return @name_tag
57
+ end
58
+
59
+ # Returns a collection of tags for the specified volume.
60
+ # @param [String] volume_id The id of the volume to query for tags.
61
+ # @return [TagsCollection] An array of tag objects
62
+ def volume_tags(volume_id)
63
+ @volume_tags = ec2.tags.
64
+ filter('resource-type', 'volume').
65
+ filter('resource-id', volume_id)
66
+ end
67
+
68
+ end # class Volume
69
+
70
+ end # module EC2
71
+ end # module VGH
72
+
73
+
74
+ require 'vgh/output'
75
+ require 'vgh/configuration'
76
+ require 'vgh/ec2'
77
+ require 'vgh/ec2/metadata'
data/lib/vgh/ec2.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'aws-sdk'
2
+
3
+ module VGH
4
+
5
+ # Creates a global ec2 method.
6
+ def ec2
7
+ $ec2 = AWS::EC2.new
8
+ end
9
+
10
+ # EC2 Module
11
+ module EC2
12
+ end
13
+
14
+ end # module VGH
15
+
data/lib/vgh/output.rb CHANGED
@@ -99,8 +99,6 @@ END_HEADER
99
99
  def footer
100
100
  stdout <<END_FOOTER
101
101
 
102
- ###############################################################################
103
- RUBY ROCKS!!!
104
102
  ###############################################################################"
105
103
  END_FOOTER
106
104
  end
@@ -12,70 +12,84 @@ module System
12
12
  #
13
13
  # == Usage:
14
14
  #
15
- # lvm = System::LV.new
16
- # lvm.suspend_lvs
15
+ # lvm = System::LVM.new
16
+ # lvm.suspend
17
17
  # # run the code that takes the snapshot
18
- # lvm.resume_lvs
18
+ # lvm.resume
19
19
  #
20
- class LV
20
+ class LVM
21
21
 
22
22
  # Loads variables and checks if LVM Tools are installed
23
23
  def initialize
24
- @dmcmd = '/sbin/dmsetup'
25
24
  installed?
26
25
  end
27
26
 
27
+ # @return [String] The dmsetup system command
28
+ def dm_cmd
29
+ @dmcmd ||= '/sbin/dmsetup'
30
+ end
31
+
28
32
  # Warn message if LVM tools are not installed
29
33
  def installed?
30
- message.warn "LVM Tools are not installed" unless File.exists?(@dmcmd)
34
+ if File.exists?(dm_cmd)
35
+ return true
36
+ else
37
+ message.warn "LVM Tools are not installed"
38
+ return false
39
+ end
40
+ end
41
+
42
+ # @return [String] A list of logical volumes present
43
+ def lvs
44
+ if ( installed? and System.is_root? )
45
+ @lvs ||= `#{dm_cmd} ls | /bin/grep -E -v 'swap|root'`
46
+ else
47
+ message.warn "Listing logical volume needs root privileges!"
48
+ return nil
49
+ end
31
50
  end
32
51
 
33
52
  # Test if logical volumes are present
34
53
  # @return [Boolean]
35
54
  def lvs_are_present?
36
- lvs_are_present = false
37
- @lvlist = `#{@dmcmd} ls | /bin/grep -E -v 'swap|root'`
38
- if @lvlist != "No devices found\n" then
39
- lvs_are_present = true
55
+ if lvs != "No devices found\n" then
56
+ return true
57
+ else
58
+ message.info "No logical volumes found."
59
+ return false
40
60
  end
41
- return lvs_are_present
42
61
  end
43
62
 
44
63
  # Suspend all logical volume
45
64
  def suspend
46
- if lvs_are_present?
47
- suspend_lvs
48
- else
49
- message.info "No logical volumes found."
50
- end
65
+ suspend_lvs if lvs_are_present?
51
66
  end
52
67
 
53
68
  # The actual suspend action
54
69
  def suspend_lvs
55
- for lv_name in @lvlist.split[0]
70
+ for lv_name in lvs.split[0]
56
71
  message.info "Suspending Logical Volume '#{lv_name}'..."
57
- `#{@dmcmd} suspend #{lv_name}`
72
+ `#{dm_cmd} suspend #{lv_name}`
58
73
  end
59
74
  end
60
75
 
61
76
  # Resume all logical volumes
62
77
  def resume
63
- if lvs_are_present?
64
- resume_lvs
65
- end
78
+ resume_lvs if lvs_are_present?
66
79
  end
67
80
 
68
81
  # The actual resume action
69
82
  def resume_lvs
70
- for lv_name in @lvlist.split[0]
83
+ for lv_name in lvs.split[0]
71
84
  message.info "Resuming Logical Volume '#{lv_name}'..."
72
- `#{@dmcmd} resume #{lv_name}`
85
+ `#{dm_cmd} resume #{lv_name}`
73
86
  end
74
87
  end
75
88
 
76
- end # class LV
89
+ end # class LVM
77
90
  end # module System
78
91
  end # module VGH
79
92
 
80
93
  require 'vgh/output'
94
+ require 'vgh/system'
81
95
 
@@ -16,20 +16,30 @@ class MySQL
16
16
 
17
17
  # Load defaults
18
18
  def initialize
19
- cfg = app_config
20
19
  @mysqladmin = '/usr/bin/mysqladmin'
21
20
  @mysql = '/usr/bin/mysql'
22
- @user = cfg[:mysql_user]
23
- @password = cfg[:mysql_pwd]
24
21
  end
25
22
 
23
+ # Get MySQL user
24
+ # @return [String]
25
+ def mysql_user
26
+ @mysql_user ||= config[:mysql_user]
27
+ end
28
+
29
+ # Get MySQL password
30
+ # @return [String]
31
+ def mysql_password
32
+ @mysql_password ||= config[:mysql_password]
33
+ end
34
+
35
+
26
36
  # Check if server is running and we have the right credentials
27
37
  # @return [Boolean]
28
38
  def mysql_exists?
29
39
  mysql_exists = false
30
40
  if File.exists?(@mysqladmin)
31
41
  mysql_exists = system "#{@mysqladmin} -s ping"
32
- if ! @user and ! @password
42
+ if ! mysql_user and ! mysql_password
33
43
  message.warning 'WARNING: MySQL exists but no credentials were found!'
34
44
  mysql_exists = false
35
45
  end
@@ -41,7 +51,7 @@ class MySQL
41
51
  def flush
42
52
  if mysql_exists?
43
53
  message.info 'Locking MySQL tables...'
44
- `#{@mysql} -u#{@user} -p#{@password} -e "FLUSH TABLES WITH READ LOCK"`
54
+ `#{@mysql} -u#{mysql_user} -p#{mysql_password} -e "FLUSH TABLES WITH READ LOCK"`
45
55
  end
46
56
  end
47
57
 
@@ -49,7 +59,7 @@ class MySQL
49
59
  def unlock
50
60
  if mysql_exists?
51
61
  message.info 'Unlocking MySQL tables...'
52
- `#{@mysql} -u#{@user} -p#{@password} -e "UNLOCK TABLES"`
62
+ `#{@mysql} -u#{mysql_user} -p#{mysql_password} -e "UNLOCK TABLES"`
53
63
  end
54
64
  end
55
65
 
data/lib/vgh/system.rb CHANGED
@@ -4,7 +4,7 @@ module VGH
4
4
  # the system's one)
5
5
  # @return [String]
6
6
  def fqdn
7
- remote_fqdn = app_config[:fqdn]
7
+ remote_fqdn = config[:fqdn]
8
8
  if remote_fqdn
9
9
  $fqdn ||= remote_fqdn
10
10
  else
@@ -12,15 +12,6 @@ module VGH
12
12
  end
13
13
  end
14
14
 
15
- # Check if the script is run as root
16
- # @return [Boolean]
17
- def is_root?
18
- if Process.uid == 0
19
- return true
20
- else
21
- return false
22
- end
23
- end
24
15
 
25
16
  # This is a parent class for different system actions performed by the scripts
26
17
  # included in this gem. For more information see the classes defined under
@@ -32,12 +23,61 @@ module VGH
32
23
  #
33
24
  module System
34
25
 
26
+ # Check if the script is run as root
27
+ # @return [Boolean]
28
+ def self.is_root?
29
+ if Process.uid == 0
30
+ return true
31
+ else
32
+ return false
33
+ end
34
+ end
35
+
35
36
  # Returns the current system's FQDN
36
37
  # @return [String]
37
38
  def self.fqdn
38
39
  $fqdn ||= `hostname -f`
39
40
  end
40
41
 
42
+ # Returns the current system's FQDN
43
+ # @return [String]
44
+ def self.lock
45
+ unless remotely?
46
+ mysql.flush
47
+ lvm.suspend
48
+ end
49
+ end
50
+
51
+ # Returns the current system's FQDN
52
+ # @return [String]
53
+ def self.unlock
54
+ unless remotely?
55
+ mysql.unlock
56
+ lvm.resume
57
+ end
58
+ end
59
+
60
+ # Checks if this script is run remotely.
61
+ # @return [Boolean]
62
+ def self.remotely?
63
+ cfg = config
64
+ if cfg[:instance] or cfg[:fqdn]
65
+ return true
66
+ else
67
+ return false
68
+ end
69
+ end
70
+
71
+ # Initializes the MySQL class
72
+ def self.mysql
73
+ mysql ||= System::MySQL.new
74
+ end
75
+
76
+ # Initializes the LVM class
77
+ def self.lvm
78
+ lvm ||= System::LVM.new
79
+ end
80
+
41
81
  end # module System
42
82
 
43
83
  end # module VGH
data/lib/vgh/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module VGH
2
2
  # Version number
3
- VERSION = "0.1.2"
3
+ VERSION = "0.2.0"
4
4
 
5
5
  # Returns the version number
6
6
  def version
data/lib/vgh.rb CHANGED
@@ -28,6 +28,8 @@ module VGH
28
28
  case app
29
29
  when 'ec2-backup'
30
30
  APPS::EC2_Backup.new.run
31
+ when 'checkpoint'
32
+ APPS::Checkpoint.new.run
31
33
  end
32
34
 
33
35
  # Display footer
@@ -48,6 +50,6 @@ module VGH
48
50
  end # module VGH
49
51
 
50
52
  require "vgh/output"
51
- require "vgh/system"
52
53
  require "vgh/apps"
53
54
  require "vgh/apps/ec2_backup"
55
+ require "vgh/apps/checkpoint"
data/spec/cli_spec.rb CHANGED
@@ -11,34 +11,26 @@ describe VGH::CLI do
11
11
  ARGV[0] = app
12
12
  end
13
13
 
14
- it "Should get the app name as the first argument" do
15
- VGH::CLI.new.options[:app].should eq app
16
- end
17
-
18
- context "When '-v' passed to the command line" do
19
- it "Verbosity should be enabled" do
20
- ARGV.push('-v')
21
- VGH::CLI.new.options[:verbose].should be_true
22
- end
14
+ it 'Should get the app name as the first argument' do
15
+ subject.options[:app].should eq app
23
16
  end
24
17
 
25
- context "When '-v' NOT passed to the command line" do
26
- it "Verbosity should be disabled" do
27
- VGH::CLI.new.options[:verbose].should be_false
28
- end
18
+ it 'Verbosity' do
19
+ subject.options[:verbose].should be_false
29
20
  end
30
21
 
31
- context "When '-l' passed to the command line" do
32
- it "Logging should be enabled" do
33
- ARGV.push('-l')
34
- VGH::CLI.new.options[:logging].should be_true
35
- end
22
+ it 'No Verbosity' do
23
+ ARGV.push('--verbose')
24
+ subject.options[:verbose].should be_true
36
25
  end
37
26
 
38
- context "When '-l' NOT passed to the command line" do
39
- it "Logging should be disabled" do
40
- VGH::CLI.new.options[:logging].should be_false
41
- end
27
+ it 'Logging' do
28
+ ARGV.push('--logging')
29
+ subject.options[:logging].should be_true
30
+ end
31
+
32
+ it 'No Logging' do
33
+ subject.options[:logging].should be_false
42
34
  end
43
35
 
44
36
  end
@@ -0,0 +1,44 @@
1
+ require 'helpers/spec'
2
+ require 'vgh'
3
+ require 'vgh/configuration'
4
+
5
+ describe VGH::Configuration do
6
+
7
+ let(:configuration) {VGH::Configuration.allocate}
8
+ let(:cli_confdir) {'/tmp'}
9
+ let(:config_hash) {"---
10
+ :key1: 'value1'
11
+ :key2: 'value2'"}
12
+
13
+ before(:each) do
14
+ configuration.stub(:cli).and_return({:confdir => nil})
15
+ end
16
+
17
+ it 'Should return the default config directory' do
18
+ configuration.global_config_dir.should eq('/etc/vgh')
19
+ end
20
+
21
+ it 'Should return the user config directory' do
22
+ configuration.user_config_dir =~ /.*\.vgh$/
23
+ end
24
+
25
+ it 'Should return a config directory from cli or return users directory' do
26
+ configuration.confdir.should =~ /.*\.vgh/
27
+ configuration.stub(:cli).and_return({:confdir => cli_confdir})
28
+ configuration.confdir.should eq(cli_confdir)
29
+ end
30
+
31
+ it 'Should return the main configuration file' do
32
+ configuration.config_file.should =~ /.*\.vgh\/config\.yml/
33
+ end
34
+
35
+ it 'Should initialize clean' do
36
+ cfg = VGH::Configuration
37
+ [:message, :cli, :log, :aws_config].each {|s|
38
+ cfg.any_instance.stub(s).and_return(Dummy.new)
39
+ }
40
+ cfg.any_instance.stub(:validate).and_return(config_hash)
41
+ subject.config.should eq(config_hash)
42
+ end
43
+
44
+ end
@@ -0,0 +1,14 @@
1
+ require 'helpers/spec'
2
+ require 'vgh/ec2/metadata'
3
+
4
+ describe VGH::EC2::MetaData do
5
+
6
+ let(:response) {'Remote server response'}
7
+ it 'Should receive valid response from the AWS metadata server' do
8
+ subject.stub_chain(:open, :read).and_return(response)
9
+ subject.instance_id.should eq(response)
10
+ subject.root_device.should eq(response)
11
+ end
12
+
13
+ end
14
+
@@ -0,0 +1,39 @@
1
+ require 'helpers/spec'
2
+ require 'aws-sdk'
3
+ require 'vgh/ec2/snapshot'
4
+ require 'vgh/system'
5
+
6
+ describe VGH::EC2::Snapshot do
7
+
8
+ include_context 'AWS Dummy'
9
+ let(:snap) {VGH::EC2::Snapshot.allocate}
10
+
11
+ before(:each) do
12
+ snap.stub(:snapshot).and_return(@snapshot)
13
+ snap.stub(:message).and_return(Dummy.new)
14
+ snap.stub(:tags).and_return(@tag_hash)
15
+ snap.stub(:ec2).and_return(@ec2)
16
+ snap.stub(:fqdn).and_return(@fqdn)
17
+ end
18
+
19
+ it "Should tag a snapshot" do
20
+ snap.tag_snapshot.each{|t|
21
+ t.should be_a AWS::EC2::Tag
22
+ }
23
+ end
24
+
25
+ it "Should list all snapshots" do
26
+ snap.all_backups.should be_an AWS::EC2::SnapshotCollection
27
+ end
28
+
29
+ it "Should create a list of expired backups" do
30
+ snap.expired_backups.should be_an Array
31
+ end
32
+
33
+ it "Should return a backup expiration integer" do
34
+ snap.stub(:config).and_return({:expiration => 5})
35
+ snap.backup_expiration.should eq(5)
36
+ end
37
+
38
+ end
39
+
@@ -0,0 +1,47 @@
1
+ require 'helpers/spec'
2
+
3
+ require 'aws-sdk'
4
+ require 'vgh/ec2/volume'
5
+
6
+ describe VGH::EC2::Volume do
7
+
8
+ include_context 'AWS Dummy'
9
+
10
+ let(:volume) {VGH::EC2::Volume.new}
11
+
12
+ before(:each) do
13
+ volume.stub(:message).and_return(Dummy.new)
14
+ volume.stub(:ec2).and_return(@ec2)
15
+ end
16
+
17
+ it "Should return an instance" do
18
+ volume.stub(:instance_id).and_return(@instance.id)
19
+ volume.instance.should be_a(AWS::EC2::Instance)
20
+ end
21
+
22
+ it "Should create a hash with block device mappings" do
23
+ volume.stub_chain(:instance, :block_device_mappings).
24
+ and_return(@instance_mappings)
25
+ volume.mappings.should be_a_kind_of Hash
26
+ end
27
+
28
+ it "Should return a collection of volume tags" do
29
+ volume.volume_tags(@volume2.id).should be_a(@tag_collection)
30
+ end
31
+
32
+ it "Should return the value of the name tag or (NOTAG)" do
33
+ volume.name_tag(@volume1.id).should be_a String
34
+ end
35
+
36
+ it "Should create a list of volumes" do
37
+ volume.stub(:mappings).and_return(@instance_mappings)
38
+ volume.list.should be_a Array
39
+ end
40
+
41
+ it "Should return a list of volumes that need a checkpoint" do
42
+ volume.stub(:list).and_return([])
43
+ volume.list_tagged('CHECKPOINT').should be_a Array
44
+ end
45
+
46
+ end
47
+