hybrid_platforms_conductor 32.6.0 → 32.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6b6259b1f05bb082b4f2c6820f74b6c90dad15f5868a311337429b80d233500
4
- data.tar.gz: c5f58e3ba104ba2365addea64ab08c43a4fe9797430c950bf01258eb7bfcdc1b
3
+ metadata.gz: 539ccbcddce0dda10361f172eb18b5db378aa6e299117e4c25f3416a0e5eb3f8
4
+ data.tar.gz: 9e3f5c649cbc70399b462698d80794a56ce4e141b552d88df809373cb0e33b3c
5
5
  SHA512:
6
- metadata.gz: b079e89a17630ed994614bc24e76ba2154ae52fcaf3712feaaef9e823487d933d6b33b87b3aa253e8d11270d5765d061349c4a37f467e81d729d994ed1f7b72e
7
- data.tar.gz: 4a89e197bed2c87dbc10d66d8a3f6240c68deb7c2799962be282634c3c109b653c2a66725baa5ca44e38fd0bf1ec992148720047668c1c7786bd494a8e3836e7
6
+ metadata.gz: f665a9dd0e2aed5c3e78fee2840db66eaa67374e81609042c42687ec65a86621ad1d6f5d0c25578526b19b8f3f534dc1c4330b5956348b83928138d0a6e30509
7
+ data.tar.gz: 4d7cc60f030cfe9ab7c24053b4c1fa2ef440c95a8170685157de7c4f25d54fc8d2b469303baffb98214d58e39613ae116bf42df0991832e68b8046c9b7eccae1
@@ -311,13 +311,24 @@ module HybridPlatformsConductor
311
311
  environment: environment,
312
312
  logger: @logger,
313
313
  logger_stderr: @logger_stderr,
314
- config: @config,
314
+ config: sub_executable.config,
315
315
  cmd_runner: @cmd_runner,
316
316
  # Here we use the NodesHandler that will be bound to the sub-Deployer only, as the node's metadata might be modified by the Provisioner.
317
317
  nodes_handler: sub_executable.nodes_handler,
318
318
  actions_executor: @actions_executor
319
319
  )
320
320
  instance.with_running_instance(stop_on_exit: true, destroy_on_exit: !reuse_instance, port: 22) do
321
+ # Test-provisioned nodes have SSH Session Exec capabilities
322
+ sub_executable.nodes_handler.override_metadata_of node, :ssh_session_exec, 'true'
323
+ # Test-provisioned nodes use default sudo
324
+ sub_executable.config.sudo_procs.replace(sub_executable.config.sudo_procs.map do |sudo_proc_info|
325
+ {
326
+ nodes_selectors_stack: sudo_proc_info[:nodes_selectors_stack].map do |nodes_selector|
327
+ @nodes_handler.select_nodes(nodes_selector).select { |selected_node| selected_node != node }
328
+ end,
329
+ sudo_proc: sudo_proc_info[:sudo_proc]
330
+ }
331
+ end)
321
332
  actions_executor = sub_executable.actions_executor
322
333
  deployer = sub_executable.deployer
323
334
  # Setup test environment for this container
@@ -233,7 +233,13 @@ module HybridPlatformsConductor
233
233
  # Parameters::
234
234
  # * *bash_cmds* (String): Bash commands to execute
235
235
  def remote_bash(bash_cmds)
236
- ssh_cmd = "#{ssh_exec} #{ssh_url} /bin/bash <<'EOF'\n#{bash_cmds}\nEOF"
236
+ ssh_cmd =
237
+ if @nodes_handler.get_ssh_session_exec_of(@node) == 'false'
238
+ # When ExecSession is disabled we need to use stdin directly
239
+ "{ cat | #{ssh_exec} #{ssh_url} -T; } <<'EOF'\n#{bash_cmds}\nEOF"
240
+ else
241
+ "#{ssh_exec} #{ssh_url} /bin/bash <<'EOF'\n#{bash_cmds}\nEOF"
242
+ end
237
243
  # Due to a limitation of Process.spawn, each individual argument is limited to 128KB of size.
238
244
  # Therefore we need to make sure that if bash_cmds exceeds MAX_CMD_ARG_LENGTH bytes (considering EOF chars) then we use an intermediary shell script to store the commands.
239
245
  if bash_cmds.size > MAX_CMD_ARG_LENGTH
@@ -290,25 +296,30 @@ module HybridPlatformsConductor
290
296
  # * *owner* (String or nil): Owner to be used when copying the files, or nil for current one [default: nil]
291
297
  # * *group* (String or nil): Group to be used when copying the files, or nil for current one [default: nil]
292
298
  def remote_copy(from, to, sudo: false, owner: nil, group: nil)
293
- run_cmd <<~EOS
294
- cd #{File.dirname(from)} && \
295
- tar \
296
- --create \
297
- --gzip \
298
- --file - \
299
- #{owner.nil? ? '' : "--owner #{owner}"} \
300
- #{group.nil? ? '' : "--group #{group}"} \
301
- #{File.basename(from)} | \
302
- #{ssh_exec} \
303
- #{ssh_url} \
304
- \"#{sudo ? "#{@nodes_handler.sudo_on(@node)} " : ''}tar \
305
- --extract \
306
- --gunzip \
299
+ if @nodes_handler.get_ssh_session_exec_of(@node) == 'false'
300
+ # We don't have ExecSession, so don't use ssh, but scp instead.
301
+ run_cmd "scp -S #{ssh_exec} #{from} #{ssh_url}:#{to}"
302
+ else
303
+ run_cmd <<~EOS
304
+ cd #{File.dirname(from)} && \
305
+ tar \
306
+ --create \
307
+ --gzip \
307
308
  --file - \
