blackstack-deployer 1.2.12 → 1.2.14

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 +678 -669
  3. metadata +5 -5
@@ -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(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
-
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