mongolly 0.0.7 → 0.1.0
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/bin/mongolly +21 -65
- data/lib/mongolly.rb +8 -2
- data/lib/mongolly/extensions.rb +6 -0
- data/lib/mongolly/extensions/aws/ec2/instance.rb +15 -0
- data/lib/mongolly/extensions/aws/ec2/instance_collection.rb +30 -0
- data/lib/mongolly/extensions/bson/timestamp.rb +12 -0
- data/lib/mongolly/extensions/mongo/mongo_client.rb +58 -0
- data/lib/mongolly/extensions/mongo/mongo_replica_set_client.rb +14 -0
- data/lib/mongolly/shepherd.rb +39 -0
- data/lib/mongolly/version.rb +1 -1
- data/mongolly.gemspec +1 -1
- metadata +10 -4
- data/lib/mongolly/snapshot_manager.rb +0 -59
data/bin/mongolly
CHANGED
@@ -2,47 +2,31 @@
|
|
2
2
|
require 'thor'
|
3
3
|
require 'yaml'
|
4
4
|
require 'time'
|
5
|
-
require 'mongo'
|
6
5
|
require 'mongolly'
|
7
6
|
|
8
7
|
module Mongolly
|
9
8
|
class Runner < Thor
|
10
9
|
|
10
|
+
CONFIG_PATH = File.expand_path '~/.mongolly'
|
11
|
+
|
11
12
|
def initialize(*args)
|
12
13
|
super
|
13
14
|
@config = read_config
|
14
|
-
|
15
|
-
exit unless valid_config?
|
15
|
+
exit unless @config && valid_config?
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def snapshot
|
23
|
-
db = Mongo::Connection.new( @db_config["host"], @db_config["port"] )
|
24
|
-
db['admin'].authenticate( @db_config["user"], @db_config["pass"] )
|
25
|
-
|
26
|
-
SnapshotManager.take_snapshots( db, @config["aws_access_key_id"], @config["aws_secret_access_key"], @db_config["region"], @db_config["volumes"] )
|
18
|
+
desc "backup", "Snapshots the Database EBS Volumes"
|
19
|
+
def backup
|
20
|
+
puts " ** Backing up ..."
|
21
|
+
Shepherd.new(@config).backup
|
27
22
|
end
|
28
23
|
|
29
|
-
desc "clean", "
|
30
|
-
method_option :
|
31
|
-
method_option :database, aliases: '-d'
|
24
|
+
desc "clean", "Removes old Database EBS Snapshots"
|
25
|
+
method_option :age, aliases: '-a', required: true
|
32
26
|
def clean
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
ec2 = AWS::EC2.new(access_key_id: @config["aws_access_key_id"], secret_access_key: @config["aws_secret_access_key"]).regions[@db_config["region"]]
|
38
|
-
ec2.snapshots.with_owner(:self).each do |snapshot|
|
39
|
-
unless snapshot.tags[:created_at].nil? || snapshot.tags[:backup_key].nil?
|
40
|
-
if Time.parse(snapshot.tags[:created_at]) < max_age
|
41
|
-
puts " ** Deleting #{snapshot.id} created on #{snapshot.tags[:created_at]} with key #{snapshot.tags[:backup_key]}"
|
42
|
-
snapshot.delete
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
27
|
+
age = Time.parse(options[:age])
|
28
|
+
puts " ** Cleaning snapshots older than #{age}"
|
29
|
+
Shepherd.new(@config).cleanup(age)
|
46
30
|
end
|
47
31
|
|
48
32
|
private
|
@@ -50,18 +34,12 @@ module Mongolly
|
|
50
34
|
return true if File.exists? CONFIG_PATH
|
51
35
|
|
52
36
|
empty_config = {
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
region: nil,
|
60
|
-
volumes: [nil]
|
61
|
-
}
|
62
|
-
},
|
63
|
-
aws_access_key_id: nil,
|
64
|
-
aws_secret_access_key: nil,
|
37
|
+
database: [],
|
38
|
+
db_username: nil,
|
39
|
+
db_password: nil,
|
40
|
+
access_key_id: nil,
|
41
|
+
secret_access_key: nil,
|
42
|
+
aws_region: 'us-east-1'
|
65
43
|
}
|
66
44
|
|
67
45
|
File.open( CONFIG_PATH, "w" ) do |f|
|
@@ -77,7 +55,7 @@ module Mongolly
|
|
77
55
|
end
|
78
56
|
|
79
57
|
def read_config
|
80
|
-
return unless seed_config
|
58
|
+
return false unless seed_config
|
81
59
|
begin
|
82
60
|
return YAML::load( File.read( CONFIG_PATH ) )
|
83
61
|
rescue e
|
@@ -86,31 +64,9 @@ module Mongolly
|
|
86
64
|
end
|
87
65
|
end
|
88
66
|
|
89
|
-
def database_config
|
90
|
-
if options[:database].to_s.strip.empty? && @config["databases"].size > 1
|
91
|
-
raise ArgumentError.new("Database name not provided and more than database specified in the config file")
|
92
|
-
elsif ! @config["databases"].keys.include? options[:database].to_s.strip
|
93
|
-
raise ArgumentError.new("Database #{options[:database]} not defined in config")
|
94
|
-
end
|
95
|
-
|
96
|
-
db_name = options[:database].to_s.strip.empty? ? @config["databases"].keys.first : options[:database]
|
97
|
-
db_config = @config["databases"][db_name]
|
98
|
-
|
99
|
-
%w(host port user pass region).each do |arg|
|
100
|
-
if db_config[arg].to_s.strip.empty?
|
101
|
-
raise ArgumentError.new( "#{arg} for database #{db_name} cannot be empty" )
|
102
|
-
end
|
103
|
-
end
|
104
|
-
if db_config["volumes"].empty? or db_config["volumes"].map { |v| v.to_s.strip.empty? }.include?(true)
|
105
|
-
raise ArgumentError.new("volumes cannot be empty or include an empty string for database #{db_name}")
|
106
|
-
end
|
107
|
-
|
108
|
-
return db_name, db_config
|
109
|
-
end
|
110
|
-
|
111
67
|
def valid_config?
|
112
|
-
%w(
|
113
|
-
raise ArgumentError.new("#{arg} cannot be empty") if @config[arg].to_s.strip.empty?
|
68
|
+
%w(database db_username db_password access_key_id secret_access_key).each do |arg|
|
69
|
+
raise ArgumentError.new("#{arg} cannot be empty") if @config[arg.to_sym].to_s.strip.empty?
|
114
70
|
end
|
115
71
|
return true
|
116
72
|
end
|
data/lib/mongolly.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'mongolly/extensions/aws/ec2/instance'
|
2
|
+
require 'mongolly/extensions/aws/ec2/instance_collection'
|
3
|
+
require 'mongolly/extensions/bson/timestamp'
|
4
|
+
require 'mongolly/extensions/mongo/mongo_client'
|
5
|
+
require 'mongolly/extensions/mongo/mongo_replica_set_client'
|
6
|
+
|
7
|
+
require 'mongolly/version'
|
8
|
+
require 'mongolly/shepherd'
|
3
9
|
|
4
10
|
module Mongolly
|
5
11
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
class AWS::EC2::Instance
|
4
|
+
|
5
|
+
def volumes_with_tag(key)
|
6
|
+
volumes = []
|
7
|
+
attachments.each do |name,attachment|
|
8
|
+
next unless attachment.status == :attached
|
9
|
+
volume = attachment.volume
|
10
|
+
volumes << volume if volume.status == :in_use && volume.tags.has_key?(key)
|
11
|
+
end
|
12
|
+
volumes
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'socket'
|
3
|
+
require 'ipaddress'
|
4
|
+
|
5
|
+
class AWS::EC2::InstanceCollection
|
6
|
+
|
7
|
+
def find_from_address(address, port = 27107)
|
8
|
+
ip_address = convert_address_to_ip(address, port)
|
9
|
+
|
10
|
+
instances = select do |instance|
|
11
|
+
instance.public_ip_address == ip_address || instance.private_ip_address == ip_address
|
12
|
+
end
|
13
|
+
raise error_class "InstanceNotFound" if instances.length != 1
|
14
|
+
|
15
|
+
return instances.first
|
16
|
+
rescue SocketError
|
17
|
+
raise RuntimeError.new("Unable to determine IP address from #{address}")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def convert_address_to_ip(address, port)
|
22
|
+
return address if ::IPAddress.valid? address
|
23
|
+
|
24
|
+
ip_addresses = ::Addrinfo.getaddrinfo(address, port, nil, :STREAM)
|
25
|
+
raise error_class "MultipleIpAddressFound" if ip_addresses.length > 1
|
26
|
+
|
27
|
+
return ip_addresses[0].ip_address
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
|
3
|
+
class Mongo::MongoClient
|
4
|
+
|
5
|
+
def snapshot_ebs(options={})
|
6
|
+
options[:volume_tag] ||= 'mongolly'
|
7
|
+
ec2 = AWS::EC2.new(access_key_id: options[:access_key_id], secret_access_key: options[:secret_access_key], region: options[:region])
|
8
|
+
|
9
|
+
instance = ec2.instances.find_from_address(*snapshot_ebs_target.split(':'))
|
10
|
+
volumes = instance.volumes_with_tag(options[:volume_tag])
|
11
|
+
|
12
|
+
raise RuntimeError "no suitable volumes found" unless volumes.length > 0
|
13
|
+
|
14
|
+
begin
|
15
|
+
if volumes.length >= 1
|
16
|
+
disable_profiling
|
17
|
+
lock!
|
18
|
+
end
|
19
|
+
|
20
|
+
backup_key = (0...8).map{65.+(rand(25)).chr}.join
|
21
|
+
volumes.each do |volume|
|
22
|
+
snapshot = volume.create_snapshot("#{backup_key} #{Time.now} mongolly backup")
|
23
|
+
snapshot.add_tag('created_at', value: Time.now)
|
24
|
+
snapshot.add_tag('backup_key', value: backup_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
ensure
|
28
|
+
unlock! if locked?
|
29
|
+
enable_profiling
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def snapshot_ebs_target
|
35
|
+
host_port.join(':')
|
36
|
+
end
|
37
|
+
|
38
|
+
def disable_profiling
|
39
|
+
@profiled_dbs = {}
|
40
|
+
database_names.each do |db|
|
41
|
+
begin
|
42
|
+
@profiled_dbs[db] = self[db].profiling_level unless self[db].profiling_level == :off
|
43
|
+
self[db].profiling_level = :off
|
44
|
+
rescue Mongo::InvalidNSName
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def enable_profiling
|
50
|
+
return false if locked?
|
51
|
+
return true unless @profiled_dbs
|
52
|
+
@profiled_dbs.each do |db,level|
|
53
|
+
self[db].profiling_level = level rescue Mongo::InvalidNSName
|
54
|
+
end
|
55
|
+
return true
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
|
3
|
+
class Mongo::MongoReplicaSetClient
|
4
|
+
|
5
|
+
def most_current_secondary
|
6
|
+
replica = self['admin'].command( replSetGetStatus: 1 )
|
7
|
+
replica['members'].select { |m| m['state'] == 2 }.sort_by { |m| m['optime'] }.reverse.first['name']
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
def snapshot_ebs_target
|
12
|
+
most_current_secondary
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Mongolly
|
2
|
+
class Shepherd
|
3
|
+
|
4
|
+
def initialize(options={})
|
5
|
+
@access_key_id = options[:access_key_id]
|
6
|
+
@secret_access_key = options[:secret_access_key]
|
7
|
+
@region = options[:aws_region] || 'us-east-1'
|
8
|
+
@database = options[:database]
|
9
|
+
@db_username = options[:db_username]
|
10
|
+
@db_password = options[:db_password]
|
11
|
+
end
|
12
|
+
|
13
|
+
def backup
|
14
|
+
connection.snapshot_ebs(access_key_id: @access_key_id, secret_access_key: @secret_access_key, region: @region)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cleanup(age)
|
18
|
+
raise ArgumentError.new("Must provide a Time object cleanup") unless age.class <= Time
|
19
|
+
|
20
|
+
ec2 = AWS::EC2.new(access_key_id: @access_key_id, secret_access_key: @secret_access_key, region: @region)
|
21
|
+
|
22
|
+
ec2.snapshots.with_owner(:self).each do |snapshot|
|
23
|
+
unless snapshot.tags[:created_at].nil? || snapshot.tags[:backup_key].nil?
|
24
|
+
snapshot.delete if Time.parse(snapshot.tags[:created_at]) < age
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def connection
|
30
|
+
db = if @database.is_a? Array
|
31
|
+
Mongo::MongoReplicaSetClient.new(@database)
|
32
|
+
else
|
33
|
+
Mongo::MongoClient.new(*@database.split(':'))
|
34
|
+
end
|
35
|
+
db['admin'].authenticate(@db_username, @db_password, true)
|
36
|
+
return db
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/mongolly/version.rb
CHANGED
data/mongolly.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongolly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-05-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 1.5.8
|
78
78
|
- !ruby/object:Gem::Dependency
|
79
|
-
name:
|
79
|
+
name: ipaddress
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
81
81
|
none: false
|
82
82
|
requirements:
|
@@ -106,7 +106,13 @@ files:
|
|
106
106
|
- Rakefile
|
107
107
|
- bin/mongolly
|
108
108
|
- lib/mongolly.rb
|
109
|
-
- lib/mongolly/
|
109
|
+
- lib/mongolly/extensions.rb
|
110
|
+
- lib/mongolly/extensions/aws/ec2/instance.rb
|
111
|
+
- lib/mongolly/extensions/aws/ec2/instance_collection.rb
|
112
|
+
- lib/mongolly/extensions/bson/timestamp.rb
|
113
|
+
- lib/mongolly/extensions/mongo/mongo_client.rb
|
114
|
+
- lib/mongolly/extensions/mongo/mongo_replica_set_client.rb
|
115
|
+
- lib/mongolly/shepherd.rb
|
110
116
|
- lib/mongolly/version.rb
|
111
117
|
- mongolly.gemspec
|
112
118
|
homepage: http://www.github.com/msaffitz/mongolly
|
@@ -1,59 +0,0 @@
|
|
1
|
-
require 'aws-sdk'
|
2
|
-
|
3
|
-
module Mongolly
|
4
|
-
class SnapshotManager
|
5
|
-
|
6
|
-
def self.take_snapshots(db, aws_key_id, aws_secret_key, region, volume_ids)
|
7
|
-
profile_levels = {}
|
8
|
-
|
9
|
-
unless db.locked?
|
10
|
-
puts " ** Locking Database"
|
11
|
-
db.database_names.each do |db_name|
|
12
|
-
begin
|
13
|
-
level = db[db_name].profiling_level
|
14
|
-
if level != :off
|
15
|
-
profile_levels[db_name] = level
|
16
|
-
db[db_name].profiling_level = :off
|
17
|
-
end
|
18
|
-
rescue Mongo::InvalidNSName
|
19
|
-
puts " ** Skiping #{db_name} with invalid database name"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
db.lock!
|
23
|
-
end
|
24
|
-
|
25
|
-
begin
|
26
|
-
ec2 = AWS::EC2.new(access_key_id: aws_key_id, secret_access_key: aws_secret_key).regions[region]
|
27
|
-
backup_key = (0...8).map{65.+(rand(25)).chr}.join
|
28
|
-
|
29
|
-
puts " ** Starting Snapshot with key #{backup_key}"
|
30
|
-
|
31
|
-
volume_ids.map{ |v| v.to_s.strip }.each do |volume_id|
|
32
|
-
puts " ** Taking snapshot of volume #{volume_id}"
|
33
|
-
volume = ec2.volumes[volume_id]
|
34
|
-
raise RuntimeError.new("Volume #{volume_id} does not exist") unless volume.exists?
|
35
|
-
|
36
|
-
snapshot = volume.create_snapshot("#{backup_key} #{Time.now} mongo backup")
|
37
|
-
snapshot.add_tag('created_at', value: Time.now)
|
38
|
-
snapshot.add_tag('backup_key', value: backup_key)
|
39
|
-
end
|
40
|
-
ensure
|
41
|
-
if db.locked?
|
42
|
-
puts " ** Unlocking Database"
|
43
|
-
db.unlock!
|
44
|
-
db.database_names.each do |db_name|
|
45
|
-
begin
|
46
|
-
level = profile_levels[db_name]
|
47
|
-
unless level.nil? || level == :off
|
48
|
-
puts " ** Setting #{db_name} profile level to #{level.to_s}"
|
49
|
-
db[db_name].profiling_level = level
|
50
|
-
end
|
51
|
-
rescue Mongo::InvalidNSName
|
52
|
-
puts " ** Skiping #{db_name} with invalid database name"
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|