snapshot_reload 1.0.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.
@@ -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