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 +4 -4
- data/lib/hybrid_platforms_conductor/deployer.rb +12 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +90 -59
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/check_deploy_and_idempotence.rb +11 -9
- data/lib/hybrid_platforms_conductor/provisioner.rb +9 -0
- data/lib/hybrid_platforms_conductor/version.rb +1 -1
- data/spec/hybrid_platforms_conductor_test.rb +1 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/connections_spec.rb +49 -3
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +24 -0
- data/spec/hybrid_platforms_conductor_test/api/deployer/provisioner_spec.rb +74 -10
- data/spec/hybrid_platforms_conductor_test/executables/options/common_spec.rb +2 -1
- data/spec/hybrid_platforms_conductor_test/helpers/cmd_runner_helpers.rb +33 -11
- data/spec/hybrid_platforms_conductor_test/helpers/connector_ssh_helpers.rb +42 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 539ccbcddce0dda10361f172eb18b5db378aa6e299117e4c25f3416a0e5eb3f8
|
4
|
+
data.tar.gz: 9e3f5c649cbc70399b462698d80794a56ce4e141b552d88df809373cb0e33b3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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 =
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
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
|
-
--
|
309
|
-
--
|
310
|
-
|
311
|
-
|
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
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
@@ -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
|
data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/connections_spec.rb
CHANGED
@@ -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
|
-
|
106
|
-
|
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
|
data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb
CHANGED
@@ -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 |
|
21
|
-
expect(
|
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 |
|
44
|
-
expect(
|
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 |
|
66
|
-
expect(
|
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 |
|
88
|
-
expect(
|
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 |
|
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 |
|
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
|
-
|
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<
|
12
|
-
# *
|
13
|
-
#
|
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
|
-
|
18
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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(
|
74
|
-
|
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<
|
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
|
-
|
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(
|
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.
|
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
|
+
date: 2021-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: range_operators
|