razorrisk-cassini-utilities-cassid 0.8.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,643 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ # ##########################################################################
5
+ #
6
+ # Copyright (c) 2019 Razor Risk Technologies Pty Limited. All rights reserved.
7
+ #
8
+ # ##########################################################################
9
+
10
+ # ##########################################################
11
+ # requires
12
+
13
+ require 'razor_risk/cassini/cli'
14
+ require 'razor_risk/cassini/diagnostics/util_functions'
15
+ require 'razor_risk/cassini/util/program_execution_util'
16
+ require 'razor_risk/cassini/utilities/cassid/validations'
17
+ require 'razor_risk/core/diagnostics/logger'
18
+
19
+ require 'pantheios'
20
+ require 'nokogiri'
21
+
22
+ require 'base64'
23
+ require 'yaml'
24
+
25
+
26
+ # ##########################################################
27
+ # includes
28
+
29
+ include ::RazorRisk::Cassini::Utilities::CassiD::Validations
30
+ include ::RazorRisk::Cassini::Util::ProgramExecutionUtil
31
+
32
+
33
+ # ##########################################################
34
+ # modules
35
+
36
+ module RazorRisk
37
+ module Cassini
38
+ module Utilities
39
+ module CassiD
40
+ module Main
41
+
42
+
43
+ # ##########################################################
44
+ # includes
45
+
46
+ include ::Pantheios
47
+ include ::RazorRisk::Core::Diagnostics::Logger
48
+
49
+
50
+ # ##########################################################
51
+ # types
52
+
53
+ class CassiDException < ::StandardError; end
54
+
55
+
56
+ # ##########################################################
57
+ # constants
58
+
59
+ DEFAULT_LOG_DIRECTORY = File.join(Dir.pwd, 'logs')
60
+ DEFAULT_LOG_THRESHOLD = [ :informational, :informational ]
61
+
62
+ DEFAULT_CASSID_WAIT = 10
63
+ DEFAULT_SS_WAIT = 4
64
+
65
+ VALID_PORTS = (1024...60000)
66
+
67
+ ERROR_EXCEPTIONS = [ ::ArgumentError, ::NameError, ::NoMethodError, ::TypeError ]
68
+
69
+
70
+ # ##########################################################################
71
+ # functions
72
+
73
+ $children = []
74
+
75
+ def self.combine_uri a0, a1
76
+
77
+ trace ParamNames[ :a0, :a1 ], a0, a1
78
+
79
+ a0 = a0[0...-1] if '/' == a0[-1]
80
+ a1 = a1[1..-1] if '/' == a1[0]
81
+
82
+ "#{a0}/#{a1}"
83
+ end
84
+
85
+ def self.kill_children
86
+
87
+ log :informational, "Killing child processes..."
88
+
89
+ $children.each do |c|
90
+ begin
91
+ Process.kill('KILL', c.pid)
92
+ rescue Errno::ESRCH
93
+ rescue
94
+ log :informational, "Failed to kill child process #{c.pid}"
95
+ else
96
+ log :informational, "Killed child process #{c.pid}"
97
+ end
98
+ end
99
+ end
100
+
101
+ def self.process_cli *args
102
+
103
+ options = {
104
+ max_restarts: 0,
105
+ log_threshold: []
106
+ }
107
+
108
+ climate = LibCLImate::Climate.new do |cl|
109
+
110
+ cl.add_option(
111
+ '--microservice-multiplier',
112
+ alias: '-m',
113
+ help: "overrides the 'us_multiplier' configuration setting to the given number. Must be greater than 0"
114
+ ) do |o, a|
115
+
116
+ options[:us_multiplier] = Integer(o.value, nil: true)
117
+ unless options[:us_multiplier] and options[:us_multiplier] > 0
118
+ raise CassiDException.new("invalid value for microservice multiplier; use --help for usage")
119
+ end
120
+ end
121
+
122
+ cl.add_flag(
123
+ '--monitor-startup',
124
+ help: 'monitors child process statuses during initialisation'
125
+ ) do
126
+ options[:monitor_startup] = true
127
+ end
128
+
129
+ cl.add_flag(
130
+ '--pedantic',
131
+ help: 'conducts checks that the configuration is correct'
132
+ ) do
133
+ options[:pedantic] = true
134
+ end
135
+
136
+ cl.add_option(
137
+ '--max-restarts',
138
+ help: 'maximum number of times to restart a microservice'
139
+ ) do |o, a|
140
+ unless (val = Integer(o.value, nil: true)) < 0
141
+ options[:max_restarts] = val
142
+ else
143
+ raise CassiDException.new("max retries must be 0 or a positive integer; use --help for usage")
144
+ end
145
+ end
146
+
147
+ # Only added here as it may get passed through from cassid
148
+ cl.add_option('--exit-listener-port', alias: '-e',)
149
+
150
+ cl.add_option(
151
+ '--ruby-path',
152
+ alias: '-m',
153
+ help: 'absolute path to the ruby executable'
154
+ ) do |o, a|
155
+ options[:ruby_path] = o.value
156
+ end
157
+
158
+ cl.option_log_threshold(
159
+ options,
160
+ no_program_name: true,
161
+ no_benchmark: true,
162
+ limit: -2
163
+ )
164
+
165
+ cl.usage_values = '<config-yaml-file>'
166
+
167
+ cl.info_lines = [
168
+
169
+ 'Startup Daemon for Cassini',
170
+ ::RazorRisk::Cassini::CLI.Copyright(2018),
171
+ :version,
172
+ '',
173
+ 'Executes a Cassini instance based on a configuration file',
174
+ '',
175
+ ]
176
+ end
177
+
178
+ r = climate.run(args)
179
+
180
+ unless r.values[0]
181
+ raise CassiDException.new(
182
+ 'configuration file not specified; use --help for usage'
183
+ )
184
+ end
185
+
186
+ config_path = File.expand_path(r.values[0])
187
+
188
+ [ config_path, options ]
189
+ end
190
+
191
+ def self.init_logging program_name, log_directory, log_threshold
192
+
193
+ ::Pantheios::Core.program_name = program_name if program_name
194
+ log_directory = log_directory || DEFAULT_LOG_DIRECTORY
195
+ log_threshold = log_threshold || DEFAULT_LOG_THRESHOLD
196
+
197
+ setup_diagnostic_logging(
198
+ ::Pantheios::Core.program_name,
199
+ log_directory,
200
+ log_threshold,
201
+ no_benchmark_log: true
202
+ )
203
+ end
204
+
205
+ def self.load_config config_path
206
+
207
+ trace ParamNames[ :config_path ], config_path
208
+
209
+ config = nil
210
+
211
+ begin
212
+ raise CassiDException.new(
213
+ "configuration file '#{config_path}' does not exist or is not a file"
214
+ ) unless File.file?(config_path)
215
+
216
+ yaml = ::YAML.load_file config_path
217
+
218
+ raise CassiDException.new(
219
+ "file '#{config_path}' does not contain a valid configuration"
220
+ ) unless valid_configuration?(yaml)
221
+
222
+ config = Configuration.new yaml
223
+ rescue *ERROR_EXCEPTIONS => x
224
+ log :violation, "unexpected exception (#{x.class}): '#{x.message}': #{x.backtrace}"
225
+ raise
226
+ rescue CassiDException
227
+ raise
228
+ rescue => x
229
+ log :debug, "exception (#{x.class}): '#{x}'"
230
+ raise CassiDException.new(
231
+ "configuration file '#{config_path}' is not YAML or could not be loaded"
232
+ )
233
+ end
234
+
235
+ config
236
+ end
237
+
238
+ def self.pedantic_check clarite_config, config
239
+
240
+ trace ParamNames[ :clarite_config, :config ], clarite_config, config
241
+
242
+ log :informational, 'Checking configuration ...'
243
+
244
+ failed = false
245
+
246
+ msg = "Checking ClarITe configuration file '#{clarite_config}' is valid XML"
247
+ config_xml = nil
248
+ begin
249
+ config_xml = ::Nokogiri.XML(File.open(clarite_config)) do |c|
250
+ c.noblanks.strict
251
+ end
252
+ log :informational, "\t#{msg}: PASSED"
253
+ rescue
254
+ failed = true
255
+ log :critical, "\t#{msg}: FAILED"
256
+ end
257
+
258
+ msg = "Checking Razor alias or environment only"
259
+ unless config.razor.alias and config.razor.environment
260
+ log :informational, "\t#{msg}: PASSED"
261
+ else
262
+ failed = true
263
+ log :critical, "\t#{msg}: FAILED"
264
+ end
265
+
266
+ space = config.razor.space
267
+ environment = config.razor.environment
268
+ if (environment or space) and config_xml
269
+
270
+ msg = "Checking"
271
+ msg += " environment '#{environment}'" if environment
272
+ msg += " and" if environment and space
273
+ msg += " space '#{space}'" if space
274
+ msg += " is present within the ClarITe configuration file"
275
+
276
+ match = config_xml.xpath('//razor_server').any? do |razor_server|
277
+
278
+ server_space = razor_server.at_xpath('@space').text
279
+ server_env = razor_server.at_xpath('@environment').text
280
+
281
+ (space ? server_space == space : true) &&
282
+ (environment ? server_env == environment : true)
283
+ end
284
+
285
+ if match
286
+ log :informational, "\t#{msg}: PASSED"
287
+ else
288
+ failed = true
289
+ log :critical, "\t#{msg}: FAILED"
290
+ end
291
+ end
292
+
293
+ raise CassiDException.new(
294
+ 'failed one or more configuration checks'
295
+ ) if failed
296
+ end
297
+
298
+ def self.children_running?
299
+ $children.none? do |child|
300
+ !child.running?
301
+ end
302
+ end
303
+
304
+ def self.restart_children max_restarts
305
+ $children.each do |child|
306
+
307
+ unless child.running?
308
+ unless max_restarts < child.starts
309
+ child.start
310
+ log :notice, "Restarted #{child.name}..."
311
+ else
312
+ msg = "Microservice #{child.name} has failed too many times (#{child.starts})"
313
+ log :critical, msg
314
+ raise CassiDException.new msg
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ Microservice = Struct.new(:name, :command, :pid) do
321
+
322
+ def start
323
+ @starts ||= 0
324
+ @starts += 1
325
+ self.pid = Process.spawn(command.cmd)
326
+ end
327
+
328
+ def running?
329
+ begin
330
+ Process.waitpid(pid, Process::WNOHANG).nil?
331
+ rescue Errno::ECHILD
332
+ false
333
+ end
334
+ end
335
+
336
+ def starts
337
+ @starts
338
+ end
339
+ end
340
+
341
+ def self.init config_path, **options
342
+
343
+ trace ParamNames[ :config_path, :options ], config_path, options
344
+
345
+ begin
346
+
347
+ config = load_config config_path
348
+
349
+ # validate configuration elements:
350
+ #
351
+ # - root_dir is evaluated relative to the config file
352
+ #
353
+ # - clarite_config is evaluated relative to the root_dir, the current dir,
354
+ # the config file
355
+
356
+ root_dir = validate_cassini_root_dir(config.cassini.root_dir, config_path) or raise CassiDException.new "value of 'cassini/root_dir' - '#{config.cassini.root_dir}' - is invalid, or cannot be determined through inference"
357
+ auth_mode = validate_auth_mode(config.cassini.auth_mode) or raise CassiDException.new "value of 'cassini/auth_mode' - '#{config.cassini.auth_mode}' - is not valid"
358
+ auth_test_mode = config.cassini.auth_test_mode
359
+ ces_svc_path = validate_service_path(config.cassini.ces['service_path'], root_dir, '.', config_path) or raise CassiDException.new "value of 'cassini/ces/service_path' - '#{config.cassini.ces['service_path']}' - missing or invalid, or cannot be determined through inference"
360
+ first_port = config.cassini.first_port and VALID_PORTS.include?(first_port) or raise CassiDException.new "value of 'cassini/first_port' - '#{config.cassini.first_port}' - is not a valid port number"
361
+ host = config.cassini.host or raise CassiDException.new "must supply 'cassini/host'"
362
+ host_ces = host
363
+ host_usvc = '0.0.0.0' == host ? 'localhost' : host
364
+ us_multiplier = options[:us_multiplier]
365
+ us_multiplier ||= config.cassini.us_multiplier and (1..100).include?(config.cassini.us_multiplier) or raise CassiDException.new "value of 'cassini/us_multiplier' - '#{config.cassini.us_multiplier}' - is not a valid multiplier"
366
+
367
+ tls = validate_tls(config.cassini.tls, config_path) or raise CassiDException.new "value of 'cassini/tls' - '#{config.cassini.tls}' - is neither the path of an existing cert file that has an accompanying key file, nor is it an object containing keys 'cert' and 'key' that specify existing certificate and key files"
368
+ web_server = config.cassini.web_server
369
+ web_ui_server = config.cassini.web_ui_server
370
+
371
+ detach_children = config.control.detach
372
+ ss_wait = config.control.ss_wait || DEFAULT_SS_WAIT
373
+ cassid_wait = config.control.cassid_wait || DEFAULT_CASSID_WAIT
374
+
375
+ log_thr_console = (options[:log_threshold][0] || config.diagnostics.console['threshold']).to_sym
376
+ log_thr_main = (options[:log_threshold][1] || config.diagnostics.main['threshold']).to_sym
377
+ log_dir = options[:log_directory] || config.diagnostics.directory
378
+
379
+ clarite_config = validate_clarite_config(config.razor.clarite_config, root_dir, '.', config_path) or raise CassiDException.new "ClarITe configuration file '#{config.razor.clarite_config}' could not be found relative to root-directory, current directory or directory of configuration file"
380
+ executable = validate_executable(config.razor.executable, root_dir, config_path) or raise CassiDException.new "value of 'executable' - '#{config.razor.executable}' - could not be round relative to root-directory, current directory, or directory of configuration file"
381
+
382
+ init_logging(
383
+ options[:program_name],
384
+ log_dir,
385
+ [ log_thr_console, log_thr_main ],
386
+ )
387
+
388
+ if :jwt == auth_mode
389
+
390
+ ss = config.cassini.secret_server or raise CassiDException.new "'cassini/secret_server' is required when using JWT authentication"
391
+
392
+ # There are four possible elements:
393
+ #
394
+ # - 'host' (::String) [Optional] If not specified, the top-level
395
+ # 'host' is used
396
+ # - 'port' (::integer) The port on which to contact the
397
+ # secret-server. [Optional] if 'secrets_path' and 'service_path'
398
+ # are specified
399
+ # - 'secrets_path' (::String) [Optional] The secrets file path. If
400
+ # specified, then 'service_path' must also be specified, and
401
+ # together they indicate that the (Razor Risk) SecretServer
402
+ # utility is to be launched
403
+ # - 'service_path' (::String) [Optional] The secrets server utility
404
+ # path. If specified, then 'secrets_path' must also be specified,
405
+ # and together they indicate that the (Razor Risk) SecretServer
406
+ # utility is to be launched
407
+
408
+ ss_host = ss.host || host
409
+
410
+ if ss.port
411
+
412
+ ss_port = Integer(ss.port, nil: true) and VALID_PORTS.include?(ss.port) or raise CassiDException.new "value of 'cassini/secret_server/port' - '#{ss.port}' - is not a valid port number"
413
+ end
414
+
415
+ if ss.secrets_path
416
+
417
+ ss_secrets_path = validate_relative_path(ss.secrets_path, root_dir, '.', config_path) or raise CassiDException.new "secrets file path '#{ss.secrets_path}' could not be found relative to root-directory, current directory or directory of configuration file"
418
+ end
419
+
420
+ if ss.service_path
421
+
422
+ ss_service_path = validate_relative_path(ss.service_path, root_dir, '.', config_path) or raise CassiDException.new "service path '#{ss.service_path}' could not be found relative to root-directory, current directory or directory of configuration file"
423
+ end
424
+
425
+ raise CassiDException.new "A Login server is required when the authorisation mode is 'jwt'" unless config.cassini.microservices.dig('stock', 'login')
426
+
427
+ else
428
+
429
+ ss_host = nil
430
+ ss_port = nil
431
+ ss_secrets_path = nil
432
+ ss_service_path = nil
433
+ end
434
+
435
+ # ##########################################################################
436
+ # pedantic
437
+
438
+ pedantic_check(clarite_config, config) if options[:pedantic]
439
+
440
+ # ##########################################################################
441
+ # main
442
+
443
+ port = first_port
444
+
445
+ common_args = [ auth_mode, executable, root_dir, clarite_config, host_usvc ]
446
+ ces_args = [ auth_mode, nil, root_dir, nil, host_ces ]
447
+
448
+ ms_options = {
449
+ razor_environment: config.razor.environment,
450
+ razor_alias: config.razor.alias,
451
+ razor_space: config.razor.space,
452
+ }
453
+
454
+ ch_options = {
455
+ log_threshold: [ log_thr_console, log_thr_main ],
456
+ log_directory: log_dir,
457
+ web_server: web_server,
458
+ auth_test_mode: auth_test_mode,
459
+ ruby_path: options[:ruby_path],
460
+ }
461
+
462
+ ces_routes = {}
463
+
464
+ if :jwt == auth_mode
465
+
466
+ ss_port ||= port += 1
467
+
468
+ ssc = make_secretserver_command(root_dir, ss_secrets_path, ss_host, ss_port, **ch_options, **ms_options, program_name: 'ss', path: ss_service_path)
469
+
470
+ $children << Microservice.new('secret-server', ssc, nil)
471
+
472
+ ch_options[:secret_server] = "http://#{ss_host}:#{ss_port}/"
473
+
474
+ ch_options[:credentials_encoding_algorithm] = 'AES-256-CBC'
475
+ ch_options[:jwt_encoding_algorithm] = 'HS256'
476
+ end
477
+
478
+ # N x M microservices
479
+
480
+ config.cassini.microservices.each do |category, svcs|
481
+
482
+ svcs.each do |service, characteristics|
483
+
484
+ external_route = characteristics['external_route']
485
+ service_path = validate_service_path(characteristics['service_path'], root_dir, '.', config_path)
486
+ routes = characteristics['routes']
487
+
488
+ raise CassiDException.new "invalid configuration (#{characteristics})" if external_route.nil? || service_path.nil? || routes.nil?
489
+
490
+ raise CassiDException.new "invalid configuration: the external route '#{external_route}' has already been specified" if ces_routes.has_key?(external_route)
491
+
492
+ ex_route_spec = ces_routes[external_route] = {}
493
+
494
+ (0...us_multiplier).each do |n|
495
+
496
+ port += 1
497
+ usc = make_microservice_command service, *common_args, port, '', **ch_options, **ms_options, program_name: "#{service}-#{n}", path: service_path
498
+
499
+ routes.each do |route|
500
+
501
+ internal = route['internal']
502
+ security = route['security'] || false
503
+
504
+ uri = combine_uri usc.uri.to_s, internal
505
+
506
+ verbs = route['verbs'] || []
507
+ verb = route['verb']
508
+ verbs << verb if verb
509
+
510
+ ex_route_spec[verbs] = [] unless ex_route_spec.has_key?(verbs)
511
+
512
+ ex_route_spec[verbs] << {
513
+
514
+ route: uri,
515
+ security: security,
516
+ }
517
+ end
518
+
519
+ $children << Microservice.new("#{service}-#{n}", usc, nil)
520
+ end
521
+ end
522
+ end
523
+
524
+ config.cassini.external_services.each do |service, characteristics|
525
+
526
+ external_route = characteristics['external_route']
527
+ uri = characteristics['uri']
528
+ routes = characteristics['routes']
529
+
530
+ raise CassiDException.new(
531
+ "invalid configuration for #{service} (#{characteristics})"
532
+ ) if external_route.nil? || uri.nil?
533
+ raise CassiDException.new(
534
+ "invalid configuration for #{service}: the external route '#{external_route}' has already been specified"
535
+ ) if ces_routes.has_key?(external_route)
536
+
537
+ ex_route_spec = ces_routes[external_route] = {}
538
+ routes.each do |route|
539
+
540
+ internal = route['internal']
541
+ security = route['security'] || false
542
+
543
+ route_uri = combine_uri uri, internal
544
+
545
+ verbs = route['verbs'] || []
546
+ verb = route['verb']
547
+ verbs << verb if verb
548
+
549
+ ex_route_spec[verbs] = [] unless ex_route_spec.has_key?(verbs)
550
+
551
+ ex_route_spec[verbs] << {
552
+ route: route_uri,
553
+ security: security,
554
+ }
555
+ end
556
+ end if config.cassini.external_services
557
+
558
+ # 1 x CES
559
+
560
+ yaml = ::YAML.dump ces_routes
561
+ routes_b64 = Base64.encode64(yaml).split.join
562
+
563
+ ces_addnl = [
564
+
565
+ '--accept-string-routes',
566
+ ]
567
+
568
+ unless tls.empty?
569
+
570
+ ces_addnl << [ '--tls-certificate-file', tls[0] ]
571
+ ces_addnl << [ '--tls-public-key-file', tls[1] ]
572
+ end
573
+
574
+ ces_opts = {
575
+
576
+ routes_config: "base64:#{routes_b64}",
577
+ additional_arguments: ces_addnl,
578
+ uri_scheme: tls.empty? ? nil : 'https',
579
+ }
580
+
581
+ us_CES = make_microservice_command 'ces', *ces_args, first_port, nil, **ch_options, **ces_opts, path: ces_svc_path, web_ui_server: web_ui_server
582
+
583
+ $children << Microservice.new('ClientEdgeService', us_CES, nil)
584
+
585
+ # start the child processes
586
+
587
+ log :notice, 'starting child processes ...'
588
+
589
+ $children.each_with_index do |child, index0|
590
+
591
+ child.start
592
+
593
+ if 0 == index0 && :jwt == auth_mode
594
+
595
+ # wait for the secret server to start up
596
+ sleep(ss_wait)
597
+ end
598
+ end
599
+
600
+ log :notice, 'waiting for child processes to be ready ...'
601
+
602
+ sleep(cassid_wait)
603
+
604
+ if options[:monitor_startup]
605
+ unless children_running?
606
+ msg = 'One or more child process(es) has failed during startup'
607
+ log :critical, msg
608
+ raise CassiDException.new msg
609
+ end
610
+ end
611
+
612
+ # Output status
613
+
614
+ log :notice, "Cassini is now up and running, and able to receive calls at '#{us_CES.uri}'"
615
+
616
+ if detach_children
617
+ log :warning, "Detach mode has been depreciated in favour of service installation"
618
+ end
619
+
620
+ rescue *ERROR_EXCEPTIONS => x
621
+ log :violation, "unexpected exception (#{x.class}): '#{x.message}': #{x.backtrace}"
622
+ raise
623
+ rescue CassiDException
624
+ raise
625
+ rescue => x
626
+ log :alert, "exception (#{x.class}): '#{x.message}': #{x.backtrace}"
627
+ raise CassiDException.new 'Unexpected failure'
628
+ end
629
+ end
630
+
631
+
632
+ # ##########################################################
633
+ # modules
634
+
635
+ end # module Main
636
+ end # module CassiD
637
+ end # module Utilities
638
+ end # module Cassini
639
+ end # module RazorRisk
640
+
641
+ # ############################## end of file ############################# #
642
+
643
+