elasticDynamoDb 1.2.1 → 1.3.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.
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