aebus 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  config/*
2
- Gemfile.lock
2
+ Gemfile.lock
3
+ pkg
data/bin/aebus CHANGED
@@ -12,6 +12,7 @@ program :version, Aebus::VERSION
12
12
  program :description, 'Automatic EC2 BackUp Software'
13
13
 
14
14
  global_option('-c','--config FILE', 'The YAML file containing the backup configuration')
15
+ global_option('-l', '--logfile FILE', 'A log file which will receive the output')
15
16
 
16
17
  default_command :help
17
18
 
@@ -28,8 +29,13 @@ command :status do |c|
28
29
  c.when_called do |args, options|
29
30
  options.default \
30
31
  :config => DEFAULT_CONFIG_NAME
31
- raise ("Config file does not exist") unless FileTest.exist?(Pathname.new(options.config).realpath)
32
- aebus = Aebus::Aebus.new
32
+ begin
33
+ FileTest.exist?(Pathname.new(options.config).realpath)
34
+ rescue Errno::ENOENT => e
35
+ puts ("Configuration file not found")
36
+ exit(2)
37
+ end
38
+ aebus = Aebus::Core.new
33
39
  aebus.status(args,options)
34
40
  end
35
41
  end
@@ -41,9 +47,9 @@ command :backup do |c|
41
47
  Backs up a set of EC2 Volumes, according to the configuration file. If no volume is specified in the
42
48
  command line, all volumes defined in the config file are backed up
43
49
  eos
44
- c.example 'Manually creates backup of vol1 and vol2', 'aebus backup --manual vol1 vol2'
50
+ c.example 'Manually creates backup of vol1 and vol2', 'aebus backup -m vol1 vol2'
45
51
  c.example 'Create, if needed, backups for all volumes', 'aebus backup'
46
- c.option '--manual', 'Starts a manual backup (always creates a snapshot for each volume)'
52
+ c.option '-m', '--manual', 'Starts a manual backup (always creates a snapshot for each volume)'
47
53
  c.option '--[no-]purge', 'Do not purge expired backups, defaults to --purge'
48
54
  c.when_called do |args, options|
49
55
  options.default \
@@ -51,7 +57,7 @@ command :backup do |c|
51
57
  :manual => false,
52
58
  :purge => true
53
59
  raise ("Config file does not exist") unless FileTest.exist?(Pathname.new(options.config).realpath)
54
- aebus = Aebus::Aebus.new
60
+ aebus = Aebus::Core.new
55
61
  aebus.backup(args,options)
56
62
  end
57
63
  end
@@ -4,73 +4,133 @@ require 'rubygems'
4
4
  require 'AWS'
5
5
  require_relative 'config/config'
6
6
  require_relative 'aebus/version'
7
+ require_relative 'aebus/logging'
8
+ require_relative 'aebus/volume_status'
7
9
  require_relative 'ec2/zones'
8
10
  require_relative 'ec2/snapshot'
9
11
 
10
12
  module Aebus
11
13
 
12
- class Aebus
14
+ class Core
15
+
16
+ include Logging
13
17
 
14
18
  AWS_NAME_TAG = "Name"
15
19
  AEBUS_TAG = "Aebus"
16
20
 
17
21
  def status(args, options)
18
- current_time_utc = Time.now.utc
19
- config = Config::Config.new(File.join(File.dirname("."), options.config), current_time_utc)
20
- @ec2 = AWS::EC2::Base.new(:access_key_id => config.defaults["access_key_id"],
21
- :secret_access_key => config.defaults["secret_access_key"],
22
- :server => EC2::zone_to_url(config.defaults["zone"]))
22
+ @current_time_utc = Time.now.utc
23
+ init_logger options
24
+ logger.info("status check started at #{@current_time_utc}")
25
+
26
+ @config = Config::Config.new(File.join(File.dirname("."), options.config), @current_time_utc)
27
+ @ec2 = AWS::EC2::Base.new(:access_key_id => @config.defaults["access_key_id"],
28
+ :secret_access_key => @config.defaults["secret_access_key"],
29
+ :server => EC2::zone_to_url(@config.defaults["zone"]))
30
+
31
+
32
+
33
+ target_volumes = target_volumes(args)
34
+
35
+ abort("Configuration contains invalid volumes") unless validate_target_volumes(target_volumes)
36
+
37
+ status = check_status(target_volumes)
38
+
39
+ message = "status check completed - #{status[:total]} volume(s) checked, #{status[:to_backup]} to be backed up, max delay detected #{status[:delay]}s, #{status[:to_purge]} snapshots to be purged"
40
+ logger.info message
41
+ puts message
23
42
 
24
- target_volumes = calculate_target_volumes(config, args)
25
- snap_map = get_snapshots_map
26
43
 
44
+
45
+ end
46
+
47
+ def check_status(target_volumes)
48
+ result = {}
49
+ result[:timestamp] = @current_time_utc
50
+ snap_map = get_snapshots_map
51
+ result[:volumes] = Array.new
52
+ to_backup = 0
53
+ to_purge = 0
27
54
  target_volumes.each do |target|
28
- volume = config.volumes[target]
29
- tags = volume.backups_to_be_run(snap_map[target])
30
- if (tags.count > 0) then
31
- puts ("[INFO] Volume #{target} needs to be backed up. Tags: #{tags.join(',')}")
55
+ vs = VolumeStatus.new(target)
56
+ volume = @config.volumes[target]
57
+ vs.last_backup = volume.last_backup
58
+ vs.next_backup = volume.next_backup
59
+ to_be_run = volume.backups_to_be_run(snap_map[target], @current_time_utc)
60
+
61
+ vs.delay = to_be_run[0]
62
+ vs.tags = to_be_run[1]
63
+
64
+ if (vs.needs_backup?) then
65
+ logger.info("Volume #{target} needs to be backed up. Tags: #{vs.tags.join(',')}, max delay #{vs.delay}")
66
+ to_backup += 1
32
67
  else
33
- puts ("[INFO] Volume #{target} does not need to be backed up")
68
+ logger.info("Volume #{target} does not need to be backed up")
34
69
  end
35
70
 
36
- purgeable_snapshots =volume.purgeable_snapshots(snap_map[target])
37
- puts ("[INFO] Volume #{target} has #{purgeable_snapshots.count} purgeable snapshot(s): #{purgeable_snapshots.inject([]){|x, snap| x << snap.id}.join(',')}")
71
+ vs.purgeable_snapshot_ids = volume.purgeable_snapshot_ids(snap_map[target])
72
+ to_purge += vs.purgeable_snapshot_ids.count if vs.purgeable_snapshot_ids
73
+ logger.info("Volume #{target} has #{vs.purgeable_snapshot_ids.count} purgeable snapshot(s): #{vs.purgeable_snapshot_ids.join(',')}")
38
74
 
39
- end
75
+ result[:volumes] << vs
40
76
 
77
+ end
78
+ result[:to_backup] = to_backup
79
+ result[:to_purge] = to_purge
80
+ result[:delay] = result[:volumes].inject([0]) {|acc, vs| acc << vs.delay}.max
81
+ result[:total] = result[:volumes].count
82
+ result
41
83
 
42
84
  end
43
85
 
86
+
87
+
44
88
  def backup(args, options)
45
89
 
46
- current_time_utc = Time.now.utc
47
- config = Config::Config.new(File.join(File.dirname("."), options.config), current_time_utc)
48
- @ec2 = AWS::EC2::Base.new(:access_key_id => config.defaults["access_key_id"],
49
- :secret_access_key => config.defaults["secret_access_key"],
50
- :server => EC2::zone_to_url(config.defaults["zone"]))
51
90
 
52
- target_volumes = calculate_target_volumes(config, args)
91
+ backed_up = 0
92
+ max_delay = 0
93
+ purged = 0
94
+ to_purge = 0
95
+ to_backup = 0
96
+ @current_time_utc = Time.now.utc
97
+ @config = Config::Config.new(File.join(File.dirname("."), options.config), @current_time_utc)
98
+
99
+ init_logger options
100
+ logger.info("backup started at #{@current_time_utc}")
101
+
102
+ @ec2 = AWS::EC2::Base.new(:access_key_id => @config.defaults["access_key_id"],
103
+ :secret_access_key => @config.defaults["secret_access_key"],
104
+ :server => EC2::zone_to_url(@config.defaults["zone"]))
105
+
106
+ target_volumes = target_volumes(args)
53
107
  if (options.manual) then
54
108
 
55
109
  target_volumes.each do |volume|
56
-
57
- backup_volume(volume, current_time_utc, [EC2::AEBUS_MANUAL_TAG])
110
+ to_backup += 1
111
+ break unless backup_volume(volume, [EC2::AEBUS_MANUAL_TAG])
112
+ backed_up += 1
58
113
 
59
114
  end
60
115
 
61
116
  else
62
117
 
63
118
  snap_map = get_snapshots_map
119
+
64
120
  target_volumes.each do |target|
65
121
 
66
- volume = config.volumes[target]
67
- tags = volume.backups_to_be_run(snap_map[target])
122
+ volume = @config.volumes[target]
123
+ to_be_run = volume.backups_to_be_run(snap_map[target], @current_time_utc)
124
+ max_delay = [max_delay, to_be_run[0]].max
125
+ tags = to_be_run[1]
68
126
  if (tags.count > 0) then
69
127
  tags << EC2::AEBUS_AUTO_TAG
70
- puts("[INFO] Creating backup for volume #{target} with tags #{tags.join(',')}")
71
- backup_volume(target, current_time_utc, tags)
128
+ logger.info("Creating backup for volume #{target} with tags #{tags.join(',')}, max delay #{max_delay}")
129
+ to_backup +=1
130
+ break unless backup_volume(target, tags)
131
+ backed_up += 1
72
132
  else
73
- puts ("[INFO] Volume #{target} does not need to be backed up")
133
+ logger.info("Volume #{target} does not need to be backed up")
74
134
  end
75
135
 
76
136
  end
@@ -78,21 +138,29 @@ module Aebus
78
138
  snap_map = get_snapshots_map # we reload the map since we may have created more snapshots
79
139
  if (options.purge) then
80
140
  target_volumes.each do |target|
81
- volume = config.volumes[target]
82
- purgeable_snapshots = volume.purgeable_snapshots(snap_map[target])
83
- purgeable_snapshots.each {|snapshot| purge_snapshot(snapshot.id)}
141
+ volume = @config.volumes[target]
142
+ purgeable_snapshot_ids = volume.purgeable_snapshot_ids(snap_map[target])
143
+ purgeable_snapshot_ids.each do |snapshot_id|
144
+ to_purge += 1
145
+ purged += 1 if purge_snapshot(snapshot_id)
146
+
147
+ end
84
148
  end
85
149
  else
86
- puts("[INFO] Skipping purging phase")
150
+ logger.info("Skipping purging phase")
87
151
  end
88
152
 
89
153
  end
90
154
 
155
+ message = "Backup Completed at #{Time.now}. Checked #{target_volumes.count} volume(s), backed up #{backed_up}, max delay detected #{max_delay}, #{to_purge} purgeable snapshot(s), #{purged} purged"
156
+ logger.info(message)
157
+ puts(message)
158
+
91
159
  end
92
160
 
93
- def calculate_target_volumes(config, args)
161
+ def target_volumes(args)
94
162
 
95
- result = config.volume_ids
163
+ result = @config.volume_ids
96
164
  if (args && (args.count > 0)) then
97
165
  result &= args
98
166
  end
@@ -101,35 +169,31 @@ module Aebus
101
169
 
102
170
  end
103
171
 
104
- def list_volumes
105
- response = @ec2.describe_volumes
106
- puts(response)
172
+ def init_logger(options)
173
+ Logging.log_to_file(options.logfile) unless options.logfile.nil?
107
174
  end
108
175
 
109
-
110
-
111
-
112
- def backup_volume(volume_id, current_time_utc, tags)
176
+ # backs up a given volume using the given time as part of the name and setting the given tags to the snapshot
177
+ # @param volume_id [String] the id of the volume to be backed up
178
+ # @param tags [Array] an array of String to be used as tags for the snapshot
179
+ # @return [boolean] true if the backup was successful, false otherwise
180
+ def backup_volume(volume_id, tags)
113
181
  begin
114
182
  volume_info = @ec2.describe_volumes(:volume_id => volume_id)
115
183
 
116
184
  rescue AWS::Error => e
117
- puts("[WARNING] Volume Id #{volume_id} not found")
185
+ logger.error("Volume Id #{volume_id} not found. Underlying message #{e.message}")
118
186
  return false
119
187
  end
120
188
 
121
189
  begin
122
- puts(volume_info)
123
190
  volume_tags = volume_info.volumeSet.item[0].tagSet.item
124
- puts(volume_tags)
125
191
 
126
- name_and_desc = Aebus.calculate_name_and_desc(volume_id, volume_tags, current_time_utc)
127
- puts(name_and_desc)
192
+ name_and_desc = Core.name_and_desc(volume_id, volume_tags, @current_time_utc)
128
193
  create_response = @ec2.create_snapshot(:volume_id => volume_id, :description => name_and_desc[1])
129
- puts(create_response)
130
194
 
131
195
  rescue AWS::Error => e
132
- puts("[ERROR] Volume Id #{volume_id} could not be backed up")
196
+ logger.error("Volume Id #{volume_id} could not be backed up. Underlying message #{e.message}")
133
197
  return false
134
198
  end
135
199
 
@@ -138,18 +202,22 @@ module Aebus
138
202
  @ec2.create_tags(:resource_id => create_response.snapshotId,
139
203
  :tag => [{AWS_NAME_TAG => name_and_desc[0]}, {AEBUS_TAG => tags.join(',')}])
140
204
  rescue AWS::Error => e
141
- puts("[WARNING] Could not set tags to snapshot #{create_response.snapshotId}")
205
+ logger.error("[WARNING] Could not set tags to snapshot #{create_response.snapshotId}. Underlying message #{e.message}")
142
206
  return false
143
207
  end
144
208
 
145
- puts("[INFO] Created snapshot #{create_response.snapshotId} for volume #{volume_id}");
209
+ logger.info("Created snapshot #{create_response.snapshotId} for volume #{volume_id}")
146
210
 
147
- return true
211
+ true
148
212
 
149
213
  end
150
214
 
151
-
152
- def self.calculate_name_and_desc(volume_id, tags, utc_time)
215
+ # calculates the name and the description to be set to a snapshot
216
+ # @param volume_id [String] the id of the volume whose snapshot we are creating
217
+ # @param tags [Array] the tags currently associated with the Volume
218
+ # @param utc_time [Time] the UTC time at which the backup process started (used to generate the correct name)
219
+ # @return [Array] an array in the form of [name, description]]
220
+ def self.name_and_desc(volume_id, tags, utc_time)
153
221
 
154
222
  name = "backup_#{utc_time.strftime("%Y%m%d")}_#{volume_id}"
155
223
  volume_name = volume_id
@@ -192,10 +260,26 @@ module Aebus
192
260
  begin
193
261
  response = @ec2.delete_snapshot(:snapshot_id => snapshot_id)
194
262
  if (response["return"]) then
195
- puts("[INFO] Purged snapshot #{snapshot_id}")
263
+ logger.info("Purged snapshot #{snapshot_id}")
264
+ true
265
+ else
266
+ false
196
267
  end
197
268
  rescue AWS::Error => e
198
- puts("[WARNING] Could not purge snapshot #{snapshot_id}")
269
+ logger.warn("Could not purge snapshot #{snapshot_id}; underlying message #{e.message}")
270
+ false
271
+ end
272
+
273
+ end
274
+
275
+ def validate_target_volumes(target_volumes)
276
+ begin
277
+ @ec2.describe_volumes(:volume_id => target_volumes)
278
+ logger.info("Target volumes validated")
279
+ true
280
+ rescue AWS::Error => e
281
+ logger.error("Target validation failed with message '#{e.message}' Check your configuration")
282
+ false
199
283
  end
200
284
 
201
285
  end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ module Aebus
4
+
5
+ module Logging
6
+
7
+ def logger
8
+ Logging.logger
9
+ end
10
+
11
+ def self.logger
12
+ @logger ||= Logger.new(STDOUT)
13
+ end
14
+
15
+ def self.log_to_file(file)
16
+ begin
17
+ @logger = Logger.new(file, 'daily')
18
+ rescue Errno::EACCES => e
19
+ logger.warn("Could not create log file, '#{e.message}'. Defaulting to STDOUT")
20
+ end
21
+ end
22
+
23
+
24
+ end
25
+
26
+
27
+ end
@@ -1,3 +1,3 @@
1
1
  module Aebus
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -0,0 +1,23 @@
1
+ module Aebus
2
+
3
+ class VolumeStatus
4
+
5
+ attr_accessor :id, :last_backup, :next_backup, :delay, :purgeable_snapshot_ids, :tags
6
+
7
+ def initialize(volume_id)
8
+ @id = volume_id
9
+ end
10
+
11
+ def needs_backup?
12
+ (!@tags.nil? && (@tags.count > 0))
13
+ end
14
+
15
+ def to_s
16
+ "Volume: id => #{id}, :last_backup => #{last_backup}, next_backup=> #{next_backup}, needs_backup? => #{needs_backup?}, delay => #{delay}, tags => #{tags}, purgeable_snapshot => #{purgeable_snapshots}"
17
+
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -16,6 +16,8 @@ module Aebus
16
16
  calculate_deadlines(current_time_utc, backup_config["when"])
17
17
  end
18
18
  @keep = backup_config["keep"]
19
+ # we use Infinity to model the keep all
20
+ @keep = 1.0 / 0 if (@keep.nil? || @keep.eql?(KEEP_ALL))
19
21
 
20
22
  end
21
23
 
@@ -39,7 +41,7 @@ module Aebus
39
41
  result = Hash.new
40
42
 
41
43
  backups_config.each_pair do |key,value|
42
- result.store(key, BackupSchedule.new(current_time_utc, key, value))
44
+ result.store(key, BackupSchedule.new(current_time_utc, key, value))
43
45
  end
44
46
 
45
47
  result
@@ -62,20 +64,23 @@ module Aebus
62
64
  end
63
65
  end
64
66
 
65
- def backups_to_be_run(snapshots)
67
+ def backups_to_be_run(snapshots,current_time_utc)
66
68
 
67
69
  result = Array.new
68
-
70
+ max_delay = 0
69
71
  @backups.each_pair do |k,v|
70
72
 
71
- result << k unless recent_backup?(k, snapshots, v.last_deadline)
73
+ unless recent_backup?(k, snapshots, v.last_deadline)
74
+ result << k
75
+ max_delay = [max_delay, current_time_utc.to_i - v.last_deadline.to_i].max
76
+ end
72
77
 
73
78
  end
74
- result
79
+ [max_delay, result]
75
80
  end
76
81
 
77
82
  def recent_backup?(label, snapshots, last_deadline)
78
-
83
+ return false unless snapshots
79
84
  snapshots.each do |snapshot|
80
85
 
81
86
  if (snapshot.aebus_tags_include?(label) && (snapshot.start_time > last_deadline))
@@ -86,31 +91,40 @@ module Aebus
86
91
  false
87
92
  end
88
93
 
89
- def purgeable_snapshots(snapshots)
90
- puts(snapshots.count)
94
+
95
+ def purgeable_snapshot_ids(snapshots)
96
+ return [] unless snapshots
91
97
  removables = snapshots.select{|snapshot| snapshot.aebus_removable_snapshot?}
92
- puts(removables.count)
93
98
  available_backups = @backups.each_with_object({}) { | (k, v) , h | h[k] = v.keep}
94
99
  removables.each do |snapshot|
95
100
  snapshot.aebus_tags.each do |tag|
96
- if (available_backups.include? tag) then
97
- if (KEEP_ALL.eql?(available_backups[tag])) then
98
- puts("Keeping snapshot #{snapshot.id} because of all on #{tag}")
99
- snapshot.keep = true
100
- elsif (available_backups[tag] > 0) then
101
+ if ((available_backups.include? tag) && (available_backups[tag] > 0)) then
101
102
  snapshot.keep = true
102
103
  available_backups[tag] -= 1
103
- end
104
104
  end
105
105
  end
106
106
  end
107
107
 
108
- removables.select{|snapshot| !snapshot.keep? }
108
+ removables.inject([]) do |acc, snapshot|
109
+ acc << snapshot.id unless snapshot.keep?
110
+ acc
111
+ end
112
+
113
+ end
114
+
115
+ def last_backup
116
+ @backups.values.map{|backup| backup.last_deadline}.max
117
+ end
109
118
 
119
+ def next_backup
120
+ @backups.values.map{|backup| backup.next_deadline}.min
110
121
  end
111
122
 
123
+
112
124
  end
113
125
 
126
+
127
+
114
128
  end
115
129
 
116
130
  end
@@ -35,7 +35,6 @@ module Aebus
35
35
 
36
36
  def aebus_tags_include?(label)
37
37
  if aebus_snapshot? then
38
- aebus_tags = @tags[AEBUS_TAG].split(',')
39
38
  return aebus_tags.include? label
40
39
  end
41
40
  false
@@ -46,11 +45,11 @@ module Aebus
46
45
  end
47
46
 
48
47
  def aebus_removable_snapshot?
48
+ return false unless aebus_snapshot?
49
49
  (aebus_tags & [AEBUS_MANUAL_TAG, AEBUS_KEEP_TAG]).count == 0
50
50
  end
51
51
 
52
52
  def aebus_tags
53
- return nil unless aebus_snapshot?
54
53
  @tags[AEBUS_TAG].split(',')
55
54
  end
56
55
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aebus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-06 00:00:00.000000000Z
12
+ date: 2011-11-28 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: commander
16
- requirement: &70320653233040 !ruby/object:Gem::Requirement
16
+ requirement: &70278571512580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 4.0.6
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70320653233040
24
+ version_requirements: *70278571512580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: parse-cron
27
- requirement: &70320653232540 !ruby/object:Gem::Requirement
27
+ requirement: &70278571512080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.1.1
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70320653232540
35
+ version_requirements: *70278571512080
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: amazon-ec2
38
- requirement: &70320653232080 !ruby/object:Gem::Requirement
38
+ requirement: &70278571511620 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: 0.9.17
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70320653232080
46
+ version_requirements: *70278571511620
47
47
  description: A tool to automate snapshot management in EC2
48
48
  email:
49
49
  - nessche@gmail.com
@@ -59,7 +59,9 @@ files:
59
59
  - aebus.gemspec
60
60
  - bin/aebus
61
61
  - lib/aebus.rb
62
+ - lib/aebus/logging.rb
62
63
  - lib/aebus/version.rb
64
+ - lib/aebus/volume_status.rb
63
65
  - lib/config/config.rb
64
66
  - lib/config/volume.rb
65
67
  - lib/ec2/snapshot.rb