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.
@@ -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
- @db_name, @db_config = database_config
15
- exit unless valid_config?
15
+ exit unless @config && valid_config?
16
16
  end
17
17
 
18
- CONFIG_PATH = File.expand_path '~/.mongolly'
19
-
20
- desc "snapshot", "takes an EBS snapshot of the given volumes"
21
- method_option :database, aliases: '-d'
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", "removes snapshots older than the given data"
30
- method_option :maximum_age, aliases: '-a', required: true
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
- max_age = Time.parse(options[:maximum_age])
34
-
35
- puts " ** Cleaning snapshots older than #{max_age}"
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
- databases: {
54
- dbname: {
55
- host: nil,
56
- port: nil,
57
- user: nil,
58
- pass: nil,
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(aws_access_key_id aws_secret_access_key).each do |arg|
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
@@ -1,5 +1,11 @@
1
- require "mongolly/version"
2
- require "mongolly/snapshot_manager"
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,6 @@
1
+ require 'extensions/aws/ec2/instance'
2
+ require 'extensions/aws/ec2/instance_collection'
3
+ require 'extensions/bson/timestamp'
4
+ require 'extensions/mongo/mongo_client'
5
+ require 'extensions/mongo/mongo_replica_set_client'
6
+
@@ -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,12 @@
1
+ require 'mongo'
2
+
3
+ class BSON::Timestamp
4
+ include Comparable
5
+
6
+ def <=>(other)
7
+ s = seconds <=> other.seconds
8
+ return s unless s == 0
9
+ increment <=> other.increment
10
+ end
11
+
12
+ 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
@@ -1,3 +1,3 @@
1
1
  module Mongolly
2
- VERSION = "0.0.7"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -21,6 +21,6 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency("mongo", ["~> 1.8.3"])
22
22
  gem.add_dependency("bson_ext", ["~> 1.8.3"])
23
23
  gem.add_dependency("aws-sdk", ["~> 1.5.8"])
24
- gem.add_dependency("debugger")
24
+ gem.add_dependency("ipaddress")
25
25
 
26
26
  end
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.7
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-03-21 00:00:00.000000000 Z
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: debugger
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/snapshot_manager.rb
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