aebus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ config/*
2
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in aebus.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,32 @@
1
+ # Aebus - Automatic EC2 BackUp Software
2
+
3
+ A small gem that allows you to easily automate EC2 backups, included purging of old backups and checking the backup status
4
+
5
+
6
+ ==Sample
7
+
8
+ default:
9
+ access_key_id: <your_access_key_id>
10
+ secret_access_key: <your_secret_access_key>
11
+ zone: eu-west1
12
+ backups:
13
+ daily:
14
+ enabled: true
15
+ keep: 7
16
+ when: 0 3 * * *
17
+ weekly:
18
+ enabled: true
19
+ keep: 5
20
+ when: 0 3 * * 1
21
+ monthly:
22
+ enabled: true
23
+ keep: all
24
+ when: 0 3 1 * *
25
+
26
+
27
+ vol-1234567:
28
+
29
+ vol-7654321:
30
+ backups:
31
+ daily:
32
+ enabled: false
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "aebus/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "aebus"
7
+ s.version = Aebus::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Marco Sandrini"]
10
+ s.email = ["nessche@gmail.com"]
11
+ s.homepage = "https://github.com/nessche/Aebus"
12
+ s.summary = "Automated EC2 BackUp Software"
13
+ s.description = "A tool to automate snapshot management in EC2"
14
+
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency("commander", "~> 4.0.6")
22
+ s.add_dependency("parse-cron", ">= 0.1.1")
23
+ s.add_dependency("amazon-ec2", "~> 0.9.17")
24
+
25
+ # specify any dependencies here; for example:
26
+ # s.add_development_dependency "rspec"
27
+ # s.add_runtime_dependency "rest-client"
28
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+ require 'pathname'
6
+ require_relative '../lib/aebus'
7
+
8
+ DEFAULT_CONFIG_NAME = "./aebus.yaml"
9
+
10
+ program :version, Aebus::VERSION
11
+ program :description, 'Automatic EC2 BackUp Software'
12
+
13
+ global_option('-c','--config FILE', 'The YAML file containing the backup configuration')
14
+
15
+ default_command :help
16
+
17
+ command :status do |c|
18
+ c.syntax = 'aebus status [options]'
19
+ c.summary = 'Checks for backup status'
20
+ c.description = <<-eos
21
+ Checks for the backup status of a given set of EC2 Volumes.
22
+ If no volume is specified in the command line, all volumes defined in the config files are checked
23
+ eos
24
+ c.example 'Checks the status of all volumes defined in the config file', 'aebus status'
25
+ c.example 'Checks the status of vol1 and vol2', 'aebus status vol1 vol2'
26
+
27
+ c.when_called do |args, options|
28
+ options.default \
29
+ :config => DEFAULT_CONFIG_NAME
30
+ raise ("Config file does not exist") unless FileTest.exist?(Pathname.new(options.config).realpath)
31
+ aebus = Aebus::Aebus.new
32
+ aebus.status(args,options)
33
+ end
34
+ end
35
+
36
+ command :backup do |c|
37
+ c.syntax = 'aebus backup [options]'
38
+ c.summary = 'Backs up a set of EC2 Volumes'
39
+ c.description = <<-eos
40
+ Backs up a set of EC2 Volumes, according to the configuration file. If no volume is specified in the
41
+ command line, all volumes defined in the config file are backed up
42
+ eos
43
+ c.example 'Manually creates backup of vol1 and vol2', 'aebus backup --manual vol1 vol2'
44
+ c.example 'Create, if needed, backups for all volumes', 'aebus backup'
45
+ c.option '--manual', 'Starts a manual backup (always creates a snapshot for each volume)'
46
+ c.option '--[no-]purge', 'Do not purge expired backups, defaults to --purge'
47
+ c.when_called do |args, options|
48
+ options.default \
49
+ :config => DEFAULT_CONFIG_NAME,
50
+ :manual => false,
51
+ :purge => true
52
+ raise ("Config file does not exist") unless FileTest.exist?(Pathname.new(options.config).realpath)
53
+ aebus = Aebus::Aebus.new
54
+ aebus.backup(args,options)
55
+ end
56
+ end
57
+
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'AWS'
5
+ require_relative 'config/config'
6
+ require_relative 'aebus_zones'
7
+ require_relative 'ec2/snapshot'
8
+
9
+ module Aebus
10
+
11
+ VERSION = '0.0.1'
12
+
13
+ class Aebus
14
+
15
+ AWS_NAME_TAG = "Name"
16
+ AEBUS_TAG = "Aebus"
17
+
18
+ def status(args, options)
19
+ current_time_utc = Time.now.utc
20
+ config = Config::Config.new(File.join(File.dirname("."), options.config), current_time_utc)
21
+ @ec2 = AWS::EC2::Base.new(:access_key_id => config.defaults["access_key_id"],
22
+ :secret_access_key => config.defaults["secret_access_key"],
23
+ :server => zone_to_url(config.defaults["zone"]))
24
+
25
+ target_volumes = calculate_target_volumes(config, args)
26
+ snap_map = get_snapshots_map
27
+
28
+ target_volumes.each do |target|
29
+ volume = config.volumes[target]
30
+ tags = volume.backups_to_be_run(snap_map[target])
31
+ if (tags.count > 0) then
32
+ puts ("[INFO] Volume #{target} needs to be backed up. Tags: #{tags.join(',')}")
33
+ else
34
+ puts ("[INFO] Volume #{target} does not need to be backed up")
35
+ end
36
+
37
+ purgeable_snapshots =volume.purgeable_snapshots(snap_map[target])
38
+ puts ("[INFO] Volume #{target} has #{purgeable_snapshots.count} purgeable snapshot(s): #{purgeable_snapshots.inject([]){|x, snap| x << snap.id}.join(',')}")
39
+
40
+ end
41
+
42
+
43
+ end
44
+
45
+ def backup(args, options)
46
+
47
+ current_time_utc = Time.now.utc
48
+ config = Config::Config.new(File.join(File.dirname("."), options.config), current_time_utc)
49
+ @ec2 = AWS::EC2::Base.new(:access_key_id => config.defaults["access_key_id"],
50
+ :secret_access_key => config.defaults["secret_access_key"],
51
+ :server => zone_to_url(config.defaults["zone"]))
52
+
53
+ target_volumes = calculate_target_volumes(config, args)
54
+ if (options.manual) then
55
+
56
+ target_volumes.each do |volume|
57
+
58
+ backup_volume(volume, current_time_utc, [EC2::AEBUS_MANUAL_TAG])
59
+
60
+ end
61
+
62
+ else
63
+
64
+ snap_map = get_snapshots_map
65
+ target_volumes.each do |target|
66
+
67
+ volume = config.volumes[target]
68
+ tags = volume.backups_to_be_run(snap_map[target])
69
+ if (tags.count > 0) then
70
+ tags << EC2::AEBUS_AUTO_TAG
71
+ puts("[INFO] Creating backup for volume #{target} with tags #{tags.join(',')}")
72
+ backup_volume(target, current_time_utc, tags)
73
+ else
74
+ puts ("[INFO] Volume #{target} does not need to be backed up")
75
+ end
76
+
77
+ end
78
+
79
+ snap_map = get_snapshots_map # we reload the map since we may have created more snapshots
80
+ if (options.purge) then
81
+ target_volumes.each do |target|
82
+ volume = config.volumes[target]
83
+ purgeable_snapshots = volume.purgeable_snapshots(snap_map[target])
84
+ purgeable_snapshots.each {|snapshot| purge_snapshot(snapshot.id)}
85
+ end
86
+ else
87
+ puts("[INFO] Skipping purging phase")
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
94
+ def calculate_target_volumes(config, args)
95
+
96
+ result = config.volume_ids
97
+ if (args && (args.count > 0)) then
98
+ result &= args
99
+ end
100
+
101
+ result
102
+
103
+ end
104
+
105
+ def list_volumes
106
+ response = @ec2.describe_volumes
107
+ puts(response)
108
+ end
109
+
110
+
111
+ def zone_to_url(zone)
112
+ ZONES[zone]
113
+ end
114
+
115
+ def backup_volume(volume_id, current_time_utc, tags)
116
+ begin
117
+ volume_info = @ec2.describe_volumes(:volume_id => volume_id)
118
+
119
+ rescue AWS::Error => e
120
+ puts("[WARNING] Volume Id #{volume_id} not found")
121
+ return false
122
+ end
123
+
124
+ begin
125
+ puts(volume_info)
126
+ volume_tags = volume_info.volumeSet.item[0].tagSet.item
127
+ puts(volume_tags)
128
+
129
+ name_and_desc = Aebus.calculate_name_and_desc(volume_id, volume_tags, current_time_utc)
130
+ puts(name_and_desc)
131
+ create_response = @ec2.create_snapshot(:volume_id => volume_id, :description => name_and_desc[1])
132
+ puts(create_response)
133
+
134
+ rescue AWS::Error => e
135
+ puts("[ERROR] Volume Id #{volume_id} could not be backed up")
136
+ return false
137
+ end
138
+
139
+ begin
140
+
141
+ @ec2.create_tags(:resource_id => create_response.snapshotId,
142
+ :tag => [{AWS_NAME_TAG => name_and_desc[0]}, {AEBUS_TAG => tags.join(',')}])
143
+ rescue AWS::Error => e
144
+ puts("[WARNING] Could not set tags to snapshot #{create_response.snapshotId}")
145
+ return false
146
+ end
147
+
148
+ puts("[INFO] Created snapshot #{create_response.snapshotId} for volume #{volume_id}");
149
+
150
+ return true
151
+
152
+ end
153
+
154
+
155
+ def self.calculate_name_and_desc(volume_id, tags, utc_time)
156
+
157
+ name = "backup_#{utc_time.strftime("%Y%m%d")}_#{volume_id}"
158
+ volume_name = volume_id
159
+ tags.each do |tag|
160
+ if tag["key"].eql?(AWS_NAME_TAG) then
161
+ volume_name = tag["value"]
162
+ break
163
+ end
164
+ end
165
+
166
+ description = "Backup for volume #{volume_name} taken at #{utc_time.strftime("%Y-%m-%d %H:%M:%S")}"
167
+
168
+ return [name, description]
169
+
170
+ end
171
+
172
+ def get_snapshots_map
173
+
174
+ response = @ec2.describe_snapshots(:owner => 'self')
175
+ snap_array = response.snapshotSet.item
176
+ result = Hash.new
177
+ snap_array.each do |snap|
178
+ snapshot = EC2::Snapshot.new(snap)
179
+ if (result.include?(snapshot.volume_id)) then
180
+ vol_array = result[snapshot.volume_id]
181
+ index = vol_array.index{ |s| snapshot.start_time > s.start_time}
182
+ index ||= vol_array.count
183
+ vol_array.insert(index, snapshot)
184
+ else
185
+ vol_array = Array.new
186
+ vol_array << snapshot
187
+ result.store(snapshot.volume_id, vol_array)
188
+ end
189
+ end
190
+ result
191
+
192
+ end
193
+
194
+ def purge_snapshot(snapshot_id)
195
+ begin
196
+ response = @ec2.delete_snapshot(:snapshot_id => snapshot_id)
197
+ if (response[return]) then
198
+ puts("[INFO] Purged snapshot #{snapshot_id}")
199
+ end
200
+ rescue AWS::Error => e
201
+ puts("[WARNING] Could not purge snapshot #{snapshot_id}")
202
+ end
203
+
204
+ end
205
+
206
+ end
207
+
208
+ end
@@ -0,0 +1,3 @@
1
+ module Aebus
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ module Aebus
2
+
3
+ ZONES = {
4
+
5
+ "eu-west1" => "eu-west-1.ec2.amazonaws.com",
6
+ "us-east-1" => "ec2.us-east-1.amazonaws.com",
7
+ "us-west-1" => "ec2.us-west-1.amazonaws.com",
8
+ "ap-southeast-1" => "ec2.ap-southeast-1.amazonaws.com"
9
+ }
10
+
11
+ end
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+ require 'cron_parser'
3
+ require_relative 'volume'
4
+
5
+ module Aebus
6
+
7
+ module Config
8
+
9
+ class Config
10
+
11
+ DEFAULT_STRING = 'default'
12
+
13
+ attr_reader :defaults, :volumes
14
+
15
+ def initialize(filename, current_time_utc)
16
+
17
+ yaml_root = YAML::load(File.open(filename))
18
+ raise "Cannot find configuration file" unless yaml_root
19
+
20
+ @defaults = yaml_root.delete(DEFAULT_STRING)
21
+ default_backups = BackupSchedule.parse_backups_config(current_time_utc, @defaults["backups"])
22
+
23
+ @volumes = Hash.new
24
+ yaml_root.each_pair do |k, v|
25
+ @volumes[k] = Volume.new(current_time_utc, k, v, default_backups)
26
+ end
27
+
28
+ end
29
+
30
+ def volume_ids
31
+
32
+ result = Array.new
33
+ @volumes.each_key do |k|
34
+ result << k
35
+ end
36
+
37
+ result
38
+
39
+ end
40
+
41
+ def get_value_for_volume(volume_id, key)
42
+ result = nil
43
+ if (@volumes.include? volume_id) then
44
+ if (@volumes[volume_id].config.include? key) then
45
+ result = @volumes[volume_id].config[key]
46
+ else
47
+ result = @defaults[key]
48
+ end
49
+ end
50
+ result
51
+ end
52
+
53
+
54
+
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,116 @@
1
+ require 'cron_parser'
2
+
3
+ module Aebus
4
+
5
+ module Config
6
+
7
+ KEEP_ALL = "all"
8
+
9
+ class BackupSchedule
10
+
11
+ attr_reader :label, :last_deadline, :next_deadline, :keep
12
+
13
+ def initialize (current_time_utc, label, backup_config)
14
+ @label = label
15
+ if (backup_config["enabled"]) then
16
+ calculate_deadlines(current_time_utc, backup_config["when"])
17
+ end
18
+ @keep = backup_config["keep"]
19
+
20
+ end
21
+
22
+ def calculate_deadlines(current_time_utc, when_string)
23
+ raise(ArgumentError, "when field cannot be empty if the backup is enabled") unless when_string
24
+
25
+ parser = CronParser.new (when_string)
26
+ @last_deadline = parser.last(current_time_utc)
27
+ @next_deadline = parser.next(current_time_utc)
28
+
29
+ end
30
+
31
+ def to_s
32
+ "Backup Schedule: label => #{@label} last_deadline => #{@last_deadline} next_deadline => #{@next_deadline} keep => #{@keep}"
33
+ end
34
+
35
+ def self.parse_backups_config(current_time_utc, backups_config)
36
+
37
+ return nil unless backups_config
38
+
39
+ result = Hash.new
40
+
41
+ backups_config.each_pair do |key,value|
42
+ result.store(key, BackupSchedule.new(current_time_utc, key, value))
43
+ end
44
+
45
+ result
46
+
47
+ end
48
+
49
+ end
50
+
51
+ class Volume
52
+
53
+ attr_reader :id, :config
54
+
55
+ def initialize(current_time_utc, volume_id, config, default_backups)
56
+
57
+ @config = config
58
+ @id = volume_id
59
+ @backups = default_backups ? default_backups.dup : Hash.new
60
+ if (config && config["backups"]) then
61
+ @backups.merge(BackupSchedule.parse_backups_config(current_time_utc,config["backups"]))
62
+ end
63
+ end
64
+
65
+ def backups_to_be_run(snapshots)
66
+
67
+ result = Array.new
68
+
69
+ @backups.each_pair do |k,v|
70
+
71
+ result << k unless recent_backup?(k, snapshots, v.last_deadline)
72
+
73
+ end
74
+ result
75
+ end
76
+
77
+ def recent_backup?(label, snapshots, last_deadline)
78
+
79
+ snapshots.each do |snapshot|
80
+
81
+ if (snapshot.aebus_tags_include?(label) && (snapshot.start_time > last_deadline))
82
+ return true
83
+ end
84
+
85
+ end
86
+ false
87
+ end
88
+
89
+ def purgeable_snapshots(snapshots)
90
+ puts(snapshots.count)
91
+ removables = snapshots.select{|snapshot| snapshot.aebus_removable_snapshot?}
92
+ puts(removables.count)
93
+ available_backups = @backups.each_with_object({}) { | (k, v) , h | h[k] = v.keep}
94
+ removables.each do |snapshot|
95
+ 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
+ snapshot.keep = true
102
+ available_backups[tag] -= 1
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ removables.select{|snapshot| !snapshot.keep? }
109
+
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,69 @@
1
+ module Aebus
2
+
3
+ module EC2
4
+
5
+ AEBUS_TAG = "Aebus"
6
+ AEBUS_MANUAL_TAG = "manual"
7
+ AEBUS_KEEP_TAG = "keep"
8
+ AEBUS_AUTO_TAG = "auto"
9
+
10
+ class Snapshot
11
+
12
+
13
+
14
+ attr_reader :start_time, :volume_id, :id, :tags
15
+
16
+ def initialize(hash)
17
+
18
+ raise(ArgumentError,"hash cannot be nil") unless hash
19
+ @keep
20
+ @id = hash.snapshotId
21
+ @start_time = Time.parse(hash.startTime)
22
+ @volume_id = hash.volumeId
23
+ @tags = Hash.new
24
+ if (hash.tagSet) then
25
+ tag_array = hash.tagSet.item
26
+ tag_array.each do |tag|
27
+ @tags.store(tag["key"],tag["value"])
28
+ end
29
+ end
30
+ end
31
+
32
+ def to_s
33
+ "{snapshot_id => #{@id}, volume_id => #{@volume_id}, start_time => #{@start_time}, tags => #{@tags} "
34
+ end
35
+
36
+ def aebus_tags_include?(label)
37
+ if aebus_snapshot? then
38
+ aebus_tags = @tags[AEBUS_TAG].split(',')
39
+ return aebus_tags.include? label
40
+ end
41
+ false
42
+ end
43
+
44
+ def aebus_snapshot?
45
+ @tags.include?(AEBUS_TAG)
46
+ end
47
+
48
+ def aebus_removable_snapshot?
49
+ (aebus_tags & [AEBUS_MANUAL_TAG, AEBUS_KEEP_TAG]).count == 0
50
+ end
51
+
52
+ def aebus_tags
53
+ return nil unless aebus_snapshot?
54
+ @tags[AEBUS_TAG].split(',')
55
+ end
56
+
57
+ def keep= value
58
+ @keep = value
59
+ end
60
+
61
+ def keep?
62
+ @keep
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aebus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marco Sandrini
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-05 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: commander
16
+ requirement: &70097059520900 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 4.0.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70097059520900
25
+ - !ruby/object:Gem::Dependency
26
+ name: parse-cron
27
+ requirement: &70097059520400 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70097059520400
36
+ - !ruby/object:Gem::Dependency
37
+ name: amazon-ec2
38
+ requirement: &70097059519940 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 0.9.17
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70097059519940
47
+ description: A tool to automate snapshot management in EC2
48
+ email:
49
+ - nessche@gmail.com
50
+ executables:
51
+ - aebus
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - README
58
+ - Rakefile
59
+ - aebus.gemspec
60
+ - bin/aebus
61
+ - lib/aebus.rb
62
+ - lib/aebus/version.rb
63
+ - lib/aebus_zones.rb
64
+ - lib/config/config.rb
65
+ - lib/config/volume.rb
66
+ - lib/ec2/snapshot.rb
67
+ homepage: https://github.com/nessche/Aebus
68
+ licenses: []
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.10
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Automated EC2 BackUp Software
91
+ test_files: []