hybrid_platforms_conductor 32.6.0 → 32.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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