elasticDynamoDb 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MWQwZjUzN2JhMTZhNTljNTE3YWZmZDJhZGM5MzA4NDUyMTJmMTA0Yw==
5
- data.tar.gz: !binary |-
6
- ZWI3OGEyNGQyNTY4NTJkMzU3M2YxNDA4ZTU0NDgwYzU3ZDczY2IyYw==
2
+ SHA1:
3
+ metadata.gz: 1bff72ed568398542c09761c32d3150a52f4cb15
4
+ data.tar.gz: 107bf653d4a6e1e6b7d3e2bd7d5af4212f795d3a
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- ZmQ2YzgxYTM1ZDAyZDNkNmVjZDg0ZDg3MTJhNmVhYzUyOGY1ZjQyZGY2NjY5
10
- ZTdmMDE2MDQyNWViNjIwNGVlZjUzMjBkY2QxY2ZiZmFlNmJkNmIwY2Q1MmZm
11
- MzhiMmVmZjA1NmUzOTAyMjE5NGVlMDQwODdmNzk4ODgwMGI2OTE=
12
- data.tar.gz: !binary |-
13
- NGZiNWY1NWJmNjk2ODgwZDJjYjk1YWJkYzU4ZDU0MjhmNzY3YjRhZmQwODI1
14
- OGEyYzFhZDk4NDlkODE1Y2I5NjJlYzZhNTg3Yjc0NTNiOGJhOGNiNzFlMjM0
15
- MDkxZmViODFjODM3M2JjNTk5OTEwZWUxYWIyMTgwYWU0MGM0ZGE=
6
+ metadata.gz: 966408d598308dceb8ed0bd75f38b8841249a1b04cecd46539b78cc8191b23622ff2e10dd52735d75bfd3d433834d657e3d29fe28a3c15ecdf29ef4e1a55b1cd
7
+ data.tar.gz: 4a51b001649f9e23eafd72556ad5b75b03306d65a5c0e5a89bf6367e8600fa542676a0923ecee2f1bc278c69420a6f781e2235a7db0e5dd34087b7ec285b0950
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.conf
data/CHANGES.md ADDED
@@ -0,0 +1,6 @@
1
+ 1.3.0
2
+ -----
3
+
4
+ * changed aws-sdk to v2 (aws-sdk-core)
5
+ * add local dynamodb support for testing
6
+ * refactor cli to its own module
data/README.md CHANGED
@@ -4,12 +4,10 @@
4
4
 