308
- --directory #{to} \
309
- --owner root \
310
- \"
311
- EOS
309
+ #{owner.nil? ? '' : "--owner #{owner}"} \
310
+ #{group.nil? ? '' : "--group #{group}"} \
311
+ #{File.basename(from)} | \
312
+ #{ssh_exec} \
313
+ #{ssh_url} \
314
+ \"#{sudo ? "#{@nodes_handler.sudo_on(@node)} " : ''}tar \
315
+ --extract \
316
+ --gunzip \
317
+ --file - \
318
+ --directory #{to} \
319
+ --owner root \
320
+ \"
321
+ EOS
322
+ end
312
323
  end
313
324
 
314
325
  # Get the ssh executable to be used when connecting to the current node
@@ -490,31 +501,46 @@ module HybridPlatformsConductor
490
501
  ssh_url = "hpc.#{node}"
491
502
  if current_users.empty?
492
503
  log_debug "[ ControlMaster - #{ssh_url} ] - Creating SSH ControlMaster..."
493
- # Create the control master
494
- ssh_control_master_start_cmd = "#{ssh_exec}#{@passwords.key?(node) || @auth_password ? '' : ' -o BatchMode=yes'} -o ControlMaster=yes -o ControlPersist=yes #{ssh_url} true"
495
504
  exit_status = nil
496
- idx_try = 0
497
- loop do
498
- stderr = nil
499
- exit_status, _stdout, stderr = @cmd_runner.run_cmd ssh_control_master_start_cmd, log_to_stdout: log_debug?, no_exception: true, timeout: timeout
500
- if exit_status == 0
501
- break
502
- elsif stderr =~ /System is booting up/
503
- if idx_try == MAX_RETRIES_FOR_BOOT
504
- if no_exception
505
- break
506
- else
507
- raise ActionsExecutor::ConnectionError, "Tried #{idx_try} times to create SSH Control Master with #{ssh_control_master_start_cmd} but system says it's booting up."
505
+ if @nodes_handler.get_ssh_session_exec_of(node) == 'false'
506
+ # Here we have to create a ControlMaster using an interactive session, as the SSH server prohibits ExecSession, and so command executions.
507
+ # We'll do that using another terminal spawned in the background.
508
+ Thread.new do
509
+ log_debug "[ ControlMaster - #{ssh_url} ] - Spawn interactive ControlMaster in separate terminal"
510
+ @cmd_runner.run_cmd "xterm -e '#{ssh_exec} -o ControlMaster=yes -o ControlPersist=yes #{ssh_url}'", log_to_stdout: log_debug?
511
+ log_debug "[ ControlMaster - #{ssh_url} ] - Separate interactive ControlMaster closed"
512
+ end
513
+ out 'External ControlMaster has been spawned.'
514
+ out 'Please login into it, keep its session opened and press enter here when done...'
515
+ raise "Can't spawn interactive ControlMaster to #{node} in non-interactive mode. You may want to change the hpc_interactive env variable." if ENV['hpc_interactive'] == 'false'
516
+ $stdin.gets
517
+ exit_status = 0
518
+ else
519
+ # Create the control master
520
+ ssh_control_master_start_cmd = "#{ssh_exec}#{@passwords.key?(node) || @auth_password ? '' : ' -o BatchMode=yes'} -o ControlMaster=yes -o ControlPersist=yes #{ssh_url} true"
521
+ idx_try = 0
522
+ loop do
523
+ stderr = nil
524
+ exit_status, _stdout, stderr = @cmd_runner.run_cmd ssh_control_master_start_cmd, log_to_stdout: log_debug?, no_exception: true, timeout: timeout
525
+ if exit_status == 0
526
+ break
527
+ elsif stderr =~ /System is booting up/
528
+ if idx_try == MAX_RETRIES_FOR_BOOT
529
+ if no_exception
530
+ break
531
+ else
532
+ raise ActionsExecutor::ConnectionError, "Tried #{idx_try} times to create SSH Control Master with #{ssh_control_master_start_cmd} but system says it's booting up."
533
+ end
508
534
  end
535
+ # Wait a bit and try again
536
+ idx_try += 1
537
+ log_debug "[ ControlMaster - #{ssh_url} ] - System is booting up (try ##{idx_try}). Wait #{WAIT_TIME_FOR_BOOT} seconds before trying ControlMaster's creation again."
538
+ sleep WAIT_TIME_FOR_BOOT
539
+ elsif no_exception
540
+ break
541
+ else
542
+ raise ActionsExecutor::ConnectionError, "Error while starting SSH Control Master with #{ssh_control_master_start_cmd}: #{stderr.strip}"
509
543
  end
510
- # Wait a bit and try again
511
- idx_try += 1
512
- log_debug "[ ControlMaster - #{ssh_url} ] - System is booting up (try ##{idx_try}). Wait #{WAIT_TIME_FOR_BOOT} seconds before trying ControlMaster's creation again."
513
- sleep WAIT_TIME_FOR_BOOT
514
- elsif no_exception
515
- break
516
- else
517
- raise ActionsExecutor::ConnectionError, "Error while starting SSH Control Master with #{ssh_control_master_start_cmd}: #{stderr.strip}"
518
544
  end
