cryo 0.0.1 → 0.0.2

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/.gitignore CHANGED
@@ -15,3 +15,8 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *~
19
+ cryo.rc
20
+ \#*\#
21
+ .\#*
22
+ cryo.arx
data/Makefile ADDED
@@ -0,0 +1,5 @@
1
+ cryo.arx:
2
+ git archive HEAD | bzip2 | arx tmpx -rm! - -e ./bin/bootstrap_cryo.sh FOO=bar > cryo.arx
3
+
4
+ clean:
5
+ rm cryo.arx
@@ -0,0 +1,3 @@
1
+ #!/bin/bash -e
2
+
3
+ echo hey there
data/bin/cryo ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/cryo"
4
+ require 'trollop'
5
+
6
+ required_parameters = ["archive_bucket",
7
+ "archive_frequency",
8
+ "aws_access_key",
9
+ "aws_secret_key",
10
+ "host",
11
+ "password",
12
+ "snapshot_bucket",
13
+ "snapshot_frequency",
14
+ "snapshot_period",
15
+ "snapshot_prefix",
16
+ "sns_topic",
17
+ "tmp_path",
18
+ "user",
19
+ ]
20
+
21
+ mode = ARGV.shift
22
+
23
+ options = Trollop::options do
24
+ version "cryo #{Cryo::VERSION} (c) 2013 Airbnb"
25
+ banner <<-END_OF_BANNER
26
+
27
+ #{version}
28
+
29
+ Welcome to Cryo, a simple backup utility
30
+
31
+ All options can be passed in with either the given command line options, or passed in via
32
+ environment with the same name, capitalized, and with CRYO_ appended. Like: CRYO_HOST instead of --host
33
+
34
+ More docs and examples can be found at https://github.com/airbnb/cryo
35
+
36
+ Usage:
37
+ cryo [redis,mysql,postgres,list,get] [options]
38
+
39
+ where [options] are:
40
+
41
+ END_OF_BANNER
42
+
43
+ opt(:tmp_path,
44
+ "where should temp files be created",
45
+ :type => String,
46
+ :default => ENV['CRYO_TMP_PATH'])
47
+
48
+ opt(:snapshot_frequency,
49
+ "how often to take backups (in mins)",
50
+ :type => Integer,
51
+ :default => ENV['CRYO_SNAPSHOT_FREQUENCY'].to_i)
52
+
53
+ opt(:archive_frequency,
54
+ "maxium time in between archives (in mins)",
55
+ :type => Integer,
56
+ :default => ENV['CRYO_ARCHIVE_FREQUENCY'].to_i)
57
+
58
+ opt(:snapshot_period,
59
+ "time before snapshots get deleted or archived (in mins)",
60
+ :type => Integer,
61
+ :default => ENV['CRYO_SNAPSHOT_PERIOD'].to_i)
62
+
63
+ opt(:snapshot_bucket,
64
+ "s3 bucket to use for snapshots",
65
+ :type => String,
66
+ :default => ENV['CRYO_SNAPSHOT_BUCKET'])
67
+
68
+ opt(:snapshot_prefix,
69
+ "s3 object prefix to use for snapshots",
70
+ :type => String,
71
+ :default => ENV['CRYO_SNAPSHOT_PREFIX'])
72
+
73
+ opt(:archive_bucket,
74
+ "s3 bucket to use for archives",
75
+ :type => String,
76
+ :default => ENV['CRYO_ARCHIVE_BUCKET'])
77
+
78
+ opt(:archive_prefix,
79
+ "s3 object prefix to use for archives",
80
+ :type => String,
81
+ :default => ENV['CRYO_ARCHIVE_PREFIX'])
82
+
83
+ opt(:sns_topic,
84
+ "sns topic",
85
+ :type => String,
86
+ :default => ENV['CRYO_SNS_TOPIC'])
87
+
88
+ opt(:aws_access_key,
89
+ "aws_access_key. Can be set using the AWS_ACCESS_KEY environment variable",
90
+ :type => String,
91
+ :default => ENV['CRYO_AWS_ACCESS_KEY'])
92
+
93
+ opt(:aws_secret_key,
94
+ "aws_secret_key. Can be set using the AWS_SECRET_KEY environment variable",
95
+ :type => String,
96
+ :default => ENV['CRYO_AWS_SECRET_KEY'])
97
+
98
+ opt(:host,
99
+ "remote host. Can be set using the CRYO_HOST environment variable",
100
+ :type => String,
101
+ :default => ENV['CRYO_HOST'])
102
+
103
+ opt(:user,
104
+ "remote user",
105
+ :type => String,
106
+ :default => ENV['CRYO_USER'])
107
+
108
+ opt(:password,
109
+ "remote password",
110
+ :type => String,
111
+ :default => ENV['CRYO_PASSWORD'])
112
+
113
+
114
+ case mode
115
+ when 'mysql'
116
+ when 'redis'
117
+ opt(:path,
118
+ "path to redis database file",
119
+ :type => String,
120
+ :default => ENV['CRYO_PATH'])
121
+ required_parameters << 'path'
122
+ when 'postgres'
123
+ when 'list'
124
+ when 'get'
125
+ else
126
+ unless mode == '--help'
127
+ STDERR.puts "ERROR! bad input. first option needs to be one of [redis, postgres, mysql, list, get]"
128
+ STDERR.puts "Please use --help for more info"
129
+ exit 1
130
+ end
131
+ end
132
+ end
133
+
134
+ options.merge!(type: mode)
135
+
136
+
137
+ required_parameters.each do |arg|
138
+ Trollop::die arg.to_sym, "needs to specified on the commandline or set by the CRYO_#{arg.upcase} environment variable" \
139
+ if options[arg.to_sym].nil? or ! options[arg.to_sym]
140
+ end unless mode == 'get'
141
+
142
+
143
+ run = Cryo.new(options)
144
+
145
+ case mode
146
+ when 'list'
147
+ run.list_snapshots
148
+ when 'get'
149
+ snapshot = ARGV.shift
150
+ raise "you did not specify a snapshot!" if snapshot.nil?
151
+ run.get_snapshot(snapshot)
152
+ else
153
+ run.backup!
154
+ run.archive_and_purge
155
+ end
156
+
157
+ #
158
+ # run.list_archives
data/bin/test ADDED
@@ -0,0 +1,100 @@
1
+ options = Trollop::options do
2
+
3
+ opt(:snapshot_frequency,
4
+ "how often to take backups (in mins)",
5
+ :type => Integer,
6
+ :default => ENV['CRYO_SNAPSHOT_FREQUENCY'].to_i || nil,
7
+ :required => true)
8
+
9
+ opt(:archive_frequency,
10
+ "maxium time in between archives (in mins)",
11
+ :type => Integer,
12
+ :default => ENV['CRYO_ARCHIVE_FREQUENCY'].to_i || nil,
13
+ :required => true)
14
+
15
+ opt(:snapshot_period,
16
+ "time before snapshots get deleted or archived (in mins)",
17
+ :type => Integer,
18
+ :default => ENV['CRYO_SNAPSHOT_PERIOD'].to_i || nil,
19
+ :required => true)
20
+
21
+ opt(:snapshot_bucket,
22
+ "s3 bucket to use for snapshots",
23
+ :type => String,
24
+ :default => ENV['CRYO_SNAPSHOT_BUCKET'] || nil,
25
+ :required => true)
26
+
27
+ opt(:snapshot_prefix,
28
+ "s3 object prefix to use for snapshots",
29
+ :type => String,
30
+ :default => ENV['CRYO_SNAPSHOT_PREFIX'] || nil,
31
+ :required => true)
32
+
33
+ opt(:archive_bucket,
34
+ "s3 bucket to use for archives",
35
+ :type => String,
36
+ :default => ENV['CRYO_ARCHIVE_BUCKET'] || nil,
37
+ :required => true)
38
+
39
+ opt(:archive_prefix,
40
+ "s3 object prefix to use for archives",
41
+ :type => String,
42
+ :default => ENV['CRYO_ARCHIVE_PREFIX'] || nil,
43
+ :required => true)
44
+
45
+ opt(:sns_topic,
46
+ "sns topic",
47
+ :type => String,
48
+ :default => ENV['CRYO_SNS_TOPIC'] || nil,
49
+ :required => true)
50
+
51
+
52
+
53
+
54
+
55
+
56
+ opt(:host,
57
+ "remote host. Can be set using the CRYO_HOST environment variable",
58
+ :type => String,
59
+ :default => ENV['CRYO_HOST'] || nil,
60
+ :required => true)
61
+
62
+ opt(:user,
63
+ "remote user",
64
+ :type => String,
65
+ :default => ENV['CRYO_USER'] || nil,
66
+ :required => true)
67
+
68
+ opt(:password,
69
+ "remote password",
70
+ :type => String,
71
+ :default => ENV['CRYO_PASSWORD'] || nil,
72
+ :required => true)
73
+
74
+
75
+ case mode
76
+ when 'mysql'
77
+ when 'redis'
78
+ opt(:path,
79
+ "path to redis database file",
80
+ :type => String,
81
+ :default => ENV['CRYO_PATH'])
82
+ required_parameters << 'path'
83
+ when 'postgres'
84
+ else
85
+ unless mode == '--help'
86
+ STDERR.puts "ERROR! bad input. first option needs to be one of [redis, postgres, mysql]"
87
+ STDERR.puts "Please use --help for more info"
88
+ exit 1
89
+ end
90
+ end
91
+ end
92
+
93
+ options.merge!(type: mode)
94
+
95
+
96
+
97
+ puts "optionss are:"
98
+ require 'pp'
99
+ pp options
100
+
data/bin/test2 ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'trollop'
4
+ require_relative "../lib/cryo"
5
+
6
+
7
+ @required_inputs = [
8
+ 'aws_access_key',
9
+ 'aws_seceret_key',
10
+ ]
11
+
12
+
13
+ @options={}
14
+
15
+ @noun=nil
16
+ @verb=nil
17
+
18
+ @log=true
19
+
20
+ def log(msg='')
21
+ STDERR.puts msg if @log
22
+ end
23
+
24
+
25
+ def print_help_and_exit
26
+ banner = <<-END_OF_BANNER
27
+ Welcome to Cryo, a simple backup utility. #{Cryo::VERSION} (c) 2013 Airbnb
28
+
29
+ All options can be passed in with either the given command line options, or passed in via
30
+ environment with the same name, capitalized, and with CRYO_ appended. Like: CRYO_HOST instead of --host
31
+
32
+ More docs and examples can be found at https://github.com/airbnb/cryo
33
+
34
+ Usage:
35
+ cryo [backup,list,get] [options]
36
+
37
+ where [options] are:
38
+
39
+ END_OF_BANNER
40
+ puts banner
41
+ Kernel.exit 1
42
+ end
43
+
44
+ def print_message_and_exit(message='')
45
+ STDERR.puts message
46
+ Kernel.exit 1
47
+ end
48
+
49
+ def parse_inputs
50
+ log "starting to parse command line inputs"
51
+ while next_argument = ARGV.shift
52
+ log "looping inside parse_inputs. next arg is #{next_argument}"
53
+ print_help_and_exit unless next_argument.start_with? '--'
54
+ formatted_key = next_argument.gsub(/^--/,'').gsub(/-/,'_').to_sym
55
+ if !ARGV.empty? and !ARGV.first.start_with? '--'
56
+ log "this option has a param"
57
+ param = ARGV.shift
58
+ else
59
+ log "this arg does not have a param"
60
+ param = true
61
+ end
62
+ @options.merge!({formatted_key => param})
63
+ end
64
+ end
65
+
66
+ def verify_environment_variable(variable='')
67
+ log "checking for environment variable #{variable}"
68
+ formatted_variable_name = "CRYO_#{variable.upcase}"
69
+ value = ENV[formatted_variable_name]
70
+ unless value.nil?
71
+ log "looks like the variable exists"
72
+ @options.merge!({variable.to_sym => value})
73
+ else
74
+ log "looks like the variable does not exist"
75
+ print_message_and_exit "\nyou need to make sure that you set the #{formatted_variable_name} variable!!!"
76
+ end
77
+ end
78
+
79
+ def verify_inputs
80
+ @required_inputs.each do |input|
81
+ verify_environment_variable input
82
+ end
83
+ end
84
+
85
+
86
+ def backup
87
+ %w{host}.each {|i| @required_inputs << i}
88
+ case @noun
89
+ when "mysql"
90
+ when "redis"
91
+ when "postgres"
92
+ else
93
+ print_help_and_exit
94
+ end
95
+ end
96
+
97
+
98
+ def list
99
+ case @noun
100
+ when "snapshots"
101
+ when "archives"
102
+ else
103
+ print_help_and_exit
104
+ end
105
+ end
106
+
107
+
108
+ def get
109
+ case @noun
110
+ when "archives"
111
+ when "snapshots"
112
+ else
113
+ print_help_and_exit
114
+ end
115
+ end
116
+
117
+
118
+ log "starting to parse arguments"
119
+ print_help_and_exit if ARGV.size < 2
120
+ @verb = ARGV.shift
121
+ @noun = ARGV.shift
122
+ log "got a noun and a verb. #{@verb} and #{@noun}"
123
+
124
+ case @verb
125
+ when "backup"
126
+ backup
127
+ when "list"
128
+ list
129
+ when "get"
130
+ get
131
+ else
132
+ print_help_and_exit
133
+ end
134
+
135
+ parse_inputs
136
+
137
+
138
+ verify_inputs
139
+
140
+ log "options are #{@options.inspect}"
141
+
142
+ Kernel.exit 2
143
+
144
+
145
+ SUB_COMMANDS = %w(backup list get)
146
+ global_opts = Trollop::options do
147
+ STDERR.puts "entering global options"
148
+ stop_on_unknown
149
+ version "cryo #{Cryo::VERSION} (c) 2013 Airbnb"
150
+
151
+
152
+ opt(:aws_access_key,
153
+ "aws_access_key. Can be set using the AWS_ACCESS_KEY environment variable",
154
+ :type => String,
155
+ :default => ENV['CRYO_AWS_ACCESS_KEY'] || nil,
156
+ :required => true)
157
+ stop_on SUB_COMMANDS
158
+ end
159
+
160
+
161
+
162
+
163
+
164
+ cmd = ARGV.shift # get the subcommand
165
+ cmd_opts = \
166
+ case cmd
167
+ when "backup" # parse delete options
168
+ Trollop::options do
169
+ stop_on_unknown
170
+ stop_on ["aws_access_key"]
171
+ STDERR.puts "entering backup options"
172
+ opt :force, "Force deletion"
173
+ end
174
+ when "list" # parse copy options
175
+ Trollop::options do
176
+ STDERR.puts "entering list options"
177
+ opt :double, "Copy twice for safety's sake"
178
+ end
179
+ when "get" # parse copy options
180
+ Trollop::options do
181
+ STDERR.puts "entering get options"
182
+ opt :double, "Copy twice for safety's sake"
183
+ end
184
+ when nil
185
+ STDERR.puts "entering nil options"
186
+ puts "please call with --help if you need help"
187
+ Kernel.exit 1
188
+ else
189
+ STDERR.puts "entering else options"
190
+ Trollop::die "unknown subcommand #{cmd.inspect}"
191
+ end
192
+
193
+ always_required_options = Trollop::options do
194
+ STDERR.puts "entering always required options"
195
+ stop_on_unknown
196
+
197
+
198
+ opt(:aws_secret_key,
199
+ "aws_secret_key. Can be set using the AWS_SECRET_KEY environment variable",
200
+ :type => String,
201
+ :default => ENV['CRYO_AWS_SECRET_KEY'] || nil,
202
+ :required => true)
203
+
204
+ end
205
+
206
+
207
+ puts "Global options: #{global_opts.inspect}"
208
+ puts "Subcommand: #{cmd.inspect}"
209
+ puts "Subcommand options: #{cmd_opts.inspect}"
210
+ puts "Remaining arguments: #{ARGV.inspect}"
data/cryo.gemspec CHANGED
@@ -4,13 +4,20 @@ require File.expand_path('../lib/cryo/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Nathan Baxter"]
6
6
  gem.email = ["nathan.baxter@airbnb.com"]
7
- gem.summary = %q{Tool for snapshotting data, backing it up, verifying it, and cycling it.}
7
+ gem.summary = %q{Tool for snapshotting data, backing it up, verifying it, cycling it, and triggering notifications.}
8
8
  gem.homepage = "https://github.com/airbnb/cryo"
9
9
 
10
10
  gem.files = `git ls-files`.split($\)
11
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
11
+ gem.executables = ["cryo"]
12
+
12
13
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
14
  gem.name = "cryo"
14
15
  gem.require_paths = ["lib"]
15
16
  gem.version = Cryo::VERSION
17
+
18
+ gem.add_runtime_dependency "colorize"
19
+ gem.add_runtime_dependency "trollop", "~> 2.0"
20
+ gem.add_runtime_dependency "aws-sdk", '~> 1.6'
21
+ gem.add_runtime_dependency "net-ntp", '~> 2.1.1'
22
+ gem.add_development_dependency "pry"
16
23
  end
data/examples/rds.sh ADDED
@@ -0,0 +1,28 @@
1
+ #!/bin/bash -ex
2
+
3
+ cd $(dirname $0) && cd ..
4
+
5
+ ./bin/cryo redis\
6
+ --host localhost \
7
+ --user me \
8
+ --password verysafe \
9
+ --sns-topic arn:aws:sns:us-east-1:172631448019:martin-redis-test \
10
+ --key somekey \
11
+ --aws-access-key some_aws_access_key \
12
+ --aws-secret-key some_secret \
13
+ --bucket some_buck \
14
+ --path /mnt/redis/foo \
15
+
16
+
17
+ # or
18
+
19
+ export CRYO_AWS_ACCESS_KEY=some_key
20
+ export CRYO_AWS_SECRET_KEY=some_secret
21
+ export CRYO_BUCKET=some_other_buk
22
+ export CRYO_SNS_TOPIC=some_sns_topic
23
+ export CRYO_HOST=some_server_somewhere
24
+ export CRYO_USER=some_user
25
+ export CRYO_PASSWORD=some_good_password
26
+ export CRYO_PATH=/some/path/to/redis/db
27
+
28
+ ./bin/cryo mysql
data/lib/cryo.rb CHANGED
@@ -1,5 +1,90 @@
1
- require "cryo/version"
1
+ require 'colorize'
2
+ require 'aws-sdk'
3
+ require 'logger'
4
+ require 'net/ntp'
5
+
6
+
7
+ ## require all ruby files recursively
8
+ Dir.glob(File.join(File.dirname(__FILE__),'**/*.rb')).sort.each do |file|
9
+ require_relative file
10
+ end
11
+
12
+
13
+ class Cryo
14
+
15
+ include Utils
16
+ # HOST = `hostname`.chomp!
17
+ attr_accessor :options, :s3, :md5, :sns, :logger, :key
18
+
19
+ def initialize(options={})
20
+ get_utc_timestamp # save start time for backup
21
+
22
+ self.options = options
23
+ self.logger = Logger.new(STDERR)
24
+ logger.level = Logger::DEBUG
25
+
26
+ @database = Database.create(options) \
27
+ unless options[:type] == 'list' or options[:type] == 'get'
28
+ @store = Store.create(options.merge(type: 's3',time: @time))
29
+ @message = Message.create(options.merge(type: 'sns'))
30
+ @snapshot_prefix = options[:snapshot_prefix]
31
+ @archive_prefix = options[:archive_prefix]
32
+ @key = get_timstamped_key_name
33
+ @snapshot_frequency = options[:snapshot_frequency]
34
+ @archive_frequency = options[:archive_frequency]
35
+ @snapshot_period = options[:snapshot_period]
36
+ @snapshot_bucket = options[:snapshot_bucket]
37
+ @archive_bucket = options[:archive_bucket]
38
+ @tmp_path = options[:tmp_path]
39
+ end
40
+
41
+
42
+ def backup!()
43
+ if @database.respond_to? 'get_gzipped_backup'
44
+ logger.info "getting compressed backup"
45
+ compressed_backup = @database.get_gzipped_backup
46
+ logger.info "got backup in #{(get_utc_time - @time).round 2} seconds"
47
+ else
48
+ logger.info "taking backup..."
49
+ backup_file = @database.get_backup
50
+ logger.info "got backup in #{(get_utc_time - @time).round 2} seconds"
51
+
52
+ timer = get_utc_time
53
+ logger.info "compressing backup..."
54
+ compressed_backup = gzip_file backup_file
55
+ logger.info "compressed backup in #{(get_utc_time - timer).round 2} seconds"
56
+ end
57
+
58
+ timer = get_utc_time
59
+ logger.info "storing backup..."
60
+ @store.put(content: Pathname.new(compressed_backup), bucket: options[:snapshot_bucket],key: @key)
61
+ logger.info "upload took #{(get_utc_time - timer).round 2} seconds"
62
+
63
+ logger.info "completed entire backup in #{(get_utc_time - @time).round 2} seconds :)"
64
+ end
65
+
66
+ def archive_and_purge()
67
+ logger.info "archiving and purging..."
68
+ @store.archive_and_purge()
69
+ logger.info "done archiving and purging :)"
70
+ end
71
+
72
+ def list_snapshots
73
+ snapshot_list = @store.get_bucket_listing(bucket: @snapshot_bucket, prefix: @snapshot_prefix)
74
+ puts "here is what I see in the snapshot bucket:"
75
+ snapshot_list.each { |i| puts " #{i.key}"}
76
+ end
77
+
78
+ def list_archives
79
+ archive_list = @store.get_bucket_listing(bucket: @archive_bucket, prefix: @archive_prefix)
80
+ puts "here is what I see in the archive bucket:"
81
+ archive_list.each { |i| puts " #{i.key}"}
82
+ end
83
+
84
+ def get_snapshot(snapshot)
85
+ basename = File.basename snapshot
86
+ puts "getting #{snapshot} and saving it in #{File.join(Dir.pwd,basename)}"
87
+ @store.get(bucket: @snapshot_bucket,key: snapshot,file: basename)
88
+ end
2
89
 
3
- module Cryo
4
- # Your code goes here...
5
90
  end
@@ -0,0 +1,16 @@
1
+ require_relative 'utils'
2
+
3
+ class Database
4
+ include Utils
5
+
6
+ def get_backup()
7
+ raise "implement me"
8
+ end
9
+
10
+ class << self
11
+ def create(options={})
12
+ const_get(options[:type].to_s.capitalize).new(options)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,44 @@
1
+ # this has all of the logic to perform an entire dump of a remote rds host
2
+
3
+ class Mysql < Database
4
+
5
+ include Utils
6
+ attr_accessor :user, :host, :password, :local_path, :tmp_path, :port
7
+
8
+ def initialize(opts={})
9
+ raise "you need to specify a password" unless opts[:password]
10
+ self.password = opts[:password]
11
+ raise "you need to specify a host" unless opts[:host]
12
+ self.host = opts[:host]
13
+ raise "you need to specify a tmp path" unless opts[:tmp_path]
14
+ self.tmp_path = opts[:tmp_path]
15
+ self.user = opts[:user] || 'ubuntu'
16
+ self.port = opts[:port] || '3306'
17
+ self.local_path = opts[:local_path] || get_tempfile
18
+ verify_system_dependency 'mysqldump'
19
+ end
20
+
21
+ ## run through all of the necessary steps to perform a backup
22
+ def get_backup()
23
+ get_dump
24
+ local_path
25
+ end
26
+
27
+ def get_gzipped_backup
28
+ get_and_gzip_dump
29
+ local_path
30
+ end
31
+
32
+ private
33
+
34
+ ## perform a mysqldump to get an entire mysql dump on the local system, while gzipping it at the same time
35
+ def get_and_gzip_dump
36
+ safe_run "mysqldump --host=#{host} --user=#{user} --password=#{password} --all-databases --single-transaction | gzip > #{local_path}"
37
+ end
38
+
39
+ ## perform a mysqldump to get an entire mysql dump on the local system
40
+ def get_dump()
41
+ safe_run "mysqldump --host=#{host} --user=#{user} --password=#{password} --all-databases --single-transaction > #{local_path}"
42
+ end
43
+
44
+ end
@@ -0,0 +1,31 @@
1
+ # this has all of the logic to perform an entire dump of a remote postgress host
2
+
3
+ class Postgres
4
+ include Utils
5
+ attr_accessor :user, :host, :password, :local_path, :tmp_path
6
+
7
+ def initialize(opts={})
8
+ raise "you need to specify a password" unless opts[:password]
9
+ self.password = opts[:password]
10
+ raise "you need to specify a host" unless opts[:host]
11
+ self.host = opts[:host]
12
+ raise "you need to specify a tmp path" unless opts[:tmp_path]
13
+ self.tmp_path = opts[:tmp_path]
14
+ self.user = opts[:user] || 'ubuntu'
15
+ self.local_path = opts[:local_path] || get_tempfile
16
+ verify_system_dependency 'pg_dumpall'
17
+ end
18
+
19
+ def get_backup()
20
+ take_dump
21
+ end
22
+
23
+ private
24
+
25
+ ## perform a pg_dumpall to get an entire pgdump on the local system
26
+ def take_dump()
27
+ safe_run "PGPASSWORD=#{password} pg_dumpall --host=#{host} --username=#{user} --file=#{local_path}"
28
+ local_path
29
+ end
30
+
31
+ end
@@ -0,0 +1,63 @@
1
+ class Redis
2
+ include Utils
3
+ attr_accessor :user, :host, :remote_path, :local_path, :opts, :tmp_path
4
+
5
+ def initialize(opts={})
6
+ raise "you need to specify a remote host" unless opts[:host]
7
+ self.host = opts[:host]
8
+ self.user = opts[:user] || 'ubuntu'
9
+ raise "you need to specify a tmp path" unless opts[:tmp_path]
10
+ self.tmp_path = opts[:tmp_path]
11
+ self.remote_path = opts[:path] || '/mnt/redis/dump.rdb'
12
+ self.local_path = opts[:local_path] || get_tempfile
13
+ end
14
+
15
+
16
+ ## get a copy of the db from remote host
17
+ def get_backup()
18
+ take_dump
19
+ end
20
+
21
+
22
+ ## get a zipped copy of the db from remote host
23
+ def get_gzipped_backup
24
+ take_dump_and_gzip
25
+ end
26
+
27
+ private
28
+
29
+ ## copy the redis db into a new file and scp it here
30
+ def take_dump()
31
+ # TODO(martin): verify that both the local and remote hosts have enough free disk space for this to complete
32
+ temp_file = remote_path + "-backup-#{rand 99999}"
33
+ # this is kinda hacky, but we need to make sure that we remove a backup if we take one
34
+ begin
35
+ ssh "cp #{remote_path} #{temp_file}"
36
+ safe_run "scp #{user}@#{host}:#{temp_file} #{local_path}"
37
+ ensure
38
+ ssh "rm -f #{temp_file}"
39
+ end
40
+ local_path
41
+ end
42
+
43
+
44
+ ## copy the redis db into a new file and stream it here while zipping
45
+ def take_dump_and_gzip()
46
+ # TODO(martin): verify that both the local and remote hosts have enough free disk space for this to complete
47
+ temp_file = remote_path + "-backup-#{rand 99999}"
48
+ # this is kinda hacky, but we need to make sure that we remove a backup if we take one
49
+ begin
50
+ ssh "cp #{remote_path} #{temp_file}"
51
+ safe_run "(ssh #{user}@#{host} cat #{temp_file}) | gzip > #{local_path}"
52
+ ensure
53
+ ssh "rm -f #{temp_file}"
54
+ end
55
+ local_path
56
+ end
57
+
58
+
59
+ def ssh(command)
60
+ safe_run "ssh #{user}@#{host} #{command}"
61
+ end
62
+
63
+ end
@@ -0,0 +1,23 @@
1
+ class Message
2
+
3
+ def initialize(opts)
4
+ end
5
+
6
+ def get()
7
+ raise "implement me"
8
+ end
9
+
10
+ def put()
11
+ raise "implement me"
12
+ end
13
+
14
+
15
+ class << self
16
+ def create(options={})
17
+ message_class = const_get(options[:type].to_s.capitalize)
18
+ return message_class.new(options)
19
+ end
20
+ end
21
+
22
+ end
23
+
@@ -0,0 +1,19 @@
1
+ class Sns < Message
2
+ require 'aws-sdk'
3
+
4
+ def initialize(opts={})
5
+ AWS.config(:access_key_id => opts[:aws_access_key],
6
+ :secret_access_key => opts[:aws_secret_key])
7
+ @sns = AWS::SNS::Client.new
8
+ @topic = opts[:topic] || opts[:topic_arn]
9
+ end
10
+
11
+
12
+ def send(opts={})
13
+ @sns.publish({
14
+ :message => opts[:message],
15
+ :subject => opts[:subject],
16
+ :topic_arn => @topic
17
+ })
18
+ end
19
+ end
data/lib/cryo/store.rb ADDED
@@ -0,0 +1,139 @@
1
+ #require 'net/ntp'
2
+ require 'logger'
3
+
4
+ class Store
5
+
6
+ include Utils
7
+ attr_accessor :logger
8
+
9
+ def initialize(opts={})
10
+ self.logger = Logger.new(STDERR)
11
+ logger.level = Logger::DEBUG
12
+
13
+ @snapshot_frequency = opts[:snapshot_frequency]
14
+ @archive_frequency = opts[:archive_frequency]
15
+ @snapshot_period = opts[:snapshot_period]
16
+ @snapshot_prefix = opts[:snapshot_prefix]
17
+ @archive_prefix = opts[:archive_prefix]
18
+ @time = opts[:time]
19
+ end
20
+
21
+ def get()
22
+ raise "implement me"
23
+ end
24
+
25
+ def put()
26
+ raise "implement me"
27
+ end
28
+
29
+ def get_snapshot_list()
30
+ raise "implement me"
31
+ end
32
+
33
+ def get_archive_list()
34
+ raise "implement me"
35
+ end
36
+
37
+ class << self
38
+ def create(options={})
39
+ const_get(options[:type].to_s.capitalize).new(options)
40
+ end
41
+ end
42
+
43
+ def archive_and_purge
44
+ snapshot_list = get_snapshot_list
45
+ newest_archive = get_newest_archive
46
+ recursive_archive_and_purge(snapshot_list: snapshot_list, newest_archive: newest_archive)
47
+ end
48
+
49
+ protected
50
+
51
+ def get_newest_archive()
52
+ raise "implment me"
53
+ end
54
+
55
+
56
+ def recursive_archive_and_purge(opts={})
57
+ logger.debug "entering recursive_archive_and_purge"
58
+ snapshot_list = opts[:snapshot_list]
59
+
60
+ # return if there are no snapshots
61
+ if snapshot_list.empty?
62
+ logger.info "no snapshots found"
63
+ return true
64
+ end
65
+
66
+ # return if there are not enough snapshots avilable
67
+ minium_number_of_snapshots = (@snapshot_period.to_f/@snapshot_frequency.to_f).ceil
68
+ if snapshot_list.size < minium_number_of_snapshots
69
+ logger.info "not enough snapshots avilable for archiving"
70
+ logger.info "we found #{snapshot_list.size} but we need to keep at least #{minium_number_of_snapshots}"
71
+ return true
72
+ end
73
+
74
+ oldest_snapshot = snapshot_list.shift
75
+ oldest_snapshot_age = get_age_from_key_name(oldest_snapshot)
76
+
77
+ logger.debug "oldest_snapshot is #{oldest_snapshot}"
78
+ logger.debug "oldest_snapshot_age is #{oldest_snapshot_age}"
79
+
80
+ # return if the oldest snapshot it not old enough to be archived
81
+ if oldest_snapshot_age < @snapshot_period
82
+ logger.info "all snapshots are younger than snapshot_period"
83
+ return true
84
+ end
85
+
86
+ # if we got this far, then the oldest snapshot needs to be either archived or deleted
87
+ newest_archive = get_newest_archive
88
+
89
+ # check to see if we have any archives
90
+ if newest_archive.empty?
91
+ logger.info "looks like we don't have any archives yet"
92
+ logger.info "archiving oldest snapshot #{oldest_snapshot}"
93
+ archive_snapshot oldest_snapshot
94
+ logger.debug "recursing..."
95
+ recursive_archive_and_purge(snapshot_list: snapshot_list, newest_archive: oldest_snapshot)
96
+ return true
97
+ end
98
+
99
+
100
+ newest_archive_age = get_age_from_key_name(newest_archive)
101
+
102
+ # check to see if the oldest snapshot should be archived
103
+ if need_to_archive?(oldest_snapshot_age,newest_archive_age)
104
+ logger.info "archiving oldest snapshot #{oldest_snapshot}"
105
+ archive_snapshot oldest_snapshot
106
+ logger.debug "recursing..."
107
+ recursive_archive_and_purge(snapshot_list: snapshot_list, newest_archive: oldest_snapshot)
108
+ return true
109
+ end
110
+
111
+ # check the next oldest snapshot too, before we throw this one away
112
+ second_oldest_snapshot = opts[:snapshot_list].first
113
+ second_oldest_snapshot_age = get_age_from_key_name(second_oldest_snapshot)
114
+
115
+ if need_to_archive?(second_oldest_snapshot_age,newest_archive_age)
116
+ logger.info "archiving oldest snapshot #{oldest_snapshot}"
117
+ archive_snapshot oldest_snapshot
118
+ logger.debug "recursing..."
119
+ recursive_archive_and_purge(snapshot_list: snapshot_list, newest_archive: oldest_snapshot)
120
+ return true
121
+ end
122
+
123
+ # if we got this far, then we just need to delete the oldest snapshot
124
+ logger.info "deleting oldest snapshot #{oldest_snapshot}"
125
+ delete_snapshot oldest_snapshot
126
+ logger.debug "recursing"
127
+ recursive_archive_and_purge(snapshot_list: snapshot_list, newest_archive: newest_archive)
128
+ return true
129
+ end
130
+
131
+ def archive_snapshot
132
+ raise "implment me"
133
+ end
134
+
135
+ def delete
136
+ raise "implment me"
137
+ end
138
+
139
+ end
@@ -0,0 +1,128 @@
1
+ class S3 < Store
2
+ require 'aws-sdk'
3
+
4
+ attr_accessor :snapshot_bucket, :archive_bucket, :prefix
5
+
6
+ def initialize(opts={})
7
+ super(opts)
8
+ AWS.config(:access_key_id => opts[:aws_access_key],
9
+ :secret_access_key => opts[:aws_secret_key])
10
+ @s3 = AWS::S3.new
11
+ @snapshot_bucket = @s3.buckets[opts[:snapshot_bucket]]
12
+ @archive_bucket = @s3.buckets[opts[:archive_bucket]]
13
+ end
14
+
15
+ def get(opts={})
16
+ bucket = opts[:bucket]
17
+ key = opts[:key]
18
+ file_path = opts[:file] || opts[:path]
19
+ if file_path
20
+ File.open(file_path,'w') do |file|
21
+ @s3.buckets[bucket].objects[key].read {|chunk| file.write chunk}
22
+ return true
23
+ end
24
+ else
25
+ return @s3.buckets[bucket].objects[key].read
26
+ end
27
+ end
28
+
29
+
30
+ def put(opts={})
31
+ bucket = opts[:bucket]
32
+ key = opts[:key]
33
+ content = opts[:content]
34
+ @s3.buckets[bucket].objects[key].write(content) # TODO: verify that bucket exists?
35
+ end
36
+
37
+
38
+ def etag(opts={})
39
+ bucket = opts[:bucket]
40
+ key = opts[:key]
41
+ @s3.buckets[bucket].objects[key].etag
42
+ end
43
+
44
+
45
+ # retun an array listing the objects in our snapshot bucket
46
+ def get_snapshot_list
47
+ get_bucket_listing(bucket: @snapshot_bucket, prefix: @prefix)
48
+ end
49
+
50
+
51
+ # retun an array listing the objects in our archive bucket
52
+ def get_archive_list
53
+ get_bucket_listing(bucket: archive_bucket, prefix: @prefix)
54
+ end
55
+
56
+ # return an array listing of objects in a bucket
57
+ def get_bucket_listing(opts={})
58
+ bucket = opts[:bucket]
59
+ prefix = opts[:prefix]
60
+ list = []
61
+ @s3.buckets[bucket].objects.with_prefix(prefix).each do |object|
62
+ list << object
63
+ end
64
+ list
65
+ end
66
+
67
+ def get_snapshot_list
68
+ snapshot_list = []
69
+ @snapshot_bucket.objects.with_prefix(@snapshot_prefix).each do |object|
70
+ snapshot_list << trim_snapshot_name(object.key)
71
+ end
72
+ snapshot_list
73
+ end
74
+
75
+ protected
76
+
77
+ def expand_snapshot_name(shortname)
78
+ @snapshot_prefix + shortname + "Z.cryo"
79
+ end
80
+
81
+ def expand_archive_name(shortname)
82
+ @archive_prefix + shortname + "Z.cryo"
83
+ end
84
+
85
+ def trim_snapshot_name(longname)
86
+ longname.gsub(/^#{@snapshot_prefix}/,'').gsub(/Z\.cryo$/,'')
87
+ end
88
+
89
+ def trim_archive_name(longname)
90
+ return "" if longname.nil?
91
+ longname.gsub(/^#{@archive_prefix}/,'').gsub(/Z\.cryo$/,'')
92
+ end
93
+
94
+ def delete_snapshot(snapshot)
95
+ full_snapshot_name = expand_snapshot_name(snapshot)
96
+ @snapshot_bucket.objects[full_snapshot_name].delete
97
+ end
98
+
99
+ def archive_snapshot(snapshot)
100
+ logger.info "archiving snapshot #{snapshot}"
101
+ full_snapshot_name = expand_snapshot_name(snapshot)
102
+ full_archive_name = expand_archive_name(snapshot)
103
+ logger.debug "full_snapshot_name is #{full_snapshot_name}"
104
+ logger.debug "full_archive_name is #{full_archive_name}"
105
+ snapshot_object = @snapshot_bucket.objects[full_snapshot_name]
106
+ # if we have already copied the object, just delete the snapshot
107
+ if @archive_bucket.objects[full_archive_name].exists?
108
+ snapshot_object.delete
109
+ else
110
+ snapshot_object.move_to(full_archive_name, :bucket => @archive_bucket)
111
+ end
112
+ end
113
+
114
+
115
+ # this function returns the last item in a bucket that matches the given prefix
116
+ def get_newest_archive(prefix=@archive_prefix)
117
+ tree = @archive_bucket.objects.with_prefix(prefix).as_tree
118
+ directories = tree.children.select(&:branch?).collect(&:prefix)
119
+ if directories.empty?
120
+ matches = []
121
+ @archive_bucket.objects.with_prefix(prefix).each {|o| matches << o.key}
122
+ return trim_archive_name(matches.last)
123
+ else
124
+ # recurse
125
+ get_newest_archive(directories.last)
126
+ end
127
+ end
128
+ end
data/lib/cryo/utils.rb ADDED
@@ -0,0 +1,117 @@
1
+ module Utils
2
+
3
+ require 'zlib'
4
+ require 'net/ntp'
5
+ require 'fileutils'
6
+
7
+
8
+ def delete_file(path)
9
+ File.delete(path) if File.exists?(path)
10
+ end
11
+
12
+
13
+ def get_tempfile
14
+ # Tempfile.new('redis-backup','/mnt/cryo').path
15
+ tmp_file = File.join(@tmp_path,"tmp-#{rand 9999}")
16
+ at_exit {delete_file tmp_file}
17
+ FileUtils.touch tmp_file
18
+ tmp_file
19
+ end
20
+
21
+
22
+ def ungzip_file(path)
23
+ # get a temp file
24
+ tempfile = get_tempfile
25
+ #logger.info "unzipping #{path} to #{tempfile}..."
26
+
27
+ # stream the gzipped file into an uncompressed file
28
+ Zlib::GzipReader.open(path) do |gz|
29
+ File.open(tempfile,'w') do |open_file|
30
+ # write 1M chunks at a time
31
+ open_file.write gz.read(1024*1024) until gz.eof?
32
+ end
33
+ end
34
+ #logger.info "finished unzipping file"
35
+
36
+ # return unzipped file
37
+ tempfile
38
+ end
39
+
40
+
41
+ def gzip_file(path)
42
+ # given a path to a file, return a gzipped version of it
43
+ tempfile = get_tempfile
44
+ #logger.info "gzipping #{path} to #{tempfile}"
45
+
46
+ # stream the gzipped content into a file as we compute it
47
+ Zlib::GzipWriter.open(tempfile) do |gz|
48
+ File.open(path) do |f|
49
+ # write 1M chunks at a time
50
+ gz.write f.read(1024*1024) until f.eof?
51
+ end
52
+ end
53
+ #logger.info "done unzipping"
54
+ tempfile
55
+ end
56
+
57
+ def safe_run(command)
58
+ #logger.debug "about to run #{command}"
59
+ output = `bash -c "set -o pipefail && #{command}"`.chomp
60
+ raise "command '#{command}' failed!\nOutput was:\n#{output}" unless $?.success?
61
+ true
62
+ end
63
+
64
+ def verify_system_dependency(command)
65
+ raise "system dependency #{command} is not unstalled" unless system "which #{command} > /dev/null"
66
+ end
67
+
68
+ def get_utc_time
69
+ retries = 5
70
+ begin
71
+ Net::NTP.get("us.pool.ntp.org").time.getutc
72
+ rescue Object => o
73
+ retries -= 1
74
+ if retries > 0
75
+ logger.debug "retrying ntp query again..."
76
+ sleep 2
77
+ retry
78
+ end
79
+ throw o
80
+ end
81
+ end
82
+
83
+ def get_utc_timestamp()
84
+ @time ||= get_utc_time # don't change the endpoint!!!
85
+ @timestamp ||= @time.strftime("%Y/%m/%d/%H:%M:%S")
86
+ end
87
+
88
+ def get_timstamped_key_name()
89
+ "#{@snapshot_prefix}#{@timestamp}Z.cryo"
90
+ end
91
+
92
+ def get_utc_time_from_key_name(key_name)
93
+ logger.debug "getting time for #{key_name}"
94
+ year,month,day,time = key_name.split('/')
95
+ hour,min,sec = time.split(':')
96
+ Time.utc(year,month,day,hour,min,sec)
97
+ end
98
+
99
+ # returns the age of the snapshot in mins
100
+ def get_age_from_key_name(key_name)
101
+ snapshot_time = get_utc_time_from_key_name(key_name)
102
+ age_in_mins_as_float = (@time - snapshot_time) / 60
103
+ age_in_mins_as_int = age_in_mins_as_float.to_i
104
+ end
105
+
106
+ # find out if we have an archive that is more recent than the snapshot period
107
+ def need_to_archive?(old_snapshot_age,new_archive_age)
108
+ logger.debug "checking to see if we should archive"
109
+ logger.debug "oldest snapshot age is #{old_snapshot_age}"
110
+ logger.debug "newest archive time is #{new_archive_age}"
111
+ logger.debug "@snapshot_period is #{@archive_frequency}"
112
+ answer = (new_archive_age - old_snapshot_age) > @archive_frequency
113
+ logger.debug "returning #{answer.inspect}"
114
+ return answer
115
+ end
116
+
117
+ end
data/lib/cryo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Cryo
2
- VERSION = "0.0.1"
1
+ class Cryo
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cryo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,118 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-06 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-04-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: colorize
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: trollop
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: aws-sdk
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.6'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ - !ruby/object:Gem::Dependency
63
+ name: net-ntp
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 2.1.1
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 2.1.1
78
+ - !ruby/object:Gem::Dependency
79
+ name: pry
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
14
94
  description:
15
95
  email:
16
96
  - nathan.baxter@airbnb.com
17
- executables: []
97
+ executables:
98
+ - cryo
18
99
  extensions: []
19
100
  extra_rdoc_files: []
20
101
  files:
21
102
  - .gitignore
22
103
  - Gemfile
23
104
  - LICENSE
105
+ - Makefile
24
106
  - README.md
25
107
  - Rakefile
108
+ - bin/bootstrap_cryo.sh
109
+ - bin/cryo
110
+ - bin/test
111
+ - bin/test2
26
112
  - cryo.gemspec
113
+ - examples/rds.sh
27
114
  - lib/cryo.rb
115
+ - lib/cryo/database.rb
116
+ - lib/cryo/database/mysql.rb
117
+ - lib/cryo/database/postgres.rb
118
+ - lib/cryo/database/redis.rb
119
+ - lib/cryo/message.rb
120
+ - lib/cryo/message/sns.rb
121
+ - lib/cryo/store.rb
122
+ - lib/cryo/store/s3.rb
123
+ - lib/cryo/utils.rb
28
124
  - lib/cryo/version.rb
29
125
  homepage: https://github.com/airbnb/cryo
30
126
  licenses: []
@@ -49,5 +145,6 @@ rubyforge_project:
49
145
  rubygems_version: 1.8.24
50
146
  signing_key:
51
147
  specification_version: 3
52
- summary: Tool for snapshotting data, backing it up, verifying it, and cycling it.
148
+ summary: Tool for snapshotting data, backing it up, verifying it, cycling it, and
149
+ triggering notifications.
53
150
  test_files: []