5
5
  [dynamic-dynamodb](https://github.com/sebdah/dynamic-dynamodb) tool is great for autoscaling but it has a few limitation:
6
6
 
7
- * it does not scale down at once to certian value
7
+ * it does not scale down at once to a certain value
8
8
 
9
9
  * it does not accomodate for anticipated traffic spike that can last X hours
10
10
 
11
- * it does not scale down to a value at once
12
-
13
11
 
14
12
  ElasticDynamoDb is intended to extend the functionality of dynamic-dynamodb, allowing you to scale by a factor (up/down) and elastically return to the original values it had before
15
13
 
data/bin/elasticDynamoDb CHANGED
@@ -1,314 +1,21 @@
1
1
  #!/usr/bin/env ruby
2
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
2
3
  require 'thor'
3
- require 'aws-sdk'
4
- require 'fileutils'
4
+ require 'elasticDynamoDb'
5
5
 
6
- require_relative '../lib/configparser'
7
-
8
- class ElasticDynamoDb < Thor
9
- default_task :show_help
10
- include Thor::Actions
11
-
12
- desc "onDemand", "Ease autoscale by schedule or scale factor"
13
- method_option :factor, :required => true, :alias => 'f', :type => :numeric, :banner => 'scale factor can be decimal too 0.5 for instance'
14
- method_option :schedule_restore, :alias => 's', :type => :numeric, :banner => 'number of minutes for ElasticDynamoDb to restore original values', :default => 0
15
- method_option :working_dir, :default => '~', :alias => 'd', :banner => 'location for backup config and change log [default home dir for the user]'
16
- method_option :config_file, :required => true, :alias => 'c', :banner => 'location of dynamic-dynamodb.conf'
17
- method_option :stop_cmd, :banner => 'bash stop command for dynamic-dynamodb service'
18
- method_option :start_cmd, :banner => 'bash start command for dynamic-dynamodb service'
19
- def onDemand
20
- @@config_file = options[:config_file]
21
- @@in_restore = false
22
- @@timestamp = Time.now.strftime("%m%d%Y-%H%M%S")
23
-
24
- @@working_dir = options[:working_dir]
25
- @@bkp_folder = "#{File.expand_path(@@working_dir)}/ElasticDynamoDb/dynamodb_config_backups"
26
- @@log_file = "#{File.expand_path(@@working_dir)}/ElasticDynamoDb/change.log"
27
- config_file_name = File.basename(@@config_file)
28
- @@original_config_file = "#{@@bkp_folder}/#{config_file_name}-#{@@timestamp}"
29
-
30
- FileUtils.mkdir_p "#{File.expand_path(@@working_dir)}/ElasticDynamoDb/dynamodb_config_backups"
31
-
32
- process_config(@@config_file, options[:factor])
33
-
34
- if options[:schedule_restore] > 0 && !@@in_restore
35
- say("#{Time.now} - Waiting here for #{options[:schedule_restore]} minutes until restore")
36
- sleep options[:schedule_restore] * 60
37
- @@in_restore = true
38
- say "#{Time.now} - Restoring to original config file (#{@@original_config_file}))"
39
- process_config(@@original_config_file, 1)
40
- end
41
-
42
- say("All done! you may restart the dynamic-dynamodb process")
43
- end
44
-
45
- desc 'show_help', 'Display Usage'
46
- def show_help
47
- help_block = <<-HELP
48
- Elastic DynamoDb - an OnDemand tool to help with auto scaling of dynamic-dynamodb
49
- Synopsis:
50
- dynamic-dynamodb tool is great for autoscaling, however it does not scale down at once to certian value
51
- and it does not accomodate for anticipated traffic spike that can last X hours
52
-
53
- This tool is intended to extend the functionality of dynamic-dynamodb, allowing you to scale by a factor (up/down) and elastically return to the original values it had before
54
-
55
- it possible to automate the start and stop of the service by passing a bash command to the --start_cmd / --stop_cmd
56
-
57
- Usage:
58
- elasticDynamoDb onDemand --config-file=location of dynamic-dynamodb.conf --factor=Scale factor (can be decimal too, i.e: 0.5) --schedule-restore 120
59
-
60
- Options:
61
- --config-file=location of dynamic-dynamodb.conf
62
- --factor scale factor can be decimal too 0.5 for instance
63
- [--schedule-restore=number of minutes for ElasticDynamoDb to restore original values] # Default: 0 (No restore)
64
- [--working-dir=location for backup config and change log [default home user dir]]
65
- [--stop-cmd=bash stop command for dynamic-dynamodb service (must be wrapped in quotes)]
66
- [--start-cmd=bash start command for dynamic-dynamodb service (must be wrapped in quotes)]
67
- HELP
68
- say(help_block)
69
- end
70
-
71
- private
72
- def process_config(file, factor)
73
- read_config(file)
74
- scale(factor)
75
- write_config(factor)
76
- begin
77
- system(options[:stop_cmd]) if options[:stop_cmd]
78
- rescue Exception => e
79
- puts "error trying the stop command: #{e}"
80
- end
81
- update_aws_api
82
- end
83
-
84
- def read_config(config_file)
85
- @@config = ConfigParser.new(config_file).parse
86
- end
87
-
88
- def scale(factor=nil)
89
- if !factor.nil?
90
- scale_factor = factor
91
-
92
- active_throughputs = @@config.keys.select{|k| k =~ /table/}
93
-
94
- active_throughputs.each do |prefix|
95
- min_reads = @@config[prefix]['min-provisioned-reads'].to_i
96
- min_writes = @@config[prefix]['min-provisioned-writes'].to_i
97
-
98
- say("(scale factor: #{scale_factor}) Global Secondary Index / Table: #{prefix.gsub('$','').gsub('^', '')} =>", color=:cyan)
99
- puts "Current min-read from #{@@config_file}: #{min_reads}"
100
- puts "Current min-write from #{@@config_file}: #{min_writes}"
101
-
102
- @@config[prefix]['min-provisioned-reads'] = (min_reads * scale_factor).to_i
103
- @@config[prefix]['min-provisioned-writes'] = (min_writes * scale_factor).to_i
104
-
105
- say("New min reads: #{@@config[prefix]['min-provisioned-reads']}", color=:yellow)
106
- say("New min writes: #{@@config[prefix]['min-provisioned-writes']}", color=:yellow)
107
- puts "------------------------------------------------"
108
- end
109
- else
110
- say("Need a factor to scale by,(i.e scale --factor 2)", color=:green)
111
- exit
112
- end
113
- end
114
-
115
- def write_config(scale_factor)
116
- if options[:schedule_restore] > 0
117
- restore = "auto restore to bakcup config file (#{@@original_config_file}) in #{options[:schedule_restore]} minutes"
118
- else
119
- restore = "Backup to #{@@original_config_file}"
120
- end
121
-
122
- if @@in_restore
123
- confirmed = true
124
- else
125
- confirmed = yes?("OverWrite the new config file (#{restore})? (yes/no)", color=:red)
126
- end
127
-
128
- if confirmed
129
- backup
130
-
131
- str_to_write = ''
132
- @@config.each do |section, lines|
133
- if !section.include?('pre_section')
134
- str_to_write += "[#{section}]\n"
135
- end
136
-
137
- lines.each do |line, value|
138
- if value =~ /^(#|;|\n)/
139
- str_to_write += "#{value}"
140
- else
141
- str_to_write += "#{line}: #{value}\n"
142
- end
143
- end
144
- end
145
-
146
- save_file(str_to_write)
147
-
148
- if !@@in_restore
149
- reason = ask('type the reason for the change: ', color=:magenta)
150
- else
151
- reason = 'Auto restore to @@original_config_file'
152
- end
153
-
154
- log_changes("#{reason} - Changed throughputs by factor of: #{scale_factor}")
155
-
156
- say("new config changes commited to file")
157
- else
158
- say("Not doing antything - Goodbye...", color=:green)
159
- exit
160
- end
161
- end
162
-
163
- def log_changes(msg)
164
- say("Recording change in #{@@log_file}")
165
-
166
- File.open(@@log_file, 'a') do |file|
167
- file.write "#{Time.now} - #{msg}\n"
168
- end
169
- end
170
-
171
- def backup
172
- `mkdir -p #{@@bkp_folder} && cp #{@@config_file} #{@@original_config_file}` if @@config_file
173
- say "backup file created: #{@@original_config_file}"
174
- end
175
-
176
- def save_file(str)
177
- File.open(@@config_file, 'w') do |file|
178
- file.write str
179
- end
180
- end
181
-
182
- def update_aws_api
183
- if @@in_restore
184
- confirmed = true
185
- else
186
- confirmed = yes?('Update all tables with these values on DynamoDb? (yes/no)', color=:red)
187
- end
188
-
189
- if confirmed
190
- AWS.config({
191
- :access_key_id => @@config['global']['aws-access-key-id'],
192
- :secret_access_key => @@config['global']['aws-secret-access-key-id'],
193
- :region => @@config['global']['region'],
194
- })
195
-
196
- @@ddb = AWS::DynamoDB::Client.new(:api_version => '2012-08-10')
197
-
198
- provisioning ={}
199
-
200
- active_throughputs = @@config.keys.select{|k| k =~ /table/}
201
-
202
- active_throughputs.inject(provisioning) { |acc, config_section|
203
- config_section =~ /^(gsi:\ *\^(?<index>.*)\$|)\ *table:\ *\^(?<table>.*)\$/
204
- index = $1
205
- table = $2
206
-
207
- if config_section.include?('gsi')
208
- acc[table] ||= {}
209
- acc[table][index] ||= {}
210
- acc[table][index]['reads'] = @@config[config_section]['min-provisioned-reads'].to_i
211
- acc[table][index]['writes'] = @@config[config_section]['min-provisioned-writes'].to_i
212
- else
213
- acc[table] ||= {}
214
- acc[table]['reads'] = @@config[config_section]['min-provisioned-reads'].to_i
215
- acc[table]['writes'] = @@config[config_section]['min-provisioned-writes'].to_i
216
- end
217
- acc
218
- }
219
- log_changes("Update AWS via api call with the following data:\n #{provisioning}\n")
220
- update_tables(provisioning)
221
- else
222
- if @@in_restore
223
- confirmed = true
224
- end
225
-
226
- if options[:start_cmd]
227
- confirmed = yes?("send the start command #{options[:start_cmd]} ? (yes/no)")
228
- end
229
-
230
- if confirmed
231
- begin
232
- system(options[:start_cmd]) if options[:start_cmd]
233
- rescue Exception => e
234
- puts "error trying the start command: #{e}"
235
- end
236
- end
237
- exit
238
- end
239
- end
240
-
241
- def check_status(table_name)
242
- until table_status(table_name) == 'ACTIVE' && !indexes_status(table_name).any? {|i| i != 'ACTIVE'}
243
- say("#{table_name} is not ACTIVE => sleeping for 30 sec and will retry again", color=:yellow)
244
- sleep 30
245
- end
246
- return true
247
- end
248
-
249
- def table_status(table_name)
250
- print "Checking table #{table_name} status..."
251
- status = @@ddb.describe_table({:table_name => table_name}).table.table_status
252
- puts status
253
- status
254
- end
255
-
256
- def indexes_status(table_name)
257
- print "Checking indexes status on #{table_name}..."
258
- indexes_status = []
259
- if @@ddb.describe_table({:table_name => table_name}).table.keys.include?(:global_secondary_indexes)
260
- @@ddb.describe_table({:table_name => table_name}).table.global_secondary_indexes.each {|i| indexes_status << i.index_status }
261
- end
262
-
263
- if indexes_status.empty?
264
- puts "No indexes for #{table_name} table"
265
- else
266
- puts indexes_status
267
- end
268
- indexes_status
269
- end
270
-
271
- def update_tables(provisioning)
272
- provisioning.each do |table, values|
273
- options = {
274
- :table_name => table,
275
- :provisioned_throughput => {
276
- :read_capacity_units => values['reads'],
277
- :write_capacity_units => values['writes']
278
- }
279
- }
280
-
281
- # if one of the keys for the table contain index merge the options for update table
282
- indexes = provisioning[table].keys.select { |key| key.match(/index/) }
283
- if !indexes.empty?
284
- indexes.each do |index|
285
- options.merge!({
286
- :global_secondary_index_updates => [{:update => {
287
- :index_name => index,
288
- :provisioned_throughput => {
289
- :read_capacity_units => values[index]['reads'],
290
- :write_capacity_units => values[index]['writes']
291
- }
292
- }
293
- }]
294
- })
295
- end
296
- end
6
+ def help
7
+ ElasticDynamoDb::Cli.help(Thor::Base.shell.new)
8
+ end
297
9
 
298
- ready = check_status(table)
299
-
300
- begin
301
- say("Updating provisioning for table: #{table}",color=:cyan)
302
- @@ddb.update_table(options) if ready
303
- rescue Exception => e
304
- if e.to_s.include?('must be at most 100 percent of current value')
305
- say("No Change - Scale up is higher then 100% of the original values, restrating dynamic-dynamodb process will auto scale up to the new values", color=:yellow)
306
- else
307
- say("unable to update table: #{e}", color=:red)
308
- end
309
- end
310
- end
311
- end
10
+ begin
11
+ ENV["THOR_DEBUG"] = "1"
12
+ ElasticDynamoDb::Cli.start
13
+ rescue Thor::RequiredArgumentMissingError => e
14
+ puts "\e[31mMissing Arguments: #{e}\e[0m\n\n"
15
+ help
16
+ rescue Thor::InvocationError => e
17
+ puts "\e[31m#{e.to_s.gsub(/Usage:.+"/, '').chomp} but there's no such option\e[0m\n\n"
18
+ help
312
19
  end