519
545
  end
520
546
  if exit_status == 0
@@ -549,28 +575,33 @@ module HybridPlatformsConductor
549
575
  end
550
576
  end
551
577
  end
578
+ else
579
+ # We have not created any ControlMaster, but still consider the nodes to be ready to connect
580
+ user_locks = Hash[nodes.map { |node| [node, nil]} ]
552
581
  end
553
582
  yield user_locks.keys
554
583
  ensure
555
- user_locks_mutex.synchronize do
556
- user_locks.each do |node, user_id|
557
- with_lock_on_control_master_for(node, user_id: user_id) do |current_users, user_id|
558
- ssh_url = "hpc.#{node}"
559
- log_warn "[ ControlMaster - #{ssh_url} ] - Current process/thread was not part of the ControlMaster users anymore whereas it should have been" unless current_users.include?(user_id)
560
- remaining_users = current_users - [user_id]
561
- if remaining_users.empty?
562
- # Stop the ControlMaster
563
- log_debug "[ ControlMaster - #{ssh_url} ] - Stopping ControlMaster..."
564
- # Dumb verbose ssh! Tricky trick to just silence what is useless.
565
- # Don't fail if the connection close fails (but still log the error), as it can be seen as only a warning: it means the connection was closed anyway.
566
- @cmd_runner.run_cmd "#{ssh_exec_for(node)} -O exit #{ssh_url} 2>&1 | grep -v 'Exit request sent.'", log_to_stdout: log_debug?, expected_code: 1, timeout: timeout, no_exception: true
567
- log_debug "[ ControlMaster - #{ssh_url} ] - ControlMaster stopped"
568
- # Uncomment if you want to test that the connection has been closed
569
- # @cmd_runner.run_cmd "#{ssh_exec_for(node)} -O check #{ssh_url}", log_to_stdout: log_debug?, expected_code: 255, timeout: timeout
570
- else
571
- log_debug "[ ControlMaster - #{ssh_url} ] - Leaving ControlMaster started as #{remaining_users.size} processes/threads are still using it."
584
+ if @ssh_use_control_master
585
+ user_locks_mutex.synchronize do
586
+ user_locks.each do |node, user_id|
587
+ with_lock_on_control_master_for(node, user_id: user_id) do |current_users, user_id|
588
+ ssh_url = "hpc.#{node}"
589
+ log_warn "[ ControlMaster - #{ssh_url} ] - Current process/thread was not part of the ControlMaster users anymore whereas it should have been" unless current_users.include?(user_id)
590
+ remaining_users = current_users - [user_id]
591
+ if remaining_users.empty?
592
+ # Stop the ControlMaster
593
+ log_debug "[ ControlMaster - #{ssh_url} ] - Stopping ControlMaster..."
594
+ # Dumb verbose ssh! Tricky trick to just silence what is useless.
595
+ # Don't fail if the connection close fails (but still log the error), as it can be seen as only a warning: it means the connection was closed anyway.
596
+ @cmd_runner.run_cmd "#{ssh_exec_for(node)} -O exit #{ssh_url} 2>&1 | grep -v 'Exit request sent.'", log_to_stdout: log_debug?, expected_code: 1, timeout: timeout, no_exception: true
597
+ log_debug "[ ControlMaster - #{ssh_url} ] - ControlMaster stopped"
598
+ # Uncomment if you want to test that the connection has been closed
599
+ # @cmd_runner.run_cmd "#{ssh_exec_for(node)} -O check #{ssh_url}", log_to_stdout: log_debug?, expected_code: 255, timeout: timeout
600
+ else
601
+ log_debug "[ ControlMaster - #{ssh_url} ] - Leaving ControlMaster started as #{remaining_users.size} processes/threads are still using it."
602
+ end
603
+ false
572
604
  end
573
- false
574
605
  end
575
606
  end
576
607
  end
@@ -54,17 +54,19 @@ module HybridPlatformsConductor
54
54
  instance.stop
55
55
  instance.with_running_instance(port: 22) do
56
56
 
57
- # ===== Deploy removes root access
58
- # Check that we can't connect with root
59
- ssh_ok = false
60
- begin
61
- Net::SSH.start(instance.ip, 'root', password: 'root_pwd', auth_methods: ['password'], verify_host_key: :never) do |ssh|
62
- ssh_ok = ssh.exec!('echo Works').strip == 'Works'
57
+ unless @nodes_handler.get_root_access_allowed_of(@node) == 'true'
58
+ # ===== Deploy removes root access
59
+ # Check that we can't connect with root
60
+ ssh_ok = false
61
+ begin
62
+ Net::SSH.start(instance.ip, 'root', password: 'root_pwd', auth_methods: ['password'], verify_host_key: :never) do |ssh|
63
+ ssh_ok = ssh.exec!('echo Works').strip == 'Works'
64
+ end
65
+ rescue
63
66
  end
64
- rescue
67
+ assert_equal ssh_ok, false, 'Root can still connect on the image after deployment'
68
+ # Even if we can connect using root, run the idempotence test
65
69
  end
66
- assert_equal ssh_ok, false, 'Root can still connect on the image after deployment'
67
- # Even if we can connect using root, run the idempotence test
68
70
 
69
71
  # ===== Idempotence
70
72
  unless ssh_ok
