blackstack-deployer 1.2.11 → 1.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/blackstack-deployer.rb +669 -678
  3. metadata +9 -9
@@ -1,679 +1,670 @@
1
- require 'blackstack-nodes'
2
- require 'sequel'
3
-
4
- module BlackStack
5
- # Deployer is a library that can be used to deploy a cluster of nodes.
6
- module Deployer
7
- @@logger = BlackStack::BaseLogger.new(nil)
8
- @@nodes = []
9
- @@routines = []
10
-
11
- # set the logger
12
- def self.set_logger(i_logger)
13
- @@logger = i_logger
14
- end
15
-
16
- # get the logger assigned to the module
17
- def self.logger
18
- @@logger
19
- end # def self.errors
20
-
21
- # get the array of nodes assigned to the module
22
- def self.nodes
23
- @@nodes
24
- end # def self.nodes
25
-
26
- # get the array of routines assigned to the module
27
- def self.routines
28
- @@routines
29
- end # def self.routines
30
-
31
- # inherit BlackStack::Infrastructure::NodeModule, including features of deployer.
32
- module NodeModule
33
- attr_accessor :deployment_routine, :parameters
34
-
35
- include BlackStack::Infrastructure::NodeModule
36
-
37
- def self.eth0_ip(insterface)
38
- ret = nil
39
- a = `ip addr show dev #{insterface}`.scan(/inet [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)
40
- if a.size > 0
41
- ret = a.last.to_s.gsub(/inet /, '')
42
- else
43
- raise "Cannot get ip address of the interface #{insterface}"
44
- end
45
- ret
46
- end
47
-
48
- # get the IP address for an interface using the ip addr command.
49
- # this is a helper method for installing cockroachdb nodes.
50
- def eth0_ip()
51
- ret = nil
52
- a = self.ssh.exec!("ip addr show dev #{parameters[:laninterface]}").scan(/inet [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)
53
- if a.size > 0
54
- ret = a.last.to_s.gsub(/inet /, '')
55
- else
56
- raise "Cannot get ip address of the interface #{parameters[:laninterface]}"
57
- end
58
- ret
59
- end
60
-
61
- def self.descriptor_errors(h)
62
- errors = BlackStack::Infrastructure::NodeModule.descriptor_errors(h)
63
-
64
- # validate: does not exist any other element in @@nodes with the same value for the parameter h[:name]
65
- errors << "The parameter h[:name] is not unique" if BlackStack::Deployer.nodes.select{|n| n.name == h[:name]}.length > 0
66
-
67
- # validate: h[:deployment_routine] is not nil
68
- errors << "The parameter h[:deployment_routine] is required" if h[:deployment_routine].nil?
69
-
70
- # validate: h[:deployment_routine] is a string
71
- errors << "The parameter h[:deployment_routine] is not a string" unless h[:deployment_routine].is_a?(String)
72
-
73
- # return list of errors
74
- errors.uniq
75
- end
76
-
77
- def initialize(h, i_logger=nil)
78
- self.parameters = h
79
- errors = BlackStack::Deployer::NodeModule.descriptor_errors(h)
80
- raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
81
- super(h, i_logger)
82
- self.deployment_routine = h[:deployment_routine]
83
- end # def self.create(h)
84
-
85
- def to_hash
86
- h = super
87
- h[:deployment_routine] = self.deployment_routine
88
- h
89
- end # def to_hash
90
-
91
- def deploy()
92
- BlackStack::Deployer::run_routine(self.name, self.deployment_routine);
93
- end
94
- end # module NodeModule
95
-
96
- # define attributes and methods of a deployer routine
97
- module RoutineModule
98
- attr_accessor :name, :commands
99
-
100
- def self.descriptor_errors(h)
101
- errors = []
102
-
103
- # validate: the parameter h is a hash
104
- errors << "The parameter h is not a hash" unless h.is_a?(Hash)
105
-
106
- # validate: the paramerer h has a key :name
107
- errors << "The parameter h does not have a key :name" unless h.has_key?(:name)
108
-
109
- # validate: the paramerer h has a key :command
110
- errors << "The parameter h does not have a key :commands" unless h.has_key?(:commands)
111
-
112
- # validate: the parameter h[:name] is a string or a symbol
113
- errors << "The parameter h[:name] is not a string" unless h[:name].is_a?(String)
114
-
115
- # validate: the parameter h[:name] is not 'reboot' because it is a reserved name
116
- errors << "The parameter h[:name] is a reserved name (#{h[:name].to_s})" if h[:name] == 'reboot'
117
-
118
- # validate: the parameter h[:commands] is required
119
- errors << "The parameter h[:commands] is required" if h[:commands].nil?
120
-
121
- # validate: the parametrer h[:commands] is an array
122
- errors << "The parameter h[:commands] is not an array" unless h[:commands].is_a?(Array)
123
-
124
- # validate: the parameter h[:commands] has at least one element
125
- errors << "The parameter h[:commands] does not have at least one element" unless h[:commands].size > 0
126
-
127
- # validate: each element of the array h[:commands] is a hash
128
- h[:commands].each do |c|
129
- errors += BlackStack::Deployer::CommandModule.descriptor_errors(c)
130
- end # h[:commands].each do |c|
131
-
132
- errors.uniq
133
- end # def self.descriptor_error(h)
134
-
135
- def initialize(h)
136
- errors = BlackStack::Deployer::RoutineModule.descriptor_errors(h)
137
- raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
138
- self.name = h[:name]
139
- self.commands = []
140
- h[:commands].each do |c|
141
- self.commands << BlackStack::Deployer::Command.new(c)
142
- end
143
- end
144
-
145
- def to_hash
146
- h = {}
147
- h[:name] = self.name
148
- h[:commands] = []
149
- self.commands.each do |c|
150
- h[:commands] << c.to_hash
151
- end
152
- h
153
- end
154
-
155
- def run(node)
156
- ret = []
157
- self.commands.each do |c|
158
- BlackStack::Deployer.logger.logs "Running command: #{c.command.to_s}... "
159
- h = c.run(node)
160
- ret << h
161
-
162
- #BlackStack::Deployer.logger.logs "Result: "
163
- #BlackStack::Deployer.logger.logf h.to_s
164
-
165
- if h[:errors].size == 0
166
- BlackStack::Deployer.logger.done
167
- else
168
- BlackStack::Deployer.logger.logf('error: ' + h.to_s)
169
- raise "Error running command: #{h.to_s}"
170
- end
171
- end
172
- ret
173
- end # def run(node)
174
- end # module RoutineModule
175
-
176
- # define attributes and methods of a routine's command
177
- module CommandModule
178
- attr_accessor :command, :matches, :nomatches, :sudo
179
-
180
- def self.descriptor_errors(c)
181
- errors = []
182
-
183
- # validate: h is a hash
184
- errors << "The command descriptor is not a hash" unless c.is_a?(Hash)
185
-
186
- # validate: the hash c has a key :command
187
- errors << "The command descriptor does not have a key :command" unless c.has_key?(:command)
188
-
189
- # validate: the value of c[:command] is a string or symbol
190
- errors << "The value of c[:command] is not a string and is not a symbol" unless c[:command].is_a?(String) || c[:command].is_a?(Symbol)
191
-
192
- # validate: if the key :sudo exists, then its value is a boolean
193
- if c.has_key?(:sudo)
194
- errors << "The value of c[:sudo] is not a boolean" unless c[:sudo].is_a?(TrueClass) || c[:sudo].is_a?(FalseClass)
195
- end
196
-
197
- # if the parameter h[:name] is a symbol
198
- if c[:command].is_a?(Symbol)
199
- if c[:command] == :reboot
200
- # :reboot is a reserved word, so it is fine to call :reboot
201
- else
202
- # validate: existis a routine with a the value c[:command].to_s on its :name key
203
- errors << "The routine with the name #{c[:command].to_s} does not exist" unless BlackStack::Deployer::routines.select { |r| r.name == c[:command].to_s }.size > 0
204
- end
205
- end
206
-
207
- # if c[:matches] exists
208
- if c.has_key?(:matches)
209
- # validate: the value of c[:matches] must by a regex or an array
210
- errors << "The value of the key :matches is not a regex nor an array" unless c[:matches].is_a?(Regexp) || c[:matches].is_a?(Array)
211
- # if c[:matches] is a array
212
- if c[:matches].is_a?(Array)
213
- # validate: each element in the the array c[:matches] is a regex
214
- c[:matches].each do |m|
215
- errors += BlackStack::Deployer::MatchModule.descriptor_errors(m)
216
- end # each
217
- end # if c[:matches].is_a?(Array)
218
- end # if :matches exists
219
-
220
- # if c[:nomatches] exists
221
- if c.has_key?(:nomatches)
222
- # validate: the value of c[:nomatches] must by a regex or an array
223
- errors << "The value of the key :nomatches is not a regex nor an array" unless c[:nomatches].is_a?(Regexp) || c[:nomatches].is_a?(Array)
224
- # if c[:nomatches] is a array
225
- if c[:nomatches].is_a?(Array)
226
- # validate: each element in the the array c[:nomatches] is a hash
227
- c[:nomatches].each do |m|
228
- errors += BlackStack::Deployer::NoMatchModule.descriptor_errors(m)
229
- end # each
230
- end # if c[:matches].is_a?(Array)
231
- end # if :matches exists
232
- #
233
- errors.uniq
234
- end # def self.descriptor_error(h)
235
-
236
- def initialize(h)
237
- errors = BlackStack::Deployer::CommandModule.descriptor_errors(h)
238
- raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
239
- self.command = h[:command]
240
- self.sudo = h[:sudo].nil? ? true : h[:sudo]
241
- self.matches = []
242
- self.nomatches = []
243
- if h.has_key?(:matches)
244
- if h[:matches].is_a?(Regexp)
245
- self.matches << BlackStack::Deployer::Match.new(h[:matches])
246
- else
247
- h[:matches].each do |m|
248
- self.matches << BlackStack::Deployer::Match.new(m)
249
- end
250
- end
251
- end
252
- if h.has_key?(:nomatches)
253
- if h[:nomatches].is_a?(Regexp)
254
- self.nomatches << BlackStack::Deployer::NoMatch.new(h[:nomatches])
255
- else
256
- h[:nomatches].each do |m|
257
- self.nomatches << BlackStack::Deployer::NoMatch.new(m)
258
- end
259
- end
260
- end
261
- end # def initialize(h)
262
-
263
- def to_hash
264
- h = {}
265
- h[:command] = self.command
266
- h[:sudo] = self.sudo
267
- h[:matches] = []
268
- h[:nomatches] = []
269
- self.matches.each do |m|
270
- h[:matches] << m.to_hash
271
- end
272
- self.nomatches.each do |m|
273
- h[:nomatches] << m.to_hash
274
- end
275
- h
276
- end # def to_hash
277
-
278
- def run(node)
279
- errors = []
280
- code = self.command
281
- output = nil
282
-
283
- # if code is a symbol
284
- if code.is_a?(Symbol)
285
-
286
- # if code is equel than :reboot
287
- if code == :reboot
288
- # call the node reboot method
289
- node.reboot
290
- else
291
- # look for a routine with this name
292
- r = BlackStack::Deployer.routines.select { |r| r.name == code.to_s }.first
293
- if !r.nil?
294
- r.run(node)
295
- else
296
- raise "The routine #{code.to_s} does not exist"
297
- end
298
- end
299
-
300
- # if code is a string
301
- elsif code.is_a?(String)
302
- # replacing parameters
303
- code.scan(/%[a-zA-Z0-9\_]+%/).each do |p|
304
- if p == '%eth0_ip%' # reserved parameter
305
- # TODO: move the method eth0_ip to the blackstack-nodes library
306
- code.gsub!(p, node.eth0_ip)
307
- elsif p == '%timestamp%' # reserved parameter
308
- # TODO: move this to a timestamp function on blackstack-core
309
- code.gsub!(p, Time.now.to_s.gsub(/\D/, ''))
310
- else
311
- if node.parameters.has_key?(p.gsub(/%/, '').to_sym)
312
- code.gsub!(p, node.parameters[p.gsub(/%/, '').to_sym].to_s)
313
- else
314
- raise "The parameter #{p} does not exist in the node descriptor #{node.parameters.to_s}"
315
- end
316
- end
317
- end
318
-
319
- # running the command
320
- output = node.exec(code, self.sudo)
321
-
322
- # validation: at least one of the matches should happen
323
- if self.matches.size > 0
324
- i = 0
325
- self.matches.each do |m|
326
- if m.validate(output).size == 0 # no errors
327
- i += 1
328
- end
329
- end
330
- errors << "Command output doesn't match with any of the :matches" if i == 0
331
- end # if self.matches.size > 0
332
-
333
- # validation: no one of the nomatches should happen
334
- self.nomatches.each do |m|
335
- errors += m.validate(output)
336
- end
337
- end # elsif code.is_a?(String)
338
-
339
- # return a hash descriptor of the command result
340
- {
341
- :command => self.command,
342
- :code => code,
343
- :output => output,
344
- :errors => errors,
345
- }
346
- end # def run(node)
347
- end # module CommandModule
348
-
349
- # define attributes and methods of a command's match
350
- module MatchModule
351
- attr_accessor :match
352
-
353
- def self.descriptor_errors(m)
354
- errors = []
355
- # validate the match is a regular expresion
356
- errors << "The match is not a regex" unless m.is_a?(Regexp)
357
- #
358
- errors.uniq
359
- end # def self.descriptor_error(h)
360
-
361
- def initialize(h)
362
- errors = BlackStack::Deployer::MatchModule.descriptor_errors(h)
363
- raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
364
- self.match = h
365
- end
366
-
367
- def to_hash
368
- h = {}
369
- h[:match] = self.match
370
- h
371
- end
372
-
373
- def validate(output)
374
- errors = []
375
- errors << "The output of the command does not match the regular expression #{self.match.inspect}" unless output.match(self.match)
376
- errors
377
- end
378
- end # module MatchModule
379
-
380
- # define attributes and methods of a command's nomatch
381
- module NoMatchModule
382
- attr_accessor :nomatch, :error_description
383
-
384
- def self.descriptor_errors(m)
385
- errors = []
386
- # validate the nomatch is a regular expresion
387
- errors << "Each nomatch is not a regex nor a hash" unless m.is_a?(Hash) || m.is_a?(Regexp)
388
- # if m is a hash
389
- if m.is_a?(Hash)
390
- # validate: the hash m has a key :nomatch
391
- errors << "The hash descriptor of the nomatch does not have a key :nomatch" unless m.has_key?(:nomatch)
392
- # validate: the value of m[:nomatch] is a string
393
- errors << "The value of the key :nomatch is not a regex" unless m[:nomatch].is_a?(Regexp)
394
- # validate: the hash m has a key :error_description
395
- errors << "The hash descriptor of the nomatch does not have a key :error_description" unless m.has_key?(:error_description)
396
- # validate: the value of m[:error_description] is a string
397
- errors << "The value of the key :error_description is not a string" unless m[:error_description].is_a?(String)
398
- end # if m.is_a?(Hash)
399
- #
400
- errors.uniq
401
- end # def self.descriptor_error(h)
402
-
403
- def initialize(h)
404
- errors = BlackStack::Deployer::NoMatchModule.descriptor_errors(h)
405
- raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
406
- self.nomatch = h[:nomatch]
407
- self.error_description = h[:error_description]
408
- end
409
-
410
- def to_hash
411
- h = {}
412
- h[:nomatch] = self.nomatch
413
- h[:error_description] = self.error_description
414
- h
415
- end
416
-
417
- def validate(output)
418
- errors = []
419
- if !self.error_description.nil?
420
- errors << self.error_description if output.match(self.nomatch)
421
- else
422
- errors << "The output of the command matches the regular expression #{self.nomatch.inspect}" if output.match(self.nomatch)
423
- end
424
- errors
425
- end
426
-
427
- end # module NoMatchModule
428
-
429
-
430
- # TODO: declare these classes (stub and skeleton) using blackstack-rpc
431
- #
432
- # Stub Classes
433
- # These classes represents a node, without using connection to the database.
434
- # Use this class at the client side.
435
- class Node
436
- include BlackStack::Deployer::NodeModule
437
- end # class Node
438
-
439
- class Command
440
- include BlackStack::Deployer::CommandModule
441
- end # class Command
442
-
443
- class Routine
444
- include BlackStack::Deployer::RoutineModule
445
- end # class Routine
446
-
447
- class Match
448
- include BlackStack::Deployer::MatchModule
449
- end # class Match
450
-
451
- class NoMatch
452
- include BlackStack::Deployer::NoMatchModule
453
- end # class NoMatch
454
-
455
- # add a node to the list of nodes.
456
- def self.add_node(h)
457
- errors = BlackStack::Deployer::NodeModule.descriptor_errors(h)
458
- raise errors.join(".\n") unless errors.empty?
459
- @@nodes << BlackStack::Deployer::Node.new(h, @@logger)
460
- end # def
461
-
462
- # add an array of nodes to the list of nodes.
463
- def self.add_nodes(a)
464
- # validate: the parameter a is an array
465
- raise "The parameter a is not an array" unless a.is_a?(Array)
466
- a.each { |h| BlackStack::Deployer.add_node(h) }
467
- end # def
468
-
469
- # remove all exisiting nodes in he list of nodes.
470
- # then, add the nodes in the parameter a to the list of nodes.
471
- def self.set_nodes(a)
472
- @@nodes.clear
473
- BlackStack::Deployer.add_nodes(a)
474
- end # def
475
-
476
- # add a routine to the list of routines.
477
- def self.add_routine(h)
478
- errors = BlackStack::Deployer::RoutineModule.descriptor_errors(h)
479
- raise errors.join(".\n") unless errors.empty?
480
- @@routines << BlackStack::Deployer::Routine.new(h)
481
- end # def
482
-
483
- # add an array of routines to the list of routines.
484
- def self.add_routines(a)
485
- # validate: the parameter a is an array
486
- raise "The parameter a is not an array" unless a.is_a?(Array)
487
- a.each { |h| BlackStack::Deployer.add_routine(h) }
488
- end # def
489
-
490
- # remove all exisiting routines in he list of routines.
491
- # then, add the routines in the parameter a to the list of routines.
492
- def self.set_routines(a)
493
- @@routines.clear
494
- BlackStack::Deployer.add_routines(a)
495
- end # def
496
-
497
- # running a routine on a node
498
- def self.run_routine(node_name, routine_name)
499
- errors = []
500
-
501
- # find the node with the value node_name in the key :name
502
- n = @@nodes.select { |n| n.name == node_name }.first
503
-
504
- # find the routine with the value routine_name in the key :name
505
- r = @@routines.select { |r| r.name == routine_name }.first
506
-
507
- # validate: the node n exists
508
- errors << "Node #{node_name} not found" unless n
509
-
510
- # validate: the routine r exists
511
- errors << "Routine #{routine_name} not found" unless r
512
-
513
- # raise exception if any error has been found
514
- raise "The routine #{routine_name} cannot be run on the node #{node_name}: #{errors.uniq.join(".\n")}" if errors.length > 0
515
-
516
- # connect the node
517
- self.logger.logs "Connecting to node #{n.name}... "
518
- n.connect
519
- self.logger.done
520
-
521
- # run the routine
522
- self.logger.logs "Running routine #{r.name}... "
523
- r.run(n)
524
- self.logger.done
525
-
526
- # disconnect the node
527
- self.logger.logs "Disconnecting from node #{n.name}... "
528
- n.disconnect
529
- self.logger.done
530
-
531
- end # def
532
-
533
- module DB
534
- LOCKFILE = './blackstack-deployer.lock'
535
- @@checkpoint = nil
536
- @@superhuser = nil
537
- @@ndb = nil
538
- @@folder = nil
539
-
540
- def self.set_checkpoint(s)
541
- @@checkpoint = s
542
- end
543
-
544
- def self.checkpoint
545
- @@checkpoint
546
- end
547
-
548
- def self.save_checkpoint
549
- File.new('./blackstack-deployer.lock', "w").write(@@checkpoint)
550
- end
551
-
552
- def self.load_checkpoint
553
- if File.exists?(BlackStack::Deployer::DB::LOCKFILE)
554
- @@checkpoint = File.new(BlackStack::Deployer::DB::LOCKFILE, "r").read
555
- else
556
- @@checkpoint = nil
557
- end
558
- @@checkpoint
559
- end
560
-
561
- def self.connect(s)
562
- @@db = Sequel::connect(s)
563
- end # def
564
-
565
- def self.set_folder(s)
566
- @@folder = s
567
- end # def
568
-
569
-
570
- # Return `true` if the name of the file matches with `/\.transactions\./`, and it doesn't match with `/\.sentences\./`, and the matches with `/\.transactions\./` are no more than one.
571
- # Otherwise, return `false`.
572
- # This method should not be called directly by user code.
573
- def self.is_transactions_file?(filename)
574
- filename =~ /\.transactions\./ && filename !~ /\.sentences\./ && filename.scan(/\.transactions\./).size == 1
575
- end
576
-
577
- # Return `true` if the name of the file matches with `/\.sentences\./`, and it doesn't match with `/\.transactions\./`, and the matches with `/\.sentences\./` are no more than one.
578
- # Otherwise({, return `false`.
579
- # This method should not be called directly by user code.
580
- def self.is_sentences_file?(filename)
581
- filename =~ /\.sentences\./ && filename !~ /\.transactions\./ && filename.scan(/\.sentences\./).size == 1
582
- end
583
-
584
- # Method to process an `.sql` file with transactions code, separated by `BEGIN;` and `COMMIT;` statements.
585
- # Reference: https://stackoverflow.com/questions/64066344/import-large-sql-files-with-ruby-sequel-gem
586
- # This method is called by `BlackStack::Deployer::db_execute_file` if the filename matches with `/\.tsql\./`.
587
- # This method should not be called directly by user code.
588
- def self.execute_transactions(sql)
589
- # TODO: Code Me!
590
- end # def db_execute_tsql
591
-
592
- # Method to process an `.sql` file with one sql sentence by line.
593
- # Reference: https://stackoverflow.com/questions/64066344/import-large-sql-files-with-ruby-sequel-gem
594
- # This method is called by `BlackStack::Deployer::db_execute_file` if the filename matches with `/\.sentences\./`.
595
- # This method should not be called directly by user code.
596
- def self.execute_sentences(sql, chunk_size=200)
597
- tlogger = BlackStack::Deployer::logger
598
-
599
- # Fix issue: Ruby `split': invalid byte sequence in UTF-8 (ArgumentError)
600
- # Reference: https://stackoverflow.com/questions/11065962/ruby-split-invalid-byte-sequence-in-utf-8-argumenterror
601
- #
602
- # Fix issue: `PG::SyntaxError: ERROR: at or near "��truncate": syntax error`
603
- #
604
- sql.encode!('UTF-8', :invalid => :replace, :replace => '')
605
-
606
- # Remove null bytes to avoid error: `String contains null byte`
607
- # Reference: https://stackoverflow.com/questions/29320369/coping-with-string-contains-null-byte-sent-from-users
608
- sql.gsub!("\u0000", "")
609
-
610
- # Get the array of sentences
611
- tlogger.logs "Splitting the sql sentences... "
612
- sentences = sql.split(/;/i)
613
- tlogger.logf "done (#{sentences.size})"
614
-
615
- # Chunk the array into parts of chunk_size elements
616
- # Reference: https://stackoverflow.com/questions/2699584/how-to-split-chunk-a-ruby-array-into-parts-of-x-elements
617
- tlogger.logs "Bunlding the array of sentences into chunks of #{chunk_size} each... "
618
- chunks = sentences.each_slice(chunk_size).to_a
619
- tlogger.logf "done (#{chunks.size})"
620
-
621
- chunk_number = -1
622
- chunks.each { |chunk|
623
- chunk_number += 1
624
- statement = chunk.join(";\n").to_s.strip
625
- tlogger.logs "lines #{chunk_size*chunk_number+1} to #{chunk_size*chunk_number+chunk.size} of #{sentences.size}... "
626
- begin
627
- @@db.execute(statement) #if statement.to_s.strip.size > 0
628
- tlogger.done
629
- rescue => e
630
- tlogger.logf e.to_s
631
- raise "Error executing statement: #{statement}\n#{e.message}"
632
- end
633
- }
634
- tlogger.done
635
- end # def db_execute_sql_sentences_file
636
-
637
- # Run a series of `.sql` files with updates to the database.
638
- #
639
- def self.deploy(save_checkpoints=false)
640
- tlogger = BlackStack::Deployer::logger
641
- # get list of `.sql` files in the directory `sql_path`, with a name higher than `last_filename`, sorted by name.
642
- Dir.entries(@@folder).select {
643
- |filename| filename =~ /\.sql$/ && filename > @@checkpoint.to_s
644
- }.uniq.sort.each { |filename|
645
- fullfilename = "#{@@folder}/#{filename}"
646
-
647
- tlogger.logs "#{fullfilename}... "
648
- BlackStack::Deployer::DB::execute_sentences( File.open(fullfilename).read )
649
- tlogger.done
650
-
651
- tlogger.logs "Updating checkpoint... "
652
- BlackStack::Deployer::DB::set_checkpoint filename
653
- tlogger.done
654
-
655
- tlogger.logs 'Saving checkpoint... '
656
- if save_checkpoints
657
- BlackStack::Deployer::DB::save_checkpoint
658
- tlogger.done
659
- else
660
- tlogger.logf 'disabled'
661
- end
662
- }
663
- end # def
664
- end # module DB
665
-
666
- # deploying all db-updates and run all routines on all nodes
667
- def self.deploy()
668
- tlogger = BlackStack::Deployer::logger
669
-
670
- @@nodes.each { |n|
671
- tlogger.logs "Node #{n.name}... "
672
- n.deploy()
673
- tlogger.done
674
- }
675
- end # def
676
-
677
- end # module Deployer
678
-
1
+ require 'blackstack-nodes'
2
+ require 'sequel'
3
+
4
+ module BlackStack
5
+ # Deployer is a library that can be used to deploy a cluster of nodes.
6
+ module Deployer
7
+ @@logger = BlackStack::BaseLogger.new(nil)
8
+ @@nodes = []
9
+ @@routines = []
10
+
11
+ # set the logger
12
+ def self.set_logger(i_logger)
13
+ @@logger = i_logger
14
+ end
15
+
16
+ # get the logger assigned to the module
17
+ def self.logger
18
+ @@logger
19
+ end # def self.errors
20
+
21
+ # get the array of nodes assigned to the module
22
+ def self.nodes
23
+ @@nodes
24
+ end # def self.nodes
25
+
26
+ # get the array of routines assigned to the module
27
+ def self.routines
28
+ @@routines
29
+ end # def self.routines
30
+
31
+ # inherit BlackStack::Infrastructure::NodeModule, including features of deployer.
32
+ module NodeModule
33
+ attr_accessor :deployment_routine, :parameters
34
+
35
+ include BlackStack::Infrastructure::NodeModule
36
+
37
+ # get the IP address for an interface using the ip addr command.
38
+ # this is a helper method for installing cockroachdb nodes.
39
+ def eth0_ip(interface='eth0')
40
+ a = self.ssh.exec!('ip addr show dev eth0').scan(/inet [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)
41
+ return nil if a.size == 0
42
+ return a.last.to_s.gsub(/inet /, '')
43
+ end
44
+
45
+ def self.descriptor_errors(h)
46
+ errors = BlackStack::Infrastructure::NodeModule.descriptor_errors(h)
47
+
48
+ # validate: does not exist any other element in @@nodes with the same value for the parameter h[:name]
49
+ errors << "The parameter h[:name] is not unique" if BlackStack::Deployer.nodes.select{|n| n.name == h[:name]}.length > 0
50
+
51
+ # validate: h[:deployment_routine] is not nil
52
+ errors << "The parameter h[:deployment_routine] is required" if h[:deployment_routine].nil?
53
+
54
+ # validate: h[:deployment_routine] is a string
55
+ errors << "The parameter h[:deployment_routine] is not a string" unless h[:deployment_routine].is_a?(String)
56
+
57
+ # return list of errors
58
+ errors.uniq
59
+ end
60
+
61
+ def initialize(h, i_logger=nil)
62
+ self.parameters = h
63
+ errors = BlackStack::Deployer::NodeModule.descriptor_errors(h)
64
+ raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
65
+ super(h, i_logger)
66
+ self.deployment_routine = h[:deployment_routine]
67
+ end # def self.create(h)
68
+
69
+ def to_hash
70
+ h = super
71
+ h[:deployment_routine] = self.deployment_routine
72
+ h
73
+ end # def to_hash
74
+
75
+ def deploy()
76
+ BlackStack::Deployer::run_routine(self.name, self.deployment_routine);
77
+ end
78
+ end # module NodeModule
79
+
80
+ # define attributes and methods of a deployer routine
81
+ module RoutineModule
82
+ attr_accessor :name, :commands
83
+
84
+ def self.descriptor_errors(h)
85
+ errors = []
86
+
87
+ # validate: the parameter h is a hash
88
+ errors << "The parameter h is not a hash" unless h.is_a?(Hash)
89
+
90
+ # validate: the paramerer h has a key :name
91
+ errors << "The parameter h does not have a key :name" unless h.has_key?(:name)
92
+
93
+ # validate: the paramerer h has a key :command
94
+ errors << "The parameter h does not have a key :commands" unless h.has_key?(:commands)
95
+
96
+ # validate: the parameter h[:name] is a string or a symbol
97
+ errors << "The parameter h[:name] is not a string" unless h[:name].is_a?(String)
98
+
99
+ # validate: the parameter h[:name] is not 'reboot' because it is a reserved name
100
+ errors << "The parameter h[:name] is a reserved name (#{h[:name].to_s})" if h[:name] == 'reboot'
101
+
102
+ # validate: the parameter h[:commands] is required
103
+ errors << "The parameter h[:commands] is required" if h[:commands].nil?
104
+
105
+ # validate: the parametrer h[:commands] is an array
106
+ errors << "The parameter h[:commands] is not an array" unless h[:commands].is_a?(Array)
107
+
108
+ # validate: the parameter h[:commands] has at least one element
109
+ errors << "The parameter h[:commands] does not have at least one element" unless h[:commands].size > 0
110
+
111
+ # validate: each element of the array h[:commands] is a hash
112
+ h[:commands].each do |c|
113
+ errors += BlackStack::Deployer::CommandModule.descriptor_errors(c)
114
+ end # h[:commands].each do |c|
115
+
116
+ errors.uniq
117
+ end # def self.descriptor_error(h)
118
+
119
+ def initialize(h)
120
+ errors = BlackStack::Deployer::RoutineModule.descriptor_errors(h)
121
+ raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
122
+ self.name = h[:name]
123
+ self.commands = []
124
+ h[:commands].each do |c|
125
+ self.commands << BlackStack::Deployer::Command.new(c)
126
+ end
127
+ end
128
+
129
+ def to_hash
130
+ h = {}
131
+ h[:name] = self.name
132
+ h[:commands] = []
133
+ self.commands.each do |c|
134
+ h[:commands] << c.to_hash
135
+ end
136
+ h
137
+ end
138
+
139
+ def run(node)
140
+ ret = []
141
+ self.commands.each do |c|
142
+ BlackStack::Deployer.logger.logs "Running command: #{c.command.to_s}... "
143
+ h = c.run(node)
144
+ ret << h
145
+
146
+ #BlackStack::Deployer.logger.logs "Result: "
147
+ #BlackStack::Deployer.logger.logf h.to_s
148
+
149
+ if h[:errors].size == 0
150
+ BlackStack::Deployer.logger.done
151
+ else
152
+ BlackStack::Deployer.logger.logf('error: ' + h.to_s)
153
+ raise "Error running command: #{h.to_s}"
154
+ end
155
+ end
156
+ ret
157
+ end # def run(node)
158
+ end # module RoutineModule
159
+
160
+ # define attributes and methods of a routine's command
161
+ module CommandModule
162
+ attr_accessor :command, :matches, :nomatches, :sudo
163
+
164
+ def self.descriptor_errors(c)
165
+ errors = []
166
+
167
+ # validate: h is a hash
168
+ errors << "The command descriptor is not a hash" unless c.is_a?(Hash)
169
+
170
+ # validate: the hash c has a key :command
171
+ errors << "The command descriptor does not have a key :command" unless c.has_key?(:command)
172
+
173
+ # validate: the value of c[:command] is a string or symbol
174
+ errors << "The value of c[:command] is not a string and is not a symbol" unless c[:command].is_a?(String) || c[:command].is_a?(Symbol)
175
+
176
+ # validate: if the key :sudo exists, then its value is a boolean
177
+ if c.has_key?(:sudo)
178
+ errors << "The value of c[:sudo] is not a boolean" unless c[:sudo].is_a?(TrueClass) || c[:sudo].is_a?(FalseClass)
179
+ end
180
+
181
+ # if the parameter h[:name] is a symbol
182
+ if c[:command].is_a?(Symbol)
183
+ if c[:command] == :reboot
184
+ # :reboot is a reserved word, so it is fine to call :reboot
185
+ else
186
+ # validate: existis a routine with a the value c[:command].to_s on its :name key
187
+ errors << "The routine with the name #{c[:command].to_s} does not exist" unless BlackStack::Deployer::routines.select { |r| r.name == c[:command].to_s }.size > 0
188
+ end
189
+ end
190
+
191
+ # if c[:matches] exists
192
+ if c.has_key?(:matches)
193
+ # validate: the value of c[:matches] must by a regex or an array
194
+ errors << "The value of the key :matches is not a regex nor an array" unless c[:matches].is_a?(Regexp) || c[:matches].is_a?(Array)
195
+ # if c[:matches] is a array
196
+ if c[:matches].is_a?(Array)
197
+ # validate: each element in the the array c[:matches] is a regex
198
+ c[:matches].each do |m|
199
+ errors += BlackStack::Deployer::MatchModule.descriptor_errors(m)
200
+ end # each
201
+ end # if c[:matches].is_a?(Array)
202
+ end # if :matches exists
203
+
204
+ # if c[:nomatches] exists
205
+ if c.has_key?(:nomatches)
206
+ # validate: the value of c[:nomatches] must by a regex or an array
207
+ errors << "The value of the key :nomatches is not a regex nor an array" unless c[:nomatches].is_a?(Regexp) || c[:nomatches].is_a?(Array)
208
+ # if c[:nomatches] is a array
209
+ if c[:nomatches].is_a?(Array)
210
+ # validate: each element in the the array c[:nomatches] is a hash
211
+ c[:nomatches].each do |m|
212
+ errors += BlackStack::Deployer::NoMatchModule.descriptor_errors(m)
213
+ end # each
214
+ end # if c[:matches].is_a?(Array)
215
+ end # if :matches exists
216
+ #
217
+ errors.uniq
218
+ end # def self.descriptor_error(h)
219
+
220
+ def initialize(h)
221
+ errors = BlackStack::Deployer::CommandModule.descriptor_errors(h)
222
+ raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
223
+ self.command = h[:command]
224
+ self.sudo = h[:sudo].nil? ? true : h[:sudo]
225
+ self.matches = []
226
+ self.nomatches = []
227
+ if h.has_key?(:matches)
228
+ if h[:matches].is_a?(Regexp)
229
+ self.matches << BlackStack::Deployer::Match.new(h[:matches])
230
+ else
231
+ h[:matches].each do |m|
232
+ self.matches << BlackStack::Deployer::Match.new(m)
233
+ end
234
+ end
235
+ end
236
+ if h.has_key?(:nomatches)
237
+ if h[:nomatches].is_a?(Regexp)
238
+ self.nomatches << BlackStack::Deployer::NoMatch.new(h[:nomatches])
239
+ else
240
+ h[:nomatches].each do |m|
241
+ self.nomatches << BlackStack::Deployer::NoMatch.new(m)
242
+ end
243
+ end
244
+ end
245
+ end # def initialize(h)
246
+
247
+ def to_hash
248
+ h = {}
249
+ h[:command] = self.command
250
+ h[:sudo] = self.sudo
251
+ h[:matches] = []
252
+ h[:nomatches] = []
253
+ self.matches.each do |m|
254
+ h[:matches] << m.to_hash
255
+ end
256
+ self.nomatches.each do |m|
257
+ h[:nomatches] << m.to_hash
258
+ end
259
+ h
260
+ end # def to_hash
261
+
262
+ def run(node)
263
+ errors = []
264
+ code = self.command
265
+ output = nil
266
+
267
+ # if code is a symbol
268
+ if code.is_a?(Symbol)
269
+
270
+ # if code is equel than :reboot
271
+ if code == :reboot
272
+ # call the node reboot method
273
+ node.reboot
274
+ else
275
+ # look for a routine with this name
276
+ r = BlackStack::Deployer.routines.select { |r| r.name == code.to_s }.first
277
+ if !r.nil?
278
+ r.run(node)
279
+ else
280
+ raise "The routine #{code.to_s} does not exist"
281
+ end
282
+ end
283
+
284
+ # if code is a string
285
+ elsif code.is_a?(String)
286
+ # replacing parameters
287
+ code.scan(/%[a-zA-Z0-9\_]+%/).each do |p|
288
+ if p == '%eth0_ip%' # reserved parameter
289
+ # TODO: move the method eth0_ip to the blackstack-nodes library
290
+ code.gsub!(p, node.eth0_ip)
291
+ elsif p == '%timestamp%' # reserved parameter
292
+ # TODO: move this to a timestamp function on blackstack-core
293
+ code.gsub!(p, Time.now.to_s.gsub(/\D/, ''))
294
+ else
295
+ if node.parameters.has_key?(p.gsub(/%/, '').to_sym)
296
+ code.gsub!(p, node.parameters[p.gsub(/%/, '').to_sym].to_s)
297
+ else
298
+ raise "The parameter #{p} does not exist in the node descriptor #{node.parameters.to_s}"
299
+ end
300
+ end
301
+ end
302
+
303
+ #puts
304
+ #puts
305
+ #puts code
306
+ #exit(0)
307
+ #puts
308
+ #puts "SUDO: #{self.sudo}"
309
+ # running the command
310
+ output = node.exec(code, self.sudo)
311
+ #puts
312
+ #puts '1'
313
+ # validation: at least one of the matches should happen
314
+ if self.matches.size > 0
315
+ i = 0
316
+ self.matches.each do |m|
317
+ if m.validate(output).size == 0 # no errors
318
+ i += 1
319
+ end
320
+ end
321
+ errors << "Command output doesn't match with any of the :matches" if i == 0
322
+ end # if self.matches.size > 0
323
+
324
+ # validation: no one of the nomatches should happen
325
+ self.nomatches.each do |m|
326
+ errors += m.validate(output)
327
+ end
328
+ end # elsif code.is_a?(String)
329
+
330
+ # return a hash descriptor of the command result
331
+ {
332
+ :command => self.command,
333
+ :code => code,
334
+ :output => output,
335
+ :errors => errors,
336
+ }
337
+ end # def run(node)
338
+ end # module CommandModule
339
+
340
+ # define attributes and methods of a command's match
341
+ module MatchModule
342
+ attr_accessor :match
343
+
344
+ def self.descriptor_errors(m)
345
+ errors = []
346
+ # validate the match is a regular expresion
347
+ errors << "The match is not a regex" unless m.is_a?(Regexp)
348
+ #
349
+ errors.uniq
350
+ end # def self.descriptor_error(h)
351
+
352
+ def initialize(h)
353
+ errors = BlackStack::Deployer::MatchModule.descriptor_errors(h)
354
+ raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
355
+ self.match = h
356
+ end
357
+
358
+ def to_hash
359
+ h = {}
360
+ h[:match] = self.match
361
+ h
362
+ end
363
+
364
+ def validate(output)
365
+ errors = []
366
+ errors << "The output of the command does not match the regular expression #{self.match.inspect}" unless output.match(self.match)
367
+ errors
368
+ end
369
+ end # module MatchModule
370
+
371
+ # define attributes and methods of a command's nomatch
372
+ module NoMatchModule
373
+ attr_accessor :nomatch, :error_description
374
+
375
+ def self.descriptor_errors(m)
376
+ errors = []
377
+ # validate the nomatch is a regular expresion
378
+ errors << "Each nomatch is not a regex nor a hash" unless m.is_a?(Hash) || m.is_a?(Regexp)
379
+ # if m is a hash
380
+ if m.is_a?(Hash)
381
+ # validate: the hash m has a key :nomatch
382
+ errors << "The hash descriptor of the nomatch does not have a key :nomatch" unless m.has_key?(:nomatch)
383
+ # validate: the value of m[:nomatch] is a string
384
+ errors << "The value of the key :nomatch is not a regex" unless m[:nomatch].is_a?(Regexp)
385
+ # validate: the hash m has a key :error_description
386
+ errors << "The hash descriptor of the nomatch does not have a key :error_description" unless m.has_key?(:error_description)
387
+ # validate: the value of m[:error_description] is a string
388
+ errors << "The value of the key :error_description is not a string" unless m[:error_description].is_a?(String)
389
+ end # if m.is_a?(Hash)
390
+ #
391
+ errors.uniq
392
+ end # def self.descriptor_error(h)
393
+
394
+ def initialize(h)
395
+ errors = BlackStack::Deployer::NoMatchModule.descriptor_errors(h)
396
+ raise "The node descriptor is not valid: #{errors.uniq.join(".\n")}" if errors.length > 0
397
+ self.nomatch = h[:nomatch]
398
+ self.error_description = h[:error_description]
399
+ end
400
+
401
+ def to_hash
402
+ h = {}
403
+ h[:nomatch] = self.nomatch
404
+ h[:error_description] = self.error_description
405
+ h
406
+ end
407
+
408
+ def validate(output)
409
+ errors = []
410
+ if !self.error_description.nil?
411
+ errors << self.error_description if output.match(self.nomatch)
412
+ else
413
+ errors << "The output of the command matches the regular expression #{self.nomatch.inspect}" if output.match(self.nomatch)
414
+ end
415
+ errors
416
+ end
417
+
418
+ end # module NoMatchModule
419
+
420
+
421
+ # TODO: declare these classes (stub and skeleton) using blackstack-rpc
422
+ #
423
+ # Stub Classes
424
+ # These classes represents a node, without using connection to the database.
425
+ # Use this class at the client side.
426
+ class Node
427
+ include BlackStack::Deployer::NodeModule
428
+ end # class Node
429
+
430
+ class Command
431
+ include BlackStack::Deployer::CommandModule
432
+ end # class Command
433
+
434
+ class Routine
435
+ include BlackStack::Deployer::RoutineModule
436
+ end # class Routine
437
+
438
+ class Match
439
+ include BlackStack::Deployer::MatchModule
440
+ end # class Match
441
+
442
+ class NoMatch
443
+ include BlackStack::Deployer::NoMatchModule
444
+ end # class NoMatch
445
+
446
+ # add a node to the list of nodes.
447
+ def self.add_node(h)
448
+ errors = BlackStack::Deployer::NodeModule.descriptor_errors(h)
449
+ raise errors.join(".\n") unless errors.empty?
450
+ @@nodes << BlackStack::Deployer::Node.new(h, @@logger)
451
+ end # def
452
+
453
+ # add an array of nodes to the list of nodes.
454
+ def self.add_nodes(a)
455
+ # validate: the parameter a is an array
456
+ raise "The parameter a is not an array" unless a.is_a?(Array)
457
+ a.each { |h| BlackStack::Deployer.add_node(h) }
458
+ end # def
459
+
460
+ # remove all exisiting nodes in he list of nodes.
461
+ # then, add the nodes in the parameter a to the list of nodes.
462
+ def self.set_nodes(a)
463
+ @@nodes.clear
464
+ BlackStack::Deployer.add_nodes(a)
465
+ end # def
466
+
467
+ # add a routine to the list of routines.
468
+ def self.add_routine(h)
469
+ errors = BlackStack::Deployer::RoutineModule.descriptor_errors(h)
470
+ raise errors.join(".\n") unless errors.empty?
471
+ @@routines << BlackStack::Deployer::Routine.new(h)
472
+ end # def
473
+
474
+ # add an array of routines to the list of routines.
475
+ def self.add_routines(a)
476
+ # validate: the parameter a is an array
477
+ raise "The parameter a is not an array" unless a.is_a?(Array)
478
+ a.each { |h| BlackStack::Deployer.add_routine(h) }
479
+ end # def
480
+
481
+ # remove all exisiting routines in he list of routines.
482
+ # then, add the routines in the parameter a to the list of routines.
483
+ def self.set_routines(a)
484
+ @@routines.clear
485
+ BlackStack::Deployer.add_routines(a)
486
+ end # def
487
+
488
+ # running a routine on a node
489
+ def self.run_routine(node_name, routine_name)
490
+ errors = []
491
+
492
+ # find the node with the value node_name in the key :name
493
+ n = @@nodes.select { |n| n.name == node_name }.first
494
+
495
+ # find the routine with the value routine_name in the key :name
496
+ r = @@routines.select { |r| r.name == routine_name }.first
497
+
498
+ # validate: the node n exists
499
+ errors << "Node #{node_name} not found" unless n
500
+
501
+ # validate: the routine r exists
502
+ errors << "Routine #{routine_name} not found" unless r
503
+
504
+ # raise exception if any error has been found
505
+ raise "The routine #{routine_name} cannot be run on the node #{node_name}: #{errors.uniq.join(".\n")}" if errors.length > 0
506
+
507
+ # connect the node
508
+ self.logger.logs "Connecting to node #{n.name}... "
509
+ n.connect
510
+ self.logger.done
511
+
512
+ # run the routine
513
+ self.logger.logs "Running routine #{r.name}... "
514
+ r.run(n)
515
+ self.logger.done
516
+
517
+ # disconnect the node
518
+ self.logger.logs "Disconnecting from node #{n.name}... "
519
+ n.disconnect
520
+ self.logger.done
521
+
522
+ end # def
523
+
524
+ module DB
525
+ LOCKFILE = './blackstack-deployer.lock'
526
+ @@checkpoint = nil
527
+ @@superhuser = nil
528
+ @@ndb = nil
529
+ @@folder = nil
530
+
531
+ def self.set_checkpoint(s)
532
+ @@checkpoint = s
533
+ end
534
+
535
+ def self.checkpoint
536
+ @@checkpoint
537
+ end
538
+
539
+ def self.save_checkpoint(lockfilename=BlackStack::Deployer::DB::LOCKFILE)
540
+ File.new(lockfilename, "w").write(@@checkpoint)
541
+ end
542
+
543
+ def self.load_checkpoint(lockfilename=BlackStack::Deployer::DB::LOCKFILE)
544
+ if File.exists?(lockfilename)
545
+ @@checkpoint = File.new(lockfilename, "r").read
546
+ else
547
+ @@checkpoint = nil
548
+ end
549
+ @@checkpoint
550
+ end
551
+
552
+ def self.connect(s)
553
+ @@db = Sequel::connect(s)
554
+ end # def
555
+
556
+ def self.set_folder(s)
557
+ @@folder = s
558
+ end # def
559
+
560
+
561
+ # Return `true` if the name of the file matches with `/\.transactions\./`, and it doesn't match with `/\.sentences\./`, and the matches with `/\.transactions\./` are no more than one.
562
+ # Otherwise, return `false`.
563
+ # This method should not be called directly by user code.
564
+ def self.is_transactions_file?(filename)
565
+ filename =~ /\.transactions\./ && filename !~ /\.sentences\./ && filename.scan(/\.transactions\./).size == 1
566
+ end
567
+
568
+ # Return `true` if the name of the file matches with `/\.sentences\./`, and it doesn't match with `/\.transactions\./`, and the matches with `/\.sentences\./` are no more than one.
569
+ # Otherwise({, return `false`.
570
+ # This method should not be called directly by user code.
571
+ def self.is_sentences_file?(filename)
572
+ filename =~ /\.sentences\./ && filename !~ /\.transactions\./ && filename.scan(/\.sentences\./).size == 1
573
+ end
574
+
575
+ # Method to process an `.sql` file with transactions code, separated by `BEGIN;` and `COMMIT;` statements.
576
+ # Reference: https://stackoverflow.com/questions/64066344/import-large-sql-files-with-ruby-sequel-gem
577
+ # This method is called by `BlackStack::Deployer::db_execute_file` if the filename matches with `/\.tsql\./`.
578
+ # This method should not be called directly by user code.
579
+ def self.execute_transactions(sql)
580
+ # TODO: Code Me!
581
+ end # def db_execute_tsql
582
+
583
+ # Method to process an `.sql` file with one sql sentence by line.
584
+ # Reference: https://stackoverflow.com/questions/64066344/import-large-sql-files-with-ruby-sequel-gem
585
+ # This method is called by `BlackStack::Deployer::db_execute_file` if the filename matches with `/\.sentences\./`.
586
+ # This method should not be called directly by user code.
587
+ def self.execute_sentences(sql, chunk_size=200)
588
+ tlogger = BlackStack::Deployer::logger
589
+
590
+ # Fix issue: Ruby `split': invalid byte sequence in UTF-8 (ArgumentError)
591
+ # Reference: https://stackoverflow.com/questions/11065962/ruby-split-invalid-byte-sequence-in-utf-8-argumenterror
592
+ #
593
+ # Fix issue: `PG::SyntaxError: ERROR: at or near "��truncate": syntax error`
594
+ #
595
+ sql.encode!('UTF-8', :invalid => :replace, :replace => '')
596
+
597
+ # Remove null bytes to avoid error: `String contains null byte`
598
+ # Reference: https://stackoverflow.com/questions/29320369/coping-with-string-contains-null-byte-sent-from-users
599
+ sql.gsub!("\u0000", "")
600
+
601
+ # Get the array of sentences
602
+ tlogger.logs "Splitting the sql sentences... "
603
+ sentences = sql.split(/;/i)
604
+ tlogger.logf "done (#{sentences.size})"
605
+
606
+ # Chunk the array into parts of chunk_size elements
607
+ # Reference: https://stackoverflow.com/questions/2699584/how-to-split-chunk-a-ruby-array-into-parts-of-x-elements
608
+ tlogger.logs "Bunlding the array of sentences into chunks of #{chunk_size} each... "
609
+ chunks = sentences.each_slice(chunk_size).to_a
610
+ tlogger.logf "done (#{chunks.size})"
611
+
612
+ chunk_number = -1
613
+ chunks.each { |chunk|
614
+ chunk_number += 1
615
+ statement = chunk.join(";\n").to_s.strip
616
+ tlogger.logs "lines #{chunk_size*chunk_number+1} to #{chunk_size*chunk_number+chunk.size} of #{sentences.size}... "
617
+ begin
618
+ @@db.execute(statement) #if statement.to_s.strip.size > 0
619
+ tlogger.done
620
+ rescue => e
621
+ tlogger.logf e.to_s
622
+ raise "Error executing statement: #{statement}\n#{e.message}"
623
+ end
624
+ }
625
+ tlogger.done
626
+ end # def db_execute_sql_sentences_file
627
+
628
+ # Run a series of `.sql` files with updates to the database.
629
+ #
630
+ def self.deploy(save_checkpoints=false, lockfilename=BlackStack::Deployer::DB::LOCKFILE)
631
+ tlogger = BlackStack::Deployer::logger
632
+ # get list of `.sql` files in the directory `sql_path`, with a name higher than `last_filename`, sorted by name.
633
+ Dir.entries(@@folder).select {
634
+ |filename| filename =~ /\.sql$/ && filename > @@checkpoint.to_s
635
+ }.uniq.sort.each { |filename|
636
+ fullfilename = "#{@@folder}/#{filename}"
637
+
638
+ tlogger.logs "#{fullfilename}... "
639
+ BlackStack::Deployer::DB::execute_sentences( File.open(fullfilename).read )
640
+ tlogger.done
641
+
642
+ tlogger.logs "Updating checkpoint... "
643
+ BlackStack::Deployer::DB::set_checkpoint filename
644
+ tlogger.done
645
+
646
+ tlogger.logs 'Saving checkpoint... '
647
+ if save_checkpoints
648
+ BlackStack::Deployer::DB::save_checkpoint(lockfilename)
649
+ tlogger.done
650
+ else
651
+ tlogger.logf 'disabled'
652
+ end
653
+ }
654
+ end # def
655
+ end # module DB
656
+
657
+ # deploying all db-updates and run all routines on all nodes
658
+ def self.deploy()
659
+ tlogger = BlackStack::Deployer::logger
660
+
661
+ @@nodes.each { |n|
662
+ tlogger.logs "Node #{n.name}... "
663
+ n.deploy()
664
+ tlogger.done
665
+ }
666
+ end # def
667
+
668
+ end # module Deployer
669
+
679
670
  end # module BlackStack