313
20
 
314
- ElasticDynamoDb.start
21
+
@@ -0,0 +1,20 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require File.expand_path('../lib/elasticDynamoDb', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "elasticDynamoDb"
6
+ s.version = ElasticDynamoDb::VERSION
7
+ s.authors = ["Ami Mahloof"]
8
+ s.email = "ami.mahloof@gmail.com"
9
+ s.homepage = "https://github.com/innovia/ElasticDynamoDb"
10
+ s.summary = "scale dynamodb by factor with dynamic-dynamodb"
11
+ s.description = "Elastically scale up or down with dynamic-dynamodb tool"
12
+ s.required_rubygems_version = ">= 1.3.6"
13
+ s.files = `git ls-files`.split($\).reject{|n| n =~ %r[png|gif\z]}.reject{|n| n =~ %r[^(test|spec|features)/]}
14
+ s.add_runtime_dependency 'configparser', '~> 0'
15
+ s.add_runtime_dependency 'thor', '~> 0.19', '>= 0.19.1'
16
+ s.add_runtime_dependency 'aws-sdk-core', '~> 2.0.17', '>= 2.0.17'
17
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ s.extra_rdoc_files = ['README.md', 'LICENSE']
19
+ s.license = 'MIT'
20
+ end
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env ruby
2
+ require 'thor'
3
+ require 'aws-sdk-core'
4
+ require 'fileutils'
5
+
6
+ autoload :ConfigParser, 'elasticDynamoDb/configparser'
7
+
8
+ class ElasticDynamoDb::Cli < Thor
9
+ include Thor::Actions
10
+ default_task :onDemand
11
+ attr_accessor :restore_in_progress, :backup_folder, :config_file_name, :original_config_file, :config, :ddb,
12
+ :log_file
13
+
14
+ desc "onDemand", "Ease autoscale by schedule or scale factor"
15
+ class_option :factor, :type => :numeric, :banner => 'scale factor can be decimal too 0.5 for instance'
16
+ class_option :config_file, :banner => 'location of dynamic-dynamodb.conf'
17
+ class_option :working_dir, :default => '~', :banner => 'location for backup config and change log [default home dir for the user]'
18
+ class_option :stop_cmd, :banner => 'bash stop command for dynamic-dynamodb service'
19
+ class_option :start_cmd, :banner => 'bash start command for dynamic-dynamodb service'
20
+ class_option :schedule_restore, :type => :numeric, :default => 0, :banner => 'number of minutes for ElasticDynamoDb to restore original values'
21
+ class_option :timestamp, :default => Time.now.utc.strftime("%m%d%Y-%H%M%S")
22
+ class_option :local, :type => :boolean, :default => false, :desc => 'run on DynamoDBLocal'
23
+ def onDemand
24
+ raise Thor::RequiredArgumentMissingError, 'You must supply a scale factor' if options[:factor].nil?
25
+ raise Thor::RequiredArgumentMissingError, 'You must supply the path to the dynamic-dyanmodb config file' if options[:config_file].nil?
26
+ init
27
+ aws_init
28
+
29
+ process_config(options[:config_file], options[:factor])
30
+
31
+ if options[:schedule_restore] > 0 && !restore_in_progress
32
+ say("#{Time.now} - Waiting here for #{options[:schedule_restore]} minutes until restore")
33
+ sleep options[:schedule_restore] * 60
34
+
35
+ self.restore_in_progress = true
36
+ say "#{Time.now} - Restoring to original config file (#{self.original_config_file})"
37
+ process_config(self.original_config_file, 1)
38
+ end
39
+
40
+ unless options[:start_cmd] || options[:stop_cmd]
41
+ say "All done! you may restart the dynamic-dynamodb process manually", color = :white
42
+ end
43
+ end
44
+
45
+ map ["-v", "--version"] => :version
46
+ desc "version", "version"
47
+ def version
48
+ say ElasticDynamoDb::ABOUT
49
+ end
50
+
51
+ private
52
+ def init
53
+ working_dir = File.expand_path(options[:working_dir])
54
+ self.config_file_name = File.basename(options[:config_file])
55
+ read_config(File.expand_path(options[:config_file]))
56
+
57
+ self.restore_in_progress = false
58
+
59
+ self.backup_folder = "#{working_dir}/ElasticDynamoDb/dynamodb_config_backups"
60
+ self.log_file = "#{working_dir}/ElasticDynamoDb/change.log"
61
+
62
+ self.original_config_file = "#{self.backup_folder}/#{self.config_file_name}-#{options[:timestamp]}"
63
+
64
+ FileUtils.mkdir_p self.backup_folder
65
+ end
66
+
67
+ def aws_init
68
+ if options[:local]
69
+ ENV['AWS_REGION'] = 'us-east-1'
70
+ say "using local DynamoDB"
71
+ self.ddb = Aws::DynamoDB::Client.new({endpoint: 'http://localhost:4567', api: '2012-08-10'})
72
+ else
73
+ credentials = Aws::Credentials.new(
74
+ self.config['global']['aws-access-key-id'], self.config['global']['aws-secret-access-key-id']
75
+ )
76
+
77
+ self.ddb = Aws::DynamoDB::Client.new({
78
+ api: '2012-08-10',
79
+ region: self.config['global']['region'],
80
+ credentials: credentials
81
+ })
82
+ end
83
+ end
84
+
85
+ def process_config(file, factor)
86
+ read_config(file)
87
+ scale(factor)
88
+ write_config(factor)
89
+
90
+ process_ctrl('Stopping', options[:stop_cmd])
91
+
92
+ update_aws_api
93
+
94
+ process_ctrl('Starting', options[:start_cmd])
95
+ end
96
+
97
+ def process_ctrl(desc, action)
98
+ begin
99
+ if action
100
+ say "#{desc} dynamic-dynamodb process using the command #{action}", color = :cyan
101
+ system(action)
102
+ end
103
+ rescue Exception => e
104
+ say "Error: fail to run the command: #{e}", color = :red
105
+ end
106
+ end
107
+
108
+ def read_config(config_file)
109
+ self.config = ConfigParser.new(config_file).parse
110
+ end
111
+
112
+ def scale(factor=nil)
113
+ if !factor.nil?
114
+ scale_factor = factor
115
+
116
+ active_throughputs = self.config.keys.select{|k| k =~ /table/}
117
+
118
+ active_throughputs.each do |prefix|
119
+ min_reads = self.config[prefix]['min-provisioned-reads'].to_i
120
+ min_writes = self.config[prefix]['min-provisioned-writes'].to_i
121
+
122
+ say("(scale factor: #{scale_factor}) Global Secondary Index / Table: #{prefix.gsub('$','').gsub('^', '')} =>", color=:cyan)
123
+ say("Current min-read from #{options[:config_file]}: #{min_reads}")
124
+ say("Current min-write from #{options[:config_file]}: #{min_writes}")
125
+
126
+ self.config[prefix]['min-provisioned-reads'] = (min_reads * scale_factor).to_i
127
+ self.config[prefix]['min-provisioned-writes'] = (min_writes * scale_factor).to_i
128
+
129
+ say("New min reads: #{self.config[prefix]['min-provisioned-reads']}", color=:yellow)
130
+ say("New min writes: #{self.config[prefix]['min-provisioned-writes']}", color=:yellow)
131
+ say("------------------------------------------------")
132
+ end
133
+ else
134
+ say("Need a factor to scale by,(i.e scale --factor 2)", color=:green)
135
+ exit
136
+ end
137
+ end
138
+
139
+ def write_config(scale_factor)
140
+ if options[:schedule_restore] > 0
141
+ restore = "\n\nAuto restore to backup config file (#{self.original_config_file}) in #{options[:schedule_restore]} minutes"
142
+ else
143
+ restore = "Backup will be save to #{self.original_config_file}"
144
+ end
145
+
146
+ if self.restore_in_progress
147
+ confirmed = true
148
+ else
149
+ say "#{restore}", color = :white
150
+ confirmed = yes?("Overwrite the new config file? (yes/no)", color=:white)
151
+ end
152
+
153
+ if confirmed
154
+ backup
155
+
156
+ str_to_write = ''
157
+ self.config.each do |section, lines|
158
+ if !section.include?('pre_section')
159
+ str_to_write += "[#{section}]\n"
160
+ end
161
+
162
+ lines.each do |line, value|
163
+ if value =~ /^(#|;|\n)/
164
+ str_to_write += "#{value}"
165
+ else
166
+ str_to_write += "#{line}: #{value}\n"
167
+ end
168
+ end
169
+ end
170
+
171
+ save_file(str_to_write)
172
+
173
+ if !self.restore_in_progress
174
+ reason = ask("\nType the reason for the change: ", color=:magenta)
175
+ else
176
+ reason = "Auto restore to #{self.original_config_file}"
177
+ end
178
+
179
+ log_changes("#{reason} - Changed throughputs by factor of: #{scale_factor}")
180
+
181
+ say("New config changes commited to file")
182
+ else
183
+ say("Not doing antything - Goodbye...", color=:green)
184
+ exit
185
+ end
186
+ end
187
+
188
+ def log_changes(msg)
189
+ say("Recording change in #{self.log_file}")
190
+
191
+ File.open(self.log_file, 'a') do |file|
192
+ file.write "#{Time.now} - #{msg}\n"
193
+ end
194
+ end
195
+
196
+ def backup
197
+ say "Backing up config file: #{self.original_config_file}"
198
+ FileUtils.mkdir_p self.backup_folder
199
+ FileUtils.cp options[:config_file], self.original_config_file
200
+ end
201
+
202
+ def save_file(str)
203
+ File.open(options[:config_file], 'w') do |file|
204
+ file.write str
205
+ end
206
+ end
207
+
208
+ def update_aws_api
209
+ if self.restore_in_progress
210
+ confirmed = true
211
+ else
212
+ confirmed = yes?("\nUpdate all tables with these values on DynamoDb? (yes/no)", color=:white)
213
+ end
214
+
215
+ if confirmed
216
+
217
+ provisioning ={}
218
+
219
+ active_throughputs = self.config.keys.select{|k| k =~ /table/}
220
+ active_throughputs.inject(provisioning) { |acc, config_section|
221
+ config_section =~ /^(gsi:\ *\^(?<index>.*)\$|)\ *table:\ *\^(?<table>.*)\$/
222
+ index = $1
223
+ table = $2
224
+
225
+ if config_section.include?('gsi')
226
+ acc[table] ||= {}
227
+ acc[table][index] ||= {}
228
+ acc[table][index]['reads'] = self.config[config_section]['min-provisioned-reads'].to_i
229
+ acc[table][index]['writes'] = self.config[config_section]['min-provisioned-writes'].to_i
230
+ else
231
+ acc[table] ||= {}
232
+ acc[table]['reads'] = self.config[config_section]['min-provisioned-reads'].to_i
233
+ acc[table]['writes'] = self.config[config_section]['min-provisioned-writes'].to_i
234
+ end
235
+ acc
236
+ }
237
+ log_changes("Update AWS via api call with the following data:\n #{provisioning}\n")
238
+ say "\nWill update: #{provisioning.keys.size} tables\n\n\n", color = :blue
239
+ update_tables(provisioning)
240
+ else
241
+ if self.restore_in_progress
242
+ confirmed = true
243
+ end
244
+
245
+ if options[:start_cmd]
246
+ confirmed = yes?("Send the start command #{options[:start_cmd]} ? (yes/no)")
247
+ end
248
+
249
+ if confirmed
250
+ begin
251
+ if options[:start_cmd]
252
+ say "Starting up dynamic-dynamodb service using the command #{options[:start_cmd]}", color = :white
253
+ system(options[:start_cmd])
254
+ end
255
+ rescue Exception => e
256
+ say "Error trying the start command: #{e.message}", color = :red
257
+ end
258
+ end
259
+ exit
260
+ end
261
+ end
262
+
263
+ def check_status(table_name)
264
+ until table_status(table_name) == 'ACTIVE' && !indexes_status(table_name).any? {|i| i != 'ACTIVE'}
265
+ say("#{table_name} is not ACTIVE => sleeping for 30 sec and will retry again", color=:yellow)
266
+ sleep 30
267
+ end
268
+ return true
269
+ end
270
+
271
+ def table_status(table_name)
272
+ print "Checking table #{table_name} status..."
273
+ status = self.ddb.describe_table({:table_name => table_name}).table.table_status
274
+ puts status
275
+ status
276
+ end
277
+
278
+ def indexes_status(table_name)
279
+ print "Checking indexes status on #{table_name}..."
280
+ indexes_status = []
281
+ if !self.ddb.describe_table({:table_name => table_name}).table.global_secondary_indexes.nil?
282
+ self.ddb.describe_table({:table_name => table_name}).table.global_secondary_indexes.each {|i| indexes_status << i.index_status }
283
+ end
284
+
285
+ if indexes_status.empty?
286
+ say("No indexes for #{table_name} table")
287
+ else
288
+ say(indexes_status)
289
+ end
290
+ indexes_status
291
+ end
292
+
293
+ def update_single_table(table_options)
294
+ while true
295
+ ready = check_status(table_options[:table_name])
296
+
297
+ if ready
298
+ say "Updating provisioning for table: #{table_options[:table_name]}...", color = :cyan
299
+ begin
300
+ result = self.ddb.update_table(table_options)
301
+ rescue Exception => e
302
+ say "\nUnable to update table: #{e.message}\n", color = :red
303
+
304
+ if e.message.include?('The requested throughput value equals the current value') || e.message.include?('The requested value equals the current value')
305
+ say "Skipping table update - the requested throughput value equals the current value", color = :yellow
306
+ return
307
+ end
308
+
309
+ dynamo_max_limit_error = 'Only 10 tables can be created, updated, or deleted simultaneously'
310
+ if e.message.include?(dynamo_max_limit_error)
311
+ say "#{dynamo_max_limit_error}\nTable #{table_options[:table_name]} is not ready for update, waiting 5 sec before retry", color = :yellow
312
+ sleep 5
313
+ end
314
+
315
+ say "\nRetrying update on #{table_options[:table_name]}", color = :yellow
316
+ update_single_table(table_options) if e.message.include?('The requested throughput value equals the current value')
317
+ end
318
+ return
319
+
320
+ else
321
+ say "Table #{table_options[:table_name]} is not ready for update, waiting 5 sec before retry", color = :yellow
322
+ sleep 5
323
+ end
324
+ end
325
+ end
326
+
327
+ def update_tables(provisioning)
328
+ provisioning.each do |table, values|
329
+ table_options = {
330
+ :table_name => table,
331
+ :provisioned_throughput => {
332
+ :read_capacity_units => values['reads'],
333
+ :write_capacity_units => values['writes']
334
+ }
335
+ }
336
+
337
+ # if one of the keys for the table contain index merge the options for update table
338
+ indexes = provisioning[table].keys.select { |key| key.match(/index/) }
339
+ if !indexes.empty?
340
+ indexes.each do |index|
341
+ table_options.merge!({
342
+ :global_secondary_index_updates => [{:update => {
343
+ :index_name => index,
344
+ :provisioned_throughput => {
345
+ :read_capacity_units => values[index]['reads'],
346
+ :write_capacity_units => values[index]['writes']
347
+ }
348
+ }
349
+ }]
350
+ })
351
+ end
352
+ end
353
+
354
+ update_single_table(table_options)
355
+ end
356
+ end
357
+ end
@@ -7,7 +7,8 @@ class ConfigParser < Hash
7
7
  begin
8
8
  input_source = File.open(@file_name, "r").each_line if @file_name
9
9
  rescue Exception => e
10
- puts "error loading file -> #{e}"
10
+ puts "\e[31mError loading file: #{e}\e[0m\n\n"
11
+ exit 1
11
12
  end
12
13
  config = {}
13
14
  config_section = nil
@@ -0,0 +1,6 @@
1
+ module ElasticDynamoDb
2
+ VERSION = "1.3.0"
3
+ ABOUT = "ElasticDynamoDb v#{VERSION} (c) #{Time.now.strftime("2014-%Y")} @innovia"
4
+
5
+ autoload :Cli, 'elasticDynamoDb/cli'
6
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticDynamoDb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ami Mahloof
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-16 00:00:00.000000000 Z
11
+ date: 2015-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: configparser
@@ -31,7 +31,7 @@ dependencies:
31
31
  - - ~>
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0.19'
34
- - - ! '>='
34
+ - - '>='
35
35
  - !ruby/object:Gem::Version
36
36
  version: 0.19.1
37
37
  type: :runtime
@@ -41,29 +41,29 @@ dependencies:
41
41
  - - ~>
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0.19'
44
- - - ! '>='
44
+ - - '>='
45
45
  - !ruby/object:Gem::Version
46
46
  version: 0.19.1
47
47
  - !ruby/object:Gem::Dependency
48
- name: aws-sdk
48
+ name: aws-sdk-core
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ~>
52
52
  - !ruby/object:Gem::Version
53
- version: '1.40'
54
- - - ! '>='
53
+ version: 2.0.17
54
+ - - '>='
55
55
  - !ruby/object:Gem::Version
56
- version: 1.40.0
56
+ version: 2.0.17
57
57
  type: :runtime
58
58
  prerelease: false
59
59
  version_requirements: !ruby/object:Gem::Requirement
60
60
  requirements:
61
61
  - - ~>
62
62
  - !ruby/object:Gem::Version
63
- version: '1.40'
64
- - - ! '>='
63
+ version: 2.0.17
64
+ - - '>='
65
65
  - !ruby/object:Gem::Version
66
- version: 1.40.0
66
+ version: 2.0.17
67
67
  description: Elastically scale up or down with dynamic-dynamodb tool
68
68
  email: ami.mahloof@gmail.com
69
69
  executables:
@@ -73,10 +73,15 @@ extra_rdoc_files:
73
73
  - README.md
74
74
  - LICENSE
75
75
  files:
76
+ - .gitignore
77
+ - CHANGES.md
76
78
  - LICENSE
77
79
  - README.md
78
80
  - bin/elasticDynamoDb
79
- - lib/configparser.rb
81
+ - elasticDynamoDb.gemspec
82
+ - lib/elasticDynamoDb.rb
83
+ - lib/elasticDynamoDb/cli.rb
84
+ - lib/elasticDynamoDb/configparser.rb
80
85
  homepage: https://github.com/innovia/ElasticDynamoDb
81
86
  licenses:
82
87
  - MIT
@@ -87,17 +92,17 @@ require_paths:
87
92
  - lib
88
93
  required_ruby_version: !ruby/object:Gem::Requirement
89
94
  requirements:
90
- - - ! '>='
95
+ - - '>='
91
96
  - !ruby/object:Gem::Version
92
97
  version: '0'
93
98
  required_rubygems_version: !ruby/object:Gem::Requirement
94
99
  requirements:
95
- - - ! '>='
100
+ - - '>='
96
101
  - !ruby/object:Gem::Version
97
102
  version: 1.3.6
98
103
  requirements: []
99
104
  rubyforge_project:
100
- rubygems_version: 2.2.2
105
+ rubygems_version: 2.4.2
101
106
  signing_key:
102
107
  specification_version: 4
103
108
  summary: scale dynamodb by factor with dynamic-dynamodb