@@ -76,6 +76,15 @@ module HybridPlatformsConductor
76
76
  # Make sure we update it.
77
77
  @nodes_handler.override_metadata_of @node, :host_ip, instance_ip
78
78
  @nodes_handler.invalidate_metadata_of @node, :host_keys
79
+ # Make sure the SSH transformations don't apply to this node
80
+ @config.ssh_connection_transforms.replace(@config.ssh_connection_transforms.map do |ssh_transform_info|
81
+ {
82
+ nodes_selectors_stack: ssh_transform_info[:nodes_selectors_stack].map do |nodes_selector|
83
+ @nodes_handler.select_nodes(nodes_selector).select { |selected_node| selected_node != @node }
84
+ end,
85
+ transform: ssh_transform_info[:transform]
86
+ }
87
+ end)
79
88
  end
80
89
  wait_for_port!(port) if port
81
90
  yield
@@ -1,5 +1,5 @@
1
1
  module HybridPlatformsConductor
2
2
 
3
- VERSION = '32.6.0'
3
+ VERSION = '32.8.0'
4
4
 
5
5
  end
@@ -94,6 +94,7 @@ module HybridPlatformsConductorTest
94
94
  ENV.delete 'hpc_password_for_thycotic'
95
95
  ENV.delete 'hpc_domain_for_thycotic'
96
96
  ENV.delete 'hpc_certificates'
97
+ ENV.delete 'hpc_interactive'
97
98
  # Set the necessary Hybrid Platforms Conductor environment variables
98
99
  ENV['hpc_ssh_user'] = 'test_user'
99
100
  HybridPlatformsConductor::ServicesHandler.packaged_deployments.clear
@@ -28,6 +28,45 @@ describe HybridPlatformsConductor::ActionsExecutor do
28
28
  end
29
29
  end
30
30
 
31
+ it 'creates an SSH master to 1 node not having Session Exec capabilities' do
32
+ with_test_platform(nodes: { 'node' => { meta: { host_ip: '192.168.42.42', ssh_session_exec: 'false' } } }) do
33
+ with_cmd_runner_mocked(
34
+ [
35
+ ['which env', proc { [0, "/usr/bin/env\n", ''] }],
36
+ ['ssh -V 2>&1', proc { [0, "OpenSSH_7.4p1 Debian-10+deb9u7, OpenSSL 1.0.2u 20 Dec 2019\n", ''] }]
37
+ ] + ssh_expected_commands_for({ 'node' => { connection: '192.168.42.42', user: 'test_user' } }, with_session_exec: false)
38
+ ) do
39
+ test_connector.ssh_user = 'test_user'
40
+ test_connector.with_connection_to(['node']) do |connected_nodes|
41
+ expect(connected_nodes).to eq ['node']
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ it 'can\'t create an SSH master to 1 node not having Session Exec capabilities when hpc_interactive is false' do
48
+ with_test_platform(nodes: { 'node' => { meta: { host_ip: '192.168.42.42', ssh_session_exec: 'false' } } }) do
49
+ ENV['hpc_interactive'] = 'false'
50
+ with_cmd_runner_mocked(
51
+ [
52
+ ['which env', proc { [0, "/usr/bin/env\n", ''] }],
53
+ ['ssh -V 2>&1', proc { [0, "OpenSSH_7.4p1 Debian-10+deb9u7, OpenSSL 1.0.2u 20 Dec 2019\n", ''] }]
54
+ ] + ssh_expected_commands_for(
55
+ { 'node' => { connection: '192.168.42.42', user: 'test_user' } },
56
+ with_control_master_create_optional: true,
57
+ with_control_master_destroy: false,
58
+ with_session_exec: false
59
+ )
60
+ ) do
61
+ test_connector.ssh_user = 'test_user'
62
+ expect do
63
+ test_connector.with_connection_to(['node']) do
64
+ end
65
+ end.to raise_error 'Can\'t spawn interactive ControlMaster to node in non-interactive mode. You may want to change the hpc_interactive env variable.'
66
+ end
67
+ end
68
+ end
69
+
31
70
  it 'creates SSH master to several nodes' do
