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 +5 -13
- data/.gitignore +3 -0
- data/CHANGES.md +6 -0
- data/README.md +1 -3
- data/bin/elasticDynamoDb +15 -308
- data/elasticDynamoDb.gemspec +20 -0
- data/lib/elasticDynamoDb/cli.rb +357 -0
- data/lib/{configparser.rb → elasticDynamoDb/configparser.rb} +2 -1
- data/lib/elasticDynamoDb.rb +6 -0
- metadata +20 -15
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZWI3OGEyNGQyNTY4NTJkMzU3M2YxNDA4ZTU0NDgwYzU3ZDczY2IyYw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1bff72ed568398542c09761c32d3150a52f4cb15
|
4
|
+
data.tar.gz: 107bf653d4a6e1e6b7d3e2bd7d5af4212f795d3a
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
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
data/CHANGES.md
ADDED
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
|
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 '
|
4
|
-
require 'fileutils'
|
4
|
+
require 'elasticDynamoDb'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
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 "
|
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
|
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.
|
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:
|
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:
|
54
|
-
- -
|
53
|
+
version: 2.0.17
|
54
|
+
- - '>='
|
55
55
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
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:
|
64
|
-
- -
|
63
|
+
version: 2.0.17
|
64
|
+
- - '>='
|
65
65
|
- !ruby/object:Gem::Version
|
66
|
-
version:
|
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
|
-
-
|
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.
|
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
|