aebus 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.
@@ -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: []