@@ -206,6 +206,40 @@ load_config() {
206206 log_info " Skip Cleanup: ${E2E_SKIP_CLEANUP} "
207207}
208208
209+ # Configure ssh/scp to use the private key matching the VM public key.
210+ configure_ssh_identity () {
211+ if [[ " ${E2E_SSH_OPTS} " =~ (^| [[:space:]])-i || " ${E2E_SSH_OPTS} " == * " IdentityFile" * ]]; then
212+ return 0
213+ fi
214+
215+ local key_file=" ${E2E_SSH_PRIVATE_KEY_FILE:- } "
216+
217+ # E2E_SSH_KEY_FILE is the public key passed to Azure VM provisioning.
218+ if [[ -z " ${key_file} " && -n " ${E2E_SSH_KEY_FILE:- } " && " ${E2E_SSH_KEY_FILE} " == * .pub ]]; then
219+ key_file=" ${E2E_SSH_KEY_FILE% .pub} "
220+ fi
221+
222+ if [[ -z " ${key_file} " && -f " ${E2E_WORK_DIR} /e2e_ssh_key" ]]; then
223+ key_file=" ${E2E_WORK_DIR} /e2e_ssh_key"
224+ fi
225+
226+ if [[ -z " ${key_file} " ]]; then
227+ local candidate
228+ # DSA keys are intentionally omitted because modern OpenSSH disables them.
229+ for candidate in " ${HOME} /.ssh/id_ed25519" " ${HOME} /.ssh/id_rsa" " ${HOME} /.ssh/id_ecdsa" ; do
230+ if [[ -f " ${candidate} " && -f " ${candidate} .pub" ]]; then
231+ key_file=" ${candidate} "
232+ break
233+ fi
234+ done
235+ fi
236+
237+ if [[ -n " ${key_file} " && -f " ${key_file} " ]]; then
238+ export E2E_SSH_IDENTITY_FILE=" ${key_file} "
239+ log_debug " Using SSH identity file: ${key_file} "
240+ fi
241+ }
242+
209243# ---------------------------------------------------------------------------
210244# Work directory & state management
211245# ---------------------------------------------------------------------------
@@ -253,6 +287,22 @@ state_dump() {
253287# ---------------------------------------------------------------------------
254288# SSH helpers
255289# ---------------------------------------------------------------------------
290+ _build_ssh_opts () {
291+ local -n out_opts=" $1 "
292+ # E2E_SSH_OPTS is split on shell IFS whitespace; an unset or empty value
293+ # produces no extra options. Identity-file paths are carried separately in
294+ # E2E_SSH_IDENTITY_FILE so spaces are preserved there.
295+ if [[ " ${E2E_SSH_OPTS:- } " == * " '" * || " ${E2E_SSH_OPTS:- } " == * ' "' * || " ${E2E_SSH_OPTS:- } " == * " \\ " * ]]; then
296+ log_error " E2E_SSH_OPTS must be an unquoted whitespace-delimited option list"
297+ return 1
298+ fi
299+ read -r -a out_opts <<< " ${E2E_SSH_OPTS:-}"
300+
301+ if [[ -n " ${E2E_SSH_IDENTITY_FILE:- } " ]]; then
302+ out_opts=(-i " ${E2E_SSH_IDENTITY_FILE} " -o IdentitiesOnly=yes " ${out_opts[@]} " )
303+ fi
304+ }
305+
256306# Wait for SSH to become available on a host
257307wait_for_ssh () {
258308 local host=" $1 "
@@ -261,8 +311,9 @@ wait_for_ssh() {
261311
262312 log_info " Waiting for SSH on ${host} (timeout: ${timeout} s)..."
263313 while [[ " ${elapsed} " -lt " ${timeout} " ]]; do
264- # shellcheck disable=SC2086
265- if ssh ${E2E_SSH_OPTS} " ${E2E_SSH_USER} @${host} " " echo ready" & > /dev/null; then
314+ local -a ssh_opts
315+ _build_ssh_opts ssh_opts
316+ if ssh " ${ssh_opts[@]} " " ${E2E_SSH_USER} @${host} " " echo ready" & > /dev/null; then
266317 log_success " SSH ready on ${host} (${elapsed} s)"
267318 return 0
268319 fi
@@ -277,15 +328,17 @@ wait_for_ssh() {
277328# Execute a command on a remote host via SSH
278329remote_exec () {
279330 local host=" $1 " ; shift
280- # shellcheck disable=SC2086
281- ssh ${E2E_SSH_OPTS} " ${E2E_SSH_USER} @${host} " " $@ "
331+ local -a ssh_opts
332+ _build_ssh_opts ssh_opts
333+ ssh " ${ssh_opts[@]} " " ${E2E_SSH_USER} @${host} " " $@ "
282334}
283335
284336# Copy files to a remote host
285337remote_copy () {
286338 local src=" $1 " host=" $2 " dest=" $3 "
287- # shellcheck disable=SC2086
288- scp ${E2E_SSH_OPTS} " ${src} " " ${E2E_SSH_USER} @${host} :${dest} "
339+ local -a ssh_opts
340+ _build_ssh_opts ssh_opts
341+ scp " ${ssh_opts[@]} " " ${src} " " ${E2E_SSH_USER} @${host} :${dest} "
289342}
290343
291344# ---------------------------------------------------------------------------
0 commit comments