snapshot_reload 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ #
2
+ # +Copyright+:: (c) 2012, Novu, LLC
3
+ # +License+:: All rights reserverd. For internal and private use only
4
+ # +Author+:: Tamara Temple <tamara.temple@novu.com>
5
+ #
6
+
7
+ # TODO: consolidate scenarios into outlines to make them DRYer
8
+
9
+ Feature: ensure command line options work as expected
10
+ In order to test the command line options
11
+ As a tester
12
+ I want to try out all the options
13
+
14
+ Scenario: learn how to use application
15
+ When I get help for "snapshot_reload"
16
+ Then the exit status should be 0
17
+ And the banner should be present
18
+ And the banner should document that this app takes options
19
+ And the following options should be documented:
20
+ |--version|
21
+ |--log-level LEVEL|
22
+ |--env ENVIRONMENT|
23
+ |--source S3SOURCE|
24
+ |--aws-conf AWS_CONFIG|
25
+ |--aws-key AWSKEY|
26
+ |--aws-secret AWSSECRET|
27
+ And the banner should document that this app's arguments are:
28
+ |config|which is required|
29
+
30
+ Scenario: omit a configuration file
31
+ When I run `snapshot_reload --log-level debug`
32
+ Then the output should contain "parse error: 'config' is required"
33
+ And the banner should be present
34
+ And the exit status should not be 0
35
+
36
+ Scenario: provide non-existant configuration file
37
+ When I run `snapshot_reload --log-level debug --dry-run config`
38
+ Then the exit status should be 1
39
+ And the output should contain "config file does not exist"
40
+
41
+ Scenario: provide non-yaml configuration file
42
+ Given a file named "config.txt" with:
43
+ """
44
+ Hello There!
45
+ """
46
+ When I run `snapshot_reload --dry-run config.txt`
47
+ Then the exit status should be 2
48
+ And the output should contain "config.txt is not YAML"
49
+
50
+ Scenario: provide existing configuration file and no credential file
51
+ Given a file named "config.yaml" with:
52
+ """
53
+ qa:
54
+ adapter: mysql2
55
+ host: localhost
56
+ encoding: utf8
57
+ database: novu_test
58
+ pool: 5
59
+ username: novuadmin
60
+ password:
61
+ """
62
+ When I run `snapshot_reload --log-level debug --dry-run --aws-conf aws_cred --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
63
+ Then the exit status should be 4
64
+ And the output should contain "aws_cred does not exist!"
65
+
66
+
67
+
68
+ Scenario: provide existing configuration file and credential file
69
+ Given a file named "config.yaml" with:
70
+ """
71
+ qa:
72
+ adapter: mysql2
73
+ host: localhost
74
+ encoding: utf8
75
+ database: novu_test
76
+ pool: 5
77
+ username: novuadmin
78
+ password:
79
+ """
80
+ Given a file named "aws_cred" with:
81
+ """
82
+ access_key = A1234
83
+ secret_key = S5678
84
+ """
85
+ When I run `snapshot_reload --log-level debug --dry-run --aws-conf aws_cred --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
86
+ Then the exit status should be 0
87
+
88
+ Scenario: provide existing configuration file with environment "other" and credential file
89
+ Given a file named "config.yaml" with:
90
+ """
91
+ other:
92
+ adapter: mysql2
93
+ host: localhost
94
+ encoding: utf8
95
+ database: novu_test
96
+ pool: 5
97
+ username: novuadmin
98
+ password:
99
+ """
100
+ Given a file named "aws_cred" with:
101
+ """
102
+ access_key = A1234
103
+ secret_key = S5678
104
+ """
105
+ When I run `snapshot_reload --log-level debug --dry-run --aws-conf aws_cred --env other --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
106
+ Then the exit status should be 0
107
+
108
+ Scenario: provide existing configuration file with environment "other" and aws-key but omit awk-secret
109
+ Given a file named "config.yaml" with:
110
+ """
111
+ other:
112
+ adapter: mysql2
113
+ host: localhost
114
+ encoding: utf8
115
+ database: novu_test
116
+ pool: 5
117
+ username: novuadmin
118
+ password:
119
+ """
120
+ When I run `snapshot_reload --log-level debug --dry-run --aws-key A1234 --env other --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
121
+ Then the exit status should be 9
122
+ And the output should contain "Must provide *both* aws-key and aws-secret"
123
+
124
+ Scenario: provide existing configuration file with environment "other" and aws-secret but omit awk-key
125
+ Given a file named "config.yaml" with:
126
+ """
127
+ other:
128
+ adapter: mysql2
129
+ host: localhost
130
+ encoding: utf8
131
+ database: novu_test
132
+ pool: 5
133
+ username: novuadmin
134
+ password:
135
+ """
136
+ When I run `snapshot_reload --log-level debug --dry-run --aws-secret S5678 --env other --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
137
+ Then the exit status should be 9
138
+ And the output should contain "Must provide *both* aws-key and aws-secret"
139
+
140
+ Scenario: provide --verbose switch
141
+ Given a file named "config.yaml" with:
142
+ """
143
+ qa:
144
+ adapter: mysql2
145
+ host: localhost
146
+ encoding: utf8
147
+ database: novu_test
148
+ pool: 5
149
+ username: novuadmin
150
+ password:
151
+ """
152
+ Given a file named "aws_cred" with:
153
+ """
154
+ access_key = A1234
155
+ secret_key = S5678
156
+ """
157
+ When I run `snapshot_reload --log-level debug --dry-run --aws-conf aws_cred --verbose --source s3://tam-test-2/db-backups/novu-2012-12-11.sql.gz config.yaml`
158
+ Then the exit status should be 0
159
+ And the output should contain "config: "
160
+ And the output should contain "env: qa"
161
+ And the output should contain "host: localhost"
162
+ And the output should contain "database: novu_test"
163
+ And the output should contain "username: novuadmin"
164
+ And the output should contain "password: "
165
+ And the output should contain "source: "
166
+ And the output should contain "aws key: A1234"
167
+ And the output should contain "aws secret: S5678"
168
+ And the output should contain "dry run: true"
169
+ And the output should contain "verbose: true"
170
+ And the output should contain "quiet: false"
171
+
@@ -0,0 +1,4 @@
1
+ Given /^a file named "(.*?)" exists$/ do |arg1|
2
+ # peending # express the regexp above with the code you wish you had
3
+ File.exists?(arg1)
4
+ end
@@ -0,0 +1 @@
1
+ # Put your step definitions here
@@ -0,0 +1,17 @@
1
+ require 'aruba/cucumber'
2
+ require 'methadone/cucumber'
3
+
4
+ ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
5
+ LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
6
+
7
+ Before do
8
+ # Using "announce" causes massive warnings on 1.9.2
9
+ @puts = true
10
+ @original_rubylib = ENV['RUBYLIB']
11
+ ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
12
+ @aruba_timeout_seconds = 3600 # need enough time to transfer the large sql file
13
+ end
14
+
15
+ After do
16
+ ENV['RUBYLIB'] = @original_rubylib
17
+ end
@@ -0,0 +1,129 @@
1
+ =begin
2
+
3
+ snapshot_reload.rb
4
+
5
+ +Copyright:+:: (c) 2012, Novu, LLC
6
+ +License:+:: All rights reserved. For internatl and private use only.
7
+ +Author:+:: Tamara Temple <tamara.temple@novu.com>
8
+
9
+ =end
10
+
11
+
12
+ require "snapshot_reload/version"
13
+ require "snapshot_reload/errors"
14
+ require "snapshot_reload/defaults"
15
+ require "snapshot_reload/validate"
16
+ require "snapshot_reload/fetch"
17
+ require "snapshot_reload/reload"
18
+ require "snapshot_reload/String"
19
+ require 'methadone'
20
+
21
+ module SnapshotReload
22
+ class SnapshotReload
23
+ include Methadone::CLILogging
24
+
25
+ # attr_reader :config, :env, :host, :database, :username, :password, :source, :aws_key, :aws_secret, :sql_file
26
+
27
+ def initialize(config, options=nil)
28
+
29
+ options = Hash.new if options.nil?
30
+
31
+ @config = validate_configuration(config)
32
+ @env = validate_environment(options[:env], @config)
33
+
34
+ @host = check_field(@config,@env,'host')
35
+ @database = check_field(@config,@env,'database')
36
+ @username = check_field(@config,@env,'username')
37
+ @password = check_field(@config,@env,'password',false)
38
+
39
+ @source = validate_source(options[:source])
40
+
41
+ if @source.match('^s3://')
42
+ aws = validate_aws(options['aws-conf'],
43
+ options['aws-key'], options['aws-secret'])
44
+ @aws_key = aws[0]
45
+ @aws_secret = aws[1]
46
+ end
47
+
48
+ @dry_run = options['dry-run'] ||= false
49
+
50
+ @verbose = options[:verbose] ||= false
51
+ @quiet = options[:quiet] ||= false
52
+
53
+ @verbose = false if @quiet
54
+
55
+ @sql_file = ''
56
+
57
+ if @verbose
58
+ info("config: #{@config.to_s}")
59
+ info("env: #{@env.to_s}")
60
+ info("host: #{@host.to_s}")
61
+ info("database: #{@database.to_s}")
62
+ info("username: #{@username.to_s}")
63
+ info("password: #{@password.to_s}")
64
+ info("source: #{@source.to_s}")
65
+ info("aws key: #{@aws_key}")
66
+ info("aws secret: #{@aws_secret}")
67
+ info("dry run: #{@dry_run}")
68
+ info("verbose: #{@verbose}")
69
+ info("quiet: #{@quiet}")
70
+ end
71
+
72
+ reload # and here all the magic happens!!
73
+
74
+ end
75
+
76
+ def config
77
+ @config
78
+ end
79
+
80
+ def env
81
+ @env
82
+ end
83
+
84
+ def host
85
+ @host
86
+ end
87
+
88
+ def database
89
+ @database
90
+ end
91
+
92
+ def username
93
+ @username
94
+ end
95
+
96
+ def password
97
+ @password
98
+ end
99
+
100
+ def source
101
+ @source
102
+ end
103
+
104
+ def aws_key
105
+ @aws_key
106
+ end
107
+
108
+ def aws_secret
109
+ @aws_secret
110
+ end
111
+
112
+ def sql_file
113
+ @sql_file
114
+ end
115
+
116
+ def dry_run?
117
+ @dry_run
118
+ end
119
+
120
+ def verbose?
121
+ @verbose
122
+ end
123
+
124
+ def quiet?
125
+ @quiet
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,25 @@
1
+ =begin
2
+
3
+ String.rb
4
+
5
+ +Copyright:+:: (c) 2012, Novu, LLC
6
+ +License:+:: All rights reserved. For internatl and private use only.
7
+ +Author:+:: Tamara Temple <tamara.temple@novu.com>
8
+
9
+ Additional methods for String and NilClass, cos I want to.
10
+ Seriously, doesn't "present?" look better that "not nil?" ??
11
+
12
+ =end
13
+
14
+
15
+ class String
16
+ def present?
17
+ true unless self.nil? or self.empty?
18
+ end
19
+ end
20
+
21
+ class NilClass
22
+ def present?
23
+ true unless self.nil?
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ =begin
2
+
3
+ Defaults used in the SnapshotReload module
4
+
5
+ +Copyright:+ (c) 2012 Novu, LLC
6
+ +License:+ All rights reserved. For internal and private use only
7
+ +Author:+ Tamara Temple <tamara.temple@novu.com>
8
+
9
+ =end
10
+
11
+ module SnapshotReload
12
+ DEFAULT_S3_SOURCE = %q{s3://novu_backups/db_backups/clean-mysqldump.sql.gz}
13
+ DEFAULT_AWS_CREDS = %q{/opt/novu/.s3cfg}
14
+ DEFAULT_ENVIRONMENT = %q{qa}
15
+
16
+ CHUNK_SIZE = 10 * 1024 * 1024 # chunk size to pull over file from S3
17
+ end
18
+
@@ -0,0 +1,31 @@
1
+ =begin
2
+
3
+ errors.rb
4
+
5
+ +Copyright:+:: (c) 2012, Novu, LLC
6
+ +License:+:: All rights reserved. For internatl and private use only.
7
+ +Author:+:: Tamara Temple <tamara.temple@novu.com>
8
+
9
+ Enumerate program exit errors as descriptive constants
10
+
11
+
12
+ =end
13
+
14
+ module SnapshotReload
15
+
16
+ ENOCONFIG = 1
17
+ ECONFIGNOTYAML = 2
18
+ ENOENVINCONFIG = 3
19
+ ENOCRED = 4
20
+ ENOFIELD = 5
21
+ ENOSQLFILE = 6
22
+ ECMDFAILED = 7
23
+ EBADS3URI = 8
24
+ EMISSINGKEYORSECRET = 9
25
+ ENOWRITE = 10
26
+ EDROPFAILED = 11
27
+ ENOBUCKET = 12
28
+ ENOOBJECT = 13
29
+ ENOS3FILES = 14
30
+
31
+ end
@@ -0,0 +1,161 @@
1
+ =begin
2
+
3
+ fetch.rb
4
+
5
+ +Copyright:+:: (c) 2012, Novu, LLC
6
+ +License:+:: All rights reserved. For internatl and private use only.
7
+ +Author:+:: Tamara Temple <tamara.temple@novu.com>
8
+
9
+ =end
10
+
11
+ require 'methadone'
12
+ require 'fog'
13
+
14
+ module SnapshotReload
15
+ class SnapshotReload
16
+
17
+ def fetch_snapshot
18
+
19
+ if @source.match('^s3://')
20
+ @sql_file = s3_fetch
21
+ else
22
+ @sql_file = @source
23
+ end
24
+
25
+ unless File.exists?(@sql_file)
26
+ fatal("#{@sql_file} does not exist!")
27
+ exit(ENOSQLFILE)
28
+ end
29
+
30
+ info("SQL file: #{@sql_file}") if @verbose
31
+
32
+ @sql_file
33
+
34
+ end
35
+
36
+
37
+ =begin
38
+
39
+ Helper methods for fetch_snapshot
40
+
41
+ =end
42
+
43
+
44
+ def s3_fetch
45
+
46
+ matches = @source.match('^s3://([^/]+)/(.*)$')
47
+ if matches
48
+ s3_bucket_name=matches[1]
49
+ s3_object_name=matches[2]
50
+ else
51
+ fatal("#{@source} is not a valid s3 uri (must be s3://bucket/object)")
52
+ exit(EBADS3URI)
53
+ end
54
+
55
+
56
+ warn("Dry run, nothing will be fetched from S3") if @dry_run
57
+
58
+ info("Connecting to AWS S3") if @verbose
59
+
60
+ connection = Fog::Storage.new(:provider => 'AWS',
61
+ :aws_access_key_id => @aws_key,
62
+ :aws_secret_access_key => @aws_secret) unless @dry_run
63
+
64
+
65
+ unless @dry_run
66
+
67
+ buckets = connection.directories.select do |dir|
68
+ debug("Dir: #{dir.key}")
69
+ dir if dir.key == s3_bucket_name
70
+ end
71
+
72
+ if buckets.nil? or buckets.empty?
73
+ fatal("no bucket #{s3_bucket_name} found")
74
+ exit(ENOBUCKET)
75
+ end
76
+
77
+ s3_bucket = buckets.first
78
+
79
+ info("S3 Bucket #{s3_bucket.key} found") if @verbose
80
+
81
+ if s3_bucket.files.nil?
82
+ error("No files in S3 bucket #{s3_bucket_name}")
83
+ exit(ENOS3FILES)
84
+ end
85
+
86
+ files = s3_bucket.files.select do |file|
87
+ debug("File: #{file.key}")
88
+ file if file.key == s3_object_name
89
+ end
90
+
91
+ if files.nil? or files.empty?
92
+ fatal("no object #{s3_object_name} found")
93
+ exit(ENOOBJECT)
94
+ end
95
+
96
+ s3_object = files.first
97
+
98
+ info("S3 Object #{s3_object_name} found in #{s3_bucket_name}") if @verbose
99
+
100
+ s3_file = File.basename(s3_object.key)
101
+ s3_object_content_length = s3_object.content_length
102
+
103
+ else # this is just a dry run, make up stuff
104
+
105
+ s3_file = File.basename(s3_object_name)
106
+ s3_object_content_length = 0
107
+
108
+ end
109
+
110
+ if File.exists?(s3_file) and
111
+ not (File.stat(s3_file).file? and File.stat(s3_file).writable?)
112
+ fatal("#{s3_file} is not writable!")
113
+ exit(ENOWRITE)
114
+ end
115
+
116
+ info("Writing to file #{s3_file}") if @verbose
117
+
118
+ File.open(s3_file,'w') do |file|
119
+
120
+ # Calculate number of batches for extremely large files
121
+
122
+ batches = s3_object_content_length / CHUNK_SIZE
123
+
124
+ (0..batches).each do |batch|
125
+ start_byte = batch * CHUNK_SIZE
126
+ end_byte = start_byte + CHUNK_SIZE - 1
127
+ contents = get_the_object(connection, s3_bucket_name, s3_object_name, start_byte, end_byte)
128
+ info("Writing #{contents.length} bytes to #{s3_file}") if @verbose
129
+ file.write(contents)
130
+ end
131
+
132
+ # Now get the remainder
133
+ start_byte = batches * CHUNK_SIZE
134
+ end_byte = s3_object_content_length
135
+ contents = get_the_object(connection, s3_bucket_name, s3_object_name, start_byte, end_byte)
136
+ info("Writing #{contents.length} bytes to #{s3_file}") if @verbose
137
+ file.write(contents)
138
+
139
+ end # File.open
140
+
141
+ s3_file_stat = File.stat(s3_file)
142
+ info("Wrote #{s3_file_stat.size} bytes to #{s3_file}") if @verbose
143
+
144
+ s3_file # return the name of file written
145
+
146
+ end # method s3_fetch
147
+
148
+ def get_the_object(cnxn, bucket, name, range_start, range_end)
149
+
150
+ get_object_options = { 'Range' => "bytes=%d-%d" % [ range_start, range_end ] }
151
+ info("Getting #{name} from #{bucket}, range #{get_object_options['Range']}") if @verbose
152
+ return '' if @dry_run
153
+ response = cnxn.get_object(bucket,name,get_object_options)
154
+ debug("Response.class: #{response.class}")
155
+ debug("Response.status: #{response.status}")
156
+ response.body
157
+ end
158
+
159
+ end
160
+
161
+ end