32
71
  with_test_platform(nodes: {
33
72
  'node1' => { meta: { host_ip: '192.168.42.1' } },
@@ -102,8 +141,14 @@ describe HybridPlatformsConductor::ActionsExecutor do
102
141
  ['which env', proc { [0, "/usr/bin/env\n", ''] }],
103
142
  ['ssh -V 2>&1', proc { [0, "OpenSSH_7.4p1 Debian-10+deb9u7, OpenSSL 1.0.2u 20 Dec 2019\n", ''] }]
104
143
  ] + ssh_expected_commands_for(
105
- 'node1' => { connection: '192.168.42.1', user: 'test_user' },
106
- 'node3' => { connection: '192.168.42.3', user: 'test_user' }
144
+ {
145
+ 'node1' => { connection: '192.168.42.1', user: 'test_user' },
146
+ 'node3' => { connection: '192.168.42.3', user: 'test_user' }
147
+ },
148
+ # Here the threads for node1's and node3's ControlMasters might not trigger before the one for node2, so they will not destroy it.
149
+ # Sometimes they don't even have time to create the Control Masters that node2 has already failed.
150
+ with_control_master_create_optional: true,
151
+ with_control_master_destroy_optional: true
107
152
  ) + ssh_expected_commands_for(
108
153
  {
109
154
  'node2' => { connection: '192.168.42.2', user: 'test_user', control_master_create_error: 'Can\'t connect to 192.168.42.2' }
@@ -278,7 +323,8 @@ describe HybridPlatformsConductor::ActionsExecutor do
278
323
  ) do
279
324
  test_connector.ssh_use_control_master = false
280
325
  test_connector.ssh_user = 'test_user'
281
- test_connector.with_connection_to(['node']) do
326
+ test_connector.with_connection_to(['node']) do |connected_nodes|
327
+ expect(connected_nodes).to eq %w[node]
282
328
  end
283
329
  end
284
330
  end
@@ -141,6 +141,30 @@ describe HybridPlatformsConductor::ActionsExecutor do
141
141
  end
142
142
  end
143
143
 
144
+ it 'executes bash commands remotely without Session Exec capabilities' do
145
+ with_test_platform_for_remote_testing(
146
+ expected_cmds: [[/^\{ cat \| .+\/ssh hpc\.node -T; } <<'EOF'\nbash_cmd.bash\nEOF$/, proc { [0, 'Bash commands executed on node', ''] }]],
147
+ expected_stdout: 'Bash commands executed on node',
148
+ session_exec: false
149
+ ) do
150
+ test_connector.remote_bash('bash_cmd.bash')
151
+ end
152
+ end
153
+
154
+ it 'copies files remotely without Session Exec capabilities' do
155
+ with_test_platform_for_remote_testing(
156
+ expected_cmds: [
157
+ [
158
+ /^scp -S .+\/ssh \/path\/to\/src.file hpc\.node:\/remote_path\/to\/dst.dir$/,
159
+ proc { [0, '', ''] }
160
+ ]
161
+ ],
162
+ session_exec: false
163
+ ) do
164
+ test_connector.remote_copy('/path/to/src.file', '/remote_path/to/dst.dir')
165
+ end
166
+ end
167
+
144
168
  end
145
169
 
146
170
  end
@@ -17,8 +17,8 @@ describe HybridPlatformsConductor::Deployer do
17
17
  block.call
18
18
  end
19
19
  provisioner = nil
20
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |test_deployer, test_instance|
21
- expect(test_deployer.local_environment).to eq true
20
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
21
+ expect(sub_test_deployer.local_environment).to eq true
22
22
  provisioner = test_instance
23
23
  expect(test_instance.node).to eq 'node'
24
24
  expect(test_instance.environment).to match /^#{Regexp.escape(`whoami`.strip)}_hpc_testing_provisioner_\d+_\d+_\w+$/
@@ -40,8 +40,8 @@ describe HybridPlatformsConductor::Deployer do
40
40
  block.call
41
41
  end
42
42
  provisioner = nil
43
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |test_deployer, test_instance|
44
- expect(test_deployer.local_environment).to eq true
43
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
44
+ expect(sub_test_deployer.local_environment).to eq true
45
45
  provisioner = test_instance
46
46
  expect(test_instance.node).to eq 'node'
47
47
  expect(test_instance.environment).to match /^#{Regexp.escape(`whoami`.strip)}_hpc_testing_provisioner_\d+_\d+_\w+$/
@@ -50,6 +50,70 @@ describe HybridPlatformsConductor::Deployer do
50
50
  end
51
51
  end
52
52
 
53
+ it 'gives a new test instance ready to be used in place of the node without SSH transformations' do
54
+ with_test_platform(
55
+ {
56
+ nodes: {
57
+ 'node1' => { meta: { host_ip: '192.168.42.1', ssh_session_exec: 'false' } },
58
+ 'node2' => { meta: { host_ip: '192.168.42.2', ssh_session_exec: 'false' } }
59
+ }
60
+ },
61
+ false,
62
+ '
63
+ for_nodes(%w[node1 node2]) do
64
+ transform_ssh_connection do |node, connection, connection_user, gateway, gateway_user|
65
+ ["#{connection}_#{node}", "#{connection_user}_#{node}", "#{gateway}_#{node}", "#{gateway_user}_#{node}"]
66
+ end
67
+ end
68
+ '
69
+ ) do |repository|
70
+ register_plugins(:provisioner, { test_provisioner: HybridPlatformsConductorTest::TestProvisioner })
71
+ File.write("#{test_config.hybrid_platforms_dir}/dummy_secrets.json", '{}')
72
+ HybridPlatformsConductorTest::TestProvisioner.mocked_states = %i[created created running exited]
73
+ HybridPlatformsConductorTest::TestProvisioner.mocked_ip = '172.17.0.1'
74
+ expect(Socket).to receive(:tcp).with('172.17.0.1', 22, { connect_timeout: 1 }) do |&block|
75
+ block.call
76
+ end
77
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node1', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
78
+ expect(sub_test_deployer.instance_eval { @nodes_handler.get_ssh_session_exec_of('node1') }).to eq 'true'
79
+ expect(sub_test_deployer.instance_eval { @nodes_handler.get_ssh_session_exec_of('node2') }).to eq 'false'
80
+ ssh_transforms = test_instance.instance_eval { @config.ssh_connection_transforms }
81
+ expect(ssh_transforms.size).to eq 1
82
+ expect(ssh_transforms[0][:nodes_selectors_stack]).to eq [%w[node2]]
83
+ end
84
+ end
85
+ end
86
+
87
+ it 'gives a new test instance ready to be used in place of the node without sudo specificities' do
88
+ with_test_platform(
89
+ {
90
+ nodes: {
91
+ 'node1' => { meta: { host_ip: '192.168.42.1' } },
92
+ 'node2' => { meta: { host_ip: '192.168.42.2' } }
93
+ }
94
+ },
95
+ false,
96
+ '
97
+ for_nodes(%w[node1 node2]) do
98
+ sudo_for { |user| "other_sudo --user #{user}" }
99
+ end
100
+ '
101
+ ) do |repository|
102
+ register_plugins(:provisioner, { test_provisioner: HybridPlatformsConductorTest::TestProvisioner })
103
+ File.write("#{test_config.hybrid_platforms_dir}/dummy_secrets.json", '{}')
104
+ HybridPlatformsConductorTest::TestProvisioner.mocked_states = %i[created created running exited]
105
+ HybridPlatformsConductorTest::TestProvisioner.mocked_ip = '172.17.0.1'
106
+ expect(Socket).to receive(:tcp).with('172.17.0.1', 22, { connect_timeout: 1 }) do |&block|
107
+ block.call
108
+ end
109
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node1', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
110
+ sudo_procs = test_instance.instance_eval { @config.sudo_procs }
111
+ expect(sudo_procs.size).to eq 1
112
+ expect(sudo_procs[0][:nodes_selectors_stack]).to eq [%w[node2]]
113
+ end
114
+ end
115
+ end
116
+
53
117
  it 'does not destroy instances when asked to reuse' do
54
118
  with_test_platform(
55
119
  nodes: { 'node' => { meta: { host_ip: '192.168.42.42' } } }
@@ -62,8 +126,8 @@ describe HybridPlatformsConductor::Deployer do
62
126
  block.call
63
127
  end
64
128
  provisioner = nil
65
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner', reuse_instance: true) do |test_deployer, test_instance|
66
- expect(test_deployer.local_environment).to eq true
129
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner', reuse_instance: true) do |sub_test_deployer, test_instance|
130
+ expect(sub_test_deployer.local_environment).to eq true
67
131
  provisioner = test_instance
68
132
  expect(test_instance.node).to eq 'node'
69
133
  expect(test_instance.environment).to eq "#{`whoami`.strip}_hpc_testing_provisioner"
@@ -84,8 +148,8 @@ describe HybridPlatformsConductor::Deployer do
84
148
  block.call
85
149
  end
86
150
  provisioner = nil
87
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner', reuse_instance: true) do |test_deployer, test_instance|
88
- expect(test_deployer.local_environment).to eq true
151
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner', reuse_instance: true) do |sub_test_deployer, test_instance|
152
+ expect(sub_test_deployer.local_environment).to eq true
89
153
  provisioner = test_instance
90
154
  expect(test_instance.node).to eq 'node'
91
155
  expect(test_instance.environment).to eq "#{`whoami`.strip}_hpc_testing_provisioner"
@@ -102,7 +166,7 @@ describe HybridPlatformsConductor::Deployer do
102
166
  File.write("#{test_config.hybrid_platforms_dir}/dummy_secrets.json", '{}')
103
167
  HybridPlatformsConductorTest::TestProvisioner.mocked_states = %i[created created created exited exited]
104
168
  expect do
105
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |test_deployer, test_instance|
169
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
106
170
  end
107
171
  end.to raise_error /\[ node\/#{Regexp.escape(`whoami`.strip)}_hpc_testing_provisioner_\d+_\d+_\w+ \] - Instance fails to be in a state among \(running\) with timeout 1\. Currently in state exited/
108
172
  end
@@ -120,7 +184,7 @@ describe HybridPlatformsConductor::Deployer do
120
184
  raise Errno::ETIMEDOUT, 'Timeout while reading from port 22'
121
185
  end
122
186
  expect do
123
- test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |test_deployer, test_instance|
187
+ test_deployer.with_test_provisioned_instance(:test_provisioner, 'node', environment: 'hpc_testing_provisioner') do |sub_test_deployer, test_instance|
124
188
  end
125
189
  end.to raise_error /\[ node\/#{Regexp.escape(`whoami`.strip)}_hpc_testing_provisioner_\d+_\d+_\w+ \] - Instance fails to have port 22 opened with timeout 1\./
126
190
  end
@@ -50,7 +50,8 @@ describe 'executables\' common options' do
50
50
  with_test_platform_for_common_options do
51
51
  exit_code, stdout, stderr = run executable, *(['--debug'] + default_options)
52
52
  expect(exit_code).to eq 0
53
- expect(stderr).to eq ''
53
+ # Make sure to ignore the deployment markers from stderr.
54
+ expect(stderr.gsub("===== [ node1 / node1_service ] - HPC Service Check ===== Begin\n===== [ node1 / node1_service ] - HPC Service Check ===== End\n", '')).to eq ''
54
55
  end
55
56
  end
56
57
 
@@ -8,14 +8,27 @@ module HybridPlatformsConductorTest
8
8
  # Run expectations on the expected commands to be called.
9
9
  #
10
10
  # Parameters::
11
- # * *commands* (Array< [String or Regexp, Proc] >): Expected commands that should be called on CmdRunner: the command name or regexp and the corresponding mocked code
12
- # * Parameters::
13
- # * Same parameters as CmdRunner@run_cmd
11
+ # * *commands* (Array<Array>): List of expected commands that should be called on CmdRunner. Each specification is a list containing those items:
12
+ # * *0* (String or Regexp): The command name or regexp matching the command name
13
+ # * *1* (Proc): The mocking code to be called in place of the real command:
14
+ # * Parameters::
15
+ # * Same parameters as CmdRunner@run_cmd
16
+ # * Result::
17
+ # * Same results as CmdRunner@run_cmd
18
+ # * *2* (Hash): Optional hash of options. Can be ommited. [default = {}]
19
+ # * *optional* (Boolean): If true then don't fail if the command to be mocked has not been called [default: false]
14
20
  # * *cmd_runner* (CmdRunner): The CmdRunner to mock [default: test_cmd_runner]
15
21
  # * Proc: Code called with the command runner mocked
16
22
  def with_cmd_runner_mocked(commands, cmd_runner: test_cmd_runner)
17
- unexpected_commands = []
18
- remaining_expected_commands = commands.clone
23
+ remaining_expected_commands = commands.map do |(expected_command, command_code, options)|
24
+ [
25
+ expected_command,
26
+ command_code,
27
+ {
28
+ optional: false
29
+ }.merge(options || {})
30
+ ]
31
+ end
19
32
  # We need to protect the access to this array as the mocked commands can be called by competing threads
20
33
  remaining_expected_commands_mutex = Mutex.new
21
34
  allow(cmd_runner).to receive(:run_cmd) do |cmd, log_to_file: nil, log_to_stdout: true, log_stdout_to_io: nil, log_stderr_to_io: nil, expected_code: 0, timeout: nil, no_exception: false|
@@ -23,7 +36,7 @@ module HybridPlatformsConductorTest
23
36
  found_command = nil
24
37
  found_command_code = nil
25
38
  remaining_expected_commands_mutex.synchronize do
26
- remaining_expected_commands.delete_if do |(expected_command, command_code)|
39
+ remaining_expected_commands.delete_if do |(expected_command, command_code, _options)|
27
40
  break unless found_command.nil?
28
41
  if (expected_command.is_a?(String) && expected_command == cmd) || (expected_command.is_a?(Regexp) && cmd =~ expected_command)
29
42
  found_command = expected_command
@@ -64,14 +77,23 @@ module HybridPlatformsConductorTest
64
77
  log_stderr_to_io << mocked_stderr if !mocked_stderr.empty? && !log_stderr_to_io.nil?
65
78
  [mocked_exit_status, mocked_stdout, mocked_stderr]
66
79
  else
67
- logger.error "[ Mocked CmdRunner ] - !!! Unexpected command run: #{cmd}"
68
- unexpected_commands << cmd
69
- [:unexpected_command_to_mock, '', "Could not mock unexpected command #{cmd}"]
80
+ raise "Unexpected command run:\n#{cmd}\nRemaining expected commands:\n#{
81
+ remaining_expected_commands.map do |(expected_command, _command_code, _options)|
82
+ expected_command
83
+ end.join("\n")
84
+ }"
70
85
  end
71
86
  end
72
87
  yield
73
- expect(unexpected_commands).to eq []
74
- expect(remaining_expected_commands).to eq([]), "Expected CmdRunner commands were not run:\n#{remaining_expected_commands.map(&:first).join("\n")}"
88
+ expect(
89
+ remaining_expected_commands.select do |(_expected_command, _command_code, options)|
90
+ !options[:optional]
91
+ end
92
+ ).to eq([]), "Expected CmdRunner commands were not run:\n#{
93
+ remaining_expected_commands.map do |(expected_command, _command_code, options)|
94
+ "#{options[:optional] ? '[Optional] ' : ''}#{expected_command}"
95
+ end.join("\n")
96
+ }"
75
97
  # Un-mock the command runner
76
98
  allow(cmd_runner).to receive(:run_cmd).and_call_original
77
99
  end
@@ -15,19 +15,25 @@ module HybridPlatformsConductorTest
15
15
  # * *times* (Integer): Number of times this connection should be used [default: 1]
16
16
  # * *control_master_create_error* (String or nil): Error to simulate during the SSH ControlMaster creation, or nil for none [default: nil]
17
17
  # * *with_control_master_create* (Boolean): Do we create the control master? [default: true]
18
+ # * *with_control_master_create_optional* (Boolean): If true, then consider the ControlMaster creation to be optional [default: false]
18
19
  # * *with_control_master_check* (Boolean): Do we check the control master? [default: false]
19
20
  # * *with_control_master_destroy* (Boolean): Do we destroy the control master? [default: true]
21
+ # * *with_control_master_destroy_optional* (Boolean): If true, then consider the ControlMaster destruction to be optional [default: false]
20
22
  # * *with_strict_host_key_checking* (Boolean): Do we use strict host key checking? [default: true]
21
23
  # * *with_batch_mode* (Boolean): Do we use BatchMode when creating the control master? [default: true]
24
+ # * *with_session_exec* (Boolean): Do we use Sessien Exec capabilities when creating the control master? [default: true]
22
25
  # Result::
23
- # * Array< [String or Regexp, Proc] >: The expected commands that should be used, and their corresponding mocked code
26
+ # * Array<Array>: The expected commands that should be used, and their corresponding mocked code and options
24
27
  def ssh_expected_commands_for(
25
28
  nodes_connections,
26
29
  with_control_master_create: true,
30
+ with_control_master_create_optional: false,
27
31
  with_control_master_check: false,
28
32
  with_control_master_destroy: true,
33
+ with_control_master_destroy_optional: false,
29
34
  with_strict_host_key_checking: true,
30
- with_batch_mode: true
35
+ with_batch_mode: true,
36
+ with_session_exec: true
31
37
  )
32
38
  nodes_connections.map do |node, node_connection_info|
33
39
  node_connection_info[:times] = 1 unless node_connection_info.key?(:times)
@@ -43,8 +49,23 @@ module HybridPlatformsConductorTest
43
49
  ])
44
50
  end
45
51
  if with_control_master_create
52
+ control_master_created = false
46
53
  ssh_commands_per_connection << [
47
- /^.+\/ssh #{with_batch_mode ? '-o BatchMode=yes ' : ''}-o ControlMaster=yes -o ControlPersist=yes hpc\.#{Regexp.escape(node)} true$/,
54
+ if with_session_exec
55
+ /^.+\/ssh #{with_batch_mode ? '-o BatchMode=yes ' : ''}-o ControlMaster=yes -o ControlPersist=yes hpc\.#{Regexp.escape(node)} true$/
56
+ else
57
+ unless ENV['hpc_interactive'] == 'false'
58
+ # Mock the user hitting enter as the Control Master will be created in another thread and the main thread waits for user input.
59
+ expect($stdin).to receive(:gets) do
60
+ # We have to wait for the Control Master creation thread to actually create the Control Master before hitting Enter.
61
+ while !control_master_created do
62
+ sleep 0.1
63
+ end
64
+ "\n"
65
+ end
66
+ end
67
+ /^xterm -e '.+\/ssh -o ControlMaster=yes -o ControlPersist=yes hpc\.#{Regexp.escape(node)}'$/
68
+ end,
48
69
  proc do
49
70
  control_file = test_actions_executor.connector(:ssh).send(:control_master_file, node_connection_info[:connection], '22', node_connection_info[:user])
50
71
  # Fail if the ControlMaster file already exists, as would SSH do if the file is stalled
@@ -53,11 +74,16 @@ module HybridPlatformsConductorTest
53
74
  elsif node_connection_info[:control_master_create_error].nil?
54
75
  # Really touch a fake control file, as ssh connector checks for its existence
55
76
  File.write(control_file, '')
77
+ control_master_created = true
78
+ # If there is no Session Exec, this is done in a separate thread.
79
+ # So keep it alive until the user wants to stop it (which is done using an ssh -O exit command).
80
+ loop { sleep 0.1 } unless with_session_exec
56
81
  [0, '', '']
57
82
  else
58
83
  [255, '', node_connection_info[:control_master_create_error]]
59
84
  end
60
- end
85
+ end,
86
+ { optional: with_control_master_create_optional }
61
87
  ]
62
88
  end
63
89
  if with_control_master_check
@@ -73,7 +99,8 @@ module HybridPlatformsConductorTest
73
99
  # Really mock the control file deletion
74
100
  File.unlink(test_actions_executor.connector(:ssh).send(:control_master_file, node_connection_info[:connection], '22', node_connection_info[:user]))
75
101
  [1, '', '']
76
- end
102
+ end,
103
+ { optional: with_control_master_destroy_optional }
77
104
  ]
