blackstack-deployer 1.2.10 → 1.2.14

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/blackstack-deployer.rb +678 -669
  3. metadata +9 -9
@@ -1,670 +1,679 @@
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
540
- File.new('./blackstack-deployer.lock', "w").write(@@checkpoint)
541
- end
542
-
543
- def self.load_checkpoint
544
- if File.exists?(BlackStack::Deployer::DB::LOCKFILE)
545
- @@checkpoint = File.new(BlackStack::Deployer::DB::LOCKFILE, "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)
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
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
-
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(lockfilename=BlackStack::Deployer::DB::LOCKFILE)
549
+ File.new(lockfilename, "w").write(@@checkpoint)
550
+ end
551
+
552
+ def self.load_checkpoint(lockfilename=BlackStack::Deployer::DB::LOCKFILE)
553
+ if File.exists?(lockfilename)
554
+ @@checkpoint = File.new(lockfilename, "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, lockfilename=BlackStack::Deployer::DB::LOCKFILE)
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(lockfilename)
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
+
670
679
  end # module BlackStack