78
105
  end
79
106
  ssh_commands_once + ssh_commands_per_connection * node_connection_info[:times]
@@ -97,6 +124,7 @@ module HybridPlatformsConductorTest
97
124
  # * *timeout* (Integer or nil): Timeout to prepare the connector for [default: nil]
98
125
  # * *password* (String or nil): Password to set for the node, or nil for none [default: nil]
99
126
  # * *additional_config* (String): Additional config [default: '']
127
+ # * *session_exec* (Boolean): Do we mock a node having an SSH connection accepting Session Exec? [default: true]
100
128
  # * Proc: Client code to execute testing
101
129
  def with_test_platform_for_remote_testing(
102
130
  expected_cmds: [],
@@ -104,9 +132,14 @@ module HybridPlatformsConductorTest
104
132
  expected_stderr: '',
105
133
  timeout: nil,
106
134
  password: nil,
107
- additional_config: ''
135
+ additional_config: '',
136
+ session_exec: true
108
137
  )
109
- with_test_platform({ nodes: { 'node' => { meta: { host_ip: '192.168.42.42' } } } }, false, additional_config) do
138
+ with_test_platform(
139
+ { nodes: { 'node' => { meta: { host_ip: '192.168.42.42', ssh_session_exec: session_exec ? 'true' : 'false' } } } },
140
+ false,
141
+ additional_config
142
+ ) do
110
143
  with_cmd_runner_mocked(
111
144
  [
112
145
  ['which env', proc { [0, "/usr/bin/env\n", ''] }],
@@ -115,7 +148,8 @@ module HybridPlatformsConductorTest
115
148
  (password ? [['sshpass -V', proc { [0, "sshpass 1.06\n", ''] }]] : []) +
116
149
  ssh_expected_commands_for(
117
150
  { 'node' => { connection: '192.168.42.42', user: 'test_user' } },
118
- with_batch_mode: password.nil?
151
+ with_batch_mode: password.nil?,
152
+ with_session_exec: session_exec
119
153
  ) +
120
154
  expected_cmds
121
155
  ) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hybrid_platforms_conductor
3
3
  version: !ruby/object:Gem::Version
4
- version: 32.6.0
4
+ version: 32.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-11 00:00:00.000000000 Z
11
+ date: 2021-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: range_operators